sql 及 ip解析db放在 sp-app resource目录中 请自行使用
sp/sp-app/src/main/java/com/anji/sp/SpAppApplication.java
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版本校验工具类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);
主要包含 用户登录处理、基础数据配置、菜单权限相关等
自定义切面
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/insert/v1/checkVersion/v1/updateById/v1/deleteById/v1SpMenuController菜单接口/select/v1/selectByParentId/v1SpRoleController角色接口/select/v1/deleteByRoleId/v1/insertRoleAndMenu/v1/update/v1/selectNoJoinApplicationByAppId/v1SpRoleMenuController角色菜单关联接口/selectByRoleId/v1/update/v1/delete/v1SpUserAppRoleController用户应用角色关联/selectByAppId/v1/insert/v1/delete/v1/update/v1/selectAppInfo/v1/selectMenuPermissionsByAppIDAndUserId/v1UserController用户接口/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安全服务工具类主要包含:
Authentication接口: /uploadFile/v1
MultipartFil/select/v1/insert/v1/update/v1/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); }
核心:
后台系统会根据当前时间判断公告时间是否过期,将用户提供在有效期内的公告信息 为移动端提供一个公告管理中心
/select/v1/insert/v1/update/v1/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>
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("初始化失败");
/appVersionif ("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);
}
处理灰度发布策略:(部分用户进行版本更新)根据当前版本APP用户数及灰度发布阶段计算出当前灰度发布可接收到版本更新 ```java /**
@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());
}