大家好,我是Java1234_小锋老师,分享一套锋哥原创的基于Python的通讯录管理系统(FastAPI+Vue3)

项目介绍

随着移动互联网与数字化办公的快速发展,个人与企业在日常工作中需要对大量联系人信息进行统一管理。传统纸质通讯录或分散存储于手机、表格中的联系方式,存在检索不便、备份困难、多人协作能力弱、权限控制不足等问题。针对上述需求,本文设计并实现了一套基于Python的通讯录管理系统。系统采用前后端分离的B/S架构,后端使用Python语言与FastAPI框架构建高性能RESTful API,结合SQLAlchemy完成对象关系映射,数据持久化采用MySQL数据库;前端采用Vue3、Vite、Pinia、Vue Router与Element Plus构建管理端界面,并使用ECharts实现首页数据可视化统计。系统围绕用户认证、个人中心、联系人管理、分组管理、Excel导入导出、操作日志与后台用户管理等核心业务展开,通过JWT实现无状态身份认证,并通过角色区分管理员与普通用户,保证数据隔离与访问控制。论文完成了需求分析、总体设计、数据库E-R建模、详细表结构设计、关键模块实现与系统测试等工作。测试结果表明,系统功能完整、运行稳定,能够满足本科毕业设计对工程实践能力的训练要求,同时为后续扩展移动端接入、密码加密存储与更细粒度权限控制提供了良好基础。

源码下载 

链接: https://pan.baidu.com/s/1INXE-TJiN7qTRTklTAPC-Q?pwd=1234
提取码: 1234

系统展示

核心代码

"""
用户服务
"""
from sqlalchemy import or_
from sqlalchemy.orm import Session

from app.common.deps import CurrentUser
from app.common.exceptions import BusinessException
from app.common.jwt_util import JwtUtil
from app.common.utils import build_page_result
from app.models.user import User
from app.schemas.serializers import user_to_dict
from app.schemas.user import LoginDTO, ProfileUpdateDTO, RegisterDTO, UserSaveDTO


class UserService:
    """用户服务类"""

    @staticmethod
    def login(db: Session, dto: LoginDTO) -> dict:
        """用户登录"""
        user = db.query(User).filter(User.username == dto.username).first()
        if not user:
            raise BusinessException("用户名不存在")
        if user.password != dto.password:
            raise BusinessException("密码错误")
        if user.status != 1:
            raise BusinessException("账号已被禁用")

        token = JwtUtil.generate_token(user.id, user.username, user.role)
        return {"token": token, "user": user_to_dict(user)}

    @staticmethod
    def register(db: Session, dto: RegisterDTO) -> None:
        """用户注册"""
        if dto.confirmPassword is not None and dto.password != dto.confirmPassword:
            raise BusinessException("两次输入的密码不一致")

        exists = db.query(User).filter(User.username == dto.username).count()
        if exists > 0:
            raise BusinessException("用户名已存在")

        user = User(
            username=dto.username,
            password=dto.password,
            nickname=dto.nickname or dto.username,
            phone=dto.phone,
            email=dto.email,
            gender=0,
            role=1,
            status=1,
        )
        db.add(user)
        db.commit()

    @staticmethod
    def get_by_id(db: Session, user_id: int) -> User | None:
        """根据ID获取用户"""
        return db.query(User).filter(User.id == user_id).first()

    @staticmethod
    def page_users(db: Session, page_num: int, page_size: int, keyword: str | None) -> dict:
        """分页查询用户"""
        query = db.query(User)
        if keyword:
            like = f"%{keyword}%"
            query = query.filter(
                or_(User.username.like(like), User.nickname.like(like), User.phone.like(like))
            )
        total = query.count()
        records = (
            query.order_by(User.create_time.desc())
            .offset((page_num - 1) * page_size)
            .limit(page_size)
            .all()
        )
        return build_page_result(
            [user_to_dict(item) for item in records], total, page_num, page_size
        )

    @staticmethod
    def save_user(db: Session, dto: UserSaveDTO) -> None:
        """管理员新增用户"""
        exists = db.query(User).filter(User.username == dto.username).count()
        if exists > 0:
            raise BusinessException("用户名已存在")
        user = User(
            username=dto.username,
            password=dto.password or "123456",
            nickname=dto.nickname,
            phone=dto.phone,
            email=dto.email,
            gender=dto.gender or 0,
            role=dto.role if dto.role is not None else 1,
            status=dto.status if dto.status is not None else 1,
            avatar=dto.avatar,
        )
        db.add(user)
        db.commit()

    @staticmethod
    def update_user(db: Session, dto: UserSaveDTO) -> None:
        """管理员修改用户"""
        user = db.query(User).filter(User.id == dto.id).first()
        if not user:
            raise BusinessException("用户不存在")
        user.nickname = dto.nickname
        user.phone = dto.phone
        user.email = dto.email
        user.gender = dto.gender if dto.gender is not None else user.gender
        user.role = dto.role if dto.role is not None else user.role
        user.status = dto.status if dto.status is not None else user.status
        if dto.password:
            user.password = dto.password
        db.commit()

    @staticmethod
    def delete_user(db: Session, user_id: int) -> None:
        """管理员删除用户"""
        if user_id == 1:
            raise BusinessException("不能删除超级管理员")
        user = db.query(User).filter(User.id == user_id).first()
        if user:
            db.delete(user)
            db.commit()

    @staticmethod
    def update_profile(db: Session, user_id: int, dto: ProfileUpdateDTO) -> None:
        """更新个人资料"""
        user = db.query(User).filter(User.id == user_id).first()
        if not user:
            raise BusinessException("用户不存在")
        if dto.nickname is not None:
            user.nickname = dto.nickname
        if dto.phone is not None:
            user.phone = dto.phone
        if dto.email is not None:
            user.email = dto.email
        if dto.gender is not None:
            user.gender = dto.gender
        if dto.avatar:
            user.avatar = dto.avatar
        db.commit()

    @staticmethod
    def change_password(db: Session, user_id: int, old_password: str, new_password: str) -> None:
        """修改密码"""
        user = db.query(User).filter(User.id == user_id).first()
        if not user:
            raise BusinessException("用户不存在")
        if user.password != old_password:
            raise BusinessException("旧密码错误")
        user.password = new_password
        db.commit()

    @staticmethod
    def count_all(db: Session) -> int:
        """统计用户总数"""
        return db.query(User).count()
<template>
  <div class="page-container">
    <div class="search-bar">
      <el-form :inline="true">
        <el-form-item label="分组名称">
          <el-input v-model="query.keyword" placeholder="搜索分组名称" clearable @keyup.enter="loadData" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="loadData"><el-icon><Search /></el-icon> 搜索</el-button>
          <el-button @click="query.keyword='';query.pageNum=1;loadData()"><el-icon><Refresh /></el-icon> 重置</el-button>
        </el-form-item>
      </el-form>
    </div>

    <div class="table-card">
      <div style="margin-bottom:16px">
        <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="groupName" label="分组名称" min-width="160" />
        <el-table-column prop="contactCount" label="联系人数量" min-width="120" align="center" />
        <el-table-column prop="sort" label="排序" min-width="80" align="center" />
        <el-table-column v-if="userStore.isAdmin" prop="ownerName" label="所属用户" min-width="120" />
        <el-table-column label="创建时间" min-width="170">
          <template #default="{ row }">{{ formatDateTime(row.createTime) }}</template>
        </el-table-column>
        <el-table-column label="操作" width="160" fixed="right" align="center">
          <template #default="{ row }">
            <el-button type="primary" link @click="openDialog(row)">编辑</el-button>
            <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, jumper"
          @size-change="loadData"
          @current-change="loadData"
        />
      </div>
    </div>

    <el-dialog v-model="dialogVisible" :title="form.id ? '编辑分组' : '新增分组'" width="480px" destroy-on-close>
      <el-form ref="formRef" :model="form" :rules="formRules" label-width="80px">
        <el-form-item label="分组名称" prop="groupName">
          <el-input v-model="form.groupName" placeholder="请输入分组名称" />
        </el-form-item>
        <el-form-item label="排序">
          <el-input-number v-model="form.sort" :min="0" :max="999" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="handleSubmit">确定</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'
import dayjs from 'dayjs'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useUserStore } from '@/store/user'
import { getGroupPage, addGroup, updateGroup, deleteGroup } from '@/api'

const userStore = useUserStore()
const tableData = ref([])
const total = ref(0)
const dialogVisible = ref(false)
const formRef = ref()
const query = reactive({ pageNum: 1, pageSize: 10, keyword: '' })
const form = reactive({ id: null, groupName: '', sort: 0 })
const formRules = { groupName: [{ required: true, message: '请输入分组名称', trigger: 'blur' }] }

function formatDateTime(d) { return d ? dayjs(d).format('YYYY-MM-DD HH:mm:ss') : '' }

async function loadData() {
  const res = await getGroupPage(query)
  tableData.value = res.data.records
  total.value = res.data.total
}

function openDialog(row) {
  if (row) {
    Object.assign(form, { id: row.id, groupName: row.groupName, sort: row.sort })
  } else {
    Object.assign(form, { id: null, groupName: '', sort: 0 })
  }
  dialogVisible.value = true
}

async function handleSubmit() {
  await formRef.value.validate()
  if (form.id) {
    await updateGroup(form)
    ElMessage.success('修改成功')
  } else {
    await addGroup(form)
    ElMessage.success('新增成功')
  }
  dialogVisible.value = false
  loadData()
}

async function handleDelete(row) {
  await ElMessageBox.confirm(`确定删除分组「${row.groupName}」吗?`, '提示', { type: 'warning' })
  await deleteGroup(row.id)
  ElMessage.success('删除成功')
  loadData()
}

onMounted(loadData)
</script>

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐