第二次结对作业
这个作业属于哪个课程 | https://edu.cnblogs.com/campus/fzu/SE2024 |
---|---|
这个作业要求在哪里 | https://edu.cnblogs.com/campus/fzu/SE2024/homework/13281 |
这个作业的目标 | 基于第一次结对作业的原型设计进行改进,用代码实现一个项目信息交流平台 |
学号 | 102201635 |
结对成员 | 10221636 黄森福 102201635 高鑫源 |
结对同学博客链接 | https://www.cnblogs.com/hsf-/p/18456325 |
github仓库项目地址 | https://github.com/zaohuan/102201635-102201636 |
1.具体分工
黄森福 | 高鑫源 |
---|---|
首页UI的设计,分类导航栏的设计,登录注册,修改个人信息、密码前后端的实现 bug的修复 | 我的页面ui的设计,创建项目,项目详情,修改项目前后端的实现 bug的修复 单元测试的书写 |
2.PSP表格
PSP | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|
计划 | 60 | 80 |
估计这个任务需要多少时间 | 20 | 30 |
开发 | 2000 | 2400 |
需求分析 (包括学习新技术) | 480 | 600 |
生成设计文档 | 30 | 20 |
设计复审 | 30 | 20 |
代码规范 (为目前的开发制定合适的规范) | 30 | 90 |
具体设计 | 60 | 90 |
具体编码 | 2000 | 2400 |
代码复审 | 60 | 60 |
测试(自我测试,修改代码,提交修改) | 150 | 180 |
报告 | 60 | 60 |
测试报告 | 30 | 40 |
计算工作量 | 10 | 10 |
事后总结, 并提出过程改进计划 | 30 | 30 |
合计 | 3050 | 3710 |
3.解题思路描述与设计实现说明
3.1流程图
根据客户现实困扰 我们的软件应该是一个信息发布平台 它能让想要参加项目的用户找到心仪的项目 让缺少队友的项目能够通过创建发布项目来达到招募队友的目的,而对于聊天的需求,目前qq和微信是中国目前最成熟且最流行的交流平台 所以我们的软件在这一方面没有优势,进行实时交流的功能并不是我们软件的核心功能。
3.2创建项目
<template>
<view class="container">
<view class="form-item">
<text>项目名称:</text>
<input v-model="projectName" placeholder="请输入项目名称" />
</view>
<view class="form-item">
<text>项目简介:</text>
<uni-easyinput
type="textarea"
autoHeight
v-model="projectDescription"
placeholder="请输入项目简介"
></uni-easyinput>
</view>
<view class="form-item">
<text>项目分类:</text>
<picker value="selectedCategory" :range="categories" @change="onCategoryChange">
<view class="picker">
{{ categories[selectedCategory] || '请选择项目分类' }}
</view>
</picker>
</view>
<view class="form-item">
<text>项目人数规模:</text>
<input
type="number"
v-model="projectScale"
placeholder="请输入目前参与项目的人数(请输入数字)"
@input="validateScale"
/>
</view>
<view class="form-item">
<text>是否缺人:</text>
<picker value="selectedQue" :range="ques" @change="onQueChange">
<view class="picker">
{{ ques[selectedQue] || '请选择' }}
</view>
</picker>
</view>
<view class="form-item">
<text>项目状态:</text>
<picker value="selectedState" :range="states" @change="onStateChange">
<view class="picker">
{{ states[selectedState] || '请选择项目状态' }}
</view>
</picker>
</view>
<view class="form-item">
<text>联系方式:</text>
<input v-model="lianxi" placeholder="请输入联系方式" />
</view>
<button @click="createProject">创建项目</button>
</view>
</template>
<script>
export default {
data() {
return {
projectName: '',
projectDescription: '',
projectScale: '',
selectedCategory: null,
selectedState: null,
selectedQue: null,
categories: ['自然科学', '工程技术', '医学健康', '社会科学', '人文历史', '交叉学科'],
states: ['准备中', '进行中', '已完结'],
ques: ['是', '否'],
lianxi:null,
};
},
methods: {
validateScale() {
const isValid = /^[0-9]*$/.test(this.projectScale);
if (!isValid) {
uni.showToast({
title: '请输入有效的数字',
icon: 'none'
});
this.projectScale = this.projectScale.replace(/[^0-9]/g, ''); // 只保留数字
}
},
onCategoryChange(e) {
this.selectedCategory = e.detail.value;
},
onStateChange(e) {
this.selectedState = e.detail.value;
},
onQueChange(e) {
this.selectedQue = e.detail.value;
},
goBack() {
uni.navigateBack({
delta: 1
});
},
async createProject() {
if (!this.projectName || !this.projectDescription ||
(this.selectedCategory === null && this.selectedCategory !== 0) ||
!this.projectScale ||
(this.selectedState === null && this.selectedState !== 0) ||
(this.selectedQue === null && this.selectedQue !== 0)||!this.lianxi) {
uni.showToast({
title: '请填写所有字段',
icon: 'none'
});
return;
}
const nameExists = await this.checkProjectName(this.projectName);
if (nameExists) {
uni.showToast({
title: '项目名称已存在,请使用其他名称',
icon: 'none'
});
return;
}
const projectData = {
name: this.projectName,
description: this.projectDescription,
category: this.categories[this.selectedCategory],
scale: this.projectScale,
state: this.states[this.selectedState],
que: this.ques[this.selectedQue],
lianxi:this.lianxi,
};
try {
const usernameRes = await uni.getStorage({
key: 'username'
});
const username = usernameRes.data;
projectData.username = username;
console.log(projectData);
await uniCloud.callFunction({
name: 'createProject',
data: projectData
});
uni.showToast({
title: '项目创建成功',
icon: 'success'
});
uni.setStorage({
key: 'projectData',
data: projectData,
success: () => {
uni.navigateTo({
url: '/pages/projectdetail/projectdetail'
});
}
});
this.resetForm();
} catch (error) {
console.error(error);
uni.showToast({
title: '项目创建失败',
icon: 'none'
});
}
},
async checkProjectName(name) {
const res = await uniCloud.callFunction({
name: 'checkProjectName',
data: { name }
});
return res.result;
},
resetForm() {
this.projectName = '';
this.projectDescription = '';
this.projectScale = '';
this.selectedCategory = null;
this.selectedState = null;
this.selectedQue = null;
this.lianxi=null;
}
}
};
</script>
<style>
.container {
padding: 20px;
}
.form-item {
margin-bottom: 15px;
}
input {
padding: 10px;
justify-content: center;
margin: 10px 0;
border: 1px solid #ccc;
}
button {
width: 100%;
padding: 10px;
background-color: #007aff;
color: #fff;
border: none;
border-radius: 5px;
}
.picker {
justify-content: center;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #f9f9f9;
text-align: center;
}
</style>
云函数createProject
const cloud = require('crypto');
const db = uniCloud.database();
exports.main = async (event, context) => {
// 解构参数
const { name, description, category, scale, state,que,username,lianxi } = event;
// 检查必填参数是否存在
if (!name || !description || !category || !scale|| !state||!que||!username||!lianxi) {
return {
success: false,
error: '缺少必要的参数'
};
}
try {
// 写入数据到数据库
await db.collection('projects').add({
data: {
name,
description,
category,
scale,
state,
que,
username,
lianxi,
createdAt: new Date()
}
});
return {
success: true
};
} catch (error) {
console.error('Error adding project:', error);
return {
success: false,
error: error.message || '数据库写入失败'
};
}
};
云函数:checkProjectName
exports.main = async (event) => {
const { name } = event; // 从请求中获取项目名称
const db = uniCloud.database();
// 查询数据库,检查名称是否存在
const result = await db.collection('projects').where({
'data.name': name
}).get();
return result.data.length > 0; // 如果找到项目,返回 true
};
当用户点击“创建项目”按钮时,数据从前端流向后端云函数。此时,前端会收集所有输入的字段,并将其打包成一个对象,提交到云函数中进行处理(其中username直接使用注册登录时的本地数据),createProject云函数接收从前端传来的项目数据,并将其存储到数据库中。数据通过 event 参数传递给云函数后,将这些数据写入 projects 集合。checkProjectName云函数作用于在项目创建之前,需要验证项目名称是否已经存在,避免重名项目的创建。
3.3修改项目
<template>
<view class="container">
<view class="form-item">
<text>项目名称:</text>
<input v-model="projectName" placeholder="请输入项目名称" />
</view>
<view class="form-item">
<text>项目简介:</text>
<uni-easyinput
type="textarea"
autoHeight
v-model="projectDescription"
placeholder="请输入项目简介"
></uni-easyinput>
</view>
<view class="form-item">
<text>项目分类:</text>
<picker value="selectedCategory" :range="categories" @change="onCategoryChange">
<view class="picker">
{{ categories[selectedCategory] || '请选择项目分类' }}
</view>
</picker>
</view>
<view class="form-item">
<text>项目人数规模:</text>
<input
type="number"
v-model="projectScale"
placeholder="请输入目前参与项目的人数(请输入数字)"
@input="validateScale"
/>
</view>
<view class="form-item">
<text>是否缺人:</text>
<picker value="selectedQue" :range="ques" @change="onQueChange">
<view class="picker">
{{ ques[selectedQue] || '请选择' }}
</view>
</picker>
</view>
<view class="form-item">
<text>项目状态:</text>
<picker value="selectedState" :range="states" @change="onStateChange">
<view class="picker">
{{ states[selectedState] || '请选择项目状态' }}
</view>
</picker>
</view>
<view class="form-item">
<text>联系方式:</text>
<input v-model="lianxi" placeholder="请输入联系方式" />
</view>
<button @click="updateProject">修改项目</button>
</view>
</template>
<script>
export default {
data() {
return {
projectName: '',
projectDescription: '',
projectScale: '',
categories: ['自然科学', '工程技术', '医学健康', '社会科学', '人文历史', '交叉学科'],
states: ['准备中', '进行中', '已完结'],
ques: ['是', '否'],
selectedCategory: undefined,
selectedState: undefined,
selectedQue: undefined,
projectId: '' ,// 用于存储项目ID
lianxi:'',
};
},
onLoad(options) {
this.projectId = options.id; // 获取传递过来的项目 ID
this.getProjectDetails(this.projectId); // 加载项目的详细信息
},
methods: {
validateScale() {
const isValid = /^[0-9]*$/.test(this.projectScale);
if (!isValid) {
uni.showToast({
title: '请输入有效的数字',
icon: 'none'
});
this.projectScale = this.projectScale.replace(/[^0-9]/g, ''); // 只保留数字
}
},
async getProjectDetails(id) {
const res = await uniCloud.callFunction({
name: 'getProjectDetails',
data: { id }
});
if (res.result) {
const project = res.result;
this.projectName = project.data.name;
this.projectDescription = project.data.description;
this.selectedCategory = this.categories.indexOf(project.data.category);
this.projectScale = project.data.scale;
this.selectedState = this.states.indexOf(project.data.state);
this.selectedQue = this.ques.indexOf(project.data.que);
this.lianxi = project.data.lianxi;
}
},
async updateProject() {
if (!this.projectName || !this.projectDescription || this.selectedCategory === undefined || !this.projectScale || this.selectedState === undefined || this.selectedQue === undefined||!this.lianxi) {
uni.showToast({
title: '请填写所有字段',
icon: 'none'
});
return;
}
const projectData = {
id: this.projectId,
name: this.projectName,
description: this.projectDescription,
category: this.categories[this.selectedCategory],
scale: this.projectScale,
state: this.states[this.selectedState],
que: this.ques[this.selectedQue],
lianxi:this.lianxi,
};
try {
await uniCloud.callFunction({
name: 'updateProject',
data: projectData
});
uni.showToast({
title: '项目修改成功',
icon: 'success'
});
uni.navigateTo({
url: '/pages/myProject/myProject' // 修改成功后跳转到 myproject 页面
});
} catch (error) {
console.error(error);
uni.showToast({
title: '项目修改失败',
icon: 'none'
});
}
},
onCategoryChange(e) {
this.selectedCategory = e.detail.value;
},
onStateChange(e) {
this.selectedState = e.detail.value;
},
onQueChange(e) {
this.selectedQue = e.detail.value;
},
}
};
</script>
<style>
.container {
padding: 20px;
}
.form-item {
margin-bottom: 15px;
}
input {
padding: 10px;
justify-content: center;
margin: 10px 0;
border: 1px solid #ccc;
}
button {
background-color: #007AFF;
color: white;
padding: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.picker {
justify-content: center;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #f9f9f9;
text-align: center;
}
</style>
云函数getProjectDetails
// getProjectDetails.js
const db = uniCloud.database();
exports.main = async (event, context) => {
const { id } = event;
try {
const res = await db.collection('projects').doc(id).get();
if (res.data.length > 0) {
return res.data[0]; // 返回项目详细信息
} else {
return { error: '未找到该项目' };
}
} catch (error) {
return { error: error.message };
}
};
const db = uniCloud.database();
exports.main = async (event, context) => {
const { id, name, description, category, scale, state, que, lianxi } = event;
// 准备要更新的数据
const updateData = {};
// 将字段放入 data 对象中
updateData.data = {};
if (name) updateData.data.name = name;
if (description) updateData.data.description = description;
if (category) updateData.data.category = category;
if (scale) updateData.data.scale = scale;
if (state) updateData.data.state = state;
if (que) updateData.data.que = que;
if (lianxi) updateData.data.lianxi = lianxi;
try {
const res = await db.collection('projects').doc(id).update(updateData);
if (res.updated === 1) {
return { success: true };
} else {
return { error: '未更新任何项目或未找到项目' };
}
} catch (error) {
return { error: error.message };
}
};
当用户修改项目数据时,表单数据打包成 projectData 对象,并通过 uniCloud.callFunction 提交给云函数 updateProject。代码除了对所有字段必须全部有输入,项目人数输入进行数字验证外,还实现了每次点击编辑页面时,会显示上次内容的效果。页面加载时,getProjectDetails 云函数从数据库中读取项目的详细信息,并将数据返回前端显示在表单中,用户修改项目数据后,updateProject 云函数将更新的数据写入数据库,前端根据云函数返回的结果,进行成功或失败的提示和页面跳转。
4.附加特点设计与展示
将项目种类分为6大类,便于便于寻找心仪的项目
<template>
<view>
<view class="image1">
<view class="icon-container" @click="navigateToPage(1)">
<image class="icon" :src="src1"></image>
<text class="icon-text">自然科学</text>
</view>
<view class="icon-container" @click="navigateToPage(2)">
<image class="icon" :src="src2"></image>
<text class="icon-text">工程技术</text>
</view>
<view class="icon-container" @click="navigateToPage(3)">
<image class="icon" :src="src3"></image>
<text class="icon-text">医学健康</text>
</view>
</view>
<view class="image2">
<view class="icon-container" @click="navigateToPage(4)">
<image class="icon" :src="src4"></image>
<text class="icon-text">社会科学</text>
</view>
<view class="icon-container" @click="navigateToPage(5)">
<image class="icon" :src="src5"></image>
<text class="icon-text">人文历史</text>
</view>
<view class="icon-container" @click="navigateToPage(6)">
<image class="icon" :src="src6"></image>
<text class="icon-text">交叉学科</text>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
src1: "/static/1.png",
src2: "/static/2.png",
src3: "/static/3.png",
src4: "/static/4.png",
src5: "/static/5.png",
src6: "/static/6.png"
}
},
methods: {
navigateToPage(pageIndex) {
let targetUrl = '';
// 根据图片点击跳转到不同页面
switch(pageIndex) {
case 1:
targetUrl = '/pages/daohang1/daohang1';
break;
case 2:
targetUrl = '/pages/daohang2/daohang2';
break;
case 3:
targetUrl = '/pages/daohang3/daohang3';
break;
case 4:
targetUrl = '/pages/daohang4/daohang4';
break;
case 5:
targetUrl = '/pages/daohang5/daohang5';
break;
case 6:
targetUrl = '/pages/daohang6/daohang6';
break;
default:
console.error('未知的页面索引');
return;
}
// 跳转到目标页面
uni.navigateTo({
url: targetUrl
});
}
}
}
</script>
<style>
.image1, .image2 {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
width: 100%;
margin: 10px 0;
justify-content: space-between;
}
.icon-container {
display: flex;
flex-direction: column;
align-items: center;
width: 30%;
}
.icon {
width: 100%;
height: 200rpx;
border-radius: 30px;
}
.icon-text {
margin-top: 5px;
text-align: center;
}
</style>
5.目录说明和使用说明
5.1目录组织结构
uniCloud:存放与 UniCloud 云服务相关的内容,如云函数、数据库等。这是阿里云的云环境配置目录。
.hbuilderx:用于存储 HBuilderX IDE 的相关配置文件。
dist:用于存放项目的编译打包后的文件,供发布部署时使用。
pages:存放项目的页面文件
static:用于存放静态资源文件
uni_modules:存放 Uni-app 的插件模块
unpackage:这是 HBuilderX 的打包相关文件夹,包含编译后生成的中间
manifest.json:项目的全局配置文件,包括应用名称、版本号、图标等信息,Uni-app 的运行环境和平台打包相关配置也在这里定义。
pages.json:定义应用的页面路由、导航栏样式、页面标题等,是 Uni-app 页面管理的核心配置文件。
App.vue:,index.html:,main.js,readme.md,uni.promisify.adaptor.js,uni.scss:uniapp的HBuilderX编译环境自带模版文件,在编写项目工程中没有修改过
编写过程中主要编写到的文件目录有:uniCloud,pages,static,pages.json
5.2测试人员运行操作
搜索进入uniapp官方网站,注册一个账号,下载HbuilderX,联系QQ:2796608260,发送你的账号注册邮箱并说明是测试人员身份,我会将你添加为云空间的管理成员,鼠标右击目录中的uniCloud,关联云空间即可。
6.界面展示
登录界面
首页界面
创建项目
“我的”
我的项目
身份完善
登录及注册页面
打开app:点开app图标即可
登录:第一次登录需先注册,注册后输入用户名及密码即可登录
注册:输入用户名,输入两次密码即可注册
首页
分类导航功能:可以根据自己感兴趣的方向来查看对应的项目
搜索项目:可以根据关键词检索对应项目
推荐项目:会展示项目状态为“准备中”“进行中”项目的信息
创建项目:点击右上角加号,点击“创建项目”,填写项目详细信息,创建项目信息后跳转到项目详情页面。
创建项目页面
创建项目:填写项目名称、描述等信息,创建并且发布新项目。
我的项目列表
项目信息:点击“我的” 点击“我的项目”,您可以查看创建项目的详细信息,包括项目名称、描述。
修改项目:点击一个项目,进入项目详情界面,可以修改项目信息或删除项目
修改信息
修改/完善个人信息:在我的页面,点击对应按钮,可以更改自己的真实姓名,院系,身份等信息
7.单元测试
describe('ProjectDetail.vue', () => {
beforeEach(() => {
// 模拟 uniCloud 和 uni 对象
global.uniCloud = {
callFunction: jest.fn(),
};
global.uni = {
getStorage: jest.fn(),
navigateBack: jest.fn(),
showToast: jest.fn(),
};
});
it('应该在加载时从存储中加载项目数据', async () => {
const projectData = {
name: '测试项目',
description: '项目描述',
category: '分类A',
scale: '10',
que: '没有',
state: '进行中',
username: 'testuser',
lianxi: '123456789',
};
// 模拟从存储中获取项目数据
uni.getStorage.mockImplementation(({ success }) => {
success({ data: projectData });
});
const vm = {
projectData: null,
loading: false,
realname: '',
identity: '',
academy: '',
onLoad() {
// 加载项目数据并获取用户信息
uni.getStorage({
key: 'projectData',
success: (res) => {
this.projectData = res.data;
this.getUserData();
},
fail: () => {
console.log('没有找到项目数据');
},
});
},
getUserData: jest.fn(),
};
vm.onLoad();
expect(vm.projectData).toEqual(projectData); // 检查项目数据是否正确加载
expect(vm.getUserData).toHaveBeenCalled(); // 检查获取用户信息的函数是否被调用
});
it('应该根据项目用户名获取用户数据', async () => {
const userData = {
realname: '测试用户',
academy: '测试学院',
identity: '学生',
};
// 模拟云函数返回用户数据
uniCloud.callFunction.mockResolvedValue({
result: { code: 200, data: userData },
});
const vm = {
projectData: { username: 'testuser' },
loading: false,
realname: '',
identity: '',
academy: '',
getUserData: async function() {
this.loading = true; // 设置加载状态为 true
try {
const res = await uniCloud.callFunction({
name: 'getdetailbyusername',
data: { username: this.projectData.username },
});
if (res.result.code === 200 && res.result.data) {
const userData = res.result.data;
this.realname = userData.realname;
this.academy = userData.academy;
this.identity = userData.identity;
} else {
console.error('获取用户信息失败', res.result.message);
}
} catch (error) {
console.error('调用云函数失败', error);
} finally {
this.loading = false; // 完成加载
}
},
};
await vm.getUserData();
expect(vm.realname).toEqual(userData.realname); // 检查真实姓名是否正确
expect(vm.academy).toEqual(userData.academy); // 检查学院是否正确
expect(vm.identity).toEqual(userData.identity); // 检查身份是否正确
});
it('应该在获取用户信息失败时输出错误信息', async () => {
// 模拟云函数返回错误信息
uniCloud.callFunction.mockResolvedValue({
result: { code: 400, message: '用户不存在' },
});
const vm = {
projectData: { username: 'testuser' },
loading: false,
getUserData: async function() {
this.loading = true; // 设置加载状态为 true
try {
await uniCloud.callFunction({
name: 'getdetailbyusername',
data: { username: this.projectData.username },
});
} catch (error) {
console.error('调用云函数失败', error);
} finally {
this.loading = false; // 完成加载
}
},
};
await vm.getUserData();
// 这里可以添加对控制台输出的检查,例如使用 spy 来确认 console.error 被调用
});
it('应该在点击返回按钮时返回上一页', () => {
const vm = {
goBack() {
uni.navigateBack({
delta: 1,
});
},
};
vm.goBack();
expect(uni.navigateBack).toHaveBeenCalledWith({ delta: 1 }); // 检查返回操作是否被调用
});
});
测试的几个函数与思路(与代码中的测试一一对应)
项目数据加载:测试在 onLoad 中是否正确加载了项目数据(在详情页面可能会出现没有数据的情况,原因之一是onload函数中的getStorage函数或者是getUserdata函数调用失败)。
用户数据获取:测试从云函数获取用户数据是否正常工作,并验证正确的属性被设置。(获取的用户数据可能结构不对,与传入数据库的数据结构有关)
错误处理:模拟云函数调用失败,确保程序能妥善处理错误。(在某些情况下云函数可能调用失败)
加载状态:测试在获取用户数据时,加载状态是否正确设置。(加载状态会影响详情页面的显示)
导航功能:验证返回按钮的功能是否正常。(返回功能失败会导致界面不能正常跳转)
测试工具
HBuilderX uni-app自动化测试插件插件地址 要想进行单元测试 首先你应该配置好相应的环境,配置好环境之后,你就要学会编写单元测试代码 在此过程中 你可以询问chapgpt有关教程,要弄清编写单元测试的意义在哪,也就是这个核心函数或功能可能会遇到哪些情况,在此基础上继续思考可能出现的极端情况,并且最重要的一点,要搞起对应代码的核心逻辑,这也就是为什么单元测试最好由自己来编写,在梳理清楚要测试的代码逻辑后编写单元测试能顺利不少。
如何应对未来开发人员的***难
只能是尽量多学,多写代码,在不断地实践中熟悉各种各样可能会出现的情况,同时在书写代码的时候注意逻辑性,逻辑的严谨也有助于减少某些极端情况的产生。
8.Github的代码签入记录截图
9.遇到的代码模块异常或结对困难及解决方法
1.代码逻辑异常问题:this.getUserData()一开始没有放在success: (res)里,而是写在uni.getStorage()的下面,导致在username还没获取到时,getUserdata就开始运行导致获取不到项目数据,后来通过将项目数据具体化和console.log打印信息,梳理代码结构,发现并解决了这个问题。
2.结对困难问题:github文件上传后的合并处理,一开始经常出现合并文件冲突问题,后来通过更加频繁的交流以及详细的记录修改的所属文件,大大减少了合并冲突问题。
10.评价队友
黄森福
值得学习的地方:在项目开发合作过程中我的队友积极沟通,能够了解对方的开发进度,行动力强,自学能力强,对相关工具应用熟练,与其团队配合非常顺利。