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

222100409任思泽 2024-03-26 23:57:59
这个作业属于哪个课程2302软件工程社区
这个作业要求在哪里结对第二次作业——编程实现
结对学号222100408冉洋、222100409任思泽
这个作业的目标1、编程实现所需功能 2、部署到云服务器 3、撰写博客
其他参考文献《构建之法》、CSDN 、Vue文档Go语言中文网

目录

  • 1. git仓库链接和代码规范链接
  • 2. PSP表格
  • 3. 项目访问链接
  • 4. 成果展示
  • 4.1 导航栏(附加功能)
  • 4.1.1 页面跳转
  • 4.1.2 页面置顶
  • 4.2 首页(附加功能)
  • 4.2.1 轮播图
  • 4.2.2 响应式设计
  • 4.2.3 合作伙伴
  • 4.3 运动员信息
  • 4.4 每日赛程
  • 4.5 详细赛况
  • 4.6 奖牌榜
  • 4.7 了解更多(附加功能)
  • 4.7.1 赛事新闻
  • 4.7.2 宣传视频
  • 4.8 留言(附加功能)
  • 4.8.1 个人留言
  • 4.8.2 查看所有留言
  • 5. 结对讨论过程描述
  • 5.1 前期分工
  • 5.2 中期开发
  • 5.3 合作交流
  • 5.4 博客撰写
  • 6. 设计实现过程
  • 6.1功能结构图
  • 6.2系统设计
  • 6.3遇到的问题以及解决方式
  • 7. 代码说明
  • 7.1 前端代码说明
  • 7.2 后端代码说明
  • 8. 心路历程和收获
  • 9. 评价结对队友

1. git仓库链接和代码规范链接

git仓库链接

GO后端开发规范链接

前端开发规范链接

2. PSP表格

PSPPersonal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning计划1515
• Estimate• 估计这个任务需要多少时间1515
Development开发27052595
• Analysis• 需求分析 (包括学习新技术)300350
• Design Spec• 生成设计文档4040
• Design Review• 设计复审3030
• Coding Standard• 代码规范 (为目前的开发制定合适的规范)2020
• Design• 具体设计3030
• Coding• 具体编码20001900
• Code Review• 代码复审3020
• Test• 测试(自我测试,修改代码,提交修改)9060
Reporting报告9080
• Test Repor• 测试报告3020
• Size Measurement• 计算工作量1515
• Postmortem & Process Improvement Plan• 事后总结, 并提出过程改进计划3030
合计27202610

3. 项目访问链接

  项目部署地址

4. 成果展示

该成果主要基于世界游泳锦标赛官网设计实现

数据来自66th International Divers’ Day Rostock

所爬取的数据仅用于教学使用❗❗❗

4.1 导航栏(附加功能)

4.1.1 页面跳转

设置一级导航栏,实现不同页面间的切换

在这里插入图片描述

4.1.2 页面置顶

设计了一个跳转按钮始终保持在各界面的右下角,单击即可回到该页面的顶部

请添加图片描述

4.2 首页(附加功能)

介绍了世界游泳锦标赛的举办背景,通过丰富的图文使平台更具吸引力,引起人们对世界游泳锦标赛的兴趣

4.2.1 轮播图

采用轮播图放置宣传照片

  • 手动切换:可通过拖拽图片或点击下方的小圆圈实现图片间的切换

  • 自动切换:图片间会自动切换,更符合正常网页逻辑

在这里插入图片描述

4.2.2 响应式设计

我们使用响应式的页面设计,支持随网页宽度的变化改变页面布局

请添加图片描述

4.2.3 合作伙伴

设置了一些合作伙伴(赞助商)并提供跳转链接,增加推广热度

img

4.3 运动员信息

基础功能:展示所有运动员的Country,Athlete,Gender,DOB等信息

附加功能:

  • 通过性别筛选运动员

  • 通过国籍筛选运动员

  • 多条件筛选:可通过性别和国籍多条件筛选运动员

在这里插入图片描述

4.4 每日赛程

基础功能:

  • 展示每一天的赛程,显示比赛类型(男子1m跳板,女子10m跳台等),参与选手和比赛时间,突出显示决赛

  • 支持点击查看详细情况

附加功能:

  • 跳转至详细赛况:

    从每日赛程中点击某一场比赛即可进入到对应的详细赛况界面

    在这里插入图片描述

  • 通过日历筛选该天赛程

  • 通过比赛性质(决赛与非决赛)筛选赛程

  • 多条件筛选:可通过日历和比赛性质多条件筛选运动员

请添加图片描述

4.5 详细赛况

基础功能:展示比赛的成绩,包含本场比赛参赛选手,选手排名,比赛积分,落后积分等

在这里插入图片描述

4.6 奖牌榜

基础功能:直观明了地展示所有获奖国家的金银铜牌及奖牌总数的获奖情况并排名

附加功能:设置了一个可交互的环形图,形象具体地展示奖牌在所有获奖国家中的分布情况

在这里插入图片描述

4.7 了解更多(附加功能)

4.7.1 赛事新闻

放置了一排的赛事相关新闻,单击即可跳转

img

4.7.2 宣传视频

设置了两个赛事的宣传视频,支持全屏、静音和倍速播放

4.8 留言(附加功能)

4.8.1 个人留言

输入看法和昵称,后点击提交按钮即可创建一条实时的个人留言

4.8.2 查看所有留言

向下滑动即可查看所有的留言,包括留言的昵称、内容与发布时间,支持留言的分页和跳转

在这里插入图片描述

5. 结对讨论过程描述

此次结对作业两人在宿舍里协作完成

5.1 前期分工

在作业发布后,我们第一时间进行了任务分工

img

5.2 中期开发

冉洋同学进行接口设计,任思泽同学具体实现

img

img

img

5.3 合作交流

双方在查询到有助于团队开发的资料后会互传,节省彼此查找资料的时间,提高项目开发的效率

img

5.4 博客撰写

博客内容汇总到任思泽同学处

img

6. 设计实现过程

6.1功能结构图

img

6.2系统设计

本次结对作业我们采用前后端分离式网站开发方式

优势

  • 提高代码质量:前后端分离可以使开发人员专注于实现业务逻辑,从而提高代码质量和可维护性
  • 减少耦合性:前后端分离可以减少代码之间的耦合性,从而减少后期维护的难度
  • 更好的测试和部署:前后端分离可以使得测试和部署更加容易,因为前后端代码是独立的
  • 提高性能:前后端分离可以加速页面加载速度,因为前端性能瓶颈通常出现在后端

前端设计:

  • 采用的技术: Vue3NaIveUIEcharts
  • 设计思路:
    • 首页: 参照了原型设计时实现的功能以及排版
    • 运动员: 参照原型设计的排版,使用表格显示所有选手数据。
    • 每日赛程: 可根据日期和比赛类型筛选比赛日程信息,每个比赛可以点击查看详细赛况。
    • 详细赛况: 展示所有比赛,可点击查看决赛或半决赛等的具体选手排名。
    • 奖牌榜: 展示国家的获奖数排名。
    • 扩展功能(部分):
      • 奖牌环形图: 使用echarts环形图展示各个国家获奖情况。
      • 留言: 用户可以留下自己对网站的看法,留言后会显示在留言板上,也可以看见别人的留言。
      • 了解更多: 介绍一些官网的新闻和一些比赛视频。

后端设计:

  • 使用的语言: golang

  • 采用的技术: ginviper

  • 设计思路: 通过http请求获取前端传入的条件数据,解析参数后,返回json数据,前端通过json数据进行页面渲染。

  • 参数校验: 使用gin框架的binding注解,进行参数校验,校验不通过则返回错误信息。
  • 数据解析: 使用go官方的encoding/json包,将json数据解析成结构体,方便后续处理。
  • 数据返回: 使用gin框架的c.JSON方法,将数据以json格式返回给前端。
  • 数据持久化 由于数据量较小,所以没有使用数据库,而是直接使用自己编写的缓存工具包,用go的gob包进行数据的序列化和反序列化,将数据存储在本地文件中。

总体流程:

img

6.3遇到的问题以及解决方式

  • 前端问题:

问题一:TS中axios的二次封装

解决方式: 因为之前都直接使用js进行开发,使用TS想对axios进行二次封装时,爆了一堆类型错误,最后只得在网上搜索,最后参考这篇文章完成了axios的二次封装

  • 后端问题:

问题一: 关于数据持久化时,报错gob: type not registered for xxx

解决方式: 经过在网上搜索,发现是因为gob包在解码时,需要提前注册类型,所以在解码之前,需要先注册类型,即在init函数中注册需要被序列化和反序列化的结构体类型

  • 前后端交互问题:

问题一:前端跨域访问后端问题

解决方式:

  • 前端:vue.config.ts中配置proxy,将前端请求代理到后端地址
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      }
    }
  }
  • 后端:gin框架中使用cors中间件,允许前端跨域访问
  r.Use(cors.New(cors.Config{
    AllowOrigins: []string{"*"},
    AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
    AllowHeaders: []string{"Origin", "Authorization", "Content-Type"},
  }))

7. 代码说明

7.1 前端代码说明

/src/router/index.ts

  • 使用vue-router实现路由跳转,通过createRouter创建路由实例,通过createWebHistory创建路由模式,通过createRouterroutes属性设置路由路径和对应的组件
  • 路由拦截器中添加了loadingBarstartfinish方法,实现路由跳转时的加载动画
import { createRouter, createWebHistory } from 'vue-router'
import { createDiscreteApi, type LoadingBarApi } from 'naive-ui'
import { useTheme } from '@/stores/theme'
import { nextTick } from 'vue'

let loadingBar: LoadingBarApi
nextTick(() => {
  const { themeColor } = useTheme()

  const { loadingBar: bar } = createDiscreteApi(['loadingBar'], {
    loadingBarProviderProps: {
      themeOverrides: {
        colorLoading: themeColor
      }
    }
  })
  loadingBar = bar
})


const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '',
      name: 'home',
      component: () => import('@/views/HomeView.vue')
    },
    {
      path: '/about',
      name: 'about',
      component: () => import('@/views/AboutView.vue')
    },
    {
      path: '/athletes',
      name: 'athletes',
      component: () => import('@/views/AthletesView.vue')
    },
    {
      path: '/schedule',
      name: 'schedule',
      component: () => import('@/views/ScheduleView.vue')
    },
    {
      path: '/results/:id?',
      name: 'results',
      component: () => import('@/views/ResultsView.vue')
    },
    {
      path: '/medal',
      name: 'medal',
      component: () => import('@/views/MedalView.vue')
    },
    {
      path: '/comment',
      name: 'comment',
      component: () => import('@/views/CommentView.vue')
    }
  ]
})

router.beforeEach(() => {
  loadingBar?.start()
})

router.afterEach(() => {
  loadingBar?.finish()
})


export default router

/src/utils/request.ts

二次封装axios,实现请求拦截和响应拦截,实现请求的统一处理

import axios from "axios"
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
export interface Response<T = any> {
 code: number
 data: T
 msg: string
 error: string
}

export interface RequestOptions {
 showLoadingBar?: boolean
 errorCallBack?: (error: Response) => void
 showErrorMessage?: boolean
}

// 拓展自定义请求配置
interface ExpandAxiosRequestConfig<D = any> extends AxiosRequestConfig<D> {
 interceptorHooks?: InterceptorHooks
 requestOptions?: RequestOptions
}
export interface RequestConfig<D = any> extends AxiosRequestConfig<D> {
 requestOptions?: RequestOptions

}
// 拓展axios请求配置
interface ExpandInternalAxiosRequestConfig<D = any> extends InternalAxiosRequestConfig<D> {
 intercetorHooks?: InterceptorHooks
 requestOptions?: RequestOptions
}

// 拓展axios响应配置
interface ExpandAxiosResponse<T = any, D = any> extends AxiosResponse<T, D> {
 config: ExpandInternalAxiosRequestConfig<D>
}
export interface InterceptorHooks {
 requestInterceptor?: (config: ExpandInternalAxiosRequestConfig) => ExpandInternalAxiosRequestConfig
 requestInterceptorCatch?: (config: ExpandInternalAxiosRequestConfig) => any
 responseInterceptor?: (response: ExpandAxiosResponse) => AxiosResponse | Promise<AxiosResponse>
 responseInterceptorCatch?: (response: ExpandAxiosResponse) => any
}


const transform: InterceptorHooks = {
 requestInterceptor: (config) => {
   if (config.requestOptions?.showLoadingBar) {
     // TODO show loading bar
   }
   return config
 },
 requestInterceptorCatch: (config) => {
   if (config.requestOptions?.showErrorMessage) {
     // TODO show error message
   }
   return Promise.reject(config)
 },

 responseInterceptor: (response) => {
   if (response.config.requestOptions?.showLoadingBar) {
     // TODO hide loading bar
   }
   if (response.data.code !== 0) {
     // 请求出现业务错误
     if (response.config.requestOptions?.showErrorMessage) {
       // TODO show error message
     }

     if (response.config.requestOptions?.errorCallBack) {
       response.config.requestOptions.errorCallBack(response.data)
     }
     return Promise.reject(response.data)
   }
   // 请求成功, 直接返回自定义返回类型的data
   return response.data.data
 },

 responseInterceptorCatch: (err) => {
   if (err.config.requestOptions?.showLoadingBar) {
     // TODO hide loading bar
   }
   if (err.config.requestOptions?.showErrorMessage) {
     // TODO show error message
   }
   return Promise.reject(err.config)
 }
}

class Request {
 private _instance: AxiosInstance
 private _defaultConfig: ExpandAxiosRequestConfig = {
   baseURL: '/api',
   timeout: 5000,
   requestOptions: {
     showLoadingBar: true,
     showErrorMessage: true
   }
 }

 private _interceptorHooks?: InterceptorHooks

 constructor(config: ExpandAxiosRequestConfig) {
   this._instance = axios.create(Object.assign(this._defaultConfig, config))
   this._interceptorHooks = config.interceptorHooks
   this.setupInterceptors()
 }

 private setupInterceptors() {
   this._instance.interceptors.request.use(this._interceptorHooks?.requestInterceptor, this._interceptorHooks?.requestInterceptorCatch)
   this._instance.interceptors.response.use(this._interceptorHooks?.responseInterceptor, this._interceptorHooks?.responseInterceptorCatch)
 }

 public request(config: RequestConfig): Promise<AxiosResponse> {
   return this._instance.request(config)
 }

 public get<T = any>(url:string, config?:RequestConfig): Promise<T> {
   return this._instance.get(url, config)
 }

 public post<T = any>(url:string, data?:any, config?:RequestConfig): Promise<T> {
   return this._instance.post(url, data, config)
 }

 public delete<T = any>(url:string, config?:RequestConfig): Promise<T> {
   return this._instance.delete(url, config)
 }

 public put<T = any>(url:string, data?:any, config?:RequestConfig): Promise<T> {
   return this._instance.put(url, data, config)
 }
}

const instance = new Request({
 interceptorHooks: transform
})

export const request: (config: RequestConfig) => Promise<AxiosResponse> = (config) => instance.request(config)
export const get: <T = any>(url:string, config?:RequestConfig) => Promise<T> = (url, config) => instance.get(url, config)
export const post: <T = any>(url:string, data?:any, config?:RequestConfig) => Promise<T> = (url, data, config) => instance.post(url, data, config)
export const del: <T = any>(url:string, config?:RequestConfig) => Promise<T> = (url, config) => instance.delete(url, config)
export const put: <T = any>(url:string, data?:any, config?:RequestConfig) => Promise<T> = (url, data, config) => instance.put(url, data, config)

  /src/layout/LayoutView.vue

使用NaiveUILayout组件实现页面布局,包括HeaderContentFooter,并在Content中使用n-back-top组件实现返回顶部功能

<template>
  <n-layout  >
    <n-layout-header bordered class="top-0 z-50"  position="absolute">
      <HeaderView  />
    </n-layout-header>
    <n-layout-content ref="layoutRef">
      <ContentView>
        
      </ContentView>
    </n-layout-content>
    <n-layout-footer>
      <FooterView></FooterView>
    </n-layout-footer>
  </n-layout>
  <n-back-top :right="100"  :listen-to="layoutRef" />
</template>

<script setup lang="ts">
import HeaderView from './header/HeaderView.vue'
import FooterView from './footer/FooterView.vue'
import ContentView from './content/ContentView.vue'
import { ref } from 'vue'
const layoutRef = ref(null)
</script>

7.2 后端代码说明

/routes/route.go
使用gin框架实现路由的注册,通过gin框架的Group方法实现路由的分组,通过GETPOST方法实现不同请求方式的路由注册

func Init() *gin.Engine {
    if conf.AppConf.Dev {
        gin.SetMode(gin.DebugMode)
    } else {
        gin.SetMode(gin.ReleaseMode)
    }
    r := gin.New()
    r.Use(ginlogger.Logger())
    r.Use(ginlogger.Recovery())

    base := r.Group(conf.ServerConf.Prefix)
    {
        base.GET("/ping", func(c *gin.Context) {
            c.String(200, "pong")
        })
        base.GET("/athletes", controller.GetAthletes)
        base.GET("/results", controller.GetResults)
        base.GET("/results/:id", controller.GetResultDetail)
        base.GET("/schedule", controller.GetSchedule)
        base.GET("/medals", controller.GetMedals)
        base.GET("/comments/:pageNo", controller.ListComment)
        base.POST("/comments", controller.CreateComment)
    }
    return r
}

  /routes/controller/athlete.go

实现了获取运动员信息的接口,通过c.JSON方法返回json数据

func GetAthletes(c *gin.Context) {
    var service athletes.AthleteService
    res := service.GetAthletes()
    c.JSON(http.StatusOK, res)
}

/service/athletes/athlete.go

解析json数据的结构体,通过json.Unmarshal方法解析json数据

var athleteJsonFile = "/athletes.json"

type Athlete struct {
    Country     string `json:"country"`
    FirstName   string `json:"firstName"`
    LastName    string `json:"lastName"`
    Gender      string `json:"gender"`
    Dob         string `json:"dob"`
    CountryCode string `json:"countryCode"`
}

type AthleteService struct {
}

type CountryJSON struct {
    CountryName string        `json:"CountryName"`
    CountryCode string        `json:"CountryCode"`
    AthleteList []AthleteJSON `json:"Participations"`
}

type AthleteJSON struct {
    Gender    int    `json:"Gender"`
    LastName  string `json:"PreferredLastName"`
    FirstName string `json:"PreferredFirstName"`
    Dob       string `json:"DOB"`
}****

init方法注册结构体类型,通过os.ReadFile方法读取json文件,通过json.Unmarshal方法解析json数据,将解析后的数据存入缓存中,通过cache.Set方法存入缓存

func init() {
    gob.Register([]Athlete{})
}

func (service *AthleteService) GetAthletes() serializer.Response {
    res, ok := cache.Get("athletes")
    if ok {
        return serializer.Response{
            Data: res,
        }
    }
    countryList := make([]CountryJSON, 0)
    bytes, err := os.ReadFile(conf.AppConf.StaticPath + athleteJsonFile)
    if err != nil {
        return serializer.AppError("Failed to read athletes.json", err)
    }
    err = json.Unmarshal(bytes, &countryList)
    if err != nil {
        return serializer.AppError("Failed to parse athletes.json", err)
    }
    athleteRes := make([]Athlete, 0)
    for _, country := range countryList {
        for _, athlete := range country.AthleteList {
            athleteRes = append(athleteRes, Athlete{
                Country:     country.CountryName,
                CountryCode: country.CountryCode,
                FirstName:   athlete.FirstName,
                LastName:    athlete.LastName,
                Gender:      parseGender(athlete.Gender),
                Dob:         athlete.Dob,
            })
        }
    }
    _ = cache.Set("athletes", athleteRes, 0)
    return serializer.Response{
        Data: athleteRes,
    }
}

func parseGender(code int) string {
    if code == 1 {
        return "Female"
    } else {
        return "Male"
    }
}

8. 心路历程和收获

  • 冉洋(222100408):

    之前写前端都是使用js,这次我选择使用ts进行开发, 因为这更加贴合现代前端开发的趋势,同时也能提高代码的可维护性和可读性。在实际的开发过程中,我发现ts的类型检查和接口定义能够帮助我更好地理解和管理代码,减少了一些潜在的bug。

    但是我也遇到了一些困难和技术挑战。特别是在设计页面布局和实现交互功能时,我花费了不少时间和精力。然而,在逐步解决这些问题的过程中,我收获了很多宝贵的经验和技能。

    这次选择了Naive UI作为UI框架,这是一个基于Vue3的组件库,提供了丰富的组件和样式,能够帮助我快速搭建页面。这也是我第一次使用这个UI框架。

    此外通过与我的搭档紧密合作,我也提高了自己的团队合作和沟通能力。

    我相信这次结对作业的经验和收获将在我未来的职业发展中发挥重要作用。

  • 任思泽(222100409):

    在项目开始之前,我对于所需的技术栈和工具并不熟悉,但我抱着积极的态度和渴望学习的心情迎接这个挑战。

    听说Go开发Web的效率很高,所以我选择了Go语言作为后端开发的技术栈。在实际的开发过程中,我发现Go语言的简洁和高效确实让我受益匪浅。通过学习和实践,我逐渐掌握了Go语言的基本语法和常用库,能够独立完成后端开发的任务。

    在项目中,我还学习了如何使用gin框架来搭建Web服务,以及如何与前端进行数据交互。通过这些实践,我对Web开发的整个流程有了更深入的了解,也提高了自己的编程能力和实践经验。

    此外,这次项目也让我更加注重细节和质量。我意识到编程实现的过程中,细致入微的工作和严谨的态度对确保程序的正确性和稳定性来说是非常重要的。

    这些经验和收获将对我的未来发展产生积极的影响。

9. 评价结对队友

  • 冉洋(222100408): 我认为任思泽是一位非常出色的队友。他展现了卓越的技术能力和团队合作精神。作为后端开发工程师,他对后端技术有深入的理解,并能够准确理解和满足我的后端需求。他在与我合作过程中表现出色,能够高效地开发后端接口。他的代码规范和对用户体验的关注使整个项目进展顺利。我非常欣赏他的团队协作能力和与我密切合作的态度。他是一个可靠的合作伙伴,我相信我们的合作将继续创造更多的价值。希望任思泽能够继续发展自己的技术能力,并保持对团队合作的热情。
  • 任思泽(222100409): 我认为冉洋是一位非常出色的队友。他拥有较为熟练的前端技能,能够很好的完成页面的设计以及开发工作, 期待与他的再次合作。
...全文
140 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

122

社区成员

发帖
与我相关
我的任务
社区描述
FZU-SE
软件工程 高校
社区管理员
  • LinQF39
  • 助教-吴可仪
  • 一杯时间
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧