|
|
|
@ -0,0 +1,262 @@
|
|
|
|
|
package com.example.venue_reservation_service.utils;
|
|
|
|
|
|
|
|
|
|
import com.example.venue_reservation_service.vo.excel.ReservationExcel;
|
|
|
|
|
|
|
|
|
|
import java.util.*;
|
|
|
|
|
import java.time.LocalDate;
|
|
|
|
|
import java.time.LocalTime;
|
|
|
|
|
import java.time.format.DateTimeFormatter;
|
|
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
|
|
public class VenueRecommender {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 用户-物品评分矩阵
|
|
|
|
|
private Map<Integer, Map<Integer, Double>> userVenueScores;
|
|
|
|
|
// 物品-物品相似度矩阵
|
|
|
|
|
private Map<Integer, Map<Integer, Double>> venueSimilarities;
|
|
|
|
|
// 所有预约记录
|
|
|
|
|
private List<ReservationExcel> allReservations;
|
|
|
|
|
// 目标用户ID
|
|
|
|
|
private int targetUserId;
|
|
|
|
|
|
|
|
|
|
public VenueRecommender(List<ReservationExcel> reservations, int targetUserId) {
|
|
|
|
|
this.allReservations = reservations;
|
|
|
|
|
this.targetUserId = targetUserId;
|
|
|
|
|
this.userVenueScores = new HashMap<>();
|
|
|
|
|
this.venueSimilarities = new HashMap<>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 构建用户-物品评分矩阵
|
|
|
|
|
private void buildUserVenueMatrix() {
|
|
|
|
|
// 按用户分组
|
|
|
|
|
Map<Integer, List<ReservationExcel>> reservationsByUser = allReservations.stream()
|
|
|
|
|
.collect(Collectors.groupingBy(ReservationExcel::getUserId));
|
|
|
|
|
|
|
|
|
|
for (Map.Entry<Integer, List<ReservationExcel>> entry : reservationsByUser.entrySet()) {
|
|
|
|
|
int userId = entry.getKey();
|
|
|
|
|
List<ReservationExcel> userReservations = entry.getValue();
|
|
|
|
|
|
|
|
|
|
// 按场地分组,计算用户对每个场地的偏好得分
|
|
|
|
|
Map<Integer, Long> venueCounts = userReservations.stream()
|
|
|
|
|
.collect(Collectors.groupingBy(ReservationExcel::getVenueId, Collectors.counting()));
|
|
|
|
|
|
|
|
|
|
// 找到最大计数用于归一化
|
|
|
|
|
long maxCount = venueCounts.values().stream().max(Long::compare).orElse(1L);
|
|
|
|
|
|
|
|
|
|
Map<Integer, Double> venueScores = new HashMap<>();
|
|
|
|
|
for (Map.Entry<Integer, Long> venueEntry : venueCounts.entrySet()) {
|
|
|
|
|
// 归一化到0-1之间
|
|
|
|
|
double score = (double) venueEntry.getValue() / maxCount;
|
|
|
|
|
venueScores.put(venueEntry.getKey(), score);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
userVenueScores.put(userId, venueScores);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 计算场地之间的余弦相似度
|
|
|
|
|
private void calculateVenueSimilarities() {
|
|
|
|
|
// 获取所有场地ID
|
|
|
|
|
Set<Integer> allVenues = allReservations.stream()
|
|
|
|
|
.map(ReservationExcel::getVenueId)
|
|
|
|
|
.collect(Collectors.toSet());
|
|
|
|
|
|
|
|
|
|
// 为每个场地构建特征向量(基于用户评分)
|
|
|
|
|
Map<Integer, Map<Integer, Double>> venueUserVectors = new HashMap<>();
|
|
|
|
|
|
|
|
|
|
// 初始化
|
|
|
|
|
for (int venueId : allVenues) {
|
|
|
|
|
venueUserVectors.put(venueId, new HashMap<>());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 填充向量
|
|
|
|
|
for (Map.Entry<Integer, Map<Integer, Double>> userEntry : userVenueScores.entrySet()) {
|
|
|
|
|
int userId = userEntry.getKey();
|
|
|
|
|
Map<Integer, Double> venueScores = userEntry.getValue();
|
|
|
|
|
|
|
|
|
|
for (Map.Entry<Integer, Double> venueEntry : venueScores.entrySet()) {
|
|
|
|
|
int venueId = venueEntry.getKey();
|
|
|
|
|
double score = venueEntry.getValue();
|
|
|
|
|
venueUserVectors.get(venueId).put(userId, score);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 计算场地之间的相似度
|
|
|
|
|
for (int venue1 : allVenues) {
|
|
|
|
|
Map<Integer, Double> vector1 = venueUserVectors.get(venue1);
|
|
|
|
|
Map<Integer, Double> similarities = new HashMap<>();
|
|
|
|
|
|
|
|
|
|
for (int venue2 : allVenues) {
|
|
|
|
|
if (venue1 == venue2) {
|
|
|
|
|
similarities.put(venue2, 1.0); // 自相似度为1
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Map<Integer, Double> vector2 = venueUserVectors.get(venue2);
|
|
|
|
|
|
|
|
|
|
// 计算余弦相似度
|
|
|
|
|
double dotProduct = 0.0;
|
|
|
|
|
double norm1 = 0.0;
|
|
|
|
|
double norm2 = 0.0;
|
|
|
|
|
|
|
|
|
|
// 收集所有共同的用户
|
|
|
|
|
Set<Integer> commonUsers = new HashSet<>(vector1.keySet());
|
|
|
|
|
commonUsers.retainAll(vector2.keySet());
|
|
|
|
|
|
|
|
|
|
// 如果没有共同用户,相似度为0
|
|
|
|
|
if (commonUsers.isEmpty()) {
|
|
|
|
|
similarities.put(venue2, 0.0);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int userId : commonUsers) {
|
|
|
|
|
double score1 = vector1.get(userId);
|
|
|
|
|
double score2 = vector2.get(userId);
|
|
|
|
|
dotProduct += score1 * score2;
|
|
|
|
|
norm1 += score1 * score1;
|
|
|
|
|
norm2 += score2 * score2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
norm1 = Math.sqrt(norm1);
|
|
|
|
|
norm2 = Math.sqrt(norm2);
|
|
|
|
|
|
|
|
|
|
double similarity = (norm1 * norm2 == 0) ? 0 : dotProduct / (norm1 * norm2);
|
|
|
|
|
similarities.put(venue2, similarity);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
venueSimilarities.put(venue1, similarities);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 为目标用户生成推荐
|
|
|
|
|
public ReservationExcel generateRecommendation() {
|
|
|
|
|
// 获取目标用户的预约记录
|
|
|
|
|
List<ReservationExcel> userReservations = allReservations.stream()
|
|
|
|
|
.filter(r -> r.getUserId() == targetUserId)
|
|
|
|
|
.collect(Collectors.toList());
|
|
|
|
|
|
|
|
|
|
// 如果用户预约记录少于50条,使用热门推荐
|
|
|
|
|
if (userReservations.size() < 50) {
|
|
|
|
|
return recommendPopular();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 否则使用基于物品的协同过滤
|
|
|
|
|
buildUserVenueMatrix();
|
|
|
|
|
calculateVenueSimilarities();
|
|
|
|
|
|
|
|
|
|
// 获取用户预约过的场地
|
|
|
|
|
Set<Integer> userVenues = userReservations.stream()
|
|
|
|
|
.map(ReservationExcel::getVenueId)
|
|
|
|
|
.collect(Collectors.toSet());
|
|
|
|
|
|
|
|
|
|
// 计算用户未预约过的场地的得分
|
|
|
|
|
Map<Integer, Double> venueScores = new HashMap<>();
|
|
|
|
|
|
|
|
|
|
for (int venueId : venueSimilarities.keySet()) {
|
|
|
|
|
if (userVenues.contains(venueId)) {
|
|
|
|
|
continue; // 跳过用户已经预约过的场地
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double score = 0.0;
|
|
|
|
|
for (int userVenue : userVenues) {
|
|
|
|
|
double similarity = venueSimilarities.get(userVenue).get(venueId);
|
|
|
|
|
double userPreference = userVenueScores.getOrDefault(targetUserId, new HashMap<>())
|
|
|
|
|
.getOrDefault(userVenue, 0.0);
|
|
|
|
|
score += similarity * userPreference;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
venueScores.put(venueId, score);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果没有合适的推荐,回退到热门推荐
|
|
|
|
|
if (venueScores.isEmpty()) {
|
|
|
|
|
return recommendPopular();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 找到得分最高的场地
|
|
|
|
|
int recommendedVenueId = Collections.max(venueScores.entrySet(), Map.Entry.comparingByValue()).getKey();
|
|
|
|
|
|
|
|
|
|
// 为该场地推荐一个合适的时间段
|
|
|
|
|
return recommendTimeSlot(recommendedVenueId, userReservations);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 热门推荐(基于所有用户的预约记录)
|
|
|
|
|
private ReservationExcel recommendPopular() {
|
|
|
|
|
// 找出最热门的场地
|
|
|
|
|
Map<Integer, Long> venuePopularity = allReservations.stream()
|
|
|
|
|
.collect(Collectors.groupingBy(ReservationExcel::getVenueId, Collectors.counting()));
|
|
|
|
|
|
|
|
|
|
int popularVenueId = Collections.max(venuePopularity.entrySet(), Map.Entry.comparingByValue()).getKey();
|
|
|
|
|
|
|
|
|
|
// 找出该场地最热门的时间段
|
|
|
|
|
Map<String, Long> timeSlotPopularity = allReservations.stream()
|
|
|
|
|
.filter(r -> r.getVenueId() == popularVenueId)
|
|
|
|
|
.collect(Collectors.groupingBy(ReservationExcel::getTimeSlot, Collectors.counting()));
|
|
|
|
|
|
|
|
|
|
String popularTimeSlot = Collections.max(timeSlotPopularity.entrySet(), Map.Entry.comparingByValue()).getKey();
|
|
|
|
|
|
|
|
|
|
// 找出该场地最热门的星期几
|
|
|
|
|
Map<String, Long> dayPopularity = allReservations.stream()
|
|
|
|
|
.filter(r -> r.getVenueId() == popularVenueId)
|
|
|
|
|
.collect(Collectors.groupingBy(ReservationExcel::getDayOfWeek, Collectors.counting()));
|
|
|
|
|
|
|
|
|
|
String popularDay = Collections.max(dayPopularity.entrySet(), Map.Entry.comparingByValue()).getKey();
|
|
|
|
|
|
|
|
|
|
// 创建推荐记录(使用下周的同一天)
|
|
|
|
|
LocalDate nextWeek = LocalDate.now().plusWeeks(1);
|
|
|
|
|
LocalDate recommendedDate = nextWeek.with(java.time.DayOfWeek.valueOf(popularDay.toUpperCase()));
|
|
|
|
|
|
|
|
|
|
// 设置时间段
|
|
|
|
|
LocalTime startTime, endTime;
|
|
|
|
|
if (popularTimeSlot.equals("morning")) {
|
|
|
|
|
startTime = LocalTime.of(9, 0);
|
|
|
|
|
endTime = LocalTime.of(11, 0);
|
|
|
|
|
} else if (popularTimeSlot.equals("afternoon")) {
|
|
|
|
|
startTime = LocalTime.of(14, 0);
|
|
|
|
|
endTime = LocalTime.of(16, 0);
|
|
|
|
|
} else {
|
|
|
|
|
startTime = LocalTime.of(19, 0);
|
|
|
|
|
endTime = LocalTime.of(21, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new ReservationExcel(0, targetUserId, popularVenueId, startTime, endTime, recommendedDate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 为指定场地推荐时间段(基于用户历史偏好)
|
|
|
|
|
private ReservationExcel recommendTimeSlot(int venueId, List<ReservationExcel> userReservations) {
|
|
|
|
|
// 分析用户偏好的时间段
|
|
|
|
|
Map<String, Long> userTimeSlotPref = userReservations.stream()
|
|
|
|
|
.collect(Collectors.groupingBy(ReservationExcel::getTimeSlot, Collectors.counting()));
|
|
|
|
|
|
|
|
|
|
String preferredTimeSlot = Collections.max(userTimeSlotPref.entrySet(), Map.Entry.comparingByValue()).getKey();
|
|
|
|
|
|
|
|
|
|
// 分析用户偏好的星期几
|
|
|
|
|
Map<String, Long> userDayPref = userReservations.stream()
|
|
|
|
|
.collect(Collectors.groupingBy(ReservationExcel::getDayOfWeek, Collectors.counting()));
|
|
|
|
|
|
|
|
|
|
String preferredDay = Collections.max(userDayPref.entrySet(), Map.Entry.comparingByValue()).getKey();
|
|
|
|
|
|
|
|
|
|
// 创建推荐记录(使用下周的同一天)
|
|
|
|
|
LocalDate nextWeek = LocalDate.now().plusWeeks(1);
|
|
|
|
|
LocalDate recommendedDate = nextWeek.with(java.time.DayOfWeek.valueOf(preferredDay.toUpperCase()));
|
|
|
|
|
|
|
|
|
|
// 设置时间段
|
|
|
|
|
LocalTime startTime, endTime;
|
|
|
|
|
if (preferredTimeSlot.equals("morning")) {
|
|
|
|
|
startTime = LocalTime.of(9, 0);
|
|
|
|
|
endTime = LocalTime.of(11, 0);
|
|
|
|
|
} else if (preferredTimeSlot.equals("afternoon")) {
|
|
|
|
|
startTime = LocalTime.of(14, 0);
|
|
|
|
|
endTime = LocalTime.of(16, 0);
|
|
|
|
|
} else {
|
|
|
|
|
startTime = LocalTime.of(19, 0);
|
|
|
|
|
endTime = LocalTime.of(21, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new ReservationExcel(0, targetUserId, venueId, startTime, endTime, recommendedDate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|