# 后端手册

sql 及 ip解析db放在 sp-app resource目录中 请自行使用

# 启动类

sp/sp-app/src/main/java/com/anji/sp/SpAppApplication.java 

# 公共模块(sp-common)

# 枚举

  • IsDeleteEnum 是否删除枚举
    • 处理数据逻辑删除状态枚举
  • UserStatus 用户状态枚举
    • 数据启用禁用状态枚举

# 工具类

# AESUtil AES加密解密工具类

登录pc 密码通过AES加密 后台拿到加密数据进行解密 得到真实密码 进行后续处理

String content = "my password";
System.out.println("加密前:" + content);
System.out.println("加密密钥和解密密钥:" + KEY);
String encrypt = AESUtil.aesEncrypt(content, KEY);
System.out.println("加密后:" + encrypt);

String decrypt = AESUtil.aesDecrypt(encrypt, KEY);
System.out.println("解密后:" + decrypt);

# APPVersionCheckUtil版本校验工具类

  • 通过拿到本地版本和线上版本进行对比
  • 将本地版本和线上版本进行转数组并转int类型,比对每组的大小
String[] oldArray = oldVersion.replaceAll("[^0-9.]", "").split("[.]");
String[] newArray = newVersion.replaceAll("[^0-9.]", "").split("[.]");
for (int i = 0; i < length; i++) {
    if (Integer.parseInt(newArray[i]) > Integer.parseInt(oldArray[i])) {
        return 1;
    } else if (Integer.parseInt(newArray[i]) < Integer.parseInt(oldArray[i])) {
        return -1;
    }
}
// doSomthing
//  ...

# Constants通用常量信息

包含 用户登录相关、应用相关静态常量

# RSAUtil RSA加密解密工具类

主要是处理APP SDK 请求数据加密解密

// RSA加密
String data = "这是加密的json文件内容";
String encryptData = RSAUtil.encrypt(data, getPublicKey(publicKey));
System.out.println("加密后内容:" + encryptData);
// RSA解密
String decryptData = RSAUtil.decrypt(encryptData, getPrivateKey(privateKey));
System.out.println("解密后内容:" + decryptData);

// RSA签名
String sign = RSAUtil.sign(data, RSAUtil.getPrivateKey(privateKey));
System.out.println("签名:" + sign);
// RSA验签
boolean result = RSAUtil.verify(data, getPublicKey(publicKey), sign);
System.out.print("验签结果:" + result);

# 权限模块(sp-auth)

主要包含 用户登录处理、基础数据配置、菜单权限相关等

# 自定义切面

# AuthorizeAspect 用户菜单权限

通过切面,根据数据用户菜单关联表,赋予用户对于的菜单权限

@Around("authorizePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
    LoginUser loginUser = SecurityUtils.getLoginUser();
    if (loginUser.getUser().getIsAdmin() != 1) {
        // 获得注解
        PreSpAuthorize preSpAuthorize = getAnnotationLog(point);
        if (preSpAuthorize == null) {
            throw new AccessDeniedException("权限不足");
        }
        String params = argsArrayToString(point.getArgs());
        //解析请求参数是否含有appId
        long appId = JSONObject.parseObject(params).getLongValue("appId");
        //权限码
        String value = preSpAuthorize.value();
        //SpUserVO spUserVO
        SpUserVO spUserVO = new SpUserVO();
        spUserVO.setUserId(loginUser.getUser().getUserId());
        spUserVO.setIsAdmin(loginUser.getUser().getIsAdmin());
        Map<Long, Set<String>> longSetMap = permissionService.selectUserMenuPerms(spUserVO);
        loginUser.setPermissions(longSetMap);
        Set<String> strings = loginUser.getPermissions().get(appId);
        if (!hasPermissions(strings, value)) {
            throw new AccessDeniedException("权限不足");
        }
    }
    //执行方法
    return point.proceed();
}

# LogAspect 用户操作行为日志

protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, Long beginTime, Long endTime, Long time) {
    try {
        // 获得注解
        Log controllerLog = getAnnotationLog(joinPoint);
        if (controllerLog == null) {
            return;
        }

        // 获取当前的用户
        LoginUser loginUser = SecurityUtils.getLoginUser();

        // *========数据库日志=========*//
        SpOperLogPO operLog = new SpOperLogPO();
        operLog.setBeginTime(beginTime);
        operLog.setEndTime(endTime);
        operLog.setTime(time);
        // 返回参数
        operLog.setJsonResult(JSON.toJSONString(jsonResult));

        operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
        if (loginUser != null) {
            operLog.setOperName(loginUser.getUsername());
        }

        if (e != null) {
            operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
        }
        // 设置方法名称
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        operLog.setMethod(className + "." + methodName + "()");
        // 设置请求方式
        operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
        // 处理设置注解上的参数
        getControllerMethodDescription(joinPoint, controllerLog, operLog);
        operLog.setOperTime(new Date());
        // 保存数据库
        operLogMapper.insert(operLog);
    } catch (Exception exp) {
        // 记录本地异常日志
        exp.printStackTrace();
    }
}

# 配置

基础配置:包含解决跨域配置、数据库配置、异常配置、MybatisPlus配置、redission配置、安全认证配置、swagger配置等

# RedissonConfig Redisson配置

由于Redisson分布式锁具有简单易用,且支持redis单实例、redis哨兵、redis cluster、redis master-slave等各种部署架构,是分布式锁的一种最佳选择。

//单机
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://" + host + ":" + port)
                .setPassword(password);
        return Redisson.create(config);

# SecurityConfig 安全配置

configure 匹配所有请求路径

    protected void configure(HttpSecurity httpSecurity) throws Exception {
}

# 接口层

包含登录、字典表、应用表、菜单表、角色菜单关联表、用户应用角色表等接口调用

# LoginController登录

  • 用户登录接口:/login/v1

# SpApplicationController应用接口

  • 查询所有应用:/select/v1
  • 分页查询所有应用:/selectByPage/v1
  • 新建应用:/insert/v1
  • 更新应用名:/update/v1
  • 删除应用:/delete/v1

# SpDictController字典接口

  • 分页进行查询字典表:/selectByPage/v1
  • 根据字典类型查询字典表:/selectByType/v1
  • 插入iOS或Android字典类型:/insert/v1
  • 检查iOS或Android字典类型的值是否可编辑或删除:/checkVersion/v1
  • 根据版本id更新iOS或Android字典类型的值:/updateById/v1
  • 删除iOS或Android字典类型的值:/deleteById/v1

# SpMenuController菜单接口

  • 查询所有菜单:/select/v1
  • 根据父级id查询菜单:/selectByParentId/v1

# SpRoleController角色接口

  • 查询所有角色:/select/v1
  • 根据角色id删除角色:/deleteByRoleId/v1
  • 新增角色及菜单:/insertRoleAndMenu/v1
  • 根据角色id更新角色:/update/v1
  • 根据项目id查询未加入对应应用管理的用户:/selectNoJoinApplicationByAppId/v1

# SpRoleMenuController角色菜单关联接口

  • 根据角色roleId查询对应菜单:/selectByRoleId/v1
  • 更新角色菜单关联表:/update/v1
  • 根据角色id删除角色菜单表:/delete/v1

# SpUserAppRoleController用户应用角色关联

  • 分页查询关联关系用户信息:/selectByAppId/v1
  • 新增用户应用角色关联:/insert/v1
  • 删除用户项目关联表数据:/delete/v1
  • 更改项目用户角色:/update/v1
  • 根据用户查询项目信息:/selectAppInfo/v1
  • 根据appId和userId 查询菜单信息:/selectMenuPermissionsByAppIDAndUserId/v1

# UserController用户接口

  • 新增用户:/addUser/v1
  • 用户列表:/queryByPage/v1
  • 更新用户:/updateUserById/v1
  • 删除用户:/deleteUserById/v1

# 枚举

主要包含 返回应答码枚举类

SUCCESS("0000", "成功"),
ERROR("0001", "操作失败"),
EXCEPTION("9999", "服务器内部异常"),
VERSION_EXIST("1101", "版本已存在"),
VERSION_INSERT_FAILURE("1102", "添加失败"),
APP_EXIST("1103", "项目已存在"),
APP_NOT_EXIST("1103", "项目不存在"),
NOT_OPERATION("1104", "该用户不是管理员,无法操作"),
...

# 工具类

# IPUntils 解析类

由于 spring boot打包后在服务器上无法读取resources下文件 本地和服务器使用ip2region.db解析在getCityInfo方法中需要配置对应的目录

public static String getCityInfo(String ip) {
        //db 本地资源 db
//        String dbPath = IPUntils.class.getResource("/ip/ip2region.db").getPath();
        String dbPath = "/app/ip2region.db";
        File file = new File(dbPath);
        if (file.exists() == false) {
            log.info("Error: Invalid ip2region.db file");
            return null;
        }
        // doSomething
        // ...
}

# SecurityUtils安全服务工具类

主要包含:

  • 获取用户信息
  • 获取用户id
  • 获取用户账户
  • 获取Authentication

# 版本管理模块(sp-version)

# 上传功能

接口: /uploadFile/v1

  • 根据上传的apk文件进行解析
  • 文件写入使用MultipartFil
  • 文件路径后缀保存格式:包名+版本名+上传时间+版本号+appkey+.apk

# 版本管理

  • 根据应用id查询所有版本信息:/select/v1
  • 版本新增:/insert/v1
  • 版本编辑:/update/v1
  • 根据id启用/禁用版本:/enable/v1
  • 版本删除:/delete/v1

核心:

iOS新增通过versionName进行对比 Android新增通过versionName和versionNumber进行对比 强制更新可用通过app版本号或者操作系统版本号进行比对

 if (APPVersionCheckUtil.compareAppVersion(versionInfo.getVersionName(), reqData.getVersionName()) < 1) {
    return ResponseModel.errorMsg("版本名不能低于" + versionInfo.getVersionName());
}

if ("Android".contains(reqData.getPlatform()) && Integer.parseInt(versionInfo.getVersionNumber()) >= Integer.parseInt(reqData.getVersionNumber())) {
    return ResponseModel.errorMsg("版本号不能低于" + versionInfo.getVersionNumber());
}

// app系统版本号入 1.0.0, 1.0.1...
// 操作系统版本号入 Android8,Android9,Android10...

灰度发布:

灰度发布分为7天,从0开始到100%, 如果在7天之内某一天禁用灰度发布功能, 灰度已用、可用发布时间将会暂停,等再次开启时将继续计时

/**
 * 更新版本时间
 * @param vo
 * @return
 */
@Override
public int updateSpVersionTime(SpVersionVO vo) {
    //返回自从GMT 1970-01-01 00:00:00到此date对象上时间的毫秒数
    Long enableTimeStamp = vo.getEnableTime().getTime();
    //当前时间戳 毫秒数
    Long nowTimeStamp = System.currentTimeMillis();
    //相差时间戳
    Long gap = nowTimeStamp - enableTimeStamp;

    //已用时间
    Long canaryReleaseUseTime = vo.getOldCanaryReleaseUseTime();
    //新已用时间
    Long newCanaryReleaseUseTime = canaryReleaseUseTime + gap;
    if (newCanaryReleaseUseTime < 0) {
        newCanaryReleaseUseTime = 0L;
    }
    vo.setCanaryReleaseUseTime(newCanaryReleaseUseTime);
    return updateAppVersion(vo);
}

# 公告管理模块(sp-notices)

核心:

后台系统会根据当前时间判断公告时间是否过期,将用户提供在有效期内的公告信息 为移动端提供一个公告管理中心

# 公告接口

  • 分页根据应用id分页查询公告信息:/select/v1
  • 新增公告信息:/insert/v1
  • 编辑公告信息:/update/v1
  • 根据id启用/禁用公告信息:/enable/v1
  • 删除公告信息:/delete/v1

# 清除过期公告

<mapper namespace="com.anji.sp.mapper.SpNoticeMapper">
    <update id="setNoticeEnableInvalid">
        update sp_notice set enable_flag = 0 where end_time <![CDATA[<]]> sysdate()
    </update>
</mapper>

# APP调用模块(sp-app)

appsdk 调用移动服务平台的核心功能是版本更新和公告,故这部分单独抽离出来 通过RSA加密解码获取用户数据校验

if (needDecrypt) {
    //------------------RSA解密-------------------
    log.info("spApplicationPO -- > {}", spApplicationPO);
    //解密
    String decryptData = RSAUtil.decrypt(spAppReqDataVO.getSign(), RSAUtil.getPrivateKey(spApplicationPO.getPrivateKey()));
    log.info("解密decryptData -- > {}", decryptData);
    spAppLogVO = JSON.parseObject(decryptData, SpAppLogVO.class);
    log.info("vo -- > {}", vo);
    log.info("解密解析 spAppLogVO -- > {}", spAppLogVO);
    //解密时效
    if (Objects.isNull(spAppLogVO)) {
        responseModel.setRepCodeEnum(RepCodeEnum.DATA_PARSING_INVALID);
        return responseModel;
    }
    //appKey不一致
    if (!vo.getAppKey().equals(spAppLogVO.getAppKey())) {
        responseModel.setRepCodeEnum(RepCodeEnum.INVALID_FORMAT_APP_KEY);
        return responseModel;
    }
    //设备id不一致
    if (!vo.getDeviceId().equals(spAppLogVO.getDeviceId())) {
        responseModel.setRepCodeEnum(RepCodeEnum.DATA_REQUEST_INVALID);
        return responseModel;
    }
    //-----------------------------------------------------------
}

# 初始化

  • 接口 /deviceInit 初始化操作 异步保存初始化log
 //校验
ResponseModel responseModel = checkReq(spAppReqDataVO, request, "1", "初始化");
//不通过返回
if (!responseModel.isSuccess()) {
    return responseModel;
}
SpAppReqVO reqVO = (SpAppReqVO) responseModel.getRepData();
int i = spAppLogMapper.insert(reqVO.getSpAppLogPO());
CompletableFuture.supplyAsync(() -> spAppDeviceService.updateDeviceInfo(reqVO.getSpAppLogPO()));

if (i > 0) {
    return ResponseModel.success();
}
return ResponseModel.errorMsg("初始化失败");

# 版本更新

  • 接口 /appVersion
  1. 校验appkey、平台类型、操作系统版本号、APP系统版本号、APP系统版本名是否存在
if (StringUtils.isEmpty(spAppLogPO.getPlatform())) {
    return ResponseModel.errorMsg("获取失败");
}
if (StringUtils.isEmpty(spAppLogPO.getOsVersion())) {
    responseModel.setRepCodeEnum(RepCodeEnum.OS_VERSION_INVALID);
    return responseModel;
}

if ("Android".equals(spAppLogPO.getPlatform())) {
    if (StringUtils.isEmpty(spAppLogPO.getVersionCode())) {
        responseModel.setRepCodeEnum(RepCodeEnum.APP_VERSION_INVALID);
        return responseModel;
    }
}

if ("iOS".equals(spAppLogPO.getPlatform())) {
    if (StringUtils.isEmpty(spAppLogPO.getVersionName())) {
        responseModel.setRepCodeEnum(RepCodeEnum.APP_VERSION_INVALID);
        return responseModel;
    }
}
  1. 根据APP系统版本号或者操作系统版本号进行比对是否进行版本更新
if (Objects.nonNull(vo)) {
    SpVersionAppTempVO spVersionAppTempVO = new SpVersionAppTempVO();
    BeanUtils.copyProperties(vo, spVersionAppTempVO);
    //os版本号
    String osVersion = APPVersionCheckUtil.getOSVersion(spAppLogPO.getOsVersion()) + "";
    SpVersionForAPPVO spVersionForAPPVO = new SpVersionForAPPVO();
    BeanUtils.copyProperties(spVersionAppTempVO, spVersionForAPPVO);
    //版的数据中的版本号是否大于接口传过来的版本号


    boolean showUpdate = false;
    if ("Android".equals(spAppLogPO.getPlatform())) {
        //数据中的versionNumber是否大于接口传过来的versionCode
        showUpdate = Integer.parseInt(vo.getVersionNumber().trim()) > Integer.parseInt(spAppLogPO.getVersionCode().trim());
    }

    if ("iOS".equals(spAppLogPO.getPlatform())) {
        int v = APPVersionCheckUtil.compareAppVersion(spAppLogPO.getVersionName(), spVersionAppTempVO.getVersionName());
        log.info("compareAppVersion {}", v);
        showUpdate = v > 0;
    }

    spVersionForAPPVO.setShowUpdate(showUpdate);
    //如果不需要更新 强制更新也为false 否则进行处理
    if (!showUpdate) {
        spVersionForAPPVO.setMustUpdate(false);
    } else {
        //[1.1.1,1.1.2,1.1.3]
        if (Objects.isNull(vo.getNeedUpdateVersionList())) {
            vo.setNeedUpdateVersionList(new ArrayList<>());
        }
        //[10,11,12]
        if (Objects.isNull(vo.getVersionConfigStrList())) {
            vo.setVersionConfigStrList(new ArrayList<>());
        }
        spVersionForAPPVO.setMustUpdate(vo.getNeedUpdateVersionList().contains(spAppLogPO.getVersionName())
                || vo.getVersionConfigStrList().contains(osVersion));
//                        spVersionForAPPVO.setMustUpdate(spVersionAppTempVO.getVersionConfig().contains(osVersion));
    }
    return ResponseModel.successData(spVersionForAPPVO);
}
  1. 处理灰度发布策略:(部分用户进行版本更新)根据当前版本APP用户数及灰度发布阶段计算出当前灰度发布可接收到版本更新
/**
     * 处理灰度发布策略
     *
     * @param spAppLogVO
     * @param vo
     * @return 是否可以发送数据
     */
    private boolean canaryReleaseConfig(SpAppLogVO spAppLogVO, SpVersionVO vo) {

        //1、接口信息及版本信息 判断 deviceId、appkey、platform是否存在  不存在 不返回信息
        if (Objects.isNull(vo) || Objects.isNull(spAppLogVO)
                || StringUtils.isEmpty(spAppLogVO.getDeviceId())
                || StringUtils.isEmpty(spAppLogVO.getAppKey())
                || StringUtils.isEmpty(spAppLogVO.getPlatform())) {
            return false;
        }
        //2、Android
        if ("Android".equals(spAppLogVO.getPlatform())) {
            //数据中的versionNumber是否大于接口传过来的versionCode
            //app版本是否大于等于数据版本 跳过 返回信息
            if (StringUtils.isNotEmpty(spAppLogVO.getVersionName())
                    && StringUtils.isNotEmpty(vo.getVersionName())
                    && Integer.parseInt(vo.getVersionNumber().trim()) <= Integer.parseInt(spAppLogVO.getVersionCode().trim())) {
                return true;
            }
        }
        // 3、iOS
        if ("iOS".equals(spAppLogVO.getPlatform())) {
            // APP版本>= 数据版本 跳过
            if (StringUtils.isNotEmpty(spAppLogVO.getVersionName())
                    && StringUtils.isNotEmpty(vo.getVersionName())
                    && APPVersionCheckUtil.compareAppVersion(vo.getVersionName(), spAppLogVO.getVersionName()) > -1) {
                return true;
            }
        }

        //4、不开启灰度发布直接跳过 展示数据
        //开启时间没有直接跳过 展示数据
        //灰度发布时间超过7天直接跳过 展示数据
        if (vo.getCanaryReleaseEnable() == UserStatus.DISABLE.getIntegerCode()
                || Objects.isNull(vo.getEnableTime())
                || vo.getCanaryReleaseUseTime() > defaultTimeStamp) {
            return true;
        }

        //5、读取Redis中对应 appKey_versionName  为key是否包含对应deviceID
        String cacheKey = Constants.APP_VERSION_KEYS + spAppLogVO.getAppKey() + "_" + spAppLogVO.getPlatform() + "_" + vo.getVersionName();
        RLock redissionLock = redissonClient.getLock(cacheKey);
        try {
            redissionLock.lock(30, TimeUnit.SECONDS);
            //返回自从GMT 1970-01-01 00:00:00到此date对象上时间的毫秒数
            Long enableTimeStamp = vo.getEnableTime().getTime();
            //当前时间戳 毫秒数
            Long nowTimeStamp = System.currentTimeMillis();
            //相差时间戳
            Long gap = nowTimeStamp - enableTimeStamp;
            //已用时间
            Long canaryReleaseUseTime = vo.getOldCanaryReleaseUseTime();
            //新已用时间
            Long newCanaryReleaseUseTime = canaryReleaseUseTime + gap;
            if (newCanaryReleaseUseTime < 0) {
                newCanaryReleaseUseTime = 0L;
            }

            //未用时间
            Long nowGap = defaultTimeStamp - newCanaryReleaseUseTime;
            //如果小于0 代表灰度发布已结束
            if (nowGap < 0) {
                spVersionService.updateSpVersionTime(vo);
                return true;
            }

            //灰度发布确认
            if (Objects.nonNull(vo.getCanaryReleaseStageList()) && vo.getCanaryReleaseStageList().size() == 7) {
                //已用时间除以每天的时间戳 得到当前是第几天
                int c = (new Long(newCanaryReleaseUseTime).intValue()) / defaultTimeStampOneDay;
                //1、拿到百分比 比如0.4 (数据库)
                double percentage = Integer.parseInt(vo.getCanaryReleaseStageList().get(c)) / 100.0;
                //2、如果大于1 全部发布 运行请求
                if (percentage >= 1) {
                    spVersionService.updateSpVersionTime(vo);
                    return true;
                }
                //3、查询当前appKey所有deviceID(去重) count

                Long deviceIdCount = Long.valueOf(spAppDeviceService.selectCount(spAppLogVO));
                //4、count * 0.4 取整
                int reqCount = new Double((new Long(deviceIdCount).intValue()) * percentage).intValue();
                //如果数值少于1,也代表全部
                if (reqCount < 1) {
                    spVersionService.updateSpVersionTime(vo);
                    return true;
                }
                log.info("sql cacheKey: {}", cacheKey);

                log.info("sql deviceIdCount: {}", deviceIdCount);

                log.info("sql reqCount: {}", reqCount);

                QueryWrapper<SpAppReleasePO> queryWrapper = new QueryWrapper<>();
                queryWrapper.eq("app_key", spAppLogVO.getAppKey());
                queryWrapper.eq("version_name", vo.getVersionName());
                //查询灰度发布已经收到版本更新接口的用户设备唯一标识表
                List<SpAppReleasePO> spAppReleasePOS = spAppReleaseMapper.selectList(queryWrapper);
                log.info("sql spAppReleasePOS: {}", spAppReleasePOS);

                //将列表唯一标示转换为 string list
                List<String> cacheList =  spAppReleasePOS.stream().map(s -> s.getDeviceId()).collect(Collectors.toList());
                log.info("sql cacheList: {}", cacheList);

                //为空代表没有数据需要添加
                if (StringUtils.isEmpty(cacheList) || cacheList.size() == 0) {
                    int i = insertReleasePO(spAppLogVO, vo);
                    log.info("insertReleasePO: {}", i);
                    spVersionService.updateSpVersionTime(vo);
                    return true;
                }

                //如果不为空 判断
                //6、如果包含 运行拿到数据 return 1
                if (cacheList.contains(spAppLogVO.getDeviceId())) {
                    return true;
                }

                //7、如果不包含 Redis中数据条数与灰度数目进行比较  r 和 c
                //8、如果 r > c return 0 不允许返回
                if (cacheList.size() >= reqCount) {
                    return false;
                }
                //9、如果 r < c  返回1   并保持到Redis中
                int i = insertReleasePO(spAppLogVO, vo);
                log.info("insertReleasePO: {}", i);

                //10、最后将 newCanaryReleaseUseTime 更新到version数据表中
                spVersionService.updateSpVersionTime(vo);
                return true;
            }
            return false;
        } finally {
            redissionLock.unlock();
        }
    }

# 公告

  • 接口 /appNotice
    //校验
    ResponseModel responseModel = checkReq(spAppReqDataVO, request, "3", "公告信息");
    //不通过返回
    if (!responseModel.isSuccess()) {
        return responseModel;
    }
    SpAppReqVO reqVO = (SpAppReqVO) responseModel.getRepData();
    SpApplicationPO spApplicationPO = reqVO.getSpApplicationPO();
    //异步保存log
    CompletableFuture.supplyAsync(() -> spAppLogMapper.insert(reqVO.getSpAppLogPO()));
    CompletableFuture.supplyAsync(() -> spAppDeviceService.updateDeviceInfo(reqVO.getSpAppLogPO()));
    try {
        SpNoticeVO spNoticeVO = new SpNoticeVO();
        spNoticeVO.setAppId(spApplicationPO.getAppId());
        List<SpNoticeForAppVO> notices = spNoticeService.getNotices(spNoticeVO);
        return ResponseModel.successData(notices);
    } catch (Exception e) {
        return ResponseModel.errorMsg(e.getMessage());
    }