diff --git a/pom.xml b/pom.xml index ce95712..ebd2172 100644 --- a/pom.xml +++ b/pom.xml @@ -280,6 +280,11 @@ thymeleaf-extras-java8time 3.0.4.RELEASE + + + org.springframework.retry + spring-retry + diff --git a/src/main/java/com/ghostchu/btn/sparkle/SparkleApplication.java b/src/main/java/com/ghostchu/btn/sparkle/SparkleApplication.java index 80ef7a4..0307324 100644 --- a/src/main/java/com/ghostchu/btn/sparkle/SparkleApplication.java +++ b/src/main/java/com/ghostchu/btn/sparkle/SparkleApplication.java @@ -3,6 +3,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.retry.annotation.EnableRetry; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.transaction.annotation.EnableTransactionManagement; @@ -12,6 +13,7 @@ @EnableScheduling @EnableTransactionManagement @EnableJpaRepositories +@EnableRetry public class SparkleApplication { public static void main(String[] args) { diff --git a/src/main/java/com/ghostchu/btn/sparkle/module/ping/PingController.java b/src/main/java/com/ghostchu/btn/sparkle/module/ping/PingController.java index d991647..f8620c6 100644 --- a/src/main/java/com/ghostchu/btn/sparkle/module/ping/PingController.java +++ b/src/main/java/com/ghostchu/btn/sparkle/module/ping/PingController.java @@ -15,6 +15,7 @@ import com.ghostchu.btn.sparkle.module.ping.dto.BtnRule; import com.ghostchu.btn.sparkle.module.userapp.UserApplicationService; import com.ghostchu.btn.sparkle.module.userapp.internal.UserApplication; +import com.ghostchu.btn.sparkle.module.userscore.UserScoreService; import com.ghostchu.btn.sparkle.util.ServletUtil; import com.ghostchu.btn.sparkle.util.ipdb.GeoIPManager; import com.google.common.hash.Hashing; @@ -67,6 +68,8 @@ public class PingController extends SparkleController { private GeoIPManager geoIPManager; @Autowired private SubmitHistoriesAbility submitHistoriesAbility; + @Autowired + private UserScoreService userScoreService; @PostMapping("/peers/submit") public ResponseEntity submitPeers(@RequestBody @Validated BtnPeerPing ping) throws AccessDeniedException, UnknownHostException { diff --git a/src/main/java/com/ghostchu/btn/sparkle/module/ping/PingService.java b/src/main/java/com/ghostchu/btn/sparkle/module/ping/PingService.java index 9b65960..93f6589 100644 --- a/src/main/java/com/ghostchu/btn/sparkle/module/ping/PingService.java +++ b/src/main/java/com/ghostchu/btn/sparkle/module/ping/PingService.java @@ -18,6 +18,7 @@ import com.ghostchu.btn.sparkle.module.torrent.TorrentService; import com.ghostchu.btn.sparkle.module.user.UserService; import com.ghostchu.btn.sparkle.module.userapp.internal.UserApplication; +import com.ghostchu.btn.sparkle.module.userscore.UserScoreService; import com.ghostchu.btn.sparkle.util.*; import com.ghostchu.btn.sparkle.util.ipdb.GeoIPManager; import com.google.common.hash.BloomFilter; @@ -59,6 +60,8 @@ public class PingService { private MeterRegistry meterRegistry; @Autowired private PeerHistoryService peerHistoryService; + @Autowired + private UserScoreService userScoreService; @Modifying @Transactional @@ -114,6 +117,7 @@ public void handlePeers(InetAddress submitterIp, UserApplication userApplication } snapshotService.saveSnapshots(snapshotList); meterRegistry.counter("sparkle_ping_peers_processed").increment(snapshotList.size()); + userScoreService.addUserScoreBytes(userApplication.getUser(), Math.max(1, ping.getPeers().size() / 100), "提交瞬时快照数据"); clientDiscoveryService.handleIdentities(now, now, identitySet); processed += snapshotList.size(); } @@ -182,6 +186,7 @@ public void handleBans(InetAddress submitterIp, UserApplication userApplication, banHistoryService.saveBanHistories(banHistoryList); meterRegistry.counter("sparkle_ping_bans_processed").increment(banHistoryList.size()); clientDiscoveryService.handleIdentities(now, now, identitySet); + userScoreService.addUserScoreBytes(userApplication.getUser(), Math.max(1, ping.getBans().size() / 100), "提交增量封禁数据"); processed += banHistoryList.size(); } @@ -259,6 +264,7 @@ public long handlePeerHistories(InetAddress inetAddress, UserApplication userApp clientDiscoveryService.handleIdentities(now, now, identitySet); meterRegistry.counter("sparkle_ping_histories_processed").increment(peerHistoryList.size()); processed += peerHistoryList.size(); + userScoreService.addUserScoreBytes(userApplication.getUser(), Math.max(1, ping.getPeers().size() / 5000), "提交连接历史数据"); return processed; } } diff --git a/src/main/java/com/ghostchu/btn/sparkle/module/user/UserViewController.java b/src/main/java/com/ghostchu/btn/sparkle/module/user/UserViewController.java index eecf5af..eda772b 100644 --- a/src/main/java/com/ghostchu/btn/sparkle/module/user/UserViewController.java +++ b/src/main/java/com/ghostchu/btn/sparkle/module/user/UserViewController.java @@ -2,6 +2,8 @@ import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.stp.StpUtil; +import com.ghostchu.btn.sparkle.module.userscore.UserScoreService; +import org.apache.commons.io.FileUtils; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -12,15 +14,20 @@ @RequestMapping("/user") public class UserViewController { private final UserService userService; + private final UserScoreService userScoreService; - public UserViewController(UserService userService) { + public UserViewController(UserService userService, UserScoreService userScoreService) { this.userService = userService; + this.userScoreService = userScoreService; } @GetMapping("/profile") public String profile(Model model) { var dto = userService.toDto(userService.getUser((StpUtil.getLoginIdAsLong())).get()); model.addAttribute("user", dto); + var userScore = userScoreService.getUserScoreBytes(userService.getUser((StpUtil.getLoginIdAsLong())).get()); + model.addAttribute("userScoreBytes.display", FileUtils.byteCountToDisplaySize(userScore)); + model.addAttribute("userScoreBytes.raw", userScore); return "user/profile"; } } diff --git a/src/main/java/com/ghostchu/btn/sparkle/module/userscore/UserScoreService.java b/src/main/java/com/ghostchu/btn/sparkle/module/userscore/UserScoreService.java new file mode 100644 index 0000000..6e65c5e --- /dev/null +++ b/src/main/java/com/ghostchu/btn/sparkle/module/userscore/UserScoreService.java @@ -0,0 +1,47 @@ +package com.ghostchu.btn.sparkle.module.userscore; + +import com.ghostchu.btn.sparkle.module.user.internal.User; +import com.ghostchu.btn.sparkle.module.userscore.internal.UserScore; +import com.ghostchu.btn.sparkle.module.userscore.internal.UserScoreHistory; +import com.ghostchu.btn.sparkle.module.userscore.internal.UserScoreHistoryRepository; +import com.ghostchu.btn.sparkle.module.userscore.internal.UserScoreRepository; +import org.springframework.dao.OptimisticLockingFailureException; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.OffsetDateTime; + +@Service +public class UserScoreService { + private final UserScoreRepository userScoreRepository; + private final UserScoreHistoryRepository userScoreHistoryRepository; + + public UserScoreService(UserScoreRepository userScoreRepository, UserScoreHistoryRepository userScoreHistoryRepository) { + this.userScoreRepository = userScoreRepository; + this.userScoreHistoryRepository = userScoreHistoryRepository; + } + + public long getUserScoreBytes(User user) { + UserScore userScore = userScoreRepository.findByUser(user); + if (userScore != null) { + return userScore.getScoreBytes(); + } else { + return 0L; + } + } + + @Retryable(retryFor = OptimisticLockingFailureException.class, backoff = @Backoff(delay = 100, multiplier = 2)) + @Transactional + public void addUserScoreBytes(User user, long changes, String reason) { + UserScore userScore = userScoreRepository.findByUser(user); + if (userScore != null) { + userScore.setScoreBytes(userScore.getScoreBytes() + changes); + } else { + userScore = new UserScore(null, user, changes, 0L); + } + userScoreRepository.save(userScore); + userScoreHistoryRepository.save(new UserScoreHistory(null, OffsetDateTime.now(), user, changes, userScore.getScoreBytes(), reason)); + } +} diff --git a/src/main/java/com/ghostchu/btn/sparkle/module/userscore/internal/UserScore.java b/src/main/java/com/ghostchu/btn/sparkle/module/userscore/internal/UserScore.java new file mode 100644 index 0000000..5c8c6bb --- /dev/null +++ b/src/main/java/com/ghostchu/btn/sparkle/module/userscore/internal/UserScore.java @@ -0,0 +1,30 @@ +package com.ghostchu.btn.sparkle.module.userscore.internal; + +import com.ghostchu.btn.sparkle.module.user.internal.User; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "user_score", + uniqueConstraints = {@UniqueConstraint(columnNames = "user")}, + indexes = {@Index(columnList = "user")}) +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +public class UserScore { + @Id + @GeneratedValue + @Column(nullable = false, unique = true) + private Long id; + @JoinColumn(nullable = false, name = "user") + @ManyToOne(fetch = FetchType.EAGER) + private User user; + @Column(nullable = false) + private Long scoreBytes; + @Version + private Long version; +} diff --git a/src/main/java/com/ghostchu/btn/sparkle/module/userscore/internal/UserScoreHistory.java b/src/main/java/com/ghostchu/btn/sparkle/module/userscore/internal/UserScoreHistory.java new file mode 100644 index 0000000..64ae441 --- /dev/null +++ b/src/main/java/com/ghostchu/btn/sparkle/module/userscore/internal/UserScoreHistory.java @@ -0,0 +1,36 @@ +package com.ghostchu.btn.sparkle.module.userscore.internal; + +import com.ghostchu.btn.sparkle.module.user.internal.User; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.OffsetDateTime; + +@Entity +@Table(name = "user_score_history", + uniqueConstraints = {@UniqueConstraint(columnNames = "user")}, + indexes = {@Index(columnList = "user")}) +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +public class UserScoreHistory { + @Id + @GeneratedValue + @Column(nullable = false, unique = true) + private Long id; + @Column(nullable = false) + private OffsetDateTime time; + @JoinColumn(nullable = false, name = "user") + @ManyToOne(fetch = FetchType.EAGER) + private User user; + @Column(nullable = false) + private Long scoreBytesChanges; + @Column(nullable = false) + private Long scoreBytesNow; + @Column() + private String reason; +} diff --git a/src/main/java/com/ghostchu/btn/sparkle/module/userscore/internal/UserScoreHistoryRepository.java b/src/main/java/com/ghostchu/btn/sparkle/module/userscore/internal/UserScoreHistoryRepository.java new file mode 100644 index 0000000..b9a1232 --- /dev/null +++ b/src/main/java/com/ghostchu/btn/sparkle/module/userscore/internal/UserScoreHistoryRepository.java @@ -0,0 +1,9 @@ +package com.ghostchu.btn.sparkle.module.userscore.internal; + +import com.ghostchu.btn.sparkle.module.repository.SparkleCommonRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserScoreHistoryRepository extends SparkleCommonRepository { + +} diff --git a/src/main/java/com/ghostchu/btn/sparkle/module/userscore/internal/UserScoreRepository.java b/src/main/java/com/ghostchu/btn/sparkle/module/userscore/internal/UserScoreRepository.java new file mode 100644 index 0000000..c58e700 --- /dev/null +++ b/src/main/java/com/ghostchu/btn/sparkle/module/userscore/internal/UserScoreRepository.java @@ -0,0 +1,10 @@ +package com.ghostchu.btn.sparkle.module.userscore.internal; + +import com.ghostchu.btn.sparkle.module.repository.SparkleCommonRepository; +import com.ghostchu.btn.sparkle.module.user.internal.User; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserScoreRepository extends SparkleCommonRepository { + UserScore findByUser(User user); +} diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index c2e6d1a..1a9250a 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -8,9 +8,9 @@ 改版装修中 BTN 2.0 正在加紧施工中。由于缺乏前端人手,目前前端仅基本功能可用,不影响后端运行和上报收集等功能。 - - 运行受限 由于 BTN 网络目前正面临巨大的负载,我们暂时禁用了 BTN - 网络的部分自动分析功能,直至负载问题得到解决。 + + 恢复正常运行 我们已应用了缓解措施以应对性能下降,目前 BTN + 的所有功能恢复正常运行。我们仍在观测程序稳定性并持续改进 Sparkle BTN。 @@ -25,6 +25,17 @@ Sparkle - BTN Instance 连接到 BTN 网络,共享威胁情报,获取云端规则。 创建用户应用程序 + + + 统计数据 (BETA) + + + +
连接到 BTN 网络,共享威胁情报,获取云端规则。