Merge branch 'master' of https://gitea.zianxin.com/zax/biz-api-cloud-zax
This commit is contained in:
commit
bc0f21a6f6
18 files changed
+499
-14
No files matched your search
+8
-7
@@ -1,6 +1,5 @@
|
||||
package com.bsd.pjcloud.canteen.controller;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
@@ -9,10 +8,10 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.canteen.entity.CanteenCategoryEntity;
|
||||
import com.bsd.pjcloud.canteen.service.CanteenCategoryService;
|
||||
import com.bsd.pjcloud.canteen.util.CanteenMerchantScopeHelper;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.excel.annotation.ResponseExcel;
|
||||
import com.bsd.pjcloud.common.log.annotation.SysLog;
|
||||
import com.bsd.pjcloud.common.security.util.SecurityUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
@@ -40,6 +39,8 @@ public class CanteenCategoryController {
|
||||
|
||||
private final CanteenCategoryService canteenCategoryService;
|
||||
|
||||
private final CanteenMerchantScopeHelper merchantScopeHelper;
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
*
|
||||
@@ -53,6 +54,7 @@ public class CanteenCategoryController {
|
||||
public R getCanteenCategoryPage(@ParameterObject Page page, @ParameterObject CanteenCategoryEntity canteenCategory) {
|
||||
LambdaQueryWrapper<CanteenCategoryEntity> wrapper = Wrappers.lambdaQuery();
|
||||
wrapper.like(StrUtil.isNotBlank(canteenCategory.getName()), CanteenCategoryEntity::getName, canteenCategory.getName());
|
||||
merchantScopeHelper.applyMerchantIdScope(wrapper, canteenCategory.getMerchantId(), CanteenCategoryEntity::getMerchantId);
|
||||
return R.ok(canteenCategoryService.selectPageByScope(page, wrapper));
|
||||
}
|
||||
|
||||
@@ -81,7 +83,6 @@ public class CanteenCategoryController {
|
||||
@PostMapping
|
||||
@PreAuthorize("@pms.hasPermission('canteen_canteenCategory_add')")
|
||||
public R save(@RequestBody CanteenCategoryEntity canteenCategory) {
|
||||
canteenCategory.setDeptId(SecurityUtils.getUser().getDeptId());
|
||||
return R.ok(canteenCategoryService.create(canteenCategory));
|
||||
}
|
||||
|
||||
@@ -143,14 +144,14 @@ public class CanteenCategoryController {
|
||||
public R<List<CanteenCategoryEntity>> getAllSimple(
|
||||
@RequestParam(value = "categoryType", required = false) String categoryType,
|
||||
@RequestParam(value = "name", required = false) String name,
|
||||
@RequestParam(value = "merchantId", required = false) String merchantId
|
||||
@RequestParam(value = "merchantId", required = false) Long merchantId
|
||||
) {
|
||||
LambdaQueryWrapper<CanteenCategoryEntity> wrapper = Wrappers.lambdaQuery();
|
||||
wrapper.like(StringUtils.isNotBlank(name), CanteenCategoryEntity::getName, name);
|
||||
wrapper.eq(ObjectUtil.isNotEmpty(merchantId), CanteenCategoryEntity::getMerchantId, merchantId);
|
||||
merchantScopeHelper.applyMerchantIdScope(wrapper, merchantId, CanteenCategoryEntity::getMerchantId);
|
||||
wrapper.orderByAsc(CanteenCategoryEntity::getSort);
|
||||
wrapper.apply(StringUtils.isNotEmpty(categoryType), "FIND_IN_SET({0}, your_column_name)", categoryType);
|
||||
List<CanteenCategoryEntity> list = canteenCategoryService.list(wrapper);
|
||||
wrapper.apply(StringUtils.isNotEmpty(categoryType), "FIND_IN_SET({0}, category_type)", categoryType);
|
||||
List<CanteenCategoryEntity> list = canteenCategoryService.listScope(wrapper);
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -148,8 +148,6 @@ public class CanteenSpuController {
|
||||
@PreAuthorize("@pms.hasPermission('canteen_canteenSpu_add')")
|
||||
@Transactional
|
||||
public R save(@RequestBody CanteenSpuEntity canteenSpu) {
|
||||
Long deptId = SecurityUtils.getUser().getDeptId();
|
||||
canteenSpu.setDeptId(deptId);
|
||||
try {
|
||||
boolean save = canteenSpuService.saveDeep(canteenSpu);
|
||||
CanteenStockEntity canteenStockEntity = new CanteenStockEntity();
|
||||
@@ -159,6 +157,7 @@ public class CanteenSpuController {
|
||||
canteenStockEntity.setInventoryUpperLimit(0);
|
||||
canteenStockEntity.setInventoryLowerLimit(0);
|
||||
canteenStockEntity.setCurrentStock(0D);
|
||||
Long deptId = canteenSpu.getDeptId() != null ? canteenSpu.getDeptId() : SecurityUtils.getUser().getDeptId();
|
||||
canteenStockEntity.setDeptId(deptId);
|
||||
canteenStockService.save(canteenStockEntity);
|
||||
return R.ok(save);
|
||||
|
||||
+7
-1
@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bsd.pjcloud.canteen.entity.CanteenCategoryEntity;
|
||||
import com.bsd.pjcloud.canteen.mapper.CanteenCategoryMapper;
|
||||
import com.bsd.pjcloud.canteen.service.CanteenCategoryService;
|
||||
import com.bsd.pjcloud.canteen.util.CanteenMerchantScopeHelper;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScope;
|
||||
import com.bsd.pjcloud.mqtt.api.feign.RemoteMqttEventService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -27,6 +28,8 @@ public class CanteenCategoryServiceImpl extends ServiceImpl<CanteenCategoryMappe
|
||||
|
||||
private final RemoteMqttEventService remoteMqttEventService;
|
||||
|
||||
private final CanteenMerchantScopeHelper merchantScopeHelper;
|
||||
|
||||
@Override
|
||||
public Page<CanteenCategoryEntity> selectPageByScope(Page page, LambdaQueryWrapper<CanteenCategoryEntity> wrapper) {
|
||||
return baseMapper.selectPageByScope(page, wrapper, DataScope.of());
|
||||
@@ -39,6 +42,7 @@ public class CanteenCategoryServiceImpl extends ServiceImpl<CanteenCategoryMappe
|
||||
|
||||
@Override
|
||||
public Long create(CanteenCategoryEntity canteenCategory) {
|
||||
merchantScopeHelper.fillDeptIdByMerchantId(canteenCategory.getMerchantId(), canteenCategory::setDeptId);
|
||||
baseMapper.insert(canteenCategory);
|
||||
// 发送mq消息
|
||||
remoteMqttEventService.sendCanteenClassifyChangeMsg(canteenCategory.getDeptId());
|
||||
@@ -49,9 +53,11 @@ public class CanteenCategoryServiceImpl extends ServiceImpl<CanteenCategoryMappe
|
||||
public Boolean edit(CanteenCategoryEntity canteenCategory) {
|
||||
CanteenCategoryEntity detail = baseMapper.selectById(canteenCategory.getId());
|
||||
if (ObjectUtil.isEmpty(detail)) throw new RuntimeException("菜品分类不存在");
|
||||
merchantScopeHelper.fillDeptIdByMerchantId(canteenCategory.getMerchantId(), canteenCategory::setDeptId);
|
||||
baseMapper.updateById(canteenCategory);
|
||||
// 发送mq消息
|
||||
remoteMqttEventService.sendCanteenClassifyChangeMsg(detail.getDeptId());
|
||||
remoteMqttEventService.sendCanteenClassifyChangeMsg(
|
||||
canteenCategory.getDeptId() != null ? canteenCategory.getDeptId() : detail.getDeptId());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+9
@@ -20,6 +20,7 @@ import com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO;
|
||||
import com.bsd.pjcloud.canteen.vo.erp.CanteenSpuSimpleVO;
|
||||
import com.bsd.pjcloud.canteen.vo.erp.CanteenSpuStockVO;
|
||||
import com.bsd.pjcloud.canteen.vo.ADCanteenSpuListVO;
|
||||
import com.bsd.pjcloud.canteen.util.CanteenMerchantScopeHelper;
|
||||
import com.bsd.pjcloud.common.core.util.BeanUtils;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScope;
|
||||
@@ -49,6 +50,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
private final CanteenStockMapper canteenStockMapper;
|
||||
private final CanteenOnlineSpuMapper canteenOnlineSpuMapper;
|
||||
private final CanteenSpuComboItemMapper comboItemMapper;
|
||||
private final CanteenMerchantScopeHelper merchantScopeHelper;
|
||||
|
||||
@Override
|
||||
public Object selectFoodList(LambdaQueryWrapper<CanteenSpuEntity> wrapper) {
|
||||
@@ -125,6 +127,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
LambdaQueryWrapper<CanteenSpuEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.like(StrUtil.isNotBlank(canteenSpu.getName()), CanteenSpuEntity::getName, canteenSpu.getName());
|
||||
wrapper.eq(Objects.nonNull(canteenSpu.getCategoryId()), CanteenSpuEntity::getCategoryId, canteenSpu.getCategoryId());
|
||||
merchantScopeHelper.applyMerchantIdScope(wrapper, canteenSpu.getMerchantId(), CanteenSpuEntity::getMerchantId);
|
||||
wrapper.orderByAsc(CanteenSpuEntity::getSort).orderByDesc(CanteenSpuEntity::getId);
|
||||
|
||||
Page pageData = baseMapper.selectPageByScope(page, wrapper, DataScope.of());
|
||||
@@ -154,6 +157,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
List<Long> caIds = canteenCategoryEntities.stream().map(CanteenCategoryEntity::getId).collect(Collectors.toList());
|
||||
|
||||
wrapper.in(!CollectionUtils.isEmpty(caIds), CanteenSpuEntity::getCategoryId, caIds);
|
||||
merchantScopeHelper.applyMerchantIdScope(wrapper, canteenDailySpuDTO.getMerchantId(), CanteenSpuEntity::getMerchantId);
|
||||
// wrapper.orderByAsc(CanteenSpuEntity::getSort);
|
||||
return baseMapper.selectPageByScope(page, wrapper, DataScope.of().only(true));
|
||||
|
||||
@@ -252,10 +256,13 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
LambdaQueryWrapper<CanteenSpuEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.like(StrUtil.isNotBlank(canteenSpu.getName()), CanteenSpuEntity::getName, canteenSpu.getName());
|
||||
wrapper.eq(Objects.nonNull(canteenSpu.getCategoryId()), CanteenSpuEntity::getCategoryId, canteenSpu.getCategoryId());
|
||||
merchantScopeHelper.applyMerchantIdScope(wrapper, canteenSpu.getMerchantId(), CanteenSpuEntity::getMerchantId);
|
||||
wrapper.orderByAsc(CanteenSpuEntity::getSort).orderByDesc(CanteenSpuEntity::getId);
|
||||
|
||||
LambdaQueryWrapper<CanteenOnlineSpuEntity> canteenOnlineSpuEntityLambdaQueryWrapper = new LambdaQueryWrapper<>();
|
||||
canteenOnlineSpuEntityLambdaQueryWrapper.isNotNull(CanteenOnlineSpuEntity::getLinkSpuId);
|
||||
merchantScopeHelper.applyMerchantIdScope(canteenOnlineSpuEntityLambdaQueryWrapper, canteenSpu.getMerchantId(),
|
||||
CanteenOnlineSpuEntity::getMerchantId);
|
||||
List<CanteenOnlineSpuEntity> canteenOnlineSpuEntities = canteenOnlineSpuMapper.selectListByScope(canteenOnlineSpuEntityLambdaQueryWrapper, DataScope.of());
|
||||
Set<Long> spuIds = canteenOnlineSpuEntities.stream().map(CanteenOnlineSpuEntity::getLinkSpuId).collect(Collectors.toSet());
|
||||
|
||||
@@ -288,6 +295,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
public Boolean saveDeep(CanteenSpuEntity entity) {
|
||||
normalizeSpuType(entity);
|
||||
validateCombo(entity, null);
|
||||
merchantScopeHelper.fillDeptIdByMerchantId(entity.getMerchantId(), entity::setDeptId);
|
||||
baseMapper.insert(entity);
|
||||
saveComboItems(entity.getId(), entity.getSpuType(), entity.getComboItems());
|
||||
return Boolean.TRUE;
|
||||
@@ -298,6 +306,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
public Boolean updateDeep(CanteenSpuEntity entity) {
|
||||
normalizeSpuType(entity);
|
||||
validateCombo(entity, entity.getId());
|
||||
merchantScopeHelper.fillDeptIdByMerchantId(entity.getMerchantId(), entity::setDeptId);
|
||||
baseMapper.updateById(entity);
|
||||
comboItemMapper.delete(Wrappers.<CanteenSpuComboItemEntity>lambdaQuery()
|
||||
.eq(CanteenSpuComboItemEntity::getComboSpuId, entity.getId()));
|
||||
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
package com.bsd.pjcloud.canteen.util;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysDept;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysRole;
|
||||
import com.bsd.pjcloud.admin.api.feign.RemoteDataScopeService;
|
||||
import com.bsd.pjcloud.admin.api.feign.RemoteDeptService;
|
||||
import com.bsd.pjcloud.common.core.constant.CommonRoleConstant;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.core.util.RetOps;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScopeTypeEnum;
|
||||
import com.bsd.pjcloud.common.security.service.PjcloudUser;
|
||||
import com.bsd.pjcloud.common.security.util.SecurityUtils;
|
||||
import com.bsd.pjcloud.wallet.api.feign.RemoteMchService;
|
||||
import com.bsd.pjcloud.wallet.api.vo.MchManageVo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 食堂后台商户数据隔离:数据权限为「全部」或 ROLE_ADMIN 可见全部;其余账号仅可见本部门及下级关联商户。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CanteenMerchantScopeHelper {
|
||||
|
||||
private final RemoteMchService remoteMchService;
|
||||
|
||||
private final RemoteDeptService remoteDeptService;
|
||||
|
||||
private final RemoteDataScopeService remoteDataScopeService;
|
||||
|
||||
/**
|
||||
* 是否可查看全部商户数据(管理员 / 数据权限=全部)
|
||||
*/
|
||||
public boolean canViewAllMerchants() {
|
||||
PjcloudUser user = SecurityUtils.getUser();
|
||||
if (user == null) {
|
||||
return false;
|
||||
}
|
||||
List<Long> roleIds = SecurityUtils.getRoleIds();
|
||||
if (CollUtil.isEmpty(roleIds)) {
|
||||
return false;
|
||||
}
|
||||
List<SysRole> roleList = RetOps.of(remoteDataScopeService.getRoleList(roleIds))
|
||||
.getData()
|
||||
.orElseGet(Collections::emptyList);
|
||||
if (CollUtil.isEmpty(roleList)) {
|
||||
return false;
|
||||
}
|
||||
for (SysRole role : roleList) {
|
||||
if (role == null) {
|
||||
continue;
|
||||
}
|
||||
if (StrUtil.equals(role.getRoleCode(), CommonRoleConstant.ROLE_ADMIN)) {
|
||||
return true;
|
||||
}
|
||||
if (Objects.equals(role.getDsType(), DataScopeTypeEnum.ALL.getType())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前登录用户可访问的食堂商户 ID 列表(部门及下级 → mch_manage)
|
||||
*/
|
||||
public List<Long> resolveAllowedMerchantIds() {
|
||||
PjcloudUser user = SecurityUtils.getUser();
|
||||
if (user == null || user.getDeptId() == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Long rootDeptId = user.getDeptId();
|
||||
List<Long> deptIds;
|
||||
R<List<SysDept>> deptR = remoteDeptService.getDescendantList(rootDeptId);
|
||||
if (deptR != null && deptR.isOk() && CollUtil.isNotEmpty(deptR.getData())) {
|
||||
deptIds = deptR.getData().stream().map(SysDept::getDeptId).filter(Objects::nonNull).distinct().toList();
|
||||
}
|
||||
else {
|
||||
deptIds = List.of(rootDeptId);
|
||||
}
|
||||
R<List<MchManageVo>> mchR = remoteMchService.getByDeptIds(deptIds);
|
||||
if (mchR == null || !mchR.isOk() || CollUtil.isEmpty(mchR.getData())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return mchR.getData().stream()
|
||||
.map(MchManageVo::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表查询商户隔离:管理员可不传 merchantId 看全部;非管理员强制限制在可访问商户内。
|
||||
*
|
||||
* @param queryMerchantId 前端可选筛选商户;非管理员时若传了且不在允许范围则查空
|
||||
*/
|
||||
public <T> void applyMerchantIdScope(LambdaQueryWrapper<T> wrapper, Long queryMerchantId,
|
||||
SFunction<T, ?> merchantIdColumn) {
|
||||
if (canViewAllMerchants()) {
|
||||
if (queryMerchantId != null) {
|
||||
wrapper.eq(merchantIdColumn, queryMerchantId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
List<Long> allowed = resolveAllowedMerchantIds();
|
||||
if (CollUtil.isEmpty(allowed)) {
|
||||
wrapper.apply("1 = 0");
|
||||
return;
|
||||
}
|
||||
if (queryMerchantId != null) {
|
||||
if (allowed.contains(queryMerchantId)) {
|
||||
wrapper.eq(merchantIdColumn, queryMerchantId);
|
||||
}
|
||||
else {
|
||||
wrapper.apply("1 = 0");
|
||||
}
|
||||
return;
|
||||
}
|
||||
wrapper.in(merchantIdColumn, allowed);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按商户回填 deptId(写入时与商户部门对齐,保证 DataScope 与商户隔离一致)
|
||||
*/
|
||||
public void fillDeptIdByMerchantId(Long merchantId, Consumer<Long> deptIdSetter) {
|
||||
if (merchantId == null || deptIdSetter == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
R<MchManageVo> r = remoteMchService.getById(merchantId);
|
||||
if (r != null && r.isOk() && ObjectUtil.isNotEmpty(r.getData()) && r.getData().getDeptId() != null) {
|
||||
deptIdSetter.accept(r.getData().getDeptId());
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("按商户回填部门失败, merchantId={}", merchantId, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+2
@@ -47,7 +47,9 @@
|
||||
t.hour_type,
|
||||
sp.price,
|
||||
sp.category_id,
|
||||
<!-- 数据权限拦截器外层按 create_by / dept_id 过滤,须透传到临时结果集 -->
|
||||
sp.dept_id,
|
||||
t.create_by,
|
||||
IFNULL(sp.spu_type, 1) as spu_type
|
||||
FROM canteen_daily_menu t
|
||||
LEFT JOIN canteen_spu sp
|
||||
|
||||
+5
@@ -137,6 +137,11 @@ public interface CacheConstants {
|
||||
*/
|
||||
String WX_PC_SOCIAL_TICKET = GLOBALLY + "wx:pc:social:ticket:";
|
||||
|
||||
/**
|
||||
* App Apple 社交登录一次性 ticket(sub 缓存)
|
||||
*/
|
||||
String APPLE_SOCIAL_TICKET = GLOBALLY + "apple:social:ticket:";
|
||||
|
||||
/**
|
||||
* PC 密码已校验待绑定微信(value=userId)
|
||||
*/
|
||||
|
||||
+11
-1
@@ -83,7 +83,17 @@ public enum LoginTypeEnum {
|
||||
/**
|
||||
* CAS 登录
|
||||
*/
|
||||
CAS("CAS", "CAS 登录");
|
||||
CAS("CAS", "CAS 登录"),
|
||||
|
||||
/**
|
||||
* Apple 登录(移动端 App,identify 入参为 identityToken)
|
||||
*/
|
||||
APPLE("APPLE", "Apple登录"),
|
||||
|
||||
/**
|
||||
* Apple identityToken 兑换后的一次性 ticket 登录
|
||||
*/
|
||||
APPLE_TICKET("APPLE_TICKET", "Apple Ticket登录");
|
||||
|
||||
/**
|
||||
* 类型
|
||||
|
||||
+6
@@ -194,6 +194,12 @@ public class SysUser extends Model<SysUser> implements Serializable {
|
||||
@Schema(description = "PC微信openid")
|
||||
private String wxPcOpenid;
|
||||
|
||||
/**
|
||||
* Apple 登录唯一标识(identityToken.sub)
|
||||
*/
|
||||
@Schema(description = "Apple openid")
|
||||
private String appleOpenid;
|
||||
|
||||
/**
|
||||
* 企微微信 userid
|
||||
*/
|
||||
|
||||
@@ -98,6 +98,12 @@ public class UserVO implements BaseFacesInterface, Serializable {
|
||||
@Schema(description = "PC微信open id")
|
||||
private String wxPcOpenid;
|
||||
|
||||
/**
|
||||
* Apple openid
|
||||
*/
|
||||
@Schema(description = "Apple open id")
|
||||
private String appleOpenid;
|
||||
|
||||
/**
|
||||
* QQ openid
|
||||
*/
|
||||
|
||||
+9
@@ -163,6 +163,15 @@ public class SysSocialDetailsController {
|
||||
return R.ok(sysSocialDetailsService.resolveWxCode(code));
|
||||
}
|
||||
|
||||
/**
|
||||
* App Apple:解析 identityToken(一次性),返回绑定状态与 ticket
|
||||
*/
|
||||
@Inner(value = false)
|
||||
@PostMapping("/apple/resolve")
|
||||
public R<WxSocialResolveVO> resolveAppleIdentityToken(String identityToken) {
|
||||
return R.ok(sysSocialDetailsService.resolveAppleIdentityToken(identityToken));
|
||||
}
|
||||
|
||||
/**
|
||||
* App 微信:使用 ticket 绑定(需已登录)
|
||||
*/
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.bsd.pjcloud.admin.handler;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.bsd.pjcloud.admin.api.dto.UserInfo;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysSocialDetails;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysUser;
|
||||
import com.bsd.pjcloud.admin.handler.apple.AppleIdentityTokenVerifier;
|
||||
import com.bsd.pjcloud.admin.mapper.SysSocialDetailsMapper;
|
||||
import com.bsd.pjcloud.admin.service.SysUserService;
|
||||
import com.bsd.pjcloud.common.core.constant.enums.LoginTypeEnum;
|
||||
import com.bsd.pjcloud.common.core.exception.CheckedException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Apple 登录:identify 入参为 App 端 identityToken
|
||||
*/
|
||||
@Slf4j
|
||||
@Component("APPLE")
|
||||
@AllArgsConstructor
|
||||
public class AppleLoginHandler extends AbstractLoginHandler {
|
||||
|
||||
private final SysUserService sysUserService;
|
||||
|
||||
private final SysSocialDetailsMapper sysSocialDetailsMapper;
|
||||
|
||||
@Override
|
||||
public String identify(String identityToken) {
|
||||
SysSocialDetails condition = new SysSocialDetails();
|
||||
condition.setType(LoginTypeEnum.APPLE.getType());
|
||||
SysSocialDetails socialDetails = sysSocialDetailsMapper.selectOne(new QueryWrapper<>(condition));
|
||||
if (socialDetails == null) {
|
||||
throw new CheckedException("未配置Apple登录");
|
||||
}
|
||||
String sub = AppleIdentityTokenVerifier.verifyAndGetSubject(identityToken, socialDetails.getAppId());
|
||||
log.debug("Apple identityToken 校验通过, sub={}", sub);
|
||||
return sub;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserInfo info(String appleOpenid) {
|
||||
SysUser user = sysUserService.getOne(Wrappers.<SysUser>query().lambda().eq(SysUser::getAppleOpenid, appleOpenid));
|
||||
if (user == null) {
|
||||
log.info("Apple未绑定:{}", appleOpenid);
|
||||
return null;
|
||||
}
|
||||
return sysUserService.findUserInfo(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean bind(SysUser user, String identify) {
|
||||
user.setAppleOpenid(identify);
|
||||
sysUserService.updateById(user);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package com.bsd.pjcloud.admin.handler;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.bsd.pjcloud.admin.api.dto.UserInfo;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysUser;
|
||||
import com.bsd.pjcloud.admin.service.SysUserService;
|
||||
import com.bsd.pjcloud.common.core.constant.CacheConstants;
|
||||
import com.bsd.pjcloud.common.core.exception.CheckedException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Apple 登录 ticket 处理器(mobile=APPLE_TICKET@{ticket})
|
||||
*/
|
||||
@Slf4j
|
||||
@Component("APPLE_TICKET")
|
||||
@RequiredArgsConstructor
|
||||
public class AppleTicketLoginHandler implements LoginHandler {
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
|
||||
private final SysUserService sysUserService;
|
||||
|
||||
private String ticketKey(String ticket) {
|
||||
return CacheConstants.APPLE_SOCIAL_TICKET + ticket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean check(String loginStr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String identify(String ticket) {
|
||||
String openid = redisTemplate.opsForValue().get(ticketKey(ticket));
|
||||
if (StrUtil.isBlank(openid)) {
|
||||
throw new CheckedException("Apple授权已过期,请重新授权");
|
||||
}
|
||||
redisTemplate.delete(ticketKey(ticket));
|
||||
return openid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserInfo info(String appleOpenid) {
|
||||
SysUser user = sysUserService.getOne(Wrappers.<SysUser>lambdaQuery().eq(SysUser::getAppleOpenid, appleOpenid));
|
||||
if (user == null) {
|
||||
log.info("Apple未绑定:{}", appleOpenid);
|
||||
return null;
|
||||
}
|
||||
return sysUserService.findUserInfo(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserInfo handle(String loginStr) {
|
||||
String openid = identify(loginStr);
|
||||
return info(openid);
|
||||
}
|
||||
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package com.bsd.pjcloud.admin.handler.apple;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.bsd.pjcloud.common.core.exception.CheckedException;
|
||||
import com.nimbusds.jose.JWSVerifier;
|
||||
import com.nimbusds.jose.crypto.RSASSAVerifier;
|
||||
import com.nimbusds.jose.jwk.JWK;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* 校验 Apple identityToken(JWKS),返回 sub
|
||||
*/
|
||||
@Slf4j
|
||||
public final class AppleIdentityTokenVerifier {
|
||||
|
||||
private static final String APPLE_ISS = "https://appleid.apple.com";
|
||||
|
||||
private static final String APPLE_KEYS_URL = "https://appleid.apple.com/auth/keys";
|
||||
|
||||
private static final long CACHE_TTL_MS = 6L * 60 * 60 * 1000;
|
||||
|
||||
private static final AtomicReference<CachedJwkSet> CACHE = new AtomicReference<>();
|
||||
|
||||
private AppleIdentityTokenVerifier() {
|
||||
}
|
||||
|
||||
public static String verifyAndGetSubject(String identityToken, String audience) {
|
||||
if (StrUtil.isBlank(identityToken)) {
|
||||
throw new CheckedException("Apple身份令牌为空");
|
||||
}
|
||||
if (StrUtil.isBlank(audience)) {
|
||||
throw new CheckedException("未配置Apple appId(Bundle ID)");
|
||||
}
|
||||
try {
|
||||
SignedJWT jwt = SignedJWT.parse(identityToken.trim());
|
||||
String kid = jwt.getHeader().getKeyID();
|
||||
RSAKey rsaKey = findRsaKey(kid, false);
|
||||
if (rsaKey == null) {
|
||||
rsaKey = findRsaKey(kid, true);
|
||||
}
|
||||
if (rsaKey == null) {
|
||||
throw new CheckedException("Apple公钥不匹配,请稍后重试");
|
||||
}
|
||||
JWSVerifier verifier = new RSASSAVerifier(rsaKey.toRSAPublicKey());
|
||||
if (!jwt.verify(verifier)) {
|
||||
throw new CheckedException("Apple身份令牌验签失败");
|
||||
}
|
||||
JWTClaimsSet claims = jwt.getJWTClaimsSet();
|
||||
if (!APPLE_ISS.equals(claims.getIssuer())) {
|
||||
throw new CheckedException("Apple身份令牌发行方无效");
|
||||
}
|
||||
List<String> aud = claims.getAudience();
|
||||
if (aud == null || !aud.contains(audience)) {
|
||||
throw new CheckedException("Apple身份令牌audience不匹配");
|
||||
}
|
||||
Date exp = claims.getExpirationTime();
|
||||
if (exp == null || exp.before(new Date())) {
|
||||
throw new CheckedException("Apple身份令牌已过期");
|
||||
}
|
||||
String sub = claims.getSubject();
|
||||
if (StrUtil.isBlank(sub)) {
|
||||
throw new CheckedException("Apple身份令牌缺少用户标识");
|
||||
}
|
||||
return sub;
|
||||
}
|
||||
catch (CheckedException e) {
|
||||
throw e;
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.error("校验Apple identityToken失败", e);
|
||||
throw new CheckedException("Apple身份令牌无效");
|
||||
}
|
||||
}
|
||||
|
||||
private static RSAKey findRsaKey(String kid, boolean forceRefresh) throws Exception {
|
||||
JWKSet jwkSet = loadJwkSet(forceRefresh);
|
||||
if (jwkSet == null || StrUtil.isBlank(kid)) {
|
||||
return null;
|
||||
}
|
||||
for (JWK jwk : jwkSet.getKeys()) {
|
||||
if (kid.equals(jwk.getKeyID()) && jwk instanceof RSAKey) {
|
||||
return (RSAKey) jwk;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static JWKSet loadJwkSet(boolean forceRefresh) throws Exception {
|
||||
CachedJwkSet cached = CACHE.get();
|
||||
long now = System.currentTimeMillis();
|
||||
if (!forceRefresh && cached != null && now - cached.loadedAt < CACHE_TTL_MS) {
|
||||
return cached.jwkSet;
|
||||
}
|
||||
String body = HttpUtil.get(APPLE_KEYS_URL);
|
||||
if (StrUtil.isBlank(body)) {
|
||||
throw new CheckedException("获取Apple公钥失败");
|
||||
}
|
||||
// 兼容部分环境返回结构校验
|
||||
JSONObject root = JSONUtil.parseObj(body);
|
||||
JSONArray keys = root.getJSONArray("keys");
|
||||
if (keys == null || keys.isEmpty()) {
|
||||
throw new CheckedException("Apple公钥为空");
|
||||
}
|
||||
JWKSet jwkSet = JWKSet.parse(body);
|
||||
CACHE.set(new CachedJwkSet(jwkSet, now));
|
||||
return jwkSet;
|
||||
}
|
||||
|
||||
private record CachedJwkSet(JWKSet jwkSet, long loadedAt) {
|
||||
}
|
||||
|
||||
}
|
||||
+5
@@ -51,6 +51,11 @@ public interface SysSocialDetailsService extends IService<SysSocialDetails> {
|
||||
*/
|
||||
WxSocialResolveVO resolveWxCode(String code);
|
||||
|
||||
/**
|
||||
* 解析 Apple identityToken(仅校验一次),返回是否已绑定及 ticket
|
||||
*/
|
||||
WxSocialResolveVO resolveAppleIdentityToken(String identityToken);
|
||||
|
||||
/**
|
||||
* 根据入参查询用户信息
|
||||
* @param inStr
|
||||
|
||||
+26
-3
@@ -66,15 +66,22 @@ public class SysSocialDetailsServiceImpl extends ServiceImpl<SysSocialDetailsMap
|
||||
|
||||
private static final long WX_TICKET_TTL_MINUTES = 15;
|
||||
|
||||
private String wxTicketKey(String type, String ticket) {
|
||||
private String socialTicketKey(String type, String ticket) {
|
||||
if ("WX_PC".equals(type)) {
|
||||
return CacheConstants.WX_PC_SOCIAL_TICKET + ticket;
|
||||
}
|
||||
if ("APPLE".equals(type)) {
|
||||
return CacheConstants.APPLE_SOCIAL_TICKET + ticket;
|
||||
}
|
||||
return CacheConstants.WX_SOCIAL_TICKET + ticket;
|
||||
}
|
||||
|
||||
private String wxTicketKey(String type, String ticket) {
|
||||
return socialTicketKey(type, ticket);
|
||||
}
|
||||
|
||||
private String wxTicketKey(String ticket) {
|
||||
return wxTicketKey("WX", ticket);
|
||||
return socialTicketKey("WX", ticket);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,7 +108,7 @@ public class SysSocialDetailsServiceImpl extends ServiceImpl<SysSocialDetailsMap
|
||||
public Boolean bindSocialByTicket(String type, String ticket) {
|
||||
String openid = redisTemplate.opsForValue().get(wxTicketKey(type, ticket));
|
||||
if (StrUtil.isBlank(openid)) {
|
||||
throw new CheckedException("微信授权已过期,请重新授权");
|
||||
throw new CheckedException("授权已过期,请重新授权");
|
||||
}
|
||||
LoginHandler loginHandler = loginHandlerMap.get(type);
|
||||
SysUser sysUser = sysUserMapper.selectById(SecurityUtils.getUser().getId());
|
||||
@@ -124,6 +131,22 @@ public class SysSocialDetailsServiceImpl extends ServiceImpl<SysSocialDetailsMap
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WxSocialResolveVO resolveAppleIdentityToken(String identityToken) {
|
||||
LoginHandler appleHandler = loginHandlerMap.get("APPLE");
|
||||
if (appleHandler == null) {
|
||||
throw new CheckedException("未启用Apple登录");
|
||||
}
|
||||
String openid = appleHandler.identify(identityToken);
|
||||
UserInfo userInfo = appleHandler.info(openid);
|
||||
String ticket = IdUtil.simpleUUID();
|
||||
redisTemplate.opsForValue().set(socialTicketKey("APPLE", ticket), openid, WX_TICKET_TTL_MINUTES, TimeUnit.MINUTES);
|
||||
WxSocialResolveVO vo = new WxSocialResolveVO();
|
||||
vo.setBound(userInfo != null);
|
||||
vo.setTicket(ticket);
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据入参查询用户信息
|
||||
*
|
||||
|
||||
+2
@@ -1196,6 +1196,8 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
||||
wrapper = Wrappers.<SysUser>lambdaUpdate().set(SysUser::getWxOpenid, null).eq(SysUser::getUserId, user.getId());
|
||||
} else if (type.equals("wechat_pc")) {
|
||||
wrapper = Wrappers.<SysUser>lambdaUpdate().set(SysUser::getWxPcOpenid, null).eq(SysUser::getUserId, user.getId());
|
||||
} else if (type.equals("apple")) {
|
||||
wrapper = Wrappers.<SysUser>lambdaUpdate().set(SysUser::getAppleOpenid, null).eq(SysUser::getUserId, user.getId());
|
||||
} else if (type.equals("gitee")) {
|
||||
wrapper = Wrappers.<SysUser>lambdaUpdate().set(SysUser::getGiteeLogin, null).eq(SysUser::getUserId, user.getId());
|
||||
} else if (type.equals("osc")) {
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<result column="avatar" property="avatar"/>
|
||||
<result column="wx_openid" property="wxOpenid"/>
|
||||
<result column="wx_pc_openid" property="wxPcOpenid"/>
|
||||
<result column="apple_openid" property="appleOpenid"/>
|
||||
<result column="qq_openid" property="qqOpenid"/>
|
||||
<result column="gitee_login" property="giteeOpenId"/>
|
||||
<result column="osc_id" property="oscOpenId"/>
|
||||
@@ -75,6 +76,7 @@
|
||||
u.avatar,
|
||||
u.wx_openid,
|
||||
u.wx_pc_openid,
|
||||
u.apple_openid,
|
||||
u.qq_openid,
|
||||
u.gitee_login,
|
||||
u.osc_id,
|
||||
@@ -110,6 +112,7 @@
|
||||
u.avatar,
|
||||
u.wx_openid,
|
||||
u.wx_pc_openid,
|
||||
u.apple_openid,
|
||||
u.qq_openid,
|
||||
u.gitee_login,
|
||||
u.osc_id,
|
||||
@@ -167,6 +170,7 @@
|
||||
u.avatar,
|
||||
u.wx_openid,
|
||||
u.wx_pc_openid,
|
||||
u.apple_openid,
|
||||
u.qq_openid,
|
||||
u.gitee_login,
|
||||
u.osc_id,
|
||||
@@ -268,6 +272,7 @@
|
||||
u.avatar,
|
||||
u.wx_openid,
|
||||
u.wx_pc_openid,
|
||||
u.apple_openid,
|
||||
u.qq_openid,
|
||||
u.gitee_login,
|
||||
u.osc_id,
|
||||
|
||||
Reference in new issue
Block a user