【赢合系】软件工程课程第二次结对作业

赢合系Milky-Way
共赢·联合·跨系

这个作业属于哪个课程 https://edu.cnblogs.com/campus/fzu/SE2024/
这个作业要求在哪里 https://edu.cnblogs.com/campus/fzu/SE2024/homework/13281
这个作业的目标 通过编程还原上一次结对作业设计的原型模型,即开发一款协助跨系组队的小程序
学号 102202123张铭心 102202141黄昕怡

一、项目链接

二、编程分工

  • 初步分配:

    • 102202123张铭心
      • 登入界面:获取全局变量openid登录、注册、数据库内容导入、云函数编写
      • 我的界面:完善用户信息、修改个人主页
      • 发帖功能实现:创建发帖页面、寻找队友和自我推荐发帖功能
    • 102202141黄昕怡
      • 评论功能:在主页的评价功能创建、评价发帖功能添加
      • 私信功能:好友列表调取、私信界面、读取状态改变
      • 个人主页修改:添加上述功能、更新发帖显示
  • 实际过程:
    过程中每一项基本都由两人先后参与或共同完成,两人都在互相修正一些逻辑

三、编程过程

初次接触小程序开发,过程难免碰壁
前端后端构建从入门到撞门

以下编程顺序是发布作业初期的摸索结果
该点将详细解释开发思路与设计实现方法
其中关键代码展示将折叠或放置在亮点展示

  • 注:function()界面pages/云数据库关键变量/操作

流程图

整体架构

主要功能页面确定后,先创建以下界面:

  • 广场界面pages/square/square
  • 私信界面pages/friends/friends
  • 发帖界面pages/add/add
  • 我的界面pages/myself/myself

为了提高用户的使用体验,我们添加了tabBar,提供用户更直观的使用引导

登入页面创建(用户创建与登录)

必要性: 当前登录用户openid获取
在摸索阶段我们发现:
后续所有的步骤测试与界面搭建都与当前登录用户相关
先保证用户的开设,才能更好地在过程中实现开发与调试穿插,以不断完善出我们理想的功能

1.注册

  • 点击登录键下方若无账号请先 注册字样进入注册界面pages/register/register
  • 进入后填写学工号(nickname)密码(password),并再次确认密码
  • 新用户可从头像选择器selectAvatar中选择一张初始头像
  • register:验证注册填写情况
  • 云函数register:将新用户的学工号(nickname)密码(password)、初始头像选择(avatarUrl)写入数据库
register云函数(注册处理)
const cloud = require('wx-server-sdk');

cloud.init({
  env: 'fufubuff-3gt0b01y042179cc'
});
// 云函数入口函数
exports.main = async (event, context) => {
  const { nickname, password } = event;

  // 查询用户信息
  const db = cloud.database();
  const users = db.collection('users');
  
  try {
    const res = await users.where({
      nickname: nickname,
      password: password // 注意:实际应用中,密码应进行加密处理
    }).get();

    if (res.data.length > 0) {
      return { openid: res.data[0].openid };
    } else {
      return { error: '学工号或密码错误' };
    }
  } catch (err) {
    console.error('查询用户失败', err);
    return { error: '查询用户失败,请稍后再试' };
  }
};
描述

2.登录

  • 注册后跳转至登入界面pages/dengru/newpage
  • 云函数login
    已经注册的用户在该界面输入对应的学工号(nickname)密码(password)
    点击登录键后立即匹配数据库users中的信息,匹配正确即登录成功
  • checkLoginStatus:登录成功获取登录用户openid,跳转至我的界面pages/myself/myself
login云函数(登录处理)
// cloudfunctions/register/index.js
const cloud = require('wx-server-sdk');

cloud.init({
  env: 'fufubuff-3gt0b01y042179cc' // 使用当前环境
});

const db = cloud.database();

exports.main = async (event, context) => {
  const { nickname, password, avatarUrl } = event;

  console.log('云函数 register 接收到的数据:', event);

  // 输入验证
  if (!nickname || !password) {
    console.log('昵称和密码不能为空');
    return {
      success: false,
      message: '昵称和密码不能为空'
    };
  }

  if (password.length < 8) {
    console.log('密码长度不足');
    return {
      success: false,
      message: '密码至少8位'
    };
  }

  const openid = cloud.getWXContext().OPENID;

  console.log('当前用户的 openid:', openid);

  try {
    // 检查用户是否已存在
    try {
      await db.collection('users').doc(openid).get();
      // 如果获取到数据,说明用户已存在
      console.log('用户已存在');
      return {
        success: false,
        message: '用户已注册'
      };
    } catch (err) {
      if (err.errCode === -1 && err.errMsg.includes('does not exist')) {
        // 用户不存在,可以继续注册
        console.log('用户不存在,可以注册');
      } else {
        // 其他错误,返回注册失败
        console.error('查询用户失败:', err);
        return {
          success: false,
          message: '注册失败,请稍后再试'
        };
      }
    }

    // 添加新用户
    await db.collection('users').doc(openid).set({
      data: {
        openid: openid,
        nickname: nickname,
        password: password, // 建议对密码进行哈希处理
        avatarUrl: avatarUrl || '/images/default-avatar.jpg',
        createdAt: db.serverDate() // 使用数据库服务器时间
      }
    });

    console.log('用户注册成功');

    return {
      success: true,
      message: '注册成功'
    };
  } catch (err) {
    console.error('注册失败:', err);
    return {
      success: false,
      message: '注册失败,请稍后再试'
    };
  }
};

描述

3.必要性实现——获取openid

  • 登录成功的同时获取该用户的openid为全局变量user_openid
  • 通过全局变量user_openid对应从数据库调取后续界面应有信息
  • 全局变量user_openid:初始化为null,用户登录后获取该用户的openid并本地缓存

我的页面创建(用户信息更改)

由于后续发帖、评价、私信功能的实现都需要以用户为前提,
完善当前用户信息是关键一步
此处完善的用户信息较多,还包括该用户发帖与评价信息展示

1.主页面创建

我的界面pages/myself/myself 实现功能(按钮button):

  • 基础显示用户信息
  • 修改资料
    (修改后会相应更新该界面的姓名(name)、学历层次(degree)、个性签名(signature)、学院(college)、合作方向(researchAreas)
  • 退出登录
  • 招聘信息
  • 历史评价

2.修改资料

修改资料界面pages/editProfile/editProfile

  • 构建目的:
    • 完善用户信息:新用户可初次填写自己的相关信息
    • 修改用户信息:相关数据已保存在数据库中的用户可以修改任意资料数据
  • 云函数getUserInfo:读取修改资料的内容,写入数据库users,且保证再次点击时会调取登录用户已有信息
描述

3.招聘信息和历史评价查看界面

主页界面pages/profile/profile

  • 通过user_openid调取登录用户相关信息
  • 在初期先完成部分个人信息显示:登录用户的姓名(name)、个性签名(signature)、头像(avatarUrl)
  • 后期发帖功能完善后更新发布的自我推荐寻找队友帖子

4.头像选择器

  • changeAvatar:在修改资料界面与主页界面中点击头像即可跳转至头像选择界面/pages/avatarSelect/avatarSelect,选择心仪的头像后点击确认选择换上即可
描述

发帖页面创建(发帖功能实现)

用户信息完善之后,下一步是为用户开发核心功能
也就是发帖寻找志同道合的队友
具体实现功能:接收成功发送的帖子信息并调用函数方法将其存入数据库

1.主页面创建

发帖界面pages/add/add 两大功能按钮:

  • 自我推荐——数据库resumes
  • 寻找队友——数据库recruitmentPosts

2.自我推荐

自我推荐发帖界面pages/self_recommendation/self_recommendation

  • getNameByOpenid:进入该页面会读取登录用户openid,并跳出欢迎 {name}
  • 当前登录用户可在该界面编辑自荐帖信息:
    意向项目(intentProject)、专业(major)、擅长技能(skills)、联系方式(contactInfo)、自我介绍详情(selfIntroduction)
  • submitDetails
    • 检查发帖信息是否填写完整
    • 读取自我推荐的发帖内容,连同当前登录用户姓名(name)和openid写入数据库resumes
submitDetails(自我推荐发帖)
  submitDetails: function() {
    const { intentProject, major, skills, contactInfo, selfIntroduction, name, email } = this.data;
    const openid = wx.getStorageSync('user_openid');

    if (!intentProject || !major || !skills || !contactInfo || !selfIntroduction) {
      wx.showToast({
        title: '请填写完整信息',
        icon: 'none'
      });
      return;
    }

    const db = wx.cloud.database();
    db.collection('resumes').add({
      data: {
        _id: db.serverDate(),  // 使用服务器生成的唯一ID
        email: contactInfo,
        name: name,
        openid: openid,
        time: new Date().toLocaleString(),
        intentProject: intentProject,
        major: major,
        skills: skills,
        selfIntroduction: selfIntroduction,
        contactInfo: contactInfo,
      },
      success: function(res) {
        wx.showToast({
          title: '发布成功',
          icon: 'success'
        });
        wx.navigateBack();
      },
      fail: function(err) {
        wx.showToast({
          title: '发布失败',
          icon: 'none'
        });
        console.error('发布自我招聘失败:', err);
      }
    });
  }

描述

3.寻找队友

寻找队友发帖界面pages/find_team/find_team

  • initData:进入该页面即调取当前登录用户信息,在左上角显示对应头像(avatarUrl)与姓名(name)数据库users
  • 当前登录用户可在该界面编辑招募帖信息:
    联系方式(contact)、人数缺口(peopleNeeded)、项目名称(title)、项目资料(content)、人员需求(personNeed)、关键词(tags)
    enable/saveProjectDescriptionEdit:点击编辑修改项目资料(content)
    enable/savePersonNeedEdit:点击编辑修改人员需求(personNeed)
  • setDisplay(Person)Text:编辑完毕后,点击完成提交修改结果
    通过truncateText方法,以省略号替代过长(最大显示字数:80字)的文本字段,在编辑页面展示修改结果
  • submitDetails
    • 检查发帖信息是否填写完整
    • 读取寻找队友的发帖内容,随机生成一个招募贴id,连同发布人的openid一并写入数据库recruitmentPosts
submitDetails(寻找队友发帖)
  submitDetails: function () {
    const { contact, peopleNeeded, projectDescription, personNeed, keyword1, keyword2, keyword3,projectNamePlaceholder } = this.data;

    if (!contact || !peopleNeeded || !projectDescription || !personNeed || !keyword1 || !keyword2 || !keyword3 || !projectNamePlaceholder) {
      wx.showToast({
        title: '请填写完整信息',
        icon: 'none'
      });
      return;
    }

    // 获取当前用户的 openid
    let openid = wx.getStorageSync('user_openid');

    if (!openid) {
      wx.showToast({
        title: '用户未登录,请先登录',
        icon: 'none'
      });
      wx.navigateTo({
        url: '/pages/dengru/newpage' // 跳转到登录页面
      });
      return;
    }

    // 数据库集合名称为 'recruitmentPosts',请根据需要更改
    db.collection('recruitmentPosts').add({
      data: {
        _id: db.serverDate(), // 或者使用自定义唯一ID
        id: Math.floor(Math.random() * 1000), // 生成一个随机id,可根据需要进行修改
        openid: openid, // 添加发布人的 openid
        tags: [keyword1, keyword2, keyword3], // 数据库中保存关键词作为 
        time: new Date().toLocaleString(), // 保存当前的日期和时间
        title: projectNamePlaceholder,
        content:projectDescription,
         // 保存项目描述为标题
        contact: contact, // 添加联系方式
        peopleNeeded: peopleNeeded, // 添加人员需求
        personNeed: personNeed, // 添加人员需求详细信息
      },
      success: res => {
        wx.showToast({
          title: '数据保存成功',
          icon: 'success'
        });
        this.clearUserData(); // 保存成功后清除数据
        wx.navigateBack(); // 可选:返回上一页
      },
      fail: err => {
        console.error('数据保存失败:', err);
        wx.showToast({
          title: '数据保存失败',
          icon: 'none'
        });
      }
    });
  },
  • clearUserData:由于上述所有输入编辑均以变量形式存储并传递显示,发帖结束需要清空所有变量的内容
描述

广场界面创建(组队信息容器)

现已实现发帖功能,其内容的对外展示也是必要的步骤
批量推广和可阅读才能真正满足用户的招募需求
我们下一步便搭建了展示发帖信息的容器
用户可点击跳转两大板块并滑动翻阅,筛选心仪项目,相遇志同道合的伙伴
当然,也可以抓住机会,主动联系,选择一同奋斗的队友

1.主页面创建

广场界面pages/square/square 两大板块:

  • 简历投放——数据库resumes
  • 招募队友——数据库recruitmentPosts

在该界面显示的是数据库中存储的所有投稿内容
两个板块的跳转:采用switchTab方法,点击对应按钮触发tabIndex的改变以实现跳转

2.简历投放

tabIndex === 0

  • 该界面调取了数据库resumes中的所有内容,按存入时间顺序从上往下纵向排放
  • fetchResumesData:每一条内容对应一个帖子,在广场页面仅展示头像(avatarUrl)、姓名(name)、联系方式(contactInfo)、发布时间(time)
  • 点击帖子 跳转详情页面pages/recruitmentDetail/recruitmentDetail
    可以查看自我推荐详情:联系方式(contact)、人数缺口(peopleNeeded)、项目名称(title)、项目资料(content)、人员需求(personNeed)、关键词(tags)
描述

3.招募队友

tabIndex === 1

  • 该界面调取了数据库recruitmentPosts中的所有内容,按存入时间顺序从上往下纵向排放
  • fetchRecruitmentPostsData:每一条内容对应一个帖子,在广场页面仅展示项目标题(title)、发布时间(time)、关键词(tags)
  • 点击帖子 跳转详情页面pages/find_team_detail/find_team_detail
    可以查看寻找队友详情:意向项目(intentProject)、专业(major)、擅长技能(skills)、联系方式(contactInfo)、自我介绍详情(selfIntroduction)
描述

私信页面创建(聊天功能实现)

在发帖功能基本完成的基础上,为了提高应用程序的交互性,
我们开发了私信功能,可以和所有已注册的用户进行私信
方便用户发起项目沟通,跟进项目进度
偶尔闲聊也是可以的哦!请注意敏感字^^

1.好友列表

私信界面pages/friends/friends

  • fetchFriendsWithUnreadCount

    • 数据库users中获取除自己外的所有已注册用户,并列出在该界面
    • 红点显示未读信息条数
  • 页面上方附有搜索栏,方便用户查找聊天对象
    performSearch:输入时检索对象(以nickname为索引),实时更新搜索结果,输入值为空时显示所有用户

描述
  • 登录用户可点击好友列表中的任意用户,与ta发起私聊
    onSelectFriend:点击私聊对象时获取其openid,跳转至该openid下的消息界面pages/chat/chat

2.私信页面

消息界面pages/chat/chat

  • sendMessage
    • 底部输入栏中编辑消息,点击发送即可发送消息
    • 发送后的消息先写入数据库messages,并从中调取,连同读取状态和发送时间显示在界面中
      注:消息接收有延迟,需要刷新页面接收新消息
  • checkMessageContent:发送消息敏感字检测,请文明聊天
  • 发送消息时间、消息读取状态透明

    描述


    描述

3.快捷性

  • 点击广场-简历投放中发布人的头像我的-招聘信息/历史评价都可进入对应用户的主页界面pages/profile/profile
    点击底部右侧私信可以与ta发起对话(pages/chat/chat
    为了同步该功能,当然也可以与自己发起聊天,就请当作一个备忘录使用吧!:)

评价功能实现

登录注册、修改资料、发送帖子、浏览帖子、私信模式主要功能基本完成后
为了丰富用户使用体验
可以查看对方的历史评价,在联系前更进一步了解队友
当然也可以留下你对ta的评价

1.主页评价

  • 点击广场-简历投放中发布人的头像我的-招聘信息/历史评价都可进入对应用户的主页界面pages/profile/profile
  • 在该页面可以查看ta的个人合作评价
  • 点击底部左侧评价可以为ta的主页增加评价信息(pages/evaluation/evaluation

2.评价编辑

评价页面pages/evaluation/evaluation

  • selectRating:评分选择器
  • submitEvaluation
    验证评分等级选择(ratingText)与评价内容(content)编辑
    读取评价对象、发布用户、评价详情,并导入数据库cooperationReviews

项目状态变动

上述功能基本完善后,我们注意到项目的展示较为呆板,缺乏项目动态变化
故优化项目展示模式

1.发帖人权限

  • 寻找队友的贴主可以编辑自己发布的帖子
  • 新增招募进度栏,以便其他用户查看详情时知晓项目招募进度
  • 非贴主无权限修改
描述

2.项目状态

  • 新增了项目动态:
    • 人员招满
    • 人员退出
    • 项目结束
描述

UI升级

一个小程序也需要美观的界面设计
在最后我们进行了如下修改

1.界面壁纸
使用了清新的chiikawa图片壁纸作为应用背景

2.完善每个页面的引导名称
修改了每个代码页面的.json文件中的navigationBarTitleText

3.统一按键色调
蓝紫色调为主

四、亮点功能

个人页面展示

1.设计意义
随着功能的不断完善,我们认为应该有一个面向他人展示的个人主页
在该界面上可以查看ta的部分个人信息,包括评价与发布项目
并且我们希望能提高用户的使用体验,让用户在看完帖子后可以更快捷地与他人发起联系
所以该页面也成为了帖子与私信、评价的桥梁

2.设计思路
界面pages/profile/profile分为三个容器:

  • 顶部个人资料loadUserInfo
  • 个人合作评价和组队招募信息切换按钮switchTab
  • 固定在最下方评价按钮openEvaluation私信按钮openChat快捷性
    底部该用户发帖信息滑动部分
    • 获取当前用户关联的合作评价数据 fetchCooperationReviewsData
    • 获取与当前用户关联的组队招募数据 fetchTeamRecruitmentsData(可查看详情viewDetails

3.代码展示

由于篇幅问题,该点代码折叠展示

loadUserInfo(加载用户信息)
  loadUserInfo: function (userOpenid) {
    const that = this;

    if (!userOpenid) {
      return Promise.reject('未找到 userOpenid,请确保用户已登录');
    }

    console.log('当前用户的 userOpenid:', userOpenid);
    const usersCollection = db.collection('users');

    // 假设 'openid' 是用户文档的 _id,如果不是,请使用 where 查询
    return usersCollection.doc(userOpenid).get()
      .then(res => {
        if (res.data && Object.keys(res.data).length !== 0) {
          console.log('获取到的用户信息:', res.data);
          that.setData({
            name: res.data.name || '',
            signature: res.data.signature || '',
            avatarUrl: res.data.avatarUrl || 'cloud://fufubuff-3gt0b01y042179cc.6675-fufubuff-3gt0b01y042179cc-1330048678/images/default-avatar.jpg',
            backgroundUrl: res.data.backgroundUrl || 'cloud://fufubuff-3gt0b01y042179cc.6675-fufubuff-3gt0b01y042179cc-1330048678/images/wsp_background.png'
          });
        } else {
          console.log('用户信息不存在,可能需要注册');
          wx.showToast({
            title: '用户信息不存在,请先注册',
            icon: 'none'
          });
          wx.navigateTo({
            url: '/pages/register/register' // 跳转到注册页面
          });
          return Promise.reject('用户信息不存在');
        }
      })
      .catch(err => {
        console.error('获取用户信息失败:', err);
        wx.showToast({
          title: '获取用户信息失败',
          icon: 'none'
        });
        return Promise.reject(err);
      });
  }
switchTab(切换按钮实现)
 switchTab: function(e) {
    const tabIndex = parseInt(e.currentTarget.dataset.index, 10); // 将字符串转换为数字
    this.setData({
      tabIndex: tabIndex
    });
    // 根据选中的标签索引加载相应的数据
    if (tabIndex === 1) {
      this.fetchTeamRecruitmentsData(this.data.userOpenid);
    } else if (tabIndex === 0) {
      this.fetchCooperationReviewsData();
    }
  }
fetchCooperationReviewsData(获取评价)
  fetchCooperationReviewsData: function () {  
    const that = this;  
    that.setData({ isLoading: true }); // 开始加载
    db.collection('cooperationReviews')
      .where({
        targetOpenid: this.data.userOpenid
      })
      .get()
      .then(res => {
        if (res.data.length === 0) {  
          that.setData({  
            noReviews: true, // 没有评价数据时显示提示  
            cooperationReviews: [] // 清空现有评价
          });  
        } else {  
          // 需要手动关联获取评价者的信息
          const reviewerOpenids = res.data.map(item => item.reviewerOpenid);
          db.collection('users').where({
            _id: db.command.in(reviewerOpenids)
          }).get().then(userRes => {
            const usersMap = {};
            userRes.data.forEach(user => {
              usersMap[user._id] = user;
            });
            const cooperationReviews = res.data.map(item => ({
              ...item,
              reviewerName: usersMap[item.reviewerOpenid]?.name || '匿名',
              reviewerAvatarUrl: usersMap[item.reviewerOpenid]?.avatarUrl || 'cloud://fufubuff-3gt0b01y042179cc.6675-fufubuff-3gt0b01y042179cc-1330048678/images/default-avatar.jpg'
            }));
            that.setData({  
              cooperationReviews, // 更新合作评价数据  
              noReviews: false, // 有评价数据时不显示提示  
            });  
            that.setData({ isLoading: false }); // 结束加载
          }).catch(err => {
            console.error('获取评价者信息失败:', err);
            wx.showToast({  
              title: '获取评价者信息失败',  
              icon: 'none'
            });  
            that.setData({ isLoading: false }); // 结束加载
          });
        }  
      })  
      .catch(err => {  
        console.error('获取合作评价数据失败:', err);  
        wx.showToast({  
          title: '获取合作评价失败',  
          icon: 'none'
        });  
        that.setData({ isLoading: false }); // 结束加载
      });  
  }
fetchTeamRecruitmentsData(获取组队招募)
  fetchTeamRecruitmentsData: function (userOpenid) {  
    const that = this;  
    db.collection('recruitmentPosts').where({  
      openid: userOpenid, // 根据您的数据库结构调整查询条件  
    }).get()  
      .then(res => {  
        if (res.data.length === 0) {  
          that.setData({  
            noRecruitments: true, // 没有招募数据时显示提示  
          });  
        } else {  
          that.setData({  
            teamRecruitments: res.data,  
            noRecruitments: false, // 有招募数据时不显示提示  
          });  
        }  
      })  
      .catch(err => {  
        console.error('获取组队招募数据失败:', err);  
        wx.showToast({  
          title: '获取组队招募失败',  
          icon: 'none'  
        });  
      });  
  }
openChat(私信按钮点击事件处理)
  openChat: function() {
    const userOpenid = this.data.userOpenid;  // 从页面的数据中获取当前用户的 openid
  
    if (!userOpenid) {
      wx.showToast({
        title: '用户未登录',
        icon: 'none'
      });
      wx.navigateTo({
        url: '/pages/dengru/newpage' // 登录页面路径
      });
      return;
    }
  
    console.log('userOpenid for chat:', userOpenid);
    wx.navigateTo({
      url: `/pages/chat/chat?chatUserId=${userOpenid}` // 跳转到聊天页面并传递 userOpenid
    });
  }
openEvaluation(评价按钮点击事件处理)
  openEvaluation: function() {
    const targetOpenid = this.data.userOpenid; // 目标用户的 openid

    if (!targetOpenid) {
      wx.showToast({
        title: '无法获取目标用户信息',
        icon: 'none'
      });
      return;
    }

    wx.navigateTo({
      url: `/pages/evaluation/evaluation?targetOpenid=${targetOpenid}` // 传递目标用户的 openid
    });
  }

4.成果展示

描述

私信功能

1.设计意义
为了提高应用交互性,我们在原型基础上重磅推出聊天系统
所有已注册用户都在私信列表中,用户只需运用搜索功能就能快速锁定联系对象
聊天消息提示清晰,便于用户及时处理未读消息

2.设计思路

好友列表显示

  • fetchFriendsWithUnreadCount
    • 数据库users中获取除自己外的所有已注册用户,并列出在该界面
    • 红点显示未读信息条数
  • 页面上方附有搜索栏,方便用户查找聊天对象
    performSearch:输入时检索对象(以nickname为索引),实时更新搜索结果,输入值为空时显示所有用户

点击好友列表中的任意用户,与ta发起私聊

  • onSelectFriend`:点击私聊对象时获取其openid,跳转至该openid下的消息界面pages/chat/chat
  • sendMessage
    • 底部输入栏中编辑消息,点击发送即可发送消息
    • 发送后的消息先写入数据库messages,并从中调取,连同读取状态和发送时间显示在界面中
  • checkMessageContent:发送消息敏感字检测
  • markMessagesAsRead:标记所有从对方发送给当前用户的消息为已读

3.代码展示

好友列表展示

  fetchFriendsWithUnreadCount: function() {
    const that = this;
    const { pageSize, currentPage } = this.data;
    this.setData({ isLoading: true });

    db.collection('users')
      .where({
        openid: db.command.neq(that.data.currentUserOpenId) // 排除当前用户
      })
      .skip((currentPage - 1) * pageSize)
      .limit(pageSize)
      .get({
        success: async function(res) {
          const friends = res.data;
          // 获取好友总数
          db.collection('users').where({
            openid: db.command.neq(that.data.currentUserOpenId)
          }).count().then(countRes => {
            that.setData({
              totalCount: countRes.total
            });
          });

          // 为每个好友获取未读消息数量
          const friendsWithUnread = await Promise.all(friends.map(async (friend) => {
            const count = await that.getUnreadCount(friend.openid);
            return {
              ...friend,
              unreadCount: count
            };
          }));
          const newFriends = that.data.friends.concat(friendsWithUnread);
          that.setData({
            friends: newFriends,
            currentPage: that.data.currentPage + 1,
            isLoading: false
          });
        },
        fail: function(err) {
          console.error('获取好友列表失败:', err);
          wx.showToast({
            title: '获取好友列表失败:' + err.message,
            icon: 'none'
          });
          that.setData({ isLoading: false });
        }
      });
  }

搜索好友功能

  performSearch: function() {
    const keyword = this.data.searchKeyword.trim();
    const that = this;

    if (keyword === '') {
      // 如果搜索关键词为空,显示全部好友列表
      this.setData({
        isSearching: false,
        searchResults: []
      });
      return;
    }

    this.setData({
      isSearching: true,
      searchResults: [],
      isLoading: true
    });

    // 使用正则表达式进行模糊搜索,排除当前用户
    db.collection('users')
      .where({
        openid: db.command.neq(that.data.currentUserOpenId), // 排除当前用户
        nickname: db.RegExp({
          regexp: keyword,
          options: 'i' // 不区分大小写
        })
      })
      .get({
        success: async function(res) {
          console.log('搜索结果:', res.data);
          const results = await Promise.all(res.data.map(async (user) => {
            const count = await that.getUnreadCount(user.openid);
            const regex = new RegExp(`(${keyword})`, 'gi');
            const highlightedNickname = user.nickname.replace(regex, '<span style="color: #3cc51f;">$1</span>');
            return {
              ...user,
              highlightedNickname,
              unreadCount: count
            };
          }));
          that.setData({
            searchResults: results,
            isLoading: false
          });
          if (res.data.length === 0) {
            wx.showToast({
              title: '未找到匹配的用户',
              icon: 'none'
            });
          }
        },
        fail: function(err) {
          console.error('搜索失败:', err);
          that.setData({
            isLoading: false
          });
          wx.showToast({
            title: '搜索失败',
            icon: 'none'
          });
        }
      });
  },

消息发送

  sendMessage: function(content) {
    const { currentUserId, chatUserId, currentUserInfo } = this.data;
    if (!currentUserId || !chatUserId) {
      wx.showToast({
        title: '用户信息不完整',
        icon: 'none'
      });
      return;
    }

    const newMessage = {
      fromUserId: currentUserId,
      toUserId: chatUserId,
      content: content,
      timestamp: db.serverDate(), // 使用服务器时间
      isRead: false,
      fromAvatarUrl: currentUserInfo.avatarUrl,
      fromNickname: currentUserInfo.nickname,
    };

    db.collection('messages').add({
      data: newMessage
    }).then(res => {
      // 获取添加的消息,以获取服务器时间戳
      db.collection('messages').doc(res._id).get().then(docRes => {
        const messageWithTime = {
          ...docRes.data,
          formattedTime: this.formatTime(docRes.data.timestamp)
        };
        // 立即将消息添加到 messageList
        this.setData({
          messageList: this.data.messageList.concat([messageWithTime]),
          inputContent: '',
          scrollIntoView: 'msg-' + (this.data.messageList.length)
        });
        this.scrollToBottom();
      }).catch(err => {
        console.error('获取新消息失败', err);
        wx.showToast({
          title: '消息发送失败',
          icon: 'none'
        });
      });
    }).catch(err => {
      console.error('消息发送失败', err);
      wx.showToast({
        title: '消息发送失败',
        icon: 'none'
      });
    });
  }

已读/未读状态

  markMessagesAsRead: function() {
    const { currentUserId, chatUserId } = this.data;
    db.collection('messages').where({
      fromUserId: chatUserId,
      toUserId: currentUserId,
      isRead: false
    }).update({
      data: {
        isRead: true
      },
      success: res => {
        console.log('已读消息更新成功', res);
        // 不再需要手动调用 fetchFriendsWithUnreadCount
        // 因为 friends.js 的监听器会自动更新未读计数
      },
      fail: err => {
        console.error('更新已读消息失败', err);
        wx.showToast({
          title: '更新已读消息失败',
          icon: 'none'
        });
      }
    });
  }

敏感字检测

  checkMessageContent: function(content) {
    // 可选的内容安全检查逻辑
    return new Promise((resolve, reject) => {
      // 示例:禁止发送特定词语
      const forbiddenWords = ['张栋', "黄昕怡", "张铭心", "傻逼", "他妈", "吴越钟"];
      const containsForbidden = forbiddenWords.some(word => content.includes(word));
      if (containsForbidden) {
        reject('包含敏感词');
      } else {
        resolve();
      }
    });
  },

4.成果展示

描述

评价功能

1.设计意义

给用户提供更全面、主观的队友选择判断依据

2.设计思路

评价编辑

  • selectRating:评分选择器
  • submitEvaluation
    验证评分等级选择(ratingText)与评价内容(content)编辑
    读取评价对象、发布用户、评价详情,并导入数据库cooperationReviews

3.代码展示

提交评价

  submitEvaluation: function(e) {
    const { rating,ratingText, content, evaluatorOpenid, targetOpenid } = this.data;

    // 验证评分
    if (rating < 1 || rating > 5) {
      wx.showToast({
        title: '评分请输入1-5之间的数字',
        icon: 'none'
      });
      return;
    }

    // 验证评价内容
    if (!content.trim()) {
      wx.showToast({
        title: '评价内容不能为空',
        icon: 'none'
      });
      return;
    }

    if (content.length < 10) {
      wx.showToast({
        title: '评价内容至少10字',
        icon: 'none'
      });
      return;
    }

    // 验证 openid
    if (!evaluatorOpenid || !targetOpenid) {
      wx.showToast({
        title: '缺少用户信息',
        icon: 'none'
      });
      return;
    }

    // 使用自定义时间格式
    const currentTime = formatTime(new Date());

    // 添加评价到 'cooperationReviews' 集合
    const db = wx.cloud.database();
    db.collection('cooperationReviews').add({
      data: {
        reviewerOpenid: evaluatorOpenid,
        targetOpenid: targetOpenid,
        content: content,
        ratingText: ratingText,
        time: currentTime // 使用格式化的字符串时间
      }
    }).then(res => {
      wx.showToast({
        title: '评价提交成功',
        icon: 'success'
      });
      wx.navigateBack(); // 返回上一页
    }).catch(err => {
      console.error('评价提交失败:', err);
      wx.showToast({
        title: '评价提交失败,请重试',
        icon: 'none'
      });
    });
  }
});
function formatTime(date) {
  const year = date.getFullYear();
  const month = (date.getMonth() + 1).toString().padStart(2, '0');
  const day = date.getDate().toString().padStart(2, '0');
  const hour = date.getHours().toString().padStart(2, '0');
  const minute = date.getMinutes().toString().padStart(2, '0');
  const second = date.getSeconds().toString().padStart(2, '0');
  
  return `${year}-${month}-${day} ${hour}:${minute}`;
}

4.成果展示

描述

五、项目说明书

目录组织

README目录组织如下:

描述

测试指南

由于小程序审核未通过,测试需要在管理端加入测试人员才可进行小程序实操
如果您需要,麻烦私戳我们要二维码,感谢配合!!
二维码维持时间为20min

安卓

描述

ios

描述

测试人员添加

描述

六、单元测试

由于我们选择开发一款小程序,单元测试采用软件自带的扫码真机测试进行测试
在完成了小程序的开发后,我们把小程序投入测试,邀请并添加了部分测试人员
以下展示测试人员评价、使用后台记录等内容

扫码真机调试

1.使用后台记录
这是某测试人员在进行真机调试时,管理员后台显示照:

描述

2.测试人员使用反馈

管理员okiqiiii:发帖后广场调取出错
管理员fufubuff:发帖编辑键错乱
测试人员1:资料修改失败

描述

测试人员2:界面较卡,头像更新有点慢

描述

测试人员3:信息的已读未读没变化
上传小程序审核员:未注重用户隐私与数据安全

描述

其他用户反馈:

描述 描述

错误持续更新中......

修正调试问题

1.广场调取出错问题
问题:

  • 原本原型设计时忘记考虑项目名称的输入,编程后续开始思考数据库的读写才把项目资料字样更改为可输入的项目名称
  • 只修改了前端wxml、wxss文件,忘记更改js文件中写入数据库相关的方法submitDetails

解决:

  • 修改submitDetails前半部分:
    增加项目标题变量projectNamePlaceholder
submitDetails: function () {
    const { contact, peopleNeeded, projectDescription, personNeed, keyword1, keyword2, keyword3,projectNamePlaceholder } = this.data;
    if (!contact || !peopleNeeded || !projectDescription || !personNeed || !keyword1 || !keyword2 || !keyword3 || !projectNamePlaceholder) {
      wx.showToast({
        title: '请填写完整信息',
        icon: 'none'
      });
      return;
    }

2.发帖编辑键错乱
问题:

  • 编辑完的项目资料/人员需求详情内容没有保存
  • 截断文本功能没有体现

解决:

  • 编辑文本后需要点击完成键才能显示截断文本结果,按钮变化wxml代码如下:
<button class="edit-button" bindtap="enablePersonNeedEdit">编辑</button>
<button class="save-button" bindtap="savePersonNeedEdit">完成</button>
  • wxss文件中仅编辑了edit-button,错误地将save-button编辑成了button
  • 修改wxss
.edit-button, .save-button {  
  position: absolute;  
  top: 8px; /* 与白框的上边距保持一致 */  
  right: 20px; /* 与白框的右边距保持一致 */  
}  

3.更新加载慢
问题:

  • 修改资料、头像、发帖、聊天更新

思考:

  • 数据库读写需要一定时间,刷新即可解决

4.修改个人资料失败
问题:

  • 编译后app.json页面缺失,有页面被误删,影响后续功能

解决:

  • 重新找回了缺失页面,修改成功

5.消息读取变化状态不变
问题:

  • 聊天功能可进行,但反复读取消息仍为未读

解决:

  • 修改wxml:
<text class="name-or-nickname">{{ item.name ? item.name : item.nickname }}</text>

6.小程序审核问题
问题:

  • 未注重用户隐私与数据安全
  • 后续更新审核问题:
描述

解决:

  • 第二条由于权限与隐私问题无法正式过审,我们二人无能申请企业级微信小程序💧
  • 为解决第一条问题,我们新创建了一个隐私页面pages/privacy/privacy,作为登录前询问用户使用意愿的提醒框
描述

用户功能测试界面

1.发帖

寻找队友

描述

自我推荐

描述

2.评价

描述

3.聊天

描述

七、编程难点

1.项目发布插入图片(未解决,暂时舍弃)

  • 编辑项目资料详情时可以跳转新页面编辑图片
  • 实现了图片编辑,可以存入数据库,且考虑到了图片序列的更改(增、删)
  • 无法读出存入数据的图片url
  • 修改后读出来url,但无法做到未发帖前保存图片序列以反复更改
  • 编辑了图片功能,文本编辑功能受影响,无法截断显示在待发帖界面中
  • 最后决定,只保留文本输入功能,希望后续可以优化

2.聊天功能升级

  • 开始只构建了好友列表和聊天
  • 为了丰富功能,添加了以下新组件:
    • 消息发送时间formatTime
    • 读取状态变动markMessagesAsRead
    • 消息提示getUnreadCount
    • 内容安全检测checkMessageContent
  • 消息同步接收问题尚未解决

3.openid的获取

  • 开始我们在各个界面中都设置了获取openid的方法
  • 但容易出现openid获取不稳定的问题,同时增加了代码的冗余度
  • 设置全局变量user_openid,初始值为null,登录后以当前登录用户的openid赋值

八、GitHub PR

描述 描述 描述

九、PSP表格

本次赢合系Milky-Way小程序开发的PSP表格如下:

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 120 120
Estimate 估计这个任务需要多少时间 120 120
Development 开发 4200 6000
Analysis 需求分析 (包括学习新技术) 400 450
Design Spec 生成设计文档 120 115
Design Review 设计复审 180 140
Coding Standard 代码规范 (为目前的开发制定合适的规范) 60 55
Design 具体设计 150 200
Coding 具体编码 2800 4300
Code Review 代码复审 180 300
Test 测试(自我测试,修改代码,提交修改) 310 440
Reporting 报告 10 15
Test Report 测试报告 420 600
Size Measurement 计算工作量 20 20
Postmortem & Process Improvement Plan 事后总结, 并提出过程改进计划 160 380
合计 4920 7020

十、结对互评

心路历程回光返照

(从一个客观视角记录本次作业大家的心态和思路解决心理变化)

写在前面

本次作业心路历程回归,因为其实一开始做原型的时候就是设想尽可能地减少国庆的代码量,但是我们为了尽可能地把我们的赢合系做得功能完善能够满足用户需求,还是不可避免地做出来了很多的页面。

初始状态与挑战

这导致其实一开始我们的状态是比较焦虑,因为本身丝毫没有做小程序或者app的什么经验,但是要在十天之内做出来一个这样的似乎和市面上小程序没什么区别的东西其实令我们感到压力很大。用什么语言去做小程序,在什么平台开发,后续如何测试,以及前后端用什么形式展现。针对吴老师的点评,我们摄取了我们第一次作业缺乏交流性,不能实时聊天的缺陷以及项目更新无法做到细致的一些不足,我们想要在我们接下来的开发里把这个做好,如何去实现,成了我们的最大问题。

时间管理与进度

是真的几乎是作业一发布我们就开始去思考怎么做,我们战线拉得非常长,但是还是不可避免地把报告拖着到最后一天才交最后一天还在赶,因为中途的工作量真的很难预判。

技术选择与困难

我们一开始想着开发小程序会说简单一些,因为微信小程序的开发现在比较完善成熟,也有现成的开发平台和功能,但是我们后续还是遇到了审核问题以及备案问题,但是我们努力解决了这些问题,包括解决隐私问题除了发帖功能要升级企业小程序这个我们实在是做不到之外我们以及扫除了我们小程序的所有困难。

编程语言与框架搭建

一开始是说先用javascript和wxml(微信开发平台自带的编程语言)先把我们大致的框架搭出来,就和原型类似,只不过是用编程语言写出来,但是我们也遇到了前所未有的困难,我们从未写过javascript,也没有写过这样的东西。一开始上手的时候一个container的格式我们都可以调节两个小时,真的异常地绝望那个时候。但是就是我们彼此都没有想过放弃,硬是自己加AI和上网搜,再加上堆时间,对,堆时间对我们这样的入门者(JavaScript从入门到直接开发)是唯一能够解决问题的途径,基本上国庆假期内也一直在熟悉框架如何搭建。

用户识别与数据关联

但是我们后面发现了一个问题,因为其实在我们的小程序里每个人有自己的信息,还有她发布的贴子以及她对别人的评价,别人对她的评价。如何识别用户,又如何记录用户和它的相关数据的关联,就像你用户发一个帖子,要显示在你的个人主页,广场。用户发私聊信息的话要对方可以收到,你这边也可以看到,如何显示如何记录。如何又是以什么样的样式出现组织。真的这个问题很大,基本上是一动牵引全局动的方式。

框架重写与数据库设计

于是我们后面有个阶段基本上是整个框架重写,整个用户数据库和资料(评价,组队信息,个人简历)全部的更新和布局。就是把各个模块互相关联,用户的个人头像怎么上传云端以及怎么在云端修改用户资料都是我们最基础要解决的问题,我们逐渐把问题都解决,如何遇到了更大的问题。

登录与身份识别问题

就是因为开发者只有一个识别自己身份的openid,匹配着所有资料,包括头像专业技能研究方向,如果我们拿这个来登入的话,相当于我们不能在开发的时候模拟交互,不同用户之间如何交互,这个openid是开发平台自己记录的会和我们的开发账号匹配,比如说我们有黄昕怡和张铭心两个账号,但是在黄心怡的开发者平台只能登入她自己的,这并不是我们想要的效果。于是我们引入了新的变量user_openid去匹配账号密码,使得我们可以用同一个openid(开发账号)去登入不同openid的账号,使得我们后续的评价私信功能进一步完善。

页面跳转与缓存管理

包括各个页面的跳转也是,退出登入要清空全部缓存,如何重新登入,修改资料不清空缓存回到原来的myself界面,这些都是我们开发中思考的问题。

界面设计与美观

必须要提到的一点的如何把界面设置得美观,因为我们真的为这个东西的原型花了很多时间,所以我们只想把它做好,怎么样设置背景,怎么样设置圆角,怎么样设置悬浮阴影,使得我们的界面既能够整洁功能明确,又能够有很好的美观效果,使得大家愿意看我们的小程序,愿意使用,我们不想打造一个敷衍了事排版很乱的劣质品,所以我们在调节布局也花了很多时间,包括可选头像的选取以及很多事情。

用户项目状态更新

用户项目状态更新也是,如何设置只有可以编辑自己的项目,也是涉及到用户信息的识别,如何修改,修改页面如何设计,还有用户评价,如何显示到别人界面,这些我们全部一一解决。

最终成果与反思

最后做出来的效果大体上是满意,虽然还有很多不足,在这次作业中我们也认识到了自己能力的局限性,但是确实是一次强大的挑战和锻炼。

102202123张铭心

同楼下,但还是说点什么吧!主体还是项目内容。

值得学习的地方
那可太多,比如她的高执行力和高效率,再比如有时间观念、有主见、有志向
在做小程序的过程我也感受到了自己的悲观和被动,学习能力低,熟悉战线长等等不足,有这样的好搭档我真的真的很幸运!

需要改进的地方
别太努力了!over!

102202141黄昕怡

做完只想说三个字:好累啊

值得学习的地方
如果这个班有人没有和铭心组队过是所有读大数据专业的损失。
特别特别安心,稳稳的幸福,因为没有太大开发经验。中间肯定有不顺利的地方,但是两个人吊着两口气就过来了………你真的辛苦了……总之我们是非常互补型的朋友。她是一个精益求精的人,而我比较喜欢速战速决忽略细节,真的能够学习到很多东西,我即使以后不从事计算机工作了我也会记得这个作业………

需要改进的地方
太独立了,不愿意别人多干活

写在后面

贴几张工作记录图
1️⃣是谁们国庆喝着同款三得利在工作

描述

2️⃣做出聊天的那天很激动的二人,出镜人员马了x

描述

3️⃣做完的心情是

描述

最后鸣谢赢合系小程序开发助力合作商
1️⃣曹氏鸭脖提供的美味午餐

描述

2️⃣泽野弘之端上的功能饮料

描述

燃尽了朋友,看看代码数吧🚬

posted @ 2024-10-08 19:02  okiqiiii  阅读(26)  评论(0编辑  收藏  举报