Browse Source

feature: add app scores and info get

ggbocoder 2 years ago
parent
commit
bf498d3f7d

+ 9 - 4
README.md

@@ -3,16 +3,15 @@
 
 - 基于邮件激活的注册方式 done
 - 基于MD5加密与加盐的密码存储方式 done 
-- 登录功能包括随机验证码的验证
 - 登录状态检查,为游客和已登录用户展示不同界面与功能 done
-- 不同用户的权限控制和网站数据统计(UV、DAU) doing
-- 管理员可以查看网站数据统计和网站监控信息 
+- 不同用户的权限控制和网站数据统计(UV、DAU) done
+- 管理员可以查看网站数据统计和网站监控信息 done
 - 用户上传头像
 - 发布帖子、评论帖子、热帖排行、发送私信与敏感词过滤等功能 done
 - 点赞、关注与系统通知功能 done
 - 全局搜索帖子信息的功能
 - 上传和下载apk
-- 爬取Fdroid的APP图标和信息介绍
+- 爬取Fdroid的APP图标和信息介绍 doing
 - 提供实时的热门app榜单
 - 构建fdroid app构建流水线
 - 开发者配置riscv的app构建文件,实现负载均衡模块,执行构建流水线,实时展示构建日志,kafka异步发送邮件通知构建完成
@@ -40,4 +39,10 @@
 ## 评论点赞设计
 - 用Redis的set结构存储点赞情况,key:postId,value:set<userId>
 
+## app下载量统计
+### 方案一
+使用Redis的hashmap存储 appId->downloads的映射,用户每次下载使用incr命令,使用定时任务,每隔一段时间将下载量同步到数据库
+### 方案二
+使用kafka采集下载日志信息,包括[appId][userId][downloadTime]信息
+
 

+ 10 - 0
db/app_detail.sql

@@ -0,0 +1,10 @@
+drop table if exists `app_detail`;
+create table `app_download_count`
+(
+    id                  BIGINT PRIMARY KEY auto_increament,
+    folder_name         VARCHAR(255),
+    img_src             VARCHAR(255),
+    app_name            VARCHAR(255),
+    brief               VARCHAR(255),
+    package_description VARCHAR(255)
+)

+ 6 - 0
db/app_score.sql

@@ -0,0 +1,6 @@
+drop table if exists `app_score`;
+create table `app_score`(
+                                     app_id INT,
+                                     user_id BIGINT,
+                                     score INT
+)

+ 34 - 0
src/main/java/cn/seecoder/fdroidrepository/Controller/AppController.java

@@ -1,13 +1,21 @@
 package cn.seecoder.fdroidrepository.Controller;
 
+import cn.seecoder.fdroidrepository.DataObject.PO.AppDetailPO;
+import cn.seecoder.fdroidrepository.DataObject.VO.AppDetailVO;
 import cn.seecoder.fdroidrepository.DataObject.VO.AppInfoVO;
 import cn.seecoder.fdroidrepository.DataObject.VO.BuildTaskVO;
 import cn.seecoder.fdroidrepository.DataObject.VO.Response;
+import cn.seecoder.fdroidrepository.Service.ServiceImpl.ApkService;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
 @RestController
 @RequestMapping("/app")
 public class AppController {
+
+    @Autowired
+    private ApkService apkService;
+
     @PostMapping("/createApp")
     private Response createApp(@RequestBody AppInfoVO appInfoVO) {
         return Response.buildSuccess();
@@ -22,4 +30,30 @@ public class AppController {
     private Response checkStatus(@RequestParam("taskId") Integer taskId) {
         return Response.buildSuccess();
     }
+
+    @PostMapping("/score")
+    private Response scoreApp(@RequestParam Integer score,
+                              @RequestParam Integer userId,
+                              @RequestParam Integer appId){
+        apkService.addAppScore(appId,userId,score);
+        return Response.buildSuccess();
+    }
+    @GetMapping("/appDetail/{appId}")
+    private Response<AppDetailVO> getAppDetail(@PathVariable int appId){
+        AppDetailVO appDetailVO = new AppDetailVO();
+        AppDetailPO appDetailPO = apkService.getAppDetailPO(appId);
+        //set main description
+        appDetailVO.setAppLogoSrc(appDetailPO.getAppLogoSrc());
+        appDetailVO.setAppName(appDetailPO.getAppName());
+        appDetailVO.setAppId(appDetailPO.getAppId());
+        appDetailVO.setDescription(appDetailPO.getDescription());
+        //set score
+        appDetailVO.setScore(apkService.getAppScore(appId));
+        //set download counts
+        appDetailVO.setDownloadCount(apkService.getAppDownLoadCount(appId));
+        return Response.buildSuccess(appDetailVO);
+    }
+
+
+
 }

+ 12 - 0
src/main/java/cn/seecoder/fdroidrepository/DataObject/PO/AppDetailPO.java

@@ -0,0 +1,12 @@
+package cn.seecoder.fdroidrepository.DataObject.PO;
+
+import lombok.Data;
+
+@Data
+public class AppDetailPO {
+    Integer appId;
+    String appName;
+    String appLogoSrc;
+    String brief;
+    String description;
+}

+ 13 - 0
src/main/java/cn/seecoder/fdroidrepository/DataObject/VO/AppBriefVO.java

@@ -0,0 +1,13 @@
+package cn.seecoder.fdroidrepository.DataObject.VO;
+
+
+import lombok.Data;
+
+@Data
+public class AppBriefVO {
+    Integer appId;
+    String appName;
+    String appLogoSrc;
+    String brief;
+    Long downloadCount;
+}

+ 16 - 0
src/main/java/cn/seecoder/fdroidrepository/DataObject/VO/AppDetailVO.java

@@ -0,0 +1,16 @@
+package cn.seecoder.fdroidrepository.DataObject.VO;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class AppDetailVO implements Serializable  {
+    Integer appId;
+    String appName;
+    String appLogoSrc;
+    String brief;
+    Long downloadCount;
+    String description;
+    Double score;
+}

+ 1 - 1
src/main/java/cn/seecoder/fdroidrepository/DataObject/vo/GitlabUserCreateVO.java → src/main/java/cn/seecoder/fdroidrepository/DataObject/VO/GitlabUserCreateVO.java

@@ -1,4 +1,4 @@
-package cn.seecoder.fdroidrepository.DataObject.vo;
+package cn.seecoder.fdroidrepository.DataObject.VO;
 
 
 import lombok.Data;

+ 0 - 4
src/main/java/cn/seecoder/fdroidrepository/Service/AppService.java

@@ -1,4 +0,0 @@
-package cn.seecoder.fdroidrepository.Service;
-
-public class AppService {
-}

+ 196 - 0
src/main/java/cn/seecoder/fdroidrepository/Service/ServiceImpl/ApkService.java

@@ -1,4 +1,200 @@
 package cn.seecoder.fdroidrepository.Service.ServiceImpl;
 
+import cn.seecoder.fdroidrepository.DataObject.PO.AppDetailPO;
+import cn.seecoder.fdroidrepository.utils.RedisUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.sql.*;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.*;
+
+@Service
 public class ApkService {
+
+    private final static String APP_SCORE_RANK="app_score_rank";
+
+    private final static String APP_DOWNLOAD_COUNT_RANK = "app_download_count_rank";
+
+    @Value("$(spring.datasource.url)")
+    private static String JDBC_URL;
+    @Value("$(spring.datasource.username)")
+    private static String JDBC_USER;
+    @Value("$(spring.datasource.password)")
+    private static String JDBC_PASSWORD;
+    @Autowired
+    private RedisTemplate redisTemplate;
+
+    private static ScheduledExecutorService updateExecutorService = Executors.newScheduledThreadPool(1);
+
+    public ApkService(){
+        updateExecutorService.scheduleAtFixedRate(() -> {
+            // update score rank
+            updateScoreRank();
+            // update downloads rank
+            updateDownloadCountRank();
+        }, 1, 2 * 60 * 60, TimeUnit.SECONDS);
+    }
+
+    private void updateScoreRank(){
+        redisTemplate.delete(APP_SCORE_RANK);
+        Map<Integer,Double> rankMap = getScoreRankFromDB();
+        for (Map.Entry<Integer, Double> entry : rankMap.entrySet()) {
+            int appId = entry.getKey();
+            double averageScore = entry.getValue();
+            redisTemplate.opsForZSet().add(APP_SCORE_RANK, RedisUtil.getSoreKey(String.valueOf(appId)), averageScore);
+        }
+    }
+
+    private void updateDownloadCountRank(){
+        redisTemplate.delete(APP_DOWNLOAD_COUNT_RANK);
+        Integer maxAppId = getMaxAppId();
+        for(int id =0; id<=maxAppId;id++){
+            String key =RedisUtil.getDownloadCountKey(String.valueOf(id));
+            Integer count = (Integer) redisTemplate.opsForValue().get(key);
+            redisTemplate.opsForZSet().add(APP_DOWNLOAD_COUNT_RANK,key, Objects.isNull(count)?0:count);
+        }
+    }
+
+    public Double getAppScore(Integer appId){
+        return redisTemplate.opsForZSet().score(APP_SCORE_RANK, RedisUtil.getSoreKey(String.valueOf(appId)));
+    }
+
+    public Long getAppDownLoadCount(Integer appId){
+        double count = redisTemplate.opsForZSet().score(APP_DOWNLOAD_COUNT_RANK,RedisUtil.getDownloadCountKey(String.valueOf(appId)));
+        return (long) count;
+    }
+
+    private Map<Integer/* appId */,Double/* score */> getScoreRankFromDB(){
+        Map<Integer,Double> res =new HashMap<>();
+        try (Connection connection = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
+             Statement statement = connection.createStatement();
+             ResultSet resultSet = statement.executeQuery("SELECT app_id, AVG(score) as average_score FROM app_score")) {
+            while (resultSet.next()) {
+                int appId = resultSet.getInt("app_id");
+                double averageScore = resultSet.getDouble("average_score");
+                res.put(appId,averageScore);
+            }
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+        return res;
+    }
+
+
+
+    public AppDetailPO getAppDetailPO(Integer appId) {
+        try (Connection connection = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
+             PreparedStatement preparedStatement = connection.prepareStatement(
+                 "SELECT * FROM app_detail WHERE id = ?"
+             )) {
+            preparedStatement.setString(1, String.valueOf(appId));
+
+            try (ResultSet resultSet = preparedStatement.executeQuery()) {
+                AppDetailPO appDetailPO = new AppDetailPO();
+                appDetailPO.setAppId(resultSet.getInt("id"));
+                appDetailPO.setAppName(resultSet.getString("app_name"));
+                appDetailPO.setAppLogoSrc(resultSet.getString("img_src"));
+                appDetailPO.setBrief(resultSet.getString("brief"));
+                appDetailPO.setDescription(resultSet.getString("package_description"));
+                return appDetailPO;
+            }
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public void addDownloadCount(Integer appId) {
+        String key = RedisUtil.getDownloadCountKey(String.valueOf(appId));
+        redisTemplate.opsForValue().increment(key);
+    }
+
+
+    public void addAppScore(Integer appId, long userId, Integer score) {
+        if (isScoreExists(appId, userId)) {
+            // if the user has scored, update it
+            updateAppScore(appId, userId, score);
+        } else {
+            // insert
+            insertAppScore(appId, userId, score);
+        }
+    }
+
+    private boolean isScoreExists(Integer appId, long userId) {
+        try (Connection connection = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
+             PreparedStatement preparedStatement = connection.prepareStatement(
+                 "SELECT COUNT(*) FROM app_score WHERE app_id = ? AND user_id = ?"
+             )) {
+
+            preparedStatement.setInt(1, appId);
+            preparedStatement.setLong(2, userId);
+
+            try (ResultSet resultSet = preparedStatement.executeQuery()) {
+                if (resultSet.next()) {
+                    return resultSet.getInt(1) > 0;
+                }
+            }
+
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+        return false;
+    }
+
+    private void updateAppScore(Integer appId, long userId, Integer score) {
+        try (Connection connection = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
+             PreparedStatement preparedStatement = connection.prepareStatement(
+                 "UPDATE app_score SET score = ? WHERE app_id = ? AND user_id = ?"
+             )) {
+
+            preparedStatement.setInt(1, score);
+            preparedStatement.setInt(2, appId);
+            preparedStatement.setLong(3, userId);
+
+            preparedStatement.executeUpdate();
+
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void insertAppScore(Integer appId, long userId, Integer score) {
+        try (Connection connection = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
+             PreparedStatement preparedStatement = connection.prepareStatement(
+                 "INSERT INTO app_score (app_id, user_id, score) VALUES (?, ?, ?)"
+             )) {
+
+            preparedStatement.setInt(1, appId);
+            preparedStatement.setLong(2, userId);
+            preparedStatement.setInt(3, score);
+
+            preparedStatement.executeUpdate();
+
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private Integer getMaxAppId(){
+        try (Connection connection = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
+             Statement statement = connection.createStatement();
+             ResultSet resultSet = statement.executeQuery("SELECT MAX(id) FROM app_detail")) {
+
+            if (resultSet.next()) {
+                int maxId = resultSet.getInt(1);
+                return maxId;
+            }
+
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+        return 0;
+    }
+
+
 }

+ 4 - 0
src/main/java/cn/seecoder/fdroidrepository/Service/ServiceImpl/AppService.java

@@ -0,0 +1,4 @@
+package cn.seecoder.fdroidrepository.Service.ServiceImpl;
+
+public class AppService {
+}

+ 8 - 0
src/main/java/cn/seecoder/fdroidrepository/utils/RedisUtil.java

@@ -30,4 +30,12 @@ public class RedisUtil {
     public static String getDAUKey(String start,String end){
         return "dau"+REDIS_SPLIT_KEY+start+REDIS_SPLIT_KEY+end;
     }
+
+    public static String getDownloadCountKey(String key){
+        return "download_app"+REDIS_SPLIT_KEY+key;
+    }
+
+    public static String getSoreKey(String key){
+        return "score_app"+ REDIS_SPLIT_KEY+key;
+    }
 }