推荐算法 用户考核 头像上传
Change-Id: Iaac96768d5238142f5ed445f5cc64ccedd239d0f
diff --git a/src/main/java/com/pt5/pthouduan/config/WebMvcConfig.java b/src/main/java/com/pt5/pthouduan/config/WebMvcConfig.java
index 85145f4..9472aa0 100644
--- a/src/main/java/com/pt5/pthouduan/config/WebMvcConfig.java
+++ b/src/main/java/com/pt5/pthouduan/config/WebMvcConfig.java
@@ -9,13 +9,23 @@
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
- // 帖子图片:D:/postuploads/ → /images/**
- registry.addResourceHandler("/images/**")
- .addResourceLocations("file:D:/postuploads/");//路径改一下
+ // 帖子图片:D:/postuploads/ → /images/** 求助帖
+ registry.addResourceHandler("/activityimgs/**")
+ .addResourceLocations("file:./activityimgs/");//路径改一下
+ // 帖子图片:D:/postuploads/ → /images/** 求助帖
+ registry.addResourceHandler("/helppostimgs/**")
+ .addResourceLocations("file:./helppostimgs/");//路径改一下
// ✅ 求助帖图片:D:/uploads/ → /uploads/**
- registry.addResourceHandler("/uploads/**")
- .addResourceLocations("file:D:/uploads/");
+ // 映射到项目根目录下的postimgs目录,这是帖子的
+ registry.addResourceHandler("/postimgs/**")
+ .addResourceLocations("file:./postimgs/");
+
+ registry.addResourceHandler("/coverimgs/**")
+ .addResourceLocations("file:./coverimgs/");
+
+ registry.addResourceHandler("/userimgs/**")
+ .addResourceLocations("file:./userimgs/");
}
diff --git a/src/main/java/com/pt5/pthouduan/controller/ExamController.java b/src/main/java/com/pt5/pthouduan/controller/ExamController.java
new file mode 100644
index 0000000..a5941be
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/controller/ExamController.java
@@ -0,0 +1,27 @@
+package com.pt5.pthouduan.controller;
+
+import com.pt5.pthouduan.service.ExamService;
+import com.pt5.pthouduan.service.ShopService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.LocalDate;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/exam")
+public class ExamController {
+ @Autowired
+ private ExamService examService;
+ @PostMapping("/MonthDownload")
+ public Map<String, Object> MonthDownload(@RequestParam LocalDate startDate, @RequestParam LocalDate endDate) {
+ return examService.MonthDownload(startDate, endDate);
+ }
+ @PostMapping("/QuarterUpload")
+ public Map<String, Object> QuarterUpload(@RequestParam LocalDate startDate, @RequestParam LocalDate endDate) {
+ return examService.QuarterUpload(startDate, endDate);
+ }
+}
diff --git a/src/main/java/com/pt5/pthouduan/controller/RecommendController.java b/src/main/java/com/pt5/pthouduan/controller/RecommendController.java
new file mode 100644
index 0000000..da981cb
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/controller/RecommendController.java
@@ -0,0 +1,26 @@
+package com.pt5.pthouduan.controller;
+
+import com.pt5.pthouduan.entity.Torrent;
+import com.pt5.pthouduan.entity.User;
+import com.pt5.pthouduan.service.InviteService;
+import com.pt5.pthouduan.service.RecommendService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/recommend")
+public class RecommendController {
+ @Autowired
+ private RecommendService recommendService;
+// @GetMapping("/test")
+// public List<Map<String, Object>> test() {
+// return recommendService.test();
+// }
+ @GetMapping("/list")
+ public List<Torrent> getRecommendList(@RequestParam Long userId) {
+ return recommendService.recommendForUser(userId);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pt5/pthouduan/controller/UserController.java b/src/main/java/com/pt5/pthouduan/controller/UserController.java
index 4a23ea9..11f77ce 100644
--- a/src/main/java/com/pt5/pthouduan/controller/UserController.java
+++ b/src/main/java/com/pt5/pthouduan/controller/UserController.java
@@ -31,7 +31,11 @@
public class UserController {
@Autowired
private UserService userService;
- private String uploadDir="../var/www/avatars/"; // 配置文件上传目录,例如: /var/www/avatars/
+
+ @Value("${torrent.user-image-dir}")
+ private String uploadDir;
+
+ //private String uploadDir="../var/www/avatars/"; // 配置文件上传目录,例如: /var/www/avatars/
private String accessPath="../avatars/";
@@ -113,68 +117,68 @@
return userService.getuserid(username);
}
- @PostMapping("/uploadimage")
- public ResponseEntity<Map<String, Object>> uploadAvatar(@RequestParam("avatar") MultipartFile file) {
- Map<String, Object> response = new HashMap<>();
+// @PostMapping("/uploadimage")
+// public ResponseEntity<Map<String, Object>> uploadAvatar(@RequestParam("avatar") MultipartFile file) {
+// Map<String, Object> response = new HashMap<>();
+//
+// // 1. 验证文件是否为空
+// if (file.isEmpty()) {
+// response.put("success", false);
+// response.put("message", "请选择要上传的文件");
+// return ResponseEntity.badRequest().body(response);
+// }
+//
+// // 2. 验证文件类型
+// String contentType = file.getContentType();
+// if (!"image/jpeg".equals(contentType) &&
+// !"image/png".equals(contentType) &&
+// !"image/gif".equals(contentType)) {
+// response.put("success", false);
+// response.put("message", "只支持JPG/PNG/GIF格式的图片");
+// return ResponseEntity.badRequest().body(response);
+// }
+//
+// // 3. 验证文件大小 (前端已验证,后端再次验证)
+// if (file.getSize() > 10 * 1024 * 1024) { // 10MB
+// response.put("success", false);
+// response.put("message", "图片大小不能超过10MB");
+// return ResponseEntity.badRequest().body(response);
+// }
- // 1. 验证文件是否为空
- if (file.isEmpty()) {
- response.put("success", false);
- response.put("message", "请选择要上传的文件");
- return ResponseEntity.badRequest().body(response);
- }
-
- // 2. 验证文件类型
- String contentType = file.getContentType();
- if (!"image/jpeg".equals(contentType) &&
- !"image/png".equals(contentType) &&
- !"image/gif".equals(contentType)) {
- response.put("success", false);
- response.put("message", "只支持JPG/PNG/GIF格式的图片");
- return ResponseEntity.badRequest().body(response);
- }
-
- // 3. 验证文件大小 (前端已验证,后端再次验证)
- if (file.getSize() > 10 * 1024 * 1024) { // 10MB
- response.put("success", false);
- response.put("message", "图片大小不能超过10MB");
- return ResponseEntity.badRequest().body(response);
- }
-
- try {
- // 4. 创建上传目录(如果不存在)
- File dir = new File(uploadDir);
- if (!dir.exists()) {
- dir.mkdirs();
- }
- System.out.println(dir.getAbsolutePath());
- // 5. 生成唯一文件名 (日期+UUID+后缀)
- String originalFilename = file.getOriginalFilename();
- String fileExt = originalFilename.substring(originalFilename.lastIndexOf("."));
- String newFilename = new SimpleDateFormat("yyyyMMdd").format(new Date()) +
- "_" + UUID.randomUUID().toString().replace("-", "") +
- fileExt.toLowerCase();
-
- // 6. 保存文件
- Path path = Paths.get(uploadDir, newFilename);
- Files.copy(file.getInputStream(), path);
-
- // 7. 返回访问URL
- String fileUrl = accessPath + newFilename;
-
- response.put("success", true);
- response.put("url", fileUrl);
- response.put("message", "头像上传成功");
-
- return ResponseEntity.ok(response);
-
- } catch (IOException e) {
- e.printStackTrace();
- response.put("success", false);
- response.put("message", "文件上传失败: " + e.getMessage());
- return ResponseEntity.status(500).body(response);
- }
- }
+// try {
+// // 4. 创建上传目录(如果不存在)
+// File dir = new File(uploadDir);
+// if (!dir.exists()) {
+// boolean created = dir.mkdirs(); // 递归创建目录
+// if (!created) {
+// throw new IOException("无法创建目录:" + uploadDir);
+// }
+// }
+// System.out.println(dir.getAbsolutePath());
+// String filename = System.currentTimeMillis()+"_"+file.getOriginalFilename();
+// Path userimagePath = Paths.get(uploadDir, filename);
+//
+// // 6. 保存文件
+// Files.createDirectories(userimagePath.getParent()); // 创建目录
+// Files.copy(file.getInputStream(), userimagePath, StandardCopyOption.REPLACE_EXISTING);
+//
+// // 7. 返回访问URL
+// //String fileUrl = accessPath + newFilename;
+// String fileUrl = "http://localhost:8080/" + uploadDir + filename;
+//
+// response.put("success", true);
+// response.put("url", fileUrl);
+// response.put("message", "头像上传成功");
+//
+// return ResponseEntity.ok(response);
+//
+// } catch (IOException e) {
+// e.printStackTrace();
+// response.put("success", false);
+// response.put("message", "文件上传失败: " + e.getMessage());
+// return ResponseEntity.status(500).body(response);
+// }
+ //}
}
diff --git a/src/main/java/com/pt5/pthouduan/entity/BehaviorType.java b/src/main/java/com/pt5/pthouduan/entity/BehaviorType.java
new file mode 100644
index 0000000..6e233d0
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/entity/BehaviorType.java
@@ -0,0 +1,27 @@
+package com.pt5.pthouduan.entity;
+
+public enum BehaviorType {
+ VIEW(1, "浏览"),
+ DOWNLOAD(3, "下载"),
+ FAVORITE(5, "收藏"),
+ SEED(4, "做种"),
+ COMMENT(2, "评论"),
+ RATE(3, "评分"),
+ SHARE(2, "分享");
+
+ private final int weight;
+ private final String description;
+
+ BehaviorType(int weight, String description) {
+ this.weight = weight;
+ this.description = description;
+ }
+
+ public int getWeight() {
+ return weight;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pt5/pthouduan/entity/UserBehavior.java b/src/main/java/com/pt5/pthouduan/entity/UserBehavior.java
new file mode 100644
index 0000000..192d3cd
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/entity/UserBehavior.java
@@ -0,0 +1,94 @@
+package com.pt5.pthouduan.entity;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 用户行为记录表
+ * </p>
+ *
+ * @author ljx
+ * @since 2025-06-04
+ */
+@TableName("user_behavior")
+public class UserBehavior implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ @TableId("behavior_id")
+ private Long behaviorId; //行为id
+
+ private Long userId; // 用户ID
+
+ private Long torrentId; // 种子资源ID
+
+ private String behaviorType; // 行为类型:VIEW/DOWNLOAD/FAVORITE等
+
+ private Integer weight; // 行为权重
+
+ private LocalDateTime timestamp; // 行为时间
+
+ public Long getBehaviorId() {
+ return behaviorId;
+ }
+
+ public void setBehaviorId(Long behaviorId) {
+ this.behaviorId = behaviorId;
+ }
+
+ public Long getUserId() {
+ return userId;
+ }
+
+ public void setUserId(Long userId) {
+ this.userId = userId;
+ }
+
+ public Long getTorrentId() {
+ return torrentId;
+ }
+
+ public void setTorrentId(Long torrentId) {
+ this.torrentId = torrentId;
+ }
+
+ public String getBehaviorType() {
+ return behaviorType;
+ }
+
+ public void setBehaviorType(String behaviorType) {
+ this.behaviorType = behaviorType;
+ }
+
+ public Integer getWeight() {
+ return weight;
+ }
+
+ public void setWeight(Integer weight) {
+ this.weight = weight;
+ }
+
+ public LocalDateTime getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(LocalDateTime timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ @Override
+ public String toString() {
+ return "UserBehavior{" +
+ "behaviorId=" + behaviorId +
+ ", userId=" + userId +
+ ", torrentId=" + torrentId +
+ ", behaviorType='" + behaviorType + '\'' +
+ ", weight=" + weight +
+ ", timestamp=" + timestamp +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pt5/pthouduan/mapper/RecommendMapper.java b/src/main/java/com/pt5/pthouduan/mapper/RecommendMapper.java
new file mode 100644
index 0000000..54f9e1d
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/mapper/RecommendMapper.java
@@ -0,0 +1,71 @@
+package com.pt5.pthouduan.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.pt5.pthouduan.entity.UserBehavior;
+import org.apache.ibatis.annotations.*;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Map;
+
+
+@Repository
+public interface RecommendMapper extends BaseMapper<UserBehavior> {
+
+
+ @Insert("INSERT INTO user_behavior(user_id, torrent_id, behavior_type, weight, timestamp) " +
+ "VALUES(#{userId}, #{torrentId}, #{behaviorType}, #{weight}, #{timestamp})")
+ int insertUserBehavior(UserBehavior userBehavior);
+
+ @Select("SELECT COALESCE(SUM(weight), 0) FROM user_behavior WHERE user_id = #{userId}")
+ double sumWeightsByUserId(@Param("userId") Long userId);
+ /**
+ * 查找用户最喜欢的分类
+ */
+ @Select("SELECT torrent.categoryid as categoryId, SUM(ub.weight) as totalWeight " +
+ "FROM user_behavior ub " +
+ "JOIN torrent ON ub.torrent_id = torrent.torrentid " +
+ "WHERE ub.user_id = #{userId} " +
+ "GROUP BY torrent.categoryid " +
+ "ORDER BY totalWeight DESC ")
+ List<Map<String, Object>> findFavoriteCategories(@Param("userId") Long userId);
+
+ /**
+ * 统计用户行为总数
+ */
+ @Select("SELECT COUNT(*) FROM user_behavior WHERE user_id = #{userId}")
+ long countByUserId(@Param("userId") Long userId);
+
+ /**
+ * 获取用户最近的行为记录
+ */
+ @Select("SELECT * FROM user_behavior WHERE user_id = #{userId} ORDER BY timestamp DESC LIMIT #{limit}")
+ List<UserBehavior> findRecentBehaviors(@Param("userId") Long userId, @Param("limit") int limit);
+
+ /**
+ * 获取用户对特定资源的行为记录
+ */
+ @Select("SELECT * FROM user_behavior WHERE user_id = #{userId} AND torrent_id = #{torrentId}")
+ List<UserBehavior> findByUserAndTorrent(@Param("userId") Long userId, @Param("torrentId") Long torrentId);
+
+ /**
+ * 获取用户的高权重行为
+ */
+ @Select("SELECT * FROM user_behavior WHERE user_id = #{userId} AND weight >= #{minWeight}")
+ List<UserBehavior> findHighWeightBehaviors(@Param("userId") Long userId, @Param("minWeight") Integer minWeight);
+
+ /**
+ * 获取用户最近交互的资源ID
+ */
+ @Select("SELECT DISTINCT torrent_id FROM user_behavior " +
+ "WHERE user_id = #{userId} AND timestamp >= DATE_SUB(NOW(), INTERVAL #{days} DAY)")
+ List<Long> findRecentTorrentIds(@Param("userId") Long userId, @Param("days") int days);
+
+
+
+ /**
+ * 删除旧的行为记录
+ */
+ @Delete("DELETE FROM user_behavior WHERE timestamp < DATE_SUB(NOW(), INTERVAL #{days} DAY)")
+ int deleteOldBehaviorsForAllUsers(@Param("days") int days);
+}
\ No newline at end of file
diff --git a/src/main/java/com/pt5/pthouduan/service/ExamService.java b/src/main/java/com/pt5/pthouduan/service/ExamService.java
new file mode 100644
index 0000000..93dbbcd
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/service/ExamService.java
@@ -0,0 +1,103 @@
+package com.pt5.pthouduan.service;
+
+import com.pt5.pthouduan.entity.Invites;
+import com.pt5.pthouduan.entity.User;
+import com.pt5.pthouduan.entity.UserTrafficStat;
+import com.pt5.pthouduan.mapper.InvitesMapper;
+import com.pt5.pthouduan.mapper.UserMapper;
+import com.pt5.pthouduan.mapper.UserTrafficMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class ExamService {
+ @Autowired
+ private UserTrafficMapper userTrafficMapper;
+ @Autowired
+ private UserMapper userMapper;
+ //所有用户月度下载量考核
+ public Map<String, Object> MonthDownload(LocalDate startDate,LocalDate endDate){
+ Map<String, Object> result = new HashMap<>();
+ List<Map<String, Object>> users = userMapper.selectAllUsersBasicInfo();
+ for (Map<String, Object> user : users) {
+ // 获取 gradeId,这里转为数字
+ Object gradeIdObj = user.get("grade_id");
+ int gradeId = (gradeIdObj instanceof Number) ? ((Number) gradeIdObj).intValue() : 0;
+ UserTrafficStat userTrafficStat=userTrafficMapper.getUserTrafficStats((String) user.get("passkey"),startDate,endDate);
+ System.out.println(gradeId+" "+userTrafficStat.getTotalUploaded()+" "+userTrafficStat.getTotalDownloaded());
+ // 根据 gradeId 的值审核下载量
+ if (gradeId == 1) {
+ if(userTrafficStat.getTotalDownloaded()<1073741824){
+ if(userTrafficStat.getTotalUploaded()< 1073741824L *50)
+ failure((String) user.get("username"),gradeId);
+ }
+ } else if (gradeId == 2) {
+ if(userTrafficStat.getTotalDownloaded()< 1073741824L *3){
+ if(userTrafficStat.getTotalUploaded()< 1073741824L *50)
+ failure((String) user.get("username"),gradeId);
+ }
+ } else if (gradeId == 3) {
+ if(userTrafficStat.getTotalDownloaded()< 1073741824L *5){
+ if(userTrafficStat.getTotalUploaded()< 1073741824L *50)
+ failure((String) user.get("username"),gradeId);
+ }
+ }
+ }
+ result.put("success", true);
+ result.put("message", "用户月度考核完毕");
+ return result;
+ }
+ //考核失败
+ void failure(String username,int gradeId){
+ System.out.println("failure"+username+gradeId);
+ if(gradeId == 1){
+ userMapper.deleteByUsername(username);
+ }else{
+ userMapper.updateGrade(username,gradeId-1);
+ }
+ }
+ //所有用户季度上传量考核
+ public Map<String, Object> QuarterUpload(LocalDate startDate,LocalDate endDate){
+ Map<String, Object> result = new HashMap<>();
+ List<Map<String, Object>> users = userMapper.selectAllUsersBasicInfo();
+ for (Map<String, Object> user : users) {
+ // 获取 gradeId,这里转为数字
+ Object gradeIdObj = user.get("grade_id");
+ int gradeId = (gradeIdObj instanceof Number) ? ((Number) gradeIdObj).intValue() : 0;
+ UserTrafficStat userTrafficStat=userTrafficMapper.getUserTrafficStats((String) user.get("passkey"),startDate,endDate);
+ // 根据 gradeId 的值审核上传
+ if (gradeId == 1) {
+ if(userTrafficStat.getTotalUploaded()< 1073741824L *50){
+ failure((String) user.get("username"),gradeId);
+ }
+ } else if (gradeId == 2) {
+ if(userTrafficStat.getTotalUploaded()< 1073741824L*60){
+ failure((String) user.get("username"),gradeId);
+ }
+ } else if (gradeId == 3) {
+ System.out.println("here");
+ if(userTrafficStat.getTotalUploaded()< 1073741824L*70){
+ System.out.println("failure");
+ failure((String) user.get("username"),gradeId);
+ }
+ }else if (gradeId == 4) {
+ if(userTrafficStat.getTotalUploaded()< 1073741824L*80){
+ failure((String) user.get("username"),gradeId);
+ }
+ }else if (gradeId == 5) {
+ if(userTrafficStat.getTotalUploaded()< 1073741824L*100){
+ failure((String) user.get("username"),gradeId);
+ }
+ }
+ }
+ result.put("success", true);
+ result.put("message", "用户季度考核完毕");
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pt5/pthouduan/service/RecommendService.java b/src/main/java/com/pt5/pthouduan/service/RecommendService.java
new file mode 100644
index 0000000..6f6f9a4
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/service/RecommendService.java
@@ -0,0 +1,170 @@
+package com.pt5.pthouduan.service;
+
+import com.pt5.pthouduan.entity.Torrent;
+import com.pt5.pthouduan.entity.UserBehavior;
+import com.pt5.pthouduan.mapper.RecommendMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.*;
+import java.util.stream.Collectors;
+
+
+@Service
+public class RecommendService {
+ @Autowired
+ private TorrentService torrentService;
+
+ @Autowired
+ private RecommendMapper recommendMapper;
+ //收集用户行为数据
+ public void recordUserBehavior(Long userId, Long torrentId, String behaviorType) {
+ UserBehavior behavior = new UserBehavior();
+ behavior.setUserId(userId);
+ behavior.setTorrentId(torrentId);
+ behavior.setBehaviorType(behaviorType);
+ behavior.setWeight(getBehaviorWeight(behaviorType));
+ behavior.setTimestamp(LocalDateTime.now());
+
+ recommendMapper.insertUserBehavior(behavior);
+ }
+ //对用户行为数据进行定期清洗 维护内存空间
+ private void cleanOldBehaviors() {
+ recommendMapper.deleteOldBehaviorsForAllUsers(30);
+ }
+
+ //设定不同用户行为的偏好权重(注意这里权重如果修改 那么BehaviorType同样需要修改 它在entity中)
+ private int getBehaviorWeight(String behaviorType) {
+ return switch (behaviorType) {
+ case "DOWNLOAD" -> 3;
+ case "FAVORITE" -> 5;
+ case "SEED" -> 4;
+ case "VIEW" -> 1;
+ case "COMMENT" -> 2;
+ default -> 1;
+ };
+ }
+
+
+
+ public List<Torrent> recommendForUser(Long userId) {//limit为资源的最大数量限制
+ // 检查用户是否有足够的行为数据
+ double totalWeight=recommendMapper.sumWeightsByUserId(userId);
+
+ if (totalWeight>50) {//行为数据足够
+ // 70% 偏好资源,15% 热门资源,15% 最新资源
+ return getPersonalizedRecommendation(userId);
+ } else {//行为数据不足
+ // 50% 热门资源,50% 最新资源
+ return getDefaultRecommendation();
+ }
+ }
+
+ //偏好资源推荐
+ private List<Torrent> getPersonalizedRecommendation(Long userId) {
+
+ // 获取偏好资源
+
+ List<Map<String, Object>> categories = recommendMapper.findFavoriteCategories(userId);//获取喜爱的类型及其权重
+ categories = categories.stream()
+ .sorted((a, b) -> Integer.compare(
+ ((Number)b.get("totalWeight")).intValue(),
+ ((Number)a.get("totalWeight")).intValue()
+ ))
+ .limit(5) // 只取前5种偏好类型
+ .toList();
+ List<List<Torrent>> allTorrents = categories.stream()
+ .map(category -> torrentService.getTorrentsByCategory((Integer) category.get("categoryId")))
+ .toList();
+ // 3. 均匀混合算法
+ List<Torrent> combinedList = new ArrayList<>();
+ if (!allTorrents.isEmpty()) {
+ int maxSize = allTorrents.stream()
+ .mapToInt(List::size)
+ .max()
+ .orElse(0);
+
+ // 按权重计算每种类型每次应该添加的元素数量
+ int[] weights = categories.stream()
+ .mapToInt(c -> ((Number)c.get("totalWeight")).intValue()) // 安全转换
+ .toArray();
+ int totalWeight = Arrays.stream(weights).sum();
+
+ // 计算每种类型在每轮中应该添加的比例
+ double[] ratios = Arrays.stream(weights)
+ .mapToDouble(w -> (double)w / totalWeight)
+ .toArray();
+
+ // 均匀混合实现
+ for (int i = 0; i < maxSize; i++) {
+ for (int j = 0; j < allTorrents.size(); j++) {
+ List<Torrent> current = allTorrents.get(j);
+ if (i < current.size()) {
+ // 根据权重比例决定是否添加当前元素
+ if (Math.random() < ratios[j]) {
+ combinedList.add(current.get(i));
+ }
+ }
+ }
+ }
+ }
+
+ // 常规推荐
+ List<Torrent> regular=getDefaultRecommendation();
+ //合并结果 把个性化推荐和普通推荐相结合7:3
+ List<Torrent> finalList = new ArrayList<>();
+ int combinedSize = combinedList.size();
+ int regularSize = regular.size();
+ int actualCombined = combinedSize;
+ int actualRegular =regularSize;
+ Iterator<Torrent> combinedIter = combinedList.iterator();
+ Iterator<Torrent> regularIter = regular.iterator();
+ while (actualCombined > 0 || actualRegular > 0) {
+ double ratio = 0.9;
+
+ if (Math.random() < ratio && actualCombined > 0) {
+ if (combinedIter.hasNext()) {
+ finalList.add(combinedIter.next());
+ actualCombined--;
+ }
+ } else if (actualRegular > 0) {
+ if (regularIter.hasNext()) {
+ finalList.add(regularIter.next());
+ actualRegular--;
+ }
+ }
+ }
+ while (regularIter.hasNext()) {
+ finalList.add(regularIter.next());
+ }
+ Set<Integer> seenIds = new HashSet<>();
+ List<Torrent> distinctList = finalList.stream()
+ .filter(torrent -> seenIds.add(torrent.getTorrentid().intValue())) // 首次出现时返回true
+ .collect(Collectors.toList());
+ return distinctList;
+ }
+ //无偏好推荐
+ private List<Torrent> getDefaultRecommendation() {
+ List<Torrent> torrents=torrentService.getAllTorrents();
+ torrents= torrents.stream()
+ .sorted((t1, t2) -> {
+ // 时间衰减因子(新种子加分)
+ long daysOld1 = ChronoUnit.DAYS.between(t1.getUploadTime(), LocalDateTime.now());
+ long daysOld2 = ChronoUnit.DAYS.between(t2.getUploadTime(), LocalDateTime.now());
+
+ // 综合分数 = downloadCount + (1 / (daysOld + 1)) * 系数
+ double score1 = t1.getDownloadCount() + 10.0 / (daysOld1 + 1);
+ double score2 = t2.getDownloadCount() + 10.0 / (daysOld2 + 1);
+
+ return Double.compare(score2, score1);
+ })
+ .toList();
+ return torrents;
+ }
+
+// public List<Map<String, Object>> test(){
+// return recommendMapper.findFavoriteCategories(1L);
+// }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index f1c382a..cdb1466 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,7 +1,13 @@
spring.application.name=PT-houduan
-spring.datasource.url=jdbc:mysql://localhost:3306/pt?useSSL=false&serverTimezone=Asia/Shanghai
-spring.datasource.username=root
-spring.datasource.password=12345
+#spring.datasource.url=jdbc:mysql://localhost:3306/pt?useSSL=false&serverTimezone=Asia/Shanghai
+##spring.datasource.url=jdbc:mysql://host.docker.internal:3306/pt?useSSL=false&serverTimezone=Asia/Shanghai
+#spring.datasource.username=root
+#spring.datasource.password=12345
+#spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
+# application.properties
+spring.datasource.url=jdbc:mysql://202.205.102.121:3306/1group5?useSSL=false&serverTimezone=Asia/Shanghai
+spring.datasource.username=team5
+spring.datasource.password=Team5001#
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
hikari.maximum-pool-size = 20
hikari.minimum-idle = 5
@@ -17,6 +23,7 @@
#logging.level.root=DEBUG
mybatis-plus.mapper-locations=classpath:mapper/xml/*.xml
mybatis-plus.type-aliases-package=com.pt5.pthouduan.entity
+
uploadDirectory= ./uploads/files # ????????
torrent-dir= ./uploads/torrents # ????????
# ??????
@@ -28,20 +35,20 @@
# ???????????????Docker????????
torrent.post-image-dir=postimgs/
torrent.helppost-image-dir=helppostimgs/
+torrent.activity-image-dir=activityimgs/
+torrent.user-image-dir=userimgs/
+spring.mail.host=smtp.163.com
+spring.mail.username=zhutai940@163.com
+spring.mail.password=SMQUgkztweuv7LdZ
+spring.mail.properties.mail.smtp.auth=true
+spring.mail.properties.mail.smtp.starttls.enable=true
+spring.mail.properties.mail.smtp.starttls.required=true
# ??????
spring.mvc.static-path-pattern=/uploads/**
spring.web.resources.static-locations=file:./postimgs/
# application.properties ??
-# ????
-spring.mail.host=smtp.163.com # ?163????
-spring.mail.username=zhutai940@163.com
-spring.mail.password=Cmr2005#
-spring.mail.properties.mail.smtp.auth=true
-spring.mail.properties.mail.smtp.starttls.enable=true
-spring.mail.properties.mail.smtp.starttls.required=true
-
tracker.url=http://localhost:6969/announce
diff --git a/src/test/java/com/pt5/pthouduan/ControllerTest/RecommendControllerTest.java b/src/test/java/com/pt5/pthouduan/ControllerTest/RecommendControllerTest.java
new file mode 100644
index 0000000..169ad5f
--- /dev/null
+++ b/src/test/java/com/pt5/pthouduan/ControllerTest/RecommendControllerTest.java
@@ -0,0 +1,76 @@
+package com.pt5.pthouduan.ControllerTest;
+
+import com.pt5.pthouduan.controller.RecommendController;
+import com.pt5.pthouduan.entity.Torrent;
+import com.pt5.pthouduan.service.RecommendService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.*;
+
+class RecommendControllerTest {
+
+ @Mock
+ private RecommendService recommendService;
+
+ @InjectMocks
+ private RecommendController recommendController;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ @Test
+ void getRecommendList_Success() {
+ // 准备测试数据
+ Torrent torrent1 = new Torrent();
+
+ // 执行测试
+ List<Torrent> actualList = recommendController.getRecommendList(123L);
+
+ // 验证结果
+ verify(recommendService, times(1)).recommendForUser(123L);
+ }
+
+ @Test
+ void getRecommendList_EmptyList() {
+ // 模拟空列表返回
+ when(recommendService.recommendForUser(anyLong()))
+ .thenReturn(List.of());
+
+ List<Torrent> result = recommendController.getRecommendList(456L);
+
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void getRecommendList_InvalidUserId() {
+ // 模拟无效用户ID情况
+ when(recommendService.recommendForUser(-1L))
+ .thenThrow(new IllegalArgumentException("Invalid user ID"));
+
+ assertThrows(IllegalArgumentException.class, () -> {
+ recommendController.getRecommendList(-1L);
+ });
+ }
+
+ @Test
+ void getRecommendList_ServiceException() {
+ // 模拟服务层异常
+ when(recommendService.recommendForUser(anyLong()))
+ .thenThrow(new RuntimeException("Recommendation service unavailable"));
+
+ assertThrows(RuntimeException.class, () -> {
+ recommendController.getRecommendList(999L);
+ });
+ }
+}
diff --git a/src/test/java/com/pt5/pthouduan/ControllerTest/UserControllerTest.java b/src/test/java/com/pt5/pthouduan/ControllerTest/UserControllerTest.java
index e843900..dacada4 100644
--- a/src/test/java/com/pt5/pthouduan/ControllerTest/UserControllerTest.java
+++ b/src/test/java/com/pt5/pthouduan/ControllerTest/UserControllerTest.java
@@ -69,46 +69,46 @@
assertEquals(expectedResponse, actualResponse);
}
- @Test
- void uploadAvatar_Success() throws IOException {
- // 准备测试文件
- MultipartFile file = new MockMultipartFile(
- "avatar",
- "test.jpg",
- "image/jpeg",
- "test image content".getBytes()
- );
+// @Test
+// void uploadAvatar_Success() throws IOException {
+// // 准备测试文件
+// MultipartFile file = new MockMultipartFile(
+// "avatar",
+// "test.jpg",
+// "image/jpeg",
+// "test image content".getBytes()
+// );
+//
+// // 模拟服务层行为
+// //doNothing().when(userService).changeImage(anyString(), anyString());
+//
+// // 执行测试
+// ResponseEntity<Map<String, Object>> response = userController.uploadAvatar(file);
+//
+// // 验证结果
+// assertEquals(200, response.getStatusCodeValue());
+// assertTrue((Boolean) response.getBody().get("success"));
+// assertNotNull(response.getBody().get("url"));
+//
+// // 验证文件是否保存到正确路径
+//// Path expectedPath = Paths.get("../var/www/avatars/").resolve(anyString());
+// // 实际项目中需要添加文件存在性断言
+// }
- // 模拟服务层行为
- //doNothing().when(userService).changeImage(anyString(), anyString());
-
- // 执行测试
- ResponseEntity<Map<String, Object>> response = userController.uploadAvatar(file);
-
- // 验证结果
- assertEquals(200, response.getStatusCodeValue());
- assertTrue((Boolean) response.getBody().get("success"));
- assertNotNull(response.getBody().get("url"));
-
- // 验证文件是否保存到正确路径
-// Path expectedPath = Paths.get("../var/www/avatars/").resolve(anyString());
- // 实际项目中需要添加文件存在性断言
- }
-
- @Test
- void uploadAvatar_InvalidFileType() {
- MultipartFile file = new MockMultipartFile(
- "avatar",
- "test.txt",
- "text/plain",
- "invalid content".getBytes()
- );
-
- ResponseEntity<Map<String, Object>> response = userController.uploadAvatar(file);
-
- assertEquals(400, response.getStatusCodeValue());
- assertEquals("只支持JPG/PNG/GIF格式的图片", response.getBody().get("message"));
- }
+// @Test
+// void uploadAvatar_InvalidFileType() {
+// MultipartFile file = new MockMultipartFile(
+// "avatar",
+// "test.txt",
+// "text/plain",
+// "invalid content".getBytes()
+// );
+//
+// ResponseEntity<Map<String, Object>> response = userController.uploadAvatar(file);
+//
+// assertEquals(400, response.getStatusCodeValue());
+// assertEquals("只支持JPG/PNG/GIF格式的图片", response.getBody().get("message"));
+// }
@Test
void changeImage_Success() {