164
社区成员
发帖
与我相关
我的任务
分享

1.用户私聊

2.市场与发布

3.核心交易

第一部分:核心交易闭环设计
相比于之前的四篇博客,我们后端团队发现了之前的交易系统不足以满足用户(雇主没有需求帮助了撤单,卖家没有货或者有事情无法接单)发起的退款需求以及没有积分托管机制造成的雇主可以不用付钱,卖家拿了钱跑路的行为。而在交易系统中,最大的挑战是如何保证积分的安全性。因此我们设计了一套“积分托管”机制以实现积分流转闭环以及支持用户取消订单的需求,积分托管机制如下:
发布悬赏时:立即冻结雇主积分(扣除),防止雇主可以不用付钱,卖家拿了钱跑路的行为。
选定帮手时:生成订单,状态由招募中/空闲状态转变为进行中。
确认完成时:平台将冻结的积分真正打入帮手账户。
取消订单时:根据规则退还买家积分。
关键代码展示 (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 函数是整个交易闭环中最关键的“结算时刻”。它不仅是简单的数据库更新,更是平台担保模式的最终执行环节,有效解决了校园交易中“先钱后货还是先货后钱”的信任死结。
第二部分:即时通讯与隐私保护
文案描述:在即使通讯方面,我们发现了之前系统里存在不在此次聊天的用户却可以看到别的用户的聊天记录(隐私偷窥)以及用户无法查看到自己的未读消息和已读消息。我们团队优化了即时通讯模块,现在的即时通讯模块不仅实现了基础的消息收发,还重点加强了隐私权限控制。我们设计了会话(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()
# ...
代码解析:
return error(message="无权查看此聊天记录")
解析:这是后端安全最重要的一环。我们拒绝盲目信任前端传来的 session_id。即使局外人猜到了别人的ID 并尝试访问别人的聊天记录,后端也会在逻辑层进行二次校验:判断当前登录用户是否属于该会话的成员。这种基于所有权的访问控制,彻底杜绝了通过别人遍历 他人ID 偷看他人隐私的可能性。第三部分:安全性与鉴权设计
在之前的系统里,我们发现了别的用户可以冒充某些用户来登陆我们平台,前端会自动传别人的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') 的这种危险写法
# ...
代码解析:
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.全局状态管理 (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 │
└─────────────┘ └─────────────┘ └─────────────┘
↑ ↑
│ │
发布任务 积分冻结中
第二部分:市场与发布模块
首页
<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>
<!-- 双功能卡片 -->
<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>
<!-- 任务列表渲染 -->
<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}`)
}
发布-我需要/我能提供
<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>
// 计算属性:表单验证状态
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),实现了简洁安全的数据交互。
| 姓名 | 百分比 |
|---|---|
| 颜一顺 | 18% |
| 杨璐 | 18% |
| 高子言 | 12% |
| 陈舒薇 | 9% |
| 薛易明 | 9% |
| 张健涛 | 6% |
| 陶炯 | 6% |
| 曾渝 | 6% |
| 程一鸣 | 5% |
| 陈乐晗 | 2.5% |
| 黄祉睿 | 2.5% |
| 林语婧 | 6% |
