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 f8620c6..cf91408 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 @@ -81,6 +81,7 @@ public ResponseEntity submitPeers(@RequestBody @Validated BtnPeerPing pi ip(req), cred.getAppId(), cred.getAppSecret(), ua(req)); audit.put("error", "UserApplication Banned"); auditService.log(req, "BTN_PEERS_SUBMIT", false, audit); + userApplicationService.updateUserApplicationLastAccessTime(cred); return ResponseEntity.status(403).body("UserApplication 已被管理员封禁,请与服务器管理员联系"); } service.handlePeers(InetAddress.getByName(ip(req)), cred, ping); @@ -89,6 +90,7 @@ public ResponseEntity submitPeers(@RequestBody @Validated BtnPeerPing pi audit.put("peers_size", ping.getPeers().size()); // audit.put("peers_handled", handled); auditService.log(req, "BTN_PEERS_SUBMIT", true, audit); + userApplicationService.updateUserApplicationLastAccessTime(cred); return ResponseEntity.status(200).build(); } @@ -102,6 +104,7 @@ public ResponseEntity submitPeerHistories(@RequestBody @Validated BtnPee ip(req), cred.getAppId(), cred.getAppSecret(), ua(req)); audit.put("error", "UserApplication Banned"); auditService.log(req, "BTN_HISTORY_SUBMIT", false, audit); + userApplicationService.updateUserApplicationLastAccessTime(cred); return ResponseEntity.status(403).body("UserApplication 已被管理员封禁,请与服务器管理员联系"); } var handled = service.handlePeerHistories(InetAddress.getByName(ip(req)), cred, ping); @@ -110,6 +113,7 @@ public ResponseEntity submitPeerHistories(@RequestBody @Validated BtnPee audit.put("peers_size", ping.getPeers().size()); audit.put("peers_handled", handled); auditService.log(req, "BTN_HISTORY_SUBMIT", true, audit); + userApplicationService.updateUserApplicationLastAccessTime(cred); return ResponseEntity.status(200).build(); } @@ -123,6 +127,7 @@ public ResponseEntity submitBans(@RequestBody @Validated BtnBanPing ping ip(req), cred.getAppId(), ua(req)); audit.put("error", "UserApplication Banned"); auditService.log(req, "BTN_BANS_SUBMIT", false, audit); + userApplicationService.updateUserApplicationLastAccessTime(cred); return ResponseEntity.status(403).body("UserApplication 已被管理员封禁,请与服务器管理员联系"); } service.handleBans(InetAddress.getByName(ip(req)), cred, ping); @@ -130,6 +135,7 @@ public ResponseEntity submitBans(@RequestBody @Validated BtnBanPing ping // ip(req), handled, cred.getAppId(), ua(req)); audit.put("bans_size", ping.getBans().size()); auditService.log(req, "BTN_BANS_SUBMIT", true, audit); + userApplicationService.updateUserApplicationLastAccessTime(cred); return ResponseEntity.status(200).build(); } @@ -143,6 +149,7 @@ public ResponseEntity config() throws AccessDeniedException, JsonProcess ip(req), cred.getAppId(), ua(req)); audit.put("error", "UserApplication Banned"); auditService.log(req, "BTN_CONFIG", false, audit); + userApplicationService.updateUserApplicationLastAccessTime(cred); return ResponseEntity.status(403).body("UserApplication 已被管理员封禁,请与服务器管理员联系"); } // log.info("[OK] [Config] [{}] 响应配置元数据 (AppId={}, UA={})", @@ -164,6 +171,7 @@ public ResponseEntity config() throws AccessDeniedException, JsonProcess if (countryIso != null && countryIso.equalsIgnoreCase("CN")) { json = json.replace(sparkleRoot, sparkleRootChina); } + userApplicationService.updateUserApplicationLastAccessTime(cred); return ResponseEntity.ok().body(json); } @@ -177,6 +185,7 @@ public ResponseEntity rule() throws IOException, AccessDeniedException { ip(req), cred.getAppId(), ua(req)); audit.put("error", "UserApplication Banned"); auditService.log(req, "BTN_RULES_RETRIEVE", false, audit); + userApplicationService.updateUserApplicationLastAccessTime(cred); return ResponseEntity.status(403).body("UserApplication 已被管理员封禁,请与服务器管理员联系"); } String version = req.getParameter("rev"); @@ -193,6 +202,7 @@ public ResponseEntity rule() throws IOException, AccessDeniedException { audit.put("from", version); audit.put("to", rev); auditService.log(req, "BTN_RULES_RETRIEVE", true, audit); + userApplicationService.updateUserApplicationLastAccessTime(cred); return ResponseEntity.status(200).contentType(MediaType.APPLICATION_JSON).body(objectMapper.writeValueAsString(btn)); } diff --git a/src/main/java/com/ghostchu/btn/sparkle/module/userapp/UserApplicationService.java b/src/main/java/com/ghostchu/btn/sparkle/module/userapp/UserApplicationService.java index 20953d7..ce44999 100644 --- a/src/main/java/com/ghostchu/btn/sparkle/module/userapp/UserApplicationService.java +++ b/src/main/java/com/ghostchu/btn/sparkle/module/userapp/UserApplicationService.java @@ -7,10 +7,15 @@ import com.ghostchu.btn.sparkle.module.userapp.internal.UserApplication; import com.ghostchu.btn.sparkle.module.userapp.internal.UserApplicationRepository; import io.micrometer.core.instrument.MeterRegistry; +import jakarta.persistence.LockModeType; import jakarta.transaction.Transactional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.dao.OptimisticLockingFailureException; +import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Modifying; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; import java.time.OffsetDateTime; @@ -113,6 +118,14 @@ public UserApplication editUserApplicationComment(Long id, String comment) throw return userApplicationRepository.save(usrApp); } + @Transactional + @Lock(value = LockModeType.OPTIMISTIC) + @Retryable(retryFor = OptimisticLockingFailureException.class, backoff = @Backoff(delay = 100, multiplier = 2)) + public void updateUserApplicationLastAccessTime(UserApplication userApplication) { + userApplication.setLastAccessAt(OffsetDateTime.now()); + userApplicationRepository.save(userApplication); + } + public UserApplicationDto toDto(UserApplication userApplication) { return UserApplicationDto.builder() .id(userApplication.getId()) diff --git a/src/main/java/com/ghostchu/btn/sparkle/module/userapp/internal/UserApplication.java b/src/main/java/com/ghostchu/btn/sparkle/module/userapp/internal/UserApplication.java index 7927950..1950eb9 100644 --- a/src/main/java/com/ghostchu/btn/sparkle/module/userapp/internal/UserApplication.java +++ b/src/main/java/com/ghostchu/btn/sparkle/module/userapp/internal/UserApplication.java @@ -38,4 +38,9 @@ public class UserApplication { private String bannedReason; @Column private OffsetDateTime deletedAt; + @Column + private OffsetDateTime lastAccessAt; + @Column(nullable = false) + @Version + private long version; }