# springboot_clock **Repository Path**: huang_siting/springboot_clock ## Basic Information - **Project Name**: springboot_clock - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2021-06-21 - **Last Updated**: 2023-05-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 期末作业:高校健康上报系统设计与实现 `学校`:东莞理工学院 `学院`:网络空间安全学院 `课程名称`:企业级开发框架 `指导老师`:黎志雄 `小组名`:矮冬瓜 `班级`:18软卓1班 `组长`:蔡俊谦(201841404101) `组员`:黄斯婷(201841412105) ## 一、项目介绍 在全国人民共同抗击新型冠状病毒疫情的严峻形势下,为了让高校师生健康信息上报,方便地了解高校师生身体健康情况,做好高校内部的疫情防控工作与管理,很多高校都自建了健康上报系统。 本实验项目作为课程的期末大作业,要求学生组队自主设计与实现一个健康上报系统。 ## 二、人员分工 * 需求分析:蔡俊谦+黄斯婷 * 前台打卡(用户打卡端)和后台管理系统(管理员端):黄斯婷 * 数据库表设计及springboot后端接口开发:蔡俊谦 ## 三、所用核心技术 * 数据库:mysql、redis * 前端:Vue、Echart * 后端:SpringBoot、SpringSecurity、Mybatis-plus * 其它:ActiveMQ、EasyExcel、Jwt ## 四、主要功能模块概述 **用户:** * 普通用户,在用户端进行登录和打卡 **普通管理员端:** * 登录和退出 * 通过文件方式导入学生账号数据 * 按条件查询用户指定日期是否打卡(支持导出数据为Excel) * 可根据用户身体情况分类查看用户的打卡内容(导出数据为Excel) * 图表可视化查看打卡情况 **超级管理员端:** * 拥有普通管理员的所有权限 * 对普通管理员的管理(包括添加禁用) ## 五、难点和亮点 ### 1.前后端通过spring security+jwt进行登录验证和权限控制 有普通用户、管理员和超级管理员三种角色,每个后端接口均有权限控制,利用jwt进行登录和认证信息的传递 利用过滤器进行jwt登录和权限校验 ```java @Component public class JwtTokenCheckFilter extends OncePerRequestFilter { @Autowired private StringRedisTemplate redisTemplate; @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException { res.setContentType("application/json;charset=utf-8"); String uri = req.getRequestURI(); String method = req.getMethod(); //登录请求,直接放行 if ("/user/login".equals(uri) && "POST".equalsIgnoreCase(method)) { filterChain.doFilter(req,res); return; } //第三方认证登录请求回调,直接放行 if ("/login/dgut".equals(uri) && "GET".equalsIgnoreCase(method)) { filterChain.doFilter(req,res); return; } //非登录请求,验证jwt //去掉前缀token String token = req.getHeader(JwtUtils.AUTHENTICATION); if(StringUtils.hasText(token)){ token = token.replace("bearer ", ""); //从redis中获取key,判断有无,无则表示已经登出 Boolean hasKey = redisTemplate.hasKey(JwtUtils.REDIS_PREFIXED + token); if(hasKey==null || !hasKey){ //说明已经登出 //在controller中会抛异常需要捕获,此处无??? String result = new ObjectMapper().writeValueAsString(Result.fail(Result.NOT_AUTH, "请先登录")); PrintWriter writer = res.getWriter(); writer.write(result); writer.flush(); writer.close(); return; } //解析token DecodedJWT decodedJWT=null; try { decodedJWT = JwtUtils.decodeJwt(token); }catch (JWTVerificationException e){ //token验证失败 e.printStackTrace(); ObjectMapper objectMapper = new ObjectMapper(); //在controller中会抛异常需要捕获,此处无??? String result = new ObjectMapper().writeValueAsString(Result.fail(Result.NOT_AUTH, "请先登录")); PrintWriter writer = res.getWriter(); writer.write(result); writer.flush(); writer.close(); return; } //获取用户名和id以及权限 String username = decodedJWT.getClaim("username").asString(); Integer userId = decodedJWT.getClaim("id").asInt(); Integer roleId = decodedJWT.getClaim("role").asInt(); HashMap details = new HashMap<>(); details.put("userId",userId); //把roleId转为SimpleGrantedAuthority ArrayList authorities = new ArrayList<>(); String loginRole = "ROLE_"; if(roleId==0)loginRole += "SUPER"; else if(roleId==1)loginRole += "ADMIN"; else loginRole += "USER"; authorities.add(new SimpleGrantedAuthority(loginRole)); //构造Authentication放入上下标识认证通过 UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, null, authorities); //保存额外的信息 authToken.setDetails(details); SecurityContextHolder.getContext().setAuthentication(authToken); //放行 filterChain.doFilter(req,res); return; } //没有token,返回错误信息 String result = new ObjectMapper().writeValueAsString(Result.fail(Result.NOT_AUTH, "请先登录")); PrintWriter writer = res.getWriter(); writer.write(result); //认证失败,没有token writer.flush(); writer.close(); } } ``` ### 2.通过redis解决jwt token登出问题 因为jwt无状态的原因,需要利用redis存储token信息解决登出问题 **认证成功把token写入redis中** ```java @Bean AuthenticationSuccessHandler authenticationSuccessHandler(){ return (request, response, authentication)->{ response.setContentType("application/json;charset=utf-8"); //获取认证成功后的用户信息,因为在UserDetailsService认证成功后会把userDetail赋值到authentication的principal中 //而在UserDetailsService中使用的是实现了UserDetail接口的User String loginType = request.getParameter(Constant.LOGIN_TYPE); // List permit = new ArrayList<>(); //需要放入jwt中的角色或权限信息 HashMap map = new HashMap<>(); //需要放入jwt中的payload if(Constant.ADMIN_LOGIN.equals(loginType)){ //管理员登录 Admin admin = (Admin)authentication.getPrincipal(); //把admin的角色或权限信息存入jwt中 map.put("id",admin.getId()); map.put("username",admin.getUsername()); map.put("role",admin.getRoleId()); } else{ //普通用户登录 User user = (User)authentication.getPrincipal(); //利用user中的信息,用于生成jwtToken map.put("id",user.getId()); map.put("username",user.getUsername()); map.put("userType",user.getUserType()); map.put("realName",user.getRealName()); map.put("role",user.getRoleId()); } String jwtToken = JwtUtils.createJwtToken(map); //把token存入redis解决登出问题 redisTemplate.opsForValue().set(JwtUtils.REDIS_PREFIXED+jwtToken,"", Duration.ofSeconds(JwtUtils.EXPIRE_TIME)); //把token返回 ObjectMapper om = new ObjectMapper(); String result = om.writeValueAsString(Result.succeed(Result.SUCCESS,"认证成功",jwtToken)); PrintWriter writer = response.getWriter(); writer.write(result); writer.flush(); writer.close(); }; } ``` **登陆出时需要移除token** ```java @RestController @RequestMapping("/user") public class UserController { @Autowired private StringRedisTemplate redisTemplate; @Autowired private UserService userService; @GetMapping("/test") public String test(){ return "123467"; } /** * 自定义了登出路径,原来的默认/logout还是可以访问,只是点击后无效,此路径携带的token已被拦截且验证正确 * @param request * @return */ @GetMapping("/logout") public Result logout(HttpServletRequest request){ //登出时根据请求头中的token删除redis中的key String header = request.getHeader(JwtUtils.AUTHENTICATION); if(StringUtils.hasText(header)){ String jwtToken = header.replaceAll("bearer ", ""); if(StringUtils.hasText(jwtToken)){ redisTemplate.delete(JwtUtils.REDIS_PREFIXED+jwtToken); } } return Result.succeed(200,"登出成功"); } } ``` ### 3.接入莞工中央认证,两套登录逻辑 可通过文件导入学生用户账号数据,也可通过中央认证账号登录 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0621/184148_d5a90ed5_5290920.png "屏幕截图.png") ![输入图片说明](https://images.gitee.com/uploads/images/2021/0621/184202_c90af4e1_5290920.png "屏幕截图.png") 代码实现: ```java @RestController public class OauthController { @Autowired private UserService userService; @Autowired private StringRedisTemplate redisTemplate; @GetMapping("/login/dgut") public Result test(HttpServletRequest request){ final String token = request.getParameter("token"); if(!StringUtils.hasText(token)){ Result.fail(401,"回调请求无token,或token无效"); } RestTemplate template = new RestTemplate(); String getUserInfoURL = "https://cas.dgut.edu.cn/oauth/getUserInfo"; String verifyUrl = "https://cas.dgut.edu.cn/ssoapi/v2/checkToken"; JacksonJsonParser parser = new JacksonJsonParser(); //准备参数 HashMap map = new HashMap<>(); map.put("token",token); map.put("appid","javaee"); map.put("appsecret","b3b52e43ccfd"); //发请求获取access_token Map resMap = parser.parseMap(template.postForObject(verifyUrl,map,String.class)); if(resMap==null || resMap.get("error").equals("1")){ return Result.fail(Result.NOT_AUTH,"token不存在或已过期",null); } //登录成功,根据access_token获取用户信息 map.clear(); map.put("access_token",resMap.get("access_token")); map.put("openid",resMap.get("openid")); resMap = parser.parseMap(template.postForObject(getUserInfoURL,map,String.class)); //判断是否需要新增用户记录 User user = userService.getOne(new LambdaQueryWrapper().eq(User::getUsername, resMap.get("username"))); if(user!=null){ //数据库中已有记录,返回jwt String jwt = generateJwtToken(user); redisTemplate.opsForValue().set(JwtUtils.REDIS_PREFIXED+jwt,"", Duration.ofSeconds(JwtUtils.EXPIRE_TIME)); return Result.succeed(Result.SUCCESS,"登录成功",jwt); } //数据库中没有记录,新增用户信息,返回jwt user = new User(); final String group = (String) resMap.get("group"); int userType = 1; if(!"Student".equals(group))userType=0; //不是学生用户就把userType设置为0 user.setUsername((String) resMap.get("username")) .setRealName((String) resMap.get("name")) .setUserType(userType) .setDepartment((String) resMap.get("faculty_title")) .setRoleId(2) .setEnablePassword(0) .setSchool("DGUT"); userService.save(user); String jwt = generateJwtToken(user); redisTemplate.opsForValue().set(JwtUtils.REDIS_PREFIXED+jwt,"", Duration.ofSeconds(JwtUtils.EXPIRE_TIME)); return Result.succeed(Result.SUCCESS,"初次登录",generateJwtToken(user)); } private String generateJwtToken(User user){ HashMap map = new HashMap<>(); //利用user中的信息,用于生成jwtToken map.put("id",user.getId()); map.put("username",user.getUsername()); map.put("userType",user.getUserType()); map.put("realName",user.getRealName()); map.put("role", user.getRoleId()); return JwtUtils.createJwtToken(map); } } ``` ### 4.支持数据可视化,利用echart生成图表统计 请求后台接口,利用echart渲染数据 **图形展示** ![输入图片说明](https://images.gitee.com/uploads/images/2021/0621/185518_7c8073a7_5290920.png "屏幕截图.png") **SQL语句** ```xml ``` **接口实现** ```java @RestController public class StatController { @Autowired private QuestionnaireService questionnaireService; @PreAuthorize("hasAnyRole('ADMIN','SUPER')") @GetMapping("/stat/ratio") public Result getRatioStat(@RequestParam(value = "date",required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date date){ if(date==null)date = new Date(); java.sql.Date targetDate = new java.sql.Date(date.getTime()); List statRatioDTOS = questionnaireService.selectStatRatio(targetDate); statRatioDTOS.forEach(dto->{ if(dto.getClockNum()==null)dto.setClockNum(0); dto.setRatio(dto.getClockNum()/(double)dto.getAllNum()); }); return Result.succeed(Result.SUCCESS,"查询成功",statRatioDTOS); } @PreAuthorize("hasAnyRole('ADMIN','SUPER')") @GetMapping("/stat/numVary") public Result getNumVaryStat(){ final List statNumVaryDTOS = questionnaireService.selectStatNumVary(); final Calendar calendar = Calendar.getInstance(); final HashSet dateSet = new HashSet<>(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); statNumVaryDTOS.forEach(it->dateSet.add(sdf.format(it.getClockInDate()))); for(int i=0;i<14;i++){ final Date date = calendar.getTime(); if(!dateSet.contains(sdf.format(date))){ statNumVaryDTOS.add(new StatNumVaryDTO().setRiskNum(0).setExceptionNum(0).setClockInDate(date)); } calendar.add(Calendar.DAY_OF_YEAR,-1); } statNumVaryDTOS.sort(Comparator.comparing(StatNumVaryDTO::getClockInDate)); return Result.succeed(Result.SUCCESS,"查询成功",statNumVaryDTOS); } } ``` ### 5.支持导出数据为Excel(前端个别导出和后端分类导出两种形式) #### 1)前端导出: ![输入图片说明](https://images.gitee.com/uploads/images/2021/0702/180739_e601f4db_8355561.png "屏幕截图.png") #### 2)后端导出: ![输入图片说明](https://images.gitee.com/uploads/images/2021/0702/175759_83a6ce43_8355561.png "屏幕截图.png") ![输入图片说明](https://images.gitee.com/uploads/images/2021/0702/180605_18df56ec_8355561.png "屏幕截图.png") ![输入图片说明](https://images.gitee.com/uploads/images/2021/0702/180515_71f64b32_8355561.png "屏幕截图.png") 接口实现: ```java //以一个接口实现为例 @PreAuthorize("hasAnyRole('ADMIN','SUPER')") @GetMapping("/download/quest/risk") public void getRiskQuest(@RequestParam(value = "date",required = false)@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date date, HttpServletResponse response) throws IOException { if(date==null)date = new Date(); java.sql.Date targetDate = new java.sql.Date(date.getTime()); List questionnaires = service.selectRiskQuest(targetDate); response.setContentType("application/vnd.ms-excel"); response.setCharacterEncoding("utf-8"); String fileName = URLEncoder.encode("风险健康上报数据", "UTF-8").replaceAll("\\+", "%20"); // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系 response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); EasyExcel.write(response.getOutputStream(), Questionnaire.class).sheet("风险健康上报数据").doWrite(questionnaires); } ``` ### 6.防止打卡重复提交 三重保障: #### 1)前端用利用js操作隐藏表单提交接口 ```js this.btnDisabled=true; /*提交表单后让按钮失效3s,防止重复提交*/ setTimeout(()=>this.btnDisabled=false,3000); ``` #### 2)后端利用redis存放打卡标志** **打卡前获取上一次打卡的信息,同时获取打卡凭证token,仅支持消费一次** ```java @PreAuthorize("hasRole('USER')") @GetMapping("/quest") public Result getQuest(Principal principal){ Authentication auth = (Authentication) principal; @SuppressWarnings("unchecked") HashMap details = (HashMap) auth.getDetails(); Integer userId = (Integer) details.get("userId"); QueryWrapper questWrapper = new QueryWrapper<>(); questWrapper.eq("user_id",userId) .orderByDesc("clock_in_date") .last("limit 1"); Questionnaire que = service.getOne(questWrapper); if(que!=null){ JacksonJsonParser parser = new JacksonJsonParser(); List address = parser.parseList(que.getAddress()); List dangerZone = parser.parseList(que.getDangerZone()); que.setAddressJson(address); que.setDangerZoneJson(dangerZone); final Date clockInDate = que.getClockInDate(); //上次打卡日期 final Calendar today = Calendar.getInstance(); //今天 today.set(Calendar.HOUR_OF_DAY,0); today.set(Calendar.MINUTE,0); today.set(Calendar.SECOND,0); today.set(Calendar.MILLISECOND,0); if(!clockInDate.equals(today.getTime())){ //表示今天还未曾打卡,在redis中放入key:NRS-timeInMillis-userId,有效期一天 redisTemplate.opsForValue().set(Constant.NON_REPEATED_SUBMIT+today.getTimeInMillis()+"-"+que.getUserId(), "", Duration.ofDays(Constant.NRS_EXPIRE)); } return Result.succeed(Result.SUCCESS,"成功获取上次打卡数据",que); } return Result.succeed(Result.NO_CONTENT,"没有打卡记录"); } ``` **打卡的时候,检测打卡凭证token是否还存在,若存在则进行数据库操作,否则操作失败** ```java private boolean checkRedisNRS(Integer userId){ //构造key final Calendar today = Calendar.getInstance(); today.set(Calendar.HOUR_OF_DAY,0); today.set(Calendar.MINUTE,0); today.set(Calendar.SECOND,0); today.set(Calendar.MILLISECOND,0); String key = Constant.NON_REPEATED_SUBMIT+today.getTimeInMillis()+"-"+userId; if(Boolean.TRUE.equals(redisTemplate.hasKey(key))) return Boolean.TRUE.equals(redisTemplate.delete(key)); return false; } ``` #### 3)mysql数据库建立唯一索引 在mysql中建立(userId,clock_in_date)复合的唯一索引,从数据库层面规避重复提交 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0702/212927_e4c2a5dd_8355561.png "屏幕截图.png") ### 7.高并发设计 **利用消息队列ActiveMQ,把耗时的数据库操作放入消息队列中,然后多线程的方式从数据库中取消息进行打卡操作,前端通过轮询的方式查看打卡结果** > 除此之外可以同时开启多个springboot实例,并利用nginx进行负载均衡转发,还可把mysql做成集群模式应对高并发。 1)controller响应请求,把生成消息放入队列 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0702/213335_386a8895_8355561.png "屏幕截图.png") 2)消费打卡的消息 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0702/214219_d7430eae_8355561.png "屏幕截图.png") 3)前端轮询后端接口查询打卡状态(尝试过websocket,未成功实现) ![输入图片说明](https://images.gitee.com/uploads/images/2021/0702/214402_ab9c810f_8355561.png "屏幕截图.png") ![输入图片说明](https://images.gitee.com/uploads/images/2021/0702/214539_d0ba8036_8355561.png "屏幕截图.png") ### 8.容器化编排部署 > 通过编写Dockerfile和docker-compose.yml文件实现——见标题九:部署 ## 六、项目实例 ### 1.用户端打卡端实例 _访问地址:http://8.129.29.84:8080 测试账号:qqq qqq_ ![输入图片说明](https://images.gitee.com/uploads/images/2021/0630/151113_f8a8cd30_5290920.png "屏幕截图.png") ![输入图片说明](https://images.gitee.com/uploads/images/2021/0630/151133_f050016e_5290920.png "屏幕截图.png") ![输入图片说明](https://images.gitee.com/uploads/images/2021/0630/151159_869af62e_5290920.png "屏幕截图.png") ### 2.管理端后台系统实例 _访问地址:http://8.129.29.84:8081 测试账号:sss sss_ ![输入图片说明](https://images.gitee.com/uploads/images/2021/0630/151259_ad654fce_5290920.png "屏幕截图.png") ![输入图片说明](https://images.gitee.com/uploads/images/2021/0630/151500_6ac72728_5290920.png "屏幕截图.png") ## 七、接口设计 ### 1.统一返回对象 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0702/215716_2df367b5_8355561.png "屏幕截图.png") ### 2.用户端接口: #### 1)用户登录 /user/login POST ``` post请求:(x-wwww-form-urlencoded) username:qqq password:qqq type: 0 (可选值为0表示管理员登录) 响应: { "code": 200, "msg": "认证成功", "data": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLov5nmmK_kuIDkuKpqd3QiLCJwZXJtaXQiOlsidXNlciJdLCJleHAiOjE2MjM0OTUwNDYsImlhdCI6MTYyMzQ5MTQ0NiwidXNlcm5hbWUiOiJxcXEifQ.fsGBUle9oc-67sKXfl7U_VuKLZvCKPgLFvIguFL00r8" } jwt解析得到 { "sub": "这是一个jwt", "permit": ["user"], //代表普通用户 "exp": 1623495046, //过期时间(14天,方便测试) "iat": 1623491446, //签发时间 "id": 1, //用户id "userType": 1, //用户类型 "realName": '张三', "username": "qqq" } 没有登录进行访问会得到 { "code": 401, "msg": "请先登录", "data": null } ``` #### 2)用户登出 user/logout GET ``` get请求 header中加入 Authorization: bearer token (注意:bearer后面有一个空格) 响应: { "code": 200, "msg": "登出成功", "data": null } ``` #### 3)请求打卡凭证和数据 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0702/221146_196c04fb_8355561.png "屏幕截图.png") #### 4)发起打卡请求 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0702/221312_c9331532_8355561.png "屏幕截图.png") #### 5)轮询打卡情况 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0702/221402_3b79ac89_8355561.png "屏幕截图.png") ### 3.管理员端接口: #### 1)打卡查询模块 ```js //获取院系信息 export function getDepartments(token) { return request({ url: "/api/user/departments", method: "get", headers: { Authorization: "bearer " + token, }, }); } //获取专业信息 export function getMajors(token, department) { return request({ url: "/api/user/majors", method: "get", headers: { Authorization: "bearer " + token, }, params: { department, }, }); } //获取班级信息 export function getClass(token, department, major) { return request({ url: "/api/user/className", method: "get", headers: { Authorization: "bearer " + token, }, params: { department, major, }, }); } //管理员查看用户打卡情况 export function clock(token, data, isFinished, pageNum, date) { return request({ url: "/api/clock", method: "post", headers: { Authorization: "bearer " + token, "Content-Type": "application/json;charset=utf-8", }, data: { ...data, }, params: { isFinished, pageNum, pageSize: 10, date, }, }); } //下载打卡的人员名单 export function downloadClockAsExcel(token, data, isFinished, date) { return request({ url: "/api/clock/download", method: "post", headers: { authorization: "bearer " + token, "Content-Type": "application/json;charset=utf-8", }, responseType: "arraybuffer", data: { ...data, }, params: { isFinished, date, }, }); } ``` #### 2)账号管理模块 ```java //文件上传学生账号信息 export function uploadAccount(token, data) { console.log(token); console.dir(data); return request({ url: "/api/upload/account", method: "post", headers: { Authorization: "bearer " + token, "Content-Type": "multipart/form-data", }, data, }); } //查看学生账号信息 export function getAccounts(token, pageNum) { return request({ url: "/api/user/accounts", method: "post", headers: { Authorization: "bearer " + token, "Content-Type": "application/json;charset=utf-8", }, params: { pageNum, pageSize: 10, }, }); } //获取所有普通管理员账号 export function getAllAdmin(token) { return request({ url: "/api/allAdmin", method: "get", headers: { Authorization: "bearer " + token, }, }); } //增加管理员 export function addAdmin(token, username, password) { return request({ url: "/api/admin", method: "post", headers: { Authorization: "bearer " + token, "Content-Type": "application/json;charset=utf-8", }, data: { username, password, }, }); } //禁用或启用管理员权限 export function toggleAdminPower(token, id, status) { return request({ url: "/api/admin/forbid", method: "get", headers: { Authorization: "bearer " + token, }, params: { id, status, }, }); } ``` #### 3)Excel导出模块 ```js //导出所有打卡的数据 export function downloadAllQuest(token, date) { return request({ url: "/api/download/quest/all", method: "get", headers: { authorization: "bearer " + token, "Content-Type": "application/json;charset=utf-8", }, responseType: "arraybuffer", params: { date, }, }); } //导出健康风险数据 export function downloadRiskQuest(token, date) { return request({ url: "/api/download/quest/risk", method: "get", headers: { authorization: "bearer " + token, "Content-Type": "application/json;charset=utf-8", }, responseType: "arraybuffer", params: { date, }, }); } //导出异常数据 export function downloadExceptionQuest(token, date) { return request({ url: "/api/download/quest/exception", method: "get", headers: { authorization: "bearer " + token, "Content-Type": "application/json;charset=utf-8", }, responseType: "arraybuffer", params: { date, }, }); } ``` #### 4)统计信息获取模块 ```js export function getDepartmentRatio(token, date) { return request({ url: "/api/stat/ratio", method: "get", headers: { Authorization: "bearer " + token, }, params: { date, }, }); } export function getDepartmentNumVary(token) { return request({ url: "/api/stat/numVary", method: "get", headers: { Authorization: "bearer " + token, }, }); } ``` > 注:登录和登出与用户端类似 ## 八、项目架构 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0705/002050_91b2194b_8355561.png "屏幕截图.png") ## 九、部署 ### 1.前端项目Nginx部署 依次执行 ``` npm install npm run build ``` 把得到的dist文件数据拷贝到nginx服务器中并配置后端接口的转发代理即可 ### 2.后端docker-compose部署 执行 `mvn package ` 得到的jar包拷贝到服务器中 #### 1)编写Dockerfile ``` FROM openjdk:8-jdk WORKDIR /app_dir ADD clock-in-backend-0.0.1-SNAPSHOT.jar /app_dir/app.jar EXPOSE 8082 ENTRYPOINT ["java","-jar"] CMD ["app.jar"] ``` #### 2)构建docker镜像 在Dockerfile所在目录执行如下命令 `docker build -t clock_in_img:01 .` 同时可以拉取mq和redis的镜像 #### 3)编写docker-compose.yml ```yaml version: "3.6" services: clock_in_backend: # 打卡后端服务名 image: clock_in_img:01 container_name: clock_in_backend #打卡后端容器名 ports: - "8082:8082" networks: - clockin_net depends_on: - redis - activeMQ redis: # redis服务 image: redis:6.2.4-buster container_name: redis ports: - "6379:6379" networks: - clockin_net volumes: - redis_conf:/usr/local/etc/redis # 把redis的配置文件映射到数据卷 command: "redis-server --appendonly yes" # run镜像后用来覆盖默认命令的 activeMQ: # activeMQ服务 image: rmohr/activemq:latest container_name: activeMQ ports: - "61616:61616" - "5672:5672" networks: - clockin_net volumes: redis_conf: #保存redis配置文件的数据卷 networks: clockin_net: # network是key-val形式,val有默认值 ``` 执行在该文件目录下docker-compose up即可 ## 十、快速启动 在下载或拷贝后得到所有镜像 执行在docker-compose.yml目录下执行docker-compose up即可(前端项目并未加入容器) > 注:由于部署遇到问题,且中央认证无法回调地址已被固定,故未把前端项目打包进容器,并且在尝试失败后未能成功把mysql加入容器中编排 ## 总结和致谢 通过这次课程作业,夯实了我们的编程基础,尤其是并发问题重复提交、容器化部署以及前后端的协作和交互,让我们受益匪浅, 这对我们来说不仅仅一次作业,更是一次锻炼和进步的机会, 感谢老师的指导和评阅,感谢小组成员的相互配合。