diff --git a/cc-admin-master/dependencies/pom.xml b/cc-admin-master/dependencies/pom.xml index 4725fbb..cf76d62 100644 --- a/cc-admin-master/dependencies/pom.xml +++ b/cc-admin-master/dependencies/pom.xml @@ -14,7 +14,7 @@ https://github.com/YunaiV/ruoyi-vue-pro - 2.6.1-SNAPSHOT + 2025.08-SNAPSHOT 1.6.0 3.4.5 @@ -24,9 +24,9 @@ 1.2.24 3.5.19 - 3.5.10.1 + 3.5.12 + 1.5.4 4.3.1 - 1.4.13 3.0.6 3.41.0 8.1.3.140 @@ -54,7 +54,7 @@ 1.6.3 5.8.35 6.0.0-M19 - 4.0.3 + 1.2.0 2.4.1 1.2.83 33.4.8-jre @@ -69,18 +69,12 @@ 0.9.0 4.5.13 - 2.17.0 - 1.27.1 2.30.14 - 1.4.0 2.0.0 1.9.5 - @@ -255,23 +249,23 @@ - - - + com.taosdata.jdbc @@ -412,6 +406,18 @@ + + com.github.fppt + jedis-mock + ${jedis-mock.version} + + + + uk.co.jemos.podam + podam + ${podam.version} + + org.flowable @@ -443,17 +449,17 @@ lombok ${lombok.version} - ${mapstruct.version} - --> - + org.mapstruct mapstruct-processor @@ -472,20 +478,11 @@ - com.alibaba - easyexcel - ${easyexcel.version} - - - commons-io - commons-io - ${commons-io.version} - - - org.apache.commons - commons-compress - ${commons-compress.version} + cn.idev.excel + fastexcel + ${fastexcel.version} + org.apache.tika tika-core @@ -553,25 +550,25 @@ ${awssdk.version} - ${justauth.version} - –>--> - cn.hutool hutool-core - --> + - + diff --git a/cc-admin-master/yudao-module-lock/pom.xml b/cc-admin-master/yudao-module-lock/pom.xml index a952cbd..823fddc 100644 --- a/cc-admin-master/yudao-module-lock/pom.xml +++ b/cc-admin-master/yudao-module-lock/pom.xml @@ -77,7 +77,15 @@ org.dromara.hutool hutool-extra - + + com.github.binarywang + wx-java-mp-spring-boot-starter + + + com.github.binarywang + wx-java-cp-spring-boot-starter + 4.6.0 + diff --git a/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/controller/admin/JsSdkController.java b/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/controller/admin/JsSdkController.java new file mode 100644 index 0000000..cf9f58b --- /dev/null +++ b/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/controller/admin/JsSdkController.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.module.lock.controller.admin; + +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.security.PermitAll; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.bean.WxJsapiSignature; +import me.chanjar.weixin.cp.bean.WxCpAgentJsapiSignature; +import me.chanjar.weixin.mp.api.WxMpService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import me.chanjar.weixin.cp.api.WxCpService; + +import java.net.http.HttpClient; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Formatter; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@Tag(name = "企业微信回调") +@RestController +@RequestMapping("/js/weixin") +@Slf4j +public class JsSdkController { + + + @Autowired + private WxCpService wxCpService; + + @GetMapping("/getConfigSignature") + @PermitAll + @TenantIgnore + public WxJsapiSignature getConfigSignature(@RequestParam(required = false) String url) { + try { + return wxCpService.createJsapiSignature(url); + + } catch (Exception e) { + log.error("[getConfigSignature][url({}) 发生异常]", url, e); + } + return null; + } + + @GetMapping("/getAgentConfigSignature") + @PermitAll + @TenantIgnore + public WxCpAgentJsapiSignature getAgentConfigSignature(@RequestParam(required = false) String url) { + try { + return wxCpService.createAgentJsapiSignature(url); + + } catch (Exception e) { + log.error("[getConfigSignature][url({}) 发生异常]", url, e); + } + return null; + } +} diff --git a/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/controller/admin/LockController.java b/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/controller/admin/LockController.java index 7970c0d..611a612 100644 --- a/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/controller/admin/LockController.java +++ b/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/controller/admin/LockController.java @@ -87,8 +87,7 @@ public class LockController { @GetMapping("/page") @Operation(summary = "获得电子锁分页") - //@PreAuthorize("@ss.hasPermission('electron:lock:query')") - @PermitAll + @PreAuthorize("@ss.hasPermission('electron:lock:query')") public CommonResult> getLockPage(@Valid LockPageReqVO pageReqVO) { PageResult pageResult = lockService.getLockPage(pageReqVO); return success(BeanUtils.toBean(pageResult, LockRespVO.class)); diff --git a/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/controller/admin/PlanController.java b/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/controller/admin/PlanController.java index 40c3dd5..d35d6a4 100644 --- a/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/controller/admin/PlanController.java +++ b/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/controller/admin/PlanController.java @@ -1,10 +1,16 @@ package cn.iocoder.yudao.module.lock.controller.admin; import cn.iocoder.yudao.module.lock.dal.PlanDO; +import cn.iocoder.yudao.module.lock.dal.entity.PlanEntity; +import cn.iocoder.yudao.module.lock.service.PlanItemDetailService; +import cn.iocoder.yudao.module.lock.service.PlanItemService; +import cn.iocoder.yudao.module.lock.service.PlanLifeLockService; import cn.iocoder.yudao.module.lock.service.PlanService; import cn.iocoder.yudao.module.lock.vo.PlanPageReqVO; import cn.iocoder.yudao.module.lock.vo.PlanRespVO; import cn.iocoder.yudao.module.lock.vo.PlanSaveReqVO; +import cn.iocoder.yudao.module.lock.vo.PlanSelectVo; +import jakarta.annotation.security.PermitAll; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; @@ -86,6 +92,8 @@ public class PlanController { @PreAuthorize("@ss.hasPermission('isolation:plan:query')") public CommonResult> getPlanPage(@Valid PlanPageReqVO pageReqVO) { PageResult pageResult = planService.getPlanPage(pageReqVO); + + return success(BeanUtils.toBean(pageResult, PlanRespVO.class)); } @@ -102,4 +110,12 @@ public class PlanController { BeanUtils.toBean(list, PlanRespVO.class)); } + @GetMapping("/planListAll") + @Operation(summary = "获得所有隔离计划") + @PermitAll + public CommonResult> getPlanListAll(@Valid PlanSelectVo pageReqVO) { + + return success(planService.listAll(pageReqVO)); + + } } \ No newline at end of file diff --git a/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/dal/PlanDO.java b/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/dal/PlanDO.java index 9678f77..ec1563a 100644 --- a/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/dal/PlanDO.java +++ b/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/dal/PlanDO.java @@ -32,5 +32,4 @@ public class PlanDO extends BaseDO { */ private String ipName; - } \ No newline at end of file diff --git a/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/dal/entity/PlanEntity.java b/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/dal/entity/PlanEntity.java new file mode 100644 index 0000000..0d3d525 --- /dev/null +++ b/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/dal/entity/PlanEntity.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.lock.dal.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.Data; + +import java.util.List; +@Data +public class PlanEntity { + @TableId + private Long id; + /** + * 计划名称 + */ + private String ipName; + + + private List planItemEntityList; + + +} + diff --git a/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/dal/entity/PlanItemDetailEntity.java b/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/dal/entity/PlanItemDetailEntity.java new file mode 100644 index 0000000..c940190 --- /dev/null +++ b/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/dal/entity/PlanItemDetailEntity.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.lock.dal.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.Data; + +import java.util.List; + +/** + * 隔离计划子项详情 DO + * + * @author 超级管理员 + */ +@Data +public class PlanItemDetailEntity { + @TableId + private Long id; + /** + * 隔离计划子项ID + */ + private Long isolationPlanItemId; + /** + * 隔离点ID + */ + private Long isolationPointId; + /** + * 电子锁ID + */ + private Long lockId; + /** + * 锁状态: 0=未上锁, 1=已上锁, 2=已解锁 + */ + private Boolean lockStatus; + + + private List lockEntities; +} \ No newline at end of file diff --git a/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/dal/entity/PlanItemEntity.java b/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/dal/entity/PlanItemEntity.java new file mode 100644 index 0000000..25dfd44 --- /dev/null +++ b/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/dal/entity/PlanItemEntity.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.lock.dal.entity; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.util.List; + +/** + * 隔离计划子项 DO + * + * @author 超级管理员 + */ +@Data +public class PlanItemEntity { + @TableId + private Long id; + /** + * 隔离计划ID + */ + private Long isolationPlanId; + /** + * 隔离指导书ID + */ + private Long guideId; + /** + * 集中挂牌人ID + */ + private Long operatorId; + /** + * 集中挂牌协助人ID + */ + private Long operatorHelperId; + /** + * 验证人ID + */ + private Long verifierId; + /** + * 验证协助人ID + */ + private Long verifierHelperId; + /** + * 子项状态: 0=未完成, 1=已完成 + */ + private Boolean status; + + + private List planItemDetailEntityList; + +} \ No newline at end of file diff --git a/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/dal/entity/PlanLifeLockEntity.java b/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/dal/entity/PlanLifeLockEntity.java new file mode 100644 index 0000000..797e4ec --- /dev/null +++ b/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/dal/entity/PlanLifeLockEntity.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.lock.dal.entity; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 个人生命锁 DO + * + * @author 超级管理员 + */ + +@Data +public class PlanLifeLockEntity { + + @TableId + private Long id; + /** + * 子项详情ID + */ + private Long isolationPlanItemDetailId; + /** + * 上锁人ID + */ + private Long userId; + /** + * 生命锁类型 + */ + private String lockType; + /** + * 锁定状态: 0=未上锁, 1=已上锁 + */ + private Boolean lockStatus; + /** + * 上锁时间 + */ + private LocalDateTime lockTime; + /** + * 解锁时间 + */ + private LocalDateTime unlockTime; + + +} \ No newline at end of file diff --git a/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/service/PlanService.java b/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/service/PlanService.java index f6f089b..a5775bd 100644 --- a/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/service/PlanService.java +++ b/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/service/PlanService.java @@ -3,8 +3,10 @@ package cn.iocoder.yudao.module.lock.service; import java.util.*; import cn.iocoder.yudao.module.lock.dal.PlanDO; +import cn.iocoder.yudao.module.lock.dal.entity.PlanEntity; import cn.iocoder.yudao.module.lock.vo.PlanPageReqVO; import cn.iocoder.yudao.module.lock.vo.PlanSaveReqVO; +import cn.iocoder.yudao.module.lock.vo.PlanSelectVo; import jakarta.validation.*; import cn.iocoder.yudao.framework.common.pojo.PageResult; @@ -60,4 +62,5 @@ public interface PlanService { */ PageResult getPlanPage(PlanPageReqVO pageReqVO); + List listAll(PlanSelectVo pageReqVO); } \ No newline at end of file diff --git a/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/service/impl/PlanServiceImpl.java b/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/service/impl/PlanServiceImpl.java index 7c33e23..bb95c9a 100644 --- a/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/service/impl/PlanServiceImpl.java +++ b/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/service/impl/PlanServiceImpl.java @@ -1,23 +1,44 @@ package cn.iocoder.yudao.module.lock.service.impl; +import cn.hutool.core.collection.CollectionUtil; import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; import cn.iocoder.yudao.module.lock.dal.PlanDO; +import cn.iocoder.yudao.module.lock.dal.PlanItemDO; +import cn.iocoder.yudao.module.lock.dal.PlanItemDetailDO; +import cn.iocoder.yudao.module.lock.dal.PlanLifeLockDO; +import cn.iocoder.yudao.module.lock.dal.entity.PlanEntity; +import cn.iocoder.yudao.module.lock.dal.entity.PlanItemDetailEntity; +import cn.iocoder.yudao.module.lock.dal.entity.PlanItemEntity; +import cn.iocoder.yudao.module.lock.dal.entity.PlanLifeLockEntity; import cn.iocoder.yudao.module.lock.enums.ErrorCodeConstants; +import cn.iocoder.yudao.module.lock.mapper.PlanItemDetailMapper; +import cn.iocoder.yudao.module.lock.mapper.PlanItemMapper; +import cn.iocoder.yudao.module.lock.mapper.PlanLifeLockMapper; import cn.iocoder.yudao.module.lock.mapper.PlanMapper; +import cn.iocoder.yudao.module.lock.service.PlanItemDetailService; +import cn.iocoder.yudao.module.lock.service.PlanItemService; +import cn.iocoder.yudao.module.lock.service.PlanLifeLockService; import cn.iocoder.yudao.module.lock.service.PlanService; import cn.iocoder.yudao.module.lock.vo.PlanPageReqVO; import cn.iocoder.yudao.module.lock.vo.PlanSaveReqVO; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.lock.vo.PlanSelectVo; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import java.util.*; +import java.util.stream.Collectors; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static java.util.stream.Collectors.groupingBy; /** * 隔离计划 Service 实现类 @@ -30,6 +51,12 @@ public class PlanServiceImpl implements PlanService { @Resource private PlanMapper planMapper; + @Resource + private PlanItemMapper planItemMapper; + @Resource + private PlanLifeLockMapper planLifeLockMapper; + @Resource + private PlanItemDetailMapper planItemDetailMapper; @Override public Long createPlan(PlanSaveReqVO createReqVO) { @@ -59,10 +86,10 @@ public class PlanServiceImpl implements PlanService { } @Override - public void deletePlanListByIds(List ids) { + public void deletePlanListByIds(List ids) { // 删除 planMapper.deleteByIds(ids); - } + } private void validatePlanExists(Long id) { @@ -81,4 +108,76 @@ public class PlanServiceImpl implements PlanService { return planMapper.selectPage(pageReqVO); } + @Override + public List listAll(PlanSelectVo pageReqVO) { + // 1. 查询顶层 Plan + var queryWrapper = new QueryWrapper(); + if (StringUtils.isNotBlank(pageReqVO.getIpName())) { + queryWrapper.like("ip_name", pageReqVO.getIpName()); + } + List planDOS = planMapper.selectList(queryWrapper); + if (CollectionUtil.isEmpty(planDOS)) { + return new ArrayList<>(); + } + // --- 核心优化:使用通用方法批量查询所有子层级 --- + var planIds = planDOS.stream().map(PlanDO::getId).toList(); + + // 批量查询所有 PlanItem + List planItemDOS = selectInBatch(planIds, "isolation_plan_id", planItemMapper); + var planItemIds = planItemDOS.stream().map(PlanItemDO::getId).toList(); + + // 批量查询所有 PlanItemDetail + List planItemDetailDOS = selectInBatch(planItemIds, "isolation_plan_item_id", planItemDetailMapper); + var planItemDetailIds = planItemDetailDOS.stream().map(PlanItemDetailDO::getId).toList(); + + // 批量查询所有 PlanLifeLock + List planLifeLockDOS = selectInBatch(planItemDetailIds, "isolation_plan_item_detail_id", planLifeLockMapper); + + // --- 高效组装:一次性转换并使用 Map 关联数据 --- + + // 1. 将所有查询到的 DO 列表转换为 Entity 列表 + List planEntities = BeanUtils.toBean(planDOS, PlanEntity.class); + List itemEntities = BeanUtils.toBean(planItemDOS, PlanItemEntity.class); + List detailEntities = BeanUtils.toBean(planItemDetailDOS, PlanItemDetailEntity.class); + List lockEntities = BeanUtils.toBean(planLifeLockDOS, PlanLifeLockEntity.class); + + // 2. 将子 Entity 列表转换为以其父ID为键的 Map,用于 O(1) 快速查找 + Map> locksByDetailId = lockEntities.stream() + .collect(groupingBy(PlanLifeLockEntity::getIsolationPlanItemDetailId)); + + Map> detailsByItemId = detailEntities.stream() + .collect(groupingBy(PlanItemDetailEntity::getIsolationPlanItemId)); + + Map> itemsByPlanId = itemEntities.stream() + .collect(groupingBy(PlanItemEntity::getIsolationPlanId)); + + // 3. 遍历顶层列表,高效地组装整个对象树 + planEntities.forEach(plan -> { + List items = itemsByPlanId.getOrDefault(plan.getId(), Collections.emptyList()); + items.forEach(item -> { + List details = detailsByItemId.getOrDefault(item.getId(), Collections.emptyList()); + details.forEach(detail -> + detail.setLockEntities(locksByDetailId.getOrDefault(detail.getId(), Collections.emptyList())) + ); + item.setPlanItemDetailEntityList(details); + }); + plan.setPlanItemEntityList(items); + }); + + return planEntities; + + } + /** + * 抽取出的通用方法:根据父ID列表批量查询子记录 + * @param ids 父ID列表 + * @param columnName "in" 查询的列名 + * @param mapper 对应的 MyBatis Mapper + * @return 查询到的子记录列表 + */ + private > List selectInBatch(List ids, String columnName, M mapper) { + if (CollectionUtil.isEmpty(ids)) { + return new ArrayList<>(); + } + return mapper.selectList(new QueryWrapper().in(columnName, ids)); + } } \ No newline at end of file diff --git a/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/vo/PlanSelectVo.java b/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/vo/PlanSelectVo.java new file mode 100644 index 0000000..6fd2769 --- /dev/null +++ b/cc-admin-master/yudao-module-lock/src/main/java/cn/iocoder/yudao/module/lock/vo/PlanSelectVo.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.lock.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +@Schema(description = "管理后台 - 隔离计划查询") +@Data +public class PlanSelectVo { + + @Schema(description = "计划名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") + @NotEmpty(message = "计划名称不能为空") + private String ipName; + + +} diff --git a/cc-admin-master/yudao-module-system/pom.xml b/cc-admin-master/yudao-module-system/pom.xml index f2c0a9c..afe5f3f 100644 --- a/cc-admin-master/yudao-module-system/pom.xml +++ b/cc-admin-master/yudao-module-system/pom.xml @@ -91,23 +91,24 @@ - - + + + com.xkcoding.justauth justauth-spring-boot-starter - --> + - com.github.binarywang - wx-java-miniapp-spring-boot-starter <!– 微信登录(小程序) –> - --> + wx-java-miniapp-spring-boot-starter + com.anji-plus diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApi.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApi.java new file mode 100644 index 0000000..7306310 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApi.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.system.api.social; + +import cn.iocoder.yudao.module.system.api.social.dto.*; +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * 社交应用的 API 接口 + * + * @author 芋道源码 + */ +public interface SocialClientApi { + + /** + * 获得社交平台的授权 URL + * + * @param socialType 社交平台的类型 {@link SocialTypeEnum} + * @param userType 用户类型 + * @param redirectUri 重定向 URL + * @return 社交平台的授权 URL + */ + String getAuthorizeUrl(Integer socialType, Integer userType, String redirectUri); + + /** + * 创建微信公众号 JS SDK 初始化所需的签名 + * + * @param userType 用户类型 + * @param url 访问的 URL 地址 + * @return 签名 + */ + SocialWxJsapiSignatureRespDTO createWxMpJsapiSignature(Integer userType, String url); + + //======================= 微信小程序独有 ======================= + + /** + * 获得微信小程序的手机信息 + * + * @param userType 用户类型 + * @param phoneCode 手机授权码 + * @return 手机信息 + */ + SocialWxPhoneNumberInfoRespDTO getWxMaPhoneNumberInfo(Integer userType, String phoneCode); + + /** + * 获得小程序二维码 + * + * @param reqVO 请求信息 + * @return 小程序二维码 + */ + byte[] getWxaQrcode(@Valid SocialWxQrcodeReqDTO reqVO); + + /** + * 获得微信小程订阅模板 + * + * @return 小程序订阅消息模版 + */ + List getWxaSubscribeTemplateList(Integer userType); + + /** + * 发送微信小程序订阅消息 + * + * @param reqDTO 请求 + */ + void sendWxaSubscribeMessage(SocialWxaSubscribeMessageSendReqDTO reqDTO); + + /** + * 上传订单发货到微信小程序 + * + * @param userType 用户类型 + * @param reqDTO 请求 + */ + void uploadWxaOrderShippingInfo(Integer userType, SocialWxaOrderUploadShippingInfoReqDTO reqDTO); + + /** + * 通知订单收货到微信小程序 + * + * @param userType 用户类型 + * @param reqDTO 请求 + */ + void notifyWxaOrderConfirmReceive(Integer userType, SocialWxaOrderNotifyConfirmReceiveReqDTO reqDTO); + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java new file mode 100644 index 0000000..d2eb00d --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java @@ -0,0 +1,107 @@ +package cn.iocoder.yudao.module.system.api.social; + +import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.system.api.social.dto.*; +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import cn.iocoder.yudao.module.system.service.social.SocialClientService; +import cn.iocoder.yudao.module.system.service.social.SocialUserService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.bean.WxJsapiSignature; +import me.chanjar.weixin.common.bean.subscribemsg.TemplateInfo; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +import static cn.hutool.core.collection.CollUtil.findOne; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +/** + * 社交应用的 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class SocialClientApiImpl implements SocialClientApi { + + @Resource + private SocialClientService socialClientService; + @Resource + private SocialUserService socialUserService; + + @Override + public String getAuthorizeUrl(Integer socialType, Integer userType, String redirectUri) { + return socialClientService.getAuthorizeUrl(socialType, userType, redirectUri); + } + + @Override + public SocialWxJsapiSignatureRespDTO createWxMpJsapiSignature(Integer userType, String url) { + WxJsapiSignature signature = socialClientService.createWxMpJsapiSignature(userType, url); + return BeanUtils.toBean(signature, SocialWxJsapiSignatureRespDTO.class); + } + + //======================= 微信小程序独有 ======================= + + @Override + public SocialWxPhoneNumberInfoRespDTO getWxMaPhoneNumberInfo(Integer userType, String phoneCode) { + WxMaPhoneNumberInfo info = socialClientService.getWxMaPhoneNumberInfo(userType, phoneCode); + return BeanUtils.toBean(info, SocialWxPhoneNumberInfoRespDTO.class); + } + + @Override + public byte[] getWxaQrcode(SocialWxQrcodeReqDTO reqVO) { + return socialClientService.getWxaQrcode(reqVO); + } + + @Override + public List getWxaSubscribeTemplateList(Integer userType) { + List list = socialClientService.getSubscribeTemplateList(userType); + return convertList(list, item -> BeanUtils.toBean(item, SocialWxaSubscribeTemplateRespDTO.class).setId(item.getPriTmplId())); + } + + @Override + public void sendWxaSubscribeMessage(SocialWxaSubscribeMessageSendReqDTO reqDTO) { + // 1.1 获得订阅模版列表 + List templateList = socialClientService.getSubscribeTemplateList(reqDTO.getUserType()); + if (CollUtil.isEmpty(templateList)) { + log.warn("[sendSubscribeMessage][reqDTO({}) 发送订阅消息失败,原因:没有找到订阅模板]", reqDTO); + return; + } + // 1.2 获得需要使用的模版 + TemplateInfo template = findOne(templateList, item -> + ObjUtil.equal(item.getTitle(), reqDTO.getTemplateTitle())); + if (template == null) { + log.warn("[sendWxaSubscribeMessage][reqDTO({}) 发送订阅消息失败,原因:没有找到订阅模板]", reqDTO); + return; + } + + // 2. 获得社交用户 + SocialUserRespDTO socialUser = socialUserService.getSocialUserByUserId(reqDTO.getUserType(), reqDTO.getUserId(), + SocialTypeEnum.WECHAT_MINI_PROGRAM.getType()); + if (StrUtil.isBlankIfStr(socialUser.getOpenid())) { + log.warn("[sendWxaSubscribeMessage][reqDTO({}) 发送订阅消息失败,原因:会员 openid 缺失]", reqDTO); + return; + } + + // 3. 发送订阅消息 + socialClientService.sendSubscribeMessage(reqDTO, template.getPriTmplId(), socialUser.getOpenid()); + } + + @Override + public void uploadWxaOrderShippingInfo(Integer userType, SocialWxaOrderUploadShippingInfoReqDTO reqDTO) { + socialClientService.uploadWxaOrderShippingInfo(userType, reqDTO); + } + + @Override + public void notifyWxaOrderConfirmReceive(Integer userType, SocialWxaOrderNotifyConfirmReceiveReqDTO reqDTO) { + socialClientService.notifyWxaOrderConfirmReceive(userType, reqDTO); + } + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialUserApi.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialUserApi.java new file mode 100644 index 0000000..3ddeb05 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialUserApi.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.system.api.social; + +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO; +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO; +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserUnbindReqDTO; +import jakarta.validation.Valid; + +/** + * 社交用户的 API 接口 + * + * @author 芋道源码 + */ +public interface SocialUserApi { + + /** + * 绑定社交用户 + * + * @param reqDTO 绑定信息 + * @return 社交用户 openid + */ + String bindSocialUser(@Valid SocialUserBindReqDTO reqDTO); + + /** + * 取消绑定社交用户 + * + * @param reqDTO 解绑 + */ + void unbindSocialUser(@Valid SocialUserUnbindReqDTO reqDTO); + + /** + * 获得社交用户,基于 userId + * + * @param userType 用户类型 + * @param userId 用户编号 + * @param socialType 社交平台的类型 + * @return 社交用户 + */ + SocialUserRespDTO getSocialUserByUserId(Integer userType, Long userId, Integer socialType); + + /** + * 获得社交用户 + * + * 在认证信息不正确的情况下,也会抛出 {@link ServiceException} 业务异常 + * + * @param userType 用户类型 + * @param socialType 社交平台的类型 + * @param code 授权码 + * @param state state + * @return 社交用户 + */ + SocialUserRespDTO getSocialUserByCode(Integer userType, Integer socialType, String code, String state); + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialUserApiImpl.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialUserApiImpl.java new file mode 100644 index 0000000..34570cb --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialUserApiImpl.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.system.api.social; + +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO; +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO; +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserUnbindReqDTO; +import cn.iocoder.yudao.module.system.service.social.SocialUserService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +/** + * 社交用户的 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class SocialUserApiImpl implements SocialUserApi { + + @Resource + private SocialUserService socialUserService; + + @Override + public String bindSocialUser(SocialUserBindReqDTO reqDTO) { + return socialUserService.bindSocialUser(reqDTO); + } + + @Override + public void unbindSocialUser(SocialUserUnbindReqDTO reqDTO) { + socialUserService.unbindSocialUser(reqDTO.getUserId(), reqDTO.getUserType(), + reqDTO.getSocialType(), reqDTO.getOpenid()); + } + + @Override + public SocialUserRespDTO getSocialUserByUserId(Integer userType, Long userId, Integer socialType) { + return socialUserService.getSocialUserByUserId(userType, userId, socialType); + } + + @Override + public SocialUserRespDTO getSocialUserByCode(Integer userType, Integer socialType, String code, String state) { + return socialUserService.getSocialUserByCode(userType, socialType, code, state); + } + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialUserBindReqDTO.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialUserBindReqDTO.java new file mode 100644 index 0000000..194afbe --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialUserBindReqDTO.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.system.api.social.dto; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 取消绑定社交用户 Request DTO + * + * @author 芋道源码 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SocialUserBindReqDTO { + + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + /** + * 用户类型 + */ + @InEnum(UserTypeEnum.class) + @NotNull(message = "用户类型不能为空") + private Integer userType; + + /** + * 社交平台的类型 + */ + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer socialType; + /** + * 授权码 + */ + @NotEmpty(message = "授权码不能为空") + private String code; + /** + * state + */ + @NotNull(message = "state 不能为空") + private String state; + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialUserRespDTO.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialUserRespDTO.java new file mode 100644 index 0000000..29fef01 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialUserRespDTO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.system.api.social.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 社交用户 Response DTO + * + * @author 芋道源码 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SocialUserRespDTO { + + /** + * 社交用户的 openid + */ + private String openid; + /** + * 社交用户的昵称 + */ + private String nickname; + /** + * 社交用户的头像 + */ + private String avatar; + + /** + * 关联的用户编号 + */ + private Long userId; + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialUserUnbindReqDTO.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialUserUnbindReqDTO.java new file mode 100644 index 0000000..e44956a --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialUserUnbindReqDTO.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.system.api.social.dto; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 社交绑定 Request DTO,使用 code 授权码 + * + * @author 芋道源码 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class SocialUserUnbindReqDTO { + + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + /** + * 用户类型 + */ + @InEnum(UserTypeEnum.class) + @NotNull(message = "用户类型不能为空") + private Integer userType; + + /** + * 社交平台的类型 + */ + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer socialType; + + /** + * 社交平台的 openid + */ + @NotEmpty(message = "社交平台的 openid 不能为空") + private String openid; + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxJsapiSignatureRespDTO.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxJsapiSignatureRespDTO.java new file mode 100644 index 0000000..7d332e9 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxJsapiSignatureRespDTO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.system.api.social.dto; + +import lombok.Data; + +/** + * 微信公众号 JSAPI 签名 Response DTO + * + * @author 芋道源码 + */ +@Data +public class SocialWxJsapiSignatureRespDTO { + + /** + * 微信公众号的 appId + */ + private String appId; + /** + * 匿名串 + */ + private String nonceStr; + /** + * 时间戳 + */ + private Long timestamp; + /** + * URL + */ + private String url; + /** + * 签名 + */ + private String signature; + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxPhoneNumberInfoRespDTO.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxPhoneNumberInfoRespDTO.java new file mode 100644 index 0000000..9d404b3 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxPhoneNumberInfoRespDTO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.system.api.social.dto; + +import lombok.Data; + +/** + * 微信小程序的手机信息 Response DTO + * + * @author 芋道源码 + */ +@Data +public class SocialWxPhoneNumberInfoRespDTO { + + /** + * 用户绑定的手机号(国外手机号会有区号) + */ + private String phoneNumber; + + /** + * 没有区号的手机号 + */ + private String purePhoneNumber; + /** + * 区号 + */ + private String countryCode; + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxQrcodeReqDTO.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxQrcodeReqDTO.java new file mode 100644 index 0000000..6f4b96f --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxQrcodeReqDTO.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.module.system.api.social.dto; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +/** + * 获取小程序码 Request DTO + * + * @author HUIHUI + * @see 获取不限制的小程序码 + */ +@Data +public class SocialWxQrcodeReqDTO { + + /** + * 页面路径不能携带参数(参数请放在scene字段里) + */ + public static final String SCENE = ""; + /** + * 二维码宽度 + */ + public static final Integer WIDTH = 430; + /** + * 自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调 + */ + public static final Boolean AUTO_COLOR = true; + /** + * 检查 page 是否存在 + */ + public static final Boolean CHECK_PATH = true; + /** + * 是否需要透明底色 + * + * hyaline 为 true 时,生成透明底色的小程序码 + */ + public static final Boolean HYALINE = true; + + /** + * 场景 + */ + @NotEmpty(message = "场景不能为空") + private String scene; + /** + * 页面路径 + */ + @NotEmpty(message = "页面路径不能为空") + private String path; + /** + * 二维码宽度 + */ + private Integer width; + + /** + * 是否需要透明底色 + */ + private Boolean autoColor; + /** + * 是否检查 page 是否存在 + */ + private Boolean checkPath; + /** + * 是否需要透明底色 + */ + private Boolean hyaline; + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaOrderNotifyConfirmReceiveReqDTO.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaOrderNotifyConfirmReceiveReqDTO.java new file mode 100644 index 0000000..d2bf62c --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaOrderNotifyConfirmReceiveReqDTO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.system.api.social.dto; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 小程序订单上传购物详情 + * + * @see 上传购物详情 + * @author 芋道源码 + */ +@Data +public class SocialWxaOrderNotifyConfirmReceiveReqDTO { + + /** + * 原支付交易对应的微信订单号 + */ + @NotEmpty(message = "原支付交易对应的微信订单号不能为空") + private String transactionId; + + /** + * 快递签收时间 + */ + @NotNull(message = "快递签收时间不能为空") + private LocalDateTime receivedTime; + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaOrderUploadShippingInfoReqDTO.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaOrderUploadShippingInfoReqDTO.java new file mode 100644 index 0000000..555da62 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaOrderUploadShippingInfoReqDTO.java @@ -0,0 +1,67 @@ +package cn.iocoder.yudao.module.system.api.social.dto; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 小程序订单上传购物详情 + * + * @see 上传购物详情 + * @author 芋道源码 + */ +@Data +public class SocialWxaOrderUploadShippingInfoReqDTO { + + /** + * 物流模式 - 实体物流配送采用快递公司进行实体物流配送形式 + */ + public static final Integer LOGISTICS_TYPE_EXPRESS = 1; + /** + * 物流模式 - 虚拟商品,虚拟商品,例如话费充值,点卡等,无实体配送形式 + */ + public static final Integer LOGISTICS_TYPE_VIRTUAL = 3; + /** + * 物流模式 - 用户自提 + */ + public static final Integer LOGISTICS_TYPE_PICK_UP = 4; + + /** + * 支付者,支付者信息(openid) + */ + @NotEmpty(message = "支付者,支付者信息(openid)不能为空") + private String openid; + + /** + * 原支付交易对应的微信订单号 + */ + @NotEmpty(message = "原支付交易对应的微信订单号不能为空") + private String transactionId; + + /** + * 物流模式 + */ + @NotNull(message = "物流模式不能为空") + private Integer logisticsType; + /** + * 物流发货单号 + */ + private String logisticsNo; + /** + * 物流公司编号 + * + * @see 物流查询插件简介 + */ + private String expressCompany; + /** + * 商品信息 + */ + @NotEmpty(message = "商品信息不能为空") + private String itemDesc; + /** + * 收件人手机号 + */ + @NotEmpty(message = "收件人手机号") + private String receiverContact; + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaSubscribeMessageSendReqDTO.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaSubscribeMessageSendReqDTO.java new file mode 100644 index 0000000..2677a34 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaSubscribeMessageSendReqDTO.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.module.system.api.social.dto; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.HashMap; +import java.util.Map; + +/** + * 微信小程序订阅消息发送 Request DTO + * + * @author HUIHUI + */ +@Data +public class SocialWxaSubscribeMessageSendReqDTO { + + /** + * 用户编号 + * + * 关联 MemberUserDO 的 id 编号 + * 关联 AdminUserDO 的 id 编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + /** + * 用户类型 + * + * 关联 {@link UserTypeEnum} + */ + @NotNull(message = "用户类型不能为空") + private Integer userType; + + /** + * 消息模版标题 + */ + @NotEmpty(message = "消息模版标题不能为空") + private String templateTitle; + + /** + * 点击模板卡片后的跳转页面,仅限本小程序内的页面 + * + * 支持带参数,(示例 index?foo=bar )。该字段不填则模板无跳转。 + */ + private String page; + + /** + * 模板内容的参数 + */ + private Map messages; + + public SocialWxaSubscribeMessageSendReqDTO addMessage(String key, String value) { + if (messages == null) { + messages = new HashMap<>(); + } + messages.put(key, value); + return this; + } + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaSubscribeTemplateRespDTO.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaSubscribeTemplateRespDTO.java new file mode 100644 index 0000000..ae14cc5 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaSubscribeTemplateRespDTO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.system.api.social.dto; + +import lombok.Data; + + +/** + * 小程序订阅消息模版 Response DTO + * + * @author HUIHUI + */ +@Data +public class SocialWxaSubscribeTemplateRespDTO { + + /** + * 模版编号 + */ + private String id; + + /** + * 模版标题 + */ + private String title; + + /** + * 模版内容 + */ + private String content; + + /** + * 模板内容示例 + */ + private String example; + + /** + * 模版类型 + * + * 2:为一次性订阅 + * 3:为长期订阅 + */ + private Integer type; + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java index c647a43..c7e290d 100644 --- a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java @@ -17,6 +17,7 @@ import cn.iocoder.yudao.module.system.service.auth.AdminAuthService; import cn.iocoder.yudao.module.system.service.permission.MenuService; import cn.iocoder.yudao.module.system.service.permission.PermissionService; import cn.iocoder.yudao.module.system.service.permission.RoleService; +import cn.iocoder.yudao.module.system.service.social.SocialClientService; import cn.iocoder.yudao.module.system.service.user.AdminUserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -59,6 +60,8 @@ public class AuthController { @Resource private SecurityProperties securityProperties; + @Resource + private SocialClientService socialClientService; @PostMapping("/login") @PermitAll @Operation(summary = "使用账号密码登录") @@ -147,5 +150,24 @@ public class AuthController { } + @GetMapping("/social-auth-redirect") + @PermitAll + @Operation(summary = "社交授权的跳转") + @Parameters({ + @Parameter(name = "type", description = "社交类型", required = true), + @Parameter(name = "redirectUri", description = "回调路径") + }) + public CommonResult socialLogin(@RequestParam("type") Integer type, + @RequestParam("redirectUri") String redirectUri) { + return success(socialClientService.getAuthorizeUrl( + type, UserTypeEnum.ADMIN.getValue(), redirectUri)); + } + + @PostMapping("/social-login") + @PermitAll + @Operation(summary = "社交快捷登录,使用 code 授权码", description = "适合未登录的用户,但是社交账号已绑定用户") + public CommonResult socialQuickLogin(@RequestBody @Valid AuthSocialLoginReqVO reqVO) { + return success(authService.socialLogin(reqVO)); + } } diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialClientController.http b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialClientController.http new file mode 100644 index 0000000..9909b51 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialClientController.http @@ -0,0 +1,20 @@ +### 请求 /system/social-client/send-subscribe-message 接口 => 发送测试订阅消息 +POST {{baseUrl}}/system/social-client/send-subscribe-message +Authorization: Bearer {{token}} +Content-Type: application/json +#Authorization: Bearer test100 +tenant-id: {{adminTenantId}} + +{ + "userId": 247, + "userType": 1, + "socialType": 34, + "templateTitle": "充值成功通知", + "page": "", + "messages": { + "character_string1":"5616122165165", + "amount2":"1000.00", + "time3":"2024-01-01 10:10:10", + "phrase4": "充值成功" + } +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialClientController.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialClientController.java new file mode 100644 index 0000000..f33de40 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialClientController.java @@ -0,0 +1,94 @@ +package cn.iocoder.yudao.module.system.controller.admin.socail; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.system.api.social.SocialClientApi; +import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO; +import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientRespVO; +import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialClientDO; +import cn.iocoder.yudao.module.system.service.social.SocialClientService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 社交客户端") +@RestController +@RequestMapping("/system/social-client") +@Validated +public class SocialClientController { + + @Resource + private SocialClientService socialClientService; + @Resource + private SocialClientApi socialClientApi; + + @PostMapping("/create") + @Operation(summary = "创建社交客户端") + @PreAuthorize("@ss.hasPermission('system:social-client:create')") + public CommonResult createSocialClient(@Valid @RequestBody SocialClientSaveReqVO createReqVO) { + return success(socialClientService.createSocialClient(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新社交客户端") + @PreAuthorize("@ss.hasPermission('system:social-client:update')") + public CommonResult updateSocialClient(@Valid @RequestBody SocialClientSaveReqVO updateReqVO) { + socialClientService.updateSocialClient(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除社交客户端") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('system:social-client:delete')") + public CommonResult deleteSocialClient(@RequestParam("id") Long id) { + socialClientService.deleteSocialClient(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Parameter(name = "ids", description = "编号列表", required = true) + @Operation(summary = "批量删除社交客户端") + @PreAuthorize("@ss.hasPermission('system:social-client:delete')") + public CommonResult deleteSocialClientList(@RequestParam("ids") List ids) { + socialClientService.deleteSocialClientList(ids); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得社交客户端") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:social-client:query')") + public CommonResult getSocialClient(@RequestParam("id") Long id) { + SocialClientDO client = socialClientService.getSocialClient(id); + return success(BeanUtils.toBean(client, SocialClientRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得社交客户端分页") + @PreAuthorize("@ss.hasPermission('system:social-client:query')") + public CommonResult> getSocialClientPage(@Valid SocialClientPageReqVO pageVO) { + PageResult pageResult = socialClientService.getSocialClientPage(pageVO); + return success(BeanUtils.toBean(pageResult, SocialClientRespVO.class)); + } + + @PostMapping("/send-subscribe-message") + @Operation(summary = "发送订阅消息") // 用于测试 + @PreAuthorize("@ss.hasPermission('system:social-client:query')") + public void sendSubscribeMessage(@RequestBody SocialWxaSubscribeMessageSendReqDTO reqDTO) { + socialClientApi.sendWxaSubscribeMessage(reqDTO); + } + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialUserController.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialUserController.java new file mode 100644 index 0000000..f26ca4d --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialUserController.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.module.system.controller.admin.socail; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO; +import cn.iocoder.yudao.module.system.controller.admin.socail.vo.user.SocialUserBindReqVO; +import cn.iocoder.yudao.module.system.controller.admin.socail.vo.user.SocialUserPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.socail.vo.user.SocialUserRespVO; +import cn.iocoder.yudao.module.system.controller.admin.socail.vo.user.SocialUserUnbindReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserDO; +import cn.iocoder.yudao.module.system.service.social.SocialUserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 社交用户") +@RestController +@RequestMapping("/system/social-user") +@Validated +public class SocialUserController { + + @Resource + private SocialUserService socialUserService; + + @PostMapping("/bind") + @Operation(summary = "社交绑定,使用 code 授权码") + public CommonResult socialBind(@RequestBody @Valid SocialUserBindReqVO reqVO) { + socialUserService.bindSocialUser(new SocialUserBindReqDTO().setSocialType(reqVO.getType()) + .setCode(reqVO.getCode()).setState(reqVO.getState()) + .setUserId(getLoginUserId()).setUserType(UserTypeEnum.ADMIN.getValue())); + return CommonResult.success(true); + } + + @DeleteMapping("/unbind") + @Operation(summary = "取消社交绑定") + public CommonResult socialUnbind(@RequestBody SocialUserUnbindReqVO reqVO) { + socialUserService.unbindSocialUser(getLoginUserId(), UserTypeEnum.ADMIN.getValue(), reqVO.getType(), reqVO.getOpenid()); + return CommonResult.success(true); + } + + @GetMapping("/get-bind-list") + @Operation(summary = "获得绑定社交用户列表") + public CommonResult> getBindSocialUserList() { + List list = socialUserService.getSocialUserList(getLoginUserId(), UserTypeEnum.ADMIN.getValue()); + return success(convertList(list, socialUser -> new SocialUserRespVO() // 返回精简信息 + .setId(socialUser.getId()).setType(socialUser.getType()).setOpenid(socialUser.getOpenid()) + .setNickname(socialUser.getNickname()).setAvatar(socialUser.getNickname()))); + } + + // ==================== 社交用户 CRUD ==================== + + @GetMapping("/get") + @Operation(summary = "获得社交用户") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:social-user:query')") + public CommonResult getSocialUser(@RequestParam("id") Long id) { + SocialUserDO socialUser = socialUserService.getSocialUser(id); + return success(BeanUtils.toBean(socialUser, SocialUserRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得社交用户分页") + @PreAuthorize("@ss.hasPermission('system:social-user:query')") + public CommonResult> getSocialUserPage(@Valid SocialUserPageReqVO pageVO) { + PageResult pageResult = socialUserService.getSocialUserPage(pageVO); + return success(BeanUtils.toBean(pageResult, SocialUserRespVO.class)); + } + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/client/SocialClientPageReqVO.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/client/SocialClientPageReqVO.java new file mode 100644 index 0000000..a225530 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/client/SocialClientPageReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.system.controller.admin.socail.vo.client; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 社交客户端分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SocialClientPageReqVO extends PageParam { + + @Schema(description = "应用名", example = "yudao商城") + private String name; + + @Schema(description = "社交平台的类型", example = "31") + private Integer socialType; + + @Schema(description = "用户类型", example = "2") + private Integer userType; + + @Schema(description = "客户端编号", example = "145442115") + private String clientId; + + @Schema(description = "状态", example = "1") + private Integer status; + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/client/SocialClientRespVO.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/client/SocialClientRespVO.java new file mode 100644 index 0000000..900f363 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/client/SocialClientRespVO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.system.controller.admin.socail.vo.client; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 社交客户端 Response VO") +@Data +public class SocialClientRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "27162") + private Long id; + + @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao商城") + private String name; + + @Schema(description = "社交平台的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "31") + private Integer socialType; + + @Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer userType; + + @Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "wwd411c69a39ad2e54") + private String clientId; + + @Schema(description = "客户端密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "peter") + private String clientSecret; + + @Schema(description = "授权方的网页应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2000045") + private String agentId; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/client/SocialClientSaveReqVO.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/client/SocialClientSaveReqVO.java new file mode 100644 index 0000000..9549b82 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/client/SocialClientSaveReqVO.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.module.system.controller.admin.socail.vo.client; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; +import java.util.Objects; + +@Schema(description = "管理后台 - 社交客户端创建/修改 Request VO") +@Data +public class SocialClientSaveReqVO { + + @Schema(description = "编号", example = "27162") + private Long id; + + @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao商城") + @NotNull(message = "应用名不能为空") + private String name; + + @Schema(description = "社交平台的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "31") + @NotNull(message = "社交平台的类型不能为空") + @InEnum(SocialTypeEnum.class) + private Integer socialType; + + @Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "用户类型不能为空") + @InEnum(UserTypeEnum.class) + private Integer userType; + + @Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "wwd411c69a39ad2e54") + @NotNull(message = "客户端编号不能为空") + private String clientId; + + @Schema(description = "客户端密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "peter") + @NotNull(message = "客户端密钥不能为空") + private String clientSecret; + + @Schema(description = "授权方的网页应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2000045") + private String agentId; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @AssertTrue(message = "agentId 不能为空") + @JsonIgnore + public boolean isAgentIdValid() { + // 如果是企业微信,必须填写 agentId 属性 + return !Objects.equals(socialType, SocialTypeEnum.WECHAT_ENTERPRISE.getType()) + || !StrUtil.isEmpty(agentId); + } + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/user/SocialUserBindReqVO.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/user/SocialUserBindReqVO.java new file mode 100644 index 0000000..5ed0042 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/user/SocialUserBindReqVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.system.controller.admin.socail.vo.user; + +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 社交绑定 Request VO,使用 code 授权码") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SocialUserBindReqVO { + + @Schema(description = "社交平台的类型,参见 UserSocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer type; + + @Schema(description = "授权码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "授权码不能为空") + private String code; + + @Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") + @NotEmpty(message = "state 不能为空") + private String state; + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/user/SocialUserPageReqVO.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/user/SocialUserPageReqVO.java new file mode 100644 index 0000000..3297db4 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/user/SocialUserPageReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.system.controller.admin.socail.vo.user; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 社交用户分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SocialUserPageReqVO extends PageParam { + + @Schema(description = "社交平台的类型", example = "30") + private Integer type; + + @Schema(description = "用户昵称", example = "李四") + private String nickname; + + @Schema(description = "社交 openid", example = "oz-Jdt0kd_jdhUxJHQdBJMlOFN7w") + private String openid; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/user/SocialUserRespVO.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/user/SocialUserRespVO.java new file mode 100644 index 0000000..fd37703 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/user/SocialUserRespVO.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.system.controller.admin.socail.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 社交用户 Response VO") +@Data +public class SocialUserRespVO { + + @Schema(description = "主键(自增策略)", requiredMode = Schema.RequiredMode.REQUIRED, example = "14569") + private Long id; + + @Schema(description = "社交平台的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "30") + private Integer type; + + @Schema(description = "社交 openid", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") + private String openid; + + @Schema(description = "社交 token", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") + private String token; + + @Schema(description = "原始 Token 数据,一般是 JSON 格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}") + private String rawTokenInfo; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + private String nickname; + + @Schema(description = "用户头像", example = "https://www.iocoder.cn/xxx.png") + private String avatar; + + @Schema(description = "原始用户数据,一般是 JSON 格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}") + private String rawUserInfo; + + @Schema(description = "最后一次的认证 code", requiredMode = Schema.RequiredMode.REQUIRED, example = "666666") + private String code; + + @Schema(description = "最后一次的认证 state", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + private String state; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/user/SocialUserUnbindReqVO.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/user/SocialUserUnbindReqVO.java new file mode 100644 index 0000000..e5b1961 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/user/SocialUserUnbindReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.system.controller.admin.socail.vo.user; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 取消社交绑定 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SocialUserUnbindReqVO { + + @Schema(description = "社交平台的类型,参见 UserSocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer type; + + @Schema(description = "社交用户的 openid", requiredMode = Schema.RequiredMode.REQUIRED, example = "IPRmJ0wvBptiPIlGEZiPewGwiEiE") + @NotEmpty(message = "社交用户的 openid 不能为空") + private String openid; + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java index c15c5fa..6f39f65 100644 --- a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java @@ -52,7 +52,7 @@ public class TenantController { @PermitAll @TenantIgnore @Operation(summary = "获取租户精简信息列表", description = "只包含被开启的租户,用于【首页】功能的选择租户选项") - public CommonResult> getTenantSimpleList() { + public CommonResult> getTenantSimpleList() { List list = tenantService.getTenantListByStatus(CommonStatusEnum.ENABLE.getStatus()); return success(convertList(list, tenantDO -> new TenantRespVO().setId(tenantDO.getId()).setName(tenantDO.getName()))); diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/social/SocialClientDO.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/social/SocialClientDO.java new file mode 100644 index 0000000..5449106 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/social/SocialClientDO.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.social; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; +import me.zhyd.oauth.config.AuthConfig; + +/** + * 社交客户端 DO + * + * 对应 {@link AuthConfig} 配置,满足不同租户,有自己的客户端配置,实现社交(三方)登录 + * + * @author 芋道源码 + */ +@TableName(value = "system_social_client", autoResultMap = true) +@KeySequence("system_social_client_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SocialClientDO extends TenantBaseDO { + + /** + * 编号,自增 + */ + @TableId + private Long id; + /** + * 应用名 + */ + private String name; + /** + * 社交类型 + * + * 枚举 {@link SocialTypeEnum} + */ + private Integer socialType; + /** + * 用户类型 + * + * 目的:不同用户类型,对应不同的小程序,需要自己的配置 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + + /** + * 客户端 id + */ + private String clientId; + /** + * 客户端 Secret + */ + private String clientSecret; + + /** + * 代理编号 + * + * 目前只有部分“社交类型”在使用: + * 1. 企业微信:对应授权方的网页应用 ID + */ + private String agentId; + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/social/SocialUserBindDO.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/social/SocialUserBindDO.java new file mode 100644 index 0000000..0f4e41f --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/social/SocialUserBindDO.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.social; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 社交用户的绑定 + * 即 {@link SocialUserDO} 与 UserDO 的关联表 + * + * @author 芋道源码 + */ +@TableName(value = "system_social_user_bind", autoResultMap = true) +@KeySequence("system_social_user_bind_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SocialUserBindDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 关联的用户编号 + * + * 关联 UserDO 的编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + + /** + * 社交平台的用户编号 + * + * 关联 {@link SocialUserDO#getId()} + */ + private Long socialUserId; + /** + * 社交平台的类型 + * + * 冗余 {@link SocialUserDO#getType()} + */ + private Integer socialType; + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/social/SocialUserDO.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/social/SocialUserDO.java new file mode 100644 index 0000000..f3adf03 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/social/SocialUserDO.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.social; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 社交(三方)用户 + * + * @author weir + */ +@TableName(value = "system_social_user", autoResultMap = true) +@KeySequence("system_social_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SocialUserDO extends BaseDO { + + /** + * 自增主键 + */ + @TableId + private Long id; + /** + * 社交平台的类型 + * + * 枚举 {@link SocialTypeEnum} + */ + private Integer type; + + /** + * 社交 openid + */ + private String openid; + /** + * 社交 token + */ + private String token; + /** + * 原始 Token 数据,一般是 JSON 格式 + */ + private String rawTokenInfo; + + /** + * 用户昵称 + */ + private String nickname; + /** + * 用户头像 + */ + private String avatar; + /** + * 原始用户数据,一般是 JSON 格式 + */ + private String rawUserInfo; + + /** + * 最后一次的认证 code + */ + private String code; + /** + * 最后一次的认证 state + */ + private String state; + +} + + diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialClientMapper.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialClientMapper.java new file mode 100644 index 0000000..4ebe226 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialClientMapper.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.system.dal.mysql.social; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialClientDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SocialClientMapper extends BaseMapperX { + + default SocialClientDO selectBySocialTypeAndUserType(Integer socialType, Integer userType) { + return selectOne(SocialClientDO::getSocialType, socialType, + SocialClientDO::getUserType, userType); + } + + default PageResult selectPage(SocialClientPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(SocialClientDO::getName, reqVO.getName()) + .eqIfPresent(SocialClientDO::getSocialType, reqVO.getSocialType()) + .eqIfPresent(SocialClientDO::getUserType, reqVO.getUserType()) + .likeIfPresent(SocialClientDO::getClientId, reqVO.getClientId()) + .eqIfPresent(SocialClientDO::getStatus, reqVO.getStatus()) + .orderByDesc(SocialClientDO::getId)); + } + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserBindMapper.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserBindMapper.java new file mode 100644 index 0000000..d9957f7 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserBindMapper.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.system.dal.mysql.social; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserBindDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface SocialUserBindMapper extends BaseMapperX { + + default void deleteByUserTypeAndUserIdAndSocialType(Integer userType, Long userId, Integer socialType) { + delete(new LambdaQueryWrapperX() + .eq(SocialUserBindDO::getUserType, userType) + .eq(SocialUserBindDO::getUserId, userId) + .eq(SocialUserBindDO::getSocialType, socialType)); + } + + default void deleteByUserTypeAndSocialUserId(Integer userType, Long socialUserId) { + delete(new LambdaQueryWrapperX() + .eq(SocialUserBindDO::getUserType, userType) + .eq(SocialUserBindDO::getSocialUserId, socialUserId)); + } + + default SocialUserBindDO selectByUserTypeAndSocialUserId(Integer userType, Long socialUserId) { + return selectOne(SocialUserBindDO::getUserType, userType, + SocialUserBindDO::getSocialUserId, socialUserId); + } + + default List selectListByUserIdAndUserType(Long userId, Integer userType) { + return selectList(new LambdaQueryWrapperX() + .eq(SocialUserBindDO::getUserId, userId) + .eq(SocialUserBindDO::getUserType, userType)); + } + + default SocialUserBindDO selectByUserIdAndUserTypeAndSocialType(Long userId, Integer userType, Integer socialType) { + return selectOne(new LambdaQueryWrapperX() + .eq(SocialUserBindDO::getUserId, userId) + .eq(SocialUserBindDO::getUserType, userType) + .eq(SocialUserBindDO::getSocialType, socialType)); + } + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserMapper.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserMapper.java new file mode 100644 index 0000000..a90e6ac --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserMapper.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.system.dal.mysql.social; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.system.controller.admin.socail.vo.user.SocialUserPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SocialUserMapper extends BaseMapperX { + + default SocialUserDO selectByTypeAndCodeAnState(Integer type, String code, String state) { + return selectOne(SocialUserDO::getType, type, + SocialUserDO::getCode, code, + SocialUserDO::getState, state); + } + + default SocialUserDO selectByTypeAndOpenid(Integer type, String openid) { + return selectFirstOne(SocialUserDO::getType, type, + SocialUserDO::getOpenid, openid); + } + + default PageResult selectPage(SocialUserPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(SocialUserDO::getType, reqVO.getType()) + .likeIfPresent(SocialUserDO::getNickname, reqVO.getNickname()) + .likeIfPresent(SocialUserDO::getOpenid, reqVO.getOpenid()) + .betweenIfPresent(SocialUserDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(SocialUserDO::getId)); + } + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/justauth/config/YudaoJustAuthConfiguration.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/justauth/config/YudaoJustAuthConfiguration.java new file mode 100644 index 0000000..56a24ee --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/justauth/config/YudaoJustAuthConfiguration.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.system.framework.justauth.config; + +import cn.iocoder.yudao.module.system.framework.justauth.core.AuthRequestFactory; +import com.xkcoding.justauth.autoconfigure.JustAuthProperties; +import com.xkcoding.justauth.support.cache.RedisStateCache; +import me.zhyd.oauth.cache.AuthStateCache; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.RedisTemplate; + +/** + * JustAuth 配置类 TODO 芋艿:等 justauth 1.4.1 版本发布!!! + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties({JustAuthProperties.class}) +public class YudaoJustAuthConfiguration { + + @Bean + @ConditionalOnProperty( + prefix = "justauth", + value = {"enabled"}, + havingValue = "true", + matchIfMissing = true + ) + public AuthRequestFactory authRequestFactory(JustAuthProperties properties, AuthStateCache authStateCache) { + return new AuthRequestFactory(properties, authStateCache); + } + + @Bean + public AuthStateCache authStateCache(RedisTemplate justAuthRedisCacheTemplate, + JustAuthProperties justAuthProperties) { + return new RedisStateCache(justAuthRedisCacheTemplate, justAuthProperties.getCache()); + } + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/justauth/core/AuthRequestFactory.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/justauth/core/AuthRequestFactory.java new file mode 100644 index 0000000..afd37ed --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/justauth/core/AuthRequestFactory.java @@ -0,0 +1,322 @@ +/* + * Copyright (c) 2019-2029, xkcoding & Yangkai.Shen & 沈扬凯 (237497819@qq.com & xkcoding.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package cn.iocoder.yudao.module.system.framework.justauth.core; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.EnumUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.xkcoding.http.config.HttpConfig; +import com.xkcoding.justauth.autoconfigure.ExtendProperties; +import com.xkcoding.justauth.autoconfigure.JustAuthProperties; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.zhyd.oauth.cache.AuthStateCache; +import me.zhyd.oauth.config.AuthConfig; +import me.zhyd.oauth.config.AuthDefaultSource; +import me.zhyd.oauth.config.AuthSource; +import me.zhyd.oauth.enums.AuthResponseStatus; +import me.zhyd.oauth.exception.AuthException; +import me.zhyd.oauth.request.*; +import org.springframework.util.CollectionUtils; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +// TODO @芋艿:等官方发布 1.4.1!!! + +/** + *

+ * AuthRequest工厂类 + *

+ * + * @author yangkai.shen + * @date Created in 2019-07-22 14:21 + */ +@Slf4j +@RequiredArgsConstructor +public class AuthRequestFactory { + private final JustAuthProperties properties; + private final AuthStateCache authStateCache; + + /** + * 返回当前Oauth列表 + * + * @return Oauth列表 + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public List oauthList() { + // 默认列表 + List defaultList = new ArrayList<>(properties.getType().keySet()); + // 扩展列表 + List extendList = new ArrayList<>(); + ExtendProperties extend = properties.getExtend(); + if (null != extend) { + Class enumClass = extend.getEnumClass(); + List names = EnumUtil.getNames(enumClass); + // 扩展列表 + extendList = extend.getConfig() + .keySet() + .stream() + .filter(x -> names.contains(x.toUpperCase())) + .map(String::toUpperCase) + .collect(Collectors.toList()); + } + + // 合并 + return (List) CollUtil.addAll(defaultList, extendList); + } + + /** + * 返回AuthRequest对象 + * + * @param source {@link AuthSource} + * @return {@link AuthRequest} + */ + public AuthRequest get(String source) { + if (StrUtil.isBlank(source)) { + throw new AuthException(AuthResponseStatus.NO_AUTH_SOURCE); + } + + // 获取 JustAuth 中已存在的 + AuthRequest authRequest = getDefaultRequest(source); + + // 如果获取不到则尝试取自定义的 + if (authRequest == null) { + authRequest = getExtendRequest(properties.getExtend().getEnumClass(), source); + } + + if (authRequest == null) { + throw new AuthException(AuthResponseStatus.UNSUPPORTED); + } + + return authRequest; + } + + /** + * 获取自定义的 request + * + * @param clazz 枚举类 {@link AuthSource} + * @param source {@link AuthSource} + * @return {@link AuthRequest} + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private AuthRequest getExtendRequest(Class clazz, String source) { + String upperSource = source.toUpperCase(); + try { + EnumUtil.fromString(clazz, upperSource); + } catch (IllegalArgumentException e) { + // 无自定义匹配 + return null; + } + + Map extendConfig = properties.getExtend().getConfig(); + + // key 转大写 + Map upperConfig = new HashMap<>(6); + extendConfig.forEach((k, v) -> upperConfig.put(k.toUpperCase(), v)); + + ExtendProperties.ExtendRequestConfig extendRequestConfig = upperConfig.get(upperSource); + if (extendRequestConfig != null) { + + // 配置 http config + configureHttpConfig(upperSource, extendRequestConfig, properties.getHttpConfig()); + + Class requestClass = extendRequestConfig.getRequestClass(); + + if (requestClass != null) { + // 反射获取 Request 对象,所以必须实现 2 个参数的构造方法 + return ReflectUtil.newInstance(requestClass, (AuthConfig) extendRequestConfig, authStateCache); + } + } + + return null; + } + + + /** + * 获取默认的 Request + * + * @param source {@link AuthSource} + * @return {@link AuthRequest} + */ + private AuthRequest getDefaultRequest(String source) { + AuthDefaultSource authDefaultSource; + + try { + authDefaultSource = EnumUtil.fromString(AuthDefaultSource.class, source.toUpperCase()); + } catch (IllegalArgumentException e) { + // 无自定义匹配 + return null; + } + + AuthConfig config = properties.getType().get(authDefaultSource.name()); + // 找不到对应关系,直接返回空 + if (config == null) { + return null; + } + + // 配置 http config + configureHttpConfig(authDefaultSource.name(), config, properties.getHttpConfig()); + + switch (authDefaultSource) { + case GITHUB: + return new AuthGithubRequest(config, authStateCache); + case WEIBO: + return new AuthWeiboRequest(config, authStateCache); + case GITEE: + return new AuthGiteeRequest(config, authStateCache); + case DINGTALK: + return new AuthDingTalkRequest(config, authStateCache); + case DINGTALK_V2: + return new AuthDingTalkV2Request(config, authStateCache); + case DINGTALK_ACCOUNT: + return new AuthDingTalkAccountRequest(config, authStateCache); + case BAIDU: + return new AuthBaiduRequest(config, authStateCache); + case CSDN: + return new AuthCsdnRequest(config, authStateCache); + case CODING: + return new AuthCodingRequest(config, authStateCache); + case OSCHINA: + return new AuthOschinaRequest(config, authStateCache); + case ALIPAY: + return new AuthAlipayRequest(config, authStateCache); + case QQ: + return new AuthQqRequest(config, authStateCache); + case WECHAT_OPEN: + return new AuthWeChatOpenRequest(config, authStateCache); + case WECHAT_MP: + return new AuthWeChatMpRequest(config, authStateCache); + case TAOBAO: + return new AuthTaobaoRequest(config, authStateCache); + case GOOGLE: + return new AuthGoogleRequest(config, authStateCache); + case FACEBOOK: + return new AuthFacebookRequest(config, authStateCache); + case DOUYIN: + return new AuthDouyinRequest(config, authStateCache); + case LINKEDIN: + return new AuthLinkedinRequest(config, authStateCache); + case MICROSOFT: + return new AuthMicrosoftRequest(config, authStateCache); + case MICROSOFT_CN: + return new AuthMicrosoftCnRequest(config, authStateCache); + + case MI: + return new AuthMiRequest(config, authStateCache); + case TOUTIAO: + return new AuthToutiaoRequest(config, authStateCache); + case TEAMBITION: + return new AuthTeambitionRequest(config, authStateCache); + case RENREN: + return new AuthRenrenRequest(config, authStateCache); + case PINTEREST: + return new AuthPinterestRequest(config, authStateCache); + case STACK_OVERFLOW: + return new AuthStackOverflowRequest(config, authStateCache); + case HUAWEI: + return new AuthHuaweiRequest(config, authStateCache); + case HUAWEI_V3: + return new AuthHuaweiV3Request(config, authStateCache); + case WECHAT_ENTERPRISE: + return new AuthWeChatEnterpriseQrcodeRequest(config, authStateCache); + case WECHAT_ENTERPRISE_V2: + return new AuthWeChatEnterpriseQrcodeV2Request(config, authStateCache); + case WECHAT_ENTERPRISE_QRCODE_THIRD: + return new AuthWeChatEnterpriseThirdQrcodeRequest(config, authStateCache); + case WECHAT_ENTERPRISE_WEB: + return new AuthWeChatEnterpriseWebRequest(config, authStateCache); + case KUJIALE: + return new AuthKujialeRequest(config, authStateCache); + case GITLAB: + return new AuthGitlabRequest(config, authStateCache); + case MEITUAN: + return new AuthMeituanRequest(config, authStateCache); + case ELEME: + return new AuthElemeRequest(config, authStateCache); + case TWITTER: + return new AuthTwitterRequest(config, authStateCache); + case FEISHU: + return new AuthFeishuRequest(config, authStateCache); + case JD: + return new AuthJdRequest(config, authStateCache); + case ALIYUN: + return new AuthAliyunRequest(config, authStateCache); + case XMLY: + return new AuthXmlyRequest(config, authStateCache); + case AMAZON: + return new AuthAmazonRequest(config, authStateCache); + case SLACK: + return new AuthSlackRequest(config, authStateCache); + case LINE: + return new AuthLineRequest(config, authStateCache); + case OKTA: + return new AuthOktaRequest(config, authStateCache); + case PROGINN: + return new AuthProginnRequest(config,authStateCache); + case AFDIAN: + return new AuthAfDianRequest(config,authStateCache); + case APPLE: + return new AuthAppleRequest(config,authStateCache); + case FIGMA: + return new AuthFigmaRequest(config,authStateCache); + case WECHAT_MINI_PROGRAM: + config.setIgnoreCheckRedirectUri(true); + config.setIgnoreCheckState(true); + return new AuthWechatMiniProgramRequest(config, authStateCache); + case QQ_MINI_PROGRAM: + config.setIgnoreCheckRedirectUri(true); + config.setIgnoreCheckState(true); + return new AuthQQMiniProgramRequest(config, authStateCache); + default: + return null; + } + } + + /** + * 配置 http 相关的配置 + * + * @param authSource {@link AuthSource} + * @param authConfig {@link AuthConfig} + */ + private void configureHttpConfig(String authSource, AuthConfig authConfig, JustAuthProperties.JustAuthHttpConfig httpConfig) { + if (null == httpConfig) { + return; + } + Map proxyConfigMap = httpConfig.getProxy(); + if (CollectionUtils.isEmpty(proxyConfigMap)) { + return; + } + JustAuthProperties.JustAuthProxyConfig proxyConfig = proxyConfigMap.get(authSource); + + if (null == proxyConfig) { + return; + } + + authConfig.setHttpConfig(HttpConfig.builder() + .timeout(httpConfig.getTimeout()) + .proxy(new Proxy(Proxy.Type.valueOf(proxyConfig.getType()), new InetSocketAddress(proxyConfig.getHostname(), proxyConfig.getPort()))) + .build()); + } +} \ No newline at end of file diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/justauth/package-info.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/justauth/package-info.java new file mode 100644 index 0000000..e9af3ab --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/justauth/package-info.java @@ -0,0 +1,6 @@ +/** + * justauth 三方登录的拓展 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.module.system.framework.justauth; \ No newline at end of file diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java index 8c30958..5916c78 100644 --- a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java @@ -56,6 +56,14 @@ public interface AdminAuthService { /** + * 社交快捷登录,使用 code 授权码 + * + * @param reqVO 登录信息 + * @return 登录结果 + */ + AuthLoginRespVO socialLogin(@Valid AuthSocialLoginReqVO reqVO); + + /** * 刷新访问令牌 * * @param refreshToken 刷新令牌 diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java index 0159f9b..b1aac03 100644 --- a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java @@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO; import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi; import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO; +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO; import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*; import cn.iocoder.yudao.module.system.convert.auth.AuthConvert; import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; @@ -20,6 +21,7 @@ import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum; import cn.iocoder.yudao.module.system.service.logger.LoginLogService; import cn.iocoder.yudao.module.system.service.member.MemberService; import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService; +import cn.iocoder.yudao.module.system.service.social.SocialUserService; import cn.iocoder.yudao.module.system.service.user.AdminUserService; import com.anji.captcha.model.common.ResponseModel; import com.anji.captcha.model.vo.CaptchaVO; @@ -63,6 +65,8 @@ public class AdminAuthServiceImpl implements AdminAuthService { private CaptchaService captchaService; @Resource private SmsCodeApi smsCodeApi; + @Resource + private SocialUserService socialUserService; /** * 验证码的开关,默认为 true @@ -138,6 +142,25 @@ public class AdminAuthServiceImpl implements AdminAuthService { return createTokenAfterLoginSuccess(user.getId(), reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE); } + @Override + public AuthLoginRespVO socialLogin(AuthSocialLoginReqVO reqVO) { + // 使用 code 授权码,进行登录。然后,获得到绑定的用户编号 + SocialUserRespDTO socialUser = socialUserService.getSocialUserByCode(UserTypeEnum.ADMIN.getValue(), reqVO.getType(), + reqVO.getCode(), reqVO.getState()); + if (socialUser == null || socialUser.getUserId() == null) { + throw exception(AUTH_THIRD_LOGIN_NOT_BIND); + } + + // 获得用户 + AdminUserDO user = userService.getUser(socialUser.getUserId()); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL); + } + private void createLoginLog(Long userId, String username, LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) { // 插入登录日志 diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientService.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientService.java new file mode 100644 index 0000000..ee08ccf --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientService.java @@ -0,0 +1,160 @@ +package cn.iocoder.yudao.module.system.service.social; + +import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.api.social.dto.SocialWxQrcodeReqDTO; +import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaOrderNotifyConfirmReceiveReqDTO; +import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaOrderUploadShippingInfoReqDTO; +import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO; +import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialClientDO; +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import jakarta.validation.Valid; +import me.chanjar.weixin.common.bean.WxJsapiSignature; +import me.chanjar.weixin.common.bean.subscribemsg.TemplateInfo; +import me.zhyd.oauth.model.AuthUser; + +import java.util.List; + +/** + * 社交应用 Service 接口 + * + * @author 芋道源码 + */ +public interface SocialClientService { + + /** + * 获得社交平台的授权 URL + * + * @param socialType 社交平台的类型 {@link SocialTypeEnum} + * @param userType 用户类型 + * @param redirectUri 重定向 URL + * @return 社交平台的授权 URL + */ + String getAuthorizeUrl(Integer socialType, Integer userType, String redirectUri); + + /** + * 请求社交平台,获得授权的用户 + * + * @param socialType 社交平台的类型 + * @param userType 用户类型 + * @param code 授权码 + * @param state 授权 state + * @return 授权的用户 + */ + AuthUser getAuthUser(Integer socialType, Integer userType, String code, String state); + + // =================== 微信公众号独有 =================== + + /** + * 创建微信公众号的 JS SDK 初始化所需的签名 + * + * @param userType 用户类型 + * @param url 访问的 URL 地址 + * @return 签名 + */ + WxJsapiSignature createWxMpJsapiSignature(Integer userType, String url); + + // =================== 微信小程序独有 =================== + + /** + * 获得微信小程序的手机信息 + * + * @param userType 用户类型 + * @param phoneCode 手机授权码 + * @return 手机信息 + */ + WxMaPhoneNumberInfo getWxMaPhoneNumberInfo(Integer userType, String phoneCode); + + /** + * 获得小程序二维码 + * + * @param reqVO 请求信息 + * @return 小程序二维码 + */ + byte[] getWxaQrcode(SocialWxQrcodeReqDTO reqVO); + + /** + * 获得微信小程订阅模板 + * + * 缓存的目的:考虑到微信小程序订阅消息选择好模版后几乎不会变动,缓存增加查询效率 + * + * @param userType 用户类型 + * @return 微信小程订阅模板 + */ + List getSubscribeTemplateList(Integer userType); + + /** + * 发送微信小程序订阅消息 + * + * @param reqDTO 请求 + * @param templateId 模版编号 + * @param openId 会员 openId + */ + void sendSubscribeMessage(SocialWxaSubscribeMessageSendReqDTO reqDTO, String templateId, String openId); + + /** + * 上传订单发货到微信小程序 + * + * @param userType 用户类型 + * @param reqDTO 请求 + */ + void uploadWxaOrderShippingInfo(Integer userType, SocialWxaOrderUploadShippingInfoReqDTO reqDTO); + + /** + * 通知订单收货到微信小程序 + * + * @param userType 用户类型 + * @param reqDTO 请求 + */ + void notifyWxaOrderConfirmReceive(Integer userType, SocialWxaOrderNotifyConfirmReceiveReqDTO reqDTO); + + // =================== 客户端管理 =================== + + /** + * 创建社交客户端 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createSocialClient(@Valid SocialClientSaveReqVO createReqVO); + + /** + * 更新社交客户端 + * + * @param updateReqVO 更新信息 + */ + void updateSocialClient(@Valid SocialClientSaveReqVO updateReqVO); + + /** + * 删除社交客户端 + * + * @param id 编号 + */ + void deleteSocialClient(Long id); + + /** + * 批量删除社交客户端 + * + * @param ids 编号数组 + */ + void deleteSocialClientList(List ids); + + /** + * 获得社交客户端 + * + * @param id 编号 + * @return 社交客户端 + */ + SocialClientDO getSocialClient(Long id); + + /** + * 获得社交客户端分页 + * + * @param pageReqVO 分页查询 + * @return 社交客户端分页 + */ + PageResult getSocialClientPage(SocialClientPageReqVO pageReqVO); + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java new file mode 100644 index 0000000..759c701 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java @@ -0,0 +1,510 @@ +package cn.iocoder.yudao.module.system.service.social; + +import cn.binarywang.wx.miniapp.api.WxMaService; +import cn.binarywang.wx.miniapp.api.WxMaSubscribeService; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl; +import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; +import cn.binarywang.wx.miniapp.bean.WxMaSubscribeMessage; +import cn.binarywang.wx.miniapp.bean.shop.request.shipping.*; +import cn.binarywang.wx.miniapp.bean.shop.response.WxMaOrderShippingInfoBaseResponse; +import cn.binarywang.wx.miniapp.config.impl.WxMaRedisBetterConfigImpl; +import cn.binarywang.wx.miniapp.constant.WxMaConstants; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.DesensitizedUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.cache.CacheUtils; +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.system.api.social.dto.SocialWxQrcodeReqDTO; +import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaOrderNotifyConfirmReceiveReqDTO; +import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaOrderUploadShippingInfoReqDTO; +import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO; +import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialClientDO; +import cn.iocoder.yudao.module.system.dal.mysql.social.SocialClientMapper; +import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants; +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import cn.iocoder.yudao.module.system.framework.justauth.core.AuthRequestFactory; +import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties; +import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import jakarta.annotation.Resource; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.bean.WxJsapiSignature; +import me.chanjar.weixin.common.bean.subscribemsg.TemplateInfo; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl; +import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl; +import me.zhyd.oauth.config.AuthConfig; +import me.zhyd.oauth.model.AuthCallback; +import me.zhyd.oauth.model.AuthResponse; +import me.zhyd.oauth.model.AuthUser; +import me.zhyd.oauth.request.AuthRequest; +import me.zhyd.oauth.utils.AuthStateUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.UTC_MS_WITH_XXX_OFFSET_FORMATTER; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.toEpochSecond; +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; +import static java.util.Collections.singletonList; + +/** + * 社交应用 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class SocialClientServiceImpl implements SocialClientService { + + /** + * 小程序码要打开的小程序版本 + * + * 1. release:正式版 + * 2. trial:体验版 + * 3. developer:开发版 + */ + @Value("${yudao.wxa-code.env-version:release}") + public String envVersion; + /** + * 订阅消息跳转小程序类型 + * + * 1. developer:开发版 + * 2. trial:体验版 + * 3. formal:正式版 + */ + @Value("${yudao.wxa-subscribe-message.miniprogram-state:formal}") + public String miniprogramState; + + @Resource + private AuthRequestFactory authRequestFactory; + + @Resource + private WxMpService wxMpService; + @Resource + private WxMpProperties wxMpProperties; + @Resource + private StringRedisTemplate stringRedisTemplate; // WxMpService 需要使用到,所以在 Service 注入了它 + /** + * 缓存 WxMpService 对象 + * + * key:使用微信公众号的 appId + secret 拼接,即 {@link SocialClientDO} 的 clientId 和 clientSecret 属性。 + * 为什么 key 使用这种格式?因为 {@link SocialClientDO} 在管理后台可以变更,通过这个 key 存储它的单例。 + * + * 为什么要做 WxMpService 缓存?因为 WxMpService 构建成本比较大,所以尽量保证它是单例。 + */ + private final LoadingCache wxMpServiceCache = CacheUtils.buildAsyncReloadingCache( + Duration.ofSeconds(10L), + new CacheLoader() { + + @Override + public WxMpService load(String key) { + String[] keys = key.split(":"); + return buildWxMpService(keys[0], keys[1]); + } + + }); + + @Resource + private WxMaService wxMaService; + @Resource + private WxMaProperties wxMaProperties; + /** + * 缓存 WxMaService 对象 + * + * 说明同 {@link #wxMpServiceCache} 变量 + */ + private final LoadingCache wxMaServiceCache = CacheUtils.buildAsyncReloadingCache( + Duration.ofSeconds(10L), + new CacheLoader() { + + @Override + public WxMaService load(String key) { + String[] keys = key.split(":"); + return buildWxMaService(keys[0], keys[1]); + } + + }); + + @Resource + private SocialClientMapper socialClientMapper; + + @Override + public String getAuthorizeUrl(Integer socialType, Integer userType, String redirectUri) { + // 获得对应的 AuthRequest 实现 + AuthRequest authRequest = buildAuthRequest(socialType, userType); + // 生成跳转地址 + String authorizeUri = authRequest.authorize(AuthStateUtils.createState()); + return HttpUtils.replaceUrlQuery(authorizeUri, "redirect_uri", redirectUri); + } + + @Override + public AuthUser getAuthUser(Integer socialType, Integer userType, String code, String state) { + // 构建请求 + AuthRequest authRequest = buildAuthRequest(socialType, userType); + AuthCallback authCallback = AuthCallback.builder().code(code).state(state).build(); + // 执行请求 + AuthResponse authResponse = authRequest.login(authCallback); + log.info("[getAuthUser][请求社交平台 type({}) request({}) response({})]", socialType, + toJsonString(authCallback), toJsonString(authResponse)); + if (!authResponse.ok()) { + throw exception(SOCIAL_USER_AUTH_FAILURE, authResponse.getMsg()); + } + return (AuthUser) authResponse.getData(); + } + + /** + * 构建 AuthRequest 对象,支持多租户配置 + * + * @param socialType 社交类型 + * @param userType 用户类型 + * @return AuthRequest 对象 + */ + @VisibleForTesting + AuthRequest buildAuthRequest(Integer socialType, Integer userType) { + // 1. 先查找默认的配置项,从 application-*.yaml 中读取 + AuthRequest request = authRequestFactory.get(SocialTypeEnum.valueOfType(socialType).getSource()); + Assert.notNull(request, String.format("社交平台(%d) 不存在", socialType)); + // 2. 查询 DB 的配置项,如果存在则进行覆盖 + SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType(socialType, userType); + if (client != null && Objects.equals(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + // 2.1 构造新的 AuthConfig 对象 + AuthConfig authConfig = (AuthConfig) ReflectUtil.getFieldValue(request, "config"); + AuthConfig newAuthConfig = ReflectUtil.newInstance(authConfig.getClass()); + BeanUtil.copyProperties(authConfig, newAuthConfig); + // 2.2 修改对应的 clientId + clientSecret 密钥 + newAuthConfig.setClientId(client.getClientId()); + newAuthConfig.setClientSecret(client.getClientSecret()); + if (client.getAgentId() != null) { // 如果有 agentId 则修改 agentId + newAuthConfig.setAgentId(client.getAgentId()); + } + // 2.3 设置会 request 里,进行后续使用 + ReflectUtil.setFieldValue(request, "config", newAuthConfig); + } + return request; + } + + // =================== 微信公众号独有 =================== + + @Override + @SneakyThrows + public WxJsapiSignature createWxMpJsapiSignature(Integer userType, String url) { + WxMpService service = getWxMpService(userType); + return service.createJsapiSignature(url); + } + + /** + * 获得 clientId + clientSecret 对应的 WxMpService 对象 + * + * @param userType 用户类型 + * @return WxMpService 对象 + */ + @VisibleForTesting + WxMpService getWxMpService(Integer userType) { + // 第一步,查询 DB 的配置项,获得对应的 WxMpService 对象 + SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType( + SocialTypeEnum.WECHAT_MP.getType(), userType); + if (client != null && Objects.equals(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + return wxMpServiceCache.getUnchecked(client.getClientId() + ":" + client.getClientSecret()); + } + // 第二步,不存在 DB 配置项,则使用 application-*.yaml 对应的 WxMpService 对象 + return wxMpService; + } + + /** + * 创建 clientId + clientSecret 对应的 WxMpService 对象 + * + * @param clientId 微信公众号 appId + * @param clientSecret 微信公众号 secret + * @return WxMpService 对象 + */ + public WxMpService buildWxMpService(String clientId, String clientSecret) { + // 第一步,创建 WxMpRedisConfigImpl 对象 + WxMpRedisConfigImpl configStorage = new WxMpRedisConfigImpl( + new RedisTemplateWxRedisOps(stringRedisTemplate), + wxMpProperties.getConfigStorage().getKeyPrefix()); + configStorage.setAppId(clientId); + configStorage.setSecret(clientSecret); + + // 第二步,创建 WxMpService 对象 + WxMpService service = new WxMpServiceImpl(); + service.setWxMpConfigStorage(configStorage); + return service; + } + + // =================== 微信小程序独有 =================== + + @Override + public WxMaPhoneNumberInfo getWxMaPhoneNumberInfo(Integer userType, String phoneCode) { + WxMaService service = getWxMaService(userType); + try { + return service.getUserService().getPhoneNumber(phoneCode); + } catch (WxErrorException e) { + log.error("[getPhoneNumber][userType({}) phoneCode({}) 获得手机号失败]", userType, phoneCode, e); + throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_PHONE_CODE_ERROR); + } + } + + @Override + public byte[] getWxaQrcode(SocialWxQrcodeReqDTO reqVO) { + WxMaService service = getWxMaService(UserTypeEnum.MEMBER.getValue()); + try { + return service.getQrcodeService().createWxaCodeUnlimitBytes( + ObjUtil.defaultIfEmpty(reqVO.getScene(), SocialWxQrcodeReqDTO.SCENE), + reqVO.getPath(), + ObjUtil.defaultIfNull(reqVO.getCheckPath(), SocialWxQrcodeReqDTO.CHECK_PATH), + envVersion, + ObjUtil.defaultIfNull(reqVO.getWidth(), SocialWxQrcodeReqDTO.WIDTH), + ObjUtil.defaultIfNull(reqVO.getAutoColor(), SocialWxQrcodeReqDTO.AUTO_COLOR), + null, + ObjUtil.defaultIfNull(reqVO.getHyaline(), SocialWxQrcodeReqDTO.HYALINE)); + } catch (WxErrorException e) { + log.error("[getWxQrcode][reqVO({}) 获得小程序码失败]", reqVO, e); + throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_QRCODE_ERROR); + } + } + + @Override + @Cacheable(cacheNames = RedisKeyConstants.WXA_SUBSCRIBE_TEMPLATE, key = "#userType", + unless = "#result == null") + public List getSubscribeTemplateList(Integer userType) { + WxMaService service = getWxMaService(userType); + try { + WxMaSubscribeService subscribeService = service.getSubscribeService(); + return subscribeService.getTemplateList(); + } catch (WxErrorException e) { + log.error("[getSubscribeTemplate][userType({}) 获得小程序订阅消息模版]", userType, e); + throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_SUBSCRIBE_TEMPLATE_ERROR); + } + } + + @Override + public void sendSubscribeMessage(SocialWxaSubscribeMessageSendReqDTO reqDTO, String templateId, String openId) { + WxMaService service = getWxMaService(reqDTO.getUserType()); + try { + WxMaSubscribeService subscribeService = service.getSubscribeService(); + subscribeService.sendSubscribeMsg(buildMessageSendReqDTO(reqDTO, templateId, openId)); + } catch (WxErrorException e) { + log.error("[sendSubscribeMessage][reqVO({}) templateId({}) openId({}) 发送小程序订阅消息失败]", reqDTO, templateId, openId, e); + throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_SUBSCRIBE_MESSAGE_ERROR); + } + } + + /** + * 构建发送消息请求参数 + * + * @param reqDTO 请求 + * @param templateId 模版编号 + * @param openId 会员 openId + * @return 微信小程序订阅消息请求参数 + */ + private WxMaSubscribeMessage buildMessageSendReqDTO(SocialWxaSubscribeMessageSendReqDTO reqDTO, + String templateId, String openId) { + // 设置订阅消息基本参数 + WxMaSubscribeMessage subscribeMessage = new WxMaSubscribeMessage().setLang(WxMaConstants.MiniProgramLang.ZH_CN) + .setMiniprogramState(miniprogramState).setTemplateId(templateId).setToUser(openId).setPage(reqDTO.getPage()); + // 设置具体消息参数 + Map messages = reqDTO.getMessages(); + if (CollUtil.isNotEmpty(messages)) { + reqDTO.getMessages().keySet().forEach(key -> findAndThen(messages, key, value -> + subscribeMessage.addData(new WxMaSubscribeMessage.MsgData(key, value)))); + } + return subscribeMessage; + } + + @Override + public void uploadWxaOrderShippingInfo(Integer userType, SocialWxaOrderUploadShippingInfoReqDTO reqDTO) { + WxMaService service = getWxMaService(userType); + List shippingList; + if (Objects.equals(reqDTO.getLogisticsType(), SocialWxaOrderUploadShippingInfoReqDTO.LOGISTICS_TYPE_EXPRESS)) { + shippingList = singletonList(ShippingListBean.builder() + .trackingNo(reqDTO.getLogisticsNo()) + .expressCompany(reqDTO.getExpressCompany()) + .itemDesc(reqDTO.getItemDesc()) + .contact(ContactBean.builder().receiverContact(DesensitizedUtil.mobilePhone(reqDTO.getReceiverContact())).build()) + .build()); + } else { + shippingList = singletonList(ShippingListBean.builder().itemDesc(reqDTO.getItemDesc()).build()); + } + WxMaOrderShippingInfoUploadRequest request = WxMaOrderShippingInfoUploadRequest.builder() + .orderKey(OrderKeyBean.builder() + .orderNumberType(2) // 使用原支付交易对应的微信订单号,即渠道单号 + .transactionId(reqDTO.getTransactionId()) + .build()) + .logisticsType(reqDTO.getLogisticsType()) // 配送方式 + .deliveryMode(1) // 统一发货 + .shippingList(shippingList) + .payer(PayerBean.builder().openid(reqDTO.getOpenid()).build()) + .uploadTime(ZonedDateTime.now().format(UTC_MS_WITH_XXX_OFFSET_FORMATTER)) + .build(); + try { + WxMaOrderShippingInfoBaseResponse response = service.getWxMaOrderShippingService().upload(request); + if (response.getErrCode() != 0) { + log.error("[uploadWxaOrderShippingInfo][上传微信小程序发货信息失败:request({}) response({})]", request, response); + throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR, response.getErrMsg()); + } + log.info("[uploadWxaOrderShippingInfo][上传微信小程序发货信息成功:request({}) response({})]", request, response); + } catch (WxErrorException ex) { + log.error("[uploadWxaOrderShippingInfo][上传微信小程序发货信息失败:request({})]", request, ex); + throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR, ex.getError().getErrorMsg()); + } + } + + @Override + public void notifyWxaOrderConfirmReceive(Integer userType, SocialWxaOrderNotifyConfirmReceiveReqDTO reqDTO) { + WxMaService service = getWxMaService(userType); + WxMaOrderShippingInfoNotifyConfirmRequest request = WxMaOrderShippingInfoNotifyConfirmRequest.builder() + .transactionId(reqDTO.getTransactionId()) + .receivedTime(toEpochSecond(reqDTO.getReceivedTime())) + .build(); + try { + WxMaOrderShippingInfoBaseResponse response = service.getWxMaOrderShippingService().notifyConfirmReceive(request); + if (response.getErrCode() != 0) { + log.error("[notifyWxaOrderConfirmReceive][确认收货提醒到微信小程序失败:request({}) response({})]", request, response); + throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_NOTIFY_CONFIRM_RECEIVE_ERROR, response.getErrMsg()); + } + log.info("[notifyWxaOrderConfirmReceive][确认收货提醒到微信小程序成功:request({}) response({})]", request, response); + } catch (WxErrorException ex) { + log.error("[notifyWxaOrderConfirmReceive][确认收货提醒到微信小程序失败:request({})]", request, ex); + throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_NOTIFY_CONFIRM_RECEIVE_ERROR, ex.getError().getErrorMsg()); + } + } + + /** + * 获得 clientId + clientSecret 对应的 WxMpService 对象 + * + * @param userType 用户类型 + * @return WxMpService 对象 + */ + @VisibleForTesting + WxMaService getWxMaService(Integer userType) { + // 第一步,查询 DB 的配置项,获得对应的 WxMaService 对象 + SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType( + SocialTypeEnum.WECHAT_MINI_PROGRAM.getType(), userType); + if (client != null && Objects.equals(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + return wxMaServiceCache.getUnchecked(client.getClientId() + ":" + client.getClientSecret()); + } + // 第二步,不存在 DB 配置项,则使用 application-*.yaml 对应的 WxMaService 对象 + return wxMaService; + } + + /** + * 创建 clientId + clientSecret 对应的 WxMaService 对象 + * + * @param clientId 微信小程序 appId + * @param clientSecret 微信小程序 secret + * @return WxMaService 对象 + */ + private WxMaService buildWxMaService(String clientId, String clientSecret) { + // 第一步,创建 WxMaRedisBetterConfigImpl 对象 + WxMaRedisBetterConfigImpl configStorage = new WxMaRedisBetterConfigImpl( + new RedisTemplateWxRedisOps(stringRedisTemplate), + wxMaProperties.getConfigStorage().getKeyPrefix()); + configStorage.setAppid(clientId); + configStorage.setSecret(clientSecret); + + // 第二步,创建 WxMpService 对象 + WxMaService service = new WxMaServiceImpl(); + service.setWxMaConfig(configStorage); + return service; + } + + // =================== 客户端管理 =================== + + @Override + public Long createSocialClient(SocialClientSaveReqVO createReqVO) { + // 校验重复 + validateSocialClientUnique(null, createReqVO.getUserType(), createReqVO.getSocialType()); + + // 插入 + SocialClientDO client = BeanUtils.toBean(createReqVO, SocialClientDO.class); + socialClientMapper.insert(client); + return client.getId(); + } + + @Override + public void updateSocialClient(SocialClientSaveReqVO updateReqVO) { + // 校验存在 + validateSocialClientExists(updateReqVO.getId()); + // 校验重复 + validateSocialClientUnique(updateReqVO.getId(), updateReqVO.getUserType(), updateReqVO.getSocialType()); + + // 更新 + SocialClientDO updateObj = BeanUtils.toBean(updateReqVO, SocialClientDO.class); + socialClientMapper.updateById(updateObj); + } + + @Override + public void deleteSocialClient(Long id) { + // 校验存在 + validateSocialClientExists(id); + // 删除 + socialClientMapper.deleteById(id); + } + + @Override + public void deleteSocialClientList(List ids) { + socialClientMapper.deleteByIds(ids); + } + + private void validateSocialClientExists(Long id) { + if (socialClientMapper.selectById(id) == null) { + throw exception(SOCIAL_CLIENT_NOT_EXISTS); + } + } + + /** + * 校验社交应用是否重复,需要保证 userType + socialType 唯一 + * 原因是,不同端(userType)选择某个社交登录(socialType)时,需要通过 {@link #buildAuthRequest(Integer, Integer)} 构建对应的请求 + * + * @param id 编号 + * @param userType 用户类型 + * @param socialType 社交类型 + */ + private void validateSocialClientUnique(Long id, Integer userType, Integer socialType) { + SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType( + socialType, userType); + if (client == null) { + return; + } + if (id == null // 新增时,说明重复 + || ObjUtil.notEqual(id, client.getId())) { // 更新时,如果 id 不一致,说明重复 + throw exception(SOCIAL_CLIENT_UNIQUE); + } + } + + @Override + public SocialClientDO getSocialClient(Long id) { + return socialClientMapper.selectById(id); + } + + @Override + public PageResult getSocialClientPage(SocialClientPageReqVO pageReqVO) { + return socialClientMapper.selectPage(pageReqVO); + } + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserService.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserService.java new file mode 100644 index 0000000..743e580 --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserService.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.system.service.social; + +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO; +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO; +import cn.iocoder.yudao.module.system.controller.admin.socail.vo.user.SocialUserPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserDO; +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * 社交用户 Service 接口,例如说社交平台的授权登录 + * + * @author 芋道源码 + */ +public interface SocialUserService { + + /** + * 获得指定用户的社交用户列表 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @return 社交用户列表 + */ + List getSocialUserList(Long userId, Integer userType); + + /** + * 绑定社交用户 + * + * @param reqDTO 绑定信息 + * @return 社交用户 openid + */ + String bindSocialUser(@Valid SocialUserBindReqDTO reqDTO); + + /** + * 取消绑定社交用户 + * + * @param userId 用户编号 + * @param userType 全局用户类型 + * @param socialType 社交平台的类型 {@link SocialTypeEnum} + * @param openid 社交平台的 openid + */ + void unbindSocialUser(Long userId, Integer userType, Integer socialType, String openid); + + /** + * 获得社交用户,基于 userId + * + * @param userType 用户类型 + * @param userId 用户编号 + * @param socialType 社交平台的类型 + * @return 社交用户 + */ + SocialUserRespDTO getSocialUserByUserId(Integer userType, Long userId, Integer socialType); + + /** + * 获得社交用户 + * + * 在认证信息不正确的情况下,也会抛出 {@link ServiceException} 业务异常 + * + * @param userType 用户类型 + * @param socialType 社交平台的类型 + * @param code 授权码 + * @param state state + * @return 社交用户 + */ + SocialUserRespDTO getSocialUserByCode(Integer userType, Integer socialType, String code, String state); + + // ==================== 社交用户 CRUD ==================== + + /** + * 获得社交用户 + * + * @param id 编号 + * @return 社交用户 + */ + SocialUserDO getSocialUser(Long id); + + /** + * 获得社交用户分页 + * + * @param pageReqVO 分页查询 + * @return 社交用户分页 + */ + PageResult getSocialUserPage(SocialUserPageReqVO pageReqVO); + +} diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceImpl.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceImpl.java new file mode 100644 index 0000000..476f31a --- /dev/null +++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceImpl.java @@ -0,0 +1,173 @@ +package cn.iocoder.yudao.module.system.service.social; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO; +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO; +import cn.iocoder.yudao.module.system.controller.admin.socail.vo.user.SocialUserPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserBindDO; +import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserDO; +import cn.iocoder.yudao.module.system.dal.mysql.social.SocialUserBindMapper; +import cn.iocoder.yudao.module.system.dal.mysql.social.SocialUserMapper; +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import jakarta.annotation.Resource; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import me.zhyd.oauth.model.AuthUser; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SOCIAL_USER_NOT_FOUND; + +/** + * 社交用户 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class SocialUserServiceImpl implements SocialUserService { + + @Resource + private SocialUserBindMapper socialUserBindMapper; + @Resource + private SocialUserMapper socialUserMapper; + + @Resource + private SocialClientService socialClientService; + + @Override + public List getSocialUserList(Long userId, Integer userType) { + // 获得绑定 + List socialUserBinds = socialUserBindMapper.selectListByUserIdAndUserType(userId, userType); + if (CollUtil.isEmpty(socialUserBinds)) { + return Collections.emptyList(); + } + // 获得社交用户 + return socialUserMapper.selectByIds(convertSet(socialUserBinds, SocialUserBindDO::getSocialUserId)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String bindSocialUser(SocialUserBindReqDTO reqDTO) { + // 获得社交用户 + SocialUserDO socialUser = authSocialUser(reqDTO.getSocialType(), reqDTO.getUserType(), + reqDTO.getCode(), reqDTO.getState()); + Assert.notNull(socialUser, "社交用户不能为空"); + + // 社交用户可能之前绑定过别的用户,需要进行解绑 + socialUserBindMapper.deleteByUserTypeAndSocialUserId(reqDTO.getUserType(), socialUser.getId()); + + // 用户可能之前已经绑定过该社交类型,需要进行解绑 + socialUserBindMapper.deleteByUserTypeAndUserIdAndSocialType(reqDTO.getUserType(), reqDTO.getUserId(), + socialUser.getType()); + + // 绑定当前登录的社交用户 + SocialUserBindDO socialUserBind = SocialUserBindDO.builder() + .userId(reqDTO.getUserId()).userType(reqDTO.getUserType()) + .socialUserId(socialUser.getId()).socialType(socialUser.getType()).build(); + socialUserBindMapper.insert(socialUserBind); + return socialUser.getOpenid(); + } + + @Override + public void unbindSocialUser(Long userId, Integer userType, Integer socialType, String openid) { + // 获得 openid 对应的 SocialUserDO 社交用户 + SocialUserDO socialUser = socialUserMapper.selectByTypeAndOpenid(socialType, openid); + if (socialUser == null) { + throw exception(SOCIAL_USER_NOT_FOUND); + } + + // 获得对应的社交绑定关系 + socialUserBindMapper.deleteByUserTypeAndUserIdAndSocialType(userType, userId, socialUser.getType()); + } + + @Override + public SocialUserRespDTO getSocialUserByUserId(Integer userType, Long userId, Integer socialType) { + // 获得绑定用户 + SocialUserBindDO socialUserBind = socialUserBindMapper.selectByUserIdAndUserTypeAndSocialType(userId, userType, socialType); + if (socialUserBind == null) { + return null; + } + // 获得社交用户 + SocialUserDO socialUser = socialUserMapper.selectById(socialUserBind.getSocialUserId()); + Assert.notNull(socialUser, "社交用户不能为空"); + return new SocialUserRespDTO(socialUser.getOpenid(), socialUser.getNickname(), socialUser.getAvatar(), + socialUserBind.getUserId()); + } + + @Override + public SocialUserRespDTO getSocialUserByCode(Integer userType, Integer socialType, String code, String state) { + // 获得社交用户 + SocialUserDO socialUser = authSocialUser(socialType, userType, code, state); + Assert.notNull(socialUser, "社交用户不能为空"); + + // 获得绑定用户 + SocialUserBindDO socialUserBind = socialUserBindMapper.selectByUserTypeAndSocialUserId(userType, + socialUser.getId()); + return new SocialUserRespDTO(socialUser.getOpenid(), socialUser.getNickname(), socialUser.getAvatar(), + socialUserBind != null ? socialUserBind.getUserId() : null); + } + + /** + * 授权获得对应的社交用户 + * 如果授权失败,则会抛出 {@link ServiceException} 异常 + * + * @param socialType 社交平台的类型 {@link SocialTypeEnum} + * @param userType 用户类型 + * @param code 授权码 + * @param state state + * @return 授权用户 + */ + @NotNull + public SocialUserDO authSocialUser(Integer socialType, Integer userType, String code, String state) { + // 优先从 DB 中获取,因为 code 有且可以使用一次。 + // 在社交登录时,当未绑定 User 时,需要绑定登录,此时需要 code 使用两次 + SocialUserDO socialUser = socialUserMapper.selectByTypeAndCodeAnState(socialType, code, state); + if (socialUser != null) { + return socialUser; + } + + // 请求获取 + AuthUser authUser = socialClientService.getAuthUser(socialType, userType, code, state); + Assert.notNull(authUser, "三方用户不能为空"); + + // 保存到 DB 中 + socialUser = socialUserMapper.selectByTypeAndOpenid(socialType, authUser.getUuid()); + if (socialUser == null) { + socialUser = new SocialUserDO(); + } + socialUser.setType(socialType).setCode(code).setState(state) // 需要保存 code + state 字段,保证后续可查询 + .setOpenid(authUser.getUuid()).setToken(authUser.getToken().getAccessToken()).setRawTokenInfo((toJsonString(authUser.getToken()))) + .setNickname(authUser.getNickname()).setAvatar(authUser.getAvatar()).setRawUserInfo(toJsonString(authUser.getRawUserInfo())); + if (socialUser.getId() == null) { + socialUserMapper.insert(socialUser); + } else { + socialUserMapper.updateById(socialUser); + } + return socialUser; + } + + // ==================== 社交用户 CRUD ==================== + + @Override + public SocialUserDO getSocialUser(Long id) { + return socialUserMapper.selectById(id); + } + + @Override + public PageResult getSocialUserPage(SocialUserPageReqVO pageReqVO) { + return socialUserMapper.selectPage(pageReqVO); + } + +} diff --git a/cc-admin-master/yudao-server/src/main/resources/application-local.yaml b/cc-admin-master/yudao-server/src/main/resources/application-local.yaml index 180dd96..bebd598 100644 --- a/cc-admin-master/yudao-server/src/main/resources/application-local.yaml +++ b/cc-admin-master/yudao-server/src/main/resources/application-local.yaml @@ -48,9 +48,9 @@ spring: primary: master datasource: master: - url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://192.168.0.180:3306/lock-dev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root - password: roomasd111 + password: Gsking164411 driver-class-name: com.mysql.cj.jdbc.Driver # MySQL Connector/J 8.X 连接的示例 # tdengine: # url: jdbc:TAOS-RS://192.168.0.180:6041/test @@ -180,6 +180,12 @@ wx: app-id: wxf56b1542b9e85f8a # 测试号(Kongdy 提供的) secret: 496379dcef1ba869e9234de8d598cfd3 # 存储配置,解决 AccessToken 的跨节点的共享 + cp: + # 你的企业ID + corpId: ww6e1eee0a8ae45397 + agentId: 1000002 + corpSecret: ITbfuoZkmUifGoDL5ZB8SyuMzVM8VXZNkfZJzYn5sGo + config-storage: type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取 key-prefix: wx # Redis Key 的前缀