学校社团管理系统

0 阅读8分钟

前言

本人从刚开始做这个项目,也是第一次做这个项目,也是得到了很多很大的提升,期间问过老师很多问题,以及询问ai,大部分时间都是自学,最终完成了这个项目。那么我将以我怎么设计这个管理系统为开头来进行接下来的文档解说。

设计思路

提前说明本项目用到了Vue3,springboot,mysql8。

设计这个学校社团系统的意义是什么?那无非就是学校纸质化管理复杂,所以需要一个平台来透明化管理,那我们在了解一个活动的流程:“明确主题,宣传推广,活动报名,活动签到,反馈总结。”,那么社团管理系统没有社团怎么行呢,所以我们还需要社团表。知道了目标就可以建立数据库表了,首先要建立一个

用户表,来存放学生的个人信息,用户id,学号,姓名,班级,头像,个人简介,以及权限(用于判断是管理员还是学生)。

活动表,需要活动id,活动标题,活动内容,活动地点,活动时间。

宣传表,宣传id,与活动id绑定关系,宣传内容,宣传图,宣传时间,与活动时间绑定关系。

活动通知表,通知id,与社团id绑定关系,通知内容,通知时间。

报名表,报名id,与用户id绑定,与活动id绑定关系,姓名,手机号,报名时间。

签到表,活动id,与用户id绑定关系,是否签到,签到时间。

反馈总结表,总结id,总结。

社团类别表:社团类别id,社团类别。

社团表:社团id,社团名称,与社团类别id绑定关系,与用户id绑定关系,社团人数,社团简介,社团图片,社团创建时间,社团更新时间。

以上就是基本的数据库结构了,接下来交给ai生成。

create table activities
(
    activity_id   bigint auto_increment comment '活动ID'
        primary key,
    title         varchar(255) not null comment '活动标题',
    content       text         null comment '活动内容',
    location      varchar(255) null comment '活动地点',
    activity_time datetime     null comment '活动时间'
)
    comment '活动表';

create table activity_notifications
(
    notification_id   bigint auto_increment comment '通知ID'
        primary key,
    club_id           bigint                             not null comment '关联社团ID',
    content           text                               null comment '通知内容',
    notification_time datetime default CURRENT_TIMESTAMP null comment '通知时间'
)
    comment '活动通知表';

create table check_ins
(
    activity_id   bigint               not null comment '活动ID',
    user_id       bigint               not null comment '用户ID',
    is_checked_in tinyint(1) default 0 null comment '是否签到',
    check_in_time datetime             null comment '签到时间',
    primary key (activity_id, user_id)
)
    comment '签到表';

create index user_id
    on check_ins (user_id);

create table club_categories
(
    category_id   bigint auto_increment comment '社团类别ID'
        primary key,
    category_name varchar(100) not null comment '社团类别名称'
)
    comment '社团类别表';

create table clubs
(
    club_id      bigint auto_increment comment '社团ID'
        primary key,
    club_name    varchar(100)                       not null comment '社团名称',
    category_id  bigint                             not null comment '关联社团类别ID',
    creator_id   bigint                             not null comment '关联用户ID(创建者)',
    member_count int      default 0                 null comment '社团人数',
    description  text                               null comment '社团简介',
    logo_url     varchar(255)                       null comment '社团图片地址',
    created_at   datetime default CURRENT_TIMESTAMP null comment '创建时间',
    updated_at   datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间'
)
    comment '社团表';

create index category_id
    on clubs (category_id);

create index creator_id
    on clubs (creator_id);

create table feedback_summaries
(
    summary_id bigint auto_increment comment '总结ID'
        primary key,
    content    text null comment '总结内容'
)
    comment '反馈总结表';

create table promotions
(
    promotion_id   bigint auto_increment comment '宣传ID'
        primary key,
    activity_id    bigint                             not null comment '关联活动ID',
    content        text                               null comment '宣传内容',
    image_url      varchar(255)                       null comment '宣传图地址',
    promotion_time datetime default CURRENT_TIMESTAMP null comment '宣传时间'
)
    comment '宣传表';

create index activity_id
    on promotions (activity_id);

create table registrations
(
    registration_id   bigint auto_increment comment '报名ID'
        primary key,
    user_id           bigint                             not null comment '关联用户ID',
    activity_id       bigint                             not null comment '关联活动ID',
    name              varchar(100)                       not null comment '报名姓名',
    phone             varchar(20)                        not null comment '手机号',
    registration_time datetime default CURRENT_TIMESTAMP null comment '报名时间'
)
    comment '报名表';

create index activity_id
    on registrations (activity_id);

create index user_id
    on registrations (user_id);

create table users
(
    user_id    bigint auto_increment comment '用户ID'
        primary key,
    student_id varchar(50)                                 not null comment '学号',
    name       varchar(100)                                not null comment '姓名',
    class_name varchar(100)                                null comment '班级',
    avatar_url varchar(255)                                null comment '头像地址',
    bio        text                                        null comment '个人简介',
    role       enum ('ADMIN', 'STUDENT') default 'STUDENT' not null comment '权限:管理员或学生'
)
    comment '用户表';

代码实现

整体来说都是一些增删改查(模糊分页查询) 运用mybatis-plus的分页插件实现起来非常简单,简化了大部分时间。整体来说,大部分后端实现都相同,这里我就展示一小部分。

@RestController
@RequestMapping("/clubinfo")
@RequiredArgsConstructor //自动注入 @Autowired
@CrossOrigin(origins = "http://example.com")
public class ClubInfoController {

    private final ClubInfoService clubInfoService;

    // 获取社团列表
    @GetMapping("/list")
    public List<ClubInfo> getClubInfoList() {
        return clubInfoService.getClubInfoList();
    }

    //注册社团
    @PostMapping("/register")
    public String registerClub(@RequestBody ClubInfo clubInfo) {
        try {
            System.out.println("!!!!!!!!"+clubInfo);
            clubInfoService.registerClub(clubInfo);
            return "社团注册成功";
        } catch (Exception e) {
            return "社团注册失败:" + e.getMessage();
        }
    }

    // 更新社团信息
    @PutMapping("/update")
    public BusinessException updateClub(@RequestBody ClubInfo clubInfo) {
        try {
            clubInfoService.updateClub(clubInfo);
            return new BusinessException(200,"更新成功") ;
        } catch (Exception e) {
            return new BusinessException(400,"更新失败");
        }
    }

    // 删除社团
    @DeleteMapping("/delete/{id}")
    public BusinessException deleteClub(@PathVariable Integer id) {
        try {
            clubInfoService.deleteClub(id);
            return new BusinessException(200,"删除成功") ;
        } catch (Exception e) {
            return new BusinessException(400,"删除失败");
        }
    }

    //获取所有社团信息
    @GetMapping("/all")
    public List<ClubInfoDto> getAllClubInfo() {
        return clubInfoService.getAllClubInfo();
    }
}
@Service
@RequiredArgsConstructor
public class ClubInfoServiceImpl implements ClubInfoService {

    private final ClubInfoMapper clubInfoMapper;
    private final UserMapper userMapper;

    @Override
    public List<ClubInfo> getClubInfoList() {
        return clubInfoMapper.selectList(null);
    }

    @Override
    public int registerClub(ClubInfo clubInfo) {
        User user = userMapper.selectByUserName(clubInfo.getPresidentName());
        clubInfo.setPresidentUserId(user.getUserId());
        return clubInfoMapper.insert(clubInfo);
    }

    @Override
    public boolean updateClub(ClubInfo clubInfo) {
        return clubInfoMapper.updateById(clubInfo) > 0;
    }

    @Override
    public boolean deleteClub(Integer id) {
        return clubInfoMapper.deleteById(id) > 0;
    }

    @Override
    public List<ClubInfoDto> getAllClubInfo() {
        MPJLambdaWrapper<ClubInfo> wrapper = new MPJLambdaWrapper<ClubInfo>()
                .selectAll(ClubInfo.class) // 查询ClubInfo的所有属性
                .leftJoin(ClubCategory.class, ClubCategory::getCategoryId, ClubInfo::getCategoryId) // 通过categoryId关联ClubCategory
                .select(ClubCategory::getCategoryName); // 查询ClubCategory的categoryName
        List<ClubInfoDto> clubInfoDtos = clubInfoMapper.selectJoinList(ClubInfoDto.class, wrapper);
        return clubInfoDtos;
    }


}

相信各位都能够实现这些简单的部分,知道了自己要干的事情是什么,就能够快速的实现。

我觉得我最重要的量点是接入了ai,能够帮助管理员更好的管理社团,创建社团活动。 下面是代码实现

<script lang="ts" setup>
import { ref } from 'vue';
import { ElMessage } from 'element-plus';
import axios from 'axios';
import { useRouter } from 'vue-router';

const router = useRouter();

// 用户输入
const userInput = ref('');
// 聊天记录
const chatHistory = ref<{ role: string; content: string }[]>([]);
// 加载状态
const isLoading = ref(false);

// 返回上一页
const goBack = () => {
    router.go(-1);
};

// 发送消息
const sendMessage = async () => {
    if (!userInput.value.trim()) {
        ElMessage.warning('请输入内容');
        return;
    }

    isLoading.value = true;

    // 将用户输入添加到聊天记录
    chatHistory.value.push({ role: 'user', content: userInput.value });

    try {
        // 调用后端 API
        const response = await axios.post('api/ai/chat', {
            message: userInput.value,
        });

        // 将后端返回的 AI 回复添加到聊天记录
        chatHistory.value.push({ role: 'assistant', content: response.data.reply });
    } catch (error) {
        if (error.response) {
            // 请求成功,但服务器返回错误状态码
            console.error('Error response:', error.response.data);
            ElMessage.error(`请求失败:${error.response.data.message}`);
        } else if (error.request) {
            // 请求已发出,但未收到响应
            console.error('No response received:', error.request);
            ElMessage.error('未收到响应,请检查网络连接');
        } else {
            // 其他错误
            console.error('Error:', error.message);
            ElMessage.error(`请求失败:${error.message}`);
        }
    } finally {
        isLoading.value = false;
        userInput.value = '';
    }
};
</script>
@RestController
@RequestMapping("/api/ai")
public class AIController {

    @PostMapping("/chat")
    public ResponseEntity<Map<String, String>> chat(@RequestBody Map<String, String> request) {
        String userMessage = request.get("message");

        if (userMessage == null || userMessage.trim().isEmpty()) {
            return ResponseEntity.badRequest().body(Map.of("message", "用户输入不能为空"));
        }

        try {
            // 调用 DeepSeek AI API
            String aiReply = callDeepSeekAPI(userMessage);

            // 返回 AI 回复
            return ResponseEntity.ok(Map.of("reply", aiReply));
        } catch (Exception e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(Map.of("message", "调用 AI 服务失败,请稍后重试"));
        }
    }

    private String callDeepSeekAPI(String userMessage) throws IOException {
        // DeepSeek AI API URL
        String apiUrl = "https://api.deepseek.com/v1/chat/completions";

        // 请求体
        String requestBody = """
                {
                    "model": "deepseek-chat",
                    "messages": [
                        {"role": "system", "content": "You are a helpful assistant."},
                        {"role": "user", "content": "%s"}
                    ]
                }
                """.formatted(userMessage);

        // 创建 HTTP 请求
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(apiUrl))
                .header("Content-Type", "application/json")
                .header("Authorization", "Bearer DeepSeek API") // 替换为你的 DeepSeek API 密钥
                .POST(HttpRequest.BodyPublishers.ofString(requestBody))
                .build();

        // 发送请求
        HttpClient client = HttpClient.newHttpClient();
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

        // 检查响应状态
        if (response.statusCode() != 200) {
            throw new RuntimeException("调用 DeepSeek AI API 失败,状态码:" + response.statusCode());
        }

        // 解析响应
        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode jsonNode = objectMapper.readTree(response.body());
        return jsonNode.get("choices").get(0).get("message").get("content").asText();
    }
}

以及可视化数据,运用了echarts依赖,虽然很简陋,但是也是做到了简单的动画效果

<template>
  <v-chart class="chart" :option="option" autoresize />
</template>

<script setup>
import { use } from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
import { PieChart } from 'echarts/charts';
import {
  TitleComponent,
  TooltipComponent,
  LegendComponent,
} from 'echarts/components';
import VChart, { THEME_KEY } from 'vue-echarts';
import { ref, provide, onMounted } from 'vue';
import axios from 'axios';

use([
  CanvasRenderer,
  PieChart,
  TitleComponent,
  TooltipComponent,
  LegendComponent,
]);

provide(THEME_KEY, 'dark');

const option = ref({
  title: {
    text: '学生社团分布',
    left: 'center',
  },
  tooltip: {
    trigger: 'item',
    formatter: '{a} <br/>{b} : {c}人 ({d}%)',
  },
  legend: {
    orient: 'vertical',
    left: 'left',
    data: [], // 初始为空,动态更新
  },
  series: [
    {
      name: '学生社团分布',
      type: 'pie',
      radius: '60%',
      center: ['50%', '45%'],
      data: [], // 初始为空,动态更新
      emphasis: {
        itemStyle: {
          shadowBlur: 10,
          shadowOffsetX: 0,
          shadowColor: 'rgba(0, 0, 0, 0.5)',
        },
      },
    },
  ],
});

// 获取社团信息并更新图表
const fetchClubInfo = async () => {
  try {
    const response = await axios.get('api/clubinfo/all');
    const clubInfoList = response.data;

    // 统计每个类别的总人数
    const typeCounts = clubInfoList.reduce((acc, club) => {
      acc[club.categoryName] = (acc[club.categoryName] || 0) + club.memberCount;
      return acc;
    }, {});

    const chartData = Object.keys(typeCounts).map(type => ({
      value: typeCounts[type],
      name: type,
    }));

    option.value.legend.data = Object.keys(typeCounts);
    option.value.series[0].data = chartData;
  } catch (error) {
    console.error('获取社团信息失败:', error);
  }
};

// 组件挂载时获取数据
onMounted(() => {
  fetchClubInfo();
});
</script>

展示成果

接下来就给大家展示一下我的成果图吧 动画.gif

动画.gif

动画.gif

动画.gif 以上就是部分的成果展示了。

结语

最后说一下,博主自己做的这个项目在学校比赛中荣获了一等奖, 对于这次的比赛,我学会了很多,也发现了很多不足,有了很多新颖的设计思路,待我下次将全部重构一遍,一定可以优化的更好,到时候再来发布完整的一套文档。

最后点点赞不迷路,关注博主,即可查看后续博主的百万年薪代码梦,我们一起共同进步。