U-Linker 第五篇冲刺博客

832301320陶炯 2025-12-21 23:25:19

U-Linker 第五篇冲刺博客

目录

  • U-Linker 第五篇冲刺博客
  • I. 项目燃尽图
  • II. 功能模块展示
  • III.前后端代码展示
  • 后端
  • 前端
  • IV. 团队评估
  • GITHUB 提交记录

I. 项目燃尽图

img

II. 功能模块展示

1.用户私聊

img

2.市场与发布

img

3.核心交易

img

III.前后端代码展示

后端

第一部分:核心交易闭环设计
相比于之前的四篇博客,我们后端团队发现了之前的交易系统不足以满足用户(雇主没有需求帮助了撤单,卖家没有货或者有事情无法接单)发起的退款需求以及没有积分托管机制造成的雇主可以不用付钱,卖家拿了钱跑路的行为。而在交易系统中,最大的挑战是如何保证积分的安全性。因此我们设计了一套“积分托管”机制以实现积分流转闭环以及支持用户取消订单的需求,积分托管机制如下:
发布悬赏时:立即冻结雇主积分(扣除),防止雇主可以不用付钱,卖家拿了钱跑路的行为。
选定帮手时:生成订单,状态由招募中/空闲状态转变为进行中。
确认完成时:平台将冻结的积分真正打入帮手账户。
取消订单时:根据规则退还买家积分。

关键代码展示 (1):发布即冻结 (Market 模块)

# market.py - 发布帖子逻辑摘要 
@market_bp.route('/add', methods=['POST'])
def add_post(): 
# ... 安全校验代码 ... 
# 核心逻辑:悬赏任务发布即扣费(积分托管) 
    if post_type == 'bounty': 
        if user.points < price: 
            return error(message="积分不足") 
        # 1. 扣除余额 (进入平台托管池)
        user.points -= price 
        # 2. 记录流水
         history = PointsHistory(user_id=user.id, points_change=-price, action='发布悬赏'...)
         db.session.add(history)           

关键代码展示 (2):完结即转账 (Transaction 模块)(防止雇主收到帮助了不给钱或者卖家收到钱跑路)

# transaction.py - 确认完成逻辑摘要 
@transaction_bp.route('/confirm_complete', methods=['POST']) 
def confirm_complete(): 
# ... 权限校验代码 ... 
    is_service = (order.post.post_type == 'service')
    can_confirm = False
    if is_service and order.seller_id == user_id: can_confirm = True #卖家确认发货
    if not is_service and order.buyer_id == user_id: can_confirm = True #雇主确认收货
    if not can_confirm:
        return error(message="您无权确认此订单")
    try: 
         # 核心逻辑:资金解冻,转入卖家/帮手账户 
         seller = db.session.get(User, order.seller_id) 
         seller.points += order.post.price # 卖家收到积分 
         # 记录收入流水 
         history = PointsHistory(user_id=seller.id, points_change=order.post.price, action='任务完成'...)
         # 更新订单和商品状态 
         order.status = 'completed' 
         order.post.status = 'sold' 
         db.session.commit() 
         return success(message="交易完成,积分已结算") 
     except Exception as e: 
          db.session.rollback() # ...

代码解析:
这段 confirm_complete 函数是整个交易闭环中最关键的“结算时刻”。它不仅是简单的数据库更新,更是平台担保模式的最终执行环节,有效解决了校园交易中“先钱后货还是先货后钱”的信任死结。

  1. 解决“雇主赖账”问题(资金预存与解冻):
    在此代码执行之前(即发布悬赏或购买服务时),买家/雇主的积分已经被系统扣除并冻结了。
    seller.points += order.post.price:这一行代码的本质是资金解冻。系统将暂时托管的积分,正式划拨给劳动者(帮手/卖家)。
    安全逻辑:由于雇主的积分早已被扣除,即便 他在任务完成后想赖账,积分也在平台手里,无法撤回,只能选择“确认完成”。
  2. 解决“卖家跑路”问题(货到付款机制):
    代码的权限校验部分的作用是只有当任务真实完成(雇主验收或买家收货)后,才会触发此接口(try后面的代码才能执行,帮手或者卖家才能拿到钱)。
    在此之前,卖家/帮手虽然付出了劳动,但拿不到一分钱。这迫使卖家必须按质按量完成任务,否则永远无法触发上述的加分代码。
  3. 保证交易的原子性(Atomicity):
    order.status = 'completed' 和 seller.points += ... 被包裹在同一个 try...commit 事务中。
    防刷单/防双重支付:状态更新和转账必须同时成功。如果转账成功但状态没变,或者状态变了钱没转,db.session.rollback() 会回滚所有操作,确保账目流水清楚。

第二部分:即时通讯与隐私保护
文案描述:在即使通讯方面,我们发现了之前系统里存在不在此次聊天的用户却可以看到别的用户的聊天记录(隐私偷窥)以及用户无法查看到自己的未读消息和已读消息。我们团队优化了即时通讯模块,现在的即时通讯模块不仅实现了基础的消息收发,还重点加强了隐私权限控制。我们设计了会话(ChatSession)与消息(Message)的双层模型,并实现了“自动已读”和“全局未读计数”功能。

# chat.py - 创建会话与权限控制 
@chat_bp.route('/create_session', methods=['POST']) 
def create_session():  
    # 技巧:通过排序 ID 确保 (A, B) 和 (B, A) 指向同一个会话 
    u1, u2 = sorted([my_id, target_id]) 
    session = ChatSession.query.filter_by(user1_id=u1, user2_id=u2).first() 
    # ...
@chat_bp.route('/history', methods=['GET']) 
def get_history(): 
    # ... # 隐私保护:只有会话当事人才能查看记录
    if current_user_id != chat_session.user1_id and current_user_id != chat_session.user2_id: 
         return error(message="无权查看此聊天记录") 
    # 亮点功能:获取历史记录时,自动将对方发来的消息标记为已读 
    unread_msgs = Message.query.filter(..., Message.is_read == False).all()
    for m in unread_msgs: m.is_read = True 
     db.session.commit() 
     # ...

代码解析:

  1. 会话存储的“归一化”设计(ID Normalization)
    代码对应:u1, u2 = sorted([my_id, target_id])
    解析:在私聊场景中,“A 和 B 聊天”与“B 和 A 聊天”在逻辑上是同一个会话。如果不做处理,数据库可能同时存在 (A, B) 和 (B, A) 两条重复记录,查询时也需要使用复杂的 OR 语句。也浪费了有限的内存空间。我们通过 sorted 函数对用户 ID 进行排序,强制规定用户的小ID 在前,大ID在后。这种数据归一化处理,确保了无论谁先发起聊天,数据库中永远只有唯一的 (u1, u2) 记录,极大地简化了查询逻辑并节省了存储空间。
  2. 零信任的隐私防线(Anti-IDOR)
    代码对应:if current_user_id != chat_session.user1_id and current_user_id != chat_session.user2_id:
      return error(message="无权查看此聊天记录") 
    
    解析:这是后端安全最重要的一环。我们拒绝盲目信任前端传来的 session_id。即使局外人猜到了别人的ID 并尝试访问别人的聊天记录,后端也会在逻辑层进行二次校验:判断当前登录用户是否属于该会话的成员。这种基于所有权的访问控制,彻底杜绝了通过别人遍历 他人ID 偷看他人隐私的可能性。
  3. 自动清除已读信息来实现自动状态同步
    代码对应:for m in unread_msgs: m.is_read = True
    解析:传统的做法是前端先获取消息,再发一个请求告诉后端“我已读”。这会增加一次网络请求,导致延迟。
    我们团队采用了获取即已读的策略:当用户调用 get_history 接口加载聊天记录时,U-Linker系统判定用户正在查看屏幕,因此自动将对方之前发来的,用户已经看过的消息批量标记为已读以次来降低延迟 增加前后端交互的顺畅性。

第三部分:安全性与鉴权设计
在之前的系统里,我们发现了别的用户可以冒充某些用户来登陆我们平台,前端会自动传别人的ID然后当时后端过度信任前端就没有对前端的请求进行二次验证。为了保障系统安全,我们摒弃了传统的从 Request Body 获取用户 ID 的做法,全面采用 Flask Session 进行服务端鉴权,彻底杜绝了“身份冒充”漏洞。同时,配合 CORS 配置解决了前后端分离架构下的跨域 Cookie 问题。
关键代码展示 (4):身份鉴权范式

# 安全接口写法 
@transaction_bp.route('/purchase', methods=['POST']) 
def purchase_service(): 
# 安全基石:只从 Session 获取当前登录用户 
    buyer_id = session.get('user_id') 
    if not buyer_id:
         return error(message="请先登录") 
    # 杜绝了 buyer_id = request.json.get('buyer_id') 的这种危险写法 
    # ...

代码解析:

  1. 安全基石:从 Session 获取当前登录用户
    对应代码: buyer_id = session.get('user_id')
    亮点: 代码使用了从 session 获取当前用户的登录状态,而不是从 request.json 直接获取 buyer_id。
    原因: 直接从请求体(request.json.get('buyer_id'))获取用户信息是不安全的,因为前端用户可以轻松伪造请求内容,传递假的 buyer_id。而从 session 获取用户信息是服务器端保存的用户会话状态,只有在用户登录后,服务器会存储该信息。这样可以确保当前请求的 buyer_id 是有效且安全的。
  2. 防止非法请求
    对应代码: if not buyer_id:
                  return error(message="请先登录")
    
    亮点: 判断 buyer_id 是否为空,如果为空,说明用户未登录,接口直接返回错误信息 请先登录。
    原因: 这种检查可以防止未登录的用户调用接口,避免因为缺乏身份验证而产生的安全漏洞。它确保只有登录用户才能访问该接口。

第四部分:数据库模型设计
数据库设计采用 ORM 技术,利用 SQLAlchemy 的 relationship 和 backref 特性,简化了复杂的联表查询。例如,在获取帖子详情时,可以直接通过对象属性访问其下的所有申请记录。

class Application(db.Model): # ... # 联合唯一索引,防止重复申请
     __table_args__ = (db.UniqueConstraint('post_id', 'applicant_id'),) 
     # 建立反向引用,让 Post 对象可以直接访问 
    applications post = db.relationship('Post', backref=db.backref('applications', lazy=True))
# 实际使用效果 (Market 模块) 
# post.applications 直接获取所有申请对象,无需手写 SQL Join 
for app in post.applications:
   print(app.applicant.name)

代码解析:

  1. 联合唯一索引 (防止重复申请)
    对应代码: table_args = (db.UniqueConstraint('post_id', 'applicant_id'),)
    亮点: 通过 UniqueConstraint 为 post_id 和 applicant_id 创建联合唯一索引,确保同一个 post_id 与 applicant_id 的组合是唯一的。
    好处: 这个唯一索引可以防止同一申请人对同一个帖子重复申请,确保数据库中的数据的一致性和完整性。这样,可以避免用户重复提交申请,减少了潜在的数据问题。
  2. 反向引用:简化联表查询
    代码: applications = db.relationship('Post', backref=db.backref('applications', lazy=True))
    亮点: db.relationship() 与 backref 配合使用,建立了 Post 和 Application 之间的关联。具体来说,Post 对象通过 post.applications 访问与其相关的所有申请记录,而无需显式编写 JOIN 查询。
    好处:
    简化了查询: 通过 post.applications,可以非常方便地访问 Post 对象关联的所有 Application 对象,避免了手写复杂的查询语句。
    反向访问: backref('applications') 提供了 Post 对象与 Application 对象之间的双向关系。通过 post.applications,我们可以方便地获取所有相关的申请记录;反过来,在 Application 对象中,我们也可以直接通过 app.post 访问对应的帖子。

前端

第一部分:核心交易流程模块
1.全局状态管理 (App.vue)

vue
<script setup>
import { ref, provide } from 'vue'

// 当前用户
const currentUser = ref({
  id: 'user_001',
  name: '我',
  college: '计算机学院',
  balance: 200,
  frozenBalance: 50
})

// 任务数据
const tasks = ref([...])

// 接受的订单
const acceptedOrders = ref([...])

// 通过 provide 向子组件注入数据
provide('currentUser', currentUser)
provide('tasks', tasks)
provide('acceptedOrders', acceptedOrders)
</script>

要点: 使用 provide/inject 实现简单的全局状态管理,所有子组件都能访问和修改数据。
2. Tab切换与下划线动画 (OrderPage.vue)

vue
<template>
  <!-- Tab选项卡 -->
  <div class="flex relative">
    <div @click="activeTab = 'published'" class="flex-1 py-3 flex items-center justify-center cursor-pointer">
      <div :class="['text-[15px]', activeTab === 'published' ? 'font-bold text-blue-600' : 'font-medium text-gray-500']">
        我发布的
      </div>
      <!-- 红点计数 -->
      <span :class="['ml-1.5 min-w-[16px] h-[16px] text-[10px] font-bold flex items-center justify-center rounded-full px-1', 
        activeTab === 'published' ? 'bg-[#FF4D4F] text-white' : 'bg-gray-200 text-gray-500']">
        {{ publishedCount }}
      </span>
    </div>
    
    <!-- 下划线指示器 - 关键动画 -->
    <div class="absolute bottom-0 h-[3px] bg-blue-600 transition-all duration-300 ease-in-out"
      :style="{ left: activeTab === 'published' ? '0' : '50%', width: '50%' }">
    </div>
  </div>
</template>

<script setup>
const activeTab = ref('published')

// 计算待处理数量
const publishedCount = computed(() => {
  return myPublishedTasks.value.filter(t => t.status === 'recruiting' || t.status === 'ongoing').length
})
</script>


要点: 通过 :style 动态绑定 left 属性实现
3. 任务状态流转逻辑 (OrderPage.vue)

vue
<script setup>
// 注入全局数据
const tasks = inject('tasks')

// 选择申请人 → 状态变为"进行中"
const handleSelectApplicant = (applicant) => {
  if (selectedTask.value) {
    const index = tasks.value.findIndex(t => t.id === selectedTask.value.id)
    if (index > -1) {
      tasks.value[index].status = 'ongoing'                    // 状态更新
      tasks.value[index].acceptedUserId = applicant.userId     // 记录帮助者
      tasks.value[index].acceptedUserName = applicant.name
    }
    showApplicantModal.value = false
    selectedTask.value = null
  }
}

// 确认验收 → 状态变为"已完成"
const handleAcceptConfirm = () => {
  if (selectedTask.value) {
    const index = tasks.value.findIndex(t => t.id === selectedTask.value.id)
    if (index > -1) {
      tasks.value[index].status = 'completed'  // 积分解冻转移
    }
    showConfirmModal.value = false
    selectedTask.value = null
  }
}
</script>

要点: 通过修改 tasks 数组中对象的 status 字段实现状态流转。

4.条件渲染按钮 (OrderPage.vue)

vue
<template>
  <div class="flex gap-2">
    <!-- 招募中状态 -->
    <template v-if="task.status === 'recruiting' && task.type === 'bounty'">
      <button class="...">编辑</button>
      <button @click="handleViewApplicants(task)" class="...bg-blue-600...">
        查看申请({{ task.applicants?.length || 0 }})
      </button>
    </template>
    
    <!-- 进行中状态 -->
    <template v-if="task.status === 'ongoing'">
      <button class="...text-blue-600 border-blue-200...">私聊</button>
      <button @click="handleConfirmComplete(task)" class="...bg-green-500...">
        确认完成
      </button>
    </template>
    
    <!-- 服务上架中 -->
    <template v-if="task.status === 'active' && task.type === 'service'">
      <button class="...">编辑</button>
      <button class="...text-red-500 border-red-200...">下架</button>
    </template>
  </div>
</template>


5.弹窗组件封装 (ConfirmModal.vue)

vue
<template>
  <!-- 使用 Teleport 将弹窗渲染到 body -->
  <Teleport to="body">
    <div v-if="show" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center px-8">
      <div class="w-full bg-white rounded-2xl p-6 flex flex-col items-center text-center shadow-2xl animate-zoom-in">
        <h2 class="text-xl font-bold text-gray-900 mb-3">确认验收?</h2>
        <p class="text-[15px] text-gray-600 leading-relaxed mb-8 px-2">
          确认后将解冻 <span class="font-bold text-gray-800">{{ points }}积分</span> 并转入对方账户,此操作不可撤销。
        </p>
        <div class="w-full flex gap-4">
          <button @click="$emit('close')" class="flex-1 bg-[#C7C7CC]...">取消</button>
          <button @click="$emit('confirm')" class="flex-1 bg-[#007AFF]...">确认</button>
        </div>
      </div>
    </div>
  </Teleport>
</template>

<script setup>
defineProps({
  show: Boolean,
  points: Number
})

defineEmits(['close', 'confirm'])
</script>

<style scoped>
.animate-zoom-in {
  animation: zoomIn 0.2s ease-out;
}
@keyframes zoomIn {
  from { opacity: 0; transform: scale(0.95); }
  to { opacity: 1; transform: scale(1); }
}
</style>


要点:
使用 Teleport 将弹窗传送到 body,避免层级问题
通过 defineProps 接收参数,defineEmits 定义事件
使用 CSS @keyframes 实现弹窗动画
要点: 使用 v-if 根据任务状态和类型显示不同的操作按钮。
6. 任务详情页核心逻辑 (TaskDetail.vue)

vue
<script setup>
const props = defineProps({ taskId: String })
const emit = defineEmits(['back'])

const currentUser = inject('currentUser')
const tasks = inject('tasks')

// 获取当前任务
const task = computed(() => {
  return tasks.value.find(t => t.id === props.taskId) || tasks.value[0]
})

// 判断是否是发布者
const isOwner = computed(() => {
  return task.value.publisherId === currentUser.value.id
})

// 申请帮助
const handleApply = (message) => {
  const newApplicant = {
    id: `app_${Date.now()}`,
    userId: currentUser.value.id,
    name: currentUser.value.name,
    college: currentUser.value.college,
    grade: '2024级',
    message,
    status: 'pending',
    avatar: currentUser.value.name.charAt(0)
  }
  
  const index = tasks.value.findIndex(t => t.id === task.value.id)
  if (index > -1) {
    if (!tasks.value[index].applicants) {
      tasks.value[index].applicants = []
    }
    tasks.value[index].applicants.push(newApplicant)  // 添加申请记录
  }
  showApplyModal.value = false
  applied.value = true
}
</script>

要点:
使用 computed 根据 taskId 获取任务详情
isOwner 判断当前用户身份,控制不同视角的UI展示
7. 底部操作栏条件渲染 (TaskDetail.vue)

vue
<footer class="absolute bottom-0 w-full bg-white border-t...">
  <!-- 收藏按钮 -->
  <div @click="isFavorited = !isFavorited" :class="[isFavorited ? 'text-yellow-500' : 'text-gray-500']">
    <svg v-if="isFavorited">...</svg>  <!-- 实心星 -->
    <svg v-else>...</svg>               <!-- 空心星 -->
  </div>

  <!-- 私聊按钮 -->
  <button class="flex-1 h-11 border border-blue-600 text-blue-600...">私聊</button>
  
  <!-- 发布者视角 -->
  <template v-if="isOwner">
    <button v-if="task.status === 'ongoing'" @click="showConfirmModal = true" class="...bg-green-500...">
      确认完成
    </button>
    <button v-else class="...bg-gray-200...">等待申请...</button>
  </template>
  
  <!-- 帮助者视角 -->
  <template v-else>
    <button v-if="task.status === 'recruiting' && !applied" @click="showApplyModal = true" class="...bg-blue-600...">
      我能帮助
    </button>
    <button v-else-if="applied" class="...bg-gray-200...">已申请</button>
    <button v-else class="...bg-gray-200...">{{ task.status === 'ongoing' ? '进行中' : '已结束' }}</button>
  </template>
</footer>


要点: 根据 isOwner(是否是发布者)和 task.status(任务状态)显示不同的操作按钮。
8. 左侧状态条颜色映射

vue
<script setup>
// 获取左侧状态条颜色
const getStatusBarColor = (task) => {
  if (task.status === 'recruiting') return 'bg-blue-500'   // 招募中 - 蓝色
  if (task.status === 'ongoing') return 'bg-green-500'     // 进行中 - 绿色
  if (task.type === 'service') return 'bg-orange-400'      // 服务 - 橙色
  return 'bg-gray-400'                                      // 其他 - 灰色
}

// 获取状态标签样式
const getStatusStyle = (task) => {
  if (task.status === 'recruiting') return 'text-blue-600 bg-blue-50'
  if (task.status === 'ongoing') return 'text-green-600 bg-green-50'
  if (task.status === 'active') return 'text-green-600 bg-green-50'
  return 'text-gray-500 bg-gray-50'
}
</script>

要点: 封装颜色映射函数,保持代码整洁。
核心业务流程图
┌─────────────┐ 选择申请人 ┌─────────────┐ 确认完成 ┌─────────────┐
│ 招募中 │ ──────────────→ │ 进行中 │ ────────────→ │ 已完成 │
│ recruiting │ │ ongoing │ │ completed │
└─────────────┘ └─────────────┘ └─────────────┘
↑ ↑
│ │
发布任务 积分冻结中

第二部分:市场与发布模块
首页

  1. 完整移动端框架与布局系统 亮点:完整的多层布局架构 - 从状态栏、导航栏、功能区域到底部Tab,实现了标准的移动端应用结构,支持响应式设计,层次清晰且易于扩展。
<div class="mobile-frame">
  <div class="status-bar"></div>
  <header class="header-bar">
    <span class="app-title">U-Linker</span>
    <div class="header-icons">...</div>
  </header>
  <main class="main-content">
    <div class="banner">...</div>
    <div class="function-cards">...</div>
    <div class="points-card">...</div>
    <div class="hot-tasks">
      <div class="section-header">...</div>
      <div class="tasks-list">
        <TaskCard v-for="item in postList" ... />
      </div>
    </div>
  </main>
  <BottomNav active-tab="home" />
</div>

  1. 双模式发布导航与积分展示逻辑 亮点:核心业务逻辑与视觉设计的深度结合 - 通过区分"我需要"(蓝色/悬赏)和"我能提供"(橙色/服务)两种模式,清晰表达平台双核心理念,配合实时积分展示,形成完整的用户激励闭环。
<!-- 双功能卡片 -->
<div class="function-cards">
  <!-- 我需要 -->
  <div class="function-card need-card" @click="handleNeedCardClick">
    <div class="card-icon">...</div>
    <div class="card-text">
      <h3 class="card-title">我需要</h3>
      <p class="card-desc">发布悬赏 / 寻求帮助</p >
    </div>
  </div>
  
  <!-- 我能提供 -->
  <div class="function-card provide-card" @click="handleProvideCardClick">
    <div class="card-icon">...</div>
    <div class="card-text">
      <h3 class="card-title">我能提供</h3>
      <p class="card-desc">出售技能 / 赚取积分</p >
    </div>
  </div>
</div>

<!-- 积分卡片 -->
<div class="points-card">
  <div class="points-info">
    <div class="points-label">当前积分余额</div>
    <div class="points-value">{{ currentPoints }}</div>
  </div>
</div>


  1. 数据流管理 + 组件化架构 亮点:从API获取、状态管理、组件传参到异常处理的全链路设计,配合空状态处理,确保应用在多种场景下都能稳定运行并提供良好用户体验。
<!-- 任务列表渲染 -->
<div class="tasks-list">
  <TaskCard 
    v-for="item in postList" 
    :key="item.id" 
    :data="item"
    @click="goToDetail(item.id)"
  />
  
  <div v-if="postList.length === 0" class="text-center text-gray-400 py-4 text-sm">
    暂无热门任务...
  </div>
</div>

const postList = ref([])
const fetchData = async () => {
  loading.value = true
  try {
    const res = await getPostList({ page: 1, page_size: 10 }) 
    postList.value = res.data.items
  } finally {
    loading.value = false
  }
}

const goToDetail = (id) => {
  if (!id) {
    console.error('❌ 严重错误: ID 是空的!无法跳转')
    alert('数据错误:该帖子没有ID')
    return
  }
  router.push(`/post/${id}`)
}

发布-我需要/我能提供

  1. 双模式切换与动态样式系统 亮点:双模式原子化设计 - 通过统一的切换组件实现了"悬赏/服务"两种发布模式的快速切换,结合图标+文字+动态样式,提供清晰的用户选择路径。
<div class="type-toggle">
  <!-- 按钮 1: 我需要 (悬赏) -->
  <button 
    class="type-button"
    :class="{ 'active': formData.post_type === 'bounty' }"
    @click="formData.post_type = 'bounty'"
  >
    <span class="iconify" data-icon="mdi:hand-extended-outline"></span>
    我需要 (悬赏)
  </button>
  
  <!-- 按钮 2: 我能提供 (服务) -->
  <button 
    class="type-button"
    :class="{ 'active': formData.post_type === 'service' }"
    @click="formData.post_type = 'service'"
  >
    <span class="iconify" data-icon="mdi:briefcase-outline"></span>
    我能提供 (服务)
  </button>
</div>


  1. 实时表单验证与交互式价格控件 亮点:全链路表单验证系统 - 从实时输入验证、条件禁用状态、到最终表单状态计算,构建了前端完整的数据质量保障体系,确保提交数据的完整性。
// 计算属性:表单验证状态
const isFormValid = computed(() => {
  return !titleError.value && 
         !descriptionError.value && 
         !priceError.value &&
         formData.title.trim().length > 0 &&
         formData.description.trim().length > 0 &&
         formData.price > 0
})

// 价格增减控件
<div class="price-controls">
  <button 
    class="price-btn minus"
    @click="decreasePrice"
    :disabled="formData.price <= 1"
  >-</button>
  <button 
    class="price-btn plus"
    @click="increasePrice"
  >+</button>
</div>

// 输入验证逻辑
const validateTitle = () => {
  const title = formData.title.trim()
  if (title.length === 0) { titleError.value = '服务标题不能为空'; return }
  if (title.length < 5) { titleError.value = '服务标题至少需要5个字符'; return }
  if (title.length > 50) { titleError.value = '服务标题不能超过50个字符'; return }
  titleError.value = ''
}

第三部分:用户与聊天模块
1。会话列表智能排序与未读计数系统
亮点:时序化列表设计 - 通过计算属性实现会话按最后消息时间倒序排序,结合红点计数 + 点击清零逻辑,同步全局未读状态,让用户快速聚焦最新消息。

const sortedConversations = computed(() => {
  return [...conversations.value].sort((a, b) => new Date(b.lastMsgTime) - new Date(a.lastMsgTime))
})

2。消息气泡双态区分与状态可视化系统
亮点:身份化气泡设计 - 基于isMine状态动态绑定样式,自己的消息用蓝色气泡居右、对方用白色气泡居左,搭配单钩 / 双钩图标直观展示未读 / 已读状态,清晰区分消息归属。

<div :class="{ 'justify-end': msg.isMine }">
  <div v-if="msg.isMine" class="bg-blue-600 text-white rounded-lg rounded-tr-none">
    <!-- 自己的消息 -->
    <svg v-if="msg.read" class="w-3 h-3" viewBox="0 0 24 24">...</svg>
  </div>
  <div v-else class="bg-white rounded-lg rounded-tl-none">
    <!-- 对方的消息 -->
  </div>
</div>


3。本地消息模拟收发与自动滚动系统
亮点:拟真化交互设计 - 实现输入框回车 / 按钮双触发发送,通过定时器模拟对方回复,搭配消息列表自动滚动到底部逻辑,还原真实聊天节奏,提升沉浸感。

const sendMessage = () => {
  // 新增本地消息
  currentConversation.value.messages.push(newMsg)
  // 模拟回复
  setTimeout(() => {
    currentConversation.value.messages.push(replyMsg)
  }, 1000)
  // 自动滚动
  msgContainer.value.scrollTo({ top: msgContainer.value.scrollHeight })
}

4.头像上传预览交互系统
亮点:即时化预览设计 - 结合 FileReader API 实现图片本地预览,搭配文件类型 / 大小校验、hover 提示交互,让头像更换过程直观可控,提升编辑体验。

const handleAvatarUpload = (e) => {
  const reader = new FileReader()
  reader.onload = (event) => {
    avatarPreview.value = event.target.result // 即时预览
  }
  reader.readAsDataURL(file)
}

第四部分:积分系统与全局组件

// 对应发布帖子逻辑
const handlePublish = async () => {
  // 发送给后端的简洁数据格式
  const postData = {
    title: formData.title.trim(),
    content: formData.description.trim(), // 注意字段映射
    price: Number(formData.price),
    post_type: formData.post_type // 'service''bounty'
  }
  
  // 积分冻结逻辑
  alert('发布成功!' + (formData.post_type === 'bounty' ? '积分已冻结' : ''))
}

发布功能优化:
• 重构了数据格式,移除冗余字段,后端从用户会话自动获取作者信息
• 区分服务发布(无积分冻结)和悬赏发布(自动冻结积分)
• 发布后自动刷新用户积分信息

// 对应积分购买服务
const handlePurchase = async () => {
  await purchaseService({
    post_id: post.value.id  // 只需传递帖子ID
  })
  alert('购买成功!积分已冻结,订单已创建')
}

// 对应申请悬赏任务
const handleApply = async () => {
  await applyForTask({
    post_id: post.value.id  // 只需传递帖子ID
  })
  alert('申请成功!等待雇主选中')
}

购买与申请逻辑:
• 服务购买:积分即时冻结,等待服务完成后转移给卖家
• 悬赏申请:无积分冻结,等待雇主选中后才进行交易
• 用户身份验证统一由后端会话管理

// 对应买家确认服务完成
const handleConfirmComplete = async (order) => {
  await confirmComplete({
    order_id: order.id
  })
  alert('任务完成!积分已转移给对方')
}

// 对应取消不同状态的订单
const handleCancel = async (order) => {
  if (order.status === 'ongoing' || order.status === 'pending') {
    await cancelOrder({
      order_id: order.id
    })
    alert('订单已取消,积分已退回')
  }
}

订单处理系统:
• 完成确认:买家确认服务完成后,冻结的积分将转移给卖家
• 订单取消:根据不同状态处理积分退款
• 实时积分更新:每个关键操作后都会刷新用户积分余额

// 对应刷新用户积分信息
const userRes = await getUserProfile(userStore.userInfo.id)
userStore.login(userRes.data)  // 更新全局用户状态

// 对应获取订单列表
const fetchMyOrders = async () => {
  const res = await getMyInvolved()  // 获取参与的所有订单
}

每个部分都通过统一的用户会话管理,后端自动识别用户身份,前端只需传递必要的数据标识(如 post_id、order_id),实现了简洁安全的数据交互。

IV. 团队评估

姓名百分比
颜一顺18%
杨璐18%
高子言12%
陈舒薇9%
薛易明9%
张健涛6%
陶炯6%
曾渝6%
程一鸣5%
陈乐晗2.5%
黄祉睿2.5%
林语婧6%

GITHUB 提交记录

img

...全文
162 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

164

社区成员

发帖
与我相关
我的任务
社区描述
2501_MU_SE_FZU
软件工程 高校
社区管理员
  • FZU_SE_LQF
  • 助教_林日臻
  • 朱仕君
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

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