预约挂号系统技术点详解(二)

一、微服务间服务的调用介绍

1. 需求(医院接口远程调用数据字典)

预约挂号系统技术点详解(二)_第1张图片
service-hosp服务调用service-cmn服务

2. 实现步骤

⑴ 搭建service-client父模块

修改pom文件,添加需要使用的model模块和工具模块依赖,并添加openfeign依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.kejizhentan</groupId>
    <artifactId>service-client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <modules>
        <module>service-client-cmn</module>
    </modules>
    <name>service-client</name>
    <packaging>pom</packaging>
    <description>Demo project for Spring Boot</description>

    <dependencies>
        <dependency>
            <groupId>com.kejizhentan</groupId>
            <artifactId>model</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.kejizhentan</groupId>
            <artifactId>common-util</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.2.12.RELEASE</version>
            <scope>provided </scope>
        </dependency>

        <!-- 服务调用feign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>2.2.0.RELEASE</version>
            <scope>provided </scope>
        </dependency>
    </dependencies>

</pro

⑵ 搭建service-cmn-client模块

修改pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>service-client</artifactId>
        <groupId>com.kejizhentan</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>service-client-cmn</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>5.3.16</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>


</project>

添加Feign接口类 DictFeignClient.java

@FeignClient("service-cmn")
@Repository
public interface DictFeignClient {
    // 根据 dictcode  value 查询
    @GetMapping("/admin/cmn/dict/getName/{dictCode}/{value}")
    String getName(@PathVariable("dictCode") String dictCode, @PathVariable("value") String value);

    // 根据 value 查询
    @GetMapping("/admin/cmn/dict/getName/{value}")String getName(@PathVariable("value") String value);
}

预约挂号系统技术点详解(二)_第2张图片

⑶ 结构如下

预约挂号系统技术点详解(二)_第3张图片

⑷ service模块引入feign依赖

<!-- 服务调用feign -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

⑸ 在服务调用方service-hosp添加feign接口模块的依赖

  <dependency>
    <groupId>com.kejizhentan</groupId>
    <artifactId>service-client-cmn</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <scope>compile</scope>
</dependency>

⑹ 启动类开启服务调用

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.kejizhentan")
@ComponentScan(basePackages = "com.kejizhentan")
public class ServiceHospApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceHospApplication.class, args);
    }
}

预约挂号系统技术点详解(二)_第4张图片

⑺ 在hosptal模块的service中注入feign接口,调用相应的方法,实现服务间的调用

@Service
public class HospitalServiceImp implements HospitalService {
    @Autowired
    private HospitalRepository hospitalRepository;
    @Autowired
    private DictFeignClient dictFeignClient;

    @Override
    public Page<Hospital> selectHospPage(Integer page, Integer limit, HospitalQueryVo hospitalQueryVo) {
        Pageable pageable = PageRequest.of(page - 1, limit);
        ExampleMatcher matcher = ExampleMatcher
                .matching()
                .withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING)
                .withIgnoreCase(true);
        Hospital hospital = new Hospital();
        BeanUtils.copyProperties(hospitalQueryVo, hospital);
        Example<Hospital> example = Example.of(hospital, matcher);
        Page<Hospital> pages = hospitalRepository.findAll(example, pageable);
        // 获取查询 list 集合,遍历进行医院等级封装
        pages.getContent().stream().forEach(item -> {
            this.setHospitalHosType(item);
        });
        return pages;
    }

    private void 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);
    }
    。。。

预约挂号系统技术点详解(二)_第5张图片

二、服务网关

1. 网关介绍

API网关出现的原因是微服务架构的出现,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:
(1)客户端会多次请求不同的微服务,增加了客户端的复杂性。
(2)存在跨域请求,在一定场景下处理相对复杂。
(3)认证复杂,每个服务都需要独立认证。
(4)难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施。
(5)某些微服务可能使用了防火墙 / 浏览器不友好的协议,直接访问会有一定的困难。
以上这些问题可以借助 API 网关解决。API 网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过API 网关这一层。也就是说,API 的实现方面更多的考虑业务逻辑,而安全、性能、监控可以交由 API 网关来做,这样既提高业务灵活性又不缺安全性。

2.Spring Cloud Gateway介绍

Spring cloud gateway是spring官方基于Spring 5.0、Spring Boot2.0和Project Reactor等技术开发的网关,Spring Cloud Gateway旨在为微服务架构提供简单、有效和统一的API路由管理方式,Spring Cloud Gateway作为Spring Cloud生态系统中的网关,目标是替代Netflix Zuul,其不仅提供统一的路由方式,并且还基于Filer链的方式提供了网关基本的功能,例如:安全、监控/埋点、限流等
预约挂号系统技术点详解(二)_第6张图片

3. 搭建server-gateway网关服务模块

⑴ 搭建server-gateway

预约挂号系统技术点详解(二)_第7张图片

⑵ 修改配置pom.xml

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>

    <!-- 服务注册 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
</dependencies>

⑶ 在resources下添加配置文件

# 服务端口
server.port=80
# 服务名
spring.application.name=service-gateway

# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

#使用服务发现路由
spring.cloud.gateway.discovery.locator.enabled=true

#设置路由id
spring.cloud.gateway.routes[0].id=service-hosp
#设置路由的uri
spring.cloud.gateway.routes[0].uri=lb://service-hosp
#设置路由断言,代理servicerId为auth-service的/auth/路径
spring.cloud.gateway.routes[0].predicates= Path=/*/hosp/**

#设置路由id
spring.cloud.gateway.routes[1].id=service-cmn
#设置路由的uri
spring.cloud.gateway.routes[1].uri=lb://service-cmn
#设置路由断言,代理servicerId为auth-service的/auth/路径
spring.cloud.gateway.routes[1].predicates= Path=/*/cmn/**

⑷ 添加启动类

@SpringBootApplication
public class ServerGatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServerGatewayApplication.class, args);
    }
}

⑸ 全局Filter,统一处理登录与外部不允许访问的服务

创建com.kejizhentan.yygh.filter.AuthGlobalFilter拦截去:

/**
 * @Auther: kejizhentan
 * @Date 2022/12/8 21:12
 * @Description: 全局Filter,统一处理登录与外部不允许访问的服务
 */
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();
        System.out.println("==="+path);

        //内部服务接口,不允许外部访问
        if(antPathMatcher.match("/**/inner/**", path)) {
            ServerHttpResponse response = exchange.getResponse();
            return out(response, ResultCodeEnum.PERMISSION);
        }

        //api接口,异步请求,校验用户必须登录
        if(antPathMatcher.match("/api/**/auth/**", path)) {
            Long userId = this.getUserId(request);
            if(StringUtils.isEmpty(userId)) {
                ServerHttpResponse response = exchange.getResponse();
                return out(response, ResultCodeEnum.LOGIN_AUTH);
            }
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }

    /**
     * api接口鉴权失败返回数据
     * @param response
     * @return
     */
    private Mono<Void> out(ServerHttpResponse response, ResultCodeEnum resultCodeEnum) {
        Result result = Result.build(null, resultCodeEnum);
        byte[] bits = JSONObject.toJSONString(result).getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bits);
        //指定编码,否则在浏览器中会中文乱码
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return response.writeWith(Mono.just(buffer));
    }

    /**
     * 获取当前登录用户id
     * @param request
     * @return
     */
    private Long getUserId(ServerHttpRequest request) {
        String token = "";
        List<String> tokenList = request.getHeaders().get("token");
        if(null  != tokenList) {
            token = tokenList.get(0);
        }
        if(!StringUtils.isEmpty(token)) {
            return JwtHelper.getUserId(token);
        }
        return null;
    }
}

⑹ 跨域处理

跨域:浏览器对于javascript的同源策略的限制 。

以下情况都属于跨域:

跨域原因说明 示例
域名不同 www.jd.com 与 www.taobao.com
域名相同,端口不同 www.jd.com:8080 与 www.jd.com:8081
二级域名不同 item.jd.com 与 miaosha.jd.com

如果域名和端口都相同,但是请求路径不同,不属于跨域,如:
www.jd.com/item
www.jd.com/goods
http和https也属于跨域
而我们刚才是从localhost:1000去访问localhost:8888,这属于端口不同,跨域了。

1)为什么有跨域问题?

跨域不一定都会有跨域问题。
因为跨域问题是浏览器对于ajax请求的一种安全限制:一个页面发起的ajax请求,只能是与当前页域名相同的路径,这能有效的阻止跨站攻击。
因此:跨域问题 是针对ajax的一种限制。
但是这却给我们的开发带来了不便,而且在实际生产环境中,肯定会有很多台服务器之间交互,地址和端口都可能不同,怎么办?

2)Spring Cloud Gateway 解决跨域问题

添加全局配置类CorsConfig类实现

@Configuration
public class CorsConfig {
    @Bean
    public CorsWebFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedMethod("*");
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);

        return new CorsWebFilter(source);
    }
}

预约挂号系统技术点详解(二)_第8张图片

⑺ 服务调整

1)目前我们已经在网关做了跨域处理,那么service服务就不需要再做跨域处理了,将之前在controller类上添加过@CrossOrigin标签的去掉,防止程序异常

预约挂号系统技术点详解(二)_第9张图片

2)改变BASE_API

预约挂号系统技术点详解(二)_第10张图片

⑻ 测试

预约挂号系统技术点详解(二)_第11张图片

三、服务端渲染技术NUXT

1. 什么是服务端渲染

服务端渲染又称SSR (Server Side Render)是在服务端完成页面的内容,而不是在客户端通过AJAX获取数据。
服务器端渲染(SSR)的优势主要在于:更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
如果你的应用程序初始展示 loading 菊花图,然后通过 Ajax 获取内容,抓取工具并不会等待异步完成后再进行页面内容的抓取。也就是说,如果 SEO 对你的站点至关重要,而你的页面又是异步获取内容,则你可能需要服务器端渲染(SSR)解决此问题。
另外,使用服务器端渲染,我们可以获得更快的内容到达时间(time-to-content),无需等待所有的 JavaScript 都完成下载并执行,产生更好的用户体验,对于那些内容到达时间(time-to-content)与转化率直接相关的应用程序而言,服务器端渲染(SSR)至关重要。

2. 什么是NUXT

Nuxt.js 是一个基于 Vue.js 的轻量级应用框架,可用来创建服务端渲染 (SSR) 应用,也可充当静态站点引擎生成静态站点应用,具有优雅的代码结构分层和热加载等特性。
官网网站:
https://zh.nuxtjs.org/

3. NUXT环境初始化

⑴ 下载压缩包

https://github.com/nuxt-community/starter-template/archive/master.zip
点击直接下载压缩包:https://kejizhentan.lanzoue.com/ildF80how6xi

⑵ 解压

在工作区文件夹中创建yygh-site文件夹,将解压后的template内容复制到该文件夹中
预约挂号系统技术点详解(二)_第12张图片

预约挂号系统技术点详解(二)_第13张图片

⑶ 修改package.json

name、description、author(必须修改这里,否则项目无法安装)

{
  "name": "yygh-site",
  "version": "1.0.0",
  "description": "预约挂号项目",
  "author": "kejizhentan",
  "private": true,
  "scripts": {
    "dev": "nuxt",
    "build": "nuxt build",
    "start": "nuxt start",
    "generate": "nuxt generate",
    "lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
    "precommit": "npm run lint"
  },
...
}

预约挂号系统技术点详解(二)_第14张图片
预约挂号系统技术点详解(二)_第15张图片

⑷ 修改nuxt.config.js

修改title: ‘{{ name }}’、content: ‘{{escape description }}’
这里的设置最后会显示在页面标题栏和meta数据中

module.exports = {
  /*
  ** Headers of the page
  */
  head: {
    title: 'yygh-site',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: '预约挂号项目' }
    ],

预约挂号系统技术点详解(二)_第16张图片
预约挂号系统技术点详解(二)_第17张图片

⑸ 终端中进入项目目录安装依赖

npm install

预约挂号系统技术点详解(二)_第18张图片

⑹ 引入element-ui

1) 下载element-ui

npm  install  element-ui

在这里插入图片描述

2)在plugins文件夹下创建myPlugin.js文件

预约挂号系统技术点详解(二)_第19张图片

3)在myPlugin.js文件引入element-ui

import Vue from 'vue'
import ElementUI from 'element-ui' //element-ui的全部组件
import 'element-ui/lib/theme-chalk/index.css'//element-ui的css
Vue.use(ElementUI) //使用elementUI

预约挂号系统技术点详解(二)_第20张图片

4) 在nuxt.config.js文件中使用myPlugin.js

在build下面添加内容:

plugins: [
    { src: '~/plugins/myPlugin.js', ssr: false }
  ]

预约挂号系统技术点详解(二)_第21张图片

⑺ 测试运行

npm run dev

预约挂号系统技术点详解(二)_第22张图片

访问项目:http://localhost:3000/
预约挂号系统技术点详解(二)_第23张图片

⑻ NUXT目录结构

预约挂号系统技术点详解(二)_第24张图片

1、资源目录 assets
用于组织未编译的静态资源如 LESS、SASS 或 JavaScript。
2、组件目录 components
用于组织应用的 Vue.js 组件。Nuxt.js 不会扩展增强该目录下 Vue.js 组件,即这些组件不会像页面组件那样有 asyncData 方法的特性。
3、布局目录 layouts
用于组织应用的布局组件。
4、页面目录 pages
用于组织应用的路由及视图。Nuxt.js 框架读取该目录下所有的 .vue 文件并自动生成对应的路由配置。
5、插件目录 plugins
用于组织那些需要在 根vue.js应用 实例化之前需要运行的 Javascript 插件。
6、nuxt.config.js 文件
nuxt.config.js 文件用于组织Nuxt.js 应用的个性化配置,以便覆盖默认配置。

4. 封装axios

⑴ 封装axios

创建utils文件夹,utils下创建request.js
预约挂号系统技术点详解(二)_第25张图片

import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
// 创建axios实例
const service = axios.create({
    baseURL: 'http://localhost',
    timeout: 15000 // 请求超时时间
})
// http request 拦截器
service.interceptors.request.use(
    config => {
    // token 先不处理,后续使用时在完善
    return config
},
  err => {
    return Promise.reject(err)
})
// http response 拦截器
service.interceptors.response.use(
    response => {
        if (response.data.code !== 200) {
            Message({
                message: response.data.message,
                type: 'error',
                duration: 5 * 1000
            })
            return Promise.reject(response.data)
        } else {
            return response.data
        }
    },
    error => {
        return Promise.reject(error.response)
})
export default service

预约挂号系统技术点详解(二)_第26张图片

⑵ 安装axios

#执行安装命令
npm install axios

预约挂号系统技术点详解(二)_第27张图片

5. 首页实现

⑴ 公共处理

1) 添加静态资源

将静态资源下面的css、images文件夹添加到assets目录,如图:
预约挂号系统技术点详解(二)_第28张图片

⑵ 定义布局

1) 修改默认布局

参考静态资源文件首页,我们可以把页头和页尾提取出来,形成布局页。
在layouts目录下修改默认布局文件default.vue,将主内容区域的内容替换成
修改layouts/default.vue文件

<template>
  <div class="app-container">
    <div id="main">
      <!-- 公共头 -->
      <myheader/>
      <div class="main-container">
        <el-scrollbar class='page-component__scroll'>
        <!-- 内容区域 -->
        <nuxt/>
        </el-scrollbar>
      </div>
        <!-- 公共底 -->
      <myfooter/>
    </div>
  </div>
</template>
<script>
import '~/assets/css/app.css'
import '~/assets/css/chunk.css'
import '~/assets/css/iconfont.css'
import '~/assets/css/main.css'
export default {
 }
</script>

预约挂号系统技术点详解(二)_第29张图片

2) 提取头文件

创建layouts/myheader.vue文件

<template>
    <div class="header-container">
        <div class="wrapper">
        <!-- logo -->
            <div class="left-wrapper v-link selected">
                <img style="width: 50px" width="50" height="50" src="~assets/images/logo.png">
                <span class="text">柯基侦探 预约挂号统一平台</span>
            </div>
        <!-- 搜索框 -->
        <div class="search-wrapper">
            <div class="hospital-search animation-show">
                <el-autocomplete
                    class="search-input small"
                    prefix-icon="el-icon-search"
                    v-model="state"
                    :fetch-suggestions="querySearchAsync"
                    placeholder="点击输入医院名称"
                    @select="handleSelect"
                    >
                    <span slot="suffix" class="search-btn v-link highlight clickable selected">搜索 </span>
                </el-autocomplete>
            </div>
        </div>
        <!-- 右侧 -->
        <div class="right-wrapper">
        <span class="v-link clickable">帮助中心</span>
        <!--        <el-dropdown >-->
        <!--              <span class="el-dropdown-link">-->
        <!--                晴天<i class="el-icon-arrow-down el-icon--right"></i>-->
        <!--              </span>-->
        <!--            <el-dropdown-menu class="user-name-wrapper" slot="dropdown">-->
        <!--                <el-dropdown-item>挂号订单</el-dropdown-item>-->
        <!--                <el-dropdown-item>就诊人管理</el-dropdown-item>-->
        <!--                <el-dropdown-item divided>退出登录</el-dropdown-item>-->
        <!--            </el-dropdown-menu>-->
        <!--        </el-dropdown>-->
        <span class="v-link clickable" @click="dialogUserFormVisible = true">登录/注册</span>
        </div>
        </div>
    </div>
</template>
<script>
export default {
}
</script>

预约挂号系统技术点详解(二)_第30张图片

3) 提取尾文件

创建layouts/myfooter.vue文件

<template>
    <div class="footer-container">
        <div class="wrapper">
            <div><span class="record">京ICP备13018369</span><span
            class="phone">电话挂号125656654</span></div>
            <div class="right"><span
            class="v-link clickable"> 联系我们 </span><span
            class="v-link clickable"> 合作伙伴 </span><span
            class="v-link clickable"> 用户协议 </span><span
            class="v-link clickable"> 隐私协议 </span></div>
        </div>
    </div>
</template>
<script>
export default {
}
</script>

预约挂号系统技术点详解(二)_第31张图片

4) 默认布局引入头尾文件

修改layouts/default.vue文件

<script>
import '~/assets/css/app.css'
import '~/assets/css/chunk.css'
import '~/assets/css/iconfont.css'
import '~/assets/css/main.css'

import myheader from './myheader'
import myfooter from './myfooter'
export default {
  components: {
      myheader,myfooter
    }
  }
</script>

预约挂号系统技术点详解(二)_第32张图片

5) 引入首页静态页面

修改pages/index.vue文件

<template>
  <div class="home page-component">
    <el-carousel indicator-position="outside">
      <el-carousel-item v-for="item in 2" :key="item">
        <img src="~assets/images/web-banner1.png" alt="">
      </el-carousel-item>
    </el-carousel>
    <!-- 搜索 -->
    <div class="search-container">
    <div class="search-wrapper">
    <div class="hospital-search">
      <el-autocomplete
      class="search-input"
      prefix-icon="el-icon-search"
      v-model="state"
      :fetch-suggestions="querySearchAsync"
      placeholder="点击输入医院名称"
      @select="handleSelect"
      >
        <span slot="suffix" class="search-btn v-link highlight clickable selected">搜索 </span>
      </el-autocomplete>
    </div>
    </div>
    </div>
    <!-- bottom -->
    <div class="bottom">
    <div class="left">
    <div class="home-filter-wrapper">
    <div class="title"> 医院</div>
    <div>
      <div class="filter-wrapper">
        <span
        class="label">等级:</span>
        <div class="condition-wrapper"><span
        class="item v-link highlight clickable selected">
                                                    全部 </span><span
        class="item v-link clickable">
                                                    三级医院 </span><span
        class="item v-link clickable">
                                                    二级医院 </span><span
        class="item v-link clickable">
                                                    一级医院 </span></div>
      </div>
    <div class="filter-wrapper">
      <span
      class="label">地区:</span>
      <div class="condition-wrapper"><span
      class="item v-link highlight clickable selected">
                                                  全部 </span><span
      class="item v-link clickable">
                                                  东城区 </span><span
      class="item v-link clickable">
                                                  西城区 </span><span
      class="item v-link clickable">
                                                  朝阳区 </span><span
      class="item v-link clickable">
                                                  丰台区 </span><span
      class="item v-link clickable">
                                                  石景山区 </span><span
      class="item v-link clickable">
                                                  海淀区 </span><span
      class="item v-link clickable">
                                                  门头沟区 </span><span
      class="item v-link clickable">
                                                  房山区 </span><span
      class="item v-link clickable">
                                                  通州区 </span><span
      class="item v-link clickable">
                                                  顺义区 </span><span
      class="item v-link clickable">
                                                  昌平区 </span><span
      class="item v-link clickable">
                                                  大兴区 </span><span
      class="item v-link clickable">
                                                  怀柔区 </span><span
      class="item v-link clickable">
                                                  平谷区 </span><span
      class="item v-link clickable">
                                                  密云区 </span><span
      class="item v-link clickable">
                                                  延庆区 </span></div>
      </div>
    </div>
    </div>
    <div class="v-scroll-list hospital-list">
    <div class="v-card clickable list-item">
    <div class="">
      <div
      class="hospital-list-item hos-item" index="0">
      <div class="wrapper">
      <div class="hospital-title"> 北京协和医院
      </div>
      <div class="bottom-container">
      <div
      class="icon-wrapper"><span
      class="iconfont"></span>
                            三级甲等
      </div>
    <div
    class="icon-wrapper"><span
    class="iconfont"></span>
                          每天8:30放号
    </div>
    </div>
    </div>
    <img
    src="images/23176337663806575.png"
    alt="北京协和医院" class="hospital-img"></div>
    </div>
    </div>
      <div class="v-card clickable list-item space">
      <div class="">
      <div
      class="hospital-list-item hos-item" index="0">
      <div class="wrapper">
      <div class="hospital-title"> 北京协和医院
      </div>
      <div class="bottom-container">
      <div
      class="icon-wrapper"><span
      class="iconfont"></span>
                            三级甲等
      </div>
    <div
    class="icon-wrapper"><span
    class="iconfont"></span>
                          每天8:30放号
    </div>
    </div>
    </div>
    <img
    src="images/23176337663806575.png"
    alt="北京协和医院" class="hospital-img"></div>
    </div>
    </div>
    <div class="v-card clickable list-item">
    <div class="">
    <div class="hospital-list-item hos-item" index="0">
      <div class="wrapper">
      <div class="hospital-title"> 北京协和医院
      </div>
      <div class="bottom-container">
      <div
      class="icon-wrapper"><span
      class="iconfont"></span>
                            三级甲等
      </div>
      <div
      class="icon-wrapper"><span
      class="iconfont"></span>
                            每天8:30放号
      </div>
    </div>
    </div>
    <img
    src="images/23176337663806575.png"
    alt="北京协和医院" class="hospital-img"></div>
    </div>
    </div>
    </div>
    </div>
    <div class="right">
      <div class="common-dept">
      <div class="header-wrapper">
      <div class="title"> 常见科室</div>
      <div class="all-wrapper"><span>全部</span>
      <span class="iconfont icon"></span>
      </div>
      </div>
      <div class="content-wrapper">
      <span class="item v-link clickable dark">神经内科 </span>
      <span class="item v-link clickable dark">消化内科 </span>
      <span class="item v-link clickable dark">呼吸内科 </span>
      <span class="item v-link clickable dark">内科 </span>
      <span class="item v-link clickable dark">神经外科 </span>
      <span class="item v-link clickable dark">妇科 </span>
      <span class="item v-link clickable dark"> 产科 </span>
      <span class="item v-link clickable dark">儿科 </span>
      </div>
    </div>
    <div class="space">
      <div class="header-wrapper">
      <div class="title-wrapper">
      <div class="icon-wrapper"><span
      class="iconfont title-icon"></span>
      </div>
      <span class="title">平台公告</span>
      </div>
      <div class="all-wrapper">
      <span>全部</span>
      <span class="iconfont icon"></span>
      </div>
      </div>
      <div class="content-wrapper">
      <div class="notice-wrapper">
      <div class="point"></div>
      <span class="notice v-link clickable dark">关于延长北京大学国际医院放假的通知 </span>
      </div>
      <div class="notice-wrapper">
      <div class="point"></div>
      <span class="notice v-link clickable dark">北京中医药大学东方医院部分科室医生门诊医 </span>
      </div>
      <div class="notice-wrapper">
      <div class="point"></div>
      <span class="notice v-link clickable dark"> 武警总医院号源暂停更新通知 </span>
      </div>
      </div>
    </div>
    <div class="suspend-notice-list space">
    <div class="header-wrapper">
    <div class="title-wrapper">
      <div class="icon-wrapper">
      <span class="iconfont title-icon"></span>
      </div>
      <span class="title">停诊公告</span>
      </div>
      <div class="all-wrapper">
      <span>全部</span>
      <span class="iconfont icon"></span>
      </div>
      </div>
      <div class="content-wrapper">
      <div class="notice-wrapper">
      <div class="point"></div>
      <span class="notice v-link clickable dark"> 中国人民解放军总医院第六医学中心(原海军总医院)呼吸内科门诊停诊公告 </span>
      </div>
      <div class="notice-wrapper">
      <div class="point"></div>
    <span class="notice v-link clickable dark"> 首都医科大学附属北京潞河医院老年医学科门诊停诊公告 </span>
    </div>
      <div class="notice-wrapper">
        <div class="point"></div>
        <span class="notice v-link clickable dark">中日友好医院中西医结合心内科门诊停诊公告 </span>
      </div>
    </div>
    </div>
    </div>
    </div>
  </div>
</template>
<script>
export default {
}
</script>

预约挂号系统技术点详解(二)_第33张图片

6)启动服务并测试

启动项目:npm run dev

预约挂号系统技术点详解(二)_第34张图片
效果如下:
预约挂号系统技术点详解(二)_第35张图片

7) 首页数据分析

1,获取医院等级(根据数据字典编码获取)
2,获取地区(根据数据字典编码获取)
3,医院分页列表
4,根据医院名称关键字搜索医院列表

6. 首页功能实现

⑴ 首页数据api接口(这里主要介绍NUXT前端框架的开发,crud的操作就不过多的解释)

1)医院分页列表

① service接口与实现

在管理平台 医院分页列表时已经提供,目前我们可以直接使用

② 添加controller接口
@Api(tags = "医院管理接口")
@RestController
@RequestMapping("/api/hosp/hospital")
public class HospitalApiController {

	@Autowired
	private HospitalService hospitalService;
	 @ApiOperation(value = "查询医院列表")
	 @GetMapping("findHospList/{page}/{limit}")
	 public Result findHospList(@PathVariable Integer page,
	                            @PathVariable Integer limit,
	                            HospitalQueryVo hospitalQueryVo) {
	     Page<Hospital> hospitals = hospitalService.selectHospPage(page, limit, hospitalQueryVo);
	     return Result.ok(hospitals);
	 }
 }

2) 根据医院名称关键字搜索医院列表

① service接口与实现

在HospitalService类添加接口

/**
 * 根据医院名称获取医院列表
*/
List<Hospital> findByHosname(String hosname);

在HospitalService类添加接口实现

@Override
public List<Hospital> findByHosname(String hosname) {
    return hospitalRepository.findHospitalByHosnameLike(hosname);
}
② repository添加接口

在HospitalRepository类添加接口

List<Hospital> findHospitalByHosnameLike(String hosname);
③ 添加controller接口

在.HospitalApiController 类添加方法

 @ApiOperation(value = "根据医院名称获取医院列表")
 @GetMapping("findByHosName/{hosname}")
 public Result findByHosName(@PathVariable String hosname) {
     return Result.ok(hospitalService.findByName(hosname));
 }

⑵ 首页前端实现

1) 封装api请求

创建api文件夹,创建/api/hospital.js
import request from '@/utils/request'

const api_name = `/api/hospital`

export default {
 getPageList(page, limit, searchObj) {
    return request({
        url: `${api_name}/${page}/${limit}`,
        method: 'get',
        params: searchObj
    })
  },

 getByHosname(hosname) {
    return request({
        url: `${api_name}/findByHosname/${hosname}`,
        method: 'get'
    })
  }
}

预约挂号系统技术点详解(二)_第36张图片

② 创建/api/dict.js
import request from '@/utils/request'

const api_name = '/admin/cmn/dict'

export default {
findByDictCode(dictCode) {
    return request({
        url: `${api_name}/findByDictCode/${dictCode}`,
        method: 'get'
    })
  },

findByParentId(parentId) {
    return request({
        url: `${api_name}/findChildData/${parentId}`,
        method: 'get'
    })
  }
}

预约挂号系统技术点详解(二)_第37张图片

2)调用接口

修改pages/index.vue文件

<script>
  import hospApi from '@/api/hospital'
  import dictApi from '@/api/dict'
  export default {
    //服务端渲染异步,显示医院列表
    asyncData({ params, error }) {
      //调用
      return hospApi.getPageList(1,10,null)
        .then(response => {
          return {
            list: response.data.content,
            pages: response.data.totalPages
          }
        })
    },
    data() {
      return {
        searchObj: {},
        page: 1,
        limit: 10,

        hosname: '', //医院名称
        hostypeList: [], //医院等级集合
        districtList: [], //地区集合

        hostypeActiveIndex: 0,
        provinceActiveIndex: 0
      }
    },
    created() {
      this.init()
    },
    methods:{
      //查询医院等级列表  所有地区列表
      init() {
        //查询医院等级列表
        dictApi.findByDictCode('Hostype')
          .then(response => {
            //hostypeList清空
            this.hostypeList = []
            //向hostypeList添加全部值
            this.hostypeList.push({"name":"全部","value":""})
            //把接口返回数据,添加到hostypeList
            for(var i=0;i<response.data.length;i++) {
                this.hostypeList.push(response.data[i])
            }
        })

        //查询地区数据
        dictApi.findByDictCode('Beijing')
          .then(response => {
            this.districtList = []
            this.districtList.push({"name":"全部","value":""})
            for(let i in response.data) {
              this.districtList.push(response.data[i])
            }
          })
      },

      //查询医院列表
      getList() {
        hospApi.getPageList(this.page,this.limit,this.searchObj)
          .then(response => {
            for(let i in response.data.content) {
              this.list.push(response.data.content[i])
            }
            this.page = response.data.totalPages
          })
      }
    }
  }
</script>

3)添加组件

修改pages/index.vue组件

<template>
  <div class="home page-component">
    <el-carousel indicator-position="outside">
      <el-carousel-item v-for="item in 2" :key="item">
        <img src="~assets/images/web-banner1.png" alt="">
      </el-carousel-item>
    </el-carousel>
    <!-- 搜索 -->
    <div class="search-container">
      <div class="search-wrapper">
        <div class="hospital-search">
          <el-autocomplete class="search-input" prefix-icon="el-icon-search" v-model="hosname"
            :fetch-suggestions="querySearchAsync" placeholder="点击输入医院名称" @select="handleSelect">
            <span slot="suffix" class="search-btn v-link highlight clickable selected">搜索 </span>
          </el-autocomplete>
        </div>
      </div>
    </div>
    <!-- bottom -->
    <div class="bottom">
      <div class="left">
        <div class="home-filter-wrapper">
          <div class="title"> 医院</div>
          <div>
            <div class="filter-wrapper">
              <span class="label">等级:</span>
              <div class="condition-wrapper">
                <span class="item v-link clickable" v-for="(item, index) in hostypeList" :key="index"
                  :class="hostypeActiveIndex == index ? 'selected' : ''" @click="hostypeSelect(item.value, index)">
                  {{item.name}} </span>
              </div>
            </div>
            <div class="filter-wrapper">
              <span class="label">地区:</span>
              <div class="condition-wrapper">
                <span class="item v-link clickable" v-for="(item, index) in districtList" :key="index"
                  :class="provinceActiveIndex  == index ? 'selected' : ''" @click="districtSelect(item.value, index)">
                  {{item.name}} </span>
              </div>
            </div>
          </div>
        </div>
        <div class="v-scroll-list hospital-list">

          <div class="v-card clickable list-item" @click="show(item.hoscode)" v-for="(item, index) in list"
            :key="index">
            <div class="">
              <div class="hospital-list-item hos-item">
                <div class="wrapper">
                  <div class="hospital-title"> {{ item.hosname }}</div>
                  <div class="bottom-container">
                    <div class="icon-wrapper">
                      <span class="iconfont"></span>{{ item.param.hostypeString }}
                    </div>
                    <div class="icon-wrapper">
                      <span class="iconfont"></span>每天{{ item.bookingRule.releaseTime }}放号
                    </div>
                  </div>
                </div>
                <img :src="'data:image/jpeg;base64,'+item.logoData" :alt="item.hosname" class="hospital-img">
              </div>
            </div>
          </div>

        </div>
      </div>
      <div class="right">
        <div class="common-dept">
          <div class="header-wrapper">
            <div class="title"> 常见科室</div>
            <div class="all-wrapper"><span>全部</span>
              <span class="iconfont icon"></span>
            </div>
          </div>
          <div class="content-wrapper">
            <span class="item v-link clickable dark">神经内科 </span>
            <span class="item v-link clickable dark">消化内科 </span>
            <span class="item v-link clickable dark">呼吸内科 </span>
            <span class="item v-link clickable dark">内科 </span>
            <span class="item v-link clickable dark">神经外科 </span>
            <span class="item v-link clickable dark">妇科 </span>
            <span class="item v-link clickable dark"> 产科 </span>
            <span class="item v-link clickable dark">儿科 </span>
          </div>
        </div>
        <div class="space">
          <div class="header-wrapper">
            <div class="title-wrapper">
              <div class="icon-wrapper"><span class="iconfont title-icon"></span>
              </div>
              <span class="title">平台公告</span>
            </div>
            <div class="all-wrapper">
              <span>全部</span>
              <span class="iconfont icon"></span>
            </div>
          </div>
          <div class="content-wrapper">
            <div class="notice-wrapper">
              <div class="point"></div>
              <span class="notice v-link clickable dark">关于延长北京大学国际医院放假的通知 </span>
            </div>
            <div class="notice-wrapper">
              <div class="point"></div>
              <span class="notice v-link clickable dark">北京中医药大学东方医院部分科室医生门诊医 </span>
            </div>
            <div class="notice-wrapper">
              <div class="point"></div>
              <span class="notice v-link clickable dark"> 武警总医院号源暂停更新通知 </span>
            </div>
          </div>
        </div>
        <div class="suspend-notice-list space">
          <div class="header-wrapper">
            <div class="title-wrapper">
              <div class="icon-wrapper">
                <span class="iconfont title-icon"></span>
              </div>
              <span class="title">停诊公告</span>
            </div>
            <div class="all-wrapper">
              <span>全部</span>
              <span class="iconfont icon"></span>
            </div>
          </div>
          <div class="content-wrapper">
            <div class="notice-wrapper">
              <div class="point"></div>
              <span class="notice v-link clickable dark"> 中国人民解放军总医院第六医学中心(原海军总医院)呼吸内科门诊停诊公告 </span>
            </div>
            <div class="notice-wrapper">
              <div class="point"></div>
              <span class="notice v-link clickable dark"> 首都医科大学附属北京潞河医院老年医学科门诊停诊公告 </span>
            </div>
            <div class="notice-wrapper">
              <div class="point"></div>
              <span class="notice v-link clickable dark">中日友好医院中西医结合心内科门诊停诊公告 </span>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

⑶ 医院详情

1) 预约挂号详情页面要求

预约挂号系统技术点详解(二)_第38张图片

说明:需要获取医院信息(医院基本信息、预约信息)和科室信息

⑴ 医院详情api接口(这里主要介绍NUXT前端框架的开发,crud的操作就不过多的解释)

1) 添加service接口与实现

1,在HospitalService类添加接口

/**
 * 医院预约挂号详情
*/
Map<String, Object> item(String hoscode);

2,在HospitalServiceImpl类实现接口

@Autowired
private DepartmentService departmentService;
.	.	.
@Override
public Map<String, Object> item(String hoscode) {
    Map<String, Object> result = new HashMap<>();
    //医院详情
    Hospital hospital = this.setHospitalHosType(this.getByHoscode(hoscode));
    result.put("hospital", hospital);
    //预约规则
    result.put("bookingRule", hospital.getBookingRule());
    //不需要重复返回
    hospital.setBookingRule(null);
    return result;
}

2) 添加controller接口

在HospitalApiController类添加方法

@ApiOperation(value = "获取科室列表")
@GetMapping("department/{hoscode}")
public Result index(
        @ApiParam(name = "hoscode", value = "医院code", required = true)
        @PathVariable String hoscode) {
    return Result.ok(departmentService.findTree(hoscode));
}

@ApiOperation(value = "医院预约挂号详情")
@GetMapping("{hoscode}")
public Result item(
        @ApiParam(name = "hoscode", value = "医院code", required = true)
        @PathVariable String hoscode) {
    return Result.ok(hospitalService.item(hoscode));
}

⑵ 预约挂号菜单栏前端前端

1)封装api请求

/api/hosp.js文件添加方法

show(hoscode) {
        return request({
            url: `${api_name}/${hoscode}`,
            method: 'get'
        })
    },
    findDepartment(hoscode) {
        return request({
            url: `${api_name}/department/${hoscode}`,
            method: 'get'
        })
    }

2)页面布局

创建/pages/hospital/_hoscode.vue组件

<template>
<!-- header -->
<div class="nav-container page-component">
<!--左侧导航 #start -->
<div class="nav left-nav">
<div class="nav-item selected">
<span class="v-link selected dark" 
 :onclick="'javascript:window.location=\'/hospital/'+hospital.hoscode+'\''">预约挂号 </span>
</div>
<div class="nav-item ">
<span class="v-link clickable dark" :onclick="'javascript:window.location=\'/hospital/detail/'+hospital.hoscode+'\''"> 医院详情 </span>
</div>
<div class="nav-item">
<span class="v-link clickable dark" :onclick="'javascript:window.location=\'/hospital/notice/'+hospital.hoscode+'\''"> 预约须知 </span>
</div>
<div class="nav-item "><span
class="v-link clickable dark"> 停诊信息 </span>
</div>
<div class="nav-item "><span
class="v-link clickable dark"> 查询/取消 </span>
</div>
</div>
<!-- 左侧导航 #end -->
<!-- 右侧内容 #start -->
<div class="page-container">
<div class="hospital-home">
<div class="common-header">
<div class="title-wrapper"><span
class="hospital-title">{{ hospital.hosname }}</span>
<div class="icon-wrapper">
<span class="iconfont"></span>{{ hospital.param.hostypeString }}
</div>
</div>
</div>
<div class="info-wrapper">
<img class="hospital-img" :src="'data:image/jpeg;base64,'+hospital.logoData" :alt="hospital.hosname">
<div class="content-wrapper">
<div> 挂号规则</div>
<div class="line">
<div><span class="label">预约周期:</span><span>{{ bookingRule.cycle }}</span></div>
<div class="space"><span class="label">放号时间:</span><span>{{ bookingRule.releaseTime }}</span></div>
<div class="space"><span class="label">停挂时间:</span><span>{{ bookingRule.stopTime }}</span></div>
</div>
<div class="line"><span class="label">退号时间:</span>
<span v-if="bookingRule.quitDay == -1">就诊前一工作日{{ bookingRule.quitTime }}前取消</span>
<span v-if="bookingRule.quitDay == 0">就诊前当天{{ bookingRule.quitTime }}前取消</span>
</div>
<div style="margin-top:20px"> 医院预约规则</div>
<div class="rule-wrapper">
<ol>
<li v-for="item in bookingRule.rule" :key="item">{{ item }}</li>
</ol>
</div>
</div>
</div>
<div class="title select-title"> 选择科室</div>
<div class="select-dept-wrapper">
<div class="department-wrapper">
<div class="hospital-department">
<div class="dept-list-wrapper el-scrollbar" style="height: 100%;">
<div class="dept-list el-scrollbar__wrap" style="margin-bottom: -17px; margin-right: -17px;">
<div class="el-scrollbar__view">
<div class="sub-item" v-for="(item,index) in departmentVoList" :key="item.id" :class="index == activeIndex ? 'selected' : ''" @click="move(index,item.depcode)"> {{ item.depname }}</div>
</div>
</div>
<div class="el-scrollbar__bar is-horizontal">
<div class="el-scrollbar__thumb" style="transform: translateX(0%);"></div>
</div>
<div class="el-scrollbar__bar is-vertical">
<div class="el-scrollbar__thumb" style="transform: translateY(0%); height: 91.4761%;"></div>
</div>
</div>
</div>
</div>
<div class="sub-dept-container">
<div v-for="(item,index) in departmentVoList" :key="item.id" :class="index == 0 ? 'selected' : ''" class="sub-dept-wrapper" :id="item.depcode">
<div class="sub-title">
<div class="block selected"></div>{{ item.depname }}
</div>
<div class="sub-item-wrapper">
<div v-for="it in item.children" :key="it.id" class="sub-item" @click="schedule(it.depcode)"><span class="v-link clickable">{{ it.depname }} </span></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧内容 #end -->
</div>
<!-- footer -->
</template>

<script>
import '~/assets/css/hospital_personal.css'
import '~/assets/css/hospital.css'

import hospitalApi from '@/api/hosp'

export default {
  data() {
    return {
      hoscode: null,
      activeIndex: 0,

      hospital: {
        param: {}
      },
      bookingRule : {},
      departmentVoList : []
    }
  },
  created() {
    this.hoscode = this.$route.params.hoscode
    this.init()
  },

  methods: {
    init() {
      hospitalApi.show(this.hoscode).then(response => {
        this.hospital = response.data.hospital
        this.bookingRule = response.data.bookingRule
      })

      hospitalApi.findDepartment(this.hoscode).then(response => {
        this.departmentVoList = response.data
      })
    },

    move(index, depcode) {
      this.activeIndex = index
      document.getElementById(depcode).scrollIntoView();
    },

    schedule(depcode) {
      window.location.href = '/hospital/schedule?hoscode=' + this.hoscode + "&depcode="+ depcode
    }
  }
}
</script>

⑶ 医院详情菜单栏

1) 页面布局

创建/pages/hospital/detail/_hoscode.vue组件

<template>
  <!-- header -->
  <div class="nav-container page-component">
    <!--左侧导航 #start -->
    <div class="nav left-nav">
      <div class="nav-item ">
        <span class="v-link clickable dark" :onclick="'javascript:window.location=\'/hosp/'+hospital.hoscode+'\''">预约挂号 </span>
      </div>
      <div class="nav-item selected">
        <span class="v-link selected dark" :onclick="'javascript:window.location=\'/hosp/detail/'+hospital.hoscode+'\''"> 医院详情 </span>
      </div>
      <div class="nav-item">
        <span class="v-link clickable dark" :onclick="'javascript:window.location=\'/hosp/notice/'+hospital.hoscode+'\''"> 预约须知 </span>
      </div>
      <div class="nav-item "><span
        class="v-link clickable dark"> 停诊信息 </span>
      </div>
      <div class="nav-item "><span
        class="v-link clickable dark"> 查询/取消 </span>
      </div>
    </div>
    <!-- 左侧导航 #end -->

    <!-- 右侧内容 #start -->
    <div class="page-container">
      <div class="hospital-detail">
        <div class="common-header">
          <div class="title-wrapper"><span class="hospital-title">{{ hospital.hosname }}</span>
            <div class="icon-wrapper"><span class="iconfont"></span> {{ hospital.param.hostypeString }}</div>
          </div>
        </div>
        <div class="info-wrapper"><img :src="'data:image/jpeg;base64,'+hospital.logoData" :alt="hospital.hosname" style="width: 80px; height: 80px;">
          <div class="content-wrapper">
            <div></div>
            <div></div>
            <div></div>
            <div>
              <div class="icon-text-wrapper"><span class="iconfont prefix-icon"></span>
                <span class="text"><p>{{ hospital.route }}</p>
              </span><span class="iconfont right-icon"></span></div>
            </div>
          </div>
        </div>
        <div class="title mt40"> 医院介绍</div>
        <div class="detail-content mt40"><p>{{ hospital.intro }}</p></div>
      </div>
    </div>
    <!-- 右侧内容 #end -->
  </div>
  <!-- footer -->
</template>

<script>
import '~/assets/css/hospital_personal.css'
import '~/assets/css/hospital.css'

import hospitalApi from '@/api/hosp'
export default {
  data() {
    return {
      hoscode: null,
      hospital: {
        param: {}
      }
    }
  },

  created() {
    this.hoscode = this.$route.params.hoscode

    this.init()
  },

  methods: {
    init() {
      hospitalApi.show(this.hoscode).then(response => {
        this.hospital = response.data.hospital
      })

    }
  }
}
</script>
<style>
  .hospital-detail .info-wrapper {
    width: 100%;
    padding-left: 0;
    padding-top: 0;
    margin-top: 0;
    flex-direction: inherit;
  }

  .hospital-detail .info-wrapper .text {
    font-size: 14px;

  }

  .hospital-detail .content-wrapper p {
    text-indent: 0;
  }
</style>

⑷ 预约须知

1) 页面布局

创建/pages/hospital/notice/_hoscode.vue组件

<template>
  <!-- header -->
  <div class="nav-container page-component">
    <!--左侧导航 #start -->
    <div class="nav left-nav">
      <div class="nav-item ">
        <span class="v-link clickable dark" :onclick="'javascript:window.location=\'/hosp/'+hospital.hoscode+'\''">预约挂号 </span>
      </div>
      <div class="nav-item ">
        <span class="v-link clickable dark" :onclick="'javascript:window.location=\'/hosp/detail/'+hospital.hoscode+'\''"> 医院详情 </span>
      </div>
      <div class="nav-item selected">
        <span class="v-link selected dark" :onclick="'javascript:window.location=\'/hosp/notice/'+hospital.hoscode+'\''"> 预约须知 </span>
      </div>
      <div class="nav-item "><span
        class="v-link clickable dark"> 停诊信息 </span>
      </div>
      <div class="nav-item "><span
        class="v-link clickable dark"> 查询/取消 </span>
      </div>
    </div>
    <!-- 左侧导航 #end -->

    <!-- 右侧内容 #start -->
    <div class="page-container">
      <div class="hospital-notice">
        <div class="content"><h2>{{ hospital.hosname }}预约挂号须知</h2>
          <p>为方便您早日就医康复,请您认真阅读预约挂号须知:</p>
          <h4 id="一、预约实名制:">一、预约实名制:</h4>
          <p>统一平台电话预约和网上预约挂号均采取实名制注册预约,请您如实提供就诊人员的真实姓名、有效证件号(身份证、护照)、性别、手机号码、社保卡号等基本信息。</p>
          <h4 id="二、预约挂号:">二、预约挂号:</h4>
          <p>按照北京市卫健委统一平台要求,预约挂号规则如下:</p>
          <ul>
            <li>在同一自然日,同一医院,同一科室,同一就诊单元,同一就诊人,可以预约最多1个号源;</li>
            <li>在同一自然周,同一就诊人,可以预约最多8个号源;</li>
            <li>在同一自然月,同一就诊人,可以预约最多12个号源;</li>
            <li>在同一自然季度,同一就诊人,可以预约最多24个号源。</li>
          </ul>
          <h4 id="三、取消预约:">三、取消预约:</h4>
          <p>已完成预约的号源,如需办理退号,至少在就诊前一工作日14:00前通过网站、微信公众号、114电话等平台预约渠道进行取消预约。</p>
          <h4 id="四、爽约处理:">四、爽约处理:</h4>
          <p>如预约成功后患者未能按时就诊且不办理取消预约号视为爽约,同一患者在自然年内爽约规则如下:</p>
          <ul>
            <li>累计爽约3次,自3次爽约日起,90天内不允许通过114平台进行预约挂号;</li>
            <li>累计爽约6次,自6次爽约日起,180天内不允许通过114平台进行预约挂号。</li>
          </ul>
        </div>

      </div>
    </div>
    <!-- 右侧内容 #end -->
  </div>
  <!-- footer -->
</template>

<script>
import '~/assets/css/hospital_personal.css'
import '~/assets/css/hospital.css'

import hospitalApi from '@/api/hosp'
export default {
  data() {
    return {
      hoscode: null,
      hospital: {
        param: {}
      }
    }
  },

  created() {
    this.hoscode = this.$route.params.hoscode

    this.init()
  },

  methods: {
    init() {
      hospitalApi.show(this.hoscode).then(response => {
        this.hospital = response.data.hospital
      })
    }
  }
}
</script>
<style>
  .hospital-detail .info-wrapper {
    width: 100%;
    padding-left: 0;
    padding-top: 0;
    margin-top: 0;
    flex-direction: inherit;
  }

  .hospital-detail .info-wrapper .text {
    font-size: 14px;

  }

  .hospital-detail .content-wrapper p {
    text-indent: 0;
  }
</style>

四、基于微服务的登录功能实现

1. 登录效果

2. 登录需求

1,登录采取弹出层的形式
2,登录方式:
(1)手机号码+手机验证码
(2)微信扫描
3,无注册界面,第一次登录根据手机号判断系统是否存在,如果不存在则自动注册
4,微信扫描登录成功必须绑定手机号码,即:第一次扫描成功后绑定手机号,以后登录扫描直接登录成功
5, 网关统一判断登录状态,如何需要登录,页面弹出登录层

3. 搭建service-user模块

创建service-user模块
预约挂号系统技术点详解(二)_第39张图片

⑴ 修改配置

1、修改pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>service</artifactId>
        <groupId>com.kejizhentan</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>service_user</artifactId>


</project

2、添加配置文件application.properties

# 服务端口
server.port=8203
# 服务名
spring.application.name=service-user

# 环境设置:dev、test、prod
spring.profiles.active=dev

# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/yygh_user?characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456

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

# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

⑵ 启动类

@SpringBootApplication
@ComponentScan(basePackages = "com.kejizhentan")
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.kejizhentan")
public class ServiceUserApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceUserApplication.class, args);
    }
}

⑶ 配置网关

#设置路由id
spring.cloud.gateway.routes[2].id=service-user
#设置路由的uri
spring.cloud.gateway.routes[2].uri=lb://service-user
#设置路由断言,代理servicerId为auth-service的/auth/路径
spring.cloud.gateway.routes[2].predicates= Path=/*/user/**

⑷ 添加model

说明:由于实体对象没有逻辑,我们已经统一导入
com.kejizhentan.yygh.model.user.UserInfo.java

⑸ 添加Mapper

1、添加com.kejizhentan.mapper.UserInfoMapper.java

public interface UserInfoMapper extends BaseMapper<UserInfo> {
}

2、添加UserInfoMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.kejizhentan.mapper.UserInfoMapper">

</mapper>

⑹ 添加service接口及实现类

1、添加com.kejizhentan.service.UserInfoService.java

public interface UserInfoService extends IService<UserInfo> {
}

2、添加com.kejizhentan.service.impl.UserInfoServiceImpl.java接口实现

@Service
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {
}

⑺ 添加controller

@RestController
@RequestMapping("/api/user")
public class UserInfoApiController {

    @Autowired
    private UserInfoService userInfoService;
}

4. 登录api接口编写

⑴ 添加service接口与实现

1、在UserInfoService类添加接口

public interface UserInfoService extends IService<UserInfo> {
    Map<String, Object> login(LoginVo loginVo);
}

2,在UserInfoServiceImpl类实现接口

@Service
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {
    //用户手机登录的实现
    @Override
    public Map<String, Object> login(LoginVo loginVo) {
        //从loginVo中获取输入的手机号和验证码
        String phone = loginVo.getPhone();
        String code = loginVo.getCode();
        //判断手机号和验证码是否为空
        if(StringUtils.isEmpty(phone) || StringUtils.isEmpty(code)){
            throw  new YyghException(ResultCodeEnum.PARAM_ERROR);
        }
        //TODO 判断手机验证码和输入的验证码是否一致
        //判断是否是第一次登录:根据手机号查询数据,如果不存在相同的手机号就是第一次登录
        QueryWrapper<UserInfo> wrapper = new QueryWrapper<>();
        wrapper.eq("phone",phone);
        UserInfo userInfo = baseMapper.selectOne(wrapper);
        if(userInfo == null){//第一次登录
            userInfo = new UserInfo();
            userInfo.setName("");
            userInfo.setPhone(phone);
            userInfo.setStatus(1);
            baseMapper.insert(userInfo);
        }
        //校验是否被禁用
        if(userInfo.getStatus() == 0) {
            throw new YyghException(ResultCodeEnum.LOGIN_DISABLED_ERROR);
        }
        //不是第一次,直接登录
        //返回登录信息
        //返回登录用户名
        //返回token信息
        Map<String, Object> map = new HashMap<>();
        String name = userInfo.getName();
        if(StringUtils.isEmpty(name)) {
            name = userInfo.getNickName();
        }
        if(StringUtils.isEmpty(name)) {
            name = userInfo.getPhone();
        }
        map.put("name", name);
        //TODO token 的生成
        map.put("token", "");
        return map;
    }
}

说明:
1、验证码先注释,后续校验
2、登录成功生成token,后续讲解

⑵ 添加controller接口

在UserInfoApiController类添加方法

@RestController
@RequestMapping("/api/user")
public class UserInfoApiController {

    @Autowired
    private UserInfoService userInfoService;

    //用户手机登录的实现
    @ApiOperation(value = "用户手机登录的实现")
    @PostMapping("/login")
    public Result loginUser(@RequestBody LoginVo loginVo){
        Map<String, Object> info = userInfoService.login(loginVo);
        return Result.ok(info);
    }
}

5. 生成token

⑴ JWT介绍

1) JWT工具

JWT(Json Web Token)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上JWT最重要的作用就是对 token信息的防伪作用。

2)JWT的原理

一个JWT由三个部分组成:公共部分、私有部分、签名部分。最后由这三者组合进行base64编码得到JWT。
预约挂号系统技术点详解(二)_第40张图片
1、公共部分
主要是该JWT的相关配置参数,比如签名的加密算法、格式类型、过期时间等等。
Key=kejizhentan
2、私有部分
用户自定义的内容,根据实际需要真正要封装的信息。
userInfo{用户的Id,用户的昵称nickName}
3、签名部分
SaltiP: 当前服务器的Ip地址!{linux 中配置代理服务器的ip}
主要用户对JWT生成字符串的时候,进行加密{盐值}
最终组成 key+salt+userInfo ——> token!

base64编码,并不是加密,只是把明文信息变成了不可见的字符串。但是其实只要用一些工具就可以把base64编码解成明文,所以不要在JWT中放入涉及私密的信息。

⑵ 集成JWT

1) 在common-util模块pom.xml添加依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
</dependency>

2)在common-util模块编写JwtHelper类

public class JwtHelper {
    //过期时间
    private static long tokenExpiration = 24*60*60*1000;
    private static String tokenSignKey = "123456";

    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;
    }
    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();
    }
    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, "zhangsan");
        System.out.println(token);
        System.out.println(JwtHelper.getUserId(token));
        System.out.println(JwtHelper.getUserName(token));
    }
}

说明:执行main方法测试
预约挂号系统技术点详解(二)_第41张图片

⑶ 完善登录service接口

修改UserInfoServiceImpl类登录方法

public Map<String, Object> loginUser(LoginVo loginVo) {
  .	 .	.	.	.	.	.
    //jwt生成token字符串
    String token = JwtHelper.createToken(userInfo.getId(), name);
    map.put("token",token);
    return map;
}

⑷ 使用Swagger测试接口

测试多次,第一次注册,以后直接登录

请求参数:
预约挂号系统技术点详解(二)_第42张图片

结果:
预约挂号系统技术点详解(二)_第43张图片

在这里插入图片描述

⑸ 通过token获取当前用户的工具类

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

6. 阿里云短信服务接入

⑴ 阿里云短信介绍

预约挂号系统技术点详解(二)_第44张图片

1) 开通阿里云短信服务

预约挂号系统技术点详解(二)_第45张图片

2) 添加签名管理与模板管理

预约挂号系统技术点详解(二)_第46张图片
预约挂号系统技术点详解(二)_第47张图片

注:审批通过后方可使用

3) 获取用户AccessKey

预约挂号系统技术点详解(二)_第48张图片

⑵ 搭建service-msm模块

1) 搭建service-msm模块

在这里插入图片描述

2) 修改配置

① 修改pom.xml
<dependencies>
    <dependency>
	  <groupId>com.aliyun</groupId>
	  <artifactId>aliyun-java-sdk-core</artifactId>
	  <version>4.5.3</version>
	</dependency>
</dependencies>
② 添加配置文件application.properties
# 服务端口
server.port=8204
# 服务名
spring.application.name=service-msm

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

spring.redis.host=127.0.0.1
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

# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

#阿里云短信相关配置
#服务器所在地区,默认即可
aliyun.sms.regionId=default

aliyun.sms.accessKeyId=LT6I0Y5633pX89qC
aliyun.sms.secret=jX8D04Dm12I3gGKj345FYSzu0fq8mT

3) 启动类

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消数据源自动配置
@EnableDiscoveryClient
public class ServiceMsmApplication {
   public static void main(String[] args) {
      SpringApplication.run(ServiceMsmApplication.class, args);
   }
}

4) 配置网关

#设置路由id
spring.cloud.gateway.routes[3].id=service-msm
#设置路由的uri
spring.cloud.gateway.routes[3].uri=lb://service-msm
#设置路由断言,代理servicerId为auth-service的/auth/路径
spring.cloud.gateway.routes[3].predicates= Path=/*/msm/**

⑶ 封装注册短信验证码接口

1) 添加工具类获取配置文件中的配置

//InitializingBean的作用是Bean注入到Spring容器且初始化后立刻拿到配置文件中的值。
@Component
public class ConstantPropertiesUtils implements InitializingBean {

    @Value("${aliyun.sms.regionId}")
    private String regionId;

    @Value("${aliyun.sms.accessKeyId}")
    private String accessKeyId;

    @Value("${aliyun.sms.secret}")
    private String secret;

    public static String REGION_Id;
    public static String ACCESS_KEY_ID;
    public static String SECRECT;

    @Override
    public void afterPropertiesSet() throws Exception {
        REGION_Id=regionId;
        ACCESS_KEY_ID=accessKeyId;
        SECRECT=secret;
    }
}

2)封装service接口和实现类

① MsmService 接口
public interface MsmService {

    //发送手机验证码
    boolean send(String phone, String code);
}
② MsmServiceImpl 实现类
@Service
public class MsmServiceImpl implements MsmService {
    @Override
    public boolean send(String phone, String code) {
        //判断手机号是否为空
        if(StringUtils.isEmpty(phone)) {
            return false;
        }
        //整合阿里云短信服务
        //设置相关参数
        DefaultProfile profile = DefaultProfile.
                getProfile(ConstantPropertiesUtils.REGION_Id,
                        ConstantPropertiesUtils.ACCESS_KEY_ID,
                        ConstantPropertiesUtils.SECRECT);
        IAcsClient client = new DefaultAcsClient(profile);
        CommonRequest request = new CommonRequest();
        //request.setProtocol(ProtocolType.HTTPS);
        request.setMethod(MethodType.POST);
        request.setDomain("dysmsapi.aliyuncs.com");
        request.setVersion("2017-05-25");
        request.setAction("SendSms");

        //手机号
        request.putQueryParameter("PhoneNumbers", phone);
        //签名名称
        request.putQueryParameter("SignName", "柯基e栈");
        //模板code
        request.putQueryParameter("TemplateCode", "SMS_180051135");
        //验证码  使用json格式   {"code":"123456"}
        Map<String,Object> param = new HashMap();
        param.put("code",code);
        request.putQueryParameter("TemplateParam", JSONObject.toJSONString(param));

        //调用方法进行短信发送
        try {
            CommonResponse response = client.getCommonResponse(request);
            System.out.println(response.getData());
            return response.getHttpResponse().isSuccess();
        } catch (ServerException e) {
            e.printStackTrace();
        } catch (ClientException e) {
            e.printStackTrace();
        }
        return false;
    }
}

3) 封装controller接口

@RestController
@RequestMapping("/api/msm")
public class MsmApiController {

    @Autowired
    private MsmService msmService;

    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    //发送手机验证码
    @GetMapping("send/{phone}")
    public Result sendCode(@PathVariable String phone) {
        //从redis获取验证码,如果获取获取到,返回ok
        // key 手机号  value 验证码
        String code = redisTemplate.opsForValue().get(phone);
        if(!StringUtils.isEmpty(code)) {
            return Result.ok();
        }
        //如果从redis获取不到,
        // 生成验证码,
        code = RandomUtil.getSixBitRandom();
        //调用service方法,通过整合短信服务进行发送
        boolean isSend = msmService.send(phone,code);
        //生成验证码放到redis里面,设置有效时间为5分钟
        if(isSend) {
            redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);
            return Result.ok();
        } else {
            return Result.fail().message("发送短信失败");
        }
    }
}

4) 添加工具类

public class RandomUtil {

    private static final Random random = new Random();

    private static final DecimalFormat fourdf = new DecimalFormat("0000");

    private static final DecimalFormat sixdf = new DecimalFormat("000000");

    public static String getFourBitRandom() {
        return fourdf.format(random.nextInt(10000));
    }

    public static String getSixBitRandom() {
        return sixdf.format(random.nextInt(1000000));
    }

    /**
     * 给定数组,抽取n个数据
     * @param list
     * @param n
     * @return
     */
    public static ArrayList getRandom(List list, int n) {

        Random random = new Random();

        HashMap<Object, Object> hashMap = new HashMap<Object, Object>();

        // 生成随机数字并存入HashMap
        for (int i = 0; i < list.size(); i++) {

            int number = random.nextInt(100) + 1;

            hashMap.put(number, i);
        }

        // 从HashMap导入数组
        Object[] robjs = hashMap.values().toArray();

        ArrayList r = new ArrayList();

        // 遍历数组并打印数据
        for (int i = 0; i < n; i++) {
            r.add(list.get((int) robjs[i]));
            System.out.print(list.get((int) robjs[i]) + "\t");
        }
        System.out.print("\n");
        return r;
    }
}

5) 测试

预约挂号系统技术点详解(二)_第49张图片
预约挂号系统技术点详解(二)_第50张图片

预约挂号系统技术点详解(二)_第51张图片

⑷ 完善登录service接口

修改UserInfoServiceImpl类登录方法,编写验证码校验

//校验校验验证码 :判断手机验证码和输入的验证码是否一致
//通过手机号从redis中获取验证码
String mobleCode = redisTemplate.opsForValue().get(phone);
if(!code.equals(mobleCode)) {
    throw new YyghException(ResultCodeEnum.CODE_ERROR);
}

7. 登录前端

⑴ 封装api请求

1) 创建api文件夹,创建/api/userInfo.js

import request from '@/utils/request'

const api_name = `/api/user`

export default {
    login(userInfo) {
        return request({
            url: `${api_name}/login`,
            method: `post`,
            data: userInfo
        })
    }
}

2) 创建/api/msm.js

import request from '@/utils/request'

const api_name = `/api/msm`

export default {
    sendCode(mobile) {
        return request({
            url: `${api_name}/send/${mobile}`,
            method: `get`
        })
    }
}

⑵ 安装cookie

登录成功,我们要把用户信息记录在cookie里面

命令行执行:
npm install js-cookie

在这里插入图片描述

⑶ 添加登录组件

登录层是一个公共层,因此我们把它放在头部组件里面
修改layouts/myheader.vue文件

<!-- 登录弹出层 -->
<el-dialog :visible.sync="dialogUserFormVisible" style="text-align: left;" top="50px" :append-to-body="true"  width="960px" @close="closeDialog()">
  <div class="container">

    <!-- 手机登录 #start -->
    <div class="operate-view" v-if="dialogAtrr.showLoginType === 'phone'">
      <div class="wrapper" style="width: 100%">
        <div class="mobile-wrapper" style="position: static;width: 70%">
          <span class="title">{{ dialogAtrr.labelTips }}</span>
          <el-form>
            <el-form-item>
              <el-input v-model="dialogAtrr.inputValue" :placeholder="dialogAtrr.placeholder" :maxlength="dialogAtrr.maxlength" class="input v-input">
                <span slot="suffix" class="sendText v-link" v-if="dialogAtrr.second > 0">{{ dialogAtrr.second }}s </span>
                <span slot="suffix" class="sendText v-link highlight clickable selected" v-if="dialogAtrr.second == 0" @click="getCodeFun()">重新发送 </span>
              </el-input>
            </el-form-item>
          </el-form>
          <div class="send-button v-button" @click="btnClick()"> {{ dialogAtrr.loginBtn }}</div>
        </div>
        <div class="bottom">
          <div class="wechat-wrapper" @click="weixinLogin()"><span
            class="iconfont icon"></span></div>
          <span class="third-text"> 第三方账号登录 </span></div>
      </div>
    </div>
    <!-- 手机登录 #end -->

    <!-- 微信登录 #start -->
    <div class="operate-view"  v-if="dialogAtrr.showLoginType === 'weixin'" >
      <div class="wrapper wechat" style="height: 400px">
        <div>
          <div id="weixinLogin"></div>
        </div>
        <div class="bottom wechat" style="margin-top: -80px;">
          <div class="phone-container">
            <div class="phone-wrapper"  @click="phoneLogin()"><span
              class="iconfont icon"></span></div>
            <span class="third-text"> 手机短信验证码登录 </span></div>
        </div>
      </div>
    </div>
    <!-- 微信登录 #end -->

    <div class="info-wrapper">
      <div class="code-wrapper">
        <div><img src="//img.114yygh.com/static/web/code_login_wechat.png" class="code-img">
          <div class="code-text"><span class="iconfont icon"></span>微信扫一扫关注
          </div>
          <div class="code-text"> “快速预约挂号”</div>
        </div>
        <div class="wechat-code-wrapper"><img
          src="//img.114yygh.com/static/web/code_app.png"
          class="code-img">
          <div class="code-text"> 扫一扫下载</div>
          <div class="code-text"> “预约挂号”APP</div>
        </div>
      </div>
      <div class="slogan">
        <div>柯基侦探官方指定平台</div>
        <div>快速挂号 安全放心</div>
      </div>
    </div>
  </div>
</el-dialog>

说明:登录成功用户信息记录cookie

<script>
import cookie from 'js-cookie'
import Vue from 'vue'

import userInfoApi from '@/api/userInfo'
import smsApi from '@/api/msm'
import hospitalApi from '@/api/hosp'

const defaultDialogAtrr = {
  showLoginType: 'phone', // 控制手机登录与微信登录切换

  labelTips: '手机号码', // 输入框提示

  inputValue: '', // 输入框绑定对象
  placeholder: '请输入您的手机号', // 输入框placeholder
  maxlength: 11, // 输入框长度控制

  loginBtn: '获取验证码', // 登录按钮或获取验证码按钮文本

  sending: true,      // 是否可以发送验证码
  second: -1,        // 倒计时间  second>0 : 显示倒计时 second=0 :重新发送 second=-1 :什么都不显示
  clearSmsTime: null  // 倒计时定时任务引用 关闭登录层清除定时任务
}
export default {
  data() {
    return {
      userInfo: {
        phone: '',
        code: '',
        openid: ''
      },

      dialogUserFormVisible: false,
      // 弹出层相关属性
      dialogAtrr:defaultDialogAtrr,

      name: '' // 用户登录显示的名称
    }
  },

  created() {
    this.showInfo()
  },
  methods: {
    // 绑定登录或获取验证码按钮
    btnClick() {
      // 判断是获取验证码还是登录
      if(this.dialogAtrr.loginBtn == '获取验证码') {
        this.userInfo.phone = this.dialogAtrr.inputValue

        // 获取验证码
        this.getCodeFun()
      } else {
        // 登录
        this.login()
      }
    },

    // 绑定登录,点击显示登录层
    showLogin() {
      this.dialogUserFormVisible = true

      // 初始化登录层相关参数
      this.dialogAtrr = { ...defaultDialogAtrr }
    },

    // 登录
    login() {
      this.userInfo.code = this.dialogAtrr.inputValue

      if(this.dialogAtrr.loginBtn == '正在提交...') {
        this.$message.error('重复提交')
        return;
      }
      if (this.userInfo.code == '') {
        this.$message.error('验证码必须输入')
        return;
      }
      if (this.userInfo.code.length != 6) {
        this.$message.error('验证码格式不正确')
        return;
      }
      this.dialogAtrr.loginBtn = '正在提交...'
      userInfoApi.login(this.userInfo).then(response => {
        console.log(response.data)
        // 登录成功 设置cookie
        this.setCookies(response.data.name, response.data.token)
      }).catch(e => {
        this.dialogAtrr.loginBtn = '马上登录'
      })
    },

    setCookies(name, token) {
      cookie.set('token', token, { domain: 'localhost' })
      cookie.set('name', name, { domain: 'localhost' })
      window.location.reload()
    },

    // 获取验证码
    getCodeFun() {
      if (!(/^1[34578]\d{9}$/.test(this.userInfo.phone))) {
        this.$message.error('手机号码不正确')
        return;
      }

      // 初始化验证码相关属性
      this.dialogAtrr.inputValue = ''
      this.dialogAtrr.placeholder = '请输入验证码'
      this.dialogAtrr.maxlength = 6
      this.dialogAtrr.loginBtn = '马上登录'

      // 控制重复发送
      if (!this.dialogAtrr.sending) return;

      // 发送短信验证码
      this.timeDown();
      this.dialogAtrr.sending = false;
      smsApi.sendCode(this.userInfo.phone).then(response => {
        this.timeDown();
      }).catch(e => {
        this.$message.error('发送失败,重新发送')
        // 发送失败,回到重新获取验证码界面
        this.showLogin()
      })
    },

    // 倒计时
    timeDown() {
      if(this.clearSmsTime) {
        clearInterval(this.clearSmsTime);
      }
      this.dialogAtrr.second = 60;

      this.dialogAtrr.labelTips = '验证码已发送至' + this.userInfo.phone
      this.clearSmsTime = setInterval(() => {
        --this.dialogAtrr.second;
        if (this.dialogAtrr.second < 1) {
          clearInterval(this.clearSmsTime);
          this.dialogAtrr.sending = true;
          this.dialogAtrr.second = 0;
        }
      }, 1000);
    },

    // 关闭登录层
    closeDialog() {
      if(this.clearSmsTime) {
        clearInterval(this.clearSmsTime);
      }
    },

    showInfo() {
      let token = cookie.get('token')
      if (token) {
        this.name = cookie.get('name')
        console.log(this.name)
      }
    },

    loginMenu(command) {
      if('/logout' == command) {
        cookie.set('name', '', {domain: 'localhost'})
        cookie.set('token', '', {domain: 'localhost'})

        //跳转页面
        window.location.href = '/'
      } else {
        window.location.href = command
      }
    },

    handleSelect(item) {
      window.location.href = '/hospital/' + item.hoscode
    },

    weixinLogin() {
      this.dialogAtrr.showLoginType = 'weixin'
    },

    phoneLogin() {
      this.dialogAtrr.showLoginType = 'phone'
      this.showLogin()
    }
  }
}
</script>

说明:

  1. 获取登录信息
    created() {
     this.showInfo()
    },
    showInfo() {
      let token = cookie.get(‘token’)
      if (token) {
       this.name = cookie.get(‘name’)
     }
    },

  2. 添加下拉列表事件处理:
    loginMenu(command) {
     if(‘/logout’ == command) {
      cookie.set(‘name’, ‘’, {domain: ‘localhost’})
      cookie.set(‘token’, ‘’, {domain: ‘localhost’})

      //跳转页面
      window.location.href = ‘/’
    } else {
       window.location.href = command
       }
    },

登录页面控制,添加如下代码

 <!-- 右侧 -->
 <div class="right-wrapper">
   <span class="v-link clickable">帮助中心</span>
   <span v-if="name == ''" class="v-link clickable" @click="showLogin()" id="loginDialog">登录/注册</span>
   <el-dropdown v-if="name != ''" @command="loginMenu">
         <span class="el-dropdown-link">
           {{ name }}<i class="el-icon-arrow-down el-icon--right"></i>
         </span>
     <el-dropdown-menu class="user-name-wrapper" slot="dropdown">
       <el-dropdown-item command="/user">实名认证</el-dropdown-item>
       <el-dropdown-item command="/order">挂号订单</el-dropdown-item>
       <el-dropdown-item command="/patient">就诊人管理</el-dropdown-item>
       <el-dropdown-item command="/logout" divided>退出登录</el-dropdown-item>
     </el-dropdown-menu>
   </el-dropdown>
 </div>

说明:实名认证“command”的值为下拉列表的访问路径,即:实名认证的访问路径,后续会有对应的页面,目前我们将它处理好,方便后续使用

五、用户认证与网关整合

思路:
1.所有请求都会经过服务网关,服务网关对外暴露服务,在网关进行统一用户认证;
2.既然要在网关进行用户认证,网关得知道对哪些url进行认证,所以我们得对ur制定规则
3.Api接口异步请求的,我们采取url规则匹配,如:/api/**/auth/**,如凡是满足该规则的都必须用户认证

1. 调整server-gateway模块

⑴ 在服务网关添加fillter

1) 在服务网关添加fillter

/**
 * @Auther: kejizhentan
 * @Date 2022/12/8 21:12
 * @Description: 全局Filter,统一处理登录与外部不允许访问的服务
 */
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();
        System.out.println("==="+path);

        //内部服务接口,不允许外部访问
        if(antPathMatcher.match("/**/inner/**", path)) {
            ServerHttpResponse response = exchange.getResponse();
            return out(response, ResultCodeEnum.PERMISSION);
        }

        //api接口,异步请求,校验用户必须登录
        if(antPathMatcher.match("/api/**/auth/**", path)) {
            Long userId = this.getUserId(request);
            if(StringUtils.isEmpty(userId)) {
                ServerHttpResponse response = exchange.getResponse();
                return out(response, ResultCodeEnum.LOGIN_AUTH);
            }
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }

    /**
     * api接口鉴权失败返回数据
     * @param response
     * @return
     */
    private Mono<Void> out(ServerHttpResponse response, ResultCodeEnum resultCodeEnum) {
        Result result = Result.build(null, resultCodeEnum);
        byte[] bits = JSONObject.toJSONString(result).getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bits);
        //指定编码,否则在浏览器中会中文乱码
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return response.writeWith(Mono.just(buffer));
    }

    /**
     * 获取当前登录用户id
     * @param request
     * @return
     */
    private Long getUserId(ServerHttpRequest request) {
        String token = "";
        List<String> tokenList = request.getHeaders().get("token");
        if(null  != tokenList) {
            token = tokenList.get(0);
        }
        if(!StringUtils.isEmpty(token)) {
            return JwtHelper.getUserId(token);
        }
        return null;
    }
}

2) 在服务网关中判断用户登录状态

1,在网关中如何获取用户信息:
我们统一从header头信息中获取
2, 如何判断用户信息合法:
登录时我们返回用户token,在服务网关中获取到token后,我在到redis中去查看用户id,如何用户id存在,则token合法,否则不合法

3) 用户认证与网关整合详解

① 在服务网关添加fillter
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    ServerHttpRequest request = exchange.getRequest();
    String path = request.getURI().getPath();
    System.out.println("==="+path);

    //内部服务接口,不允许外部访问
    if(antPathMatcher.match("/**/inner/**", path)) {
        ServerHttpResponse response = exchange.getResponse();
        return out(response, ResultCodeEnum.PERMISSION);
    }

    //api接口,异步请求,校验用户必须登录
    if(antPathMatcher.match("/api/**/auth/**", path)) {
        Long userId = this.getUserId(request);
        if(StringUtils.isEmpty(userId)) {
            ServerHttpResponse response = exchange.getResponse();
            return out(response, ResultCodeEnum.LOGIN_AUTH);
        }
    }
    return chain.filter(exchange);
}
② 取用户信息
/**
 * 获取当前登录用户id
 * @param request
 * @return
 */
private Long getUserId(ServerHttpRequest request) {
    String token = "";
    List<String> tokenList = request.getHeaders().get("token");
    if(null  != tokenList) {
        token = tokenList.get(0);
    }
    if(!StringUtils.isEmpty(token)) {
        return JwtHelper.getUserId(token);
    }
    return null;
}

⑵ 调整前端yygh-site

请求服务器端接口时我们默认带上token,需要登录的接口如果token没有或者token过期,服务器端会返回208状态,然后发送登录事件打开登录弹出层登录
修改utils/request.js文件

import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import cookie from 'js-cookie'

// 创建axios实例
const service = axios.create({
    baseURL: 'http://localhost',
    timeout: 15000 // 请求超时时间
})
// http request 拦截器
service.interceptors.request.use(
    config => {
    // token 先不处理,后续使用时在完善
    //判断cookie是否有token值
    if(cookie.get('token')) {
        //token值放到cookie里面
        config.headers['token']=cookie.get('token')
    }
    return config
},
  err => {
    return Promise.reject(err)
})
// http response 拦截器
service.interceptors.response.use(
    response => {
        //状态码是208
        if(response.data.code === 208) {
            //弹出登录输入框
            loginEvent.$emit('loginDialogEvent')
            return
        } else {
            if (response.data.code !== 200) {
                Message({
                    message: response.data.message,
                    type: 'error',
                    duration: 5 * 1000
                })
                return Promise.reject(response.data)
            } else {
                return response.data
            }
        }
    },
    error => {
        return Promise.reject(error.response)
})
export default service

六、httpclient工具类介绍

1. 相关依赖

<dependency>
   <groupId>org.apache.httpcomponents</groupId>
   <artifactId>httpclient</artifactId>
</dependency>

2. 工具类

public class HttpClientUtils {

    public static final int connTimeout=10000;
    public static final int readTimeout=10000;
    public static final String charset="UTF-8";
    private static HttpClient client = null;

    static {
        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
        cm.setMaxTotal(128);
        cm.setDefaultMaxPerRoute(128);
        client = HttpClients.custom().setConnectionManager(cm).build();
    }

    public static String postParameters(String url, String parameterStr) throws ConnectTimeoutException, SocketTimeoutException, Exception{
        return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);
    }

    public static String postParameters(String url, String parameterStr,String charset, Integer connTimeout, Integer readTimeout) throws ConnectTimeoutException, SocketTimeoutException, Exception{
        return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);
    }

    public static String postParameters(String url, Map<String, String> params) throws ConnectTimeoutException,
            SocketTimeoutException, Exception {
        return postForm(url, params, null, connTimeout, readTimeout);
    }

    public static String postParameters(String url, Map<String, String> params, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,
            SocketTimeoutException, Exception {
        return postForm(url, params, null, connTimeout, readTimeout);
    }

    public static String get(String url) throws Exception {
        return get(url, charset, null, null);
    }

    public static String get(String url, String charset) throws Exception {
        return get(url, charset, connTimeout, readTimeout);
    }

    /**
     * 发送一个 Post 请求, 使用指定的字符集编码.
     *
     * @param url
     * @param body RequestBody
     * @param mimeType 例如 application/xml "application/x-www-form-urlencoded" a=1&b=2&c=3
     * @param charset 编码
     * @param connTimeout 建立链接超时时间,毫秒.
     * @param readTimeout 响应超时时间,毫秒.
     * @return ResponseBody, 使用指定的字符集编码.
     * @throws ConnectTimeoutException 建立链接超时异常
     * @throws SocketTimeoutException  响应超时
     * @throws Exception
     */
    public static String post(String url, String body, String mimeType,String charset, Integer connTimeout, Integer readTimeout)
            throws ConnectTimeoutException, SocketTimeoutException, Exception {
        HttpClient client = null;
        HttpPost post = new HttpPost(url);
        String result = "";
        try {
            if (StringUtils.isNotBlank(body)) {
                HttpEntity entity = new StringEntity(body, ContentType.create(mimeType, charset));
                post.setEntity(entity);
            }
            // 设置参数
            Builder customReqConf = RequestConfig.custom();
            if (connTimeout != null) {
                customReqConf.setConnectTimeout(connTimeout);
            }
            if (readTimeout != null) {
                customReqConf.setSocketTimeout(readTimeout);
            }
            post.setConfig(customReqConf.build());

            HttpResponse res;
            if (url.startsWith("https")) {
                // 执行 Https 请求.
                client = createSSLInsecureClient();
                res = client.execute(post);
            } else {
                // 执行 Http 请求.
                client = HttpClientUtils.client;
                res = client.execute(post);
            }
            result = IOUtils.toString(res.getEntity().getContent(), charset);
        } finally {
            post.releaseConnection();
            if (url.startsWith("https") && client != null&& client instanceof CloseableHttpClient) {
                ((CloseableHttpClient) client).close();
            }
        }
        return result;
    }


    /**
     * 提交form表单
     *
     * @param url
     * @param params
     * @param connTimeout
     * @param readTimeout
     * @return
     * @throws ConnectTimeoutException
     * @throws SocketTimeoutException
     * @throws Exception
     */
    public static String postForm(String url, Map<String, String> params, Map<String, String> headers, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,
            SocketTimeoutException, Exception {

        HttpClient client = null;
        HttpPost post = new HttpPost(url);
        try {
            if (params != null && !params.isEmpty()) {
                List<NameValuePair> formParams = new ArrayList<NameValuePair>();
                Set<Entry<String, String>> entrySet = params.entrySet();
                for (Entry<String, String> entry : entrySet) {
                    formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
                }
                UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, Consts.UTF_8);
                post.setEntity(entity);
            }

            if (headers != null && !headers.isEmpty()) {
                for (Entry<String, String> entry : headers.entrySet()) {
                    post.addHeader(entry.getKey(), entry.getValue());
                }
            }
            // 设置参数
            Builder customReqConf = RequestConfig.custom();
            if (connTimeout != null) {
                customReqConf.setConnectTimeout(connTimeout);
            }
            if (readTimeout != null) {
                customReqConf.setSocketTimeout(readTimeout);
            }
            post.setConfig(customReqConf.build());
            HttpResponse res = null;
            if (url.startsWith("https")) {
                // 执行 Https 请求.
                client = createSSLInsecureClient();
                res = client.execute(post);
            } else {
                // 执行 Http 请求.
                client = HttpClientUtils.client;
                res = client.execute(post);
            }
            return IOUtils.toString(res.getEntity().getContent(), "UTF-8");
        } finally {
            post.releaseConnection();
            if (url.startsWith("https") && client != null
                    && client instanceof CloseableHttpClient) {
                ((CloseableHttpClient) client).close();
            }
        }
    }

    /**
     * 发送一个 GET 请求
     */
    public static String get(String url, String charset, Integer connTimeout,Integer readTimeout)
            throws ConnectTimeoutException,SocketTimeoutException, Exception {

        HttpClient client = null;
        HttpGet get = new HttpGet(url);
        String result = "";
        try {
            // 设置参数
            Builder customReqConf = RequestConfig.custom();
            if (connTimeout != null) {
                customReqConf.setConnectTimeout(connTimeout);
            }
            if (readTimeout != null) {
                customReqConf.setSocketTimeout(readTimeout);
            }
            get.setConfig(customReqConf.build());

            HttpResponse res = null;

            if (url.startsWith("https")) {
                // 执行 Https 请求.
                client = createSSLInsecureClient();
                res = client.execute(get);
            } else {
                // 执行 Http 请求.
                client = HttpClientUtils.client;
                res = client.execute(get);
            }

            result = IOUtils.toString(res.getEntity().getContent(), charset);
        } finally {
            get.releaseConnection();
            if (url.startsWith("https") && client != null && client instanceof CloseableHttpClient) {
                ((CloseableHttpClient) client).close();
            }
        }
        return result;
    }

    /**
     *  response 里获取 charset
     */
    @SuppressWarnings("unused")
    private static String getCharsetFromResponse(HttpResponse ressponse) {
        // Content-Type:text/html; charset=GBK
        if (ressponse.getEntity() != null  && ressponse.getEntity().getContentType() != null && ressponse.getEntity().getContentType().getValue() != null) {
            String contentType = ressponse.getEntity().getContentType().getValue();
            if (contentType.contains("charset=")) {
                return contentType.substring(contentType.indexOf("charset=") + 8);
            }
        }
        return null;
    }

    /**
     * 创建 SSL连接
     * @return
     * @throws GeneralSecurityException
     */
    private static CloseableHttpClient createSSLInsecureClient() throws GeneralSecurityException {
        try {
            SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
                public boolean isTrusted(X509Certificate[] chain,String authType) throws CertificateException {
                    return true;
                }
            }).build();

            SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, new X509HostnameVerifier() {

                @Override
                public boolean verify(String arg0, SSLSession arg1) {
                    return true;
                }

                @Override
                public void verify(String host, SSLSocket ssl)
                        throws IOException {
                }

                @Override
                public void verify(String host, X509Certificate cert)
                        throws SSLException {
                }

                @Override
                public void verify(String host, String[] cns,
                                   String[] subjectAlts) throws SSLException {
                }
            });
            return HttpClients.custom().setSSLSocketFactory(sslsf).build();

        } catch (GeneralSecurityException e) {
            throw e;
        }
    }
}

3. 测试

public class TestHttps {
    public static void main(String[] args) throws Exception {
        //获取授权临时票据
        System.out.println("微信授权服务器回调。。。。。。");
        //使用code和appid以及appscrect换取access_token
        StringBuffer baseAccessTokenUrl = new StringBuffer()
                .append("https://apis.juhe.cn/fapigw/worldcup2022/schedule")
                .append("?key=%s")//使用%s占位
                .append("&type=%s");
        String accessTokenUrl = String.format(baseAccessTokenUrl.toString(),"73347fgsgasfagfaffaafaffaerty6a",6);

        String result = null;
        result = HttpClientUtils.get(accessTokenUrl);
        System.out.println(result);
    }
}

预约挂号系统技术点详解(二)_第52张图片

七、阿里云对象存储oss

1. 阿里云oss

用户认证需要上传证件图片、首页轮播也需要上传图片,因此我们要做文件服务,阿里云oss是一个很好的分布式文件服务系统,所以我们只需要集成阿里云oss即可

⑴ 开通“对象存储OSS”服务

  • 申请阿里云账号
  • 实名认证
  • 开通“对象存储OSS”服务
  • 进入管理控制台

⑵ 阿里云oss对象存储控制台操作

1) 创建Bucket

选择:标准存储、公共读、不开通

预约挂号系统技术点详解(二)_第53张图片

2) 上传默认头像

创建文件夹avatar,上传默认的用户头像

预约挂号系统技术点详解(二)_第54张图片

3) 获取用户acesskeys

预约挂号系统技术点详解(二)_第55张图片

⑶ 使用SDK文档

预约挂号系统技术点详解(二)_第56张图片
预约挂号系统技术点详解(二)_第57张图片

2. 文件服务实现

⑴ 搭建service-oss模块

预约挂号系统技术点详解(二)_第58张图片

⑵ 修改配置

1) 修改pom.xml,引入阿里云oss依赖

<dependencies>
   <!-- 阿里云oss依赖 -->
   <dependency>
       <groupId>com.aliyun.oss</groupId>
       <artifactId>aliyun-sdk-oss</artifactId>
       <version>3.10.2</version>
   </dependency>

   <!-- 日期工具栏依赖 -->
   <dependency>
       <groupId>joda-time</groupId>
       <artifactId>joda-time</artifactId>
   </dependency>
</dependencies>

2) 添加配置文件application.properties

# 服务端口
server.port=8205
# 服务名
spring.application.name=service-oss

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

# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

aliyun.oss.endpoint=oss-cn-beijing.aliyuncs.com
aliyun.oss.accessKeyId=LTAI4G4SV6454UYH776b64
aliyun.oss.secret=X9KHNY56445Zp8JffQPZO4uJo5
aliyun.oss.bucket=kejizhentan

⑶ 启动类

//取消数据源自动配置
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@ComponentScan(basePackages = {"com.kejizhentan"})
public class ServiceOssApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceOssApplication.class, args);
    }
}

⑷ 配置网关

#设置路由id
spring.cloud.gateway.routes[4].id=service-oss
#设置路由的uri
spring.cloud.gateway.routes[4].uri=lb://service-oss
#设置路由断言,代理servicerId为auth-service的/auth/路径
spring.cloud.gateway.routes[4].predicates= Path=/*/oss/**

⑸ 测试SDK

public class TestOSS {
    public static void main(String[] args) throws Exception {
        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
        // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
        String accessKeyId = "yourAccessKeyId";
        String accessKeySecret = "yourAccessKeySecret";
        // 填写Bucket名称,例如examplebucket。
        String bucketName = "kjzt-yygh";

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try {
            // 创建存储空间。
            ossClient.createBucket(bucketName);

        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }
}

结果如下:
预约挂号系统技术点详解(二)_第59张图片

⑹ 创建ConstantOssPropertiesUtils配置类

@Component
public class ConstantOssPropertiesUtils implements InitializingBean {

    @Value("${aliyun.oss.endpoint}")
    private String endpoint;

    @Value("${aliyun.oss.accessKeyId}")
    private String accessKeyId;

    @Value("${aliyun.oss.secret}")
    private String secret;

    @Value("${aliyun.oss.bucket}")
    private String bucket;

    public static String EDNPOINT;
    public static String ACCESS_KEY_ID;
    public static String SECRECT;
    public static String BUCKET;

    @Override
    public void afterPropertiesSet() throws Exception {
        EDNPOINT=endpoint;
        ACCESS_KEY_ID=accessKeyId;
        SECRECT=secret;
        BUCKET=bucket;
    }
}

⑺ 封装service接口

创建FileService接口

public interface FileService {
    //上传文件到阿里云oss
    String upload(MultipartFile file);
}

创建FileService接口类实现类

@Service
public class FileServiceImpl implements FileService {

    @Override
    public String upload(MultipartFile file) {
        // Endpoint以杭州为例,其它Region请按实际情况填写。
        String endpoint = ConstantOssPropertiesUtils.EDNPOINT;
        String accessKeyId = ConstantOssPropertiesUtils.ACCESS_KEY_ID;
        String accessKeySecret = ConstantOssPropertiesUtils.SECRECT;
        String bucketName = ConstantOssPropertiesUtils.BUCKET;
        try {
            // 创建OSSClient实例。
            OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
            // 上传文件流。
            InputStream inputStream = file.getInputStream();
            String fileName = file.getOriginalFilename();
            //生成随机唯一值,使用uuid,添加到文件名称里面
            String uuid = UUID.randomUUID().toString().replaceAll("-","");
            fileName = uuid+fileName;
            //按照当前日期,创建文件夹,上传到创建文件夹里面
            //  2021/02/02/01.jpg
            String timeUrl = new DateTime().toString("yyyy/MM/dd");
            fileName = timeUrl+"/"+fileName;
            //调用方法实现上传
            ossClient.putObject(bucketName, fileName, inputStream);
            // 关闭OSSClient。
            ossClient.shutdown();
            //上传之后文件路径
            // https://yygh-atguigu.oss-cn-beijing.aliyuncs.com/01.jpg
            String url = "https://"+bucketName+"."+endpoint+"/"+fileName;
            //返回
            return url;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}

⑻ 封装controller接口

@RestController
@RequestMapping("/api/oss/file")
public class FileApiController {
    @Autowired
    private FileService fileService;
    //上传文件到阿里云oss
    @PostMapping("fileUpload")
    public Result fileUpload(MultipartFile file) {
        //获取上传文件
        String url = fileService.upload(file);
        return Result.ok(url);
    }
}

结果如下:
预约挂号系统技术点详解(二)_第60张图片
预约挂号系统技术点详解(二)_第61张图片

八、通过rabbitmq消息队列改造短息发送

短信发送验证码,为了提高验证码发送的并发性,这部分逻辑我们就交给mq为我们完成,点击验证码发送消息即可

1. RabbitMQ简介

以商品订单场景为例,
如果商品服务和订单服务是两个不同的微服务,在下单的过程中订单服务需要调用商品服务进行扣库存操作。按照传统的方式,下单过程要等到调用完毕之后才能返回下单成功,如果网络产生波动等原因使得商品服务扣库存延迟或者失败,会带来较差的用户体验,如果在高并发的场景下,这样的处理显然是不合适的,那怎么进行优化呢?这就需要消息队列登场了。
消息队列提供一个异步通信机制,消息的发送者不必一直等待到消息被成功处理才返回,而是立即返回。消息中间件负责处理网络通信,如果网络连接不可用,消息被暂存于队列当中,当网络畅通的时候在将消息转发给相应的应用程序或者服务,当然前提是这些服务订阅了该队列。如果在商品服务和订单服务之间使用消息中间件,既可以提高并发量,又降低服务之间的耦合度。
RabbitMQ就是这样一款消息队列。RabbitMQ是一个开源的消息代理的队列服务器,用来通过普通协议在完全不同的应用之间共享数据。

2. 典型应用场景:

异步处理。 把消息放入消息中间件中,等到需要的时候再去处理。
流量削峰。 例如秒杀活动,在短时间内访问量急剧增加,使用消息队列,当消息队列满了就拒绝响应,跳转到错误页面,这样就可以使得系统不会因为超负载而崩溃。
日志处理
应用解耦

3. rabbit-util模块封装

由于可能多个模块都会使用mq,所以我们把它封装成一个模块,需要的地方直接引用即可

⑴ 搭建rabbit-util模块

预约挂号系统技术点详解(二)_第62张图片

⑵ 修改pom.xml

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

⑶ 封装service方法

@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;
    }
}

⑷ 配置mq消息转换器

@Configuration
public class MQConfig {
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

说明:默认是字符串转换器

⑸ 添加常量配置

创建com.kejizhentan.constant.MqConst

public class MqConst {
    /**
     * 短信
     */
    public static final String EXCHANGE_DIRECT_MSM = "exchange.direct.msm";
    public static final String ROUTING_MSM_ITEM = "msm.item";
    //队列
    public static final String QUEUE_MSM_ITEM  = "queue.msm.item";
}

4. 封装并改造短信接口

操作模块:service-msm

⑴ 引入依赖

<dependencies>
    <dependency>
        <groupId>com.aliyun</groupId>
        <artifactId>aliyun-java-sdk-core</artifactId>
    </dependency>
    <dependency>
        <groupId>com.kejizhentan</groupId>
        <artifactId>rabbit-util</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

⑵ 添加配置

resources/application.properties添加

#rabbitmq地址
spring.rabbitmq.host=192.168.193.225
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

⑶ 在model模块封装短信实体

com.kejizhentan.yygh.vo.msm.MsmVo

@Data
@ApiModel(description = "短信实体")
public class MsmVo {
    @ApiModelProperty(value = "phone")
    private String phone;
    @ApiModelProperty(value = "短信模板code")
    private String templateCode;
    @ApiModelProperty(value = "短信模板参数")
    private Map<String,Object> param;
}

⑷ 封装service接口

1) 在MsmService类添加接口

public interface MsmService {

    // 注册发送短信接口
    boolean send(String phone, String code);

    // mq 发送短信接口
    boolean send(MsmVo msmVo);

}

2) 在MsmServiceImpl类添加接口实现

@Service
public class MsmServiceImpl implements MsmService {
    @Override
    public boolean send(String phone, String code) {
        //判断手机号是否为空
        if(StringUtils.isEmpty(phone)) {
            return false;
        }
        //整合阿里云短信服务
        //设置相关参数
        DefaultProfile profile = DefaultProfile.
                getProfile(ConstantPropertiesUtils.REGION_Id,
                        ConstantPropertiesUtils.ACCESS_KEY_ID,
                        ConstantPropertiesUtils.SECRECT);
        IAcsClient client = new DefaultAcsClient(profile);
        CommonRequest request = new CommonRequest();
        //request.setProtocol(ProtocolType.HTTPS);
        request.setMethod(MethodType.POST);
        request.setDomain("dysmsapi.aliyuncs.com");
        request.setVersion("2017-05-25");
        request.setAction("SendSms");

        //手机号
        request.putQueryParameter("PhoneNumbers", phone);
        //签名名称
        request.putQueryParameter("SignName", "柯基侦探");
        //模板code
        request.putQueryParameter("TemplateCode", "SMS_18888565");
        //验证码  使用json格式   {"code":"123456"}
        Map<String,Object> param = new HashMap();
        param.put("code",code);
        request.putQueryParameter("TemplateParam", JSONObject.toJSONString(param));

        //调用方法进行短信发送
        try {
            CommonResponse response = client.getCommonResponse(request);
            System.out.println(response.getData());
            return response.getHttpResponse().isSuccess();
        } catch (ServerException e) {
            e.printStackTrace();
        } catch (ClientException e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * @Auther: kejizhentan
     * @Date 2022/12/21 22:31
     * @Description: 通过消息队列的方式发送短信
     */
    @Override
    public boolean sendmsm(MsmVo msmVo) {
        if(!StringUtils.isEmpty(msmVo.getPhone())) {
            String code = (String)msmVo.getParam().get("code");
            return this.send(msmVo.getPhone(),code);
        }
        return false;
    }
}

⑸ 封装mq监听器

@Component
public class SmsReceiver {
    @Autowired
    private MsmService msmService;
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = MqConst.QUEUE_MSM_ITEM, durable = "true"),
            exchange = @Exchange(value = MqConst.EXCHANGE_DIRECT_MSM),
            key = {MqConst.ROUTING_MSM_ITEM}
    ))
    //@RabbitListener标注在方法上,直接监听指定的队列,此时接收的参数需要与发送市类型一致
    public void send(MsmVo msmVo, Message message, Channel channel) {
        msmService.sendmsm(msmVo);
    }
}

预约挂号系统技术点详解(二)_第63张图片

⑹ 调整验证码发送接口

@RestController
@RequestMapping("/api/msm")
public class MsmApiController {

    @Autowired
    private MsmService msmService;

    @Autowired
    private RabbitService rabbitService;

    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    //发送手机验证码
    @GetMapping("send/{phone}")
    public Result sendCode(@PathVariable String phone) {
        //从redis获取验证码,如果获取获取到,返回ok
        // key 手机号  value 验证码
        String code = redisTemplate.opsForValue().get(phone);
        if(!StringUtils.isEmpty(code)) {
            return Result.ok();
        }
        //如果从redis获取不到,
        // 生成验证码,
        code = RandomUtil.getSixBitRandom();
        //调用service方法,通过整合短信服务进行发送
        //boolean isSend = msmService.send(phone,code);
        MsmVo msmVo = new MsmVo();
        msmVo.setPhone(phone);
        msmVo.setTemplateCode(code);
        //将MsmVo发送到mq中
        boolean isSend = rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_MSM,MqConst.ROUTING_MSM_ITEM , msmVo);
        //boolean isSend  = msmService.sendmsm(msmVo);
        //生成验证码放到redis里面,设置有效时间
        if(isSend) {
            redisTemplate.opsForValue().set(phone,code,2, TimeUnit.MINUTES);
            return Result.ok();
        } else {
            return Result.fail().message("发送短信失败");
        }
    }
}

九、定时任务介绍

1. 后台写死的定时任务

⑴ 搭建定时任务模块service-task

预约挂号系统技术点详解(二)_第64张图片

⑵ 添加配置文件

application.properties

# 服务端口
server.port=8207
# 服务名
spring.application.name=service-task
# 环境设置:dev、test、prod
spring.profiles.active=dev

# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

⑶ 添加启动类

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消数据源自动配置
@EnableDiscoveryClient
public class ServiceTaskApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceTaskApplication.class, args);
    }
}

⑷ 添加定时任务

在线生成Cron表达式:http://cron.ciding.cc/
预约挂号系统技术点详解(二)_第65张图片
com.kejizhentan.task.ScheduledTask

@Component
@EnableScheduling
public class ScheduledTask {

    /**
     * 每隔5秒执行一次
     */
    //@Scheduled(cron = "0 0 1 * * ?")
    @Scheduled(cron = "0/5 * * * * ?")
    public void task1() {
        System.out.println("每隔5秒打印一次");
    }
}

启动项目,效果如下:
预约挂号系统技术点详解(二)_第66张图片

完整代码如下:https://gitee.com/wangren2020/keji-hospital-manager.git

你可能感兴趣的:(spring,spring,boot,java)