一、各种链接
二、PSP表格
2.1 PSP —— 222100213
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|
demand digesting | 需求理解 | 20 | 25 |
• understanding | • 理解需求 | 20 | 25 |
discussing | 结对讨论 | 60 | 70 |
• talk | • 交流 | 60 | 70 |
new technique learning | 学习新技术 | 50 | 120 |
• git learning | • 学习git使用 | 10 | 20 |
• other new skills | • 其他新技术 | 40 | 100 |
coding | 编码实现 | 700 | 780 |
• coding | • 编码 | 600 | 720 |
• testing & debuging | • 测试 & 改错 | 100 | 60 |
acquirement | 收获总结 | 60 | 105 |
• postmortem | • 事后总结 | 15 | 30 |
• write report | • 报告撰写 | 45 | 75 |
| | 890 | 1100 |
2.2 PSP —— 222100217
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|
demand digesting | 需求理解 | 20 | 25 |
• understanding | • 理解需求 | 20 | 25 |
discussing | 结对讨论 | 60 | 70 |
• talk | • 交流 | 60 | 70 |
new technique learning | 学习新技术 | 1200 | 1200 |
• git learning | • 学习git使用 | 0 | 0 |
• other new skills | • 其他新技术 | 1200 | 1200 |
coding | 编码实现 | 810 | 730 |
• coding | • 编码 | 750 | 700 |
• testing & debuging | • 测试 & 改错 | 60 | 30 |
acquirement | 收获总结 | 90 | 105 |
• postmortem | • 事后总结 | 30 | 30 |
• write report | • 报告撰写 | 60 | 75 |
| | 2180 | 2130 |
三、成果展示
3.1 主UI
3.2 每日赛况和赛况详情模块展示
两个功能联系紧密,选择集成 两人交流后发现这两个功能在源网站上是通过点击每日赛况的某个赛况而直接跳到Results页面中的某个锚点,我们觉得这样子较为繁琐,因此将这两个功能集成到一个模块中。 该模块主要功能有,按照日期分类比赛,并按结束时间逆序排序(即最新的比赛放最前面),决赛突出强调,计数,点击可查看某比赛所有参赛选手的得分,基本信息,排名。 |
3.3 运动员模块展示
按照主国籍次名字排序展示所有运动员 由于每个运动员参加的比赛项目是不同的,所以在Dving这个大模块下给出选手排名是不合理的,因此该模块仅有展示所有运动员这项功能,包括国籍,全名,BOD,性别。很可惜的是由于何某数据库创建时忘记考虑选手头像存储问题,导致后期要添加时间和成本都来不及,因此遗憾取消了该功能。 |
3.4 国家奖牌榜模块展示
列表式奖牌榜 其实本来想直接整个柱状奖牌榜(横版的那种)的,但是对于陈某来说工期有点赶,要学的东西有点多,因此最终取消了这一计划。 |
四、结对讨论过程描述
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 前端关键代码
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: '奖牌榜'
},
}
})
//由于本项目中有多个表格,在此仅展示一个表格的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%;
}
<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
}
},
}
body{
height: auto;
}
a{
text-decoration: none;
color: black;
}
.leftfix{
float: left;
}
.rightfix{
float: right;
}
.clearfix{
content:'';
display:block;
clear:both;
}
.center{
margin:0 auto;
}
.container{
width: 1690px;
margin: 0 auto;
}
.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) {
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);
} 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);
}
}
}
}
} 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());
}
}
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));
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;
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。总之,何某简直是全知全能的神啊!!!!! |