yh 2 päivää sitten
vanhempi
commit
0abc441b6b

+ 129 - 7
fs-admin/src/main/java/com/fs/web/controller/system/SysLoginController.java

@@ -1,22 +1,28 @@
 package com.fs.web.controller.system;
 
-import java.util.List;
-import java.util.Set;
+import java.util.*;
+import java.util.stream.Collectors;
 
 import com.alibaba.fastjson.JSONObject;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.entity.SysRole;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.ServiceException;
 import com.fs.common.utils.PatternUtils;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.ip.IpUtils;
+import com.fs.framework.web.service.TokenService;
 import com.fs.his.utils.ConfigUtil;
 import com.fs.hisStore.config.MedicalMallConfig;
 import com.fs.system.service.ISysRoleService;
+import com.fs.system.service.ISysUserService;
 import lombok.Synchronized;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Async;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.web.bind.annotation.*;
 import com.fs.common.constant.Constants;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.entity.SysMenu;
@@ -33,7 +39,7 @@ import com.fs.system.service.ISysMenuService;
 
  */
 @RestController
-
+@Slf4j
 public class SysLoginController
 {
     @Autowired
@@ -54,6 +60,18 @@ public class SysLoginController
     @Autowired
     private MedicalMallConfig medicalMallConfig;
 
+    @Autowired
+    RedisCache redisCache;
+
+    @Autowired
+    private ISysUserService userService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private UserDetailsService userDetailsService;
+
     /**
      * 登录方法
      *
@@ -141,4 +159,108 @@ public class SysLoginController
         List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
         return AjaxResult.success(menuService.buildMenus(menus));
     }
+
+    @PostMapping("/checkIsNeedCheck")
+    public boolean checkIsNeedCheck(@RequestBody LoginBody loginBody)
+    {
+//        return  false;
+        return loginService.checkIsNeedCheck(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(), loginBody.getUuid());
+    }
+
+    @PostMapping("/checkCode")
+    public AjaxResult checkCode(@RequestBody Map<String,String> map)
+    {
+        String phone = map.get("phone");
+        String code = map.get("code");
+        String smsKey = "doctorLogin:sms:" + map.get("phone");
+        String smsCode = redisCache.getCacheObject(smsKey);
+
+        if (smsCode == null) {
+            throw new ServiceException("验证码已过期,请重新发送");
+        } else {
+            String string = redisCache.getCacheObject("doctorLogin:sms:" + phone).toString();
+            if (!string.equals(code)){
+                throw new ServiceException("验证码错误");
+            }else{
+                redisCache.deleteObject("doctorLogin:sms:" + phone);
+                List<SysUser> sysUsers = userService.selectUserByPhone(phone);
+                if(sysUsers.size()>1){
+                    throw new ServiceException("此电话号码绑定了多个医生,请核实");
+                }
+                SysUser sysUser = sysUsers.get(0);
+                String ipAddr = IpUtils.getIpAddr(ServletUtils.getRequest());
+                String username = sysUser.getUserName();
+
+                // 调 UserDetailsServiceImpl.loadUserByUsername 获取完整 LoginUser
+                sysUser.setLoginIp(ipAddr);
+                sysUser.setLoginDate(new Date());
+                userService.updateUserProfile(sysUser);
+                LoginUser loginUser = (LoginUser) userDetailsService.loadUserByUsername(username);
+                String token = tokenService.createToken(loginUser);
+                AjaxResult ajax = AjaxResult.success();
+                ajax.put(Constants.TOKEN, token);
+                return ajax;
+            }
+        }
+    }
+
+    @GetMapping("/checkWechatScan")
+    public AjaxResult checkWechatScan(@RequestParam String ticket)
+    {
+        String status = redisCache.getCacheObject("wechat:scan:" + ticket);
+        if ("ok".equals(status)) {
+            String username = redisCache.getCacheObject("login:ticket:" + ticket);
+            redisCache.deleteObject("login:ticket:" + ticket);
+            redisCache.deleteObject("wechat:scan:" + ticket);
+            SysUser sysUser = userService.selectUserByUserName(username);
+
+            String ipAddr = IpUtils.getIpAddr(ServletUtils.getRequest());
+            String loginIp = sysUser.getLoginIp();
+            if (com.fs.common.utils.StringUtils.isEmpty(loginIp)) {
+                sysUser.setLoginIp(ipAddr.trim());
+            } else {
+                List<String> ipList = Arrays.stream(loginIp.split(","))
+                        .map(String::trim)       // 去掉前后空格
+                        .filter(s -> !s.isEmpty())
+                        .distinct()              // 去重
+                        .collect(Collectors.toList());
+
+                String newIp = ipAddr.trim();
+                if (!ipList.contains(newIp)) {
+                    ipList.add(newIp);
+                }
+
+                sysUser.setLoginIp(String.join(",", ipList));
+            }
+
+            sysUser.setLoginDate(new Date());
+            userService.updateUserProfile(sysUser);
+            LoginUser loginUser = (LoginUser) userDetailsService.loadUserByUsername(username);
+            String token = tokenService.createToken(loginUser);
+            if (token != null){
+                return AjaxResult.success(Constants.TOKEN, token);
+            }
+            return AjaxResult.success("waiting");
+        }else if (com.fs.common.utils.StringUtils.isNotEmpty(status)&&status.startsWith("error:")) {
+            // 把错误返回给前端
+            throw new ServiceException(status);
+        }
+        return null;
+    }
+
+    @PostMapping("/getWechatQrCode")
+    public AjaxResult getWechatQrCode(@RequestBody Map<String,String> params) throws Exception {
+        Map<String,String> qr = loginService.getWechatQrCode(params.get("username"));
+        return AjaxResult.success(qr);
+    }
+    @GetMapping("/callback")
+    public String wechatCallback(@RequestParam String code, @RequestParam String state) {
+        try {
+            log.info("触发回调");
+            loginService.handleCallback(code, state);
+            return "success"; // 微信要求返回内容,显示给用户即可
+        } catch (Exception e) {
+            return "error";
+        }
+    }
 }

+ 10 - 0
fs-common/src/main/java/com/fs/common/core/domain/entity/SysUser.java

@@ -103,6 +103,16 @@ public class SysUser extends BaseEntity
     @Excel(name = "角色名称")
     private List<String> roleName;
 
+    private String unionId;
+
+    public String getUnionId() {
+        return unionId;
+    }
+
+    public void setUnionId(String unionId) {
+        this.unionId = unionId;
+    }
+
     public SysUser()
     {
 

+ 51 - 4
fs-company/src/main/java/com/fs/company/controller/company/CompanyLoginController.java

@@ -4,11 +4,18 @@ import com.alibaba.fastjson.JSONObject;
 import com.fs.common.constant.Constants;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.ServiceException;
+import com.fs.common.exception.user.CaptchaException;
+import com.fs.common.exception.user.CaptchaExpireException;
+import com.fs.common.exception.user.UserPasswordNotMatchException;
+import com.fs.common.utils.MessageUtils;
 import com.fs.common.utils.PatternUtils;
 import com.fs.common.utils.ServletUtils;
 import com.fs.company.domain.CompanyMenu;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.service.ICompanyMenuService;
+import com.fs.framework.manager.AsyncManager;
+import com.fs.framework.manager.factory.AsyncFactory;
 import com.fs.framework.security.LoginBody;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.CompanyLoginService;
@@ -16,13 +23,16 @@ import com.fs.framework.service.CompanyPermissionService;
 import com.fs.framework.service.TokenService;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.service.ISysConfigService;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.*;
 
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -31,6 +41,7 @@ import java.util.Set;
 
  */
 @RestController
+@Slf4j
 public class CompanyLoginController
 {
     @Autowired
@@ -133,5 +144,41 @@ public class CompanyLoginController
         return AjaxResult.success(false);
     }
 
+    @PostMapping("/checkIsNeedCheck")
+    public boolean checkIsNeedCheck(@RequestBody LoginBody loginBody)
+    {
+//        return false;
+        return loginService.checkIsNeedCheck(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(), loginBody.getUuid());
+    }
+
+    @PostMapping("/getWechatQrCode")
+    public AjaxResult getWechatQrCode(@RequestBody Map<String,String> params) throws Exception {
+        Map<String,String> qr = loginService.getWechatQrCode(params.get("username"));
+        return AjaxResult.success(qr);
+    }
+
+    @GetMapping("/checkWechatScan")
+    public AjaxResult checkWechatScan(@RequestParam String ticket) {
+        //log.info("触发轮询");
+        String token = loginService.checkWechatScan(ticket);
+        Map<String, String> stringStringMap = Collections.singletonMap(Constants.TOKEN, token);
+        if (token != null){
+            return AjaxResult.success(Constants.TOKEN, token);
+        }
+        return AjaxResult.success("waiting");
+    }
+
+    @GetMapping("/callback")
+    public String wechatCallback(@RequestParam String code, @RequestParam String state) {
+        try {
+            log.info("触发回调");
+            loginService.handleCallback(code, state);
+            return "success"; // 微信要求返回内容,显示给用户即可
+        } catch (Exception e) {
+            return "error";
+        }
+    }
+
+
 }
 

+ 1 - 1
fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -100,7 +100,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 // 过滤请求
                 .authorizeRequests()
                 // 对于登录login 注册register 验证码captchaImage 允许匿名访问
-                .antMatchers("/chat/upload/**","/login", "/register", "/captchaImage").anonymous()
+                .antMatchers("/chat/upload/**","/login", "/register", "/captchaImage","/checkIsNeedCheck","/getWechatQrCode","/checkWechatScan","/callback").anonymous()
                 .antMatchers(
                         HttpMethod.GET,
                         "/",

+ 256 - 0
fs-company/src/main/java/com/fs/framework/service/CompanyLoginService.java

@@ -1,26 +1,40 @@
 package com.fs.framework.service;
 
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
 import com.fs.common.constant.Constants;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.exception.ServiceException;
 import com.fs.common.exception.user.CaptchaException;
 import com.fs.common.exception.user.CaptchaExpireException;
 import com.fs.common.exception.user.UserPasswordNotMatchException;
+import com.fs.common.service.WechatLoginService;
 import com.fs.common.utils.MessageUtils;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.ip.IpUtils;
+import com.fs.company.domain.CompanyUser;
+import com.fs.company.service.ICompanyUserService;
 import com.fs.framework.manager.AsyncManager;
 import com.fs.framework.manager.factory.AsyncFactory;
 import com.fs.framework.security.LoginUser;
 import com.fs.his.domain.StoreLoginUser;
 import com.fs.system.service.ISysConfigService;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
+import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.stereotype.Component;
 
 import javax.annotation.Resource;
+import java.util.*;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 /**
  * 登录校验方法
@@ -28,6 +42,7 @@ import java.util.concurrent.TimeUnit;
  
  */
 @Component
+@Slf4j
 public class CompanyLoginService
 {
     @Autowired
@@ -39,6 +54,24 @@ public class CompanyLoginService
     @Autowired
     private RedisCache redisCache;
 
+    @Autowired
+    private ICompanyUserService companyUserService;
+
+    @Value("${wechat.company.appid}")
+    private String appId;
+    @Value("${wechat.company.secret}")
+    private String secret;
+    @Value("${wechat.company.redirectUri}")
+    private String redirectUri;
+    @Value("${wechat.isNeedScan}")
+    private Boolean isNeedScan;
+
+    @Autowired
+    private WechatLoginService wechatLoginService;
+
+    @Autowired
+    private UserDetailsService userDetailsService;
+
     /**
      * 登录验证
      *
@@ -91,4 +124,227 @@ public class CompanyLoginService
         return tokenService.createToken(loginUser);
     }
 
+
+    public boolean checkIsNeedCheck(String username, String password, String code, String uuid)
+    {
+        String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
+        String captcha = redisCache.getCacheObject(verifyKey);
+        //redisCache.deleteObject(verifyKey);
+        if (captcha == null)
+        {
+            AsyncManager.me().execute(AsyncFactory.recordLogininfor(0l,username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
+            throw new CaptchaExpireException();
+        }
+        if (!code.equalsIgnoreCase(captcha))
+        {
+            AsyncManager.me().execute(AsyncFactory.recordLogininfor(0l,username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
+            throw new CaptchaException();
+        }
+        // 用户验证
+        Authentication authentication = null;
+        try
+        {
+            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
+            authentication = authenticationManager
+                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
+        }
+        catch (Exception e)
+        {
+            if (e instanceof BadCredentialsException)
+            {
+                AsyncManager.me().execute(AsyncFactory.recordLogininfor(0l,username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
+                throw new UserPasswordNotMatchException();
+            }
+            else
+            {
+                AsyncManager.me().execute(AsyncFactory.recordLogininfor(0l,username, Constants.LOGIN_FAIL, e.getMessage()));
+                throw new ServiceException(e.getMessage());
+            }
+        }
+        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
+        //查询当前登录用户信息
+        CompanyUser companyUser = companyUserService.selectCompanyUserById(loginUser.getUser().getUserId());
+
+        Long[] userIds = new Long[]{2020L};
+        for (Long userId : userIds) {
+            if (userId.equals(companyUser.getUserId())) {
+                return false;
+            }
+        }
+
+        // 判断是否开启了扫码配置
+        if (!isNeedScan){
+            return false;
+        }
+
+        //true → 要发短信验证码再登录
+        //false → 直接登录
+        return needCheck(companyUser);
+    }
+
+    public boolean needCheck(CompanyUser companyUser) {
+        // 1. 校验 IP
+        if (!checkIp(companyUser)) {
+            // IP 不一致
+            return true;
+        }
+
+        // 2. 校验是否首次登录
+        if (checkIsFirstLogin(companyUser)) {
+            return true;
+        }
+
+        // 3. 校验上次登录时间是否在五天前
+        if (checkIsLoginTime(companyUser)) {
+            return true;
+        }
+
+        // 4. 检查是否在设置的某一天
+        /*if (checkIsSpecialDay(new Date())) {
+            return true;
+        }*/
+        if (haveUnionId(companyUser)){
+            return true;
+        }
+
+        return false;
+    }
+
+    public boolean checkIp(CompanyUser companyUser) {
+        // 获取当前 IP
+        String ipAddr = IpUtils.getIpAddr(ServletUtils.getRequest()).split(",")[0].trim();
+
+        // 获取已记录的登录 IP
+        String lastLoginIp = companyUser.getLoginIp();
+
+        if (StringUtils.isNotEmpty(lastLoginIp)) {
+            List<String> ipList = Arrays.stream(lastLoginIp.split(","))
+                    .map(String::trim)
+                    .filter(s -> !s.isEmpty())
+                    .distinct()
+                    .collect(Collectors.toList());
+
+            return ipList.contains(ipAddr);
+        }
+        return false;
+    }
+
+    //检查是否第一次登录
+    public boolean checkIsFirstLogin(CompanyUser companyUser){
+        // 获取上次登录 IP
+        String lastLoginIp = companyUser.getLoginIp();
+        if (StringUtils.isEmpty(lastLoginIp)||companyUser.getLoginDate()==null){
+            return true;
+        }
+        return false;
+    }
+    public boolean checkIsLoginTime(CompanyUser companyUser) {
+        // 获取上次登录时间
+        Date loginDate = companyUser.getLoginDate();
+        if (loginDate == null) {
+            // 没有登录记录,直接返回 true(需要处理)
+            return true;
+        }
+
+        // 当前时间
+        Date now = new Date();
+
+        // 计算两个时间的毫秒差
+        long diff = now.getTime() - loginDate.getTime();
+
+        // 5天 = 5 * 24 * 60 * 60 * 1000 毫秒
+        long fiveDays = 5L * 24 * 60 * 60 * 1000;
+
+        return diff >= fiveDays;
+    }
+
+    public boolean haveUnionId( CompanyUser companyUser){
+        if (StringUtils.isEmpty(companyUser.getUnionId())){
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 获取微信登录二维码参数
+     * @param username 当前登录用户名
+     * @return 二维码参数
+     */
+    public Map<String, String> getWechatQrCode(String username) throws Exception {
+        // 生成 loginTicket
+        String ticket = UUID.randomUUID().toString();
+        redisCache.setCacheObject("login:ticket:" + ticket, username, 60, TimeUnit.SECONDS);
+
+        return wechatLoginService.getQrCode(ticket,appId,secret,redirectUri); // 返回二维码参数
+    }
+
+    public String checkWechatScan(String ticket) {
+        String status = redisCache.getCacheObject("wechat:scan:" + ticket);
+        if ("ok".equals(status)) {
+            String username = redisCache.getCacheObject("login:ticket:" + ticket);
+            redisCache.deleteObject("login:ticket:" + ticket);
+            redisCache.deleteObject("wechat:scan:" + ticket);
+            CompanyUser companyUser = companyUserService.selectUserByUserName(username);
+
+            // 调 UserDetailsServiceImpl.loadUserByUsername 获取完整 LoginUser
+            LoginUser loginUser = (LoginUser) userDetailsService.loadUserByUsername(username);
+            companyUser.setUserId(loginUser.getUser().getUserId());
+            String loginIp = companyUser.getLoginIp();
+            String ipAddr = IpUtils.getIpAddr(ServletUtils.getRequest()).split(",")[0].trim();
+            log.info("销售用户{}扫码验证过后登录获取到的ip地址{}", loginUser.getUser().getUserId(), ipAddr);
+            List<String> ipList = new ArrayList<>();
+            if (StringUtils.isNotEmpty(loginIp)) {
+                String[] ips = loginIp.split(",");
+                for (String ip : ips) {
+                    ip = ip.trim();
+                    if (!ip.isEmpty()) {
+                        ipList.add(ip);
+                    }
+                }
+            }
+            ipList.add(ipAddr);
+            List<String> distinctList = ipList.stream()
+                    .map(String::trim)       // 再次确保去掉空格
+                    .distinct()
+                    .collect(Collectors.toList());
+            companyUser.setLoginIp(String.join(",", distinctList));
+            companyUser.setLoginDate(new Date());
+            companyUserService.updateCompanyUser(companyUser);
+            return tokenService.createToken(loginUser);
+        }else if (StringUtils.isNotEmpty(status)&&status.startsWith("error:")) {
+            // 把错误返回给前端
+            throw new ServiceException(status);
+        }
+        return null;
+    }
+    /**
+     * 微信扫码回调
+     */
+    public void handleCallback(String code, String ticket) {
+        String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + appId
+                + "&secret=" + secret
+                + "&code=" + code
+                + "&grant_type=authorization_code";
+
+        JSONObject json = JSON.parseObject(HttpUtil.get(url));
+        String unionid = json.getString("unionid");
+        if (unionid == null) throw new ServiceException("微信授权失败");
+
+        String username = redisCache.getCacheObject("login:ticket:" + ticket);
+        if (username == null) throw new ServiceException("ticket无效或过期");
+        CompanyUser companyUser = companyUserService.selectUserByUserName(username);
+        if (companyUser == null) throw new ServiceException("用户不存在");
+        if (companyUser.getUnionId() == null || companyUser.getUnionId().isEmpty()) {
+            // 如果用户没有绑定 unionid,则绑定当前扫码用户的 unionid
+            companyUser.setUnionId(unionid);
+            companyUserService.updateCompanyUser(companyUser);
+        } else if (!companyUser.getUnionId().equals(unionid)) {
+            // 如果用户已绑定 unionid,但与扫码用户不一致,则拒绝登录
+            redisCache.setCacheObject("wechat:scan:" + ticket, "error:账号与绑定用户不匹配", 30, TimeUnit.SECONDS);
+            return;
+        }
+
+        redisCache.setCacheObject("wechat:scan:" + ticket, "ok", 30, TimeUnit.SECONDS);
+    }
+
 }

+ 1 - 1
fs-framework/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -97,7 +97,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 // 过滤请求
                 .authorizeRequests()
                 // 对于登录login 注册register 验证码captchaImage 允许匿名访问
-                .antMatchers("/login", "/register", "/captchaImage").anonymous()
+                .antMatchers("/login", "/register", "/captchaImage","/getWechatQrCode","/checkWechatScan","/callback","/checkIsNeedCheck").anonymous()
                 .antMatchers("/app/common/test").anonymous()
                 .antMatchers("/ad/adDyApi/authorized").anonymous()
                 .antMatchers(

+ 195 - 0
fs-framework/src/main/java/com/fs/framework/web/service/SysLoginService.java

@@ -1,7 +1,13 @@
 package com.fs.framework.web.service;
 
 import javax.annotation.Resource;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.service.WechatLoginService;
+import com.fs.common.utils.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -24,6 +30,9 @@ import com.fs.framework.manager.factory.AsyncFactory;
 import com.fs.system.service.ISysConfigService;
 import com.fs.system.service.ISysUserService;
 
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
 /**
  * 登录校验方法
  * 
@@ -46,6 +55,17 @@ public class SysLoginService
 
     @Autowired
     private ISysConfigService configService;
+    @Autowired
+    private WechatLoginService wechatLoginService;
+
+    @Value("${wechat.admin.appid}")
+    private String appId;
+    @Value("${wechat.admin.secret}")
+    private String secret;
+    @Value("${wechat.admin.redirectUri}")
+    private String redirectUri;
+    @Value("${wechat.isNeedScan}")
+    private Boolean isNeedScan;
 
     /**
      * 登录验证
@@ -126,4 +146,179 @@ public class SysLoginService
         user.setLoginDate(DateUtils.getNowDate());
         userService.updateUserProfile(user);
     }
+
+
+    public boolean checkIsNeedCheck(String username, String password, String code, String uuid)
+    {
+        String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
+        String captcha = redisCache.getCacheObject(verifyKey);
+        //redisCache.deleteObject(verifyKey);
+        if (captcha == null)
+        {
+            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
+            throw new CaptchaExpireException();
+        }
+        if (!code.equalsIgnoreCase(captcha))
+        {
+            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
+            throw new CaptchaException();
+        }
+        // 用户验证
+        Authentication authentication = null;
+        try
+        {
+            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
+            authentication = authenticationManager
+                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
+        }
+        catch (Exception e)
+        {
+            if (e instanceof BadCredentialsException)
+            {
+                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
+                throw new UserPasswordNotMatchException();
+            }
+            else
+            {
+                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
+                throw new ServiceException(e.getMessage());
+            }
+        }
+        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
+        //查询当前登录用户信息
+        SysUser sysUser = userService.selectUserById(loginUser.getUserId());
+        Long[] userIds = new Long[]{236L, 246L, 247L, 253L,119L};
+        for (Long userId : userIds) {
+            if (userId.equals(sysUser.getUserId())){
+                return false;
+            }
+        }
+
+        // 判断是否开启了扫码配置
+        if (!isNeedScan){
+            return false;
+        }
+
+        //true → 要发短信验证码再登录
+        //false → 直接登录
+        return needCheck(sysUser);
+    }
+    public boolean needCheck(SysUser sysUser) {
+
+
+        // 1. 校验 IP
+        if (!checkIp(sysUser)) {
+            // IP 不一致
+            return true;
+        }
+
+        // 2. 校验是否首次登录
+        if (checkIsFirstLogin(sysUser)) {
+            return true;
+        }
+
+        // 3. 校验上次登录时间是否在五天前
+        if (checkIsLoginTime(sysUser)) {
+            return true;
+        }
+
+        // 4. 检查是否在设置的某一天
+//        if (checkIsSpecialDay(new Date())) {
+//            return true;
+//        }
+        if (haveUnionId(sysUser)){
+            return true;
+        }
+
+        return false;
+    }
+    public boolean haveUnionId( SysUser sysUser){
+        if (StringUtils.isEmpty(sysUser.getUnionId())){
+            return true;
+        }
+        return false;
+    }
+    public boolean checkIp(SysUser sysUser){
+        // 获取当前 IP
+        String ipAddr = IpUtils.getIpAddr(ServletUtils.getRequest());
+        // 获取已记录的登录 IP
+        String lastLoginIp = sysUser.getLoginIp();
+
+        if (StringUtils.isNotEmpty(lastLoginIp)) {
+            List<String> ipList = Arrays.asList(lastLoginIp.split(","));
+            return ipList.contains(ipAddr);
+        }
+        return false;
+    }
+    //检查是否第一次登录
+    public boolean checkIsFirstLogin(SysUser sysUser){
+        // 获取上次登录 IP
+        String lastLoginIp = sysUser.getLoginIp();
+        if (StringUtils.isEmpty(lastLoginIp)||sysUser.getLoginDate()==null){
+            return true;
+        }
+        return false;
+    }
+    public boolean checkIsLoginTime(SysUser sysUser) {
+        // 获取上次登录时间
+        Date loginDate = sysUser.getLoginDate();
+        if (loginDate == null) {
+            // 没有登录记录,直接返回 true(需要处理)
+            return true;
+        }
+
+        // 当前时间
+        Date now = new Date();
+
+        // 计算两个时间的毫秒差
+        long diff = now.getTime() - loginDate.getTime();
+
+        // 5天 = 5 * 24 * 60 * 60 * 1000 毫秒
+        long fiveDays = 5L * 24 * 60 * 60 * 1000;
+
+        return diff >= fiveDays;
+    }
+
+    /**
+     * 获取微信登录二维码参数
+     * @param account 当前登录用户名
+     * @return 二维码参数
+     */
+    public Map<String, String> getWechatQrCode(String account) throws Exception {
+        // 生成 loginTicket
+        String ticket = UUID.randomUUID().toString();
+        redisCache.setCacheObject("login:ticket:" + ticket, account, 60, TimeUnit.SECONDS);
+
+        return wechatLoginService.getQrCode(ticket,appId,secret,redirectUri); // 返回二维码参数
+    }
+
+    /**
+     * 微信扫码回调
+     */
+    public void handleCallback(String code, String ticket) {
+        String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + appId
+                + "&secret=" + secret
+                + "&code=" + code
+                + "&grant_type=authorization_code";
+
+        JSONObject json = JSON.parseObject(cn.hutool.http.HttpUtil.get(url));
+        String unionid = json.getString("unionid");
+        if (unionid == null) throw new ServiceException("微信授权失败");
+
+        String username = redisCache.getCacheObject("login:ticket:" + ticket);
+        if (username == null) throw new ServiceException("ticket无效或过期");
+        SysUser sysUser = userService.selectUserByUserName(username);
+        if (sysUser == null) throw new ServiceException("用户不存在");
+        if (sysUser.getUnionId() == null || sysUser.getUnionId().isEmpty()) {
+            // 如果用户没有绑定 unionid,则绑定当前扫码用户的 unionid
+            sysUser.setUnionId(unionid);
+            userService.updateUserProfile(sysUser);
+        } else if (!sysUser.getUnionId().equals(unionid)) {
+            // 如果用户已绑定 unionid,但与扫码用户不一致,则拒绝登录
+            redisCache.setCacheObject("wechat:scan:" + ticket, "error:账号与绑定用户不匹配", 30, TimeUnit.SECONDS);
+            return;
+        }
+
+        redisCache.setCacheObject("wechat:scan:" + ticket, "ok", 30, TimeUnit.SECONDS);
+    }
 }

+ 49 - 0
fs-service/src/main/java/com/fs/common/service/WechatLoginService.java

@@ -0,0 +1,49 @@
+package com.fs.common.service;
+
+import com.fs.common.core.redis.RedisCache;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+@Service
+public class WechatLoginService {
+
+    @Autowired
+    private RedisCache redisCache;
+    /**
+     * 获取二维码参数
+     */
+    public Map<String, String> getQrCode(String ticket,String appId,String secret,String redirectUri) throws UnsupportedEncodingException {
+        String username = redisCache.getCacheObject("login:ticket:" + ticket);
+        if (username == null) throw new RuntimeException("ticket无效或过期");
+
+        Map<String, String> data = new HashMap<>();
+        data.put("appid", appId);
+        data.put("scope", "snsapi_login");
+        data.put("state", ticket);
+        data.put("redirect_uri", redirectUri);
+
+        // 拼接完整的微信扫码 URL
+        String url = "https://open.weixin.qq.com/connect/qrconnect?appid=" + appId
+                + "&redirect_uri=" + URLEncoder.encode(redirectUri, StandardCharsets.UTF_8.name())
+                + "&response_type=code"
+                + "&scope=snsapi_login"
+                + "&state=" + ticket
+                + "#wechat_redirect";
+        data.put("url", url);
+        log.info("url{}",url);
+        return data;
+    }
+
+
+
+
+
+}

+ 10 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyUser.java

@@ -94,6 +94,16 @@ public class CompanyUser extends BaseEntity
     /** 医生id */
     private Long doctorId;
 
+    private String unionId;
+
+    public String getUnionId() {
+        return unionId;
+    }
+
+    public void setUnionId(String unionId) {
+        this.unionId = unionId;
+    }
+
     public String getIdCard() {
         return idCard;
     }

+ 2 - 0
fs-service/src/main/java/com/fs/system/mapper/SysUserMapper.java

@@ -130,4 +130,6 @@ public interface SysUserMapper
      * @return 结果
      */
     public SysUser checkEmailUnique(String email);
+
+    List<SysUser> selectUserByPhone(String phone);
 }

+ 2 - 0
fs-service/src/main/java/com/fs/system/service/ISysUserService.java

@@ -214,4 +214,6 @@ public interface ISysUserService
     public String importUser(List<SysUser> userList, Boolean isUpdateSupport, String operName);
 
     int updateUserInfo(SysUser sysuser);
+
+    List<SysUser> selectUserByPhone(String phone);
 }

+ 4 - 0
fs-service/src/main/java/com/fs/system/service/impl/SysUserServiceImpl.java

@@ -586,4 +586,8 @@ public class SysUserServiceImpl implements ISysUserService
     public int updateUserInfo(SysUser sysuser) {
         return userMapper.updateUser(sysuser);
     }
+
+    public List<SysUser> selectUserByPhone(String phone) {
+        return userMapper.selectUserByPhone(phone);
+    }
 }

+ 9 - 0
fs-service/src/main/resources/application-common.yml

@@ -143,3 +143,12 @@ wechat:
   api:
     base-url: https://api.weixin.qq.com
     upload-shipping-info: /wxa/sec/order/upload_shipping_info
+  company:
+    appid: wxd7c1e221622a0ccf
+    secret: 70d3ed4f8eb68cca0cf525b8ce07405d
+    redirectUri: http://rfa96c48.natappfree.cc/callback
+  admin:
+    appid: wxd7c1e221622a0ccf
+    secret: 70d3ed4f8eb68cca0cf525b8ce07405d
+    redirectUri: http://rfa96c48.natappfree.cc/callback
+  isNeedScan: false

+ 3 - 1
fs-service/src/main/resources/mapper/company/CompanyUserMapper.xml

@@ -44,6 +44,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="isAllowedAllRegister"    column="is_allowed_all_register"    />
         <result property="doctorId"    column="doctor_id"    />
         <result property="bindCompanyUserId"    column="bind_company_user_id"    />
+        <result property="unionId"    column="union_id"    />
         <association property="dept"    column="dept_id" javaType="CompanyDept" resultMap="deptResult" />
         <collection  property="roles"   javaType="java.util.List"        resultMap="RoleResult" />
     </resultMap>
@@ -315,6 +316,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="domain != null">domain = #{domain},</if>
             <if test="isAudit != null">`is_audit` = #{isAudit},</if>
             <if test="doctorId != null">`doctor_id` = #{doctorId},</if>
+            <if test="unionId != null">`union_id` = #{unionId},</if>
         </trim>
         where user_id = #{userId}
     </update>
@@ -443,7 +445,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                u.create_time,u.id_card, u.remark,u.user_type,u.open_id,u.qr_code_weixin,u.qr_code_wecom,u.jpush_id,u.domain,u.is_audit,u.address_id,
                d.dept_id, d.parent_id, d.dept_name, d.order_num, d.leader, d.status as dept_status,
                r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status,
-               u.is_need_register_member, u.is_allowed_all_register,u.doctor_id
+               u.is_need_register_member, u.is_allowed_all_register,u.doctor_id,u.union_id
         from company_user u
                  left join company_dept d on u.dept_id = d.dept_id
                  left join company_user_role ur on u.user_id = ur.user_id

+ 9 - 2
fs-service/src/main/resources/mapper/system/SysUserMapper.xml

@@ -50,6 +50,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 		<result property="updateTime"   column="update_time"  />
 		<result property="remark"       column="remark"       />
 		<result property="companyId"       column="company_id"       />
+		<result property="unionId"       column="union_id"       />
 		<association property="dept"    column="dept_id" javaType="SysDept" resultMap="deptResult" />
 		<collection  property="roles"   javaType="java.util.List"        resultMap="RoleResult" />
 	</resultMap>
@@ -75,7 +76,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 	<sql id="selectUserVo">
         select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark,
         d.dept_id, d.parent_id, d.dept_name, d.order_num, d.leader, d.status as dept_status,
-        r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status,u.company_id
+        r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status,u.company_id,u.union_id
         from sys_user u
 		    left join sys_dept d on u.dept_id = d.dept_id
 		    left join sys_user_role ur on u.user_id = ur.user_id
@@ -247,7 +248,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 		select user_id, email from sys_user where email = #{email} limit 1
 	</select>
 
-	<insert id="insertUser" parameterType="SysUser" useGeneratedKeys="true" keyProperty="userId">
+    <select id="selectUserByPhone" resultType="com.fs.common.core.domain.entity.SysUser">
+		<include refid="selectUserVo"/>
+		where u.phonenumber = #{phone}
+	</select>
+
+    <insert id="insertUser" parameterType="SysUser" useGeneratedKeys="true" keyProperty="userId">
  		insert into sys_user(
  			<if test="userId != null and userId != 0">user_id,</if>
  			<if test="deptId != null and deptId != 0">dept_id,</if>
@@ -298,6 +304,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  			<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
  			<if test="remark != null">remark = #{remark},</if>
  			<if test="companyId != null">company_id = #{companyId},</if>
+ 			<if test="unionId != null">union_id = #{unionId},</if>
  			update_time = sysdate()
  		</set>
  		where user_id = #{userId}