# guli_parent **Repository Path**: wspApp/guli_parent ## Basic Information - **Project Name**: guli_parent - **Description**: 分布式在线教育项目——后台系统 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2024-10-23 - **Last Updated**: 2024-10-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 工程简介 在线教育系统使用的是B2C模式,分为前台网站系统和后台运营系统。后端使用springcloud微服务架构,持久层使用MybatisPlus。还用到像redis、nginx、阿里云的图像存储、EC2服务器、视频点播、短信服务,还有腾讯的微信支付,微信扫码登录。 后端开发分为9个服务,讲师、课程、课程分类作为教育资源服务、还有讲师头像涉及到的阿里云头像上传服务、课程的章节中小节需要的视频上传服务、统计当天注册人数,新增课程数的图表统计服务、前台网站系统首页轮播图相关的服务、前台用户相关的服务(微信扫码登录的信息也在这个服务进行处理)、用户注册短信验证的发送短信服务、课程支付订单相关的服务、网关是一个服务。 后端总共分为9个服务: - 教育资源服务:讲师、课程、课程分类 - 头像上传服务:讲师头像涉及到的阿里云上传 - 视频上传服务:各个小节的视频上传 - 图表统计服务:统计当天注册人数,新增课程数 - 前台网站首页轮播图相关的服务:前台网站系统首页 - 用户相关的服务:前台网站用户(微信扫码登录的信息也在这个服务进行处理) - 发送短信服务:用户注册短信验证的 - 订单服务:课程支付订单相关的服务 - 网关服务:Api Gateway ``` 在线教育系统分为前台网站系统和后台运营系统,B2C模式。项目前后端分离开发,后端采用 SpringCloud微服务架构。 前台用户系统包括:用户登录注册、首页、课程、名师。 后台管理系统包括:讲师管理、课程分类管理、课程管理、统计分析、Banner管理、订单管理。 ``` # B端 ## 1 service-edu > B端:讲师,课程,课程分类, 章节,小节 ### 讲师 - 根据页号、页大小进行分页查询 ``` Page limitPage=new Page<>(current,limit); teacherService.page(limitPage,null); ``` - 多条件分页查询 创建时间,修改时间,姓名,级别(1 高级讲师,2 首席讲师) ### 课程 - 删除课程 删除小节->章节->课程 - 获得课程最终发布信息(C端) 课程,讲师,章节,小节 - 提交课程发布,修改课程发布状态。(C端) ### 章节 - **课程id**查询所有章节和小节 定义两个新的**vo**类使用**BeanUtils.copyProperties()**进行封装。在每个章节中循环小节,如果小节中的章节id和当前循环章节相等,变封装进当前章节。 - 根据**课程id**删除章节**(课程一删除,所有章节与小节都删除)** - **章节id**删除章节 根据章节id查询小节,如果查到**videoService.count(wrapper)**数量大于0则不能删除 ### 小节(远程调用vod-service) - 删除小节,调用**vod-service**删除视频 ### 课程分类(EasyExcel) - SubjectData(OneSubject、TwoSubject),AnalysisEventListener中invoke方法处理每一行数据。 ### C端相关(远程调用service-order) - 首页数据查询(8条热门课程,4条名师) - 讲师详情页, 讲师id得到讲师详情和课程列表 - 获得课程列表(课程一级分类,课程二级分类,关注度排序,最新排序,价格排序) - 课程详情页。(课程基本信息,课程描述,讲师信息,课程分类信息。章节,小节信息。**订单状态**) **调用service-order服务查询订单状态,判断为:立即观看还是立即支付** ## 2 service-oss > B端:上传讲师头像得到url ``` // 创建OSSClient实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); //返回文件url String url=""; try { InputStream inputStream = file.getInputStream(); //上传初始文件名 String fileName = file.getOriginalFilename(); //获得随机文件名 fileName = UUID.randomUUID().toString().replaceAll("-","")+fileName; //生成时间路径 String datePath=new DateTime().toString("yyyy/MM/dd"); fileName=datePath+fileName; // 创建PutObject请求。 ossClient.putObject(bucketName, fileName, inputStream); //https://guli-edu-20220527.oss-cn-guangzhou.aliyuncs.com/avatar/svg1.jpg url="https://"+bucketName+"."+endpoint+"/"+fileName; } catch (Exception e) { return null; }finally { if (ossClient != null) { ossClient.shutdown(); } return url; } ``` ## 3 service-vod > B端:上传视频返回视频id,删除一个视频,删除多个视频,获得视频播放凭证 上传视频返回视频id ``` String fileName = file.getOriginalFilename(); //title:上传之后显示名称 String title = fileName.substring(0, fileName.lastIndexOf(".")); //inputStream:上传文件输入流 InputStream inputStream = file.getInputStream(); UploadStreamRequest request = new UploadStreamRequest(ConstantVodUtils.ACCESS_KEY_ID, ConstantVodUtils.ACCESS_KEY_SECRET, title, fileName, inputStream); //手动设置存储地址 request.setStorageLocation("outin-452ff6b5df3311ecbfcd00163e021072.oss-cn-shenzhen.aliyuncs.com"); //手动设置服务接入点 request.setApiRegionId("cn-shenzhen"); UploadVideoImpl uploader = new UploadVideoImpl(); UploadStreamResponse response = uploader.uploadStream(request); String videoId = null; if (response.isSuccess()) { videoId = response.getVideoId(); } else { //如果设置回调URL无效,不影响视频上传,可以返回VideoId同时会返回错误码。其他情况上传失败时,VideoId为空,此时需要根据返回错误码分析具体错误原因 videoId = response.getVideoId(); } return videoId; ``` 删除一个视频,删除多个视频 ``` DefaultAcsClient client = InitVodCilent.initVodClient(ConstantVodUtils.ACCESS_KEY_ID,ConstantVodUtils.ACCESS_KEY_SECRET); DeleteVideoRequest request=new DeleteVideoRequest(); request.setVideoIds(id); //删除一个视频 request.setVideoIds(StringUtils.join(ids.toArray(),",")); //删除多个视频,ids为List client.getAcsResponse(request); ``` 获得视频播放凭证 ``` DefaultAcsClient client = null; try { client = InitVodCilent.initVodClient("LTAI5tBUYwEuz4tzRDrXHBxw", "cv0vhb6EOd9mQOhj44NzR1LpmA56Iw"); GetVideoPlayAuthRequest request = new GetVideoPlayAuthRequest(); request.setVideoId(id); GetVideoPlayAuthResponse response = client.getAcsResponse(request); System.out.println("++++++++++++++++++++++++++++++++++++++++++++++++"); System.out.println(response); return CommonResult.ok().data("playAuth",response.getPlayAuth()); } catch (ClientException e) { e.printStackTrace(); throw new GuliException(20001,"获取播放凭证失败"); } ``` ## 4 service-statistics 远程调用**ucenter** > B端:先删除再生成某天注册人数记录(调用service-ucenter模块查询注册人数)。查询一段时间内指定类型的日期和数据列表 ## 5 service-gateway > B端:请求转发(路由,断言),跨域(CorsWebFilter),全局过滤器(GlobalFilter,Ordered)。整合异常josn处理 # C端 ## 1 service-cms(Redis缓存:@Cacheable(value="",key="")) > C/B端:首页banner ## 2 service-msm(Redis设置过期时间) > C端:发送短信(发送前看redis中是否存在,发送后存进redis设置5分钟过期时间) ``` String isSended = (String) redisTemplate.opsForValue().get(phone); String host = "https://dfsns.market.alicloudapi.com"; String path = "/data/send_sms"; String method = "POST"; String appcode = "f6e342906576475091ca9b35fe2e584e"; Map headers = new HashMap(); //最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105 headers.put("Authorization", "APPCODE " + appcode); //根据API的要求,定义相对应的Content-Type headers.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); Map querys = new HashMap(); Map bodys = new HashMap(); bodys.put("content", "code:"+code); bodys.put("phone_number", phone); bodys.put("template_id", "TPL_0000"); try { HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys); String status=response.getStatusLine().toString().split(" ")[1]; if(status.equals("200")){ return true; } return false; } catch (Exception e) { return false; } redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES); ``` ## 3 service-ucenter(Jwt生成token,微信扫码登录) > C端:用户登录(Jwt生成token),注册,解析token jwt生成token 头信息+信息体+防伪标注 用户登录(生成token) ``` 判断用户名,密码是否为空 判断该手机用户是否存在,加密后密码是否正确 判断用户是否禁用 根据用户id,用户名生成token ``` 用户注册 ``` 判断用户名,手机,密码,验证码是否为空 判断验证码是否正确(Redis中) 判断手机号是否已被注册 插入到数据库,包括密码加密,禁用信息,用户默认头像 ``` 微信扫码登录入口 ``` @GetMapping(value = "login") String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" + "?appid=%s" + "&redirect_uri=%s" + "&response_type=code" + "&scope=snsapi_login" + "&state=%s" + "#wechat_redirect"; //对redirect_url进行URLEncoder编码 redirectUrl = URLEncoder.encode("http://localhost:8160/api/ucenter/wx/callback", "utf-8"); //设置%s里面值 String url = String.format( baseUrl, ConstantWxUtils.WX_OPEN_APP_ID, redirectUrl, "atguigu" ); //重定向到请求微信地址里面 return "redirect:"+url; @GetMapping(value = "callback") try{ //1.通过临时凭证code向腾讯发送请求,返回accsess_token 和 openid String baseAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" + "?appid=%s" + "&secret=%s" + "&code=%s" + "&grant_type=authorization_code"; //拼接三个参数 :id 秘钥 和 code值 String accessTokenUrl = String.format( baseAccessTokenUrl, ConstantWxUtils.WX_OPEN_APP_ID, ConstantWxUtils.WX_OPEN_APP_SECRET, code ); String accessTokenInfo = HttpClientUtils.get(accessTokenUrl); //2.利用返回的accessToken再次向腾讯发送请求,获得用户信息 //2.1 解析返回的数据获得access_token,openid Gson gson=new Gson(); HashMap hashMap = gson.fromJson(accessTokenInfo, HashMap.class); String access_token= (String) hashMap.get("access_token"); String openid= (String) hashMap.get("openid"); //3.根据用户openid查询数据库,判断该用户是否存在 Member member=memberService.getOpenIdMember(openid); if(member == null){ //4.不存在利用access_token,openid再次向腾讯发送请求,获得用户信息并将用户信息添加进数据库 String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" + "?access_token=%s" + "&openid=%s"; //拼接两个参数 String userInfoUrl = String.format( baseUserInfoUrl, access_token, openid ); String userInfo = HttpClientUtils.get(userInfoUrl); HashMap userInfoMap = gson.fromJson(userInfo, HashMap.class); String nickname = (String)userInfoMap.get("nickname");//昵称 String headimgurl = (String)userInfoMap.get("headimgurl");//头像 member = new Member(); member.setOpenid(openid); member.setNickname(nickname); member.setAvatar(headimgurl); memberService.save(member); } //5.跳转到C端 String jwtToken = JwtUtils.getJwtToken(member.getId(), member.getNickname()); //最后:返回首页面,通过路径传递token字符串 return "redirect:http://192.168.10.31:3000?token="+jwtToken; }catch (Exception e){ throw new GuliException(20001,"微信登录失败"); } ``` ## 4 service-order(调用edu、ucenter服务、微信支付) > C端:订单 - 通过课程id和token解析出用户id生成订单返回订单编号 ``` //通过课程id和用户id生成订单 @Override public String createOrder(String courseId, String memberId) { //1.通过课程id调用eduservice获得课程信息 CourseWebVoOrder courseInfoOrder = eduClient.getCourseInfoOrder(courseId); //2.通过用户id调用ucenter服务获得用户信息 UcenterMemberOrder userInfoOrder = ucenterClient.getUserInfoOrder(memberId); //3.封装订单信息 Order order = new Order(); order.setOrderNo(OrderNoUtil.getOrderNo());//订单号 order.setCourseId(courseId); //课程id order.setCourseTitle(courseInfoOrder.getTitle()); order.setCourseCover(courseInfoOrder.getCover()); order.setTeacherName(courseInfoOrder.getTeacherName()); order.setTotalFee(courseInfoOrder.getPrice()); order.setMemberId(memberId); order.setMobile(userInfoOrder.getMobile()); order.setNickname(userInfoOrder.getNickname()); order.setStatus(0); //订单状态(0:未支付 1:已支付) order.setPayType(1); //支付类型 ,微信1 baseMapper.insert(order); //4.返回订单号 return order.getOrderNo(); } ``` - 通过订单编号查询订单信息 - 查询订单状态(课程id,用户id) - 生成支付二维码 ``` //1 获取订单信息 QueryWrapper queryWrapper=new QueryWrapper(); queryWrapper.eq("order_no",orderNo); Order order = orderService.getOne(queryWrapper); //2 生成请求参数map Map m = new HashMap(); m.put("appid","wx74862e0dfcf69954"); m.put("mch_id", "1558950191"); m.put("nonce_str", WXPayUtil.generateNonceStr()); m.put("body", order.getCourseTitle()); //课程标题 m.put("out_trade_no", orderNo); //订单号 m.put("total_fee", order.getTotalFee().multiply(new BigDecimal("100")).longValue()+""); m.put("spbill_create_ip", "127.0.0.1"); m.put("notify_url", "http://guli.shop/api/order/weixinPay/weixinNotify\n"); m.put("trade_type", "NATIVE"); //3 发送请求 HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/unifiedorder"); //设置xml格式的参数 client.setXmlParam(WXPayUtil.generateSignedXml(m,"T6m9iK73b0kn9g5v426MKfHQH7X8rKwb")); client.setHttps(true); //执行post请求发送 client.post(); //4 获得请求结果并封装最终成返回结果 //返回内容,是使用xml格式返回 String xml = client.getContent(); //把xml格式转换map集合,把map集合返回 Map resultMap = WXPayUtil.xmlToMap(xml); //最终返回数据 的封装 Map map = new HashMap(); map.put("out_trade_no", orderNo); map.put("course_id", order.getCourseId()); map.put("total_fee", order.getTotalFee()); map.put("result_code", resultMap.get("result_code")); //返回二维码操作状态码 map.put("code_url", resultMap.get("code_url")); //二维码地址 //5 返回最终结果 return map; ``` - 查询支付状态 ``` //1 查询订单状态 Map map=payLogService.queryPayStatus(orderNo); //1、封装参数 Map m = new HashMap<>(); m.put("appid", "wx74862e0dfcf69954"); m.put("mch_id", "1558950191"); m.put("out_trade_no", orderNo); m.put("nonce_str", WXPayUtil.generateNonceStr()); //2 发送httpclient HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/orderquery"); client.setXmlParam(WXPayUtil.generateSignedXml(m,"T6m9iK73b0kn9g5v426MKfHQH7X8rKwb")); client.setHttps(true); client.post(); //3 得到请求返回内容 String xml = client.getContent(); Map resultMap = WXPayUtil.xmlToMap(xml); //6、转成Map再返回 return resultMap; //2 状态为空,返回错误信息 if (map == null) { return CommonResult.error().message("支付出错"); } //3 如果支付成功,新增支付记录,更新订单信息 if(map.get("trade_state").equals("SUCCESS")){ payLogService.updateOrdersStatus(map); //从map获取订单号 String orderNo = map.get("out_trade_no"); //根据订单号查询订单信息 QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("order_no",orderNo); Order order = orderService.getOne(wrapper); //更新订单表订单状态 if(order.getStatus().intValue() == 1) { return; } order.setStatus(1);//1代表已经支付 orderService.updateById(order); //向支付表添加支付记录 PayLog payLog = new PayLog(); payLog.setOrderNo(orderNo); //订单号 payLog.setPayTime(new Date()); //订单完成时间 payLog.setPayType(1);//支付类型 1微信 payLog.setTotalFee(order.getTotalFee());//总金额(分) payLog.setTradeState(map.get("trade_state"));//支付状态 payLog.setTransactionId(map.get("transaction_id")); //流水号 payLog.setAttr(JSONObject.toJSONString(map)); baseMapper.insert(payLog); return CommonResult.ok().message("支付成功"); } //4 否则返回正在支付中 return CommonResult.ok().code(25000).message("支付中"); ``` # 其他 ## 1 nacos ## 2 nginx ## 3 redis # 项目遇到的问题 ## 1 前后端依赖相关 - 前端工程下载依赖报错,发现是node.js版本过高。卸载后重装项目正常运行。 - 后端工程启动报找不到方法或者类(spring中filter的组件、阿里云视频点播服务依赖问题),发现是依赖版本冲突。下载maven helper,剔除冲突版本问题得到解决。 ## 2 前后端报跨域问题 - 路径确实存在跨域 1. 在后端接口controller添加注解:@CrossOrigin 2. 使用网关解决(Spring Cloud alibaba Getway) - 路径写错了 1. 修改正确路径 2. 路径没错,使用了nginx做动态代理,映射存在问题: 修改为正确路径 实现内网穿透(钉钉) - 跨域问题 (1)访问协议,ip地址1) (2)跨域解决:在Controller 添加注解,通过网关解决 ## 3 前后端数据不一致 - 前端工程出错,通过排查发现是数据在返回过程中出现不一致的情况,通过返回的id查询不到数据。排查后发现是传输过程后数据不一致,考虑是类型问题。将实体类字段类型Long修改为String,问题解决。 - mp生成19位id值 mp生成id值是19位,JavaScript处理数字类型值时候只会处理到16位 ## 4 报连接超时,nginx反向代理 - 通过ping发现云服务器可以ping百度,而ping不了我的ip。 1. 发现是由于外网无法直接找到内网 2. 使用花生壳工具实现内网穿透,使Nginx服务器可以访问到edu和oss微服务 ## 5 报注入组件失败: - servoceimpl没有实现service所有接口抛异常,alt+enter快捷键将serviceimpl声明为抽象方法造成。 ## 6 阿里云服务器遭恶意脚本攻击 - 第一次,cpu达到几十,根据csdn查到的操作:查找恶意进程——找到恶意脚本——杀死进程——删除脚本解决 - 第二次,cpu百分百,根本无法运行命令甚至xshell连接不了。只能重置网盘,根据csdn博客的提示——设置reids登录密码。 ## 7 413错误 上传视频时候,因为Nginx有上传文件大小限制,如果超过nginx大小,出现413 ## 8 maven加载xml问题