从10到100:基于java的志愿者小程序开发笔记
当前志愿服务活动中,组织者与志愿者之间存在信息不对称、管理效率低下等痛点。传统的管理方式依赖人工统计和线下沟通,难以满足日益增长的志愿服务需求。 Excel导出:后端使用POI或EasyExcel库,将查询结果封装为Excel流返回前端下载。背景
研究意义
系统需求分析

业务流程设计
关键代码片段
package com.volunteer.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("t_appointment")
public class Appointment {
@TableId(type = IdType.AUTO)package com.volunteer.service.impl;
import com.volunteer.entity.Activity;
import com.volunteer.entity.Appointment;
import com.volunteer.mapper.ActivityMapper;
import com.volunteer.mapper.AppointmentMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.UUID;
@Service
public class AppointmentServiceImpl {
@Autowired
private ActivityMapper activityMapper;
@Autowired
private AppointmentMapper appointmentMapper;
/**
* 用户预约活动
* @param userId 用户ID
* @param activityId 活动ID
* @param timeSlot 时段
* @return 预约结果
*/
@Transactional(rollbackFor = Exception.class)
public boolean bookActivity(Long userId, Long activityId, String timeSlot) {
// 1. 查询活动信息 (使用 selectById 并锁定行,或使用版本号机制)
// 这里演示简单的检查逻辑,生产环境建议结合 Redis 预减库存
Activity activity = activityMapper.selectById(activityId);
if (activity == null || activity.getStatus() != 1) {
throw new RuntimeException("活动不存在或未开启");
}
if (activity.getBookedCount() >= activity.getTotalQuota()) {
throw new RuntimeException("名额已满");
}
// 2. 检查用户是否重复预约该时段
long count = appointmentMapper.selectCount(
new LambdaQueryWrapper<Appointment>()
.eq(Appointment::getUserId, userId)
.eq(Appointment::getActivityId, activityId)
.eq(Appointment::getTimeSlot, timeSlot)
.in(Appointment::getStatus, 0, 1) // 只查有效预约
);
if (count > 0) {
throw new RuntimeException("您已预约该时段");
}
// 3. 扣减库存 (利用数据库原子更新,防止并发超卖)
// SQL: UPDATE t_activity SET booked_count = booked_count + 1
// WHERE activity_id = ? AND booked_count < total_quota
int updatedRows = activityMapper.decrementQuota(activityId);
if (updatedRows == 0) {
throw new RuntimeException("预约失败,名额已被抢光");
}
// 4. 创建预约记录
Appointment appointment = new Appointment();
appointment.setUserId(userId);
appointment.setActivityId(activityId);
appointment.setTimeSlot(timeSlot);
appointment.setVerifyCode(UUID.randomUUID().toString().replace("-", "")); // 生成核销码
appointment.setStatus(0); // 待参加
appointment.setBookTime(LocalDateTime.now());
appointmentMapper.insert(appointment);
return true;
}
}数据库设计
字段名 类型 长度 主键 非空 默认值 说明 user_idBIGINT 20 YES YES AUTO_INC 用户ID openidVARCHAR 64 NO YES - 微信OpenID (唯一标识) nameVARCHAR 50 NO YES - 真实姓名 phoneVARCHAR 20 NO NO - 联系电话 specialtyVARCHAR 255 NO NO - 服务特长/技能 avatar_urlVARCHAR 255 NO NO - 头像链接 created_atDATETIME - NO YES NOW() 注册时间 updated_atDATETIME - NO YES NOW() 更新时间 字段名 类型 长度 主键 非空 默认值 说明 activity_idBIGINT 20 YES YES AUTO_INC 活动ID titleVARCHAR 100 NO YES - 活动标题 typeVARCHAR 50 NO YES - 活动类型 (如: 环保, 助老) descriptionTEXT - NO NO - 活动详情描述 locationVARCHAR 255 NO NO - 活动地点 total_quotaINT 11 NO YES 0 总招募人数 booked_countINT 11 NO YES 0 已报名人数 start_timeDATETIME - NO YES - 活动开始时间 end_timeDATETIME - NO YES - 活动结束时间 statusTINYINT 1 NO YES 1 状态 (0:下架, 1:进行中, 2:已结束) created_byBIGINT 20 NO YES - 创建者ID (Admin) created_atDATETIME - NO YES NOW() 发布时间 字段名 类型 长度 主键 非空 默认值 说明 content_idBIGINT 20 YES YES AUTO_INC 内容ID categoryVARCHAR 50 NO YES - 分类 (notice, knowledge, etc.) titleVARCHAR 100 NO YES - 标题 bodyLONGTEXT - NO NO - 正文内容 (HTML/Markdown) cover_imgVARCHAR 255 NO NO - 封面图URL view_countINT 11 NO YES 0 浏览次数 created_atDATETIME - NO YES NOW() 发布时间 字段名 类型 长度 主键 非空 默认值 说明 admin_idBIGINT 20 YES YES AUTO_INC 管理员ID usernameVARCHAR 50 NO YES - 登录用户名 password_hashVARCHAR 100 NO YES - 加密后的密码 roleVARCHAR 20 NO YES 'admin' 角色 (super_admin, admin, checker) is_activeTINYINT 1 NO YES 1 账户状态 (0:禁用, 1:启用) last_loginDATETIME - NO NO - 最后登录时间 关键代码逻辑
UI设计




后台管理系统设计



测试结果
git下载