server.md 24 KB

后端手册

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
  • 校验appkey、平台类型、操作系统版本号、APP系统版本号、APP系统版本名是否存在 ``` java 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;
}

}

2. 根据APP系统版本号或者操作系统版本号进行比对是否进行版本更新
```java
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用户数及灰度发布阶段计算出当前灰度发布可接收到版本更新 ```java /**

    • 处理灰度发布策略 *
    • @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_VERSIONKEYS + 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`
```java
    //校验
    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());
    }