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