这个作业属于哪个课程 | 软件工程实践-2023 学年-W 班 |
---|---|
这个作业要求在哪里 | 结对第二次作业–编程实现 |
结对学号 | 132101256 222100234 |
这个作业的目标 | 完成 DWASearch 的原型设计 |
其他参考文献 | 构建之法、CSDN |
一、Gitcode 仓库地址、代码规范、项目地址链接
二、PSP 表格
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 40 | 30 |
• Estimate | • 估计这个任务需要多少时间 | 30 | 40 |
Development | 开发 | 1000 | 1200 |
• Analysis | • 需求分析 (包括学习新技术) | 600 | 650 |
• Design Spec | • 生成设计文档 | 20 | 30 |
• Design Review | • 设计复审 | 20 | 30 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 10 | 20 |
• Design | • 具体设计 | 60 | 70 |
• Coding | • 具体编码 | 500 | 600 |
• Code Review | • 代码复审 | 20 | 30 |
• Test | • 测试(自我测试,修改代码,提交修改) | 110 | 120 |
Reporting | 报告 | 30 | 40 |
• Test Repor | • 测试报告 | 20 | 20 |
• Size Measurement | • 计算工作量 | 10 | 10 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 20 | 20 |
合计 | 1490 | 1710 |
三、成果展示
1.主页
2.每日赛程
3.选手排名
4.详细赛况
5.奖牌榜
四、结对讨论过程描述
前期准备:因为 132101256 负责后端开发、项目部署与博客编写,222100234 负责前端页面设计编码
各自技术栈:
前端 vue
后端 SpringBoot、Mysql、MyBatisPlus。
讨论主要工作
开发前明确所需的数据格式与所需内容,并统一了接口文档,开发过程相对顺利。
五、设计实现过程
5.1 功能结构图
5.2 前端实现
前端采用vue 框架制作
使用axios发送数据请求
5.2.1 页面设计
页面设计上总体参照上次作业的原型设计,在具体数据展示部分,由于时间没安排好,所以没有写css,后续会补上
5.2.2 前后端联调
通过axios向服务器发送请求,得到json文件。并将其装入表格中呈现
5.3 后端实现
后端采用 SpringBoot 框架制作、RestFul 架构风格获取操作资源、 MybatisPlus操作数据库,maven 进行项目管理,项目主要由五个模块组成:
config:管理配置文件
service:负责业务逻辑实现
entity:用于存放实体类
dao:负责实体类之间的映射关系与数据持久化
controller:包括数据库初始化操作以及负责响应前端请求并返回数据。
5.3.1 数据爬取
与网站建立 http 连接,采用 fastjson 依赖库提供的组件,不断迭代解析 jsonObject 与 jsonArray 对象提取需要的信息保存到实体类中
5.3.2 数据持久化
通过 MybatisPlus 提供的注解式开发组件,将对象存储至数据库表中,通过@Results 组件实现多表查询操作
5.3.3 接口实现
采用 RestFul 风格响应请求并从数据库中查询数据返回给前端,统一接口文档由 swagger 生成。
六、主要代码说明
###6.1 前端
轮播图
<div class="lunbo">
<div class="slide" v-for="(image, index) in images"
:key="index"
:style="{ backgroundImage: 'url(' + image + ')' }"
:class="{ active: index === currentIndex }">
</div>
.slide {
z-index: 3;
position: absolute;
left: 72px;
top: 210px;
width: 1058px;
height: 420px;
border-radius: 40px;
opacity: 0;
transition: opacity 0.5s;
}
.slide.active {
opacity: 1;
}
.lcontrol {
z-index: 3;
position: absolute;
top: 400px;
left: 72px;
display: flex;
justify-content: space-between;
font-size: 24px;
background-color: rgb(1, 1, 1,0);
border: none;
font-size: 50px;
}
.rcontrol{
z-index: 3;
position: absolute;
top: 400px;
left: 1080px;
display: flex;
justify-content: space-between;
font-size: 24px;
background-color: rgb(1, 1, 1,0);
border: none;
font-size: 50px;
}
data() {
return {
images: [
require('../components/lunbo1.png'),
require('../components/lunbo2.png'),
require('../components/lunbo3.png'),
],
currentIndex: 0
};
},
methods:{
prevSlide() {
this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length;
},
nextSlide() {
this.currentIndex = (this.currentIndex + 1) % this.images.length;
}
}
导航栏
<div id="navi">
<button class="ex" @click="toMedal"> 奖牌榜</button>
<img id="logo" src="../components/logo.png" alt="logo">
<div class="naviRec"></div>
<div class="naviBtn">
<button @click="toIndex" class="cur">首页</button>
<button @click="toRank" class="others">选手排名</button>
<button @click="toData" class="others">每日赛程</button>
<button @click="toDetail" class="others">详细赛况</button>
</div>
</div>
.ex{
position: absolute;
left: 700px;
top: 5px;
border: none;
background-color: white;
width: 200px;
height: 121px;
font-size: 32px;
text-align: center;
color: rgba(65, 80, 88, 1);
border-radius: 100%;
}
.naviRec{
position: absolute;
left: 920px;
top: 0;
width: 100px;
height: 133px;
background-color: white;
border-bottom-left-radius:80px;
}
.naviBtn{
position: absolute;
left: 1003px;
top: 0;
width: 831px;
height: 133px;
background-color: white;
border: none;
}
.cur{
width: 200px;;
height:121px;
font-size: 36px;
text-align: center;
color: rgba(22, 132, 252, 1);
background-color: white;
border: none;
}
.others{
width: 200px;
height: 121px;
font-size: 32px;
text-align: center;
color: rgba(65, 80, 88, 1);
background-color: white;
border: none;
}
#logo{
position: absolute;
left: 21px;
top: 0px;
width: 454px;
height: 121px;
border-top-left-radius: 40px;
border-bottom-right-radius: 80px;
}
#navi{
position: absolute;
z-index: 2;
left: 42px;
top: 0px;
width: 1834px;
height: 133px;
}
methods: {
toData() {
this.$router.push('/Data')
},
toIndex() {
this.$router.push('/')
},
toDetail() {
this.$router.push('/Detail')
},
toRank() {
this.$router.push('/Rank')
},
toMedal() {
this.$router.push('/medals')
}
}
展示按钮&展示框&下拉菜单
<div id="combo">
<select v-model="opt" class="data">
<option class="init" value="init" disabled>选择日期</option>
<option class="choice" value="18">Thursday 18th</option>
<option class="choice" value="19">Friday 19th</option>
<option class="choice" value="20">Saturday 20th</option>
<option class="choice" value="21">Sunday 21st</option>
</select>
<label class="month">January</label>
</div>
<div id="rec">
<div id="show"></div>
</div>
<button class="show" @click="clickTest">展示</button>
#show{
position: absolute;
top:200px;
font-size: 50px;
}
.show{
position: absolute;
left: 500px;
top: 5px;
border: none;
background-color: white;
width: 200px;
height: 121px;
font-size: 32px;
text-align: center;
color: rgba(65, 80, 88, 1);
border-radius: 100%;
}
#combo{
position: absolute;
z-index: 3;
left: 64px;
top: 150px;
width: 516px;
height: 89px;
border-radius: 40px;
background-color: white;
}
.month{
position: absolute;
z-index: 4;
left: 350px;
top: 20px;
width: 157px;
height: 55px;
border: none;
background-color: white;
font-size: 36px;
color: rgba(187, 187, 187, 1);
font-family: "台北黑体";
font-style: italic;
}
.data{
position: absolute;
z-index: 4;
left: 30px;
top: 13px;
width: 303px;
height: 55px;
border: none;
background-color: white;
font-size: 36px;
color: rgba(16, 16, 16, 1);
font-weight: 700;
text-align: center;
}
.init{
display: none;
}
.choice{
font-size: 20px;
font-weight: 500;
}
#rec{
position: absolute;
z-index: 2;
left: 64px;
top: 153px;
width: 1793px;
height: 907px;
background-color: rgba(147, 210, 243, 51%);
}
data(){
return{
opt: 'init',
cur: 0
}
},
methods: {
clickTest () {
console.log(this.opt+'\n'+this.cur)
if(this.cur!=this.opt){
this.cur=this.opt;
axios
.get("/2024-01-"+this.opt)
.then(function (response) {
const medals = response.data;
const table = document.createElement('table');
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
const headers = ['比赛类型', '比赛时间', '是否决赛'];
headers.forEach(headerText => {
const th = document.createElement('th');
th.textContent = headerText;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
// 创建表格内容
const tbody = document.createElement('tbody');
medals.forEach(date => {
const row = document.createElement('tr');
const { gameName, time, phaseName } = date;
const gameNameCell = document.createElement('td');
gameNameCell.textContent = gameName;
row.appendChild(gameNameCell);
const timeCell = document.createElement('td');
timeCell.textContent = time;
row.appendChild(timeCell);
const phaseNameCell = document.createElement('td');
phaseNameCell.textContent = phaseName;
row.appendChild(phaseNameCell);
tbody.appendChild(row);
});
table.appendChild(tbody);
// 将表格添加到页面中的某个容器元素中
const container = document.getElementById('show'); // 替换为你的容器元素 ID
container.innerHTML='';
container.appendChild(table);
})
.catch(function (error) {
console.log(error);
})
}
}
}
配置路由
// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Index from '@/view/Index.vue'
import Data from '@/view/data.vue'
import Detail from '@/view/detail.vue'
import Rank from '@/view/rank.vue'
import Test from '@/view/test.vue'
import medals from '@/view/medals.vue'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Index',
component: Index
}, {
path: '/Data',
name: 'Data',
component: Data
}, {
path: '/Detail',
name: 'Detail',
component: Detail
}, {
path: '/Rank',
name: 'Rank',
component: Rank
}, {
path:'/Test',
name: 'Test',
component:Test
}, {
path:'/medals',
name: 'medals',
component:medals
}
]
})
###6.2 后端
数据爬取
//建立http连接
public String crawl(String url) {
URL httpurl = null;
try {
//与网站建立http连接
httpurl = new URL(url);
HttpURLConnection conn = (HttpURLConnection) httpurl.openConnection();
//缓冲字符流readline()读取JSON数据
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String s;
StringBuffer sb = new StringBuffer();
while ((s = br.readLine()) != null) {
sb.append(s);
}
return sb.toString();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
//解析详细赛况数据
public ArrayList<RaceResult> saveRaceResult(String json) throws Exception {
ArrayList<RaceResult> raceResults = new ArrayList<>();
JSONObject object = JSON.parseObject(json);
JSONArray heats = JSON.parseArray(object.getString("Heats"));
for (int i = 0; i < heats.size(); i++) {
RaceResult raceResult = new RaceResult();
JSONObject race = (JSONObject) heats.get(i);
ArrayList<Result> rl = new ArrayList<>();
raceResult.setGameName(object.getString("DisciplineName"));
raceResult.setPhaseName(race.getString("Name"));
raceResult.setDate(race.getString("Date"));
raceResult.setTime(race.getString("Time"));
JSONArray playlists = JSON.parseArray(race.getString("Results"));
for (int j = 0; j < playlists.size(); j++) {
Result r = new Result();
JSONObject player = (JSONObject) playlists.get(j);
r.setCountry(player.getString("NAT"));
r.setResultAge(player.getString("AthleteResultAge"));
r.setOverRank(player.getString("Rank"));
r.setPlayerName(player.getString("FullName"));
r.setPoints(player.getString("TotalPoints"));
if (player.getString("PointsBehind") != null) {
r.setBehindPoint(player.getString("PointsBehind"));
}
r.setGameName(raceResult.getGameName());
r.setPhaseName(raceResult.getPhaseName());
rl.add(r);
}
raceResult.setResults(rl);
raceResults.add(raceResult);
}
return raceResults;
}
//解析奖牌榜
public ArrayList<Medal> saveMedals(String json) throws Exception {
ArrayList<Medal> medals = new ArrayList<>();
JSONObject object = JSON.parseObject(json);
JSONObject oooo = JSON.parseObject(object.getString("Medals"));
JSONArray sportMedals = JSON.parseArray(oooo.getString("SportMedals"));
JSONObject object2 = (JSONObject) sportMedals.get(0);
JSONArray countries = JSON.parseArray(object2.getString("Countries"));
for (int i = 0; i < countries.size(); i++) {
Medal m = new Medal();
JSONObject country = (JSONObject) countries.get(i);
m.setOverallRank(country.getString("Rank"));
m.setCountry(country.getString("CountryCode"));
m.setGold(country.getJSONObject("Gold").getString("Count"));
m.setSilver(country.getJSONObject("Silver").getString("Count"));
m.setBronze(country.getJSONObject("Bronze").getString("Count"));
m.setTotal(country.getString("TotalCount"));
medals.add(m);
}
return medals;
}
数据持久化(以比赛结果为例)
@Mapper
public interface ResultMapper {
@Insert("insert into result values (#{r_id},#{playerName},#{gameName},#{country},#{overRank},#{resultAge},#{phaseName},#{points},#{behindPoint})")
int addResult(Result result);
@Delete("delete from result")
void clearResult();
@Select("select * from result where r_id=#{id}")
ArrayList<Result> selectById(int id);
}
@Mapper
public interface RaceResultMapper extends BaseMapper<RaceResult> {
@Insert("insert into raceresult values (#{id},#{gameName},#{phaseName},#{date},#{time})")
void addRaceResult(RaceResult raceResult);
@Delete("delete from raceresult")
void clearRaceResult();
@Select("select * from raceresult where gameName=#{gameName} and phaseName=#{phaseName} ")
@Results({
@Result(column = "id", property = "id"),
@Result(column = "gameName", property = "gameName"),
@Result(column = "phaseName", property = "phaseName"),
@Result(column = "time", property = "time"),
@Result(column = "date", property = "date"),
@Result(column = "id", property = "results", javaType = ArrayList.class,
many = @Many(select = "com.example.demo.dao.ResultMapper.selectById")
)
})
RaceResult selectByNameAndPhase(String gameName, String phaseName);
@Select("select * from raceresult where date=#{date} order by time asc")
@Results({
@Result(column = "id", property = "id"),
@Result(column = "gameName", property = "gameName"),
@Result(column = "phaseName", property = "phaseName"),
@Result(column = "time", property = "time"),
@Result(column = "date", property = "date"),
@Result(column = "id", property = "results", javaType = ArrayList.class,
many = @Many(select = "com.example.demo.dao.ResultMapper.selectById")
)
})
ArrayList<RaceResult> selectByDate(String date);
@Select("select * from raceresult ")
@Results({
@Result(column = "id", property = "id"),
@Result(column = "gameName", property = "gameName"),
@Result(column = "phaseName", property = "phaseName"),
@Result(column = "time", property = "time"),
@Result(column = "date", property = "date"),
@Result(column = "id", property = "results", javaType = ArrayList.class,
many = @Many(select = "com.example.demo.dao.ResultMapper.selectById")
)
})
ArrayList<RaceResult> selectAll();
}
接口实现与接口文档生成
@RestController
public class DWAController {
@Autowired
MedalMapper medalMapper;
@Autowired
RaceResultMapper raceResultMapper;
@Autowired
private DataBaseController dataBaseController;
@GetMapping("/init")
public void initDb() throws Exception {
dataBaseController.initDateBase();
}
@GetMapping("/dwa/all")
public ArrayList<RaceResult> showRaceResult() throws Exception {
return raceResultMapper.selectAll();
}
//每日赛况
@GetMapping("/dwa/{date}")
public ArrayList<RaceResult> showOneDayRace(@PathVariable String date) {
return raceResultMapper.selectByDate(date);
}
//详细赛况
@GetMapping("/dwa/{gameName}/{phaseName}")
public RaceResult showDetail(@PathVariable String gameName, @PathVariable String phaseName) {
gameName = gameName.replace("%20", " ");
return raceResultMapper.selectByNameAndPhase(gameName, phaseName);
}
//奖牌板
@GetMapping("/dwa/medals")
public ArrayList<Medal> showMedal() throws Exception {
return medalMapper.findAllMedal();
}
}
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com"))
.paths(PathSelectors.any()).build();
}
private ApiInfo apiInfo(){
return new ApiInfoBuilder()
.title("DWA_API")
.description("跳水网站")
.version("1.0")
.build();
}
}
6.3 代码遵守规范
七、结对感受、收获与评价
132101256 对结对编程的感受
由于是第一次进行前后端分离的项目,对于整个项目过程的统筹安排还不够充分,对于后端开发的整个框架还不够熟悉,特别是在模块的划分上,前期的项目设计不够理想常常是突然想可以封装一个类再添加上去的,前期的项目设计不够理想。整体项目做下来还是收获颇多的,不仅让我了解到框架的便利,还让我体验到自己部署项目到服务器的快乐所在。
132101256 to 222100234
222100234 同学是一个认真负责的同学,积极与我沟通讨论,提出创新性的解决思路。希望在未来的合作中还能继续保持和谐愉快的合作关系。
222100234 对结对编程的感受
第一次做前后端分离的项目,对项目的时间安排不够合理,小看了联调所需的时间,认为时间充裕从而没有写Mock提前写,是本次项目的最大问题。其次,对于vue框架和前端代码的不熟悉,也对我造成了很多困扰。对于本次结对编程,我们选择了前后端分离开发的发生,这与我们第一次结对编程时采用的方式大为不同,使我看到了结对编程的另一种方式。同时也让我积累了前后端项目分离开发的经验。
222100234 to 132101256
132101256同学真的是非常优秀的一名同学,在本次作业中,他的进度总是能领先于我,在我遇到困难,他也会很乐意来帮助我解决。很抱歉这次拖了他的后腿,希望将来还能够一起合作编程。
}
}
### 6.3 代码遵守规范
[外链图片转存中...(img-cnKbOch1-1711506898114)]
## 七、结对感受、收获与评价
**132101256 对结对编程的感受**
> 由于是第一次进行前后端分离的项目,对于整个项目过程的统筹安排还不够充分,对于后端开发的整个框架还不够熟悉,特别是在模块的划分上,前期的项目设计不够理想常常是突然想可以封装一个类再添加上去的,前期的项目设计不够理想。整体项目做下来还是收获颇多的,不仅让我了解到框架的便利,还让我体验到自己部署项目到服务器的快乐所在。
**132101256 to 222100234**
> 222100234 同学是一个认真负责的同学,积极与我沟通讨论,提出创新性的解决思路。希望在未来的合作中还能继续保持和谐愉快的合作关系。
**222100234 对结对编程的感受**
>第一次做前后端分离的项目,对项目的时间安排不够合理,小看了联调所需的时间,认为时间充裕从而没有写Mock提前写,是本次项目的最大问题。其次,对于vue框架和前端代码的不熟悉,也对我造成了很多困扰。对于本次结对编程,我们选择了前后端分离开发的发生,这与我们第一次结对编程时采用的方式大为不同,使我看到了结对编程的另一种方式。同时也让我积累了前后端项目分离开发的经验。
**222100234 to 132101256**
>132101256同学真的是非常优秀的一名同学,在本次作业中,他的进度总是能领先于我,在我遇到困难,他也会很乐意来帮助我解决。很抱歉这次拖了他的后腿,希望将来还能够一起合作编程。