|
@@ -1,132 +1,794 @@
|
|
|
package com.fs.app.controller.course;
|
|
package com.fs.app.controller.course;
|
|
|
|
|
|
|
|
import cn.hutool.core.date.DateTime;
|
|
import cn.hutool.core.date.DateTime;
|
|
|
|
|
+import cn.hutool.json.JSONUtil;
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
import com.fs.app.annotation.UserOperationLog;
|
|
import com.fs.app.annotation.UserOperationLog;
|
|
|
|
|
+import com.fs.app.controller.AppBaseController;
|
|
|
|
|
+import com.fs.app.param.FsUserClaimCourseMaterialParam;
|
|
|
|
|
+import com.fs.app.param.FsUserLoginByMpOauthParam;
|
|
|
import com.fs.app.param.FsUserLoginByMpParam;
|
|
import com.fs.app.param.FsUserLoginByMpParam;
|
|
|
-import com.fs.app.utils.JwtUtils;
|
|
|
|
|
|
|
+import com.fs.app.utils.WxUtil;
|
|
|
import com.fs.common.core.domain.R;
|
|
import com.fs.common.core.domain.R;
|
|
|
-import com.fs.common.core.redis.RedisCache;
|
|
|
|
|
-import com.fs.course.mapper.FsCourseSopLogsMapper;
|
|
|
|
|
|
|
+import com.fs.company.mapper.CompanyUserMapper;
|
|
|
|
|
+import com.fs.course.config.CourseConfig;
|
|
|
|
|
+import com.fs.course.domain.FsCourseLink;
|
|
|
|
|
+import com.fs.course.domain.FsCoursePlaySourceConfig;
|
|
|
|
|
+import com.fs.course.domain.FsCourseWatchLog;
|
|
|
|
|
+import com.fs.course.mapper.FsCourseLinkMapper;
|
|
|
import com.fs.course.mapper.FsCourseWatchLogMapper;
|
|
import com.fs.course.mapper.FsCourseWatchLogMapper;
|
|
|
|
|
+import com.fs.course.service.IFsCoursePlaySourceConfigService;
|
|
|
|
|
+import com.fs.course.utils.FsCoursePlaySourceConfigQuery;
|
|
|
|
|
+import com.fs.common.exception.CustomException;
|
|
|
|
|
+import com.fs.feishu.domain.FeishuAccount;
|
|
|
|
|
+import com.fs.feishu.model.FeishuLinkParam;
|
|
|
|
|
+import com.fs.feishu.service.FeiShuService;
|
|
|
|
|
+import com.fs.feishu.service.IFeishuAccountService;
|
|
|
import com.fs.his.domain.FsUser;
|
|
import com.fs.his.domain.FsUser;
|
|
|
import com.fs.his.enums.FsUserOperationEnum;
|
|
import com.fs.his.enums.FsUserOperationEnum;
|
|
|
import com.fs.his.service.IFsUserService;
|
|
import com.fs.his.service.IFsUserService;
|
|
|
-import com.fs.his.utils.ConfigUtil;
|
|
|
|
|
|
|
+import com.fs.qw.domain.QwExternalContact;
|
|
|
|
|
+import com.fs.qw.domain.QwGroupChat;
|
|
|
|
|
+import com.fs.qw.domain.QwGroupChatUser;
|
|
|
import com.fs.qw.mapper.QwExternalContactMapper;
|
|
import com.fs.qw.mapper.QwExternalContactMapper;
|
|
|
-import com.fs.sop.params.QwSopSettingTimeParam;
|
|
|
|
|
-import com.fs.system.mapper.SysConfigMapper;
|
|
|
|
|
-import com.fs.wx.mp.WxEeventType;
|
|
|
|
|
-import com.fs.wx.mp.WxMessageType;
|
|
|
|
|
-import com.fs.wx.mp.WxServiceMsgDto;
|
|
|
|
|
|
|
+import com.fs.qw.mapper.QwGroupChatMapper;
|
|
|
|
|
+import com.fs.qw.mapper.QwGroupChatUserMapper;
|
|
|
|
|
+import com.fs.sop.domain.SopUserLogsInfo;
|
|
|
|
|
+import com.fs.sop.service.ISopUserLogsInfoService;
|
|
|
|
|
+import com.fs.system.service.ISysConfigService;
|
|
|
|
|
+import io.jsonwebtoken.Claims;
|
|
|
import io.swagger.annotations.Api;
|
|
import io.swagger.annotations.Api;
|
|
|
import io.swagger.annotations.ApiOperation;
|
|
import io.swagger.annotations.ApiOperation;
|
|
|
-import lombok.Synchronized;
|
|
|
|
|
-import me.chanjar.weixin.common.bean.WxJsapiSignature;
|
|
|
|
|
import me.chanjar.weixin.common.bean.WxOAuth2UserInfo;
|
|
import me.chanjar.weixin.common.bean.WxOAuth2UserInfo;
|
|
|
-import me.chanjar.weixin.common.bean.menu.WxMenu;
|
|
|
|
|
import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken;
|
|
import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken;
|
|
|
import me.chanjar.weixin.common.error.WxErrorException;
|
|
import me.chanjar.weixin.common.error.WxErrorException;
|
|
|
-import me.chanjar.weixin.mp.api.WxMpMenuService;
|
|
|
|
|
import me.chanjar.weixin.mp.api.WxMpService;
|
|
import me.chanjar.weixin.mp.api.WxMpService;
|
|
|
-import me.chanjar.weixin.mp.api.WxMpUserService;
|
|
|
|
|
-import me.chanjar.weixin.mp.bean.result.WxMpUser;
|
|
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
import org.apache.commons.lang3.StringUtils;
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.Logger;
|
|
|
import org.slf4j.LoggerFactory;
|
|
import org.slf4j.LoggerFactory;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
-import org.springframework.web.bind.annotation.*;
|
|
|
|
|
|
|
+import org.springframework.validation.annotation.Validated;
|
|
|
|
|
+import org.springframework.web.bind.annotation.PostMapping;
|
|
|
|
|
+import org.springframework.web.bind.annotation.RequestBody;
|
|
|
|
|
+import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
|
|
+import org.springframework.web.bind.annotation.RestController;
|
|
|
|
|
|
|
|
-import java.io.UnsupportedEncodingException;
|
|
|
|
|
import java.net.URLDecoder;
|
|
import java.net.URLDecoder;
|
|
|
-import java.text.SimpleDateFormat;
|
|
|
|
|
-import java.util.Calendar;
|
|
|
|
|
|
|
+import java.util.Arrays;
|
|
|
import java.util.Date;
|
|
import java.util.Date;
|
|
|
import java.util.HashMap;
|
|
import java.util.HashMap;
|
|
|
|
|
+import java.util.List;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
|
|
-
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * 看课服务号授权登录 / H5 公众号 OAuth 登录与领取资料
|
|
|
|
|
+ */
|
|
|
@Api("看课服务号授权登录接口")
|
|
@Api("看课服务号授权登录接口")
|
|
|
@RestController
|
|
@RestController
|
|
|
@RequestMapping("/app/wx/mp")
|
|
@RequestMapping("/app/wx/mp")
|
|
|
-public class CourseMpLoginController {
|
|
|
|
|
- Logger logger= LoggerFactory.getLogger(getClass());
|
|
|
|
|
- @Autowired
|
|
|
|
|
- private WxMpService wxMpService;
|
|
|
|
|
- @Autowired
|
|
|
|
|
- private IFsUserService userService;
|
|
|
|
|
-
|
|
|
|
|
- @Autowired
|
|
|
|
|
- JwtUtils jwtUtils;
|
|
|
|
|
- @Autowired
|
|
|
|
|
- RedisCache redisCache;
|
|
|
|
|
-
|
|
|
|
|
- @Autowired
|
|
|
|
|
- FsCourseWatchLogMapper fsCourseWatchLogMapper;
|
|
|
|
|
- @Autowired
|
|
|
|
|
- QwExternalContactMapper qwExternalContactMapper;
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
- @ApiOperation("课程短链公众号登录")
|
|
|
|
|
- @PostMapping("/loginByMp")
|
|
|
|
|
- @Transactional
|
|
|
|
|
- @UserOperationLog(operationType = FsUserOperationEnum.H5LOGIN)
|
|
|
|
|
- public R loginByMp( @RequestBody FsUserLoginByMpParam param) {
|
|
|
|
|
-
|
|
|
|
|
- if (StringUtils.isBlank(param.getCode())) {
|
|
|
|
|
- return R.error("code不存在");
|
|
|
|
|
- }
|
|
|
|
|
- try{
|
|
|
|
|
- WxOAuth2AccessToken wxMpOAuth2AccessToken = wxMpService.getOAuth2Service().getAccessToken(param.getCode());
|
|
|
|
|
- WxOAuth2UserInfo wxMpUser = wxMpService.getOAuth2Service().getUserInfo(wxMpOAuth2AccessToken, null);
|
|
|
|
|
- if(StringUtils.isBlank(wxMpUser.getUnionId())){
|
|
|
|
|
- return R.error("请使用微信扫码登录");
|
|
|
|
|
- }
|
|
|
|
|
- FsUser user=userService.selectFsUserByUnionid(wxMpUser.getUnionId());
|
|
|
|
|
- if(user!=null){
|
|
|
|
|
- FsUser userMap=new FsUser();
|
|
|
|
|
- userMap.setUserId(user.getUserId());
|
|
|
|
|
- userMap.setNickName(wxMpUser.getNickname());
|
|
|
|
|
- userMap.setAvatar(wxMpUser.getHeadImgUrl());
|
|
|
|
|
- userMap.setMpOpenId(wxMpUser.getOpenid());
|
|
|
|
|
- userMap.setUpdateTime(new DateTime());
|
|
|
|
|
- userService.updateFsUser(userMap);
|
|
|
|
|
- }
|
|
|
|
|
- else{
|
|
|
|
|
- //写入
|
|
|
|
|
- user=new FsUser();
|
|
|
|
|
- user.setNickName(wxMpUser.getNickname());
|
|
|
|
|
- user.setAvatar(wxMpUser.getHeadImgUrl());
|
|
|
|
|
- user.setStatus(1);
|
|
|
|
|
- user.setSex(wxMpUser.getSex());
|
|
|
|
|
- user.setMpOpenId(wxMpUser.getOpenid());
|
|
|
|
|
- user.setUnionId(wxMpUser.getUnionId());
|
|
|
|
|
- // 新用户 - 添加 appId
|
|
|
|
|
- if (StringUtils.isNotEmpty(param.getAppId())) {
|
|
|
|
|
- user.setAppId(param.getAppId());
|
|
|
|
|
- }
|
|
|
|
|
- user.setCreateTime(new Date());
|
|
|
|
|
- userService.insertFsUser(user);
|
|
|
|
|
- }
|
|
|
|
|
- System.out.println("user:id:"+user.getUserId());
|
|
|
|
|
- String token = jwtUtils.generateToken(user.getUserId());
|
|
|
|
|
- redisCache.setCacheObject("token:"+user.getUserId(),token,604800, TimeUnit.SECONDS);
|
|
|
|
|
- Map<String,Object> map=new HashMap<>();
|
|
|
|
|
- map.put("token",token);
|
|
|
|
|
- map.put("user",user);
|
|
|
|
|
- logger.info("zyp \n 【点播公众号登录】:{}",user.getUserId());
|
|
|
|
|
- return R.ok(map);
|
|
|
|
|
-
|
|
|
|
|
- }
|
|
|
|
|
- catch (WxErrorException e){
|
|
|
|
|
- if(e.getError().getErrorCode()==40163){
|
|
|
|
|
- return R.error(40163,e.getError().getErrorMsg());
|
|
|
|
|
- }
|
|
|
|
|
- else{
|
|
|
|
|
- return R.error("授权失败,"+e.getMessage());
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+public class CourseMpLoginController extends AppBaseController {
|
|
|
|
|
+
|
|
|
|
|
+ private static final Logger logger = LoggerFactory.getLogger(CourseMpLoginController.class);
|
|
|
|
|
+
|
|
|
|
|
+ /** H5 公众号登录 token 有效期:3 天 */
|
|
|
|
|
+ private static final long MP_OAUTH_TOKEN_TTL_SECONDS = 3L * 24 * 60 * 60;
|
|
|
|
|
+
|
|
|
|
|
+ /** 领取资料:按短链 link 缓存飞书 docx(一个 link 一条,含群链接) */
|
|
|
|
|
+ private static final String FEISHU_CLAIM_LINK_CACHE_PREFIX = "feishu:h5_claim_link:";
|
|
|
|
|
+ private static final int FEISHU_CLAIM_LINK_CACHE_HOURS = 24;
|
|
|
|
|
+
|
|
|
|
|
+ /** 与 isAddKf 群/独属提示对齐 */
|
|
|
|
|
+ private static final String TIP_NO_REGISTER = "由于您还未完成注册,请联系伴学助手完成注册即可观看!";
|
|
|
|
|
+ private static final String TIP_NO_MEMBER = "此链接已被绑定,请联系学习助理~";
|
|
|
|
|
+ private static final String TIP_GROUP_PARAM = "群参数异常!";
|
|
|
|
|
+ private static final String TIP_RATE_LIMIT = "当前学习通道访问限流,暂时无法观看!";
|
|
|
|
|
+ private static final String TIP_EXPIRED = "资料链接已过期,请联系伴学助手";
|
|
|
|
|
+ private static final String TIP_LINK_MISSING = "未找到资料链接,请联系伴学助手";
|
|
|
|
|
+ private static final String TIP_FEISHU_APP = "资料生成失败,请联系伴学助手";
|
|
|
|
|
+ private static final String TIP_SYSTEM = "系统繁忙,请稍后再试;若持续失败请联系伴学助手";
|
|
|
|
|
+
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private WxMpService wxMpService;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private IFsUserService userService;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private QwExternalContactMapper qwExternalContactMapper;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private QwGroupChatMapper qwGroupChatMapper;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private QwGroupChatUserMapper qwGroupChatUserMapper;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private IFsCoursePlaySourceConfigService fsCoursePlaySourceConfigService;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private FsCourseLinkMapper fsCourseLinkMapper;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private FsCourseWatchLogMapper fsCourseWatchLogMapper;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private CompanyUserMapper companyUserMapper;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private ISysConfigService configService;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private FeiShuService feiShuService;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private IFeishuAccountService feishuAccountService;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private ISopUserLogsInfoService iSopUserLogsInfoService;
|
|
|
|
|
+
|
|
|
|
|
+ @ApiOperation("课程短链公众号登录")
|
|
|
|
|
+ @PostMapping("/loginByMp")
|
|
|
|
|
+ @Transactional
|
|
|
|
|
+ @UserOperationLog(operationType = FsUserOperationEnum.H5LOGIN)
|
|
|
|
|
+ public R loginByMp(@RequestBody FsUserLoginByMpParam param) {
|
|
|
|
|
+
|
|
|
|
|
+ if (StringUtils.isBlank(param.getCode())) {
|
|
|
|
|
+ return R.error("code不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ WxOAuth2AccessToken wxMpOAuth2AccessToken = wxMpService.getOAuth2Service().getAccessToken(param.getCode());
|
|
|
|
|
+ WxOAuth2UserInfo wxMpUser = wxMpService.getOAuth2Service().getUserInfo(wxMpOAuth2AccessToken, null);
|
|
|
|
|
+ if (StringUtils.isBlank(wxMpUser.getUnionId())) {
|
|
|
|
|
+ return R.error("请使用微信扫码登录");
|
|
|
|
|
+ }
|
|
|
|
|
+ FsUser user = userService.selectFsUserByUnionid(wxMpUser.getUnionId());
|
|
|
|
|
+ if (user != null) {
|
|
|
|
|
+ FsUser userMap = new FsUser();
|
|
|
|
|
+ userMap.setUserId(user.getUserId());
|
|
|
|
|
+ userMap.setNickName(wxMpUser.getNickname());
|
|
|
|
|
+ userMap.setAvatar(wxMpUser.getHeadImgUrl());
|
|
|
|
|
+ userMap.setMpOpenId(wxMpUser.getOpenid());
|
|
|
|
|
+ userMap.setUpdateTime(new DateTime());
|
|
|
|
|
+ userService.updateFsUser(userMap);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 写入新用户
|
|
|
|
|
+ user = new FsUser();
|
|
|
|
|
+ user.setNickName(wxMpUser.getNickname());
|
|
|
|
|
+ user.setAvatar(wxMpUser.getHeadImgUrl());
|
|
|
|
|
+ user.setStatus(1);
|
|
|
|
|
+ user.setSex(wxMpUser.getSex());
|
|
|
|
|
+ user.setMpOpenId(wxMpUser.getOpenid());
|
|
|
|
|
+ user.setUnionId(wxMpUser.getUnionId());
|
|
|
|
|
+ if (StringUtils.isNotEmpty(param.getAppId())) {
|
|
|
|
|
+ user.setAppId(param.getAppId());
|
|
|
|
|
+ }
|
|
|
|
|
+ user.setCreateTime(new Date());
|
|
|
|
|
+ userService.insertFsUser(user);
|
|
|
|
|
+ }
|
|
|
|
|
+ String token = jwtUtils.generateToken(user.getUserId());
|
|
|
|
|
+ redisCache.setCacheObject("token:" + user.getUserId(), token, 604800, TimeUnit.SECONDS);
|
|
|
|
|
+ Map<String, Object> map = new HashMap<>();
|
|
|
|
|
+ map.put("token", token);
|
|
|
|
|
+ map.put("user", user);
|
|
|
|
|
+ logger.info("【点播公众号登录】:{}", user.getUserId());
|
|
|
|
|
+ return R.ok(map);
|
|
|
|
|
+
|
|
|
|
|
+ } catch (WxErrorException e) {
|
|
|
|
|
+ if (e.getError().getErrorCode() == 40163) {
|
|
|
|
|
+ return R.error(40163, e.getError().getErrorMsg());
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return R.error("授权失败," + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * H5 消防科普页等场景:前端传 code + appId,按播放源配置换票登录,token 缓存 3 天
|
|
|
|
|
+ */
|
|
|
|
|
+ @ApiOperation("H5公众号OAuth登录(按appId查配置,返回token、userId、昵称、头像)")
|
|
|
|
|
+ @PostMapping("/loginByMpOauth")
|
|
|
|
|
+ @Transactional
|
|
|
|
|
+ @UserOperationLog(operationType = FsUserOperationEnum.H5LOGIN)
|
|
|
|
|
+ public R loginByMpOauth(@Validated @RequestBody FsUserLoginByMpOauthParam param) {
|
|
|
|
|
+ long startMs = System.currentTimeMillis();
|
|
|
|
|
+ String codePreview = param != null && StringUtils.isNotBlank(param.getCode())
|
|
|
|
|
+ ? param.getCode().substring(0, Math.min(8, param.getCode().length())) + "***"
|
|
|
|
|
+ : "";
|
|
|
|
|
+ logger.info("[H5登录] 开始 appId={} uuid={} codePreview={}",
|
|
|
|
|
+ param != null ? param.getAppId() : null,
|
|
|
|
|
+ param != null ? param.getUuid() : null,
|
|
|
|
|
+ codePreview);
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (StringUtils.isBlank(param.getCode())) {
|
|
|
|
|
+ logger.warn("[H5登录] 失败 reason=code为空 appId={} uuid={}", param.getAppId(), param.getUuid());
|
|
|
|
|
+ return R.error("code不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StringUtils.isBlank(param.getAppId())) {
|
|
|
|
|
+ logger.warn("[H5登录] 失败 reason=appId为空 uuid={}", param.getUuid());
|
|
|
|
|
+ return R.error("appId不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ QueryWrapper<FsCoursePlaySourceConfig> appIdQuery = new QueryWrapper<FsCoursePlaySourceConfig>()
|
|
|
|
|
+ .eq("appid", param.getAppId())
|
|
|
|
|
+ .eq("is_del", 0);
|
|
|
|
|
+ FsCoursePlaySourceConfigQuery.excludeBanned(appIdQuery);
|
|
|
|
|
+ FsCoursePlaySourceConfig playSourceConfig = fsCoursePlaySourceConfigService.getOne(appIdQuery);
|
|
|
|
|
+ if (playSourceConfig == null
|
|
|
|
|
+ || StringUtils.isBlank(playSourceConfig.getAppid())
|
|
|
|
|
+ || StringUtils.isBlank(playSourceConfig.getSecret())) {
|
|
|
|
|
+ logger.warn("[H5登录] 失败 reason=公众号配置不存在或未启用 appId={} uuid={}",
|
|
|
|
|
+ param.getAppId(), param.getUuid());
|
|
|
|
|
+ return R.error("公众号配置不存在或未启用");
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ String appId = playSourceConfig.getAppid();
|
|
|
|
|
+ String appSecret = playSourceConfig.getSecret();
|
|
|
|
|
+ logger.info("[H5登录] 已匹配播放源配置 appId={} configId={} name={}",
|
|
|
|
|
+ appId, playSourceConfig.getId(), playSourceConfig.getName());
|
|
|
|
|
+
|
|
|
|
|
+ Map<?, ?> tokenResult = WxUtil.getAccessToken(param.getCode(), appId, appSecret);
|
|
|
|
|
+ Object unionidObj = tokenResult.get("unionid");
|
|
|
|
|
+ Object openidObj = tokenResult.get("openid");
|
|
|
|
|
+ Object accessTokenObj = tokenResult.get("access_token");
|
|
|
|
|
+ if (unionidObj == null || StringUtils.isBlank(String.valueOf(unionidObj))) {
|
|
|
|
|
+ logger.warn("[H5登录] 失败 reason=无unionid appId={} uuid={} wxKeys={}",
|
|
|
|
|
+ appId, param.getUuid(), tokenResult != null ? tokenResult.keySet() : null);
|
|
|
|
|
+ return R.error("未绑定开放平台,无法获取unionid");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (openidObj == null || accessTokenObj == null) {
|
|
|
|
|
+ logger.warn("[H5登录] 失败 reason=无openid或access_token appId={} uuid={} wxKeys={}",
|
|
|
|
|
+ appId, param.getUuid(), tokenResult != null ? tokenResult.keySet() : null);
|
|
|
|
|
+ return R.error("授权失败,未获取到openid");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String unionid = String.valueOf(unionidObj);
|
|
|
|
|
+ String openid = String.valueOf(openidObj);
|
|
|
|
|
+ String accessToken = String.valueOf(accessTokenObj);
|
|
|
|
|
+ logger.info("[H5登录] 微信换票成功 appId={} openid={} unionid={}", appId, openid, unionid);
|
|
|
|
|
+
|
|
|
|
|
+ String nickname = "微信用户";
|
|
|
|
|
+ String avatar = "";
|
|
|
|
|
+ Integer sex = 0;
|
|
|
|
|
+ try {
|
|
|
|
|
+ Map<?, ?> userInfo = WxUtil.getUserInfo(accessToken, openid);
|
|
|
|
|
+ if (userInfo != null) {
|
|
|
|
|
+ if (userInfo.get("nickname") != null) {
|
|
|
|
|
+ nickname = String.valueOf(userInfo.get("nickname"));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (userInfo.get("headimgurl") != null) {
|
|
|
|
|
+ avatar = String.valueOf(userInfo.get("headimgurl"));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (userInfo.get("sex") instanceof Integer) {
|
|
|
|
|
+ sex = (Integer) userInfo.get("sex");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ logger.info("[H5登录] 拉取微信资料 nickname={} hasAvatar={}", nickname, StringUtils.isNotBlank(avatar));
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ logger.warn("[H5登录] 获取微信用户资料失败,使用默认昵称 openid={} err={}", openid, e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ FsUser user = userService.selectFsUserByUnionid(unionid);
|
|
|
|
|
+ boolean isNewUser = false;
|
|
|
|
|
+ if (user != null) {
|
|
|
|
|
+ if (user.getStatus() != null && user.getStatus().equals(0)) {
|
|
|
|
|
+ logger.warn("[H5登录] 失败 reason=会员已禁用 userId={} unionid={}", user.getUserId(), unionid);
|
|
|
|
|
+ return R.error("此会员已禁用");
|
|
|
|
|
+ }
|
|
|
|
|
+ FsUser userMap = new FsUser();
|
|
|
|
|
+ userMap.setUserId(user.getUserId());
|
|
|
|
|
+ userMap.setNickName(nickname);
|
|
|
|
|
+ userMap.setAvatar(avatar);
|
|
|
|
|
+ userMap.setMpOpenId(openid);
|
|
|
|
|
+ userMap.setUpdateTime(new DateTime());
|
|
|
|
|
+ userService.updateFsUser(userMap);
|
|
|
|
|
+ user.setNickName(nickname);
|
|
|
|
|
+ user.setAvatar(avatar);
|
|
|
|
|
+ user.setMpOpenId(openid);
|
|
|
|
|
+ logger.info("[H5登录] 更新已有用户 userId={} mpOpenId={}", user.getUserId(), openid);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ isNewUser = true;
|
|
|
|
|
+ user = new FsUser();
|
|
|
|
|
+ user.setNickName(nickname);
|
|
|
|
|
+ user.setAvatar(avatar);
|
|
|
|
|
+ user.setStatus(1);
|
|
|
|
|
+ if (sex != null && sex != 0) {
|
|
|
|
|
+ user.setSex(sex);
|
|
|
|
|
+ }
|
|
|
|
|
+ user.setMpOpenId(openid);
|
|
|
|
|
+ user.setUnionId(unionid);
|
|
|
|
|
+ user.setAppId(appId);
|
|
|
|
|
+ user.setCreateTime(new Date());
|
|
|
|
|
+ userService.insertFsUser(user);
|
|
|
|
|
+ logger.info("[H5登录] 注册新用户 userId={} unionid={} appId={}", user.getUserId(), unionid, appId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String token = jwtUtils.generateToken(user.getUserId(), MP_OAUTH_TOKEN_TTL_SECONDS);
|
|
|
|
|
+ redisCache.setCacheObject("token:" + user.getUserId(), token,
|
|
|
|
|
+ Integer.valueOf((int) MP_OAUTH_TOKEN_TTL_SECONDS), TimeUnit.SECONDS);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> map = new HashMap<>();
|
|
|
|
|
+ // H5:token、userId、昵称、头像(不回传 openid/unionid/完整 user)
|
|
|
|
|
+ map.put("token", token);
|
|
|
|
|
+ map.put("userId", user.getUserId());
|
|
|
|
|
+ map.put("nickname", nickname);
|
|
|
|
|
+ map.put("headimgurl", avatar);
|
|
|
|
|
+ logger.info("[H5登录] 成功 isNew={} userId={} appId={} uuid={} nickname={} hasAvatar={} costMs={}",
|
|
|
|
|
+ isNewUser, user.getUserId(), appId, param.getUuid(), nickname,
|
|
|
|
|
+ StringUtils.isNotBlank(avatar), System.currentTimeMillis() - startMs);
|
|
|
|
|
+ return R.ok(map);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ logger.error("[H5登录] 异常 appId={} uuid={} costMs={} err={}",
|
|
|
|
|
+ param != null ? param.getAppId() : null,
|
|
|
|
|
+ param != null ? param.getUuid() : null,
|
|
|
|
|
+ System.currentTimeMillis() - startMs,
|
|
|
|
|
+ e.getMessage(), e);
|
|
|
|
|
+ return R.error("登录失败");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 领取资料:Header 带 APPToken,Body 只传 uuid(= fs_course_link.link)
|
|
|
|
|
+ * 先校验短链归属,通过后再按 getFeishuCourseNewLink 逻辑返回飞书 docx
|
|
|
|
|
+ */
|
|
|
|
|
+ @ApiOperation("H5领取资料(先归属校验,再返回飞书看课链接)")
|
|
|
|
|
+ @PostMapping("/claimCourseMaterial")
|
|
|
|
|
+ public R claimCourseMaterial(@Validated @RequestBody FsUserClaimCourseMaterialParam param) {
|
|
|
|
|
+ long startMs = System.currentTimeMillis();
|
|
|
|
|
+ String uuid = param != null ? param.getUuid() : null;
|
|
|
|
|
+ logger.info("[H5领取] 开始 uuid={}", uuid);
|
|
|
|
|
+
|
|
|
|
|
+ String userIdStr = getUserId();
|
|
|
|
|
+ if (StringUtils.isBlank(userIdStr)) {
|
|
|
|
|
+ logger.warn("[H5领取] 失败 reason=APPToken无法解析用户 uuid={}", uuid);
|
|
|
|
|
+ return R.error(401, "未登录");
|
|
|
|
|
+ }
|
|
|
|
|
+ Long userId;
|
|
|
|
|
+ try {
|
|
|
|
|
+ userId = Long.valueOf(userIdStr);
|
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
|
+ logger.warn("[H5领取] 失败 reason=userId非法 userIdStr={} uuid={}", userIdStr, uuid);
|
|
|
|
|
+ return R.error(401, "未登录");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 校验 Redis 中 token 是否仍有效(与 3 天缓存对齐)
|
|
|
|
|
+ String headerToken = com.fs.common.utils.ServletUtils.getRequest().getHeader("APPToken");
|
|
|
|
|
+ if (StringUtils.isBlank(headerToken)) {
|
|
|
|
|
+ logger.warn("[H5领取] 失败 reason=缺少APPToken userId={} uuid={}", userId, uuid);
|
|
|
|
|
+ return R.error(401, "未登录");
|
|
|
|
|
+ }
|
|
|
|
|
+ Claims claims = jwtUtils.getClaimByToken(headerToken);
|
|
|
|
|
+ if (claims == null || jwtUtils.isTokenExpired(claims.getExpiration())) {
|
|
|
|
|
+ logger.warn("[H5领取] 失败 reason=JWT无效或过期 userId={} uuid={}", userId, uuid);
|
|
|
|
|
+ return R.error(401, "未登录");
|
|
|
|
|
+ }
|
|
|
|
|
+ Object cachedToken = redisCache.getCacheObject("token:" + userId);
|
|
|
|
|
+ if (cachedToken == null || !headerToken.equals(String.valueOf(cachedToken))) {
|
|
|
|
|
+ logger.warn("[H5领取] 失败 reason=Redis token不匹配或已失效 userId={} uuid={} hasRedisToken={}",
|
|
|
|
|
+ userId, uuid, cachedToken != null);
|
|
|
|
|
+ return R.error(401, "未登录");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ FsUser currentUser = userService.selectFsUserByUserId(userId);
|
|
|
|
|
+ if (currentUser == null) {
|
|
|
|
|
+ logger.warn("[H5领取] 失败 reason=用户不存在 userId={} uuid={}", userId, uuid);
|
|
|
|
|
+ return R.error(401, "未登录");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (currentUser.getStatus() != null && currentUser.getStatus().equals(0)) {
|
|
|
|
|
+ logger.warn("[H5领取] 失败 reason=会员已禁用 userId={} uuid={}", userId, uuid);
|
|
|
|
|
+ return R.error("此会员已禁用");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ logger.info("[H5领取] 鉴权通过 userId={} unionId={} uuid={}",
|
|
|
|
|
+ userId, currentUser.getUnionId(), uuid);
|
|
|
|
|
+ R result = resolveMaterialJump(uuid, currentUser);
|
|
|
|
|
+ logger.info("[H5领取] 结束 userId={} uuid={} hasMaterial={} msg={} costMs={}",
|
|
|
|
|
+ userId,
|
|
|
|
|
+ uuid,
|
|
|
|
|
+ result.get("hasMaterial"),
|
|
|
|
|
+ result.get("msg"),
|
|
|
|
|
+ System.currentTimeMillis() - startMs);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 按 uuid 查短链:先归属门禁(群对齐 isAddKf),通过后再取飞书看课文档
|
|
|
|
|
+ */
|
|
|
|
|
+ private R resolveMaterialJump(String uuid, FsUser currentUser) {
|
|
|
|
|
+ if (StringUtils.isBlank(uuid)) {
|
|
|
|
|
+ logger.info("[H5领取] 失败 reason=uuid为空 userId={}", currentUser.getUserId());
|
|
|
|
|
+ return claimFail("link_missing", TIP_LINK_MISSING);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ String linkCode = URLDecoder.decode(uuid, "UTF-8");
|
|
|
|
|
+ FsCourseLink courseLink = fsCourseLinkMapper.selectFsCourseLinkByLink(linkCode);
|
|
|
|
|
+ if (courseLink == null) {
|
|
|
|
|
+ logger.info("[H5领取] 失败 reason=短链不存在 userId={} link={}", currentUser.getUserId(), linkCode);
|
|
|
|
|
+ return claimFail("link_missing", TIP_LINK_MISSING);
|
|
|
|
|
+ }
|
|
|
|
|
+ logger.info("[H5领取] 查到短链 userId={} linkId={} link={} chatId={} qwExternalId={} companyUserId={} "
|
|
|
|
|
+ + "feishuNeedAuth={} videoId={} courseId={} expireAt={}",
|
|
|
|
|
+ currentUser.getUserId(),
|
|
|
|
|
+ courseLink.getLinkId(),
|
|
|
|
|
+ courseLink.getLink(),
|
|
|
|
|
+ courseLink.getChatId(),
|
|
|
|
|
+ courseLink.getQwExternalId(),
|
|
|
|
|
+ courseLink.getCompanyUserId(),
|
|
|
|
|
+ courseLink.getFeishuNeedAuth(),
|
|
|
|
|
+ courseLink.getVideoId(),
|
|
|
|
|
+ courseLink.getCourseId(),
|
|
|
|
|
+ courseLink.getUpdateTime());
|
|
|
|
|
+
|
|
|
|
|
+ if (courseLink.getUpdateTime() != null && new Date().after(courseLink.getUpdateTime())) {
|
|
|
|
|
+ logger.info("[H5领取] 失败 reason=短链已过期 userId={} linkId={} expireAt={}",
|
|
|
|
|
+ currentUser.getUserId(), courseLink.getLinkId(), courseLink.getUpdateTime());
|
|
|
|
|
+ return claimFail("expired", TIP_EXPIRED);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 归属门禁:群链走 isAddKf/handleQwRoom;私聊走独属校验。未通过不调飞书
|
|
|
|
|
+ R ownershipFail = checkLinkOwnership(courseLink, currentUser);
|
|
|
|
|
+ if (ownershipFail != null) {
|
|
|
|
|
+ return ownershipFail;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return resolveJumpUrlAfterOwned(courseLink, currentUser);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ logger.error("[H5领取] 校验异常 userId={} uuid={} err={}",
|
|
|
|
|
+ currentUser.getUserId(), uuid, e.getMessage(), e);
|
|
|
|
|
+ return claimFail("system", TIP_SYSTEM);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 归属校验。返回 null 表示通过;否则返回带具体提示的失败结果
|
|
|
|
|
+ */
|
|
|
|
|
+ private R checkLinkOwnership(FsCourseLink courseLink, FsUser currentUser) {
|
|
|
|
|
+ if (StringUtils.isNotBlank(courseLink.getChatId())) {
|
|
|
|
|
+ return checkGroupOwnership(courseLink, currentUser);
|
|
|
|
|
+ }
|
|
|
|
|
+ return checkPrivateOwnership(courseLink, currentUser);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 群看课归属:对齐 isAddKf → handleQwRoom(在群、已注册)
|
|
|
|
|
+ */
|
|
|
|
|
+ private R checkGroupOwnership(FsCourseLink courseLink, FsUser currentUser) {
|
|
|
|
|
+ String chatId = courseLink.getChatId();
|
|
|
|
|
+ QwGroupChat qwGroupChat = qwGroupChatMapper.selectQwGroupChatByChatId(chatId);
|
|
|
|
|
+ if (qwGroupChat == null) {
|
|
|
|
|
+ logger.info("[H5领取] 群归属失败 reason=群不存在 userId={} chatId={}", currentUser.getUserId(), chatId);
|
|
|
|
|
+ return claimFail("ownership_group", TIP_GROUP_PARAM);
|
|
|
|
|
+ }
|
|
|
|
|
+ SopUserLogsInfo sopQuery = new SopUserLogsInfo();
|
|
|
|
|
+ sopQuery.setChatId(chatId);
|
|
|
|
|
+ List<QwGroupChatUser> qwGroupChatUsers = qwGroupChatUserMapper.selectByChatId(sopQuery);
|
|
|
|
|
+ if (qwGroupChatUsers == null || qwGroupChatUsers.isEmpty()) {
|
|
|
|
|
+ logger.info("[H5领取] 群归属失败 reason=群成员为空 userId={} chatId={}", currentUser.getUserId(), chatId);
|
|
|
|
|
+ return claimFail("ownership_group", TIP_GROUP_PARAM);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String corpId = StringUtils.isNotBlank(courseLink.getCorpId())
|
|
|
|
|
+ ? courseLink.getCorpId() : qwGroupChat.getCorpId();
|
|
|
|
|
+ QwExternalContact contact = findGroupExternalContact(qwGroupChat.getOwner(), currentUser, corpId);
|
|
|
|
|
+ if (contact == null) {
|
|
|
|
|
+ logger.info("[H5领取] 群归属失败 reason=未匹配外部联系人 userId={} chatId={} owner={}",
|
|
|
|
|
+ currentUser.getUserId(), chatId, qwGroupChat.getOwner());
|
|
|
|
|
+ return claimFail("ownership_group", TIP_NO_REGISTER);
|
|
|
|
|
+ }
|
|
|
|
|
+ Integer contactStatus = contact.getStatus();
|
|
|
|
|
+ if (contactStatus == null || !Arrays.asList(0, 1, 2).contains(contactStatus)) {
|
|
|
|
|
+ logger.info("[H5领取] 群归属失败 reason=客户状态异常 userId={} status={}",
|
|
|
|
|
+ currentUser.getUserId(), contactStatus);
|
|
|
|
|
+ return claimFail("ownership_group", TIP_RATE_LIMIT);
|
|
|
|
|
+ }
|
|
|
|
|
+ final String externalUserId = contact.getExternalUserId();
|
|
|
|
|
+ boolean inGroup = qwGroupChatUsers.stream()
|
|
|
|
|
+ .anyMatch(e -> e != null && externalUserId != null && externalUserId.equals(e.getUserId()));
|
|
|
|
|
+ if (!inGroup) {
|
|
|
|
|
+ logger.info("[H5领取] 群归属失败 reason=客户不在群 userId={} chatId={} externalUserId={}",
|
|
|
|
|
+ currentUser.getUserId(), chatId, externalUserId);
|
|
|
|
|
+ return claimFail("ownership_group", TIP_NO_REGISTER);
|
|
|
|
|
+ }
|
|
|
|
|
+ // 对齐 handleQwRoom:已绑他人则拒绝;未绑则绑定当前用户
|
|
|
|
|
+ R bindFail = ensureExternalContactBound(contact, currentUser, courseLink, true);
|
|
|
|
|
+ if (bindFail != null) {
|
|
|
|
|
+ return bindFail;
|
|
|
|
|
+ }
|
|
|
|
|
+ logger.info("[H5领取] 群归属通过 userId={} chatId={} qwExternalId={}",
|
|
|
|
|
+ currentUser.getUserId(), chatId, contact.getId());
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 群主下匹配外部联系人:优先 fsUserId,其次昵称(对齐 handleQwRoom)
|
|
|
|
|
+ */
|
|
|
|
|
+ private QwExternalContact findGroupExternalContact(String ownerUserId, FsUser currentUser, String corpId) {
|
|
|
|
|
+ if (StringUtils.isBlank(ownerUserId) || currentUser == null || currentUser.getUserId() == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ QueryWrapper<QwExternalContact> byFsUser = new QueryWrapper<QwExternalContact>()
|
|
|
|
|
+ .eq("user_id", ownerUserId)
|
|
|
|
|
+ .eq("fs_user_id", currentUser.getUserId())
|
|
|
|
|
+ .in("status", Arrays.asList(0, 1, 2))
|
|
|
|
|
+ .orderByAsc("status")
|
|
|
|
|
+ .orderByDesc("create_time");
|
|
|
|
|
+ if (StringUtils.isNotBlank(corpId)) {
|
|
|
|
|
+ byFsUser.eq("corp_id", corpId);
|
|
|
|
|
+ }
|
|
|
|
|
+ List<QwExternalContact> list = qwExternalContactMapper.selectList(byFsUser);
|
|
|
|
|
+ if (list != null && !list.isEmpty()) {
|
|
|
|
|
+ return list.get(0);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StringUtils.isBlank(currentUser.getNickName())) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ QueryWrapper<QwExternalContact> byName = new QueryWrapper<QwExternalContact>()
|
|
|
|
|
+ .eq("user_id", ownerUserId)
|
|
|
|
|
+ .eq("name", currentUser.getNickName())
|
|
|
|
|
+ .in("status", Arrays.asList(0, 1, 2))
|
|
|
|
|
+ .orderByAsc("status")
|
|
|
|
|
+ .orderByDesc("create_time");
|
|
|
|
|
+ if (StringUtils.isNotBlank(corpId)) {
|
|
|
|
|
+ byName.eq("corp_id", corpId);
|
|
|
|
|
+ }
|
|
|
|
|
+ list = qwExternalContactMapper.selectList(byName);
|
|
|
|
|
+ return (list == null || list.isEmpty()) ? null : list.get(0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 私聊独属短链归属:对齐 isAddKf/handleExt——
|
|
|
|
|
+ * 已绑他人则拒绝;未绑则绑定当前用户后放行;已绑本人则补齐看课/SOP 关联后放行。
|
|
|
|
|
+ */
|
|
|
|
|
+ private R checkPrivateOwnership(FsCourseLink courseLink, FsUser currentUser) {
|
|
|
|
|
+ if (courseLink.getQwExternalId() == null) {
|
|
|
|
|
+ logger.info("[H5领取] 私聊归属失败 reason=短链无qwExternalId userId={} linkId={}",
|
|
|
|
|
+ currentUser.getUserId(), courseLink.getLinkId());
|
|
|
|
|
+ return claimFail("ownership", TIP_NO_MEMBER);
|
|
|
|
|
+ }
|
|
|
|
|
+ QwExternalContact contact = qwExternalContactMapper.selectQwExternalContactById(courseLink.getQwExternalId());
|
|
|
|
|
+ if (contact == null) {
|
|
|
|
|
+ logger.info("[H5领取] 私聊归属失败 reason=企微客户不存在 userId={} qwExternalId={}",
|
|
|
|
|
+ currentUser.getUserId(), courseLink.getQwExternalId());
|
|
|
|
|
+ return claimFail("ownership", TIP_NO_REGISTER);
|
|
|
|
|
+ }
|
|
|
|
|
+ logger.info("[H5领取] 私聊归属比对 userId={} contactFsUserId={} contactUnionid={} userUnionId={}",
|
|
|
|
|
+ currentUser.getUserId(),
|
|
|
|
|
+ contact.getFsUserId(),
|
|
|
|
|
+ contact.getUnionid(),
|
|
|
|
|
+ currentUser.getUnionId());
|
|
|
|
|
+ return ensureExternalContactBound(contact, currentUser, courseLink, false);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 对齐 handleExt / handleQwRoom 的绑定门禁:
|
|
|
|
|
+ * - 已绑其他 fsUser → 拒绝
|
|
|
|
|
+ * - 已绑本人 → 补齐看课记录 userId / SOP 关联后放行
|
|
|
|
|
+ * - 未绑 → 写入 fsUserId 等绑定字段后放行(独属链以短链上的 qwExternalId 为准)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param groupScene true 时失败文案用群未注册提示
|
|
|
|
|
+ */
|
|
|
|
|
+ private R ensureExternalContactBound(QwExternalContact contact, FsUser currentUser,
|
|
|
|
|
+ FsCourseLink courseLink, boolean groupScene) {
|
|
|
|
|
+ String failTip = groupScene ? TIP_NO_REGISTER : TIP_NO_MEMBER;
|
|
|
|
|
+ String failReason = groupScene ? "ownership_group" : "ownership";
|
|
|
|
|
+
|
|
|
|
|
+ if (contact.getFsUserId() != null) {
|
|
|
|
|
+ if (!contact.getFsUserId().equals(currentUser.getUserId())) {
|
|
|
|
|
+ logger.info("[H5领取] 归属失败 reason=已绑其他用户 userId={} contactFsUserId={} qwExternalId={} group={}",
|
|
|
|
|
+ currentUser.getUserId(), contact.getFsUserId(), contact.getId(), groupScene);
|
|
|
|
|
+ return claimFail(failReason, failTip);
|
|
|
|
|
+ }
|
|
|
|
|
+ syncClaimBindSideEffects(courseLink, contact, currentUser);
|
|
|
|
|
+ logger.info("[H5领取] 归属通过(已绑定本人) userId={} qwExternalId={} group={}",
|
|
|
|
|
+ currentUser.getUserId(), contact.getId(), groupScene);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 未绑定:对齐 handleExt,把当前用户绑到该外部联系人后放行(独属链以 qwExternalId 为准)
|
|
|
|
|
+ logger.info("[H5领取] 外部联系人未绑定,执行绑定 userId={} qwExternalId={} group={}",
|
|
|
|
|
+ currentUser.getUserId(), contact.getId(), groupScene);
|
|
|
|
|
+ bindUnboundExternalContact(contact, currentUser);
|
|
|
|
|
+ syncClaimBindSideEffects(courseLink, contact, currentUser);
|
|
|
|
|
+ logger.info("[H5领取] 归属通过(新绑定) userId={} qwExternalId={} group={}",
|
|
|
|
|
+ currentUser.getUserId(), contact.getId(), groupScene);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 未绑定外部联系人时写入 fsUserId(对齐 handleExt)
|
|
|
|
|
+ */
|
|
|
|
|
+ private void bindUnboundExternalContact(QwExternalContact contact, FsUser currentUser) {
|
|
|
|
|
+ QwExternalContact update = new QwExternalContact();
|
|
|
|
|
+ update.setId(contact.getId());
|
|
|
|
|
+ update.setFsUserId(currentUser.getUserId());
|
|
|
|
|
+ update.setRegisterTime(new Date());
|
|
|
|
|
+ qwExternalContactMapper.updateQwExternalContact(update);
|
|
|
|
|
+ contact.setFsUserId(currentUser.getUserId());
|
|
|
|
|
+
|
|
|
|
|
+ FsUser userUpdate = new FsUser();
|
|
|
|
|
+ userUpdate.setUserId(currentUser.getUserId());
|
|
|
|
|
+ userUpdate.setIsAddQw(1);
|
|
|
|
|
+ userUpdate.setQwExtId(contact.getId());
|
|
|
|
|
+ userService.updateFsUser(userUpdate);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 绑定后补齐 SOP 用户关联、看课记录 userId(对齐 handleExt)
|
|
|
|
|
+ */
|
|
|
|
|
+ private void syncClaimBindSideEffects(FsCourseLink courseLink, QwExternalContact contact, FsUser currentUser) {
|
|
|
|
|
+ Long qwExternalId = contact.getId();
|
|
|
|
|
+ try {
|
|
|
|
|
+ iSopUserLogsInfoService.updateSopUserInfoByExternalId(qwExternalId, currentUser.getUserId());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ logger.warn("[H5领取] 更新SOP用户关联失败 qwExternalId={} userId={} err={}",
|
|
|
|
|
+ qwExternalId, currentUser.getUserId(), e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (courseLink.getVideoId() == null || contact.getQwUserId() == null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ FsCourseWatchLog watchLog = fsCourseWatchLogMapper.getWatchCourseVideoByExt(
|
|
|
|
|
+ qwExternalId, courseLink.getVideoId(), String.valueOf(contact.getQwUserId()));
|
|
|
|
|
+ if (watchLog == null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (watchLog.getUserId() == null || watchLog.getUserId().equals(0L)
|
|
|
|
|
+ || !watchLog.getUserId().equals(currentUser.getUserId())) {
|
|
|
|
|
+ watchLog.setUserId(currentUser.getUserId());
|
|
|
|
|
+ watchLog.setUpdateTime(new Date());
|
|
|
|
|
+ fsCourseWatchLogMapper.updateFsCourseWatchLog(watchLog);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ logger.warn("[H5领取] 更新看课记录失败 qwExternalId={} videoId={} userId={} err={}",
|
|
|
|
|
+ qwExternalId, courseLink.getVideoId(), currentUser.getUserId(), e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 归属通过后解析跳转地址:优先按 link 缓存的飞书 docx;缺字段才走 H5 兜底
|
|
|
|
|
+ */
|
|
|
|
|
+ private R resolveJumpUrlAfterOwned(FsCourseLink courseLink, FsUser currentUser) {
|
|
|
|
|
+ if (canBuildFeishuLink(courseLink)) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ String feishuUrl = buildFeishuJumpUrlCachedByLink(courseLink, currentUser);
|
|
|
|
|
+ if (StringUtils.isNotBlank(feishuUrl)) {
|
|
|
|
|
+ logger.info("[H5领取] 成功 userId={} linkId={} jumpUrl={}",
|
|
|
|
|
+ currentUser.getUserId(), courseLink.getLinkId(), feishuUrl);
|
|
|
|
|
+ return R.ok()
|
|
|
|
|
+ .put("hasMaterial", true)
|
|
|
|
|
+ .put("jumpUrl", feishuUrl)
|
|
|
|
|
+ .put("msg", "ok");
|
|
|
|
|
+ }
|
|
|
|
|
+ logger.warn("[H5领取] 飞书建链返回空 userId={} linkId={}",
|
|
|
|
|
+ currentUser.getUserId(), courseLink.getLinkId());
|
|
|
|
|
+ return claimFail("feishu_app", TIP_FEISHU_APP);
|
|
|
|
|
+ } catch (CustomException e) {
|
|
|
|
|
+ logger.error("[H5领取] 飞书建链业务失败 userId={} linkId={} err={}",
|
|
|
|
|
+ currentUser.getUserId(), courseLink.getLinkId(), e.getMessage(), e);
|
|
|
|
|
+ return claimFail(mapFeishuFailReason(e.getMessage()), mapFeishuFailTip(e.getMessage()));
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ logger.error("[H5领取] 飞书建链异常 userId={} linkId={} err={}",
|
|
|
|
|
+ currentUser.getUserId(), courseLink.getLinkId(), e.getMessage(), e);
|
|
|
|
|
+ return claimFail("system", TIP_SYSTEM);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ logger.info("[H5领取] 短链不具备飞书建链字段,走 H5 兜底 userId={} linkId={}",
|
|
|
|
|
+ currentUser.getUserId(), courseLink.getLinkId());
|
|
|
|
|
+ if (StringUtils.isBlank(courseLink.getRealLink())) {
|
|
|
|
|
+ return claimFail("system", "资料地址未配置,请联系伴学助手");
|
|
|
|
|
+ }
|
|
|
|
|
+ String h5Url = buildH5JumpUrl(courseLink);
|
|
|
|
|
+ if (StringUtils.isBlank(h5Url)) {
|
|
|
|
|
+ return claimFail("system", "资料域名未配置,请联系伴学助手");
|
|
|
|
|
+ }
|
|
|
|
|
+ return R.ok()
|
|
|
|
|
+ .put("hasMaterial", true)
|
|
|
|
|
+ .put("jumpUrl", h5Url)
|
|
|
|
|
+ .put("msg", "ok");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean canBuildFeishuLink(FsCourseLink courseLink) {
|
|
|
|
|
+ return courseLink != null
|
|
|
|
|
+ && courseLink.getCompanyId() != null
|
|
|
|
|
+ && courseLink.getCompanyUserId() != null
|
|
|
|
|
+ && courseLink.getCourseId() != null
|
|
|
|
|
+ && courseLink.getVideoId() != null
|
|
|
|
|
+ && StringUtils.isNotBlank(courseLink.getLink());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 按 link + 销售飞书 appId 缓存;账号被封换新号后 appId 变,自动未命中并重建
|
|
|
|
|
+ */
|
|
|
|
|
+ private String buildFeishuJumpUrlCachedByLink(FsCourseLink courseLink, FsUser currentUser) {
|
|
|
|
|
+ String linkCode = courseLink.getLink();
|
|
|
|
|
+ FeishuAccount activeAccount = feishuAccountService.resolveEnabledOrLatestAccount(
|
|
|
|
|
+ courseLink.getCompanyUserId(), courseLink.getFeishuAccountId());
|
|
|
|
|
+ if (courseLink.getFeishuAccountId() == null
|
|
|
|
|
+ || !activeAccount.getId().equals(courseLink.getFeishuAccountId())) {
|
|
|
|
|
+ logger.info("[H5领取] 飞书账号已切换 link={} oldAccountId={} newAccountId={} appId={}",
|
|
|
|
|
+ linkCode, courseLink.getFeishuAccountId(), activeAccount.getId(), activeAccount.getAppId());
|
|
|
|
|
+ courseLink.setFeishuAccountId(activeAccount.getId());
|
|
|
|
|
+ fsCourseLinkMapper.updateFsCourseLink(courseLink);
|
|
|
|
|
+ }
|
|
|
|
|
+ String cacheKey = FEISHU_CLAIM_LINK_CACHE_PREFIX + linkCode + ":" + activeAccount.getAppId();
|
|
|
|
|
+ Object cached = redisCache.getCacheObject(cacheKey);
|
|
|
|
|
+ if (cached != null && StringUtils.isNotBlank(String.valueOf(cached))) {
|
|
|
|
|
+ logger.info("[H5领取] 命中飞书缓存 link={} appId={} userId={}",
|
|
|
|
|
+ linkCode, activeAccount.getAppId(), currentUser.getUserId());
|
|
|
|
|
+ return String.valueOf(cached);
|
|
|
|
|
+ }
|
|
|
|
|
+ String feishuUrl = createFeishuJumpUrl(courseLink, currentUser);
|
|
|
|
|
+ if (StringUtils.isNotBlank(feishuUrl)) {
|
|
|
|
|
+ redisCache.setCacheObject(cacheKey, feishuUrl, FEISHU_CLAIM_LINK_CACHE_HOURS, TimeUnit.HOURS);
|
|
|
|
|
+ logger.info("[H5领取] 写入飞书缓存 link={} appId={}", linkCode, activeAccount.getAppId());
|
|
|
|
|
+ }
|
|
|
|
|
+ return feishuUrl;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 调用飞书建链(销售自己的飞书 AppID 账号)
|
|
|
|
|
+ */
|
|
|
|
|
+ private String createFeishuJumpUrl(FsCourseLink courseLink, FsUser currentUser) {
|
|
|
|
|
+ String linkCode = courseLink.getLink();
|
|
|
|
|
+ if (FeiShuService.isAuthRequired(courseLink.getFeishuNeedAuth())) {
|
|
|
|
|
+ FeishuLinkParam param = new FeishuLinkParam();
|
|
|
|
|
+ param.setCompanyId(courseLink.getCompanyId());
|
|
|
|
|
+ param.setCompanyUserId(courseLink.getCompanyUserId());
|
|
|
|
|
+ param.setCourseId(courseLink.getCourseId());
|
|
|
|
|
+ param.setVideoId(courseLink.getVideoId());
|
|
|
|
|
+ param.setUserId(currentUser.getUserId());
|
|
|
|
|
+ param.setLink(linkCode);
|
|
|
|
|
+ logger.info("[H5领取] 调用需授权飞书建链 userId={} link={} companyUserId={} videoId={}",
|
|
|
|
|
+ currentUser.getUserId(), linkCode, courseLink.getCompanyUserId(), courseLink.getVideoId());
|
|
|
|
|
+ return feiShuService.getFeishuCourseNewLink(param);
|
|
|
|
|
+ }
|
|
|
|
|
+ logger.info("[H5领取] 调用免授权飞书建链 userId={} link={} companyUserId={} videoId={}",
|
|
|
|
|
+ currentUser.getUserId(), linkCode, courseLink.getCompanyUserId(), courseLink.getVideoId());
|
|
|
|
|
+ return feiShuService.getFeishuDirectCourseLink(
|
|
|
|
|
+ courseLink.getVideoId(),
|
|
|
|
|
+ courseLink.getCompanyId(),
|
|
|
|
|
+ courseLink.getCourseId(),
|
|
|
|
|
+ courseLink.getCompanyUserId(),
|
|
|
|
|
+ linkCode,
|
|
|
|
|
+ courseLink.getFeishuAccountId());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String buildH5JumpUrl(FsCourseLink courseLink) {
|
|
|
|
|
+ String json = configService.selectConfigByKey("course.config");
|
|
|
|
|
+ CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
|
|
|
|
|
+ String domainName = companyUserMapper.selectDomainByUserId(courseLink.getCompanyUserId());
|
|
|
|
|
+ if (StringUtils.isEmpty(domainName) && config != null) {
|
|
|
|
|
+ domainName = config.getRealLinkDomainName();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StringUtils.isEmpty(domainName)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return domainName + courseLink.getRealLink();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 领取失败:带 reason 便于排查,msg 给前端直接展示
|
|
|
|
|
+ */
|
|
|
|
|
+ private R claimFail(String reason, String tip) {
|
|
|
|
|
+ return R.ok(tip)
|
|
|
|
|
+ .put("hasMaterial", false)
|
|
|
|
|
+ .put("msg", tip)
|
|
|
|
|
+ .put("reason", reason);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String mapFeishuFailReason(String errMsg) {
|
|
|
|
|
+ if (StringUtils.isBlank(errMsg)) {
|
|
|
|
|
+ return "system";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (errMsg.contains("未开启飞书") || errMsg.contains("飞书账号") || errMsg.contains("没有可用")
|
|
|
|
|
+ || errMsg.contains("账号不可用") || errMsg.contains("更换账号")) {
|
|
|
|
|
+ return "feishu_app";
|
|
|
|
|
+ }
|
|
|
|
|
+ return "system";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String mapFeishuFailTip(String errMsg) {
|
|
|
|
|
+ if (StringUtils.isBlank(errMsg)) {
|
|
|
|
|
+ return TIP_SYSTEM;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (errMsg.contains("未开启飞书") || errMsg.contains("飞书账号") || errMsg.contains("没有可用")
|
|
|
|
|
+ || errMsg.contains("账号不可用") || errMsg.contains("更换账号")) {
|
|
|
|
|
+ return TIP_FEISHU_APP;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 业务异常文案本身已是中文,可直接给客户看
|
|
|
|
|
+ if (errMsg.length() <= 80 && !errMsg.contains("Exception") && !errMsg.contains("null")) {
|
|
|
|
|
+ return errMsg;
|
|
|
|
|
+ }
|
|
|
|
|
+ return TIP_SYSTEM;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|