结对第二次作业——编程实现

这个作业属于哪个课程2023年福大-软件工程实践-W班
这个作业要求在哪里结对第二次作业——编程实现
结对学号<222100213 222100217>
这个作业的目标<实现跳水赛事网站原型设计>

一、各种链接

二、PSP表格

2.1 PSP —— 222100213

PSPPersonal Software Process Stages预估耗时(分钟)实际耗时(分钟)
demand digesting需求理解2025
• understanding• 理解需求2025
discussing结对讨论6070
• talk• 交流6070
new technique learning学习新技术50120
• git learning• 学习git使用1020
• other new skills• 其他新技术40100
coding编码实现700780
• coding• 编码600720
• testing & debuging• 测试 & 改错10060
acquirement收获总结60105
• postmortem• 事后总结1530
• write report• 报告撰写4575
8901100

2.2 PSP —— 222100217

PSPPersonal Software Process Stages预估耗时(分钟)实际耗时(分钟)
demand digesting需求理解2025
• understanding• 理解需求2025
discussing结对讨论6070
• talk• 交流6070
new technique learning学习新技术12001200
• git learning• 学习git使用00
• other new skills• 其他新技术12001200
coding编码实现810730
• coding• 编码750700
• testing & debuging• 测试 & 改错6030
acquirement收获总结90105
• postmortem• 事后总结3030
• write report• 报告撰写6075
21802130

三、成果展示

3.1 主UI

在这里插入图片描述

3.2 每日赛况和赛况详情模块展示

两个功能联系紧密,选择集成
两人交流后发现这两个功能在源网站上是通过点击每日赛况的某个赛况而直接跳到Results页面中的某个锚点,我们觉得这样子较为繁琐,因此将这两个功能集成到一个模块中。
该模块主要功能有,按照日期分类比赛,并按结束时间逆序排序(即最新的比赛放最前面),决赛突出强调,计数,点击可查看某比赛所有参赛选手的得分,基本信息,排名。

模块演示1

3.3 运动员模块展示

按照主国籍次名字排序展示所有运动员
由于每个运动员参加的比赛项目是不同的,所以在Dving这个大模块下给出选手排名是不合理的,因此该模块仅有展示所有运动员这项功能,包括国籍,全名,BOD,性别。很可惜的是由于何某数据库创建时忘记考虑选手头像存储问题,导致后期要添加时间和成本都来不及,因此遗憾取消了该功能。

演示视频2

3.4 国家奖牌榜模块展示

列表式奖牌榜
其实本来想直接整个柱状奖牌榜(横版的那种)的,但是对于陈某来说工期有点赶,要学的东西有点多,因此最终取消了这一计划。

演示视频3

四、结对讨论过程描述

4.1 讨论过程

由于就是对门宿舍的朋友,所以线上讨论记录不多。

前期任务分工
由于何某对后端较为熟悉且打算从事java后端就业,因此准备考研的陈某宽宏大量的选择了两人都较为不熟悉的前端领域(感激!)。由于陈某学习成本与时间较高,因此运维由何某负责。
技术栈考虑
这个其实在原型设计时就已经确定要采用前后端分离开发,前端学习上手快的Vue,后端使用Springboot+mysql,运维使用docker
原型设计
一起做的原型设计,作为后端何某没有太多的提意见,敲定了基本框架后便全权交给陈某进行前端的美化和实践,并在前后端联调时相互提意见(比如何某提出前端页面大背景有些模糊希望更换以及整体表格有点挤,而陈某发现了后端发来的数据的bug并交由何某解决)
讨论主体
即接口文档。开发之前,两人根据源网站的页面进行分析,明确需要的功能与明确前后端之间的数据通信标准(如返回数据格式与需要哪些内容)。比如赛事模块后端返回给前端的数据应该是以日期进行的排序,并且我们讨论后决定逆序展示而不是像源网站一样的正序(即最后的比赛放最前面,感觉比较合理)。但关于前端与后端具体实现方式不过问对方,模拟真实的前后端分离开发,遇到无法解决的问题再一起解决。
讨论截图

在这里插入图片描述

4.2 问题解决

典型案例1
比如我们在第一次前后端联调时发现出现了跨域访问问题,即出现了CORS报错。经过两人交流后定位到错误发生在后端,进而收集资料将Access-Control-Allow-Origin配置成了*来解决这个问题。但当何某将前端项目部署到阿里云服务器上并进行nginx反向代理时发现应该将Access-Control-Allow-Origin设置为前端项目所在的访问路径。
典型案例2
在前端项目部署后,发现前端页面跳转时无法定位到具体页面,即出现404错误。经过两人讨论后定位到错误发生在前端,通过在vue.config.js文件中添加pages映射完成了页面间跳转

五、设计实现过程

5.1 功能结构图

功能结构图

5.2 系统架构

前端 —— Vue + Axios + Nginx
后端 —— Springboot + Mybatis
数据库 —— mysql
运维 —— Docker

系统架构

5.3 前端实现

框架搭建
本次项目中,作为前端的陈某使用的是Vue框架,然而使用Vue脚手架搭建的项目结构只适用于单页面网站,本次作业要求编写的是多页面网站。因此在框架搭建的过程中,需要修改vue.config.js文件中的pages配置项,配置多个EnterPage,才能实现同IP的页面切换。
功能分配
本次项目共需实现四个功能,经过“深思熟虑”,决定将赛程信息和显示比赛结果两个功能整合到一个页面上,另外两个功能则直接分别一个页面。
分割页面
本次项目中,多页面结构意味着组件的分割变得更加棘手。经过观察,陈某发现页面上部均是同一个样式,因此将页面上部独立出来作为一个组件。之后对三个页面进行分析,发现运动员信息和奖牌榜页面均只需一个表格即可,便将这两个页面简单分别分割为两个组件。而对于赛程信息页面,发现页面下部由多个日期及对应日期的赛事组成,因此将该页面的下部做成三重嵌套形式的组件,最外层组件包含多个由日期及对应赛事信息组成的组件,该组件又包含赛事信息组件。
代码实现
确定好组件范围,其实就已经完成了前端页面搭建的50%。通过对Vue的学习,v-for使得表格的实现变得轻而易举,这使得html框架的搭建变得极为节省时间,可谓是“框架五分钟,样式两小时”。当然实现框架只是一个页面最最最基础的要求,所以良好的交互和简洁的数据展示方式才是前端需要注意的重点。在项目中,陈某使用了大量css样式来使得页面具有良好的交互性能,并且整体采用冷色调使得页面简洁明了。

5.4 后端实现

框架搭建
重点是Springboot和Mybatis的整合。这波采用application.yml中配置sql连接配置项,由于Springboot官方有集成Mybatis,且本次没使用MybatisPlus,所以是很简单的。
数据爬取模块
使用jackson第三方库,基于JsonNode类的ReadTree进行Json处理,一直get,get,循环即可
数据持久化
关于数据持久化方式问题,何某进行了充分的考虑。事实上本次作业的数据量并不大且原网站数据接口是十分容易拿到的,直接将json静态数据直接存储在本地其实是很快的。但是基于为之后的团队协作练手考虑(何某和陈某分别为所在团队的前后端组长),何某决定进行网络爬取并转换为数据库。
因此何某经历了数据库分析与设计(充分锻炼了多对多表设计等实际业务难点,获得了规范的数据库设计经验),爬虫代码编写(直接用的Spring自带的RestTemplate类),Json文件处理并调用Mapper接口转存数据库等过程。虽然之后出现了忘记考虑双人比赛而重新设计数据库的难蹦场面,解决后收获满满。
接口开发
普通的数据库多表查询,封装到VO类返回给前端,都是一些规范且重复的内容,没什么好说的。比较值得一提的是Rank的sql函数使用吧,简化了生成rank排名的操作。

六、代码说明

6.1 前端关键代码

1. 框架配置
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  pages: {
    index: {
        entry: './src/main.js', 
        template: './public/index.html', 
        title: '首页' 
    },
    athlete: {
        entry: './src/pages/Athlete/Athlete.js',
        template: './public/athlete.html',
        title: '运动员'
    },
    info: {
        entry: './src/pages/Info/Info.js',
        template: './public/info.html',
        title: '奖牌榜'
    },
}
})
2. 表格实现
//由于本项目中有多个表格,在此仅展示一个表格的html框架及样式
<div class="medal-list center clearfix">
        <div class="list-header header">
            <span class="column-rank medal-header arial text-center">Ranks</span>
            <span class="column-coun medal-header arial">Country</span>
            <span class="column-gold medal-header arial text-center">Gold</span>
            <span class="column-silv medal-header arial text-center">Silver</span>
            <span class="column-bron medal-header arial text-center">Bronze</span>
            <span class="column-total medal-header arial text-center">Total</span>
        </div>
        <div class="list-item item" v-for="(medal,index) in medals" :key="index">
            <span class="column-rank medal-item arial text-center">{{medal.rank}}</span>
            <span class="column-coun medal-item arial">{{medal.name}}</span>
            <span class="column-gold medal-item arial text-center">{{medal.gold}}</span>
            <span class="column-silv medal-item arial text-center">{{medal.silver}}</span>
            <span class="column-bron medal-item arial text-center">{{medal.bronze}}</span>
            <span class="column-total medal-item arial text-center">{{medal.total}}</span>
        </div>
    </div>
.medal-list{
        width: 90%;
        padding: 100px 0 0 0;
    }
    .list-header{
        width: 100%;
    }
    .list-item{
        width: 100%;
    }
    .list-item:hover{
        box-shadow: 0 0 5px #d3d2d2;
        transition: 500ms;
    }
    .medal-header{
        height: 66px;
        line-height: 66px;
        font-size: 24px;
        border-bottom: 3px solid #ccc;
        color: #5f5e5e;
        opacity: 1;
    }
    .medal-item{
        height:100px;
        line-height: 100px;
        font-size: 20px;
        border-bottom: 1px solid #ccc;
        opacity: 0.5;
    }
    .medal-list span{
        display: inline-block;
    }
    .column-rank{
        width: 10%;
    }
    .column-coun{
        width: 24%;
        padding: 0 3%;
    }
    .column-gold{
        width: 15%;
        background: linear-gradient(to right, rgb(234, 225, 130), rgb(243, 192, 27));
    }
    .column-silv{
        width: 15%;
        background: linear-gradient(to right, rgb(212, 211, 211), rgb(144, 144, 143));
    }
    .column-bron{
        width: 15%;
        background: linear-gradient(to right, rgb(212, 189, 135), rgba(169, 140, 74));
    }
    .column-total{
        width: 15%;
    }
3. 点击赛程item显示对应信息
<div class="competition-item">
        <button 
            class="item-btn clearfix" 
            @click="showMessage" 
            @mouseover="showBtn = !showBtn" 
            @mouseout="showBtn = !showBtn">
            <div v-show="!showBtn" class="item-mess">
                <span class="item-time leftfix arial">
                    <span class="time-left center">{{correctTime.split(' ')[0]}}</span>
                    <span class="time-right center">{{correctTime.split(' ')[1]}}</span>
                </span>
                <span class="item-image leftfix"></span>
                <span class="item-type leftfix arial">{{ competition.type }}</span> 
                <span class="item-phase rightfix arial" 
                    :style="{color : this.competition.phaseName === 'Final' ? 'rgb(255,184,25)': '#635c5c',
                             'font-size' :this.competition.phaseName === 'Final'? '35px': '25px'}">
                    {{competition.phaseName}}
                    <span v-show="this.competition.phaseName === 'Final'" class="item-gold rightfix"></span>
                </span>
            </div>
            <div v-show="showBtn" class="item-show-more">
                <span class="show-more-text center arial">点击查看详情</span>
            </div>
        </button>
        <CompetitionDetail v-show="showTable" :athletes="athletes" />
    </div>
import CompetitionDetail from './CompetitionDetail.vue'

export default {
    name:'CompetitionItem',
    components:{CompetitionDetail,},
    data(){
        return {
            athletes:null,
            showBtn:false,
            showTable:false,
        }
    },
    props:['competition',],
    computed:{
        correctTime(){
            let time = this.competition.endTime.split('T')[1].split(':');
            if(parseInt(time[0]) > 12){
                return '' + (parseInt(time[0]) - 12) + ':' + time[1] + ' PM';
            }
            return time[0] + ':' + time[1] + ' AM';
        }
    },
    methods:{
        showMessage(){
            if(this.athletes === null)
                this.$axios.get('/competition/'+this.competition.id).then(res => {
                    this.athletes = res.data.data.results
                })
            this.showTable = !this.showTable
        }
    },
}
4. 公用css样式
 body{
    height: auto;
  }
  /* public css style */
  /* float style */
  a{
    text-decoration: none;
    color: black;
  }
  .leftfix{
    float: left;
  }
  .rightfix{
    float: right;
  }
  .clearfix{
    content:'';
    display:block;
    clear:both;
  }
  /* position style */
  .center{
    margin:0 auto;
  }
  /* muti style */
  .container{
    width: 1690px;
    margin: 0 auto;
  }
  /* font style */
  .arial{
    font-family: Arial, sans-serif;
  }
  .blod{
    font-weight: blod;
  }
  .text-center{
    text-align: center;
  }

6.2 后端关键代码

爬虫代码
public void updateAthletes() {
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
        String data = ReptileUtils.getJson(ATHLETES_URL);
        ObjectMapper mapper = new ObjectMapper();
        try {
            JsonNode countries = mapper.readTree(data);
            for (JsonNode country : countries) {
                //获取国家名并尝试获取id,没有则插入并获取新ID
                Country newCountry = Country.builder()
                        .name(country.get("CountryName").asText()).build();
                List<Country> result1 = countryMapper.selectByName(newCountry.getName());
                if (result1.isEmpty()) {
                    countryMapper.insert(newCountry);
                    log.info("插入了:" + newCountry);
                } else {
                    newCountry.setId(result1.get(0).getId());
                }

                //解析并插入运动员信息
                JsonNode participations = country.get("Participations");
                for (JsonNode participation : participations) {
                    LocalDateTime dob = LocalDateTime.parse(participation.get("DOB").asText(), dateTimeFormatter);
                    Athlete athlete = Athlete.builder()
                            .gender(participation.get("Gender").asText())
                            .fullName(participation.get("PreferredLastName").asText() + " " + participation.get("PreferredFirstName").asText())
                            .dateOfBirth(dob)
                            .countryId(newCountry.getId())
                            .build();
                    log.info("插入了:" + athlete);
                    athleteMapper.insert(athlete);
                }
            }

        } catch (JsonProcessingException e) {
            log.error(e.getMessage());
        }
    }

    public void updateCompetition(String url) {
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
        String data = ReptileUtils.getJson(url);
        ObjectMapper mapper = new ObjectMapper();
        try {
            //解析赛事信息并查找数据库中是否有该赛事
            JsonNode competition = mapper.readTree(data);
            String type = competition.get("DisciplineName").asText();
            String gender = competition.get("Gender").asText();
            for (JsonNode heat : competition.get("Heats")) {
                LocalDateTime endTime = LocalDateTime.parse(heat.get("EndUtcDateTime").asText(), dateTimeFormatter);
                String phaseName = heat.get("Name").asText();
                Competition newCompetition = Competition.builder()
                        .type(type)
                        .gender(gender.equals("Women") ? "1" : "0")
                        .endTime(endTime)
                        .phaseName(phaseName)
                        .build();
                List<Competition> result1 = competitionMapper.select(newCompetition);
                if (result1.isEmpty()) {
                    competitionMapper.insert(newCompetition);
//                    log.info("插入数据:"+newCompetition);
                } else {
                    newCompetition.setId(result1.get(0).getId());
                }

                //解析赛事结果并录入数据库
                for (JsonNode result : heat.get("Results")) {
                    String fullName = result.get("FullName").asText();
                    String[] split = fullName.split(" / ");
                    for (String s : split) {
                        Athlete build = Athlete.builder()
                                .fullName(s)
                                .build();
                        List<Athlete> result2 = athleteMapper.select(build);
                        if (result2.isEmpty()) {
                            log.error("数据库中没有名为:" + build.getFullName() + "的运动员");
                            continue;
                        } else {
                            CompetitionResult newCompetitionResult = CompetitionResult.builder()
                                    .competitionId(newCompetition.getId())
                                    .athleteId(result2.get(0).getId())
                                    .points(BigDecimal.valueOf(result.get("TotalPoints").asDouble()))
                                    .build();
                            competitionResultMapper.insert(newCompetitionResult);
//                            log.info("插入数据:"+newCompetitionResult);
                        }
                    }
                }
            }
        } catch (JsonProcessingException e) {
            log.error(e.getMessage());
        }
    }

    public void updateCountryModels() {
        String data = ReptileUtils.getJson(MEDALS_URL);
        ObjectMapper mapper = new ObjectMapper();
        try {
            JsonNode root = mapper.readTree(data);
            JsonNode sportMedals = root.get("Medals").get("SportMedals");
            JsonNode DVModels = sportMedals.get(2);
            for (JsonNode country : DVModels.get("Countries")) {
                String countryName = country.get("CountryName").asText();
                Integer gold = country.get("Gold").get("Count").asInt();
                Integer silver = country.get("Silver").get("Count").asInt();
                Integer bronze = country.get("Bronze").get("Count").asInt();
                Country build = Country.builder()
                        .gold(gold)
                        .name(countryName)
                        .silver(silver)
                        .bronze(bronze)
                        .build();
                countryMapper.updateByName(build);
            }
        } catch (
                JsonProcessingException e) {
            log.error(e.getMessage());
        }
    }
根据ID返回比赛的具体结果
public CompetitionResultVO queryDetail(Long id){
        //拷贝初始数据
        Competition competition = competitionMapper.selectById(id);
        CompetitionResultVO competitionResultVO = new CompetitionResultVO();
        competitionResultVO.setResults(new ArrayList<>());
        competition.setEndTime(competition.getEndTime().plusHours(1));
        BeanUtils.copyProperties(competition,competitionResultVO);
        competitionResultVO.setEndTime(competitionResultVO.getEndTime().plusHours(1));

        //构造List<ResultPointsVO>
        //查找比赛结果
        CompetitionResult build = CompetitionResult.builder()
                .competitionId(competition.getId())
                .build();
        List<CompetitionResult> results = competitionResultMapper.select(build);

        //转换结果中的选手信息
        if(Objects.equals(results.get(0).getPoints(), results.get(1).getPoints())){
            //双人比赛
            for (int i = 0; i < results.size(); i+=2) {
                AthleteVO athleteVO1 = athleteService.queryById(results.get(i).getAthleteId());
                AthleteVO athleteVO2 = athleteService.queryById(results.get(i+1).getAthleteId());
                ArrayList<AthleteVO> athleteVOS = new ArrayList<>();
                athleteVOS.add(athleteVO1);
                athleteVOS.add(athleteVO2);
                competitionResultVO.getResults().add(new ResultPointsVO(results.get(i).getRank()/2+1,athleteVOS,results.get(i).getPoints()));
            }
        }else {
            for (CompetitionResult result : results) {
                AthleteVO athleteVO1 = athleteService.queryById(result.getAthleteId());
                ArrayList<AthleteVO> athleteVOS = new ArrayList<>();
                athleteVOS.add(athleteVO1);
                competitionResultVO.getResults().add(new ResultPointsVO(result.getRank(), athleteVOS, result.getPoints()));
            }
        }

        return competitionResultVO;
    }
前后端联调统一数据类
@Data
public class Result<T> implements Serializable {

    private Integer code; //编码:1成功,0和其它数字为失败
    private String msg; //错误信息
    private T data; //数据

    public static <T> Result<T> success() {
        Result<T> result = new Result<T>();
        result.code = 1;
        return result;
    }

    public static <T> Result<T> success(T object) {
        Result<T> result = new Result<T>();
        result.data = object;
        result.code = 1;
        return result;
    }

    public static <T> Result<T> error(String msg) {
        Result result = new Result();
        result.msg = msg;
        result.code = 0;
        return result;
    }

}

6.3 运维代码

docker run -d \
    --name diving-frontend \
    -p 80:80 \
    -v /root/diving/nginx/html:/usr/share/nginx/html \
    -v /root/diving/nginx/nginx.conf:/etc/nginx/nginx.conf \
    nginx

docker build -t diving-backend .

docker run -d \
    --name diving-backend \
    -p 8080:8080 \
    diving-backend

6.4 代码规范遵守展示

命名规范遵守展示

在这里插入图片描述

数据库建表规范遵守展示

在这里插入图片描述

七、结对感受、收获与评价

7.1 感受与收获

222100213
本次作业与陈某一拍即合,实现了一次完全规范的前后端分离开发,充分地信任对方并在不断的沟通中完成了几乎完美的前后端联调。本次作业结束后,我在数据库设计,表现层业务层持久层间开发规范和调用逻辑,项目部署服务器,git使用与团队合作等方面都有了很大的经验积累。
222100217
本次作业我学习了Vue框架并且使用Vu而完成了这次前端项目的搭建,在学习Vue的过程中,我发现了前端框架和后端框架相似的部分,对框架设计原理的理解有所提升,当然,学习之路仍道阻且长

7.2 结对评价

222100213 to 222100217
陈某做事有拼劲,肯学肯干,遇到问题有很强的主动学习能力,对开发抱有自己的审美和原则。在团队协作上听得进队友的意见,遇到要改的地方也会从善如流,简直堪称团队合作者中的t1级别。我只能说cyz yyds!!!
222100217 to 222100213
何某富有责任心,技术力强,数据接口简直信手拈来;对前后端技术均有所涉猎,因此在搭建过程中,总能够为我提出KeyIdea。总之,何某简直是全知全能的神啊!!!!!
  • 6
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值