大家好,我是Java1234_小锋老师,分享一套锋哥原创的基于Python的微信小程序校园失物招领管理系统(FastAPI+Vue3)。

项目介绍
校园日常学习生活中,学生证、校园卡、钥匙、雨伞、耳机和教材等物品丢失较为常见。传统失物招领主要依赖公告栏张贴、班级群转发或值班室口头登记,信息分散、检索困难、核验不便,既增加了失主找回物品的成本,也给后勤管理部门带来重复劳动。针对上述问题,本文设计并实现了一套微信小程序校园失物招领管理系统,面向学生提供轻量发布与检索入口,面向管理员提供统一审核与统计后台。
系统采用前后端分离架构。学生端基于微信小程序开发,完成用户名密码登录、注册(含确认密码)、招领与寻物信息浏览发布、认领申请、公告查看、留言反馈以及个人资料与密码修改等功能;管理端基于 Vue3、Vite 与 Element Plus 构建,完成用户管理、物品分类、招领寻物维护、认领审核、公告发布、留言回复、个人中心以及首页数据统计;服务端基于 Python 与 FastAPI 实现 REST 接口,使用 SQLAlchemy 访问 MySQL 数据库 db_lost_found,并以 JWT 完成身份认证。数据库共设计管理员、学生用户、物品分类、失物招领、寻物启事、认领申请、系统公告和留言反馈八张业务表。
测试结果表明,系统能够覆盖校园失物招领的主要业务闭环,界面交互清晰,接口响应稳定,能够提升失物信息的公开效率与认领过程的可追溯性,对建设诚信、便捷的数字化校园具有实际应用价值。
源码下载
链接: https://pan.baidu.com/s/1WKpZbyvr5658ZAWogYD86A?pwd=1234 提取码: 1234
系统展示 







核心代码
"""失物招领业务服务。"""
from datetime import datetime
from sqlalchemy.orm import Session
from app.common import model_to_dict, page_result
from app.models import FoundItem, ItemType, User
def _attach_extra(item: FoundItem, type_name, user_name) -> FoundItem:
"""附加分类名称和发布人姓名。"""
item.type_name = type_name
item.user_name = user_name
return item
def page_items(db: Session, page_num: int, page_size: int, title: str | None,
type_id: int | None, status: int | None, user_id: int | None) -> dict:
"""分页查询招领信息(关联分类和发布人)。"""
query = (
db.query(FoundItem, ItemType.type_name, User.real_name)
.outerjoin(ItemType, FoundItem.type_id == ItemType.id)
.outerjoin(User, FoundItem.user_id == User.id)
)
if title:
query = query.filter(FoundItem.title.like(f"%{title}%"))
if type_id is not None:
query = query.filter(FoundItem.type_id == type_id)
if status is not None:
query = query.filter(FoundItem.status == status)
if user_id is not None:
query = query.filter(FoundItem.user_id == user_id)
total = query.count()
rows = (
query.order_by(FoundItem.create_time.desc())
.offset((page_num – 1) * page_size)
.limit(page_size)
.all()
)
records = [model_to_dict(_attach_extra(item, type_name, user_name)) for item, type_name, user_name in rows]
return page_result(total, records)
def get_by_id(db: Session, item_id: int) -> dict | None:
"""根据ID查询招领详情。"""
item = db.query(FoundItem).filter(FoundItem.id == item_id).first()
return model_to_dict(item)
def save(db: Session, data: dict) -> None:
"""发布招领信息。"""
status = data.get("status")
if status is None:
status = 0
item = FoundItem(
type_id=data.get("type_id"),
user_id=data.get("user_id"),
title=data.get("title"),
item_name=data.get("item_name"),
found_time=data.get("found_time"),
found_place=data.get("found_place"),
storage_place=data.get("storage_place"),
description=data.get("description"),
image=data.get("image"),
contact_name=data.get("contact_name"),
contact_phone=data.get("contact_phone"),
status=status,
create_time=datetime.now(),
)
db.add(item)
db.commit()
def update(db: Session, data: dict) -> None:
"""修改招领信息(仅更新非空字段)。"""
item = db.query(FoundItem).filter(FoundItem.id == data.get("id")).first()
if item is None:
return
for key in (
"type_id", "user_id", "title", "item_name", "found_time", "found_place",
"storage_place", "description", "image", "contact_name", "contact_phone", "status",
):
if key in data and data.get(key) is not None:
setattr(item, key, data.get(key))
db.commit()
def delete(db: Session, item_id: int) -> None:
"""删除招领信息。"""
item = db.query(FoundItem).filter(FoundItem.id == item_id).first()
if item is not None:
db.delete(item)
db.commit()
<script setup>
/**
* 后台失物招领管理页面
*/
import { onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { pageFound, saveFound, updateFound, deleteFound, listItemType, uploadFile } from '../../api'
import { fileUrl } from '../../api/request'
const types = ref([])
const query = reactive({ pageNum: 1, pageSize: 10, title: '', typeId: null, status: null })
const table = reactive({ records: [], total: 0 })
const dialog = reactive({ visible: false, title: '新增招领', form: {} })
/**
* 加载招领列表
*/
async function loadList() {
const res = await pageFound(query)
table.records = res.data.records
table.total = res.data.total
}
/**
* 打开新增弹窗
*/
function openAdd() {
dialog.title = '新增招领'
dialog.form = { title: '', itemName: '', typeId: null, foundTime: '', foundPlace: '', storagePlace: '', description: '', image: '', contactName: '', contactPhone: '', status: 0 }
dialog.visible = true
}
/**
* 打开编辑弹窗
*/
function openEdit(row) {
dialog.title = '编辑招领'
dialog.form = { …row }
dialog.visible = true
}
/**
* 上传物品图片
*/
async function onImageChange(file) {
const res = await uploadFile(file.raw)
dialog.form.image = res.data.url
}
/**
* 保存招领
*/
async function save() {
if (dialog.form.id) {
await updateFound(dialog.form)
} else {
await saveFound(dialog.form)
}
ElMessage.success('保存成功')
dialog.visible = false
loadList()
}
/**
* 删除招领
*/
async function remove(id) {
await ElMessageBox.confirm('确认删除该招领信息吗?', '提示')
await deleteFound(id)
ElMessage.success('删除成功')
loadList()
}
onMounted(async () => {
const res = await listItemType()
types.value = res.data
loadList()
})
</script>
<template>
<div class="page-card">
<div class="search-bar">
<el-input v-model="query.title" placeholder="标题" style="width: 200px;" clearable />
<el-select v-model="query.typeId" placeholder="分类" clearable style="width: 160px;">
<el-option v-for="t in types" :key="t.id" :label="t.typeName" :value="t.id" />
</el-select>
<el-select v-model="query.status" placeholder="状态" clearable style="width: 140px;">
<el-option label="待认领" :value="0" />
<el-option label="已认领" :value="1" />
</el-select>
<el-button type="primary" @click="query.pageNum = 1; loadList()">查询</el-button>
<el-button type="success" @click="openAdd">新增</el-button>
</div>
<el-table :data="table.records">
<el-table-column prop="id" label="ID" min-width="70" />
<el-table-column label="图片" min-width="90">
<template #default="{ row }">
<img v-if="row.image" class="img-cell" :src="fileUrl(row.image)" />
<span v-else>暂无</span>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" min-width="180" />
<el-table-column prop="itemName" label="物品" min-width="110" />
<el-table-column prop="typeName" label="分类" min-width="110" />
<el-table-column prop="foundPlace" label="拾取地点" min-width="150" />
<el-table-column prop="foundTime" label="拾取时间" min-width="170" />
<el-table-column prop="status" label="状态" min-width="90">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'warning'">{{ row.status === 1 ? '已认领' : '待认领' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="发布时间" min-width="170" />
<el-table-column label="操作" min-width="150">
<template #default="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" @click="remove(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination background layout="total, prev, pager, next" :total="table.total" v-model:current-page="query.pageNum" v-model:page-size="query.pageSize" @current-change="loadList" />
</div>
<el-dialog v-model="dialog.visible" :title="dialog.title" width="640px">
<el-form :model="dialog.form" label-width="90px">
<el-form-item label="标题"><el-input v-model="dialog.form.title" /></el-form-item>
<el-form-item label="物品名称"><el-input v-model="dialog.form.itemName" /></el-form-item>
<el-form-item label="分类">
<el-select v-model="dialog.form.typeId" style="width: 100%;">
<el-option v-for="t in types" :key="t.id" :label="t.typeName" :value="t.id" />
</el-select>
</el-form-item>
<el-form-item label="拾取时间"><el-date-picker v-model="dialog.form.foundTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width: 100%;" /></el-form-item>
<el-form-item label="拾取地点"><el-input v-model="dialog.form.foundPlace" /></el-form-item>
<el-form-item label="暂存地点"><el-input v-model="dialog.form.storagePlace" /></el-form-item>
<el-form-item label="联系人"><el-input v-model="dialog.form.contactName" /></el-form-item>
<el-form-item label="联系电话"><el-input v-model="dialog.form.contactPhone" /></el-form-item>
<el-form-item label="图片">
<el-upload :show-file-list="false" :auto-upload="false" accept="image/*" :on-change="onImageChange">
<el-button>上传图片</el-button>
</el-upload>
<img v-if="dialog.form.image" class="img-cell" style="margin-top:8px;" :src="fileUrl(dialog.form.image)" />
</el-form-item>
<el-form-item label="描述"><el-input v-model="dialog.form.description" type="textarea" /></el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="dialog.form.status">
<el-radio :value="0">待认领</el-radio>
<el-radio :value="1">已认领</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialog.visible = false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
</template>
</el-dialog>
</div>
</template>




