分享一套锋哥原创的基于Python的会员管理系统(带微信小程序会员端)(FastAPI+Vue3)优质版

分享一套锋哥原创的基于Python的会员管理系统(带微信小程序会员端)(FastAPI+Vue3)优质版

大家好,我是Java1234_小锋老师,分享一套锋哥原创的基于Python的会员管理系统(带微信小程序会员端)(FastAPI+Vue3)优质版

项目介绍

随着移动互联网与数字化运营的快速发展,会员制已成为零售、餐饮、生活服务等行业提升客户黏性与复购率的重要手段。传统依赖纸质会员卡或分散表格登记的管理模式,存在信息滞后、统计困难、等级折扣难以自动执行、会员端触达不便等问题。因此,设计并实现一套前后端分离、移动端可访问的会员管理系统,对中小型商户实现精细化运营具有现实意义。

本文设计并实现了“基于Python的会员管理系统(带微信小程序端)”。系统采用B/S架构与前后端分离思想:后端基于Python语言与FastAPI框架构建RESTful接口,使用SQLAlchemy操作MySQL数据库(库名db_member),并结合JWT完成管理员与会员双端身份认证;管理后台基于Vue3、Vue Router、Pinia与Element Plus实现,首页集成ECharts数据统计图表;微信小程序端面向会员用户,提供用户名密码登录、注册、公告浏览、资产查看、头像与个人资料修改、密码修改等功能。业务上覆盖会员档案、会员等级与折扣、商品管理、充值、消费、积分获取与兑换、公告发布以及管理员与角色管理等核心场景,并实现消费折扣计算、余额扣减、积分累计与自动升级等关键业务闭环。

系统经过功能测试验证,各模块运行稳定,接口响应正常,能够满足本科毕业设计对完整性、规范性与实用性的要求。实践表明,Python与FastAPI适合快速构建高可读性的后端服务,Vue3适合构建交互友好的管理端,微信小程序则能有效降低会员使用门槛。论文同时给出了E-R图、功能结构图、时序图与数据表字段设计,并对关键功能附核心代码说明,便于复现与二次开发。

源码下载

链接: https://pan.baidu.com/s/1hbcKeOPblLjWPn4royqf3A?pwd=1234
提取码: 1234

系统展示

核心代码

""" 消费业务服务 对齐 Java ConsumeServiceImpl.doConsume """ from decimal import Decimal, ROUND_DOWN, ROUND_HALF_UP from sqlalchemy.orm import Session from app.common.exception import BusinessException from app.models.consume import Consume from app.models.member import Member from app.models.member_level import MemberLevel from app.models.points_record import PointsRecord from app.models.product import Product class ConsumeService: """消费服务类""" @staticmethod def do_consume(db: Session, consume: Consume) -> None: """ 执行会员消费:按等级折扣扣费、扣库存、增加积分并记录 :param db: 数据库会话 :param consume: 消费信息 """ member = ( db.query(Member) .filter(Member.id == consume.member_id, Member.deleted == 0) .first() ) if member is None: raise BusinessException("会员不存在") if member.status is not None and member.status == 0: raise BusinessException("会员已冻结,无法消费") amount = consume.amount # 若选择了商品,以商品价格为准并扣库存 if consume.product_id is not None: product = ( db.query(Product) .filter(Product.id == consume.product_id, Product.deleted == 0) .first() ) if product is None: raise BusinessException("商品不存在") if product.status is not None and product.status == 0: raise BusinessException("商品已下架") if product.stock is not None and product.stock <= 0: raise BusinessException("商品库存不足") amount = product.price product.stock = product.stock - 1 db.add(product) if amount is None or amount <= 0: raise BusinessException("消费金额必须大于0") # 按会员等级计算折扣 discount = Decimal("1") if member.level_id is not None: level = ( db.query(MemberLevel) .filter(MemberLevel.id == member.level_id, MemberLevel.deleted == 0) .first() ) if level is not None and level.discount is not None: discount = Decimal(str(level.discount)) real_amount = (amount * discount).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) balance = member.balance if member.balance is not None else Decimal("0") if balance < real_amount: raise BusinessException("会员余额不足") new_balance = balance - real_amount points_gained = int(real_amount.to_integral_value(rounding=ROUND_DOWN)) current_points = member.points if member.points is not None else 0 new_points = current_points + points_gained member.balance = new_balance member.points = new_points ConsumeService._upgrade_level(db, member, new_points) db.add(member) consume.amount = amount consume.real_amount = real_amount consume.points_gained = points_gained consume.balance_after = new_balance consume.deleted = 0 db.add(consume) # 记录积分获取 if points_gained > 0: record = PointsRecord( member_id=member.id, change_points=points_gained, type="获取", remark="消费获得积分", deleted=0, ) db.add(record) db.commit() @staticmethod def _upgrade_level(db: Session, member: Member, new_points: int) -> None: """ 根据积分自动升级会员等级 :param db: 数据库会话 :param member: 会员 :param new_points: 最新积分 """ levels = ( db.query(MemberLevel) .filter(MemberLevel.deleted == 0) .order_by(MemberLevel.upgrade_points.desc()) .all() ) for level in levels: if level.upgrade_points is not None and new_points >= level.upgrade_points: member.level_id = level.id break
<template> <div class="page-card"> <div class="search-bar"> <el-select v-model="query.memberId" placeholder="选择会员" clearable filterable style="width: 200px"> <el-option v-for="m in members" :key="m.id" :label="`${m.name}(${m.cardNo})`" :value="m.id" /> </el-select> <el-button type="primary" @click="loadData"><el-icon><Search /></el-icon>查询</el-button> <el-button @click="query.memberId=null; query.pageNum=1; loadData()"><el-icon><Refresh /></el-icon>重置</el-button> </div> <div class="toolbar"> <el-button type="primary" @click="openDialog()"><el-icon><Plus /></el-icon>会员消费</el-button> </div> <el-table :data="tableData" stripe border style="width: 100%"> <el-table-column prop="cardNo" label="会员卡号" min-width="120" /> <el-table-column prop="memberName" label="会员姓名" min-width="100" /> <el-table-column prop="productName" label="商品" min-width="120" /> <el-table-column prop="amount" label="原价" min-width="90" /> <el-table-column prop="realAmount" label="实付" min-width="90" /> <el-table-column prop="pointsGained" label="获得积分" min-width="90" /> <el-table-column prop="balanceAfter" label="消费后余额" min-width="110" /> <el-table-column prop="operator" label="操作人" min-width="90" /> <el-table-column prop="createTime" label="消费时间" min-width="160" /> <el-table-column label="操作" min-width="90" fixed="right"> <template #default="{ row }"> <el-button type="danger" link @click="handleDelete(row)">删除</el-button> </template> </el-table-column> </el-table> <div class="pagination-wrap"> <el-pagination v-model:current-page="query.pageNum" v-model:page-size="query.pageSize" :total="total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next" @size-change="loadData" @current-change="loadData" /> </div> <el-dialog v-model="dialogVisible" title="会员消费" width="480px" destroy-on-close> <el-form ref="formRef" :model="form" :rules="rules" label-width="90px"> <el-form-item label="会员" prop="memberId"> <el-select v-model="form.memberId" filterable placeholder="请选择会员" style="width: 100%"> <el-option v-for="m in members" :key="m.id" :label="`${m.name}(${m.cardNo}) 余额:${m.balance}`" :value="m.id" /> </el-select> </el-form-item> <el-form-item label="商品" prop="productId"> <el-select v-model="form.productId" filterable placeholder="请选择商品" style="width: 100%"> <el-option v-for="p in products" :key="p.id" :label="`${p.productName} ¥${p.price}`" :value="p.id" /> </el-select> </el-form-item> <el-form-item label="备注"> <el-input v-model="form.remark" /> </el-form-item> </el-form> <template #footer> <el-button @click="dialogVisible = false">取消</el-button> <el-button type="primary" :loading="saving" @click="handleSave">确认消费</el-button> </template> </el-dialog> </div> </template> <script setup> /** * 消费管理页面 */ import { ref, reactive, onMounted } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import request from '@/utils/request' const tableData = ref([]) const members = ref([]) const products = ref([]) const total = ref(0) const dialogVisible = ref(false) const saving = ref(false) const formRef = ref() const query = reactive({ pageNum: 1, pageSize: 10, memberId: null }) const form = reactive({ memberId: null, productId: null, remark: '' }) const rules = { memberId: [{ required: true, message: '请选择会员', trigger: 'change' }], productId: [{ required: true, message: '请选择商品', trigger: 'change' }] } async function loadData() { const res = await request.get('/api/consume/list', { params: query }) tableData.value = res.data.records total.value = res.data.total } async function loadOptions() { const [m, p] = await Promise.all([ request.get('/api/member/all'), request.get('/api/product/all') ]) members.value = m.data products.value = p.data } function openDialog() { Object.assign(form, { memberId: null, productId: null, remark: '' }) dialogVisible.value = true } async function handleSave() { await formRef.value.validate() saving.value = true try { await request.post('/api/consume', form) ElMessage.success('消费成功') dialogVisible.value = false loadData() loadOptions() } finally { saving.value = false } } function handleDelete(row) { ElMessageBox.confirm('确定删除该消费记录吗?', '提示', { type: 'warning' }).then(async () => { await request.delete(`/api/consume/${row.id}`) ElMessage.success('删除成功') loadData() }).catch(() => {}) } onMounted(() => { loadOptions(); loadData() }) </script>