Selaa lähdekoodia

订阅节点链接头信息展示优化v1

zhiqiang.lv 1 kuukausi sitten
vanhempi
commit
acaf0ab6e3

+ 6 - 5
src/main/java/top/lvzhiqiang/controller/SubscriptionController.java

@@ -40,12 +40,13 @@ public class SubscriptionController {
             // 标识文件类型
             headers.add("Content-Type", "text/plain; charset=UTF-8");
             // 流量统计条 (upload=0; download=used; total=total; expire=timestamp)
-            long expire = account.getExpireTime() != null ? account.getExpireTime().atZone(ZoneId.systemDefault()).toInstant().getEpochSecond() : 0;
-            String userInfo = String.format("upload=0; download=%d; total=%d; expire=%d",
-                    account.getUsedTraffic(), account.getTotalTraffic(), expire);
+            String userInfo = String.format("upload=%d; download=%d; total=%d; expire=%d",
+                    account.getUploadUsedTraffic(), account.getDownloadUsedTraffic(), account.getTotalTraffic(), account.getExpireTimeSeconds());
             headers.add("Subscription-Userinfo", userInfo);
-            // 建议客户端更新间隔 (秒)
-            headers.add("Profile-Update-Interval", "21600");
+            // 自动更新间隔 (小时):告诉客户端“请每隔多少小时自动更新一次这个订阅”。
+            headers.add("Profile-Update-Interval", "12");
+            // 网页链接:告诉客户端“如果用户想要查看这个订阅的详情,请引导他们访问这个链接”。
+            headers.add("Profile-Web-Page-Url", "https://tv.yc2140.qzz.io/");
 
             return new ResponseEntity<>(body, headers, HttpStatus.OK);
         } catch (Exception e) {

+ 11 - 4
src/main/java/top/lvzhiqiang/entity/NetAccount.java

@@ -27,14 +27,19 @@ public class NetAccount {
     private String token;
 
     /**
-     * 流量(字节)
+     * 上传流量(字节)
      */
-    private Long totalTraffic;
+    private Long uploadUsedTraffic;
+
+    /**
+     * 下载流量(字节)
+     */
+    private Long downloadUsedTraffic;
 
     /**
-     * 已用流量(字节)
+     * 总流量配额(字节)
      */
-    private Long usedTraffic;
+    private Long totalTraffic;
 
     /**
      * 过期时间
@@ -42,6 +47,8 @@ public class NetAccount {
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private LocalDateTime expireTime;
 
+    private Long expireTimeSeconds;
+
     /**
      * 1:启用 0:禁用
      */

+ 0 - 1
src/main/java/top/lvzhiqiang/mapper/NetMapper.java

@@ -16,7 +16,6 @@ public interface NetMapper {
     @Select("SELECT * FROM net_account WHERE token = #{token} LIMIT 1")
     @Results({
             @Result(property = "totalTraffic", column = "total_traffic"),
-            @Result(property = "usedTraffic", column = "used_traffic"),
             @Result(property = "expireTime", column = "expire_time")
     })
     NetAccount findAccountByToken(String token);

+ 125 - 3
src/main/java/top/lvzhiqiang/service/impl/SubscribeServiceImpl.java

@@ -1,9 +1,13 @@
 package top.lvzhiqiang.service.impl;
 
+import com.alibaba.fastjson.JSONObject;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.zaxxer.hikari.HikariDataSource;
+
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.core.io.ClassPathResource;
 import org.springframework.stereotype.Service;
 import org.yaml.snakeyaml.DumperOptions;
@@ -17,11 +21,23 @@ import top.lvzhiqiang.util.StringUtils;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.text.DecimalFormat;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+
 @Service
 @Slf4j
 public class SubscribeServiceImpl implements SubscribeService {
@@ -32,9 +48,89 @@ public class SubscribeServiceImpl implements SubscribeService {
     // Jackson 实例建议定义为全局单例或静态常量
     private final ObjectMapper objectMapper = new ObjectMapper();
 
+    // 1. 定义成员变量,不直接初始化
+    private HikariDataSource privateDataSource;
+    // 2. 注入配置参数
+    @Value("${rn.db.url}")
+    private String url;
+    @Value("${rn.db.username}")
+    private String userName;
+    @Value("${rn.db.password}")
+    private String password;
+
+    // 定义单位数组
+    private static final String[] UNITS = new String[] { "B", "KB", "MB", "GB", "TB", "PB", "EB" };
+
+    /**
+     * 3. 初始化方法
+     * 该方法会在 @Value 注入完成后,由 Spring 自动调用一次
+     */
+    @PostConstruct
+    public void init() {
+        System.out.println("正在初始化 Service 私有的连接池...");
+
+        privateDataSource = new HikariDataSource();
+        privateDataSource.setJdbcUrl(url);
+        privateDataSource.setUsername(userName);
+        privateDataSource.setPassword(password);
+        privateDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
+
+        // 建议设置特定的 PoolName,方便排查问题
+        privateDataSource.setPoolName("Private-Pool-Service-1");
+        // 针对私有连接池,通常建议调小连接数
+        privateDataSource.setMaximumPoolSize(5);
+    }
+    
+    /**
+     * 4. 销毁方法 (非常重要!)
+     * 当这个 Service 被销毁(或应用关闭)时,必须关闭连接池,
+     * 否则会导致数据库连接泄露(Connection Leak)。
+     */
+    @PreDestroy
+    public void cleanup() {
+        if (privateDataSource != null && !privateDataSource.isClosed()) {
+            System.out.println("正在关闭 Service 私有的连接池...");
+            privateDataSource.close();
+        }
+    }
+
     @Override
     public NetAccount getAccount(String token) {
-        return netMapper.findAccountByToken(token);
+        NetAccount netAccount = netMapper.findAccountByToken(token);
+
+        if (netAccount == null) {
+            throw new BusinessException("Account not found");
+        }
+
+        if (netAccount.getStatus() != null && netAccount.getStatus() == 0) {
+            throw new BusinessException("Account is disabled");
+        }
+
+        // 获取流量使用信息
+        try (Connection connection = privateDataSource.getConnection();
+                PreparedStatement statement = connection.prepareStatement("select * from users where username = ?")) {
+            statement.setObject(1, netAccount.getUsername());
+            try (ResultSet rs = statement.executeQuery()) {
+                while (rs.next()) {
+                    Long download = rs.getLong("download");
+                    Long upload = rs.getLong("upload");
+                    String expiryDate = rs.getString("expiryDate");
+                    if (StringUtils.isEmpty(expiryDate)) {
+                        expiryDate = "9999-12-31";
+                    }
+
+                    netAccount.setDownloadUsedTraffic(download);
+                    netAccount.setUploadUsedTraffic(upload);
+                    netAccount.setTotalTraffic(1125899906842624L);
+                    long expire = LocalDate.parse(expiryDate).atTime(LocalTime.MAX).atZone(ZoneId.systemDefault()).toInstant().getEpochSecond();
+                    netAccount.setExpireTimeSeconds(expire);
+                }
+            }
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+
+        return netAccount;
     }
 
     /**
@@ -159,8 +255,9 @@ public class SubscribeServiceImpl implements SubscribeService {
         // 2. 处理 JSON 扩展字段 (Hysteria2/Vless 特有字段等)
         try {
             if (StringUtils.isNotEmpty(node.getMetaJson())) {
-                Map<String, Object> meta = objectMapper.readValue(node.getMetaJson(), new TypeReference<Map<String, Object>>() {
-                });
+                Map<String, Object> meta = objectMapper.readValue(node.getMetaJson(),
+                        new TypeReference<Map<String, Object>>() {
+                        });
                 map.putAll(meta);
             }
         } catch (Exception e) {
@@ -173,4 +270,29 @@ public class SubscribeServiceImpl implements SubscribeService {
 
         return map;
     }
+
+    /**
+     * 格式化文件大小
+     * @param size 字节数 (long)
+     * @return 格式化后的字符串 (例如: "1.74 MB")
+     */
+    public static String formatFileSize(long size) {
+        if (size <= 0) {
+            return "0";
+        }
+        
+        // 计算层级 (0=B, 1=KB, 2=MB, etc.)
+        // 使用 log10(size) / log10(1024) 可以快速计算出该数字属于哪个量级
+        int digitGroups = (int) (Math.log10(size) / Math.log10(1024));
+        
+        // 格式化数字,保留两位小数 (#.##)
+        // new DecimalFormat("#,##0.##") 也可以处理千分位,如 1,024.50 MB
+        DecimalFormat df = new DecimalFormat("#0.##");
+        
+        // 计算显示值:原数值 / (1024的n次方)
+        double displayValue = size / Math.pow(1024, digitGroups);
+        
+        // 拼接结果:数字 + 空格 + 单位
+        return df.format(displayValue) + " " + UNITS[digitGroups];
+    }
 }