56 changed files with 3308 additions and 60 deletions
@ -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; |
||||
|
} |
||||
|
} |
@ -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<PlanItemEntity> planItemEntityList; |
||||
|
|
||||
|
|
||||
|
} |
||||
|
|
@ -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<PlanLifeLockEntity> lockEntities; |
||||
|
} |
@ -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<PlanItemDetailEntity> planItemDetailEntityList; |
||||
|
|
||||
|
} |
@ -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; |
||||
|
|
||||
|
|
||||
|
} |
@ -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; |
||||
|
|
||||
|
|
||||
|
} |
@ -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<SocialWxaSubscribeTemplateRespDTO> 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); |
||||
|
|
||||
|
} |
@ -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<SocialWxaSubscribeTemplateRespDTO> getWxaSubscribeTemplateList(Integer userType) { |
||||
|
List<TemplateInfo> 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<TemplateInfo> 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); |
||||
|
} |
||||
|
|
||||
|
} |
@ -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); |
||||
|
|
||||
|
} |
@ -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); |
||||
|
} |
||||
|
|
||||
|
} |
@ -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; |
||||
|
|
||||
|
} |
@ -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; |
||||
|
|
||||
|
} |
@ -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; |
||||
|
|
||||
|
} |
@ -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; |
||||
|
|
||||
|
} |
@ -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; |
||||
|
|
||||
|
} |
@ -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 <a href="https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/qr-code/getUnlimitedQRCode.html">获取不限制的小程序码</a> |
||||
|
*/ |
||||
|
@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; |
||||
|
|
||||
|
} |
@ -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 <a href="https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/shopping-order/normal-shopping-detail/uploadShoppingInfo.html">上传购物详情</a> |
||||
|
* @author 芋道源码 |
||||
|
*/ |
||||
|
@Data |
||||
|
public class SocialWxaOrderNotifyConfirmReceiveReqDTO { |
||||
|
|
||||
|
/** |
||||
|
* 原支付交易对应的微信订单号 |
||||
|
*/ |
||||
|
@NotEmpty(message = "原支付交易对应的微信订单号不能为空") |
||||
|
private String transactionId; |
||||
|
|
||||
|
/** |
||||
|
* 快递签收时间 |
||||
|
*/ |
||||
|
@NotNull(message = "快递签收时间不能为空") |
||||
|
private LocalDateTime receivedTime; |
||||
|
|
||||
|
} |
@ -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 <a href="https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/shopping-order/normal-shopping-detail/uploadShoppingInfo.html">上传购物详情</a> |
||||
|
* @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 <a href="https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/express_search.html#%E8%8E%B7%E5%8F%96%E8%BF%90%E5%8A%9Bid%E5%88%97%E8%A1%A8get-delivery-list">物流查询插件简介</a> |
||||
|
*/ |
||||
|
private String expressCompany; |
||||
|
/** |
||||
|
* 商品信息 |
||||
|
*/ |
||||
|
@NotEmpty(message = "商品信息不能为空") |
||||
|
private String itemDesc; |
||||
|
/** |
||||
|
* 收件人手机号 |
||||
|
*/ |
||||
|
@NotEmpty(message = "收件人手机号") |
||||
|
private String receiverContact; |
||||
|
|
||||
|
} |
@ -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<String, String> messages; |
||||
|
|
||||
|
public SocialWxaSubscribeMessageSendReqDTO addMessage(String key, String value) { |
||||
|
if (messages == null) { |
||||
|
messages = new HashMap<>(); |
||||
|
} |
||||
|
messages.put(key, value); |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
} |
@ -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; |
||||
|
|
||||
|
} |
@ -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": "充值成功" |
||||
|
} |
||||
|
} |
@ -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<Long> createSocialClient(@Valid @RequestBody SocialClientSaveReqVO createReqVO) { |
||||
|
return success(socialClientService.createSocialClient(createReqVO)); |
||||
|
} |
||||
|
|
||||
|
@PutMapping("/update") |
||||
|
@Operation(summary = "更新社交客户端") |
||||
|
@PreAuthorize("@ss.hasPermission('system:social-client:update')") |
||||
|
public CommonResult<Boolean> 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<Boolean> 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<Boolean> deleteSocialClientList(@RequestParam("ids") List<Long> 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<SocialClientRespVO> 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<PageResult<SocialClientRespVO>> getSocialClientPage(@Valid SocialClientPageReqVO pageVO) { |
||||
|
PageResult<SocialClientDO> 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); |
||||
|
} |
||||
|
|
||||
|
} |
@ -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<Boolean> 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<Boolean> 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<List<SocialUserRespVO>> getBindSocialUserList() { |
||||
|
List<SocialUserDO> 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<SocialUserRespVO> 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<PageResult<SocialUserRespVO>> getSocialUserPage(@Valid SocialUserPageReqVO pageVO) { |
||||
|
PageResult<SocialUserDO> pageResult = socialUserService.getSocialUserPage(pageVO); |
||||
|
return success(BeanUtils.toBean(pageResult, SocialUserRespVO.class)); |
||||
|
} |
||||
|
|
||||
|
} |
@ -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; |
||||
|
|
||||
|
} |
@ -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; |
||||
|
|
||||
|
} |
@ -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); |
||||
|
} |
||||
|
|
||||
|
} |
@ -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; |
||||
|
|
||||
|
} |
@ -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; |
||||
|
|
||||
|
} |
@ -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; |
||||
|
|
||||
|
} |
@ -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; |
||||
|
|
||||
|
} |
@ -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; |
||||
|
|
||||
|
} |
@ -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; |
||||
|
|
||||
|
} |
@ -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; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
|
@ -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<SocialClientDO> { |
||||
|
|
||||
|
default SocialClientDO selectBySocialTypeAndUserType(Integer socialType, Integer userType) { |
||||
|
return selectOne(SocialClientDO::getSocialType, socialType, |
||||
|
SocialClientDO::getUserType, userType); |
||||
|
} |
||||
|
|
||||
|
default PageResult<SocialClientDO> selectPage(SocialClientPageReqVO reqVO) { |
||||
|
return selectPage(reqVO, new LambdaQueryWrapperX<SocialClientDO>() |
||||
|
.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)); |
||||
|
} |
||||
|
|
||||
|
} |
@ -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<SocialUserBindDO> { |
||||
|
|
||||
|
default void deleteByUserTypeAndUserIdAndSocialType(Integer userType, Long userId, Integer socialType) { |
||||
|
delete(new LambdaQueryWrapperX<SocialUserBindDO>() |
||||
|
.eq(SocialUserBindDO::getUserType, userType) |
||||
|
.eq(SocialUserBindDO::getUserId, userId) |
||||
|
.eq(SocialUserBindDO::getSocialType, socialType)); |
||||
|
} |
||||
|
|
||||
|
default void deleteByUserTypeAndSocialUserId(Integer userType, Long socialUserId) { |
||||
|
delete(new LambdaQueryWrapperX<SocialUserBindDO>() |
||||
|
.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<SocialUserBindDO> selectListByUserIdAndUserType(Long userId, Integer userType) { |
||||
|
return selectList(new LambdaQueryWrapperX<SocialUserBindDO>() |
||||
|
.eq(SocialUserBindDO::getUserId, userId) |
||||
|
.eq(SocialUserBindDO::getUserType, userType)); |
||||
|
} |
||||
|
|
||||
|
default SocialUserBindDO selectByUserIdAndUserTypeAndSocialType(Long userId, Integer userType, Integer socialType) { |
||||
|
return selectOne(new LambdaQueryWrapperX<SocialUserBindDO>() |
||||
|
.eq(SocialUserBindDO::getUserId, userId) |
||||
|
.eq(SocialUserBindDO::getUserType, userType) |
||||
|
.eq(SocialUserBindDO::getSocialType, socialType)); |
||||
|
} |
||||
|
|
||||
|
} |
@ -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<SocialUserDO> { |
||||
|
|
||||
|
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<SocialUserDO> selectPage(SocialUserPageReqVO reqVO) { |
||||
|
return selectPage(reqVO, new LambdaQueryWrapperX<SocialUserDO>() |
||||
|
.eqIfPresent(SocialUserDO::getType, reqVO.getType()) |
||||
|
.likeIfPresent(SocialUserDO::getNickname, reqVO.getNickname()) |
||||
|
.likeIfPresent(SocialUserDO::getOpenid, reqVO.getOpenid()) |
||||
|
.betweenIfPresent(SocialUserDO::getCreateTime, reqVO.getCreateTime()) |
||||
|
.orderByDesc(SocialUserDO::getId)); |
||||
|
} |
||||
|
|
||||
|
} |
@ -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<String, String> justAuthRedisCacheTemplate, |
||||
|
JustAuthProperties justAuthProperties) { |
||||
|
return new RedisStateCache(justAuthRedisCacheTemplate, justAuthProperties.getCache()); |
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,322 @@ |
|||||
|
/* |
||||
|
* Copyright (c) 2019-2029, xkcoding & Yangkai.Shen & 沈扬凯 (237497819@qq.com & xkcoding.com). |
||||
|
* <p> |
||||
|
* 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 |
||||
|
* <p> |
||||
|
* http://www.gnu.org/licenses/lgpl.html
|
||||
|
* <p> |
||||
|
* 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!!!
|
||||
|
|
||||
|
/** |
||||
|
* <p> |
||||
|
* AuthRequest工厂类 |
||||
|
* </p> |
||||
|
* |
||||
|
* @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<String> oauthList() { |
||||
|
// 默认列表
|
||||
|
List<String> defaultList = new ArrayList<>(properties.getType().keySet()); |
||||
|
// 扩展列表
|
||||
|
List<String> extendList = new ArrayList<>(); |
||||
|
ExtendProperties extend = properties.getExtend(); |
||||
|
if (null != extend) { |
||||
|
Class enumClass = extend.getEnumClass(); |
||||
|
List<String> names = EnumUtil.getNames(enumClass); |
||||
|
// 扩展列表
|
||||
|
extendList = extend.getConfig() |
||||
|
.keySet() |
||||
|
.stream() |
||||
|
.filter(x -> names.contains(x.toUpperCase())) |
||||
|
.map(String::toUpperCase) |
||||
|
.collect(Collectors.toList()); |
||||
|
} |
||||
|
|
||||
|
// 合并
|
||||
|
return (List<String>) 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<String, ExtendProperties.ExtendRequestConfig> extendConfig = properties.getExtend().getConfig(); |
||||
|
|
||||
|
// key 转大写
|
||||
|
Map<String, ExtendProperties.ExtendRequestConfig> 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<? extends AuthRequest> 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<String, JustAuthProperties.JustAuthProxyConfig> 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()); |
||||
|
} |
||||
|
} |
@ -0,0 +1,6 @@ |
|||||
|
/** |
||||
|
* justauth 三方登录的拓展 |
||||
|
* |
||||
|
* @author 芋道源码 |
||||
|
*/ |
||||
|
package cn.iocoder.yudao.module.system.framework.justauth; |
@ -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<TemplateInfo> 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<Long> ids); |
||||
|
|
||||
|
/** |
||||
|
* 获得社交客户端 |
||||
|
* |
||||
|
* @param id 编号 |
||||
|
* @return 社交客户端 |
||||
|
*/ |
||||
|
SocialClientDO getSocialClient(Long id); |
||||
|
|
||||
|
/** |
||||
|
* 获得社交客户端分页 |
||||
|
* |
||||
|
* @param pageReqVO 分页查询 |
||||
|
* @return 社交客户端分页 |
||||
|
*/ |
||||
|
PageResult<SocialClientDO> getSocialClientPage(SocialClientPageReqVO pageReqVO); |
||||
|
|
||||
|
} |
@ -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<String, WxMpService> wxMpServiceCache = CacheUtils.buildAsyncReloadingCache( |
||||
|
Duration.ofSeconds(10L), |
||||
|
new CacheLoader<String, WxMpService>() { |
||||
|
|
||||
|
@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<String, WxMaService> wxMaServiceCache = CacheUtils.buildAsyncReloadingCache( |
||||
|
Duration.ofSeconds(10L), |
||||
|
new CacheLoader<String, WxMaService>() { |
||||
|
|
||||
|
@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<TemplateInfo> 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<String, String> 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<ShippingListBean> 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<Long> 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<SocialClientDO> getSocialClientPage(SocialClientPageReqVO pageReqVO) { |
||||
|
return socialClientMapper.selectPage(pageReqVO); |
||||
|
} |
||||
|
|
||||
|
} |
@ -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<SocialUserDO> 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<SocialUserDO> getSocialUserPage(SocialUserPageReqVO pageReqVO); |
||||
|
|
||||
|
} |
@ -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<SocialUserDO> getSocialUserList(Long userId, Integer userType) { |
||||
|
// 获得绑定
|
||||
|
List<SocialUserBindDO> 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<SocialUserDO> getSocialUserPage(SocialUserPageReqVO pageReqVO) { |
||||
|
return socialUserMapper.selectPage(pageReqVO); |
||||
|
} |
||||
|
|
||||
|
} |
Loading…
Reference in new issue