这个作业属于哪个课程 | 2022年福大-软件工程;软件工程实践-W班 |
---|---|
这个作业要求在哪里 | 要求在这里 |
这个作业的目标 | 以北京冬奥会赛事信息平台为载体,初步接触需求分析,了解NABCD模型在实际中的应用,并初步学会使用一些原型设计工具进行原型设计 |
结对学号 | 221900231 221900203 |
其他参考文献 | 见博客末尾 |
0x01 about 链接
1.1 Gitcode仓库地址
PairProject of 221900203 and 221900231
1.2 服务器链接
1.3 代码规范链接
-
前端代码规范:前端规范 -refer to 阿里巴巴
-
后端代码规范:后端规范 -refer to 腾讯
0x02 PSP表格&效能分析
2.1 PSP表格
203
同学的PSP表格
PSP | Personal Software Process Stages | 预估消耗时间(分钟) | 实际消耗时间(分钟) |
---|---|---|---|
计划 | Planning | ||
•估计这个任务需要多久 | •Estimate | 5 | 10 |
开发 | Development | ||
•需求分析 | •Analysis | 10 | 15 |
•制定代码规范 | •Deciding Code Style | 10 | 15 |
•前端技术学习 | •Tool Learning | 150 | 120 |
•原型改进 | •Previous Improving | 50 | 30 |
•前端编码 | •Coding | 1400 | 1600 |
•测试及改进 | •Testing and Improving | 400 | 400 |
•撰写报告 | •Reporting | 30 | 50 |
•计算工作量 | •Size Measurement | 5 | 3 |
•事后总结 | •Postmortem | 20 | 30 |
总计 | 2080 | 2253 |
231
同学的PSP表格
PSP | Personal Software Process Stages | 预估消耗时间(分钟) | 实际消耗时间(分钟) |
---|---|---|---|
计划 | Planning | ||
•估计这个任务需要多久 | •Estimate | 5 | 10 |
开发 | Development | ||
•需求分析 | •Analysis | 10 | 15 |
•制定代码规范 | •Deciding Code Style | 10 | 10 |
•后端技术学习 | •Learning | 1200 | 1400 |
•后端编码 | •Coding | 240 | 180 |
•测试及改进 | •Test and Improving | 150 | 180 |
•云服务器部署 | •Deploying | 400 | 1000 |
•撰写报告 | •Reporting | 120 | 150 |
•计算工作量 | •Size Measurement | 5 | 3 |
•事后总结 | •Postmortem | 20 | 20 |
总计 | 2160 | 2968 |
2.2 效能分析
-
203
同学:本次结对中,花费时间较多的是编码
和测试改进
模块,与预估出入较大的是编码
部分。我负责结对的前端部分,由于之前学过Vue2.0
相关知识,所以学习技术环节没有什么时间成本,但由于学艺不精
,外加直男审美
,编写界面时极其痛苦,导致一直在调样式,超时较多,而且最后界面也不是很好看,后面的测试也是如此,所以如果后面还要继续写前端的话,应该要多练手,不然真正有用上的时候就会感觉有心无力。 -
231
同学:本次作业的效能简直惨不忍睹
。由于我的技术栈过于浅薄(真的是书到用时方恨少),在本次结对作业中,为了能完成目标,不得不学习新技术。但由于工期非常紧,只能学习个易入门的技术且只能学个大概。但即使如此,照样也花费了很多时间。另外一个就是部署服务器也花了很多时间,因为之前完全没有接触过linux,其中的很多东西都只能跟着教程做,出问题只能借助搜索引擎的力量,导致效率很低下。综上所述,菜是原罪
,应该多抓紧时间学技术,否则遇到这种情况只能感慨自己没用了。
0x03 成果展示
根据原型,我们一共完成了奖牌地图
、奖牌榜
、赛程赛况
、关于冬奥
四大栏目
3.0 header and footer
3.1 奖牌地图界面
装载所有91个冬奥参赛国家的奖牌信息(包括获奖和没获奖的国家/地区信息),没获奖的国家/地区也会有颜色,和其他未参赛国家/地区区分
(这里插播一条地理小常识:南美洲有一块有颜色地区,其实是法国海外属地法属圭亚那)
鼠标移动到有数据的国家时,会显示国家/地区名称和他的具体奖牌信息:
将鼠标锚定在图例的某个位置,会显示奖牌总数在这个值附近的国家/地区名:
3.2 奖牌榜界面
奖牌榜显示排名、国家/地区名、金牌数、银牌数、铜牌数、奖牌总数,以分页的形式展示:
3.3 赛程赛况界面
赛程赛况中又进行了页内导航,分为日期赛程
、分项赛程
、大项赛程
、多选项(混合查询)
页面
3.3.1 日期赛程页面
以日历形式展示,点击某一天,就可以查看当天赛程
3.3.2 分项赛程页面
以卡片形式展示所有分项(共15个),点击可以查看对应赛程,还可以查看对应比赛的赛况结果(目前只支持单人雪车
、单人雪橇
、钢架雪车
三个项目的赛果查看):
3.3.3 场馆赛程页面
展示策略同上
3.3.4 组合查询页面
可以在此选择日期
、项目
、场馆
查询指定赛事。如果不选择其中一项或几项,则该条件不限制。全部都不选时,查询所有赛事:
3.4 关于冬奥界面
3.4.1 北京冬奥会介绍页面
简单介绍本次冬奥会的基本情况,并用图表展示赛事构成、金银铜牌归属分布等数据:
3.4.2 历届冬奥会介绍页面
罗列了2002年以来的冬奥会举办简介,用户可以点击某一张卡片查看对应冬奥会的赛事介绍百度百科:
0x04 编码过程实录
4.1 结对分工
由于203
同学之前已经比较系统地学习了Vue的有关知识并且已经实战过,所以本次结对前端部分的编码任务自然而然就交给了他。但是,231
同学啥也不会,所以只能选择后端。考虑到前端的任务繁重,工作量大,所以231
同学还承担部署服务器、最后博客的大部分编写工作。
4.2 技术栈
- 前端:Vue2.0+axios(前后端交互中介)
- 后端:Servlet 3
4.3 编码工具
VSCode+IntelliJ IDEA Ultimate
4.4 网页设计思路
4.5 细说编码中的困难
Barrier 1:如何在Vue中嵌入地图
估计这是前端编码中最大的困难了,因为之前前端实现的时候从来没有遇到过这种需求。一开始两个人都没什么头绪,只能硬着头皮在网上搜罗相关经验,最后还真的找到了。Vue的本质其实是javascript
,所以js中支持的语法在Vue中一定适用,所以我们找了如何在js中嵌入可视化地图
,然后就搜出了本次作业的重磅友情插件——EChart
。不得不叹服Echart
强大的数据集成、可视化能力。只需要照着他的模板,剔除不参加显示的国家/地区名,并取得需要显示的国家/地区的奖牌数据,就可以实现我们的目的。(由于本次冬奥会有奖牌的国家只有29个,所以我们决定直接将数据写在js脚本里面)
具体代码会在后面展示
Barrier 2:前端如何获取赛程数据
由于两个人头一次接触前后端分离的编码模式,对于如何接受数据的认识,仅仅停留在理论
层面,因此也是一个大麻烦。同样是经过了多方查找,发现其实实现方式或者技术还是挺多的。最后我们使用了axios
来充当数据交互的中间媒介,他本质是ajax
,不过操纵会更加方便。将基本代码写好之后,只要和后端沟通好数据接口,前台就可以拿到数据信息了。
Barrier 3:后端应该采用什么技术
对于啥也不会的后端同学,由于作业时间很短,所以这个问题非常头疼。最开始打算用Springboot
,但是粗浅看了一下,发现东西太多了,如果不系统学,用起来会很乱,于是后来选择了它的先导版本——Servlet
。他的职能更倾向于数据处理,所以只需要掌握基本的请求/响应流程就行,对于本次任务的需求足够了,唯一的不足就是代码重复性较高。
0x05 关键代码展示
5.0 项目结构
5.1 前端关键代码
奖牌地图
引入Echart
组件世界地图部分,手动把参赛国家/地区列出,并将获取到的奖牌数据渲染到地图中,最后设置悬停时的显示样式。
import world from '../../static/mapData/world.json'
...
jsonMap: {
'world':world,
},
worldData :[
{name: '美国', gold: 8, silver: 10, copper: 7, value: 25,rank: 4},
...//此处填充奖牌信息
],
nameMap : {
'Afghanistan':'阿富汗','Afghanistan':'阿富汗',
...//为原json中的英文国家名建立中文映射
},
...//其他非重要代码
methods:{
chinaConfigure(area) {
this.myChart = this.$echarts.init(document.getElementById("map")); //这里是为了获得容器所在位置
window.onresize = this.myChart.resize;
let option={ // 进行相关配置
backgroundColor: "#E4F3FA",
tooltip: {
triggerOn: 'mousemove',
formatter: function (params){
console.log(params);
if(params.data.name!=null){//设置悬停时的显示信息
return params.data.name+"\n"+"金牌数:"+params.data.gold+
"\n"+"银牌数:"+params.data.silver+"\n"+"铜牌数:"+params.data.copper+"\n"+"奖牌总数:"+params.data.value+"金牌榜名次:"+params.data.rank
}
}
},
visualMap: {//设置图例
max: 40,//设置奖牌总数最大值
calculable: true,
inRange: {
color: [ '#ffffbf', '#fee090', '#fdae61', '#f46d43', '#d73027', '#a50026']//为图例锚定颜色变换区间,系统会让颜色自然过渡
}
},
series: [{
type: 'map',
map: area,
data: area=='world'?this.worldData:[],
nameMap: area=='world'?this.nameMap:{},
}]
}
this.myChart.setOption(option,true);
},
}
};
axios封装前台数据请求及响应结果
请求时,将参数params
先封装好,然后接入后台给出的数据接口,由axios传入后台,最后响应一个数据结构(json形式),从中取出自己想要的值。
export default {
name: 'DailyScheduleMore',
data() {
return {
tableData:[
],
date:"date",
game_kind:"game_kind",
game_venue:"game_venue",
currentPage:1,//当前页码
total:0,//总数据
pageSize:9, //每页的数据条数
url:'http://47.97.91.198:8081/Olympics-backend/SearchByMixedConditions',//后台接口
}
},
mounted(){
...
},
methods: {
handleSizeChange(val) {//每页条数改变时触发 选择一页显示多少行
console.log(`每页 ${val} 条`);
this.pageSize = val;
},
handleCurrentChange(val) {//当前页改变时触发 跳转其他页
console.log(`当前页: ${val}`);
this.currentPage = val;
},
getlivestockInfo(){
var that = this;
var url = this.url;
var params;
params = {search:"true",date:""};//请求参数封装(默认值)
// params.append('pageNum',num1);
this.$axios.get(url,{params:{
search:"true",date:this.date,game_kind:this.game_kind,game_venue:this.game_venue
}}
)
.then(response => {//请求成功
this.tableData=response.data.matchList;//需事先了解json结构
that.total=response.data.matchList.length;//便于分页
console.log(that.tableData.length);
}).catch(error => {//请求失败
// console.log('请求失败');
console.log(error);
})
},
}
}
分页
用到了ElementUI
的pagination
组件,可以自定义每页显示几条数据等个性值。
<!-- 分页器 -->
<div class="block" style="margin-top:15px;">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[5,10,15]"
:page-size="pageSize"
layout="total, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
router的使用
在router
文件夹下的index.js
中配置好路由信息,目的主要是实现页面的正常跳转。
Vue.use(Router)
export default new Router({
base: '/Olympics_frontend/',
mode: 'history',
routes: [
{
path: '/',
name: 'report',
component: report,
redirect: '/HelloWorld',//奖牌地图做为首页
children: [
{
name:'HelloWorld',
path:'/HelloWorld',
component:HelloWorld
},
{
name:'ScheduleAction',
path:'/ScheduleAction',
component:ScheduleAction,
redirect: '/ScheduleAction/DateSchedule',//界面内分页(二级导航)
children:[
{
name:'DateSchedule',
path:'/ScheduleAction/DateSchedule',
component:DateSchedule
},
...//其他路由
}
]
})
5.2 后端关键代码
首先附上整个数据流在请求/响应周期的流向:
5.2.0 项目结构
5.2.1 装填数据库
本次作业实际上可以不用后台,因为官方的api是公开的,我们可以根据此来直接获取数据并在前台解析,但是这样子的话组合查询实现就有点困难,所以最后还是选择做后端传数据。
数据我们利用了上一次作业的成果(谁能想到这作业竟是连续剧
),先跑了上次的程序在记事本上,然后写程序解析填入数据库。
下面这段代码的作用是解析赛事到数据库:
...//数据库连接配置信息
Class.forName(JDBC_DRIVER);
// 打开链接
System.out.println("连接数据库...");
conn = DriverManager.getConnection(DB_URL, USER, PASS);
BufferedReader reader;
try {
reader = new BufferedReader(new InputStreamReader(new FileInputStream("output.txt"), StandardCharsets.UTF_8));
String line = reader.readLine();
int id = 1;
stmt = conn.prepareStatement("insert into gameData values (?,?,?,?,?)");
String time = null;
..//初始化字段
while (line != null) {
if (line.contains("time")) {
time = line.replaceAll("time:", "").trim();
}
..//处理空格
stmt.setInt(1, id);
id++;
stmt.setString(2, time);
//填充字段
stmt.execute();
}
line = reader.readLine();
}
reader.close();
// 完成后关闭
stmt.close();
conn.close();
}
...//异常处理
5.2.2 解决跨域造成数据无法传输的问题
由于前后端采取的策略不一致,这会造成所谓的跨域问题,如果不处理,会造成500
错误,需要设置一下:
//设置跨域问题及编码问题
response.setContentType("application/json;charset=utf-8");//以json格式传输响应
response.setCharacterEncoding("UTF-8");//设置中文编码格式,解决乱码问题
response.setHeader("Access-Control-Allow-Origin","*");//设置消息头,防止拦截
response.addHeader("Access-Control-Allow-Headers","Origin, X-requested-with, Content-Type, Accept");
response.setHeader("Access-Control-Allow-Methods","GET,POST");//请求类型:GET,POST
5.2.3 数据库连接
专门写了一个类处理数据库连接:(这里其实更好的方式应该是把连接类设为单例,这样可以保证资源不被盲目创建,保证数据安全)
public class DBConnectionImpl implements DBConnection{//实现接口,让代码更加规范
public Connection conn = null;
private static final String JDBC_DRIVER = "com.mysql.cj.jdbc.Driver";
private static final String DB_URL = "jdbc:mysql://47.97.91.198:3306/beijingOlympics?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC";
private static final String USER = "root";
private static final String PASS = "zxc010320";
@Override
public void connectToDatabase() {
try {
Class.forName(JDBC_DRIVER);
// 打开链接
System.out.println("连接数据库...");
conn = DriverManager.getConnection(DB_URL, USER, PASS);
}//数据库的关闭写在Servlet层处理完后
catch (SQLException | ClassNotFoundException e)
{
e.printStackTrace();
}
}
}
5.2.4 查询数据
使用Preparedstatement+占位符的方式构建查询语句,将Servlet传来的条件填入占位符后完成查询(其实使用Mybatis更好)
public class DateSearchDaoImpl implements DateSearchDao{
//本层真正与数据库交互
@Override
public List<GameData> searchDateScheduleOnDatabase(String date) {
...
try {
PreparedStatement stmt = dbConnection.conn.prepareStatement("select * from gamedata where date=?");
stmt.setString(1,date);
ResultSet resultSet = stmt.executeQuery();
while (resultSet.next())//将数据填入Bean对象
{
GameData gameData = new GameData();
gameData.setDate(resultSet.getString("date"));
gameData.setTime(resultSet.getString("time"));
//...
resultList.add(gameData);
}
} ...//处理异常
try {
dbConnection.conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
return resultList;
}
}
5.2.5 封装json对象
使用第三方库封装json
对象,只需把Bean中的数据以此装入即可(这里还需装入total
字段,便于前端显示时分页)
FourRoundsDetailService fourRoundsDetailService = new FourRoundsDetailServiceImpl();
List<FourRoundsData> resultList=fourRoundsDetailService.searchFourRoundsResult(competition);
int resultNumber = resultList.size();
JSONObject resultAll = new JSONObject();
JSONObject resultMain;
JSONArray resultArray = new JSONArray();
resultAll.put("total",resultNumber);
//装载JSON
for (FourRoundsData fourRoundsData : resultList) {
resultMain = new JSONObject();//每次都需要初始化,JSONObject对象内容无法被覆盖
resultMain.put("rank",fourRoundsData.getRank());
//...
resultArray.put(resultMain);
}
resultAll.put("resultList",resultArray);
PrintWriter writer = response.getWriter();
writer.print(resultAll);
5.2.6 如何发现前端请求
在这里我们做了一个约定:用一个字段来标记。在前台的消息中加入search
字段,后台servlet
层读到请求信息中的search
字段后做个判断再处理:
String search = request.getParameter("search");
...
if (search.equals("true"))
{
...//操纵数据库
}
0x06 新手部署云服务器
之前从来没接触过服务器部署,全都是localhost
产品。借助此次作业,粗浅地跟着视频学习了一下部署。
使用的是阿里云ECS
,采用最低的配置进行部署,系统为Linux
,Cent OS 8.5
。
由于部署过程非常繁琐,所以这里只是粗略地讲下我们的部署过程。
6.0 工具准备
XShell,Xftp,前者用于命令输入,后者用于传输文件
6.1 下载JDK
从Oracle
官网下载linux
的JDK,然后传输到云服务器上。传输完成后,设置环境变量并生效,输入java -version
命令,有版本信息出来就代表成功:
6.2 下载tomcat
从官网下载tomcat
,我们在此下了和本地一个版本号的tomcat
(服务器其实就是一个啥也没有的远程电脑),解压后进入bin
目录,输入指令./start.sh
指令可以查看是否成功安装。
最后还需要设置tomcat
的端口并开放,防止端口冲突。
6.3 安装MySQL
这是部署中最麻烦的一个环节。首先也是先下载linux
的MySQL安装包,然后将他传输到云服务器。
接下来解压,以此按照指令下载common
、libs
、client
、server
。完成后对端口进行开放,并修改数据库密码,在Navicat
中检查是否连接成功:
6.4 打包本地项目,投到云服务器
用maven
打包后台程序,用run npm build
指令打包前台程序。生成后,后端为war
包,前端程序在dist
文件夹下。
然后将程序放入tomcat
的webapps
下。(war
包拖入此会自动被解压)
6.* 完成部署
输入http://ip地址:端口号/运行目录
就可以正常访问。
0x07 结对过程
7.1 分工情况
203
同学:前端框架搭建编写、样式调整、部分博客撰写231
同学:后端数据操作、云服务器部署、博客大体撰写
7.2 结对记录
本次结对可以线下开展,这为团队协作提供了便利。结对过程主要以线下交流为准,辅以少量的线上资料共享。
0x08 心得体会
- 本次作业暴露出的最大一个问题是
技术栈浅薄
,因此会造成工作量加倍,完成任务时会非常痛苦,因此应该抓紧时间学习相应技术 - 其实在写博客的时候有发现项目存在的不足,如导航栏出现了滚轮(没部署前是没有的)、页面存在两个滚轴、页脚留白过多等等问题,这些在后续应该继续改进、界面不太美观等等
- 虽然完成的很痛苦,但是学到了技术的相当多东西,对于个人的成长是有帮助的,是一次很好的锻炼机会
- 第一次尝试前后端分离开发,充分感受到了这种开发模式的好处。当没有涉及到数据交互时,前后端的耦合度极低,大大方便了协调开发,开发起来比较有条理
0x09 队友互评
-
203
同学评231
同学:虽然231
同学在本次作业前没有接触过后端的相关知识,但是学习能力比较强,对新事物上手速度快,写的后端代码整体bug很少,方便了前台编程。同时231
同学愿意主动分担任务,让本次我们的工作量得到了很好的平衡,是一个可靠的队友。 -
231
同学评203
同学:203
同学有一定的技术栈,项目经历比较广,这为本次作业的完成提供了便利,同时他有很多的好点子,在项目中得到了较多的应用。203
同学动手能力强,抗压能力也较强,是一个好队友。
0x0A 补充:数据库设计
本次作业涉及的数据虽然很多,但是数据结构很简单,后端建立了赛事信息
、赛况
(部分比赛)这两张实表,以及一张虚的空表,用以处理不满足条件的数据请求。
由于我们制作的赛况不能在一维尺度(即一行)上展现,所以无法实现用外键连接,上面提到的表全是独立的。
gamedata
表:
mensskeleton
表(其他已完成赛况的表结构同此):
0x0B 参考文献
B站:Servlet+Http+Tomcat入门教程
B站:阿里云部署vue+springboot
CSDN:Linux部署Tomcat报错:Could not contact localhost:8005.
博客园:js中实现世界地图数据可视化
RUNOOB:vue.js中使用axios