Pārlūkot izejas kodu

feat:新增出包配置修改日志

hidewnd 5 dienas atpakaļ
vecāks
revīzija
d0a1ff215b

+ 2 - 0
yt-common/src/main/java/com/ytpm/agent/model/YtApp.java

@@ -1,5 +1,6 @@
 package com.ytpm.agent.model;
 
+import com.ytpm.annotation.BeanChange;
 import lombok.Data;
 
 @Data
@@ -51,6 +52,7 @@ public class YtApp {
     /**
      * 更新提示
      */
+    @BeanChange(name = "更新提示")
     private String updateTips;
     /**
      * 启用状态

+ 33 - 0
yt-common/src/main/java/com/ytpm/agent/model/YtAppConfigLog.java

@@ -0,0 +1,33 @@
+package com.ytpm.agent.model;
+
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * @author lih
+ * @date 2025-11-03 15:43
+ */
+@Data
+@ApiModel("出包配置修改记录日志表")
+public class YtAppConfigLog {
+
+    @ApiModelProperty("记录Id")
+    private String recordId;
+
+    @ApiModelProperty("appId")
+    private String appId;
+
+    @ApiModelProperty("修改内容")
+    private String changeContent;
+
+    @ApiModelProperty("修改人")
+    private String changeBy;
+
+    @ApiModelProperty("修改时间")
+    private Date changeTime;
+
+}

+ 4 - 0
yt-common/src/main/java/com/ytpm/agent/view/AgentAppView.java

@@ -1,5 +1,6 @@
 package com.ytpm.agent.view;
 
+import com.ytpm.agent.model.YtAppConfigLog;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
@@ -177,4 +178,7 @@ public class AgentAppView {
     private Integer canAllowAutoRefresh;
 
 
+    @ApiModelProperty("最近修改记录")
+    private YtAppConfigLog lastChangeLog;
+
 }

+ 35 - 0
yt-common/src/main/java/com/ytpm/annotation/BeanChange.java

@@ -0,0 +1,35 @@
+package com.ytpm.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Bean属性变化比较工具类
+ * 用于比较两个相同类型实体中被@BeanChange注解标记的属性变化
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface BeanChange {
+
+    /**
+     * 属性中文名称
+     */
+    String name();
+
+    /**
+     * 日期格式化格式
+     */
+    String dateFormat() default "yyyy-MM-dd HH:mm:ss";
+
+    /**
+     * 是否忽略
+     */
+    boolean ifIgnored() default false;
+
+    /**
+     * 是否值
+     */
+    boolean ifYesNo() default false;
+}

+ 39 - 1
yt-common/src/main/java/com/ytpm/app/view/WxDefaultConfig.java

@@ -1,6 +1,7 @@
 package com.ytpm.app.view;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.ytpm.annotation.BeanChange;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.AllArgsConstructor;
@@ -20,61 +21,98 @@ public class WxDefaultConfig {
     private String platformAppId;
     @JsonIgnore
     private String platformAppSecret;
-    private int appType;
+    private Integer appType;
     private String userPath;
     private String loginPath;
     private String adPath;
     private String answerPath;
     private String powerPath;
+
     @ApiModelProperty("taku应用ID")
+    @BeanChange(name = "taku应用ID")
     private String takuAppId;
+
     @ApiModelProperty("taku应用key")
+    @BeanChange(name = "takuKey")
     private String takuKey;
+
     @ApiModelProperty("taku横幅")
+    @BeanChange(name = "taku横幅ID")
     private String takuBannerPid;
+
     @ApiModelProperty("taku原生")
+    @BeanChange(name = "taku原生ID")
     private String takuNativePid;
+
     @ApiModelProperty("taku激励")
+    @BeanChange(name = "taku激励ID")
     private String takuRewardPid;
+
     @ApiModelProperty("taku插屏")
+    @BeanChange(name = "taku插屏ID")
     private String takuInterstitialPid;
+
     @ApiModelProperty("taku插屏")
+    @BeanChange(name = "taku ScreenID")
     private String takuScreenPid;
+
     @ApiModelProperty("是否允许root")
+    @BeanChange(name = "是否允许root", ifYesNo = true)
     private Integer canUseRoot;
+
     @ApiModelProperty("是否允许adb")
+    @BeanChange(name = "是否允许adb", ifYesNo = true)
     private Integer canUseAdb;
+
     @ApiModelProperty("是否允许浮窗")
+    @BeanChange(name = "是否允许浮窗", ifYesNo = true)
     private Integer canUseFloat;
+
     @ApiModelProperty("是否允许累加体力")
+    @BeanChange(name = "是否允许累加体力", ifYesNo = true)
     private Integer canAccumulation;
+
     @ApiModelProperty("是否允许模拟器")
+    @BeanChange(name = "是否允许模拟器", ifYesNo = true)
     private Integer canSimulator;
+
     @ApiModelProperty("体力等待时间(秒)")
+    @BeanChange(name = "体力等待时间(秒)")
     private Integer powerWaitTime;
+
     @ApiModelProperty("插屏间隔时间(秒)")
+    @BeanChange(name = "插屏间隔时间(秒)")
     private Integer interstitialIntervalTime;
+
     @ApiModelProperty("渠道类型id")
+    @BeanChange(name = "渠道类型id")
     private String ditchId;
 
     @ApiModelProperty("用户低收益提示语")
+    @BeanChange(name = "用户低收益提示语")
     private String lowValueTip;
 
     @ApiModelProperty("刷子提示语")
+    @BeanChange(name = "刷子提示语")
     private String brushTip;
 
     @ApiModelProperty("原生(信息流)间隔时长(秒)")
+    @BeanChange(name = "原生(信息流)间隔时长(秒)")
     private Integer flowIntervalTime;
 
     @ApiModelProperty("体力不足提示语")
+    @BeanChange(name = "体力不足提示语")
     private String taskLimitTip;
 
     @ApiModelProperty("是否缓存激励视频")
+    @BeanChange(name = "是否缓存激励视频", ifYesNo = true)
     private Integer canCacheVideo;
 
     @ApiModelProperty("启动页等待时间(秒)")
+    @BeanChange(name = "启动页等待时间(秒)")
     private Integer startWaitTime;
 
     @ApiModelProperty("是否允许自动刷新")
+    @BeanChange(name = "是否允许自动刷新", ifYesNo = true)
     private Integer canAllowAutoRefresh;
 }

+ 11 - 0
yt-middle/middle-platform/src/main/java/com/ytpm/middle/dao/AppMapper.java

@@ -1,5 +1,7 @@
 package com.ytpm.middle.dao;
 
+import com.ytpm.agent.model.YtApp;
+import com.ytpm.agent.model.YtAppConfigLog;
 import com.ytpm.agent.model.YtPlatformUserApp;
 import com.ytpm.middle.model.YtAppGrantRecord;
 import com.ytpm.middle.param.AppListParam;
@@ -66,4 +68,13 @@ public interface AppMapper {
      * @return
      */
     List<String> getAppListForSuperior(@Param("appId")String appId);
+
+    /**
+     * 保存出包配置修改记录
+     */
+    void insertConfigLog(YtAppConfigLog configLog);
+
+    List<YtAppConfigLog> selectLastConfigLog(@Param("appIds") String appIds);
+
+    YtApp selectByPrimaryId(@Param("appId")String appId);
 }

+ 71 - 8
yt-middle/middle-platform/src/main/java/com/ytpm/middle/service/impl/ApkServiceImpl.java

@@ -1,6 +1,7 @@
 package com.ytpm.middle.service.impl;
 
 import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.text.CharSequenceUtil;
 import cn.hutool.core.util.IdUtil;
 import cn.hutool.core.util.StrUtil;
@@ -9,6 +10,7 @@ import com.alibaba.fastjson.JSONArray;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import com.ytpm.agent.model.YtApp;
+import com.ytpm.agent.model.YtAppConfigLog;
 import com.ytpm.agent.model.YtPlatformUserApp;
 import com.ytpm.agent.param.AppListParam;
 import com.ytpm.agent.param.AppParam;
@@ -23,13 +25,15 @@ import com.ytpm.middle.dao.AppMapper;
 import com.ytpm.middle.dao.RiskMapper;
 import com.ytpm.middle.service.ApkService;
 import com.ytpm.middle.util.AliOSSUtil;
+import com.ytpm.middle.util.BeanChangeUtil;
 import com.ytpm.middle.util.FeignClientInvoker;
 import com.ytpm.middle.view.MiddleUserInfo;
 import com.ytpm.risk.model.YtRiskConfig;
 import com.ytpm.risk.model.YtRiskTemplate;
 import com.ytpm.risk.model.YtRiskTemplateConfig;
 import com.ytpm.util.DateUtil;
-import org.springframework.beans.factory.annotation.Autowired;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.cloud.context.config.annotation.RefreshScope;
 import org.springframework.stereotype.Service;
@@ -44,6 +48,7 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.stream.Collectors;
 
+@Slf4j
 @Service
 @RefreshScope
 public class ApkServiceImpl implements ApkService {
@@ -52,6 +57,11 @@ public class ApkServiceImpl implements ApkService {
     private ApkMapper apkMapper;
     @Resource
     private RiskMapper riskMapper;
+    @Resource
+    private FeignClientInvoker feignInvoker;
+    @Resource
+    private AppMapper appMapper;
+
     @Value("${risk.config.initEcpm}")
     private String initEcpm;
     @Value("${risk.config.initRevenue}")
@@ -89,10 +99,7 @@ public class ApkServiceImpl implements ApkService {
     @Value(("${risk.config.visitor.initRidCount:10}"))
     private String initVisitorRidCount;
 
-    @Autowired
-    private FeignClientInvoker feignInvoker;
-    @Autowired
-    private AppMapper appMapper;
+
 
     /**
      * 查询应用列表
@@ -119,6 +126,12 @@ public class ApkServiceImpl implements ApkService {
         Map<String, Integer> countDownloadMap = AliOSSUtil.getCountDownload(
                 Math.toIntExact(DateUtil.getTodayStart().getTime() / 1000), (int) (System.currentTimeMillis() / 1000));
         WxDefaultConfig config;
+        // 查询配置修改记录
+        List<YtAppConfigLog> configLogs = appMapper.selectLastConfigLog(appIds);
+        Map<String, YtAppConfigLog> configLogMap = configLogs.stream().collect(Collectors.toMap(
+                YtAppConfigLog::getAppId, item-> item,
+                (o1,o2) -> o2
+        ));
         for (AgentAppView view : views) {
             if(appTypeMap.containsKey(view.getAppId())) {
                 config = appTypeMap.get(view.getAppId());
@@ -144,6 +157,7 @@ public class ApkServiceImpl implements ApkService {
                 view.setBrushTip(config.getBrushTip());
                 view.setFlowIntervalTime(config.getFlowIntervalTime());
                 view.setCanAllowAutoRefresh(config.getCanAllowAutoRefresh());
+                view.setLastChangeLog(configLogMap.get(view.getAppId()));
             }
             if(CharSequenceUtil.isBlank(view.getApkUrl()))continue;
             String substring = view.getApkUrl().substring(view.getApkUrl().lastIndexOf("/")+1).toLowerCase(Locale.ENGLISH);
@@ -160,17 +174,19 @@ public class ApkServiceImpl implements ApkService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Result<?> saveApp(AppParam param, MiddleUserInfo loginUser) {
+        YtPlatformUserApp app = appMapper.getByPrimary(param.getSuperiorId());
+        // 保存修改记录
+        saveConfigChangeLog(param, app, loginUser.getUserId());
         //数据库操作,有appId为修改, 没有时为新增
         changeDataAction(param, loginUser.getUserId());
         //出包修改 default_config
         changeDefaultConfig(param);
         if(CharSequenceUtil.isNotBlank(param.getAppId())){
             //修改时判断已生成风控配置,则说明出过包,仅需更换即可
-            YtRiskTemplate template =  riskMapper.getTemplateByAppId(param.getAppId());
+            YtRiskTemplate template = riskMapper.getTemplateByAppId(param.getAppId());
             if(Objects.isNull(template)){
                 generateRiskDefaultConfig(param,loginUser);
             }
-            return Result.resultOk(RepMessage.SAVE_SUCCESS);
         }
         return Result.resultOk(RepMessage.SAVE_SUCCESS);
     }
@@ -181,6 +197,8 @@ public class ApkServiceImpl implements ApkService {
     @Override
     public Result<?> changeConfig(AppParam param) {
         //出包修改 default_config
+        YtPlatformUserApp app = appMapper.getByPrimary(param.getSuperiorId());
+        saveConfigChangeLog(param, app, null);
         changeDefaultConfig(param);
         return Result.resultOk(RepMessage.SAVE_SUCCESS);
     }
@@ -190,6 +208,11 @@ public class ApkServiceImpl implements ApkService {
      */
     private void changeDefaultConfig(AppParam param) {
         YtPlatformUserApp app = appMapper.getByPrimary(param.getSuperiorId());
+        YtAppDefaultConfig config = convertToYtDefaultConfig(param, app);
+        feignInvoker.invoke(app.getServiceName(), "updateAppConfig", config);
+    }
+
+    private YtAppDefaultConfig convertToYtDefaultConfig(AppParam param, YtPlatformUserApp app){
         YtAppDefaultConfig config = new YtAppDefaultConfig();
         config.setAppId(param.getAppId());
         if(Objects.nonNull(param.getDitchId())){
@@ -214,7 +237,47 @@ public class ApkServiceImpl implements ApkService {
         config.setCanCacheVideo(param.getCanCacheVideo());
         config.setStartWaitTime(param.getStartWaitTime());
         config.setCanAllowAutoRefresh(param.getCanAllowAutoRefresh());
-        feignInvoker.invoke(app.getServiceName(), "updateAppConfig",config);
+        return config;
+    }
+
+    private void saveConfigChangeLog(AppParam appParam, YtPlatformUserApp app, String loginUserId) {
+        StringBuilder changeContent = new StringBuilder();
+        try {
+            YtApp oldYtApp = appMapper.selectByPrimaryId(appParam.getAppId());
+            if (oldYtApp != null) {
+                YtApp newYtApp = new YtApp();
+                BeanUtil.copyProperties(appParam, newYtApp);
+                changeContent.append(BeanChangeUtil.compareBeans(oldYtApp, newYtApp));
+            }
+        } catch (Exception e) {
+            log.error(e.getMessage(), e);
+        }
+        try {
+            Object object = feignInvoker.invoke(app.getServiceName(), "getConfigs", appParam.getAppId());
+            WxDefaultConfig oldDefault = null;
+            if (object != null) {
+                List<WxDefaultConfig> configList = JSONArray.parseArray(JSON.toJSONString(object), WxDefaultConfig.class);
+                oldDefault = CollUtil.isNotEmpty(configList) ? configList.get(0) : null;
+            }
+            if (oldDefault != null) {
+                // 变更记录修改
+                YtAppDefaultConfig config = convertToYtDefaultConfig(appParam, app);
+                WxDefaultConfig newConfig = new WxDefaultConfig();
+                BeanUtils.copyProperties(config, newConfig);
+                changeContent.append(BeanChangeUtil.compareBeans(oldDefault, newConfig));
+            }
+        }catch (Exception e){
+            log.error(e.getMessage(), e);
+        }
+        if (changeContent.length() > 0) {
+            YtAppConfigLog configLog = new YtAppConfigLog();
+            configLog.setRecordId(IdUtil.fastSimpleUUID());
+            configLog.setAppId(appParam.getAppId());
+            configLog.setChangeContent(changeContent.toString());
+            configLog.setChangeTime(new Date());
+            configLog.setChangeBy(loginUserId);
+            appMapper.insertConfigLog(configLog);
+        }
     }
 
     /**

+ 103 - 0
yt-middle/middle-platform/src/main/java/com/ytpm/middle/util/BeanChangeUtil.java

@@ -0,0 +1,103 @@
+package com.ytpm.middle.util;
+
+
+import cn.hutool.core.util.StrUtil;
+import com.ytpm.annotation.BeanChange;
+import lombok.extern.slf4j.Slf4j;
+
+import java.lang.reflect.Field;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Objects;
+
+/**
+ * @author lih
+ * @date 2025-11-03 15:55
+ */
+@Slf4j
+public class BeanChangeUtil<T> {
+
+
+    /**
+     * 比较两个实体对象中被@BeanChange注解标记的属性变化
+     *
+     * @param oldBean 旧对象
+     * @param newBean 新对象
+     * @return 变化描述字符串
+     */
+    public static <T> String compareBeans(T oldBean, T newBean) {
+        if (oldBean == null || newBean == null) {
+            return "";
+        }
+        if (!oldBean.getClass().equals(newBean.getClass())) {
+            throw new IllegalArgumentException("两个对象类型不一致");
+        }
+        StringBuilder changeLog = new StringBuilder();
+        Class<?> clazz = oldBean.getClass();
+        Field[] fields = clazz.getDeclaredFields();
+
+        // 遍历所有字段
+        for (Field field : fields) {
+            BeanChange annotation = field.getAnnotation(BeanChange.class);
+            if (annotation == null) {
+                continue;
+            }
+            field.setAccessible(true);
+            try {
+                Object oldValue = field.get(oldBean);
+                Object newValue = field.get(newBean);
+
+                // 比较值是否发生变化
+                if (!Objects.equals(oldValue, newValue)) {
+                    String fieldName = annotation.name();
+                    String formattedOldValue = formatValue(oldValue, annotation);
+                    String formattedNewValue = formatValue(newValue, annotation);
+
+                    // 拼接变化描述
+                    if (changeLog.length() > 0) {
+                        changeLog.append(" ");
+                    }
+                    changeLog.append(StrUtil.format("{}由'{}'修改为'{}';", fieldName, formattedOldValue, formattedNewValue));
+                }
+            } catch (IllegalAccessException e) {
+                // 记录访问异常,继续处理其他字段
+                System.err.println("访问字段失败: " + field.getName() + ", 错误: " + e.getMessage());
+            }
+        }
+
+        return changeLog.toString();
+    }
+
+
+    /**
+     * 格式化属性值
+     *
+     * @param value      属性值
+     * @return 格式化后的字符串
+     */
+    private static String formatValue(Object value, BeanChange annotation) {
+        if (value == null) {
+            return "空";
+        }
+        // 处理Date类型
+        if (value instanceof Date && StrUtil.isNotEmpty(annotation.dateFormat())) {
+            try {
+                SimpleDateFormat sdf = new SimpleDateFormat(annotation.dateFormat());
+                return sdf.format((Date) value);
+            } catch (Exception e) {
+                return value.toString(); // 格式化失败返回原始字符串
+            }
+        }
+        if (value instanceof Integer && annotation.ifYesNo()) {
+            switch ((Integer)value) {
+                case 0:
+                    return "否";
+                case 1:
+                    return "是";
+                default:
+                    return value.toString();
+            }
+        }
+        return value.toString();
+    }
+}

+ 31 - 0
yt-middle/middle-platform/src/main/resources/mapper/AppMapper.xml

@@ -77,6 +77,10 @@
          #{operateTime}
         );
     </insert>
+    <insert id="insertConfigLog">
+        insert into yt_app_config_log(record_id, app_id, change_content, change_by, change_time)
+        values (#{recordId}, #{appId}, #{changeContent}, #{changeBy}, #{changeTime})
+    </insert>
     <update id="updateById">
         update yt_platform_user_app
         <set>
@@ -233,4 +237,31 @@
             #{item}
         </foreach>
     </select>
+    <select id="selectLastConfigLog" resultType="com.ytpm.agent.model.YtAppConfigLog">
+        SELECT
+            l.record_id,
+            l.app_id,
+            l.change_content,
+            l.change_by,
+            l.change_time
+        FROM yt_platform.yt_app_config_log l
+                 INNER JOIN (
+            SELECT
+                app_id,
+                MAX(change_time) as max_change_time
+            FROM yt_platform.yt_app_config_log
+            where app_id in
+            <foreach collection="appIds.split(',')" item="item" separator="," open="(" close=")">
+                #{item}
+            </foreach>
+            GROUP BY app_id
+        ) t ON l.app_id = t.app_id AND l.change_time = t.max_change_time;
+    </select>
+    <select id="selectByPrimaryId" resultType="com.ytpm.agent.model.YtApp">
+        select app_id, app_key, app_name, user_id, app_type, apk_url, qr_code, version_code, update_tips, enabled,
+               store_on_sale, store_type, store_url, package_name, domain, category, sub_category, coppa,
+               screen_orientation, ccpa, feign_path, ditch_id, ditch_name, superior_id, revenue_display_rate
+        from yt_app
+        where app_id = #{appId}
+    </select>
 </mapper>