毕设所有选题:
https://blog.csdn.net/2303_76227485/article/details/131104075
基于Springboot+Vue3的共享单车管理系统(源代码+数据库)
项目编号:266
一、系统介绍
本项目前后端分离,分为用户、管理员、运营人员3种角色(角色菜单可自行分配)。
1、用户:
- 注册登录、立即用车、查看附件单车、我的订单、评价、问题反馈
2、维修员:
- 首页大屏数据图表统计
- 单车管理:报修、维修管理
3、运营人员:
- 报修、维修管理、区域规划管理(电子围栏)、投放策略、热点分析、订单列表、异常列表、用户管理、用户反馈
4、管理员:
- 首页大屏数据图表统计:用户数、订单数、今日订单数、单车总量、订单趋势曲线图、收入柱状图、区域订单排行柱状图、订单状态分布环状图
- 用户管理
- 单车管理:报修、维修管理、区域规划管理(电子围栏)、投放策略、热点分析、订单列表、异常列表、用户管理、用户反馈
- 数据分析:订单总量、活跃用户、日均订单、总订单、单车使用趋势、收入分析、骑行时段分布
- 单车费用设置、个人信息、管理员管理、角色管理、菜单管理
5、亮点:
- 使用redis缓存token,既简化业务开发,又能支撑高并发、高可用的系统需求
- 后台首页大屏使用echarts图表统计,更直观的看出系统的运行数据
- 高德地图引入单车电子围栏
二、所用技术
后端技术栈:
- Springboot3
- mybatisPlus
- Jwt
- Spring Security
- Mysql
- Maven
前端技术栈:
- Vue3
- Vue-router
- axios
- elementPlus
- echarts
三、环境介绍
基础环境 :IDEA/eclipse, JDK17或以上, Mysql5.7及以上, Maven3.6, node14, navicat, 高德地图api秘钥
所有项目以及源代码本人均调试运行无问题 可支持远程调试运行
四、页面截图
文档截图:

1、用户:












2、维修员:





3、运营人员:










4、管理员:














五、浏览地址
- 用户账号密码:user1/123456
-
维修员账号密码:maintainer/123456
-
运营人员账号密码:operator/123456
-
管理员账户密码:admin/123456
六、部署教程
-
使用Navicat或者其它工具,在mysql中创建对应名称的数据库,并执行项目的sql文件
-
使用IDEA/Eclipse导入backend项目,若为maven项目请选择maven,等待依赖下载完成
-
修改application.yml里面的数据库配置,启动redis后
src/main/java/com/bikesharing/BikeApplication.java启动后端项目
-
vscode或idea打开client项目
-
在编译器中打开terminal,执行npm install 依赖下载完成后执行 npm run serve,执行成功后会显示访问地址
-
vscode或idea打开frontend项目
-
在编译器中打开terminal,执行npm install 依赖下载完成后执行 npm run serve,执行成功后会显示访问地址
七、部分代码
java
public class BikeOrderServiceImpl extends ServiceImpl<BikeOrderMapper, BikeOrder> implements BikeOrderService {
@Autowired
private BikeMapper bikeMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private BikeService bikeService;
@Autowired
private SystemSettingsMapper systemSettingsMapper;
@Override
public PageResult<OrderDTO> getOrderPage(Map<String, Object> params) {
Integer page = params.get("page") != null ? Integer.parseInt(params.get("page").toString()) : 1;
Integer size = params.get("size") != null ? Integer.parseInt(params.get("size").toString()) : 10;
// 创建分页对象
Page<OrderDTO> pageParam = new Page<>(page, size);
// 提取查询参数
String orderNo = params.get("orderNo") != null ? params.get("orderNo").toString() : null;
String userAccount = params.get("userAccount") != null ? params.get("userAccount").toString() : null;
String bikeNo = params.get("bikeNo") != null ? params.get("bikeNo").toString() : null;
Integer status = params.get("status") != null ? Integer.parseInt(params.get("status").toString()) : null;
String startDate = params.get("startDate") != null ? params.get("startDate").toString() : null;
String endDate = params.get("endDate") != null ? params.get("endDate").toString() : null;
// 查询订单列表
IPage<OrderDTO> orderIPage = baseMapper.selectOrderPage(pageParam, orderNo, userAccount, bikeNo, status, startDate, endDate);
// 封装分页结果
return new PageResult<OrderDTO>(orderIPage.getTotal(), orderIPage.getRecords());
}
@Override
public BikeOrder getOrderById(Long id) {
return baseMapper.selectById(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean createOrder(BikeOrder order) {
// 设置订单编号
order.setOrderNo(generateOrderNo());
// 设置创建时间和更新时间
LocalDateTime now = LocalDateTime.now();
order.setCreateTime(now);
order.setUpdateTime(now);
// 设置订单状态为进行中
order.setStatus(1);
// 更新单车状态为使用中
bikeService.updateBikeStatus(order.getBikeId(), 2);
return baseMapper.insert(order) > 0;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean completeOrder(Long id, Double endLng, Double endLat,String endLocation) {
// 查询订单信息
BikeOrder order = baseMapper.selectById(id);
if (order == null || order.getStatus() != 1) {
return false;
}
// 设置结束时间
LocalDateTime now = LocalDateTime.now();
order.setEndTime(now);
// 设置结束经纬度位置
order.setEndLng(new BigDecimal(endLng));
order.setEndLat(new BigDecimal(endLat));
// 设置结束位置描述
order.setEndLocation(endLocation);
// 计算骑行时长(核心修改:不满1分钟按1分钟算,超过1分钟向上取整)
Duration duration = Duration.between(order.getStartTime(), now);
long totalSeconds = duration.getSeconds(); // 获取总秒数
long minutes;
if (totalSeconds <= 0) {
// 极端情况:时长为0秒(立即结束),按1分钟算
minutes = 1;
} else if (totalSeconds < 60) {
// 不满1分钟(1-59秒),按1分钟算
minutes = 1;
} else {
// 超过1分钟:先取整分钟数,有余数则+1(向上取整)
long integerMinutes = totalSeconds / 60; // 整数部分(比如70秒=1,130秒=2)
long remainderSeconds = totalSeconds % 60; // 余数部分(比如70秒=10,130秒=10)
minutes = remainderSeconds > 0 ? integerMinutes + 1 : integerMinutes;
}
order.setRideTime((int) minutes);
// 计算骑行距离(千米)模拟一分钟走0.2千米(同步适配新的分钟数)
BigDecimal distance = BigDecimal.valueOf(order.getRideTime() * 0.2);
order.setRideDistance(distance);
// 获取系统配置(取第一条记录)
LambdaQueryWrapper<SystemSettings> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByAsc(SystemSettings::getId).last("LIMIT 1");
SystemSettings settings = systemSettingsMapper.selectOne(queryWrapper);
// 定义默认计费参数(兜底值)
BigDecimal unlockFee = BigDecimal.valueOf(1.00); // 默认解锁费1元
BigDecimal rideRate = BigDecimal.valueOf(0.10); // 默认费率0.1元/分钟
// 如果获取到配置,使用配置值
if (settings != null) {
unlockFee = settings.getUnlockFee() != null ? settings.getUnlockFee() : unlockFee;
rideRate = settings.getRideRate() != null ? settings.getRideRate() : rideRate;
}
// 计算订单金额:解锁费 + 骑行分钟数 × 每分钟费率(使用新的分钟数)
// 使用BigDecimal精确计算,避免浮点数误差
BigDecimal minutesBig = BigDecimal.valueOf(minutes);
BigDecimal amount = unlockFee.add(rideRate.multiply(minutesBig));
// 保留2位小数(精确到分),四舍五入(银行家舍入法)
amount = amount.setScale(2, RoundingMode.HALF_UP);
order.setAmount(amount);
// 设置订单状态为已完成
order.setStatus(2);
// 更新单车状态为可用
bikeService.updateBikeStatus(order.getBikeId(), 1);
return baseMapper.updateById(order) > 0;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean cancelOrder(Long id) {
// 查询订单信息
BikeOrder order = baseMapper.selectById(id);
if (order == null || order.getStatus() != 1) {
return false;
}
// 设置订单状态为已取消
order.setStatus(3);
// 更新单车状态为可用
bikeService.updateBikeStatus(order.getBikeId(), 1);
return baseMapper.updateById(order) > 0;
}
@Override
public int getTodayOrderCount() {
return baseMapper.selectTodayOrderCount();
}
@Override
public Long getUserCount() {
return userMapper.selectCount(null);
}
@Override
public Map<String, Object> getOrderTrend(Integer days, Integer type, String startDate) {
Map<String, Object> result = new HashMap<>();
List<String> dates = new ArrayList<>();
List<Integer> orders = new ArrayList<>();
List<BigDecimal> incomes = new ArrayList<>();
LocalDate endDate = LocalDate.now();
LocalDate startLocalDate;
DateTimeFormatter formatter;
// 如果提供了startDate参数,则使用它作为开始日期
if (startDate != null && !startDate.isEmpty()) {
startLocalDate = LocalDate.parse(startDate);
// 根据类型计算结束日期
if (type == 2) { // 月
endDate = startLocalDate.plusMonths(days - 1);
} else if (type == 3) { // 年
endDate = startLocalDate.plusYears(days - 1);
} else { // 日
endDate = startLocalDate.plusDays(days - 1);
}
} else {
// 根据类型确定开始日期和日期格式
switch (type) {
case 2: // 月
startLocalDate = endDate.minusMonths(days - 1).withDayOfMonth(1);
break;
case 3: // 年
startLocalDate = endDate.minusYears(days - 1).withDayOfYear(1);
break;
case 1: // 日(默认)
default:
startLocalDate = endDate.minusDays(days - 1);
break;
}
}
// 确定日期格式
switch (type) {
case 2: // 月
formatter = DateTimeFormatter.ofPattern("yyyy-MM");
break;
case 3: // 年
formatter = DateTimeFormatter.ofPattern("yyyy");
break;
case 1: // 日(默认)
default:
formatter = DateTimeFormatter.ofPattern("MM-dd");
break;
}
// 查询订单趋势数据
Map<String, Object> params = new HashMap<>();
params.put("startDate", startLocalDate.toString());
params.put("endDate", endDate.toString());
params.put("type", type);
System.out.println("订单趋势SQL参数: startDate=" + startLocalDate + ", endDate=" + endDate + ", type=" + type);
List<Map<String, Object>> trendData = baseMapper.selectOrderTrend(params);
System.out.println("订单趋势SQL查询结果: " + trendData);
// 处理查询结果
if (type == 3) { // 年
for (int i = 0; i < days; i++) {
LocalDate date = startLocalDate.plusYears(i);
String dateStr = date.format(formatter);
dates.add(dateStr);
Map<String, Object> yearData = findDataByDateFormat(trendData, dateStr, "year");
orders.add(yearData != null ? ((Number) yearData.get("orders")).intValue() : 0);
incomes.add(yearData != null ? (BigDecimal) yearData.get("income") : BigDecimal.ZERO);
}
} else if (type == 2) { // 月
for (int i = 0; i < days; i++) {
LocalDate date = startLocalDate.plusMonths(i);
String dateStr = date.format(formatter);
dates.add(dateStr);
Map<String, Object> monthData = findDataByDateFormat(trendData, dateStr, "month");
orders.add(monthData != null ? ((Number) monthData.get("orders")).intValue() : 0);
incomes.add(monthData != null ? (BigDecimal) monthData.get("income") : BigDecimal.ZERO);
}
} else { // 日
for (int i = 0; i < days; i++) {
LocalDate date = startLocalDate.plusDays(i);
String dateStr = date.format(formatter);
dates.add(dateStr);
Map<String, Object> dayData = findDataByDateFormat(trendData, dateStr, "day");
System.out.println("日期: " + dateStr + ", 查找结果: " + dayData);
orders.add(dayData != null ? ((Number) dayData.get("orders")).intValue() : 0);
incomes.add(dayData != null ? (BigDecimal) dayData.get("income") : BigDecimal.ZERO);
}
}
result.put("dates", dates);
result.put("orders", orders);
result.put("incomes", incomes.stream().map(BigDecimal::doubleValue).collect(Collectors.toList()));
return result;
}
/**
* 根据日期格式查找数据
*/
private Map<String, Object> findDataByDateFormat(List<Map<String, Object>> dataList, String dateStr, String dateField) {
for (Map<String, Object> data : dataList) {
Object dateObj = data.get(dateField);
if (dateObj != null && dateStr.equals(dateObj.toString())) {
return data;
}
}
return null;
}
@Override
public List<Map<String, Object>> getAreaRanking() {
return baseMapper.selectAreaRanking();
}
@Override
public List<Map<String, Object>> getLatestOrders(int limit) {
return baseMapper.selectLatestOrders(limit);
}
@Override
public List<Map<String, Object>> getAbnormalOrders(int limit) {
return baseMapper.selectAbnormalOrders(limit);
}
@Override
public Integer getOrderCount(String startDate, String endDate, Long areaId) {
Map<String, Object> params = new HashMap<>();
params.put("startDate", startDate);
params.put("endDate", endDate);
params.put("areaId", areaId);
return baseMapper.selectOrderCount(params);
}
@Override
public Double getOrderCountTrend(String startDate, String endDate, Long areaId) {
// 计算同期时间
LocalDate start = LocalDate.parse(startDate);
LocalDate end = LocalDate.parse(endDate);
long days = ChronoUnit.DAYS.between(start, end) + 1;
LocalDate lastStart = start.minusDays(days);
LocalDate lastEnd = end.minusDays(days);
// 查询当前时间段订单数
Integer currentCount = getOrderCount(startDate, endDate, areaId);
// 查询同期时间段订单数
Integer lastCount = getOrderCount(lastStart.toString(), lastEnd.toString(), areaId);
// 计算增长率
if (lastCount == 0) {
return 100.0; // 上期为0,本期有数据,增长率为100%
}
double result = (double) (currentCount - lastCount) * 100 / lastCount;
return BigDecimal.valueOf(result).setScale(1, RoundingMode.HALF_UP).doubleValue();
}
@Override
public Integer getActiveUserCount(String startDate, String endDate, Long areaId) {
Map<String, Object> params = new HashMap<>();
params.put("startDate", startDate);
params.put("endDate", endDate);
params.put("areaId", areaId);
return baseMapper.selectActiveUserCount(params);
}
@Override
public Double getActiveUserCountTrend(String startDate, String endDate, Long areaId) {
// 计算同期时间
LocalDate start = LocalDate.parse(startDate);
LocalDate end = LocalDate.parse(endDate);
long days = ChronoUnit.DAYS.between(start, end) + 1;
LocalDate lastStart = start.minusDays(days);
LocalDate lastEnd = end.minusDays(days);
// 查询当前时间段活跃用户数
Integer currentCount = getActiveUserCount(startDate, endDate, areaId);
// 查询同期时间段活跃用户数
Integer lastCount = getActiveUserCount(lastStart.toString(), lastEnd.toString(), areaId);
// 计算增长率
if (lastCount == 0) {
return 100.0; // 上期为0,本期有数据,增长率为100%
}
double result = (double) (currentCount - lastCount) * 100 / lastCount;
return BigDecimal.valueOf(result).setScale(1, RoundingMode.HALF_UP).doubleValue();
}
@Override
public Double getDailyOrderAvg(String startDate, String endDate, Long areaId) {
Integer totalOrders = getOrderCount(startDate, endDate, areaId);
// 计算天数
LocalDate start = LocalDate.parse(startDate);
LocalDate end = LocalDate.parse(endDate);
long days = ChronoUnit.DAYS.between(start, end) + 1;
if (days == 0) {
return 0.0;
}
double result = (double) totalOrders / days;
return BigDecimal.valueOf(result).setScale(1, RoundingMode.HALF_UP).doubleValue();
}
@Override
public Double getDailyOrderAvgTrend(String startDate, String endDate, Long areaId) {
// 计算当前时间段日均订单
Double currentAvg = getDailyOrderAvg(startDate, endDate, areaId);
// 计算同期时间
LocalDate start = LocalDate.parse(startDate);
LocalDate end = LocalDate.parse(endDate);
long days = ChronoUnit.DAYS.between(start, end) + 1;
LocalDate lastStart = start.minusDays(days);
LocalDate lastEnd = end.minusDays(days);
// 计算同期时间段日均订单
Double lastAvg = getDailyOrderAvg(lastStart.toString(), lastEnd.toString(), areaId);
// 计算增长率
if (lastAvg == 0) {
return 100.0; // 上期为0,本期有数据,增长率为100%
}
double result = (currentAvg - lastAvg) * 100 / lastAvg;
return BigDecimal.valueOf(result).setScale(1, RoundingMode.HALF_UP).doubleValue();
}
@Override
public Double getTotalIncome(String startDate, String endDate, Long areaId) {
Map<String, Object> params = new HashMap<>();
params.put("startDate", startDate);
params.put("endDate", endDate);
params.put("areaId", areaId);
BigDecimal totalIncome = baseMapper.selectTotalIncome(params);
double result = totalIncome == null ? 0.0 : totalIncome.doubleValue();
return BigDecimal.valueOf(result).setScale(1, RoundingMode.HALF_UP).doubleValue();
}
@Override
public Double getTotalIncomeTrend(String startDate, String endDate, Long areaId) {
// 计算当前时间段总收入
Double currentIncome = getTotalIncome(startDate, endDate, areaId);
// 计算同期时间
LocalDate start = LocalDate.parse(startDate);
LocalDate end = LocalDate.parse(endDate);
long days = ChronoUnit.DAYS.between(start, end) + 1;
LocalDate lastStart = start.minusDays(days);
LocalDate lastEnd = end.minusDays(days);
// 计算同期时间段总收入
Double lastIncome = getTotalIncome(lastStart.toString(), lastEnd.toString(), areaId);
// 计算增长率
if (lastIncome == 0) {
return 100.0; // 上期为0,本期有数据,增长率为100%
}
double result = (currentIncome - lastIncome) * 100 / lastIncome;
return BigDecimal.valueOf(result).setScale(1, RoundingMode.HALF_UP).doubleValue();
}
@Override
public Map<String, Object> getIncomeAnalysis(String startDate, String endDate, Long areaId) {
Map<String, Object> result = new HashMap<>();
// 处理日期参数
LocalDate start = startDate == null ? LocalDate.now().minusDays(6) : LocalDate.parse(startDate);
LocalDate end = endDate == null ? LocalDate.now() : LocalDate.parse(endDate);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd");
// 生成日期列表
List<String> dates = new ArrayList<>();
List<LocalDate> dateList = new ArrayList<>();
for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
dates.add(date.format(formatter));
dateList.add(date);
}
// 查询收入数据
Map<String, Object> params = new HashMap<>();
params.put("startDate", start.toString());
params.put("endDate", end.toString());
params.put("areaId", areaId);
List<Map<String, Object>> incomeData = baseMapper.selectIncomeAnalysis(params);
// 处理收入数据
List<Double> total = new ArrayList<>();
List<Double> electric = new ArrayList<>();
List<Double> normal = new ArrayList<>();
for (LocalDate date : dateList) {
String dateStr = date.format(formatter);
// 查找当天数据
Map<String, Object> dayData = findDataByDateStr(incomeData, dateStr);
if (dayData != null) {
double totalValue = ((BigDecimal) dayData.get("total_income")).doubleValue();
double electricValue = ((BigDecimal) dayData.get("electric_income")).doubleValue();
double normalValue = ((BigDecimal) dayData.get("normal_income")).doubleValue();
total.add(BigDecimal.valueOf(totalValue).setScale(1, RoundingMode.HALF_UP).doubleValue());
electric.add(BigDecimal.valueOf(electricValue).setScale(1, RoundingMode.HALF_UP).doubleValue());
normal.add(BigDecimal.valueOf(normalValue).setScale(1, RoundingMode.HALF_UP).doubleValue());
} else {
total.add(0.0);
electric.add(0.0);
normal.add(0.0);
}
}
result.put("dates", dates);
result.put("total", total);
result.put("electric", electric);
result.put("normal", normal);
return result;
}
/**
* 根据日期字符串查找数据(适用于date_str字段)
*/
private Map<String, Object> findDataByDateStr(List<Map<String, Object>> dataList, String dateStr) {
for (Map<String, Object> data : dataList) {
Object dateObj = data.get("date_str");
if (dateObj != null && dateStr.equals(dateObj.toString())) {
return data;
}
}
return null;
}
@Override
public List<Integer> getTimeDistribution(String startDate, String endDate, Long areaId) {
// 处理日期参数
LocalDate start = startDate == null ? LocalDate.now().minusDays(6) : LocalDate.parse(startDate);
LocalDate end = endDate == null ? LocalDate.now() : LocalDate.parse(endDate);
// 查询时段分布数据
Map<String, Object> params = new HashMap<>();
params.put("startDate", start.toString());
params.put("endDate", end.toString());
params.put("areaId", areaId);
List<Map<String, Object>> timeDistData = baseMapper.selectTimeDistribution(params);
// 初始化24小时的数据,默认为0
List<Integer> result = IntStream.range(0, 24)
.mapToObj(i -> 0)
.collect(Collectors.toList());
// 填充查询结果
for (Map<String, Object> data : timeDistData) {
int hour = (int) data.get("hour");
int count = ((Number) data.get("count")).intValue();
if (hour >= 0 && hour < 24) {
result.set(hour, count);
}
}
return result;
}
@Override
public List<Map<String, Object>> getHotRoutes(String startDate, String endDate, Long areaId) {
// 处理日期参数
LocalDate start = startDate == null ? LocalDate.now().minusDays(6) : LocalDate.parse(startDate);
LocalDate end = endDate == null ? LocalDate.now() : LocalDate.parse(endDate);
// 查询热门路线数据
Map<String, Object> params = new HashMap<>();
params.put("startDate", start.toString());
params.put("endDate", end.toString());
params.put("areaId", areaId);
params.put("limit", 10); // 限制返回前10条
List<Map<String, Object>> hotRoutes = baseMapper.selectHotRoutes(params);
// 处理结果,添加排名
int rank = 1;
for (Map<String, Object> route : hotRoutes) {
route.put("rank", rank++);
// 计算趋势值(示例:使用骑行次数占比作为趋势值,范围0-100)
int count = ((Number) route.get("count")).intValue();
int totalCount = hotRoutes.stream()
.mapToInt(r -> ((Number) r.get("count")).intValue())
.sum();
double trend = totalCount > 0 ? (double) count * 100 / totalCount : 0;
route.put("trend", Math.min(100, (int) trend));
}
return hotRoutes;
}
@Override
public Map<String, Object> getIncomeData(Integer month) {
Map<String, Object> result = new HashMap<>();
List<String> days = new ArrayList<>();
List<BigDecimal> income = new ArrayList<>();
// 获取指定月份的第一天和最后一天
LocalDate now = LocalDate.now();
LocalDate firstDay = now.withMonth(month).withDayOfMonth(1);
LocalDate lastDay = firstDay.with(TemporalAdjusters.lastDayOfMonth());
// 如果是当前月份,则只统计到当前日期
if (month == now.getMonthValue()) {
lastDay = now;
}
// 查询收入数据
Map<String, Object> params = new HashMap<>();
params.put("startDate", firstDay.toString());
params.put("endDate", lastDay.toString());
params.put("month", month);
params.put("year", now.getYear());
List<Map<String, Object>> incomeData = baseMapper.selectIncomeData(params);
// 处理查询结果
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd");
for (int i = 1; i <= lastDay.getDayOfMonth(); i++) {
LocalDate date = firstDay.withDayOfMonth(i);
String dayStr = formatter.format(date);
days.add(dayStr);
// 查找当天收入
BigDecimal dayIncome = BigDecimal.ZERO;
for (Map<String, Object> data : incomeData) {
if (dayStr.equals(data.get("day").toString())) {
dayIncome = (BigDecimal) data.get("income");
break;
}
}
income.add(dayIncome);
}
result.put("days", days);
result.put("income", income);
return result;
}
/**
* 生成订单编号
*/
private String generateOrderNo() {
return "O" + System.currentTimeMillis();
}
/**
* 根据日期查找数据
*/
private Map<String, Object> findDataByDate(List<Map<String, Object>> dataList, String dateStr) {
for (Map<String, Object> data : dataList) {
Object dateObj = data.get("date");
if (dateObj != null && dateStr.equals(dateObj.toString())) {
return data;
}
}
return null;
}
}