Skip to content

Commit

Permalink
用户积分系统
Browse files Browse the repository at this point in the history
  • Loading branch information
Ghost-chu committed Jan 4, 2025
1 parent 8f88727 commit 917cfd3
Show file tree
Hide file tree
Showing 12 changed files with 237 additions and 9 deletions.
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,11 @@
<artifactId>thymeleaf-extras-java8time</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.retry/spring-retry -->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -12,6 +13,7 @@
@EnableScheduling
@EnableTransactionManagement
@EnableJpaRepositories
@EnableRetry
public class SparkleApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> submitPeers(@RequestBody @Validated BtnPeerPing ping) throws AccessDeniedException, UnknownHostException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -59,6 +60,8 @@ public class PingService {
private MeterRegistry meterRegistry;
@Autowired
private PeerHistoryService peerHistoryService;
@Autowired
private UserScoreService userScoreService;

@Modifying
@Transactional
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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";
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<UserScoreHistory, Long> {

}
Original file line number Diff line number Diff line change
@@ -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, Long> {
UserScore findByUser(User user);
}
17 changes: 14 additions & 3 deletions src/main/resources/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
<b><i class="fa-solid fa-road-barrier"></i> 改版装修中</b> BTN 2.0 正在加紧施工中。由于缺乏前端人手,目前前端仅基本功能可用,不影响后端运行和上报收集等功能。
</div>

<div class="alert alert-warning" role="alert">
<b><i class="fa-solid fa-clock"></i> 运行受限</b> 由于 BTN 网络目前正面临巨大的负载,我们暂时禁用了 BTN
网络的部分自动分析功能,直至负载问题得到解决
<div class="alert alert-success" role="alert">
<b><i class="fa-solid fa-check"></i> 恢复正常运行</b> 我们已应用了缓解措施以应对性能下降,目前 BTN
的所有功能恢复正常运行。我们仍在观测程序稳定性并持续改进 Sparkle BTN
</div>

<div class="alert alert-danger" role="alert" th:if="${user.getBannedAt() != null}">
Expand All @@ -25,6 +25,17 @@ <h1 class="display-4">Sparkle - BTN Instance</h1>
<p>连接到 BTN 网络,共享威胁情报,获取云端规则。</p>
<a class="btn btn-primary btn-lg" th:href="@{/userapp/}" role="button">创建用户应用程序</a>
</div>

<div class="container-fluid">
<h2 style="text-align: center;">统计数据 (BETA)</h2>
<iframe style="width: 100%; height: 100vh; border: none;"
src="https://grafana.ghostchu-services.top/public-dashboards/eded9089491241099c9ab3e384d33595"></iframe>
</div>
<!-- <div class="container-fluid">
<h2 style="text-align: center;">查询面板 (BETA)</h2>
<iframe style="width: 100%; height: 100vh; border: none;" src="https://grafana.ghostchu-services.top/public-dashboards/56d23fd566f44eaebdf442120555c56c"></iframe>
</div> -->

<div th:replace="~{components/common::footer}"></div>
</body>
</html>
72 changes: 67 additions & 5 deletions src/main/resources/templates/user/profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,55 @@
<body>
<div th:replace="~{components/common::navbar}"></div>

<style>
.profile-avatar {
width: 296px;
height: 296px;
object-fit: cover;
}

.profile-info {
display: flex;
flex-direction: column;
justify-content: space-between;
margin-left: 20px;
}

.profile-header {
display: flex;
align-items: center;
}

.score-section {
display: flex;
align-items: center;
margin-top: 10px;
}

.score-section .info-icon {
margin-left: 10px;
cursor: pointer;
position: relative;
}

.score-section .info-icon:hover .tooltip {
display: block;
}

.tooltip {
display: none;
position: absolute;
top: 20px;
left: 0;
background-color: #333;
color: #fff;
padding: 5px;
border-radius: 5px;
font-size: 12px;
width: 200px;
z-index: 1;
}
</style>

<div class="container">
<h1>个人资料</h1>
Expand All @@ -16,6 +65,19 @@ <h2 th:text="${user.nickname}">昵称</h2>
<strong>电子邮件:</strong>
<span th:text="${user.email}">[email protected]</span>
</p>
<div class="score-section">
<div>
<strong>Bytes</strong>
<div>--------------------</div>
<div th:text="${userScoreBytes.display}">0 Bytes</div>
</div>
<div class="info-icon">
<i class="fa-solid fa-info-circle"></i>
<div class="tooltip">
Bytes 是您在 Sparkle BTN 中活跃并作出贡献的证明,越高的 Bytes 代表您做出了越多的贡献。
</div>
</div>
</div>
</div>
</div>
<div class="profile-details">
Expand All @@ -30,17 +92,17 @@ <h2 th:text="${user.nickname}">昵称</h2>
<p>
<strong>账户状态:</strong>
<span th:if="${user.bannedAt == null}" class="status-normal">
<i class="fa-solid fa-check"></i> 正常
</span>
<i class="fa-solid fa-check"></i> 正常
</span>
<span th:if="${user.bannedAt != null}" class="status-banned"
th:title="${'暂停原因:' + user.bannedReason}">
<i class="fa-solid fa-ban"></i> 已暂停
</span>
<i class="fa-solid fa-ban"></i> 已暂停
</span>
</p>
</div>
</div>
</div>

<div th:replace="~{components/common::footer}"></div>
</body>
</html>
</html>

0 comments on commit 917cfd3

Please sign in to comment.