尚医通(医院预约挂号系统)笔记

全栈项目【尚医通】预约挂号系统项目介绍 尚医通项目是一款医院在线预约挂号项目,使用前后端分离,主要功能如下图所示:管理端主要功能分为数据管理,会员管理,订单管理,医院管理与统计管理五大模块。数据管理模块是对共通数据进行的封装处理,相当于数据字典。会员管理模块是对会员用户的管理,会员权限审批等。订单管理模块是对客户预约挂号订单的管理。医院管理就是管理相关医院的信息,统计管理是对某一时间段中预约挂号流量的统计。 阅读详情

参考资料

对每一个模块流程的具体介绍

业务架构及流程图

在这里插入图片描述
在这里插入图片描述

模块一: 登录系统

1. 手机登录

1.1 业务流程

  • 传入手机号和验证码
  • 校验手机号和验证码是否为空
  • 校验手机验证码和输入的验证码是否一致
    (在点击发送验证码按钮时,调用短信模块,使用阿里云短信实现发送验证码,且保存到redis中)
  • 绑定手机号 (微信登录相关)
  • 判断是否为第一次登录,若是第一次登录,保存信息到数据库
  • 返回登录信息,用户名和token(用JWT生成)

1.2 代码

    public Map<String, Object> loginUser(LoginVo loginVo) {
        //从loginVo获取输入的手机号,和验证码
        String phone = loginVo.getPhone();
        String code = loginVo.getCode();

        //判断手机号和验证码是否为空
        if(StringUtils.isEmpty(phone) || StringUtils.isEmpty(code)) {
            throw new YyghException(ResultCodeEnum.PARAM_ERROR);
        }

        //判断手机验证码和输入的验证码是否一致
        String redisCode = redisTemplate.opsForValue().get(phone);
        if(!code.equals(redisCode)) {
            throw new YyghException(ResultCodeEnum.CODE_ERROR);
        }

        //绑定手机号码
        UserInfo userInfo = null;
        if(!StringUtils.isEmpty(loginVo.getOpenid())) {
            userInfo = this.selectWxInfoOpenId(loginVo.getOpenid());
            if(null != userInfo) {
                userInfo.setPhone(loginVo.getPhone());
                this.updateById(userInfo);
            } else {
                throw new YyghException(ResultCodeEnum.DATA_ERROR);
            }
        }

        //如果userinfo为空,进行正常手机登录
        if(userInfo == null) {
            //判断是否第一次登录:根据手机号查询数据库,如果不存在相同手机号就是第一次登录
            QueryWrapper<UserInfo> wrapper = new QueryWrapper<>();
            wrapper.eq("phone",phone);
            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);

        //jwt生成token字符串
        String token = JwtHelper.createToken(userInfo.getId(), name);
        map.put("token",token);
        return map;
    }

1.3 JWT

在这里插入图片描述
base64编码,并不是加密,只是把明文信息变成了不可见的字符串。但是其实只要用一些工具就可以把base64编码解成明文,所以不要在JWT中放入涉及私密的信息。
由id+name得到token,且能由token反向得到name和id

    private static long tokenExpiration = 24*60*60*1000;
    //签名秘钥
    private static String tokenSignKey = "123456";

    //根据参数生成token
    public static String createToken(Long userId, String userName) {
        String token = Jwts.builder()
                .setSubject("YYGH-USER")
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
                .claim("userId", userId)
                .claim("userName", userName)
                .signWith(SignatureAlgorithm.HS512, tokenSignKey)
                .compressWith(CompressionCodecs.GZIP)
                .compact();
        return token;
    }

2. 微信登陆

2.1 业务流程

  • 打开页面后,出现二维码
    申请二维码过程略,得到id,密钥,域名放到配置文件中;
    写一个接口,返回生成二维码需要的参数
wx.open.app_id=wxed9954c01bb89b47
wx.open.app_secret=a7482517235173ddb4083788de60b90e
wx.open.redirect_url=http://guli.shop/api/ucenter/wx/callback
yygh.baseUrl=http://localhost:3000
  • 扫码,完成后绑定手机号

2.2 代码

生成扫描二维码 (参考文档)

    //1 生成微信扫描二维码
    //返回生成二维码需要参数
    @GetMapping("getLoginParam")
    @ResponseBody
    public Result genQrConnect() {
        try {
            Map<String, Object> map = new HashMap<>();
            map.put("appid", ConstantWxPropertiesUtils.WX_OPEN_APP_ID);
            map.put("scope","snsapi_login");
            String wxOpenRedirectUrl = ConstantWxPropertiesUtils.WX_OPEN_REDIRECT_URL;
            wxOpenRedirectUrl = URLEncoder.encode(wxOpenRedirectUrl, "utf-8");
            map.put("redirect_uri",wxOpenRedirectUrl);
            map.put("state",System.currentTimeMillis()+"");
            return Result.ok(map);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }
    }

回调函数,扫码后的返回内容
扫码后的过程:
在这里插入图片描述
回调方法的逻辑:
在这里插入图片描述
access_token token凭证
openid 微信的唯一id,判断用户是否存在
在第三步完成后,还需要绑定手机号,返回token,跳转到前端页面
跳转前端页面:携带token,openid重定向到前端页面

    //微信扫描后回调的方法
    @GetMapping("callback")
    public String callback(String code,String state) {
        //第一步 获取临时票据 code
        System.out.println("code:"+code);
        //第二步 拿着code和微信id和秘钥,请求微信固定地址 ,得到两个值
        //使用code和appid以及appscrect换取access_token
        //  %s   占位符
        StringBuffer baseAccessTokenUrl = new StringBuffer()
                .append("https://api.weixin.qq.com/sns/oauth2/access_token")
                .append("?appid=%s")
                .append("&secret=%s")
                .append("&code=%s")
                .append("&grant_type=authorization_code");
        String accessTokenUrl = String.format(baseAccessTokenUrl.toString(),
                ConstantWxPropertiesUtils.WX_OPEN_APP_ID,
                ConstantWxPropertiesUtils.WX_OPEN_APP_SECRET,
                code);
        //使用httpclient请求这个地址
        try {
            String accesstokenInfo = HttpClientUtils.get(accessTokenUrl);
            System.out.println("accesstokenInfo:"+accesstokenInfo);
            //从返回字符串获取两个值 openid  和  access_token
            JSONObject jsonObject = JSONObject.parseObject(accesstokenInfo);
            String access_token = jsonObject.getString("access_token");
            String openid = jsonObject.getString("openid");

            //判断数据库是否存在微信的扫描人信息
            //根据openid判断
            UserInfo userInfo = userInfoService.selectWxInfoOpenId(openid);
            if(userInfo == null) { //数据库不存在微信信息
                //第三步 拿着openid  和  access_token请求微信地址,得到扫描人信息
                String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
                        "?access_token=%s" +
                        "&openid=%s";
                String userInfoUrl = String.format(baseUserInfoUrl, access_token, openid);
                String resultInfo = HttpClientUtils.get(userInfoUrl);
                System.out.println("resultInfo:"+resultInfo);
                JSONObject resultUserInfoJson = JSONObject.parseObject(resultInfo);
                //解析用户信息
                //用户昵称
                String nickname = resultUserInfoJson.getString("nickname");
                //用户头像
                String headimgurl = resultUserInfoJson.getString("headimgurl");

                //获取扫描人信息添加数据库
                userInfo = new UserInfo();
                userInfo.setNickName(nickname);
                userInfo.setOpenid(openid);
                userInfo.setStatus(1);
                userInfoService.save(userInfo);
            }
            //返回name和token字符串
            Map<String,String> 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);

            //判断userInfo是否有手机号,如果手机号为空,返回openid
            //如果手机号不为空,返回openid值是空字符串
            //前端判断:如果openid不为空,绑定手机号,如果openid为空,不需要绑定手机号
            if(StringUtils.isEmpty(userInfo.getPhone())) {
                map.put("openid", userInfo.getOpenid());
            } else {
                map.put("openid", "");
            }
            //使用jwt生成token字符串
            String token = JwtHelper.createToken(userInfo.getId(), name);
            map.put("token", token);
            //跳转到前端页面
            return "redirect:" + ConstantWxPropertiesUtils.YYGH_BASE_URL + "/weixin/callback?token="+map.get("token")+ "&openid="+map.get("openid")+"&name="+URLEncoder.encode(map.get("name"),"utf-8");
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

2.3 OAthu2

  • (1) 开放系统间的授权

在这里插入图片描述
在这里插入图片描述
资源拥有者可以访问资源,但是客户应用不能。但是此时客户应用要访问资源,因此拥有者需要给客户应用授权。
那么如何授权呢?
采用颁发令牌的方式。接近OAuth2方式,需要考虑如何管理令牌、颁发令牌、吊销令牌,需要统一的协议,因此就有了OAuth2协议。
在这里插入图片描述

  • (2) 单点登录问题
    什么是单点登录问题?
    一个模块登录后,其他模块也可以访问,不需要再次登录。
    在这里插入图片描述
    解决方法:
    在这里插入图片描述

3. 用户认证与网关整合


把登录校验放到网关里面去。

  • Filter判断哪些需要登录校验,哪些不需要
  • 利用JWT工具从token中拿出id,若不为空,说明已经登录
package com.atguigu.yygh.gateway.filter;

import com.alibaba.fastjson.JSONObject;
import com.atguigu.yygh.common.helper.JwtHelper;
import com.atguigu.yygh.common.result.Result;
import com.atguigu.yygh.common.result.ResultCodeEnum;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * <p>
 * 全局Filter,统一处理会员登录与外部不允许访问的服务
 * </p>
 *  * @author qy
 * @since 2019-11-21
 */
@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;
    }
}

模块二:预约挂号及下单

1. 模块设计

  • 展示科室信息页面
    Feign调用获取就诊人接口,排班下单信息接口

  • 挂号功能
    预约成功后,消息队列发送短信和更新排班数量信息(在短信和排班模块中,添加一个监听器,监听消息队列,有消息就调用方法)

  • 支付功能
    (1)通过appid与商户编号还有密钥,可以生成微信支付二维码
    (2)前端点开预约订单,之后点开支付,前端会传给后端一个订单id,这是订单数据库中的主键,后端根据这个主键,来生成支付二维码,存储支付信息等。controller层接到订单id之后,调用service层的方法
    (3)在service中,首先会在redis中查找是否已经存在该订单id相关的二维码信息,也就是二维码是否已经生成。如果找到了,就直接返回二维码信息就可以了。
    (4)如果用户第一次点开收款码,那么后端就会根据订单id生成微信收款码。首先需要更新收款信息表,先根据订单xid取出订单信息,根据订单信息保存收款信息,将微信支付所需要的参数都封装到map集合中。使用写好的HttpClient工具类向微信发送请求,在这个类中微信返回的数据会使用content参数接收。将微信返回的结果集封装到map中,其中包括订单id,订单金额,订单状态码,以及返回的二维码图片地址。如果微信能够返回状态码的话,说明生成支付二维码成功,将这些信息保存在redis数据库中两个小时,当用户关闭支付页面后,再次点开就不需要再次请求微信了。
    (5)微信支付状态判断,前端在生成二维码之后,会每隔一段时间申请后端的服务器方法,判断支付状态。前端传入订单id,后端根据订单id来申请微信,判断支付状态。后端的controller根据前端传入的订单id,调用service层,接收到一个map集合,如果为null,那么说明二维码已过期,如果map中的支付状态显示SUCCESS,表示已经支付成功,那么就可以根据订单编号修改数据库中的支付状态,返回支付成功。其他情况就是支付中。
    在这里插入图片描述

在这里插入图片描述

  • 微信退款
    **整体介绍:**当用户要取消预约时,除了修改数据库中的信息,如果已经付了款的话,还需要将已经付的款退还给用户,这时候需要用到微信退款功能,如果用到微信退款功能,则需要一个cert证书,这里直接复制的老师,一般学生搞不到这个(300元一年)。
    (1) 前端访问服务器,在controller直接调用业务层,并没有其他的代码,业务层返回一个boolean类型的值,表示删除成功或失败。
    (2) 在service层中,先根据订单id获取订单的全部信息,之后判断是否可以取消订单,如果订单超时会抛异常。
    (3) 之后调用医院接口,将医院方的数据库的订单信息修改,医院方会返回一个状态码,如果为200表示医院方修改成功,那么就可以进行后续操作。
    (4) 如果医院方删除成功,那么就直接调用WeixinService中的退款功能,以及更新订单状态,之后通过消息队列RabbitMQ发送短信。
    (5) 微信退款功能,首先需要根据订单id与支付方式获取到支付相关的信息,之后添加到退款数据库中(这里的添加退款方法中,如果数据已存在,那么就不需要继续添加,返回存在的数据),已退款则直接返回true.未退款则封装数据,发送请求,接收参数等,返回信息,修改数据库
  • 其他一些模块
    用户自己的订单管理,系统的订单管理模块

2. 代码分析及详细流程

在这里插入图片描述

在这里插入图片描述
(项目好像没有提到超时支付的问题)

    //生成挂号订单
    //入参:排班id,就诊人id
    @Override
    public Long saveOrder(String scheduleId, Long patientId) {
        //获取就诊人信息
        Patient patient = patientFeignClient.getPatientOrder(patientId);

        //获取排班相关信息
        ScheduleOrderVo scheduleOrderVo = hospitalFeignClient.getScheduleOrderVo(scheduleId);

        //判断当前时间是否还可以预约
        if(new DateTime(scheduleOrderVo.getStartTime()).isAfterNow()
                || new DateTime(scheduleOrderVo.getEndTime()).isBeforeNow()) {
            throw new YyghException(ResultCodeEnum.TIME_NO);
        }

        //获取签名信息
        SignInfoVo signInfoVo = hospitalFeignClient.getSignInfoVo(scheduleOrderVo.getHoscode());

        //添加到订单表
        OrderInfo orderInfo = new OrderInfo();
        //scheduleOrderVo 数据复制到 orderInfo
        BeanUtils.copyProperties(scheduleOrderVo,orderInfo);
        //向orderInfo设置其他数据
        String outTradeNo = System.currentTimeMillis() + ""+ new Random().nextInt(100);
        orderInfo.setOutTradeNo(outTradeNo);
        orderInfo.setScheduleId(scheduleId);
        orderInfo.setUserId(patient.getUserId());
        orderInfo.setPatientId(patientId);
        orderInfo.setPatientName(patient.getName());
        orderInfo.setPatientPhone(patient.getPhone());
        orderInfo.setOrderStatus(OrderStatusEnum.UNPAID.getStatus());
        baseMapper.insert(orderInfo);

        //调用医院接口,实现预约挂号操作
        //设置调用医院接口需要参数,参数放到map集合
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("hoscode",orderInfo.getHoscode());
        paramMap.put("depcode",orderInfo.getDepcode());
        paramMap.put("hosScheduleId",orderInfo.getScheduleId());
        paramMap.put("reserveDate",new DateTime(orderInfo.getReserveDate()).toString("yyyy-MM-dd"));
        paramMap.put("reserveTime", orderInfo.getReserveTime());
        paramMap.put("amount",orderInfo.getAmount());

        paramMap.put("name", patient.getName());
        paramMap.put("certificatesType",patient.getCertificatesType());
        paramMap.put("certificatesNo", patient.getCertificatesNo());
        paramMap.put("sex",patient.getSex());
        paramMap.put("birthdate", patient.getBirthdate());
        paramMap.put("phone",patient.getPhone());
        paramMap.put("isMarry", patient.getIsMarry());
        paramMap.put("provinceCode",patient.getProvinceCode());
        paramMap.put("cityCode", patient.getCityCode());
        paramMap.put("districtCode",patient.getDistrictCode());
        paramMap.put("address",patient.getAddress());
        //联系人
        paramMap.put("contactsName",patient.getContactsName());
        paramMap.put("contactsCertificatesType", patient.getContactsCertificatesType());
        paramMap.put("contactsCertificatesNo",patient.getContactsCertificatesNo());
        paramMap.put("contactsPhone",patient.getContactsPhone());
        paramMap.put("timestamp", HttpRequestHelper.getTimestamp());

        String sign = HttpRequestHelper.getSign(paramMap, signInfoVo.getSignKey());
        paramMap.put("sign", sign);

        //请求医院系统接口
        JSONObject result = HttpRequestHelper.sendRequest(paramMap, signInfoVo.getApiUrl() + "/order/submitOrder");

        if(result.getInteger("code")==200) {
            JSONObject jsonObject = result.getJSONObject("data");
            //预约记录唯一标识(医院预约记录主键)
            String hosRecordId = jsonObject.getString("hosRecordId");
            //预约序号
            Integer number = jsonObject.getInteger("number");;
            //取号时间
            String fetchTime = jsonObject.getString("fetchTime");;
            //取号地址
            String fetchAddress = jsonObject.getString("fetchAddress");;
            //更新订单
            orderInfo.setHosRecordId(hosRecordId);
            orderInfo.setNumber(number);
            orderInfo.setFetchTime(fetchTime);
            orderInfo.setFetchAddress(fetchAddress);
            baseMapper.updateById(orderInfo);
            //排班可预约数
            Integer reservedNumber = jsonObject.getInteger("reservedNumber");
            //排班剩余预约数
            Integer availableNumber = jsonObject.getInteger("availableNumber");
            //发送mq消息,号源更新和短信通知
            //发送mq信息更新号源
            OrderMqVo orderMqVo = new OrderMqVo();
            orderMqVo.setScheduleId(scheduleId);
            orderMqVo.setReservedNumber(reservedNumber);
            orderMqVo.setAvailableNumber(availableNumber);
            //短信提示
            MsmVo msmVo = new MsmVo();
            msmVo.setPhone(orderInfo.getPatientPhone());
            String reserveDate = new DateTime(orderInfo.getReserveDate()).toString("yyyy-MM-dd") + (orderInfo.getReserveTime()==0 ? "上午" : "下午");
            Map<String,Object> param = new HashMap<String,Object>(){{
                put("title", orderInfo.getHosname()+"|"+orderInfo.getDepname()+"|"+orderInfo.getTitle());
                put("amount", orderInfo.getAmount());
                put("reserveDate", reserveDate);
                put("name", orderInfo.getPatientName());
                put("quitTime", new DateTime(orderInfo.getQuitTime()).toString("yyyy-MM-dd HH:mm"));
            }};
            msmVo.setParam(param);
            orderMqVo.setMsmVo(msmVo);

            rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_ORDER, MqConst.ROUTING_ORDER, orderMqVo);
        } else {
            throw new YyghException(result.getString("message"), ResultCodeEnum.FAIL.getCode());
        }
        return orderInfo.getId();
    }

面试问答QA

0. 介绍

校园医院预约挂号平台是一款互联网在线预约挂号平台,旨在缓解疫情期间学生和教职工看病难、挂号难的就医难题,帮助患者轻松挂号看病。该项目采用了SpringCloud微服务架构和前后端分离技术,主要由后台管理系统和前台用戶系统2大部分组成。我在该项目主要负责了登录模块和预约挂号模块。项目中遇到的最大挑战主要有两点:1是在高并发请求下,保证系统的性能和可扩展性;2是用户认证与网关整合。

1. 预约挂号超抢问题

添加链接描述

  • 解决方案1:(之前就是用的这个)
    将存库从MySQL前移到Redis中,所有的写操作放到内存中,由于Redis中不存在锁故不会出现互相等待,并且由于Redis的写性能和读性能都远高于MySQL,(因为Redis是单线程,可以信任返回结果)这就解决了高并发下的性能问题。然后通过队列等异步手段,将变化的数据异步写入到DB中。
    优点:解决性能问题
    缺点:没有解决超卖问题,同时由于异步写入DB,存在某一时刻DB和Redis中数据不一致的风险。
    找一下解决这个问题的方案
  • 解决方案2:
    引入队列,然后将所有写DB操作在单队列中排队,完全串行处理。当达到库存阀值的时候就不在消费队列,并关闭购买功能。这就解决了超卖问题。
    优点:解决超卖问题,略微提升性能。
    缺点:性能受限于队列处理机处理性能和DB的写入性能中最短的那个,另外多商品同时抢购的时候需要准备多条队列。
  • 解决方案3:
    将提交操作变成两段式,先申请后确认。然后利用Redis的原子自增操作(相比较MySQL的自增来说没有空洞),同时利用Redis的事务特性来发号,保证拿到小于等于库存阀值的号的人都可以成功提交订单。然后数据异步更新到DB中。
    优点:解决超卖问题,库存读写都在内存中,故同时解决性能问题。
    缺点:由于异步写入DB,可能存在数据不一致。另可能存在少买,也就是如果拿到号的人不真正下订单,可能库存减为0,但是订单数并没有达到库存阀值。

2. 超时支付问题

添加链接描述
1.在生成订单时(表中记录订单的创建时间)
2.根据订单的创建时间到30分钟为准,如果订单没有支付
3.将订单生成消息放到死信队列
4.消费死信队列里面的消息: 删除数据库中未付款的订单, 需要去修改商品的库存

如何获取超过限定时间的订单?我们可以使用延迟消息队列(死信队列)来实现。
所谓延迟消息队列,就是消息的生产者发送的消息并不会立刻被消费,而是在设定的时间之后才可以消费。
我们可以在订单创建时发送一个延迟消息,消息为订单号,系统会在限定时间之后取出这个消息,然后查询订单的支付状态,根据结果做出相应的处理。

3. 对登录的理解,为何采用JWT

3.1 登陆的业务逻辑

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

3.2 JWT

  • (1) JWT登录流程
    1、在用户登录网站的时候,需要输入用户名、密码或者短信验证的方式登录,登录请求到达服务端的时候,服务端对账号、密码进行验证,然后计算出 JWT 字符串,返回给客户端。
    2、客户端拿到这个 JWT 字符串后,存储到 cookie 或者 浏览器的 LocalStorage 中。
    3、再次发送请求,比如请求用户设置页面的时候,在 HTTP 请求头中加入 JWT 字符串,或者直接放到请求主体中。
    4、服务端拿到这串 JWT 字符串后,使用 base64的头部和 base64 的载荷部分,通过HMACSHA256算法计算签名部分,比较计算结果和传来的签名部分是否一致,如果一致,说明此次请求没有问题,如果不一致,说明请求过期或者是非法请求。
  • (2)早期的cookie-session认证方式
    流程:
    用户输入用户名、密码或者用短信验证码方式登录系统;
    服务端验证后,创建一个 Session 信息,并且将 SessionID 存到 cookie,发送回浏览器;
    下次客户端再发起请求,自动带上 cookie 信息,服务端通过 cookie 获取 Session 信息进行校验;
    存在的问题:
  1. Cookie-Session 只能在 web 场景下使用,如果是 APP 呢,APP 可没有地方存 cookie。
  2. 跨域问题, 但Cookie 是不能跨域的。拿天猫商城来说,当你进入天猫商城后,会看到顶部有天猫超市、天猫国际、天猫会员这些菜单。而点击这些菜单都会进入不同的域名,不同的域名下的 cookie 都是不一样的,你在 A 域名下是没办法拿到 B 域名的 cookie 的,即使是子域也不行。
  3. 如果是分布式服务,需要考虑 Session 同步问题。

3.3 权限管理

所有请求经过服务网关,服务网关对外暴露接口,网关进行统一用户认证。
Api接口异步请求的,我们采取url规则匹配,如:/api//auth/,如凡是满足该规则的都必须用户认证

3.4 判断用户登录状态

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

3.5 跨域问题

在这里插入图片描述
因为跨域问题是浏览器对于ajax请求的一种安全限制:一个页面发起的ajax请求,只能是与当前页域名相同的路径,这能有效的阻止跨站攻击。
因此:跨域问题 是针对ajax的一种限制。
gateway网关解决了跨域问题。

4. 微服务

4.1 如何划分,搭建

在这里插入图片描述
在这里插入图片描述
微服务写在项目的service里面
公共服务模块:异常结果处理,返回结果处理,rabbitMQ,redis等中间件的配置;
医院服务模块:排班信息,医院信息等
短信服务:发送短信通知患者
用户服务:查看用户信息,绑定个人信息等
订单服务:预约挂号,支付功能等
存储服务:将数据上传到阿里云oss
统计服务:统计预约挂号的数据
定时任务:每天定时提醒就医

4.2 模块间如何调用

通过Feign调用
Feign的原理

5. 消息队列

5.1 哪里用到了消息队列,作用

订单相关操作时,用mq发送短信消息给短信消费者。
如果商品服务和订单服务是两个不同的微服务,在下单的过程中订单服务需要调用商品服务进行扣库存操作。按照传统的方式,下单过程要等到调用完毕之后才能返回下单成功,如果网络产生波动等原因使得商品服务扣库存延迟或者失败,会带来较差的用户体验,使用MQ发送短信

5.2 项目中用到了消息队列的哪种模式?

选择的是Rounting模式。
在这里插入图片描述

6. MongoData对比Mysql,Redis的优缺点

项目的数据库表结构

  • 为什么用MongoData
    使用MongoDb存储一些非关系型的数据,提高访问速度

  • 对比Mysql
    mysql:
    在不同的引擎上有不同的存储方式。
    查询语句是使用传统的sql语句,拥有较为成熟的体系,成熟度很高。
    开源数据库的份额在不断增加,mysql的份额页在持续增长。
    MongoDB
    快速:在适量级的内存的Mongodb的性能是非常迅速的,它将热数据存储在物理内存中,使得热数据的读写变得十分快。
    高扩展性:mongodb的高可用与集群架构拥有十分高的扩展性,通过物理机器的增加,以及sharding的增加,mongodb的扩展将达到一个十分惊人的地步。
    自身的failover机制。Mongodb的副本集配置中,当主库遇到问题,无法继续提供服务的时候,副本集将选举出一个新的主库继续提供服务
    json的存储格式:mongodb的json与bson格式很适合文档格式的存储与查询。

  • 对比Redis
    内存管理
    Redis 数据全部存在内存,定期写入磁盘,当内存不够时,可以选择指定的 LRU 算法删除数据。
    MongoDB 数据会优先存于内存,当内存不够时,只将热点数据放入内存,其他数据存在磁盘。
    需要注意的是Redis 和mongoDB特别消耗内存,一般不建议将它们和别的服务部署在同一台服务器上。
    数据结构
    Redis 支持的数据结构丰富,包括hash、set、list等。
    MongoDB 数据结构比较单一,但是支持丰富的数据表达,索引,最类似关系型数据库,支持的查询语言非常丰富。
    数据量和性能
    当物理内存够用的时候,性能,redis>mongodb>mysql
    数据量,mysql>mongodb>redis
    注意mongodb可以存储文件,适合存放大量的小文件,内置了GirdFS 的分布式文件系统。
    可靠性
    mongodb从1.8版本后,采用binlog方式(MySQL同样采用该方式)支持持久化,增加可靠性;
    Redis依赖快照进行持久化;AOF增强可靠性;增强可靠性的同时,影响访问性能。
    可靠性上MongoDB优于Redis。

7. Redis用在哪里

1)使用Redis作为缓存
2)验证码有效时间、支付二维码有效时间

8. 介绍什么是数据字典,为什么用它

数据字典就是管理系统**常用的分类数据或者一些固定数据,**例如:省市区三级联动数据、民族数据、行业数据、学历数据等,由于该系统大量使用这种数据,所以我们要做一个数据管理方便管理系统数据,一般系统基本都会做数据管理。

9. 阿里云oss

  • OSS作用
    OSS 具有与平台无关的 RESTful API 接口,可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
  • OSS的基本概念
    存储空间(Bucket)
    用于存储对象(Object)的容器,同一个存储空间的内部是扁平的,没有文件系统的目录等概念,所有的对象都必须隶属于某个存储空间。存储空间具有各种配置属性,包括地域、访问权限、存储类型等。可根据实际需求,创建不同存储空间存储不同数据。
    对象/文件(Object)
    是 OSS 存储数据的基本单元,也被称为 OSS 的文件。对象由元信息(Object Meta)、用户数据(Data)和文件名(Key)组成。对象由存储空间内部唯一的 Key 来标识。对象元信息是一组键值对,表示了对象的一些属性,比如最后修改时间、大小等信息,支持在元信息中存储一些自定义的信息。对象的生命周期是从上传成功到被删除为止。
    在这里插入图片描述

10. 项目中用到的哪些定时任务?

定时就医提醒。

  • 业务逻辑:
  1. 写好定时任务:每天早上八点钟向负责定时任务的交换机对应的队列发送消息
@Component
@EnableScheduling
public class ScheduledTask {

    @Autowired
    private RabbitService rabbitService;

    //每天8点执行方法,就医提醒
    //cron表达式,设置执行间隔
    //0 0 8 * * ?
    @Scheduled(cron = "0/30 * * * * ?")
    public void taskPatient() {
        //交换机exchange.direct.task
        //路由key:task.8
        rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_TASK,MqConst.ROUTING_TASK_8,"");
    }
}
  1. 监听对应交换机的消息,调用订单service的方法去消费消息
    RabbitListener用于绑定交换机和队列
    @Autowired
    private OrderService orderService;
    //消费者
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = MqConst.QUEUE_TASK_8, durable = "true"),
            exchange = @Exchange(value = MqConst.EXCHANGE_DIRECT_TASK),
            key = {MqConst.ROUTING_TASK_8}
    ))
    public void patientTips(Message message, Channel channel) throws IOException {
        orderService.patientTips();
    }
  1. 调用patientTip方法去消费消息,消费逻辑:先查数据库,满足时间为当天且状态不为-1的订单;从订单提取手机号等有用信息,封装好消息,发给短信模块的交换机
    public void patientTips() {
        QueryWrapper<OrderInfo> wrapper = new QueryWrapper<>();
        //查数据库 找出当天且订单状态不是-1(-1即取消的)
        wrapper.eq("reserve_date",new DateTime().toString("yyyy-MM-dd"));
        wrapper.ne("order_status",OrderStatusEnum.CANCLE.getStatus());
        List<OrderInfo> orderInfoList = baseMapper.selectList(wrapper);
        for(OrderInfo orderInfo:orderInfoList) {
            //短信提示
            MsmVo msmVo = new MsmVo();
            msmVo.setPhone(orderInfo.getPatientPhone());
            String reserveDate = new DateTime(orderInfo.getReserveDate()).toString("yyyy-MM-dd") + (orderInfo.getReserveTime()==0 ? "上午": "下午");
            Map<String,Object> param = new HashMap<String,Object>(){{
                put("title", orderInfo.getHosname()+"|"+orderInfo.getDepname()+"|"+orderInfo.getTitle());
                put("reserveDate", reserveDate);
                put("name", orderInfo.getPatientName());
            }};
            msmVo.setParam(param);
            // 往短信交换机exchange.direct.msm发信息
            rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_MSM, MqConst.ROUTING_MSM_ITEM, msmVo);
        }
    }

11. 项目中有哪些需要改进的地方

预约挂号也是一种方便患者提前安排就医计划,减小候诊时间,便于医院提升管理水平,提高工作效率和医疗质量,降低医疗安全风险的门诊挂号方式。但是预约患者不可能做到100%应诊。因此研究这些浪费医疗资源的人特征,对医疗资源的合理分配有重要的意义。
那么,关于后台需要建立对用户的数据清洗、数据分析、用户画像分层。
如:中老年患者如约就诊的人数最多,65岁以后的老年患者如约就诊的可能性最高,青少年患者如约就诊的可能性较低。我们可以通过数据的分析避免医疗资源的浪费。

12. 项目业务逻辑

12.1 登录

  • 手机登录
  1. 用户输入手机号,点击发送验证码,调用短信模块,使用阿里云短信发送验证码,保存到redis,设置一个过期时间;
  2. 输入手机号和验证码,进行校验,是否一致;
  3. 如果一致,且是首次登录(通过手机号去查数据库),保存数据到数据库,登录成功;
  • 微信登录业务流程

在这里插入图片描述

  1. 用户扫码请求登录第三方应用
  2. 第三方应用请求微信OAuth2.0授权登录微信开放平台
  3. 用户确认后,微信开放平台拉起第三方应用或者重定向到第三方,携带授权临时票据code
  4. 第三方应用通过code和appid,appsecret(id和密钥是申请微信二维码时提供的,需要事先配置好),请求微信开放平台,返回access token
  5. 拿到access_token后实现登录
    回调函数的处理逻辑:
    使用code和appid以及appscrect(密钥)请求微信提供的地址,获取token凭证和微信id,再拿着token和微信id去请求,返回扫描人的信息,根据微信id判断是否绑定了手机号,若没有绑定进行绑定,保存数据到数据库。
    OAuth
    OAuth(开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容,

12.2 预约挂号

在这里插入图片描述

1.用户登录系统,选择医院科室,判断是否可以预约
2.选择可以预约的科室和就诊人,确认挂号
3.通过RabbitMQ消息队列异步处理,更新号源信息和短信通知
取消预约:
1是没有支付的 直接取消,更新数据库
2 是已经支付的,先退款,后更新数据库

13. 项目中的数据库表介绍

添加链接描述

  • 1.yygh_cmn
    这个数据库就是数据字典部分的数据库,只有一个表那就是dict
  • 2.yygh_hosp (MongoDB)
    该数据库也只有一个表 hospital_set对应实体类为model包下hosp中的hospitalSet
  • 3.yygh_manage (MongoDB)
    hospital_set 医院设置
    schedule 排班表
  • 4.yygh_order (Mysql)
    refund_info 退款信息表
    patment_info
    order_info
  • 5.yygh_user (Mysql)
    patient
    user_info
    user_login_record
尚医通【预约挂号系统】总结 1、项目介绍 尚医通即为网上预约挂号系统,网上预约挂号是近年来开展的一项便民就医服务,旨在缓解看病难、挂号难的就医难题,许多患者为看一次病要跑很多次医院,最终还不一定能保证看得上医生。网上预约挂号全面提供的预约挂号业务从根本上解决了这一就医难题。随时随地轻松挂号! 2、技术点 后端: SpringBoot,SpringCloud,mybatisplus,redis,mongodb,easyExcel,rabbitmq, httpclient,nginx,mysql,oss,vue,微信支付,docker 阅读详情

相关推荐

大数据最新全栈项目【尚医通】预约挂号系统项目介绍_尚医通项目(2),你不懂还不学

是使用前后端分离的一款医院在线预约挂号项目,主要功能如下图所示:​管理端主要功能分为五大模块。数据管理模块是对共通数据进行的封装处理,相当于数据字典。会员管理模块是对会员用户的管理,会员权限审批等。订单管理模块是对客户预约挂号订单的管理。医院管理就是管理相关医院的信息,统计管理是对某一时间段中挂号预约流量的统计。

2401_84182271的博客 1030

尚医通笔记(含bug修改方法)

目录 common模块 swagger2 Result(全局统一返回结果) YyghException(自定义全局异常) GlobalExceptionHandler(全局异常处理器) JwtHelper(生成Token、根据Token获取用户信息) AuthContextHolder(获取用户信息) RedisConfig(配置redis的相关规则) HttpRequestHelper MD5加密 HttpUtil model模块 BaseEntity service 尚医.

LX1232456的博客 1万+

尚医通学习笔记,尚医通学习笔记

尚医通学习笔记

day01 尚医通项目简介+mybatisplus 入门

1 尚医通项目介绍2业务流程3系统架构4 MyBatisPlus学习1、是什么、为什么尚医通即为网上预约挂号系统,网上预约挂号是近年来开展的一项便民就医服务,旨在缓解看病难、挂号难的就医难题,许多患者为看一次病要跑很多次医院,最终还不一定能保证看得上医生。网上预约挂号全面提供的预约挂号业务从根本上解决了这一就医难题。随时随地轻松挂号!不用排长队!1、是什么(简称 MP)是一个MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。Redirect2。

leader_song的博客 1994

尚硅谷-尚医通 笔记代码资料.zip

redis缓存数据,使用MongoDB实现高并发读写,整合消息中间件RabbitMQ提高订单的并发量,同时还整合了定时任务,实现就医提醒功能,综合应用了阿里云OSS

Zynq裸机调试RTL8211FS网卡踩坑记:从ping不通到完美兼容的完整代码修改

本文详细记录了在Zynq平台上使用Xilinx SDK(XSDK)调试RTL8211FS网卡的完整过程,从最初的ping不通问题到最终实现完美兼容的解决方案。通过分析PHY芯片的寄存器配置差异,提供了关键代码修改和调试技巧,帮助开发者快速解决类似问题。

weixin_42550597的博客 419

尚医通项目总结

尚医通项目总结

qq_54549253的博客 1万+

ANSYS Discovery 2023R1 保姆级教程:手把手教你玩转几何模型参数化(附避坑指南)

本文提供ANSYS Discovery 2023R1几何模型参数化的全面教程,涵盖基础环境搭建、核心操作解析及高级应用场景。重点讲解参数化建模中的常见问题与避坑方案,帮助用户高效掌握Workbench与Discovery的协同使用技巧,提升工程仿真效率。

weixin_29204205的博客 752

Day221.尚医通介绍、环境搭建、Gitee使用 -尚医通

尚医通 一、尚医通介绍 1、简介 尚医通即为网上预约挂号系统,网上预约挂号是近年来开展的一项便民就医服务,旨在缓解看病难、挂号难的就医难题,许多患者为看一次病要跑很多次医院,最终还不一定能保证看得上医生。网上预约挂号全面提供的预约挂号业务从根本上解决了这一就医难题。随时随地轻松挂号!不用排长队! 2、 我们能从这个项目中学到什么? 2.1巩固以前知识,学习技术点与技术点应用场景,掌握预约挂号业务流程 2.2 核心技术 3、 业务流程 4、 服务架构 二、预约挂号微服务模块搭建 项

阿昌爱Java 4871

保姆级教程:用高云GW2A开发板搭建RISC-V软核处理器(含DDR3内存优化配置)

本文提供了一份详细的保姆级教程,指导开发者使用高云GW2A-LV18PG256C8 FPGA开发板搭建RISC-V软核处理器系统。教程涵盖了从开发环境搭建、PicoRV32软核集成,到关键的DDR3内存控制器配置与优化,并深入讲解了时序约束与系统调试方法,帮助初学者和爱好者实现从逻辑设计到软硬件协同的深度实践。

2a4s6d8f0g的博客 566

Python自动化批量识别化学分子结构——从图片到SMILES/SDF的实战指南

本文提供了使用Python自动化批量识别化学分子结构图片的实战指南。详细对比了基于Molvec API的在线快速方案与本地部署OSRA工具的两种路径,包含完整的代码示例、环境配置及优化技巧,帮助化学研究者高效地将图片批量转换为SMILES或SDF格式,显著提升数据处理效率。

apple5的专栏 942

尚医通预约挂号平台:基于 Vue 3 生态的医疗预约系统实践

本文介绍了基于Vue3技术栈开发的"尚医通预约挂号平台"学习项目。该项目完整模拟医院查询、预约挂号全流程,采用Vue3+TypeScript+Vite+Pinia等前沿技术,包含医院筛选、科室选择、号源预约、订单管理等核心功能模块。文章重点解析了项目的技术实现亮点:全局路由守卫、Pinia状态管理、Axios请求封装、组件自动导入等工程化实践。该项目可作为Vue3生态的实战范本,适合开发者学习参考,但仅限技术交流使用。

2301_79762459的博客 1148

Win系统蓝牙设备频繁卡顿/断连 - 解决方案

无线网卡有2个模块,一个是WiFi,一个是蓝牙,因此蓝牙问题可能和无线网卡有关。在某些情况下,例如高温或WiFi速率过高,会导致蓝牙频繁卡顿/断连。解决方法分为2种,1是更新驱动,2是更换无线网卡。

NSJim的博客 2万+

尚医通总结(面试模板篇)

尚医通即为网上预约挂号系统,网上预约挂号是近年来开展的一项便民就医服务,旨在缓解看病难、挂号难的就医难题,许多患者为看一次病要跑很多次医院,最终还不一定能保证看得上医生。网上预约挂号全面提供的预约挂号业务从根本上解决了这一就医难题。随时随地轻松挂号!不用排长队!hospital-manage:医院接口模拟端(已开发,直接使用)yygh-parent:根目录,管理子模块:common:公共模块父节点common-util:工具类模块,所有模块都可以依赖于它。

leader_song的博客 2328

工业相机的检测原理课件(建议收藏)

本课件为各位工程师详细介绍了工业相机的视觉检测原理 ,旨在为工程师调试工业相机的视觉检测原理时提供理论性指导。

Bamboo265925的博客 498

沉降预测算法-指数曲线法

沉降预测的指数曲线法原理和代码实现

yh523的博客 1009

Java后端实战项目推荐-尚医通

嗨,大家好,我是代码界的小白,如果喜欢我的文章,可以给个关注、点赞和手操哦,加油! 很多准备找工作的同学都有一个困扰,简历单薄,没有好的项目,担心简历被刷,今天开始后续会给大家推荐一些比较好的开源项目。 虽然大家都知道github或者gitee上都有很多优秀的开源项目,但大多数没有视频教学,对于初学者或者小白来说是很难上手,总不能直接下载下来配置一下环境跑一下就算学习了吧,这显然是不能拿来参加面试的。【当然如果你有基础,建议到github找点开源的项目做做。】 1.项目介绍 该项目是真实存在的项目,本人.

代码届的小白的博客 1万+

利用 SAP Joule 提升 ABAP 开发效率的前提条件与限制全解析

综上,想要在 ABAP Development Tools 中顺利使用 SAP Joule,既要准备好 BTP、IAS、ABAP 系统与 ADT 的版本与信任链,也要理解许可证、配额、语法范围等限制。只有在这些前提全部满足后,Joule 才能真正发挥生成式 AI 的价值,帮助 ABAP 开发者从繁琐模板中解放出来,把注意力投入在业务创新与 Clean Core 改造之上。未来 SAP 还将扩展中文语境、自动修复建议以及跨系统智能代理等功能,值得持续关注。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 661

尚医通(一)

尚医通 一、前置知识 锁 乐观锁 在查询数据的时候不会加锁,在更新数据之前如果有人更新数据,就不会更新数据 实现 取出记录时,获取当前version 更新时,带上这个version 执行更新时, set version = newVersion where version = oldVersion 如果version不对,就更新失败 悲观锁 每次读取数据的时候都默认其他线程会更改数据,因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起 实现 传统的关系型数据库使用这种锁机制,比如行锁,表锁等,读

lovewangyihui的博客 1991

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

1.java项目-尚医通(1)

尚医通项目,第一部分内容,医院设置

hutc_Alan的博客 2372

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

尚医通 (一)项目介绍

尚医通即为网上预约挂号系统,网上预约挂号是近年来开展的一项便民就医服务,旨在缓解看病难、挂号难的就医难题,许多患者为看一次病要跑很多次医院,最终还不一定能保证看得上医生。网上预约挂号全面提供的预约挂号业务从根本上解决了这一就医难题。随时随地轻松挂号!不用排长队!

Knight 2273

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

网络热门编程项目导学:尚医通

鱼皮决定带大家一起学习和鉴定一些网络热门编程项目,希望能帮大家更快地选择适合自己的项目,少走弯路~

努力做最接地气的编程干货分享,感谢关注 916
上一篇: 面经:消息队列
下一篇: 面经:微服务
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值