# 后端手册
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系统版本名是否存在
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;
}
}
- 根据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);
}
- 处理灰度发布策略:(部分用户进行版本更新)根据当前版本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());
}