目录
项目简介:
包含系统
项目架构
前端开发流程:
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网上预约挂号 - 北京市预约挂号统一平台
医院接口系统:
约定 > 配置 > 编码,项目父工程中规定所有共用依赖的版本。
将全局要使用的实体类和工具放到此模块中,避免代码冗余
swagger通过注解表明该接口会生成文档,包括接口名、请求方法、参数、返回信息的等等。
使用swagger要完成以下三部
@Api:修饰整个类,描述Controller的作用
@ApiOperation:描述一个类的一个方法,或者说一个接口
@ApiParam:单个参数描述
@ApiModel:用对象来接收参数
@ApiModelProperty:用对象接收参数时,描述对象的一个字段
@ApiImplicitParam:一个请求参数
@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.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;
}
}
@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() +
'}';
}
}
@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();
}
}
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类封装了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;
}
}
@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加密
*/
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);
}
}
}
@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;
}
}
定义了所有共用的枚举类和实体类(表数据)封装了所有表连接查询类(将不同表中的部分数据封装在一起作为查询字段)。
所有关于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<>();
}
与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接口服务
数据字典中包全国省市区、医院等级、证件类型、民族、学历。
表中id与parent_id相对应,dict_code和id建立联系,value表示数据对应的值或者说用该值代表数据,所有数据存在一张表中,避免连表查询(笛卡尔积)
导入导出数据字典理应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();
}
}
在读取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 ,具体看前端传数据。
//根据数据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;
}
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
#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")//第一次查询后将数据放入缓存中
结构:api包下接口提供给前台用户系统使用,其余接口提供给后台管理系统使用。
医院的基础信息设置是在mysql中(由后台管理系统crud)
医院的详细信息在Mongodb中(由医院接口系统crud)
项目中所有的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使用分两种 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规范就可以。
在使用时先将 定义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;
}
做成树性列表,大科室包含很多小科室。配合上面查出的排版信息可做成下列画面
这个功能最难的代码是:查询出的所有部门信息是一个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);
}
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;
}
}
pom
org.springframework.boot
spring-boot-starter-actuator
org.springframework.cloud
spring-cloud-starter-bus-amqp
com.alibaba
fastjson
将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实现图表折线类统计图
采用服务调用(需要配置网关),Statistics 调用 order ,具体方法实现在order,
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中的修改较复杂(太多了),最简单的方法就是修改本地时间,可以查出之前的排班数据,但是微信退款也会获取本地时间导致证书过期,我妥协了只好当要退款时又将时间修改回来即可
。