package com.tourism.webadmin.app.website.controller;

import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.api.WxMaUserService;
import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.google.code.kaptcha.Producer;
import com.tourism.common.additional.config.ApplicationConfig;
import com.tourism.common.additional.constant.CacheConstants;
import com.tourism.common.additional.constant.Constants;
import com.tourism.common.additional.sign.Base64;
import com.tourism.common.additional.uuid.IdUtils;
import com.tourism.common.core.annotation.DisableDataFilter;
import com.tourism.common.core.annotation.MyRequestBody;
import com.tourism.common.core.constant.ApplicationConstant;
import com.tourism.common.core.constant.ErrorCodeEnum;
import com.tourism.common.core.object.LoginUserInfo;
import com.tourism.common.core.object.ResponseResult;
import com.tourism.common.core.object.TokenData;
import com.tourism.common.core.upload.BaseUpDownloader;
import com.tourism.common.core.upload.UpDownloaderFactory;
import com.tourism.common.core.upload.UploadResponseInfo;
import com.tourism.common.core.upload.UploadStoreInfo;
import com.tourism.common.core.util.*;
import com.tourism.common.log.annotation.OperationLog;
import com.tourism.common.log.model.constant.SysOperationLogType;
import com.tourism.common.redis.cache.SessionCacheHelper;
import com.tourism.common.satoken.util.SaTokenUtil;
import com.tourism.webadmin.app.website.dto.CheckImageDto;
import com.tourism.webadmin.app.website.dto.TourUserRegisterDto;
import com.tourism.webadmin.app.website.service.LoginToWebsiteService;
import com.tourism.webadmin.app.website.vo.WebSiteCaptchaImageVo;
import com.tourism.webadmin.back.dto.TourBookInfoDto;
import com.tourism.webadmin.back.model.TourBookInfo;
import com.tourism.webadmin.back.model.TourCountryCode;
import com.tourism.webadmin.back.model.TourUser;
import com.tourism.webadmin.back.model.constant.TourUserStatus;
import com.tourism.webadmin.back.service.TourCountryCodeService;
import com.tourism.webadmin.back.service.TourUserService;
import com.tourism.webadmin.back.service.impl.TourBookInfoServiceImpl;
import com.tourism.webadmin.back.util.SMSUtils;
import com.tourism.webadmin.config.SmsSwitch;
import com.tourism.webadmin.upms.model.SysUser;
import io.jsonwebtoken.lang.Collections;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 登录接口控制器类。
 *
 * @author chenchen
 * @date 2024-09-18
 */
@Tag(name = "门户用户登录接口")
@DisableDataFilter
@Slf4j
@RestController
@RequestMapping("/website/web")
public class LoginToWebsiteController {
    @Autowired
    private TourUserService tourUserService;
    @Autowired
    private LoginToWebsiteService loginToWebsiteService;
    @Resource(name = "captchaProducerMath")
    private Producer captchaProducerMath;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private ApplicationConfig appConfig;
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private SessionCacheHelper cacheHelper;
    @Autowired
    private UpDownloaderFactory upDownloaderFactory;
    @Autowired
    private WxMaService wxMaService;
    @Autowired
    private TourCountryCodeService tourCountryCodeService;
    @Autowired
    private TourBookInfoServiceImpl tourBookInfoService;
    @Autowired
    private SmsSwitch smsSwitch;

    @Autowired
    private SMSUtils smsUtils;

    private static final String SHOW_NAME_FIELD = "showName";
    private static final String HEAD_IMAGE_URL_FIELD = "headImageUrl";


    /**
     * 列出符合过滤条件的国家地区代码表列表。
     *
     * @return 应答结果对象，包含查询结果集。
     */
    @SaIgnore
    @GetMapping("/list")
    public ResponseResult<Map<String, List<TourCountryCode>>> list() {
        List<TourCountryCode> tourCountryCodeList =
                tourCountryCodeService.getTourCountryCodeList(new TourCountryCode(), "");
        Map<String, List<TourCountryCode>> collect = tourCountryCodeList.stream().collect(Collectors.groupingBy(TourCountryCode::getFirstCode));
        return ResponseResult.success(collect);
    }


    /**
     * 门户网站留存信息
     * 返回值：1预订成功 2预定失败
     */
    @Transactional(rollbackFor = Exception.class)
    @PostMapping("/saveBookInfo")
    public ResponseResult<Integer> saveBookInfo(HttpServletRequest request,
                                             @MyRequestBody TourBookInfoDto tourBookInfoDto){

        TokenData tokenData = TokenData.takeFromRequest();
        if(tokenData == null){
            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED);
        }
        Long userId = tokenData.getUserId();
        TourUser tourUser = tourUserService.getById(userId);
        if(tourUser == null){
            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED);
        }

        //在redis中查看key为该ip的缓存对象是否大于6个;>=6个则不保存了;<6个则进行保存并缓存数据(缓存超时时间为1分钟)
        String remoteIpAddress = IpUtil.getRemoteIpAddress(request);
        String startKey = CacheConstants.TOUR_BOOK_IP.concat(remoteIpAddress+tourBookInfoDto.getProjectId()+tourBookInfoDto.getType());
        String convertStartKey = com.tourism.common.additional.utils.StringUtils.replacePattern(startKey, ":", "_");
        Iterable<String> keysByPattern = redissonClient.getKeys().getKeysByPattern(convertStartKey.concat("*"));

        // 将 Iterable 转换为 List
        List<String> keyList = new ArrayList<>();
        for (String key : keysByPattern) {
            keyList.add(key);
        }
        if(Collections.isEmpty(keyList)) {
            String key = convertStartKey;
            // 获取 RBucket 对象
            RBucket<String> bucket = redissonClient.getBucket(key);
            // 设置缓存对象
            bucket.set(tourUser.getMobile());
            // 设置过期时间
            bucket.expire(1, TimeUnit.MINUTES);
            //根据手机号+项目id判断是否已经预约过
            TourBookInfo tourBookInfo = new TourBookInfo();
            tourBookInfo.setBookMobile(tourUser.getMobile());
            tourBookInfo.setType(tourBookInfoDto.getType());
            tourBookInfo.setProjectId(tourBookInfoDto.getProjectId());
            boolean b = tourBookInfoService.existByFilter(tourBookInfo);
            if(b){
                return ResponseResult.success(2);
            }

            tourBookInfoDto.setBookMobile(tourUser.getMobile());
            boolean isSave =
                    loginToWebsiteService.saveBookInfo(request, tourBookInfoDto);
            if(!isSave){
                return ResponseResult.success(2);
            }
            return ResponseResult.success(1);
        }else {
            return ResponseResult.success(2);
        }
    }
    /**
     * 生成验证码(math)
     */
    @SaIgnore
    @GetMapping("/captchaImage")
    public ResponseResult<WebSiteCaptchaImageVo> getCode() throws IOException
    {
        // 保存验证码信息
        String uuid = IdUtils.simpleUUID();
        String verifyKey = CacheConstants.getCaptchaCodeKey(uuid);

        String capStr = null, code = null;
        BufferedImage image = null;

        // 生成数字验证码(乘除)
//        String capText = captchaProducerMath.createText();
//        capStr = capText.substring(0, capText.lastIndexOf("@"));
//        code = capText.substring(capText.lastIndexOf("@") + 1);
//        image = captchaProducerMath.createImage(capStr);

        int randomNumber = RandomUtils.nextInt(1000, 9999);
        image = captchaProducerMath.createImage(String.valueOf(randomNumber));

        // 获取 RBucket 对象
        RBucket<String> bucket = redissonClient.getBucket(verifyKey);

        // 设置缓存对象并设置过期时间
        bucket.set(String.valueOf(randomNumber), Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);

//        redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
        // 转换流信息写出
        FastByteArrayOutputStream os = new FastByteArrayOutputStream();
        try
        {
            ImageIO.write(image, "jpg", os);
        }
        catch (IOException e)
        {
            return ResponseResult.error(ErrorCodeEnum.PICTURE_CODE_ERR);
        }

        WebSiteCaptchaImageVo webSiteCaptchaImageVo = new WebSiteCaptchaImageVo();
        webSiteCaptchaImageVo.setUuid(uuid);
        webSiteCaptchaImageVo.setImg(Base64.encode(os.toByteArray()));
        return ResponseResult.success(webSiteCaptchaImageVo);
    }

    /**
     * 校验图形验证码，校验成功，发送手机短信到手机
     */
    @SaIgnore
    @GetMapping("/validateCaptchaAndSendSms")
    public ResponseResult<Void> validateCaptcha(@Valid CheckImageDto checkImageDto){
        // 校验图形验证码
        String verifyKey = CacheConstants.getCaptchaCodeKey(checkImageDto.getUuid());
        RBucket<String> bucket = redissonClient.getBucket(verifyKey);
        String code = bucket.get();
        if(StringUtils.isBlank(code)){
            return ResponseResult.error(ErrorCodeEnum.PICTURE_CODE_TIMEOUT_ERR);
        }
        if (!(code.equals(checkImageDto.getCode()))){
            return ResponseResult.error(ErrorCodeEnum.PICTURE_CODE_ERR);
        }

        // 检查一分钟内是否发送过验证码
        boolean exists = redissonClient.getBucket(CacheConstants.getSmsRepeatLimitKey(checkImageDto.getPhoneNumber())).isExists();
        if(exists){
            return ResponseResult.error(ErrorCodeEnum.SMS_CODE_SEND_REPEAT_LIMIT_ERR);
        }

        // 生成随机验证码数字
        String random = String.valueOf(new Random().nextInt(9000) + 1000);
//        String code = String.valueOf(rondom);
        // 校验成功后，发短信到手机
//        SmsResponse huawei = SmsFactory.getSmsBlend("huawei").sendMessage(checkImageDto.getPhoneNumber(), random);
        smsUtils.sendMsg(checkImageDto.getCountryCode(), checkImageDto.getPhoneNumber(), random);
        String smsCodeKey = CacheConstants.getSmsCodeKey(checkImageDto.getPhoneNumber());
        //5分钟有效时长
        redissonClient.getBucket(smsCodeKey).set(random, CacheConstants.getSmsEffectiveSeconds(), TimeUnit.SECONDS);
        //控制一分钟内不允许重复发送
        redissonClient.getBucket(CacheConstants.getSmsRepeatLimitKey(checkImageDto.getPhoneNumber())).set(true, 60, TimeUnit.SECONDS);
        return ResponseResult.success();
    }

    /**
     * 验证手机验证码，登录
     */
    @SaIgnore
    @OperationLog(type = SysOperationLogType.LOGIN, saveResponse = false)
    @PostMapping("/doLoginBySms")
    public ResponseResult<JSONObject> doLoginBySms(
            @MyRequestBody String loginMoblie,
            @MyRequestBody String smsCode,
            @MyRequestBody String countryCode) throws UnsupportedEncodingException
    {
        if(smsSwitch.isEnable()) {
            // 校验短信验证码
            String smsCodeKey = CacheConstants.getSmsCodeKey(loginMoblie);
            RBucket<String> bucket = redissonClient.getBucket(smsCodeKey);
            String redisCode = bucket.get();
            if(StringUtils.isBlank(redisCode) || !redisCode.equals(smsCode)){
                return ResponseResult.error(ErrorCodeEnum.SMS_CODE_ERR);
            }
            // 短信验证码校验成功，清除验证码
            bucket.delete();
        }
        // 查询用户信息
        TourUser tourUser = tourUserService.getOne(Wrappers.<TourUser>lambdaQuery().eq(TourUser::getMobile, loginMoblie));
        // 如果用户信息不存在，则新建
        if(tourUser == null){
            tourUser = new TourUser();
            tourUser.setLoginName(loginMoblie);
            tourUser.setUserStatus(TourUserStatus.STATUS_NORMAL);
            tourUser.setMobile(loginMoblie);
            tourUser.setCountryCode(StringUtils.isBlank(countryCode)? "86" : countryCode);
            tourUser.setPassword(passwordEncoder.encode("123456"));
            //创建用户，给出默认头像和默认昵称
            tourUser.setShowName("逍遥游用户_" + RandomUtil.randomString(6));
            tourUser = tourUserService.saveNew(tourUser);
        }
        // 生成token
        JSONObject jsonObject = this.buildTourLoginDataAndLogin(tourUser);
        return ResponseResult.success(jsonObject);
    }

    /**
     * 小程序根据phoneCode登录
     */
    @SaIgnore
    @OperationLog(type = SysOperationLogType.LOGIN, saveResponse = false)
    @GetMapping("/doLoginByWxMini")
    public ResponseResult<JSONObject> doLoginByWxMini(String phoneCode) throws WxErrorException {
        log.info("小程序获取手机号code:" + phoneCode);
        WxMaUserService userService = wxMaService.getUserService();
        WxMaPhoneNumberInfo phoneNoInfo = userService.getPhoneNoInfo(phoneCode);

        log.info("小程序获取手机号信息:" + phoneNoInfo);
        TourUser tourUser = tourUserService.getOne(Wrappers.<TourUser>lambdaQuery().eq(TourUser::getMobile, phoneNoInfo.getPhoneNumber()));
        // 如果用户信息不存在，则新建
        if(tourUser == null){
            tourUser = new TourUser();
            tourUser.setLoginName(phoneNoInfo.getPhoneNumber());
            tourUser.setUserStatus(TourUserStatus.STATUS_NORMAL);
            tourUser.setMobile(phoneNoInfo.getPhoneNumber());
            tourUser.setPassword(passwordEncoder.encode("123456"));
            tourUser.setCountryCode(phoneNoInfo.getCountryCode());
            tourUser = tourUserService.saveNew(tourUser);
        }
        // 生成token
        JSONObject jsonObject = this.buildTourLoginDataAndLogin(tourUser);
        jsonObject.put("userInfo", tourUser);
        return ResponseResult.success(jsonObject);
    }



    /**
     * 用户注册
     * @return
     * @throws
     */
    @SaIgnore
    @OperationLog(type = SysOperationLogType.WEBSITE_Register, saveResponse = false)
    @Transactional(rollbackFor = Exception.class)
    @PostMapping("/doRegister")
    public ResponseResult doRegister(@MyRequestBody TourUserRegisterDto tourUserRegisterDto) {

        String errorMessage = MyCommonUtil.getModelValidationError(tourUserRegisterDto, false);
        if (errorMessage != null) {
            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
        }
//        //验证图形验证码
//        if(!loginToWebsiteService.validateCaptcha(tourUserRegisterDto.getUuid(),tourUserRegisterDto.getCode())){
//            return ResponseResult.error(ErrorCodeEnum.PICTURE_CODE_ERR);
//        }
//        // 验证短信验证码
//        if (!loginToWebsiteService.validateSmsCode(tourUserRegisterDto.getMobile(),tourUserRegisterDto.getSmsCode())) {
//            return ResponseResult.error(ErrorCodeEnum.SMSCODE_ERR);
//        }
        //查看用户的手机号是否已经注册;注册的话,返回该手机号已存在的提示
        if (tourUserService.getTourUserByLoginMobile(tourUserRegisterDto.getMobile()) != null){
            return ResponseResult.error(ErrorCodeEnum.MOBILE_EXIST);
        }
        loginToWebsiteService.saveRegisterUserInfo(tourUserRegisterDto);
        return ResponseResult.success();

    }


    /**
     * 登录接口。
     *
     * @param loginName 登录名。
     * @param password  密码。
     * @return 应答结果对象，其中包括Token数据，以及菜单列表。
     */
    @SaIgnore
    @OperationLog(type = SysOperationLogType.LOGIN, saveResponse = false)
    @PostMapping("/doLoginByLonginName")
    public ResponseResult<JSONObject> doLoginByLonginName(
            @MyRequestBody String loginName,
            @MyRequestBody String password) throws UnsupportedEncodingException {
        if (MyCommonUtil.existBlankArgument(loginName, password)) {
            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
        }
        ResponseResult<TourUser> verifyResult = this.verifyAndHandleLoginUser(loginName, password);
        if (!verifyResult.isSuccess()) {
            return ResponseResult.errorFrom(verifyResult);
        }
        JSONObject jsonData = this.buildTourLoginDataAndLogin(verifyResult.getData());
        //返回token
        return ResponseResult.success(jsonData);
    }


    /**
     * 手机号登录接口。
     *
     * @param loginMoblie 手机号。
     * @param password  密码。
     * @return 应答结果对象，其中包括Token数据，以及菜单列表。
     */
    @SaIgnore
    @OperationLog(type = SysOperationLogType.LOGIN, saveResponse = false)
    @PostMapping("/doLoginByMobile")
    public ResponseResult<JSONObject> doLoginByMobile(
            @MyRequestBody String loginMoblie,
            @MyRequestBody String password) throws UnsupportedEncodingException {
        if (MyCommonUtil.existBlankArgument(loginMoblie, password)) {
            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
        }
        ResponseResult<TourUser> verifyResult = this.verifyAndHandleLoginUserByMoblie(loginMoblie, password);
        if (!verifyResult.isSuccess()) {
            return ResponseResult.errorFrom(verifyResult);
        }
        JSONObject jsonData = this.buildTourLoginDataAndLogin(verifyResult.getData());
        //返回token
        return ResponseResult.success(jsonData);
    }

    /**
     * 登出操作。同时将Session相关的信息从缓存中删除。
     *
     * @return 应答结果对象。
     */
    @OperationLog(type = SysOperationLogType.LOGOUT)
    @PostMapping("/doLogout")
    public ResponseResult<Void> doLogout() {
        String sessionId = TokenData.takeFromRequest().getSessionId();
        redissonClient.getBucket(TokenData.takeFromRequest().getMySessionId()).deleteAsync();
        redissonClient.getBucket(RedisKeyUtil.makeSessionPermCodeKey(sessionId)).deleteAsync();
        redissonClient.getBucket(RedisKeyUtil.makeSessionPermIdKey(sessionId)).deleteAsync();
//        sysDataPermService.removeDataPermCache(sessionId);
        cacheHelper.removeAllSessionCache(sessionId);
        StpUtil.logout();
        return ResponseResult.success();
    }

    /**
     * 在登录之后，通过token再次获取登录信息。
     * 用于在当前浏览器登录系统后，在新tab页中可以免密登录。
     *
     * @return 应答结果对象，其中包括JWT的Token数据，以及菜单列表。
     */
    @GetMapping("/getLoginInfo")
    public ResponseResult<JSONObject> getLoginInfo() {
        TokenData tokenData = TokenData.takeFromRequest();
        JSONObject jsonData = new JSONObject();
        jsonData.put(SHOW_NAME_FIELD, tokenData.getShowName());
        if (StrUtil.isNotBlank(tokenData.getHeadImageUrl())) {
            jsonData.put(HEAD_IMAGE_URL_FIELD, tokenData.getHeadImageUrl());
        }
        return ResponseResult.success(jsonData);
    }

    /**
     * 用户修改自己的密码。
     *
     * @param oldPass 原有密码。
     * @param newPass 新密码。
     * @return 应答结果对象。
     */
    @PostMapping("/changePassword")
    public ResponseResult<Void> changePassword(
            @MyRequestBody String oldPass, @MyRequestBody String newPass) throws UnsupportedEncodingException {
        if (MyCommonUtil.existBlankArgument(newPass, oldPass)) {
            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
        }
        TokenData tokenData = TokenData.takeFromRequest();
        TourUser tourUser = tourUserService.getById(tokenData.getUserId());
        oldPass = URLDecoder.decode(oldPass, StandardCharsets.UTF_8.name());
        // NOTE: 第一次使用时，请务必阅读ApplicationConstant.PRIVATE_KEY的代码注释。
        // 执行RsaUtil工具类中的main函数，可以生成新的公钥和私钥。
        oldPass = RsaUtil.decrypt(oldPass, ApplicationConstant.PRIVATE_KEY);
        if (tourUser == null || !passwordEncoder.matches(oldPass, tourUser.getPassword())) {
            return ResponseResult.error(ErrorCodeEnum.INVALID_USERNAME_PASSWORD);
        }
        newPass = URLDecoder.decode(newPass, StandardCharsets.UTF_8.name());
        newPass = RsaUtil.decrypt(newPass, ApplicationConstant.PRIVATE_KEY);
        if (!tourUserService.changePassword(tokenData.getUserId(), newPass)) {
            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
        }
        return ResponseResult.success();
    }

    /**
     * 上传并修改用户头像。
     *
     * @param uploadFile 上传的头像文件。
     */
    @PostMapping("/changeHeadImage")
    public void changeHeadImage(@RequestParam("uploadFile") MultipartFile uploadFile) throws IOException {
        UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(TourUser.class, HEAD_IMAGE_URL_FIELD);
        BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType());
        UploadResponseInfo responseInfo = upDownloader.doUpload(null,
                appConfig.getUploadFileBaseDir(), TourUser.class.getSimpleName(), HEAD_IMAGE_URL_FIELD, true, uploadFile);
        if (BooleanUtil.isTrue(responseInfo.getUploadFailed())) {
            ResponseResult.output(HttpServletResponse.SC_FORBIDDEN,
                    ResponseResult.error(ErrorCodeEnum.UPLOAD_FAILED, responseInfo.getErrorMessage()));
            return;
        }
        responseInfo.setDownloadUri("/admin/upms/login/downloadHeadImage");
        String newHeadImage = JSONArray.toJSONString(CollUtil.newArrayList(responseInfo));
        if (!tourUserService.changeHeadImage(TokenData.takeFromRequest().getUserId(), newHeadImage)) {
            ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST));
            return;
        }
        ResponseResult.output(ResponseResult.success(responseInfo));
    }

    /**
     * 下载用户头像。
     *
     * @param filename 文件名。如果没有提供该参数，就从当前记录的指定字段中读取。
     * @param response Http 应答对象。
     */
    @GetMapping("/downloadHeadImage")
    public void downloadHeadImage(String filename, HttpServletResponse response) {
        try {
            UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(SysUser.class, HEAD_IMAGE_URL_FIELD);
            BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType());
            upDownloader.doDownload(appConfig.getUploadFileBaseDir(),
                    TourUser.class.getSimpleName(), HEAD_IMAGE_URL_FIELD, filename, true, response);
        } catch (Exception e) {
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            log.error(e.getMessage(), e);
        }
    }
    private JSONObject buildTourLoginDataAndLogin(TourUser tourUser) {
        TokenData tokenData = this.loginAndCreateToken(tourUser);
        // 这里手动将TokenData存入request，便于OperationLogAspect统一处理操作日志。
        TokenData.addToRequest(tokenData);
        JSONObject jsonData = this.createResponseData(tourUser);
        return jsonData;
    }


    private JSONObject createResponseData(TourUser tourUser) {
        JSONObject jsonData = new JSONObject();
        jsonData.put(TokenData.REQUEST_ATTRIBUTE_NAME, StpUtil.getTokenValue());
        jsonData.put(SHOW_NAME_FIELD, tourUser.getShowName());
        if (StrUtil.isNotBlank(tourUser.getHeadImageUrl())) {
            jsonData.put(HEAD_IMAGE_URL_FIELD, tourUser.getHeadImageUrl());
        }
        return jsonData;
    }

    private TokenData loginAndCreateToken(TourUser tourUser) {
        String deviceType = MyCommonUtil.getDeviceTypeWithString();
        LoginUserInfo userInfo = BeanUtil.copyProperties(tourUser, LoginUserInfo.class);
        String loginId = SaTokenUtil.makeLoginId(userInfo);
        StpUtil.login(loginId, deviceType);
        SaSession session = StpUtil.getTokenSession();
        TokenData tokenData = this.buildTokenData(tourUser, session.getId(), StpUtil.getLoginDevice());
        String mySessionId = RedisKeyUtil.getSessionIdPrefix(tokenData, tourUser.getLoginName()) + MyCommonUtil.generateUuid();
        tokenData.setMySessionId(mySessionId);
        tokenData.setToken(session.getToken());
        redissonClient.getBucket(mySessionId)
                .set(JSON.toJSONString(tokenData), appConfig.getSessionExpiredSeconds(), TimeUnit.SECONDS);
        session.set(TokenData.REQUEST_ATTRIBUTE_NAME, tokenData);
        return tokenData;
    }

    private TokenData buildTokenData(TourUser tourUser, String sessionId, String deviceType) {
        TokenData tokenData = new TokenData();
        tokenData.setSessionId(sessionId);
        tokenData.setUserId(tourUser.getUserId());
        tokenData.setLoginName(tourUser.getLoginName());
        tokenData.setShowName(tourUser.getShowName());
        tokenData.setLoginIp(IpUtil.getRemoteIpAddress(ContextUtil.getHttpRequest()));
        tokenData.setLoginTime(new Date());
        tokenData.setDeviceType(deviceType);
        tokenData.setHeadImageUrl(tourUser.getHeadImageUrl());
        return tokenData;
    }

    private ResponseResult<TourUser> verifyAndHandleLoginUser(
            String loginName, String password) throws UnsupportedEncodingException {
        String errorMessage;
        TourUser tourUser = tourUserService.getTourUserByLoginName(loginName);
        password = URLDecoder.decode(password, StandardCharsets.UTF_8.name());
        // NOTE: 第一次使用时，请务必阅读ApplicationConstant.PRIVATE_KEY的代码注释。
        // 执行RsaUtil工具类中的main函数，可以生成新的公钥和私钥。
        password = RsaUtil.decrypt(password, ApplicationConstant.PRIVATE_KEY);
        if (tourUser == null || !passwordEncoder.matches(password, tourUser.getPassword())) {
            return ResponseResult.error(ErrorCodeEnum.INVALID_USERNAME_PASSWORD);
        }
        if (tourUser.getUserStatus() == TourUserStatus.STATUS_LOCKED) {
            errorMessage = "登录失败，用户账号被锁定！";
            return ResponseResult.error(ErrorCodeEnum.INVALID_USER_STATUS, errorMessage);
        }
        //本项目是否开启排他登录
        if (BooleanUtil.isTrue(appConfig.getExcludeLogin())) {
            String deviceType = MyCommonUtil.getDeviceTypeWithString();
            LoginUserInfo userInfo = BeanUtil.copyProperties(tourUser, LoginUserInfo.class);
            String loginId = SaTokenUtil.makeLoginId(userInfo);
            StpUtil.kickout(loginId, deviceType);
        }
        return ResponseResult.success(tourUser);
    }

    private ResponseResult<TourUser> verifyAndHandleLoginUserByMoblie(
            String loginMoblie, String password) throws UnsupportedEncodingException {
        String errorMessage;
        TourUser tourUser = tourUserService.getTourUserByLoginMobile(loginMoblie);
        password = URLDecoder.decode(password, StandardCharsets.UTF_8.name());
        // NOTE: 第一次使用时，请务必阅读ApplicationConstant.PRIVATE_KEY的代码注释。
        // 执行RsaUtil工具类中的main函数，可以生成新的公钥和私钥。
        password = RsaUtil.decrypt(password, ApplicationConstant.PRIVATE_KEY);
        if (tourUser == null || !passwordEncoder.matches(password, tourUser.getPassword())) {
            return ResponseResult.error(ErrorCodeEnum.INVALID_USERNAME_PASSWORD);
        }
        if (tourUser.getUserStatus() == TourUserStatus.STATUS_LOCKED) {
            errorMessage = "登录失败，用户账号被锁定！";
            return ResponseResult.error(ErrorCodeEnum.INVALID_USER_STATUS, errorMessage);
        }
        //本项目是否开启排他登录
        if (BooleanUtil.isTrue(appConfig.getExcludeLogin())) {
            String deviceType = MyCommonUtil.getDeviceTypeWithString();
            LoginUserInfo userInfo = BeanUtil.copyProperties(tourUser, LoginUserInfo.class);
            String loginId = SaTokenUtil.makeLoginId(userInfo);
            StpUtil.kickout(loginId, deviceType);
        }
        return ResponseResult.success(tourUser);
    }
}
