Browse Source

完善门户网站的注册,登录(手机,用户名),验证码,登出,再次获取登录信息,修改密码接口

chenchen 6 months ago
parent
commit
0404635c4b
40 changed files with 5554 additions and 1326 deletions
  1. 19 1
      application-webadmin/pom.xml
  2. 428 0
      application-webadmin/src/main/java/com/tourism/webadmin/app/website/controller/LoginToWebsiteController.java
  3. 368 0
      application-webadmin/src/main/java/com/tourism/webadmin/app/website/controller/TourUserController.java
  4. 33 0
      application-webadmin/src/main/java/com/tourism/webadmin/app/website/dao/TourUserMapper.java
  5. 97 0
      application-webadmin/src/main/java/com/tourism/webadmin/app/website/dao/mapper/TourUserMapper.xml
  6. 80 0
      application-webadmin/src/main/java/com/tourism/webadmin/app/website/dto/TourUserDto.java
  7. 95 0
      application-webadmin/src/main/java/com/tourism/webadmin/app/website/dto/TourUserRegisterDto.java
  8. 13 26
      application-webadmin/src/main/java/com/tourism/webadmin/app/website/model/TourUser.java
  9. 44 0
      application-webadmin/src/main/java/com/tourism/webadmin/app/website/model/constant/TourUserStatus.java
  10. 20 0
      application-webadmin/src/main/java/com/tourism/webadmin/app/website/service/LoginToWebsiteService.java
  11. 122 0
      application-webadmin/src/main/java/com/tourism/webadmin/app/website/service/TourUserService.java
  12. 59 0
      application-webadmin/src/main/java/com/tourism/webadmin/app/website/service/impl/LoginToWebsiteServiceImpl.java
  13. 246 0
      application-webadmin/src/main/java/com/tourism/webadmin/app/website/service/impl/TourUserServiceImpl.java
  14. 75 0
      application-webadmin/src/main/java/com/tourism/webadmin/app/website/vo/TourUserVo.java
  15. 0 620
      application-webadmin/src/main/java/com/tourism/webadmin/back/controller/LoginToWebController.java
  16. 0 189
      application-webadmin/src/main/java/com/tourism/webadmin/back/dao/SysUserWebMapper.java
  17. 0 288
      application-webadmin/src/main/java/com/tourism/webadmin/back/dao/mapper/SysUserWebMapper.xml
  18. 0 68
      application-webadmin/src/main/java/com/tourism/webadmin/back/dto/SysUserWebDto.java
  19. 0 39
      application-webadmin/src/main/java/com/tourism/webadmin/back/service/SysUserWebService.java
  20. 0 94
      application-webadmin/src/main/java/com/tourism/webadmin/back/service/impl/SysUserWebServiceImpl.java
  21. 65 0
      application-webadmin/src/main/java/com/tourism/webadmin/config/KaptchaConfig.java
  22. 56 0
      common/common-additional/pom.xml
  23. 87 0
      common/common-additional/src/main/java/com/tourism/common/additional/config/CaptchaConfig.java
  24. 69 0
      common/common-additional/src/main/java/com/tourism/common/additional/config/KaptchaTextCreator.java
  25. 44 0
      common/common-additional/src/main/java/com/tourism/common/additional/constant/CacheConstants.java
  26. 164 0
      common/common-additional/src/main/java/com/tourism/common/additional/constant/Constants.java
  27. 94 0
      common/common-additional/src/main/java/com/tourism/common/additional/constant/HttpStatus.java
  28. 26 0
      common/common-additional/src/main/java/com/tourism/common/additional/exception/UtilException.java
  29. 217 0
      common/common-additional/src/main/java/com/tourism/common/additional/model/AjaxResult.java
  30. 268 0
      common/common-additional/src/main/java/com/tourism/common/additional/redis/RedisCache.java
  31. 291 0
      common/common-additional/src/main/java/com/tourism/common/additional/sign/Base64.java
  32. 68 0
      common/common-additional/src/main/java/com/tourism/common/additional/sign/Md5Utils.java
  33. 86 0
      common/common-additional/src/main/java/com/tourism/common/additional/text/CharsetKit.java
  34. 1011 0
      common/common-additional/src/main/java/com/tourism/common/additional/text/Convert.java
  35. 92 0
      common/common-additional/src/main/java/com/tourism/common/additional/text/StrFormatter.java
  36. 680 0
      common/common-additional/src/main/java/com/tourism/common/additional/utils/StringUtils.java
  37. 49 0
      common/common-additional/src/main/java/com/tourism/common/additional/uuid/IdUtils.java
  38. 485 0
      common/common-additional/src/main/java/com/tourism/common/additional/uuid/UUID.java
  39. 2 1
      common/common-core/src/main/java/com/tourism/common/core/constant/ErrorCodeEnum.java
  40. 1 0
      common/pom.xml

+ 19 - 1
application-webadmin/pom.xml

@@ -80,7 +80,25 @@
             <artifactId>common-dict</artifactId>
             <version>1.0.0</version>
         </dependency>
-<!--        <dependency>-->
+        <dependency>
+            <groupId>com.tourism</groupId>
+            <artifactId>common-additional</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.google.code.kaptcha</groupId>
+            <artifactId>kaptcha</artifactId>
+            <version>2.3</version>
+        </dependency>
+
+        <dependency>
+            <groupId>javax.annotation</groupId>
+            <artifactId>javax.annotation-api</artifactId>
+            <version>1.3</version>
+        </dependency>
+
+        <!--        <dependency>-->
 <!--            <groupId>com.anji-plus</groupId>-->
 <!--            <artifactId>spring-boot-starter-captcha</artifactId>-->
 <!--            <version>1.3.0</version>-->

+ 428 - 0
application-webadmin/src/main/java/com/tourism/webadmin/app/website/controller/LoginToWebsiteController.java

@@ -0,0 +1,428 @@
+package com.tourism.webadmin.app.website.controller;
+
+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.StrUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.tourism.common.additional.constant.CacheConstants;
+import com.tourism.common.additional.constant.Constants;
+import com.tourism.common.additional.model.AjaxResult;
+import com.tourism.common.additional.redis.RedisCache;
+import com.tourism.common.additional.sign.Base64;
+import com.tourism.common.additional.uuid.IdUtils;
+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.TourUserRegisterDto;
+import com.tourism.webadmin.app.website.model.TourUser;
+import com.tourism.webadmin.app.website.model.constant.TourUserStatus;
+import com.tourism.webadmin.app.website.service.LoginToWebsiteService;
+import com.tourism.webadmin.app.website.service.TourUserService;
+import com.tourism.webadmin.config.ApplicationConfig;
+import com.tourism.webadmin.upms.model.SysUser;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import com.tourism.common.core.annotation.DisableDataFilter;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+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 com.google.code.kaptcha.Producer;
+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 com.tourism.common.core.annotation.MyRequestBody;
+import org.springframework.web.multipart.MultipartFile;
+
+/**
+ * 登录接口控制器类。
+ *
+ * @author chenchen
+ * @date 2024-09-18
+ */
+@Tag(name = "网页用户登录接口")
+@DisableDataFilter
+@Slf4j
+@RestController
+@RequestMapping("/website/web")
+public class LoginToWebsiteController {
+    @Autowired
+    private RedisCache redisCache;
+    @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;
+
+    private static final String SHOW_NAME_FIELD = "showName";
+    private static final String HEAD_IMAGE_URL_FIELD = "headImageUrl";
+
+    /**
+     * 生成验证码(math)
+     */
+    @SaIgnore
+    @GetMapping("/captchaImage")
+    public AjaxResult getCode(HttpServletResponse response) throws IOException
+    {
+        AjaxResult ajax = AjaxResult.success();
+
+        // 保存验证码信息
+        String uuid = IdUtils.simpleUUID();
+        String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + 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);
+
+        redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
+        // 转换流信息写出
+        org.springframework.util.FastByteArrayOutputStream os = new FastByteArrayOutputStream();
+        try
+        {
+            ImageIO.write(image, "jpg", os);
+        }
+        catch (IOException e)
+        {
+            return AjaxResult.error(e.getMessage());
+        }
+
+        ajax.put("uuid", uuid);
+        ajax.put("img", Base64.encode(os.toByteArray()));
+        return ajax;
+    }
+
+
+
+    /**
+     * 用户注册
+     * @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);
+//        }
+
+        //存储用户信息
+        TourUser tourUser = MyModelUtil.copyTo(tourUserRegisterDto, TourUser.class);
+        tourUser.setUserStatus(TourUserStatus.STATUS_NORMAL);
+        tourUser.setPassword(passwordEncoder.encode(tourUser.getPassword()));
+        tourUser = tourUserService.saveNew(tourUser);
+        return ResponseResult.success(tourUser.getUserId());
+
+    }
+
+
+    /**
+     * 登录接口。
+     *
+     * @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);
+    }
+}

+ 368 - 0
application-webadmin/src/main/java/com/tourism/webadmin/app/website/controller/TourUserController.java

@@ -0,0 +1,368 @@
+package com.tourism.webadmin.app.website.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import cn.hutool.core.util.ReflectUtil;
+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.log.annotation.OperationLog;
+import com.tourism.common.log.model.constant.SysOperationLogType;
+import com.github.pagehelper.page.PageMethod;
+import com.tourism.webadmin.app.website.vo.*;
+import com.tourism.webadmin.app.website.dto.*;
+import com.tourism.webadmin.app.website.model.*;
+import com.tourism.webadmin.app.website.service.*;
+import com.tourism.common.core.object.*;
+import com.tourism.common.core.util.*;
+import com.tourism.common.core.constant.*;
+import com.tourism.common.core.annotation.MyRequestBody;
+import com.tourism.common.redis.cache.SessionCacheHelper;
+import com.tourism.webadmin.config.ApplicationConfig;
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.*;
+
+/**
+ * 门户网站用户管理操作控制器类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Tag(name = "门户用户管理管理接口")
+@Slf4j
+@RestController
+@RequestMapping("/website/tour/user")
+public class TourUserController {
+
+    @Autowired
+    private ApplicationConfig appConfig;
+    @Autowired
+    private SessionCacheHelper cacheHelper;
+    @Autowired
+    private UpDownloaderFactory upDownloaderFactory;
+    @Autowired
+    private TourUserService tourUserService;
+
+    /**
+     * 新增门户用户管理数据。
+     *
+     * @param tourUserDto 新增对象。
+     * @return 应答结果对象,包含新增对象主键Id。
+     */
+    @ApiOperationSupport(ignoreParameters = {"tourUserDto.userId"})
+    @SaCheckPermission("tourUser.add")
+    @OperationLog(type = SysOperationLogType.ADD)
+    @PostMapping("/add")
+    public ResponseResult<Long> add(@MyRequestBody TourUserDto tourUserDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(tourUserDto, false);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        TourUser tourUser = MyModelUtil.copyTo(tourUserDto, TourUser.class);
+        tourUser = tourUserService.saveNew(tourUser);
+        return ResponseResult.success(tourUser.getUserId());
+    }
+
+    /**
+     * 更新门户用户管理数据。
+     *
+     * @param tourUserDto 更新对象。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("tourUser.update")
+    @OperationLog(type = SysOperationLogType.UPDATE)
+    @PostMapping("/update")
+    public ResponseResult<Void> update(@MyRequestBody TourUserDto tourUserDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(tourUserDto, true);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        TourUser tourUser = MyModelUtil.copyTo(tourUserDto, TourUser.class);
+        TourUser originalTourUser = tourUserService.getById(tourUser.getUserId());
+        if (originalTourUser == null) {
+            // NOTE: 修改下面方括号中的话述
+            errorMessage = "数据验证失败,当前 [数据] 并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        if (!tourUserService.update(tourUser, originalTourUser)) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 删除门户用户管理数据。
+     *
+     * @param userId 删除对象主键Id。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("tourUser.delete")
+    @OperationLog(type = SysOperationLogType.DELETE)
+    @PostMapping("/delete")
+    public ResponseResult<Void> delete(@MyRequestBody Long userId) {
+        if (MyCommonUtil.existBlankArgument(userId)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        return this.doDelete(userId);
+    }
+
+    /**
+     * 批量删除门户用户管理数据。
+     *
+     * @param userIdList 待删除对象的主键Id列表。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("tourUser.delete")
+    @OperationLog(type = SysOperationLogType.DELETE_BATCH)
+    @PostMapping("/deleteBatch")
+    public ResponseResult<Void> deleteBatch(@MyRequestBody List<Long> userIdList) {
+        if (MyCommonUtil.existBlankArgument(userIdList)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        for (Long userId : userIdList) {
+            ResponseResult<Void> responseResult = this.doDelete(userId);
+            if (!responseResult.isSuccess()) {
+                return responseResult;
+            }
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 列出符合过滤条件的门户用户管理列表。
+     *
+     * @param tourUserDtoFilter 过滤对象。
+     * @param orderParam 排序参数。
+     * @param pageParam 分页参数。
+     * @return 应答结果对象,包含查询结果集。
+     */
+    @SaCheckPermission("tourUser.view")
+    @PostMapping("/list")
+    public ResponseResult<MyPageData<TourUserVo>> list(
+            @MyRequestBody TourUserDto tourUserDtoFilter,
+            @MyRequestBody MyOrderParam orderParam,
+            @MyRequestBody MyPageParam pageParam) {
+        if (pageParam != null) {
+            PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount());
+        }
+        TourUser tourUserFilter = MyModelUtil.copyTo(tourUserDtoFilter, TourUser.class);
+        String orderBy = MyOrderParam.buildOrderBy(orderParam, TourUser.class);
+        List<TourUser> tourUserList = tourUserService.getTourUserListWithRelation(tourUserFilter, orderBy);
+        return ResponseResult.success(MyPageUtil.makeResponseData(tourUserList, TourUserVo.class));
+    }
+
+    /**
+     * 导入主表数据列表。
+     *
+     * @param importFile 上传的文件,目前仅仅支持xlsx和xls两种格式。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("tourUser.import")
+    @OperationLog(type = SysOperationLogType.IMPORT)
+    @PostMapping("/import")
+    public ResponseResult<Void> importBatch(
+            @RequestParam Boolean skipHeader,
+            @RequestParam("importFile") MultipartFile importFile) throws IOException {
+        String filename = ImportUtil.saveImportFile(appConfig.getUploadFileBaseDir(), null, importFile);
+        // 这里可以指定需要忽略导入的字段集合。如创建时间、创建人、更新时间、更新人、主键Id和逻辑删除,
+        // 以及一些存在缺省值且无需导入的字段。其中主键字段和逻辑删除字段不需要在这里设置,批量插入逻辑会自动处理的。
+        Set<String> ignoreFieldSet = new HashSet<>();
+        ignoreFieldSet.add("createUserId");
+        ignoreFieldSet.add("createTime");
+        ignoreFieldSet.add("updateUserId");
+        ignoreFieldSet.add("updateTime");
+        List<ImportUtil.ImportHeaderInfo> headerInfoList = ImportUtil.makeHeaderInfoList(TourUser.class, ignoreFieldSet);
+        // 下面是导入时需要注意的地方,如果我们缺省生成的代码,与实际情况存在差异,请手动修改。
+        // 1. 头信息数据字段,我们只是根据当前的主表实体对象生成了缺省数组,开发者可根据实际情况,对headerInfoList进行修改。
+        ImportUtil.ImportHeaderInfo[] headerInfos = headerInfoList.toArray(new ImportUtil.ImportHeaderInfo[]{});
+        // 2. 这里需要根据实际情况决定,导入文件中第一行是否为中文头信息,如果是可以跳过。这里我们默认为true。
+        // 这里根据自己的实际需求,为doImport的最后一个参数,传递需要进行字典转换的字段集合。
+        // 注意,集合中包含需要翻译的Java字段名,如: gradeId。
+        Set<String> translatedDictFieldSet = new HashSet<>();
+        List<TourUser> dataList =
+                ImportUtil.doImport(headerInfos, skipHeader, filename, TourUser.class, translatedDictFieldSet);
+        CallResult result = tourUserService.verifyImportList(dataList, translatedDictFieldSet);
+        if (!result.isSuccess()) {
+            // result中返回了具体的验证失败对象,如果需要返回更加详细的错误,可根据实际情况手动修改。
+            return ResponseResult.errorFrom(result);
+        }
+        tourUserService.saveNewBatch(dataList, -1);
+        return ResponseResult.success();
+    }
+
+    /**
+     * 导出符合过滤条件的门户用户管理列表。
+     *
+     * @param tourUserDtoFilter 过滤对象。
+     * @param orderParam 排序参数。
+     * @throws IOException 文件读写失败。
+     */
+    @SaCheckPermission("tourUser.export")
+    @OperationLog(type = SysOperationLogType.EXPORT, saveResponse = false)
+    @PostMapping("/export")
+    public void export(
+            @MyRequestBody TourUserDto tourUserDtoFilter,
+            @MyRequestBody MyOrderParam orderParam) throws IOException {
+        TourUser tourUserFilter = MyModelUtil.copyTo(tourUserDtoFilter, TourUser.class);
+        String orderBy = MyOrderParam.buildOrderBy(orderParam, TourUser.class);
+        List<TourUser> resultList =
+                tourUserService.getTourUserListWithRelation(tourUserFilter, orderBy);
+        // 导出文件的标题数组
+        // NOTE: 下面的代码中仅仅导出了主表数据,主表聚合计算数据和主表关联字典的数据。
+        // 一对一从表数据的导出,可根据需要自行添加。如:headerMap.put("slaveFieldName.xxxField", "标题名称")
+        Map<String, String> headerMap = new LinkedHashMap<>(13);
+        headerMap.put("userId", "主键Id");
+        headerMap.put("loginName", "用户登录名称");
+        headerMap.put("password", "密码");
+        headerMap.put("showName", "用户显示名称");
+        headerMap.put("headImageUrl", "用户头像的Url");
+        headerMap.put("userStatusDictMap.name", "状态(0: 正常 1: 锁定)");
+        headerMap.put("email", "用户邮箱");
+        headerMap.put("mobile", "用户手机");
+        headerMap.put("createUserId", "创建者Id");
+        headerMap.put("createTime", "创建时间");
+        headerMap.put("updateUserId", "更新者Id");
+        headerMap.put("updateTime", "最后更新时间");
+        headerMap.put("deletedFlag", "删除标记(1: 正常 -1: 已删除)");
+        ExportUtil.doExport(resultList, headerMap, "tourUser.xlsx");
+    }
+
+    /**
+     * 查看指定门户用户管理对象详情。
+     *
+     * @param userId 指定对象主键Id。
+     * @return 应答结果对象,包含对象详情。
+     */
+    @SaCheckPermission("tourUser.view")
+    @GetMapping("/view")
+    public ResponseResult<TourUserVo> view(@RequestParam Long userId) {
+        TourUser tourUser = tourUserService.getByIdWithRelation(userId, MyRelationParam.full());
+        if (tourUser == null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        TourUserVo tourUserVo = MyModelUtil.copyTo(tourUser, TourUserVo.class);
+        return ResponseResult.success(tourUserVo);
+    }
+
+    /**
+     * 附件文件下载。
+     * 这里将图片和其他类型的附件文件放到不同的父目录下,主要为了便于今后图片文件的迁移。
+     *
+     * @param userId 附件所在记录的主键Id。
+     * @param fieldName 附件所属的字段名。
+     * @param filename  文件名。如果没有提供该参数,就从当前记录的指定字段中读取。
+     * @param asImage   下载文件是否为图片。
+     * @param response  Http 应答对象。
+     */
+    @SaCheckPermission("tourUser.view")
+    @OperationLog(type = SysOperationLogType.DOWNLOAD, saveResponse = false)
+    @GetMapping("/download")
+    public void download(
+            @RequestParam(required = false) Long userId,
+            @RequestParam String fieldName,
+            @RequestParam String filename,
+            @RequestParam Boolean asImage,
+            HttpServletResponse response) {
+        if (MyCommonUtil.existBlankArgument(fieldName, filename, asImage)) {
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            return;
+        }
+        // 使用try来捕获异常,是为了保证一旦出现异常可以返回500的错误状态,便于调试。
+        // 否则有可能给前端返回的是200的错误码。
+        try {
+            // 如果请求参数中没有包含主键Id,就判断该文件是否为当前session上传的。
+            if (userId == null) {
+                if (!cacheHelper.existSessionUploadFile(filename)) {
+                    ResponseResult.output(HttpServletResponse.SC_FORBIDDEN);
+                    return;
+                }
+            } else {
+                TourUser tourUser = tourUserService.getById(userId);
+                if (tourUser == null) {
+                    ResponseResult.output(HttpServletResponse.SC_NOT_FOUND);
+                    return;
+                }
+                String fieldJsonData = (String) ReflectUtil.getFieldValue(tourUser, fieldName);
+                if (fieldJsonData == null && !cacheHelper.existSessionUploadFile(filename)) {
+                    ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST);
+                    return;
+                }
+                if (!BaseUpDownloader.containFile(fieldJsonData, filename)
+                        && !cacheHelper.existSessionUploadFile(filename)) {
+                    ResponseResult.output(HttpServletResponse.SC_FORBIDDEN);
+                    return;
+                }
+            }
+            UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(TourUser.class, fieldName);
+            if (!storeInfo.isSupportUpload()) {
+                ResponseResult.output(HttpServletResponse.SC_NOT_IMPLEMENTED,
+                        ResponseResult.error(ErrorCodeEnum.INVALID_UPLOAD_FIELD));
+                return;
+            }
+            BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType());
+            upDownloader.doDownload(appConfig.getUploadFileBaseDir(),
+                    TourUser.class.getSimpleName(), fieldName, filename, asImage, response);
+        } catch (Exception e) {
+            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+            log.error(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 文件上传操作。
+     *
+     * @param fieldName  上传文件名。
+     * @param asImage    是否作为图片上传。如果是图片,今后下载的时候无需权限验证。否则就是附件上传,下载时需要权限验证。
+     * @param uploadFile 上传文件对象。
+     */
+    @SaCheckPermission("tourUser.view")
+    @OperationLog(type = SysOperationLogType.UPLOAD, saveResponse = false)
+    @PostMapping("/upload")
+    public void upload(
+            @RequestParam String fieldName,
+            @RequestParam Boolean asImage,
+            @RequestParam("uploadFile") MultipartFile uploadFile) throws IOException {
+        UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(TourUser.class, fieldName);
+        // 这里就会判断参数中指定的字段,是否支持上传操作。
+        if (!storeInfo.isSupportUpload()) {
+            ResponseResult.output(HttpServletResponse.SC_FORBIDDEN,
+                    ResponseResult.error(ErrorCodeEnum.INVALID_UPLOAD_FIELD));
+            return;
+        }
+        // 根据字段注解中的存储类型,通过工厂方法获取匹配的上传下载实现类,从而解耦。
+        BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType());
+        UploadResponseInfo responseInfo = upDownloader.doUpload(null,
+                appConfig.getUploadFileBaseDir(), TourUser.class.getSimpleName(), fieldName, asImage, uploadFile);
+        if (Boolean.TRUE.equals(responseInfo.getUploadFailed())) {
+            ResponseResult.output(HttpServletResponse.SC_FORBIDDEN,
+                    ResponseResult.error(ErrorCodeEnum.UPLOAD_FAILED, responseInfo.getErrorMessage()));
+            return;
+        }
+        cacheHelper.putSessionUploadFile(responseInfo.getFilename());
+        ResponseResult.output(ResponseResult.success(responseInfo));
+    }
+
+    private ResponseResult<Void> doDelete(Long userId) {
+        String errorMessage;
+        // 验证关联Id的数据合法性
+        TourUser originalTourUser = tourUserService.getById(userId);
+        if (originalTourUser == null) {
+            // NOTE: 修改下面方括号中的话述
+            errorMessage = "数据验证失败,当前 [对象] 并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        if (!tourUserService.remove(userId)) {
+            errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        return ResponseResult.success();
+    }
+}

+ 33 - 0
application-webadmin/src/main/java/com/tourism/webadmin/app/website/dao/TourUserMapper.java

@@ -0,0 +1,33 @@
+package com.tourism.webadmin.app.website.dao;
+
+import com.tourism.common.core.base.dao.BaseDaoMapper;
+import com.tourism.webadmin.app.website.model.TourUser;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.*;
+
+/**
+ * 门户网站用户管理数据操作访问接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+public interface TourUserMapper extends BaseDaoMapper<TourUser> {
+
+    /**
+     * 批量插入对象列表。
+     *
+     * @param tourUserList 新增对象列表。
+     */
+    void insertList(List<TourUser> tourUserList);
+
+    /**
+     * 获取过滤后的对象列表。
+     *
+     * @param tourUserFilter 主表过滤对象。
+     * @param orderBy 排序字符串,order by从句的参数。
+     * @return 对象列表。
+     */
+    List<TourUser> getTourUserList(
+            @Param("tourUserFilter") TourUser tourUserFilter, @Param("orderBy") String orderBy);
+}

+ 97 - 0
application-webadmin/src/main/java/com/tourism/webadmin/app/website/dao/mapper/TourUserMapper.xml

@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.tourism.webadmin.app.website.dao.TourUserMapper">
+    <resultMap id="BaseResultMap" type="com.tourism.webadmin.app.website.model.TourUser">
+        <id column="user_id" jdbcType="BIGINT" property="userId"/>
+        <result column="login_name" jdbcType="VARCHAR" property="loginName"/>
+        <result column="password" jdbcType="VARCHAR" property="password"/>
+        <result column="show_name" jdbcType="VARCHAR" property="showName"/>
+        <result column="head_image_url" jdbcType="VARCHAR" property="headImageUrl"/>
+        <result column="user_status" jdbcType="INTEGER" property="userStatus"/>
+        <result column="email" jdbcType="VARCHAR" property="email"/>
+        <result column="mobile" jdbcType="VARCHAR" property="mobile"/>
+        <result column="create_user_id" jdbcType="BIGINT" property="createUserId"/>
+        <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
+        <result column="update_user_id" jdbcType="BIGINT" property="updateUserId"/>
+        <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
+        <result column="deleted_flag" jdbcType="INTEGER" property="deletedFlag"/>
+    </resultMap>
+
+    <insert id="insertList">
+        INSERT INTO tour_user
+            (user_id,
+            login_name,
+            password,
+            show_name,
+            head_image_url,
+            user_status,
+            email,
+            mobile,
+            create_user_id,
+            create_time,
+            update_user_id,
+            update_time,
+            deleted_flag)
+        VALUES
+        <foreach collection="list" index="index" item="item" separator="," >
+            (#{item.userId},
+            #{item.loginName},
+            #{item.password},
+            #{item.showName},
+            #{item.headImageUrl},
+            #{item.userStatus},
+            #{item.email},
+            #{item.mobile},
+            #{item.createUserId},
+            #{item.createTime},
+            #{item.updateUserId},
+            #{item.updateTime},
+            #{item.deletedFlag})
+        </foreach>
+    </insert>
+
+    <!-- 如果有逻辑删除字段过滤,请写到这里 -->
+    <sql id="filterRef">
+        <!-- 这里必须加上全包名,否则当filterRef被其他Mapper.xml包含引用的时候,就会调用Mapper.xml中的该SQL片段 -->
+        <include refid="com.tourism.webadmin.app.website.dao.TourUserMapper.inputFilterRef"/>
+        AND tour_user.deleted_flag = ${@com.tourism.common.core.constant.GlobalDeletedFlag@NORMAL}
+    </sql>
+
+    <!-- 这里仅包含调用接口输入的主表过滤条件 -->
+    <sql id="inputFilterRef">
+        <if test="tourUserFilter != null">
+            <if test="tourUserFilter.loginName != null and tourUserFilter.loginName != ''">
+                <bind name = "safeTourUserLoginName" value = "'%' + tourUserFilter.loginName + '%'" />
+                AND tour_user.login_name LIKE #{safeTourUserLoginName}
+            </if>
+            <if test="tourUserFilter.showName != null and tourUserFilter.showName != ''">
+                <bind name = "safeTourUserShowName" value = "'%' + tourUserFilter.showName + '%'" />
+                AND tour_user.show_name LIKE #{safeTourUserShowName}
+            </if>
+            <if test="tourUserFilter.userStatus != null">
+                AND tour_user.user_status = #{tourUserFilter.userStatus}
+            </if>
+            <if test="tourUserFilter.email != null and tourUserFilter.email != ''">
+                <bind name = "safeTourUserEmail" value = "'%' + tourUserFilter.email + '%'" />
+                AND tour_user.email LIKE #{safeTourUserEmail}
+            </if>
+            <if test="tourUserFilter.mobile != null and tourUserFilter.mobile != ''">
+                <bind name = "safeTourUserMobile" value = "'%' + tourUserFilter.mobile + '%'" />
+                AND tour_user.mobile LIKE #{safeTourUserMobile}
+            </if>
+            <if test="tourUserFilter.deletedFlag != null">
+                AND tour_user.deleted_flag = #{tourUserFilter.deletedFlag}
+            </if>
+        </if>
+    </sql>
+
+    <select id="getTourUserList" resultMap="BaseResultMap" parameterType="com.tourism.webadmin.app.website.model.TourUser">
+        SELECT * FROM tour_user
+        <where>
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+</mapper>

+ 80 - 0
application-webadmin/src/main/java/com/tourism/webadmin/app/website/dto/TourUserDto.java

@@ -0,0 +1,80 @@
+package com.tourism.webadmin.app.website.dto;
+
+import com.tourism.common.core.validator.UpdateGroup;
+import com.tourism.common.core.validator.ConstDictRef;
+import com.tourism.webadmin.upms.model.constant.SysUserStatus;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import jakarta.validation.constraints.*;
+
+/**
+ * 门户网站用户管理Dto对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Schema(description = "TourUserDto对象")
+@Data
+public class TourUserDto {
+
+    /**
+     * 主键Id。
+     */
+    @Schema(description = "主键Id。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class})
+    private String userId;
+
+    /**
+     * 用户登录名称。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "用户登录名称。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "数据验证失败,用户登录名称不能为空!")
+    private String loginName;
+
+    /**
+     * 密码。
+     */
+    @Schema(description = "密码。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "数据验证失败,密码不能为空!")
+    private String password;
+
+    /**
+     * 用户显示名称。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "用户显示名称。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "数据验证失败,用户显示名称不能为空!")
+    private String showName;
+
+    /**
+     * 用户头像的Url。
+     */
+    @Schema(description = "用户头像的Url。")
+    private String headImageUrl;
+
+    /**
+     * 状态(0: 正常 1: 锁定)。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "状态(0: 正常 1: 锁定)。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,状态(0: 正常 1: 锁定)不能为空!")
+    @ConstDictRef(constDictClass = SysUserStatus.class, message = "数据验证失败,状态(0: 正常 1: 锁定)为无效值!")
+    private Integer userStatus;
+
+    /**
+     * 用户邮箱。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "用户邮箱。可支持等于操作符的列表数据过滤。")
+    private String email;
+
+    /**
+     * 用户手机。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "用户手机。可支持等于操作符的列表数据过滤。")
+    private String mobile;
+}

+ 95 - 0
application-webadmin/src/main/java/com/tourism/webadmin/app/website/dto/TourUserRegisterDto.java

@@ -0,0 +1,95 @@
+package com.tourism.webadmin.app.website.dto;
+
+import com.tourism.common.core.validator.ConstDictRef;
+import com.tourism.common.core.validator.UpdateGroup;
+import com.tourism.webadmin.upms.model.constant.SysUserStatus;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * 门户网站用户管理Dto对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Schema(description = "TourUserDto对象")
+@Data
+public class TourUserRegisterDto {
+
+    /**
+     * 主键Id。
+     */
+    @Schema(description = "主键Id。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class})
+    private Long userId;
+
+    /**
+     * 用户登录名称。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "用户登录名称。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    private String loginName;
+
+    /**
+     * 密码。
+     */
+    @Schema(description = "密码。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "数据验证失败,密码不能为空!")
+    private String password;
+
+    /**
+     * 用户显示名称。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "用户显示名称。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    private String showName;
+
+    /**
+     * 用户头像的Url。
+     */
+    @Schema(description = "用户头像的Url。")
+    private String headImageUrl;
+
+    /**
+     * 状态(0: 正常 1: 锁定)。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "状态(0: 正常 1: 锁定)。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ConstDictRef(constDictClass = SysUserStatus.class, message = "数据验证失败,状态(0: 正常 1: 锁定)为无效值!")
+    private Integer userStatus;
+
+    /**
+     * 用户邮箱。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "用户邮箱。可支持等于操作符的列表数据过滤。")
+    private String email;
+
+    /**
+     * 用户手机。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "用户手机。可支持等于操作符的列表数据过滤。")
+    @NotBlank(message = "数据验证失败,手机号不能为空!")
+    private String mobile;
+
+    /**
+     * 图形验证码的uuid。
+     */
+//    @NotBlank(message = "数据验证失败,uuid不能为空!")
+    private String uuid;
+
+    /**
+     * 图形验证码的结果。
+     */
+//    @NotBlank(message = "数据验证失败,结果不能为空!")
+    private String code;
+
+    /**
+     * 短信验证码。
+     */
+//    @NotBlank(message = "数据验证失败,短信验证码不能为空!")
+    private String smsCode;
+}

+ 13 - 26
application-webadmin/src/main/java/com/tourism/webadmin/back/model/SysUserWeb.java → application-webadmin/src/main/java/com/tourism/webadmin/app/website/model/TourUser.java

@@ -1,7 +1,6 @@
-package com.tourism.webadmin.back.model;
+package com.tourism.webadmin.app.website.model;
 
 import com.baomidou.mybatisplus.annotation.*;
-import com.tourism.webadmin.upms.model.constant.SysUserType;
 import com.tourism.webadmin.upms.model.constant.SysUserStatus;
 import com.tourism.common.core.upload.UploadStoreTypeEnum;
 import com.tourism.common.core.annotation.*;
@@ -10,33 +9,32 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 
 import java.util.Map;
-import java.util.List;
 
 /**
- * 用户管理实体对象。
+ * 门户网站用户管理实体对象。
  *
  * @author 吃饭睡觉
  * @date 2024-09-06
  */
 @Data
 @EqualsAndHashCode(callSuper = true)
-@TableName(value = "sys_user_web")
-public class SysUserWeb extends BaseModel {
+@TableName(value = "tour_user")
+public class TourUser extends BaseModel {
 
     /**
-     * 用户Id。
+     * 主键Id。
      */
     @TableId(value = "user_id")
     private Long userId;
 
     /**
-     * 登录用户名。
+     * 用户登录名
      */
     @TableField(value = "login_name")
     private String loginName;
 
     /**
-     * 用户密码。
+     * 密码。
      */
     @TableField(value = "password")
     private String password;
@@ -50,12 +48,12 @@ public class SysUserWeb extends BaseModel {
     /**
      * 用户头像的Url。
      */
-    @UploadFlagColumn(storeType = UploadStoreTypeEnum.HUAWEI_OBS_SYSTEM)
+    @UploadFlagColumn(storeType = UploadStoreTypeEnum.LOCAL_SYSTEM)
     @TableField(value = "head_image_url")
     private String headImageUrl;
 
     /**
-     * 用户状态(0: 正常 1: 锁定)。
+     * 状态(0: 正常 1: 锁定)。
      */
     @TableField(value = "user_status")
     private Integer userStatus;
@@ -79,20 +77,9 @@ public class SysUserWeb extends BaseModel {
     @TableField(value = "deleted_flag")
     private Integer deletedFlag;
 
-    /**
-     * createTime 范围过滤起始值(>=)。
-     */
+    @RelationConstDict(
+            masterIdField = "userStatus",
+            constantDictClass = SysUserStatus.class)
     @TableField(exist = false)
-    private String createTimeStart;
-
-    /**
-     * createTime 范围过滤结束值(<=)。
-     */
-    @TableField(exist = false)
-    private String createTimeEnd;
-
-    /**
-     * 国家
-     */
-    private String country;
+    private Map<String, Object> userStatusDictMap;
 }

+ 44 - 0
application-webadmin/src/main/java/com/tourism/webadmin/app/website/model/constant/TourUserStatus.java

@@ -0,0 +1,44 @@
+package com.tourism.webadmin.app.website.model.constant;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 用户网站状态常量字典对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+public final class TourUserStatus {
+
+    /**
+     * 正常状态。
+     */
+    public static final int STATUS_NORMAL = 0;
+    /**
+     * 锁定状态。
+     */
+    public static final int STATUS_LOCKED = 1;
+
+    private static final Map<Object, String> DICT_MAP = new HashMap<>(2);
+    static {
+        DICT_MAP.put(STATUS_NORMAL, "正常状态");
+        DICT_MAP.put(STATUS_LOCKED, "锁定状态");
+    }
+
+    /**
+     * 判断参数是否为当前常量字典的合法值。
+     *
+     * @param value 待验证的参数值。
+     * @return 合法返回true,否则false。
+     */
+    public static boolean isValid(Integer value) {
+        return value != null && DICT_MAP.containsKey(value);
+    }
+
+    /**
+     * 私有构造函数,明确标识该常量类的作用。
+     */
+    private TourUserStatus() {
+    }
+}

+ 20 - 0
application-webadmin/src/main/java/com/tourism/webadmin/app/website/service/LoginToWebsiteService.java

@@ -0,0 +1,20 @@
+package com.tourism.webadmin.app.website.service;
+
+/**
+ * 注册校验方法
+ * 
+ * @author tourism
+ */
+public interface LoginToWebsiteService
+{
+    /**
+     * 校验验证码
+     *
+     * @param code 验证码
+     * @param uuid 唯一标识
+     * @return 结果
+     */
+    public boolean validateCaptcha(String uuid, String code);
+
+    public boolean validateSmsCode(String mobile,String smsCode);
+}

+ 122 - 0
application-webadmin/src/main/java/com/tourism/webadmin/app/website/service/TourUserService.java

@@ -0,0 +1,122 @@
+package com.tourism.webadmin.app.website.service;
+
+import com.tourism.webadmin.app.website.model.*;
+import com.tourism.common.core.object.CallResult;
+import com.tourism.common.core.base.service.IBaseService;
+import com.tourism.webadmin.upms.model.SysUser;
+
+import java.util.*;
+
+/**
+ * 门户网站用户管理数据操作服务接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+public interface TourUserService extends IBaseService<TourUser, Long> {
+
+    /**
+     * 保存新增对象。
+     *
+     * @param tourUser 新增对象。
+     * @return 返回新增对象。
+     */
+    TourUser saveNew(TourUser tourUser);
+
+    /**
+     * 利用数据库的insertList语法,批量插入对象列表。
+     *
+     * @param tourUserList 新增对象列表。
+     */
+    void saveNewBatch(List<TourUser> tourUserList);
+
+    /**
+     * 利用数据库的insertList语法,批量插入对象列表。通常适用于更大的插入数据量,如批量导入。
+     *
+     * @param tourUserList 新增对象列表。
+     * @param batchSize  每批插入的数量。如果该值小于等于0,则使用缺省值10000。
+     */
+    void saveNewBatch(List<TourUser> tourUserList, int batchSize);
+
+    /**
+     * 更新数据对象。
+     *
+     * @param tourUser         更新的对象。
+     * @param originalTourUser 原有数据对象。
+     * @return 成功返回true,否则false。
+     */
+    boolean update(TourUser tourUser, TourUser originalTourUser);
+
+    /**
+     * 删除指定数据。
+     *
+     * @param userId 主键Id。
+     * @return 成功返回true,否则false。
+     */
+    boolean remove(Long userId);
+
+    /**
+     * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。
+     * 如果需要同时获取关联数据,请移步(getTourUserListWithRelation)方法。
+     *
+     * @param filter  过滤对象。
+     * @param orderBy 排序参数。
+     * @return 查询结果集。
+     */
+    List<TourUser> getTourUserList(TourUser filter, String orderBy);
+
+    /**
+     * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。
+     * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。
+     * 如果仅仅需要获取主表数据,请移步(getTourUserList),以便获取更好的查询性能。
+     *
+     * @param filter 主表过滤对象。
+     * @param orderBy 排序参数。
+     * @return 查询结果集。
+     */
+    List<TourUser> getTourUserListWithRelation(TourUser filter, String orderBy);
+
+    /**
+     * 对批量导入数据列表进行数据合法性验证。
+     * 验证逻辑主要覆盖主表的常量字典字段、字典表字典字段、数据源字段和一对一关联数据是否存在。
+     *
+     * @param dataList 主表的数据列表。
+     * @param ignoreFieldSet 需要忽略校验的字典字段集合。通常对于字典反向翻译过来的字段适用,
+     *                       避免了二次验证,以提升效率。
+     * @return 验证结果。如果失败,包含具体的错误信息和导致错误的数据对象。
+     */
+    CallResult verifyImportList(List<TourUser> dataList, Set<String> ignoreFieldSet);
+
+    /**
+     * 获取指定登录名的用户对象。
+     *
+     * @param loginName 指定登录用户名。
+     * @return 用户对象。
+     */
+    TourUser getTourUserByLoginName(String loginName);
+
+    /**
+     * 获取指定登录名的用户对象。
+     *
+     * @param loginMoblie 指定登录手机号。
+     * @return 用户对象。
+     */
+    TourUser getTourUserByLoginMobile(String loginMoblie);
+
+    /**
+     * 修改用户密码。
+     * @param userId  用户主键Id。
+     * @param newPass 新密码。
+     * @return 成功返回true,否则false。
+     */
+    boolean changePassword(Long userId, String newPass);
+
+    /**
+     * 修改用户头像。
+     *
+     * @param tourUserId  用户主键Id。
+     * @param newHeadImage 新的头像信息。
+     * @return 成功返回true,否则false。
+     */
+    boolean changeHeadImage(Long tourUserId, String newHeadImage);
+}

+ 59 - 0
application-webadmin/src/main/java/com/tourism/webadmin/app/website/service/impl/LoginToWebsiteServiceImpl.java

@@ -0,0 +1,59 @@
+package com.tourism.webadmin.app.website.service.impl;
+
+import com.tourism.common.additional.constant.CacheConstants;
+import com.tourism.common.additional.redis.RedisCache;
+import com.tourism.common.additional.utils.StringUtils;
+import com.tourism.webadmin.app.website.service.LoginToWebsiteService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.stereotype.Service;
+
+/**
+ * 注册校验方法
+ * 
+ * @author tourism
+ */
+@Slf4j
+@Service("LoginToWebsiteService")
+public class LoginToWebsiteServiceImpl implements LoginToWebsiteService
+{
+    @Autowired
+    private RedisCache redisCache;
+
+    /**
+     * 校验验证码
+     *
+     * @param code 验证码
+     * @param uuid 唯一标识
+     * @return 结果
+     */
+    @Override
+    public boolean validateCaptcha(String uuid, String code)
+    {
+        String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");
+        String captcha = redisCache.getCacheObject(verifyKey);
+        redisCache.deleteObject(verifyKey);
+        if (captcha == null || !(captcha.equals(code.toLowerCase())))
+        {
+            return false;
+        }
+        return true;
+    }
+    /**
+     * 校验短信验证码
+     *
+     * @param mobile 手机号
+     * @param smsCode 短信验证码
+     * @return 结果
+     */
+    @Override
+    public boolean validateSmsCode(String mobile,String smsCode){
+        String cacheObject = redisCache.getCacheObject(mobile);
+        redisCache.deleteObject(mobile);
+        if(cacheObject == null || !(smsCode.equals(cacheObject.toLowerCase())) ){
+            return false;
+        }
+        return true;
+    }
+}

+ 246 - 0
application-webadmin/src/main/java/com/tourism/webadmin/app/website/service/impl/TourUserServiceImpl.java

@@ -0,0 +1,246 @@
+package com.tourism.webadmin.app.website.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ReflectUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.tourism.webadmin.app.website.service.*;
+import com.tourism.webadmin.app.website.dao.*;
+import com.tourism.webadmin.app.website.model.*;
+import com.tourism.common.core.base.dao.BaseDaoMapper;
+import com.tourism.common.core.constant.GlobalDeletedFlag;
+import com.tourism.common.core.object.MyRelationParam;
+import com.tourism.common.core.object.CallResult;
+import com.tourism.common.core.base.service.BaseService;
+import com.tourism.common.core.util.MyModelUtil;
+import com.tourism.common.sequence.wrapper.IdGeneratorWrapper;
+import com.github.pagehelper.Page;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.lang.reflect.Field;
+import java.util.*;
+
+/**
+ * 门户网站用户管理数据操作服务类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Slf4j
+@Service("tourUserService")
+public class TourUserServiceImpl extends BaseService<TourUser, Long> implements TourUserService {
+
+    @Autowired
+    private IdGeneratorWrapper idGenerator;
+    @Autowired
+    private TourUserMapper tourUserMapper;
+    @Autowired
+    private PasswordEncoder passwordEncoder;
+    /**
+     * 整个工程的实体对象中,创建者Id字段的Java对象名。
+     */
+    public static final String CREATE_USER_ID_FIELD_NAME = "createUserId";
+    /**
+     * 整个工程的实体对象中,创建时间字段的Java对象名。
+     */
+    public static final String CREATE_TIME_FIELD_NAME = "createTime";
+    /**
+     * 整个工程的实体对象中,更新者Id字段的Java对象名。
+     */
+    public static final String UPDATE_USER_ID_FIELD_NAME = "updateUserId";
+    /**
+     * 整个工程的实体对象中,更新时间字段的Java对象名。
+     */
+    public static final String UPDATE_TIME_FIELD_NAME = "updateTime";
+    /**
+     * 整个工程的实体对象中,更新时间字段的Java对象名。
+     * 用户注册的则把createUserId字段作为注册用户id
+     */
+    public static final Integer CREATE_USER_ID_FIELD_REGISTER_NAME = 0;
+    /**
+     * 返回当前Service的主表Mapper对象。
+     *
+     * @return 主表Mapper对象。
+     */
+    @Override
+    protected BaseDaoMapper<TourUser> mapper() {
+        return tourUserMapper;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public TourUser saveNew(TourUser tourUser) {
+        tourUserMapper.insert(this.buildDefaultValue(tourUser));
+        return tourUser;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public void saveNewBatch(List<TourUser> tourUserList) {
+        if (CollUtil.isNotEmpty(tourUserList)) {
+            tourUserList.forEach(this::buildDefaultValue);
+            tourUserMapper.insertList(tourUserList);
+        }
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public void saveNewBatch(List<TourUser> tourUserList, int batchSize) {
+        if (CollUtil.isEmpty(tourUserList)) {
+            return;
+        }
+        if (batchSize <= 0) {
+            batchSize = 10000;
+        }
+        int start = 0;
+        do {
+            int end = Math.min(tourUserList.size(), start + batchSize);
+            List<TourUser> subList = tourUserList.subList(start, end);
+            // 如果数据量过大,同时当前表中存在createTime或updateTime等字段,可以考虑在外部创建一次 new Date(),
+            // 然后传入buildDefaultValue,这样可以减少对象的创建次数,降低GC,提升效率。橙单之所以没有这样生成,是因为
+            // 有些业务场景下需要按照这两个日期字段排序,因此我们只是在这里给出优化建议。
+            subList.forEach(this::buildDefaultValue);
+            tourUserMapper.insertList(subList);
+            if (end == tourUserList.size()) {
+                break;
+            }
+            start += batchSize;
+        } while (true);
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean update(TourUser tourUser, TourUser originalTourUser) {
+        MyModelUtil.fillCommonsForUpdate(tourUser, originalTourUser);
+        // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。
+        UpdateWrapper<TourUser> uw = this.createUpdateQueryForNullValue(tourUser, tourUser.getUserId());
+        return tourUserMapper.update(tourUser, uw) == 1;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean remove(Long userId) {
+        return tourUserMapper.deleteById(userId) == 1;
+    }
+
+    @Override
+    public List<TourUser> getTourUserList(TourUser filter, String orderBy) {
+        return tourUserMapper.getTourUserList(filter, orderBy);
+    }
+
+    @Override
+    public List<TourUser> getTourUserListWithRelation(TourUser filter, String orderBy) {
+        List<TourUser> resultList = tourUserMapper.getTourUserList(filter, orderBy);
+        // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。
+        // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。
+        int batchSize = resultList instanceof Page ? 0 : 1000;
+        this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize);
+        return resultList;
+    }
+
+    @Override
+    public CallResult verifyImportList(List<TourUser> dataList, Set<String> ignoreFieldSet) {
+        CallResult callResult;
+        if (!CollUtil.contains(ignoreFieldSet, "userStatus")) {
+            callResult = verifyImportForConstDict(dataList, "userStatusDictMap", TourUser::getUserStatus);
+            if (!callResult.isSuccess()) {
+                return callResult;
+            }
+        }
+        return CallResult.ok();
+    }
+
+    private TourUser buildDefaultValue(TourUser tourUser) {
+        if (tourUser.getUserId() == null) {
+            tourUser.setUserId(idGenerator.nextLongId());
+        }
+        fillCommonsForInsert(tourUser);
+        tourUser.setDeletedFlag(GlobalDeletedFlag.NORMAL);
+        return tourUser;
+    }
+
+    /**
+     * 获取指定登录名的用户对象。
+     *
+     * @param loginName 指定登录用户名。
+     * @return 用户对象。
+     */
+    @Override
+    public TourUser getTourUserByLoginName(String loginName){
+        TourUser filter = new TourUser();
+        filter.setLoginName(loginName);
+        return tourUserMapper.selectOne(new QueryWrapper<>(filter));
+    }
+    /**
+     * 获取指定登录名的用户对象。
+     *
+     * @param loginMoblie 指定登录手机号。
+     * @return 用户对象。
+     */
+    @Override
+    public TourUser getTourUserByLoginMobile(String loginMoblie){
+        TourUser filter = new TourUser();
+        filter.setMobile(loginMoblie);
+        return tourUserMapper.selectOne(new QueryWrapper<>(filter));
+    }
+
+    /**
+     * 修改用户密码。
+     * @param tourUserId  用户主键Id。
+     * @param newPass 新密码。
+     * @return 成功返回true,否则false。
+     */
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean changePassword(Long tourUserId, String newPass) {
+        TourUser updatedTourUser = new TourUser();
+        updatedTourUser.setUserId(tourUserId);
+        updatedTourUser.setPassword(passwordEncoder.encode(newPass));
+        return tourUserMapper.updateById(updatedTourUser) == 1;
+    }
+
+    /**
+     * 修改用户头像。
+     *
+     * @param tourUserId  用户主键Id。
+     * @param newHeadImage 新的头像信息。
+     * @return 成功返回true,否则false。
+     */
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean changeHeadImage(Long tourUserId, String newHeadImage) {
+        TourUser updatedUser = new TourUser();
+        updatedUser.setUserId(tourUserId);
+        updatedUser.setHeadImageUrl(newHeadImage);
+        return tourUserMapper.updateById(updatedUser) == 1;
+    }
+
+    /**
+     * 在插入实体对象数据之前,可以调用该方法,初始化通用字段的数据。
+     *
+     * @param data 实体对象。
+     * @param <M>  实体对象类型。
+     */
+    public static <M> void fillCommonsForInsert(M data) {
+        Field createdByField = ReflectUtil.getField(data.getClass(), CREATE_USER_ID_FIELD_NAME);
+        if (createdByField != null) {
+            ReflectUtil.setFieldValue(data, createdByField, CREATE_USER_ID_FIELD_REGISTER_NAME);
+        }
+        Field createTimeField = ReflectUtil.getField(data.getClass(), CREATE_TIME_FIELD_NAME);
+        if (createTimeField != null) {
+            ReflectUtil.setFieldValue(data, createTimeField, new Date());
+        }
+        Field updatedByField = ReflectUtil.getField(data.getClass(), UPDATE_USER_ID_FIELD_NAME);
+        if (updatedByField != null) {
+            ReflectUtil.setFieldValue(data, updatedByField, CREATE_USER_ID_FIELD_REGISTER_NAME);
+        }
+        Field updateTimeField = ReflectUtil.getField(data.getClass(), UPDATE_TIME_FIELD_NAME);
+        if (updateTimeField != null) {
+            ReflectUtil.setFieldValue(data, updateTimeField, new Date());
+        }
+    }
+}

+ 75 - 0
application-webadmin/src/main/java/com/tourism/webadmin/app/website/vo/TourUserVo.java

@@ -0,0 +1,75 @@
+package com.tourism.webadmin.app.website.vo;
+
+import com.tourism.common.core.base.vo.BaseVo;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * 门户用户管理VO视图对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Schema(description = "TourUserVO视图对象")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class TourUserVo extends BaseVo {
+
+    /**
+     * 主键Id。
+     */
+    @Schema(description = "主键Id")
+    private String userId;
+
+    /**
+     * 用户登录名称。
+     */
+    @Schema(description = "用户登录名称")
+    private String loginName;
+
+    /**
+     * 密码。
+     */
+    @Schema(description = "密码")
+    private String password;
+
+    /**
+     * 用户显示名称。
+     */
+    @Schema(description = "用户显示名称")
+    private String showName;
+
+    /**
+     * 用户头像的Url。
+     */
+    @Schema(description = "用户头像的Url")
+    private String headImageUrl;
+
+    /**
+     * 状态(0: 正常 1: 锁定)。
+     */
+    @Schema(description = "状态(0: 正常 1: 锁定)")
+    private Integer userStatus;
+
+    /**
+     * 用户邮箱。
+     */
+    @Schema(description = "用户邮箱")
+    private String email;
+
+    /**
+     * 用户手机。
+     */
+    @Schema(description = "用户手机")
+    private String mobile;
+
+    /**
+     * userStatus 常量字典关联数据。
+     */
+    @Schema(description = "userStatus 常量字典关联数据")
+    private Map<String, Object> userStatusDictMap;
+}

+ 0 - 620
application-webadmin/src/main/java/com/tourism/webadmin/back/controller/LoginToWebController.java

@@ -1,620 +0,0 @@
-package com.tourism.webadmin.back.controller;
-
-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.StrUtil;
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
-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.UploadStoreInfo;
-import com.tourism.common.core.util.*;
-import com.tourism.common.flow.online.service.FlowOnlineOperationService;
-import com.tourism.common.log.annotation.OperationLog;
-import com.tourism.common.log.model.constant.SysOperationLogType;
-import com.tourism.common.mobile.model.MobileEntry;
-import com.tourism.common.online.service.OnlineOperationService;
-import com.tourism.common.redis.cache.SessionCacheHelper;
-import com.tourism.common.report.service.ReportOperationService;
-import com.tourism.common.satoken.util.SaTokenUtil;
-import com.tourism.webadmin.back.model.SysUserWeb;
-import com.tourism.webadmin.back.service.SmsService;
-import com.tourism.webadmin.back.service.SysUserWebService;
-import com.tourism.webadmin.config.ApplicationConfig;
-import com.tourism.webadmin.back.dto.SysUserWebDto;
-import com.tourism.webadmin.upms.model.*;
-import com.tourism.webadmin.upms.model.constant.SysMenuType;
-import com.tourism.webadmin.upms.model.constant.SysOnlineMenuPermType;
-import com.tourism.webadmin.upms.model.constant.SysUserStatus;
-import com.tourism.webadmin.upms.service.*;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import jakarta.servlet.http.HttpServletResponse;
-import lombok.Data;
-import lombok.extern.slf4j.Slf4j;
-import org.redisson.api.RSet;
-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.ObjectUtils;
-import org.springframework.web.bind.annotation.*;
-
-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 吃饭睡觉
- * @date 2024-09-06
- */
-@Tag(name = "网页用户登录接口")
-@DisableDataFilter
-@Slf4j
-@RestController
-@RequestMapping("/admin/web/login")
-public class LoginToWebController {
-
-
-    @Autowired
-    private SysUserWebService sysUserWebService;
-    @Autowired
-    private SysDataPermService sysDataPermService;
-    @Autowired
-    private OnlineOperationService onlineOperationService;
-    @Autowired
-    private FlowOnlineOperationService flowOnlineOperationService;
-    @Autowired
-    private ReportOperationService reportOperationService;
-    @Autowired
-    private ApplicationConfig appConfig;
-    @Autowired
-    private RedissonClient redissonClient;
-    @Autowired
-    private SessionCacheHelper cacheHelper;
-    @Autowired
-    private PasswordEncoder passwordEncoder;
-    @Autowired
-    private UpDownloaderFactory upDownloaderFactory;
-    @Autowired
-    private SaTokenUtil saTokenUtil;
-    @Autowired
-    private SmsService smsService;
-
-    private static final String IS_ADMIN = "isAdmin";
-    private static final String SHOW_NAME_FIELD = "showName";
-    private static final String SHOW_ORDER_FIELD = "showOrder";
-    private static final String HEAD_IMAGE_URL_FIELD = "headImageUrl";
-
-    /**
-     * 用户注册
-     * @return
-     * @throws UnsupportedEncodingException
-     */
-    @SaIgnore
-    @OperationLog(type = SysOperationLogType.Register, saveResponse = false)
-    @Transactional(rollbackFor = Exception.class)
-    @PostMapping("/doRegister")
-    public ResponseResult doRegister(@RequestBody SysUserWebDto sysUserWebDto) {
-
-        if (MyCommonUtil.existBlankArgument(sysUserWebDto.getLoginName(), sysUserWebDto.getPassword())) {
-            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
-        }
-
-        // 验证短信验证码
-        if (!smsService.validateSmsCode(sysUserWebDto.getSmsCode(),sysUserWebDto.getMobile())) {
-            return ResponseResult.error(ErrorCodeEnum.SMSCODE_ERR);
-        }
-
-        //存储用户信息
-        SysUserWeb sysUserWeb = new SysUserWeb();
-        BeanUtil.copyProperties(sysUserWebDto,sysUserWeb);
-        sysUserWebService.saveNew(sysUserWeb);
-
-        return ResponseResult.success();
-    }
-
-    private ResponseResult<SysUserWeb> verifyAndHandleLoginUser(
-            String loginName, String password) throws UnsupportedEncodingException {
-        String errorMessage;
-        SysUserWeb user = sysUserWebService.getSysUserByLoginName(loginName);
-        password = URLDecoder.decode(password, StandardCharsets.UTF_8.name());
-
-        // NOTE: 第一次使用时,请务必阅读ApplicationConstant.PRIVATE_KEY的代码注释。
-        // 执行RsaUtil工具类中的main函数,可以生成新的公钥和私钥。
-        password = RsaUtil.decrypt(password, ApplicationConstant.PRIVATE_KEY);
-        if (user == null || !passwordEncoder.matches(password, user.getPassword())) {
-            return ResponseResult.error(ErrorCodeEnum.INVALID_USERNAME_PASSWORD);
-        }
-        if (user.getUserStatus() == SysUserStatus.STATUS_LOCKED) {
-            errorMessage = "登录失败,用户账号被锁定!";
-            return ResponseResult.error(ErrorCodeEnum.INVALID_USER_STATUS, errorMessage);
-        }
-        if (BooleanUtil.isTrue(appConfig.getExcludeLogin())) {
-            String deviceType = MyCommonUtil.getDeviceTypeWithString();
-            LoginUserInfo userInfo = BeanUtil.copyProperties(user, LoginUserInfo.class);
-            String loginId = SaTokenUtil.makeLoginIdByWeb(userInfo);
-            StpUtil.kickout(loginId, deviceType);
-        }
-        return ResponseResult.success(user);
-    }
-
-    /**
-     * 登录接口。
-     *
-     * @param loginName 登录名。
-     * @param password  密码。
-     * @return 应答结果对象,其中包括Token数据,以及菜单列表。
-     */
-    @SaIgnore
-    @OperationLog(type = SysOperationLogType.LOGIN_WEB, saveResponse = false)
-    @PostMapping("/doLogin")
-    public ResponseResult<JSONObject> doLogin(
-            @MyRequestBody String loginName,
-            @MyRequestBody String password) throws UnsupportedEncodingException {
-        if (MyCommonUtil.existBlankArgument(loginName, password)) {
-            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
-        }
-        ResponseResult<SysUserWeb> verifyResult = this.verifyAndHandleLoginUser(loginName, password);
-        if (!verifyResult.isSuccess()) {
-            return ResponseResult.errorFrom(verifyResult);
-        }
-        JSONObject jsonData = this.buildLoginDataAndLogin(verifyResult.getData());
-        return ResponseResult.success(jsonData);
-    }
-
-    /**
-     * 手机号登录接口。
-     * @return 应答结果对象,其中包括Token数据,以及菜单列表。
-     */
-    @SaIgnore
-    @OperationLog(type = SysOperationLogType.LOGIN_WEB, saveResponse = false)
-    @PostMapping("/doLoginByPhone")
-    public ResponseResult<JSONObject> doLoginByPhone(
-            @MyRequestBody String mobile,
-            @MyRequestBody String smsCode) throws UnsupportedEncodingException {
-        String errorMessage;
-        if (MyCommonUtil.existBlankArgument(mobile, smsCode)) {
-            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
-        }
-        //查询手机号是否存在
-        SysUserWeb sysUserByMobile = sysUserWebService.getSysUserByMobile(mobile);
-        if (ObjectUtils.isEmpty(sysUserByMobile)){
-            return ResponseResult.error(ErrorCodeEnum.INVALID_MOBILE_CODE);
-        }
-
-        // 验证短信验证码
-        if (!smsService.validateSmsCode(smsCode,mobile)) {
-            return ResponseResult.error(ErrorCodeEnum.SMSCODE_ERR);
-        }
-
-        if (sysUserByMobile.getUserStatus() == SysUserStatus.STATUS_LOCKED) {
-            errorMessage = "登录失败,用户账号被锁定!";
-            return ResponseResult.error(ErrorCodeEnum.INVALID_USER_STATUS, errorMessage);
-        }
-        if (BooleanUtil.isTrue(appConfig.getExcludeLogin())) {
-            String deviceType = MyCommonUtil.getDeviceTypeWithString();
-            LoginUserInfo userInfo = BeanUtil.copyProperties(sysUserByMobile, LoginUserInfo.class);
-            String loginId = SaTokenUtil.makeLoginIdByWeb(userInfo);
-            StpUtil.kickout(loginId, deviceType);
-        }
-        JSONObject jsonData = this.buildLoginDataAndLogin(sysUserByMobile);
-        return ResponseResult.success(jsonData);
-    }
-
-
-
-
-
-    /**
-     * 登录移动端接口。
-     *
-     * @param loginName 登录名。
-     * @param password  密码。
-     * @return 应答结果对象,其中包括Token数据,以及菜单列表。
-     */
-//    @Parameter(name = "loginName", example = "admin")
-//    @Parameter(name = "password", example = "IP3ccke3GhH45iGHB5qP9p7iZw6xUyj28Ju10rnBiPKOI35sc%2BjI7%2FdsjOkHWMfUwGYGfz8ik31HC2Ruk%2Fhkd9f6RPULTHj7VpFdNdde2P9M4mQQnFBAiPM7VT9iW3RyCtPlJexQ3nAiA09OqG%2F0sIf1kcyveSrulxembARDbDo%3D")
-//    @SaIgnore
-//    @OperationLog(type = SysOperationLogType.LOGIN_MOBILE, saveResponse = false)
-//    @PostMapping("/doMobileLogin")
-//    public ResponseResult<JSONObject> doMobileLogin(
-//            @MyRequestBody String loginName,
-//            @MyRequestBody String password) throws UnsupportedEncodingException {
-//        if (MyCommonUtil.existBlankArgument(loginName, password)) {
-//            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
-//        }
-//        ResponseResult<SysUser> verifyResult = this.verifyAndHandleLoginUser(loginName, password);
-//        if (!verifyResult.isSuccess()) {
-//            return ResponseResult.errorFrom(verifyResult);
-//        }
-//        JSONObject jsonData = this.buildMobileLoginDataAndLogin(verifyResult.getData());
-//        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());
-        jsonData.put(IS_ADMIN, tokenData.getIsAdmin());
-        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();
-        SysUserWeb user = sysUserWebService.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 (user == null || !passwordEncoder.matches(oldPass, user.getPassword())) {
-            return ResponseResult.error(ErrorCodeEnum.INVALID_USERNAME_PASSWORD);
-        }
-        newPass = URLDecoder.decode(newPass, StandardCharsets.UTF_8.name());
-        newPass = RsaUtil.decrypt(newPass, ApplicationConstant.PRIVATE_KEY);
-        if (!sysUserWebService.changePassword(tokenData.getUserId(), newPass)) {
-            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
-        }
-        return ResponseResult.success();
-    }
-
-    /**
-     * 忘记密码。
-     * @return 应答结果对象。
-     */
-    @PostMapping("/findPassword")
-    public ResponseResult<String> findPassword(
-            @MyRequestBody String smsCode, @MyRequestBody String mobile) throws UnsupportedEncodingException {
-        //校验是否为空
-        if (MyCommonUtil.existBlankArgument(smsCode, mobile)) {
-            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
-        }
-
-        SysUserWeb sysUserByMobile = sysUserWebService.getSysUserByMobile(mobile);
-//        if (ObjectUtils.isEmpty(sysUserByMobile)){
-//            return ResponseResult.error(ErrorCodeEnum.INVALID_MOBILE_CODE);
-//        }
-
-        // 验证短信验证码
-        if (!smsService.validateSmsCode(smsCode,mobile)) {
-            return ResponseResult.error(ErrorCodeEnum.SMSCODE_ERR);
-        }
-
-        return ResponseResult.success(sysUserByMobile.getPassword());
-    }
-
-
-    /**
-     * 上传并修改用户头像。
-     *
-     * @param uploadFile 上传的头像文件。
-     */
-//    @PostMapping("/changeHeadImage")
-//    public void changeHeadImage(@RequestParam("uploadFile") MultipartFile uploadFile) throws IOException {
-//        UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(SysUser.class, HEAD_IMAGE_URL_FIELD);
-//        BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType());
-//        UploadResponseInfo responseInfo = upDownloader.doUpload(null,
-//                appConfig.getUploadFileBaseDir(), SysUser.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 (!sysUserService.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(),
-                    SysUser.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 ResponseResult<SysUser> verifyAndHandleLoginUser(
-//            String loginName, String password) throws UnsupportedEncodingException {
-//        String errorMessage;
-//        SysUser user = sysUserService.getSysUserByLoginName(loginName);
-//        password = URLDecoder.decode(password, StandardCharsets.UTF_8.name());
-//        // NOTE: 第一次使用时,请务必阅读ApplicationConstant.PRIVATE_KEY的代码注释。
-//        // 执行RsaUtil工具类中的main函数,可以生成新的公钥和私钥。
-//        password = RsaUtil.decrypt(password, ApplicationConstant.PRIVATE_KEY);
-//        if (user == null || !passwordEncoder.matches(password, user.getPassword())) {
-//            return ResponseResult.error(ErrorCodeEnum.INVALID_USERNAME_PASSWORD);
-//        }
-//        if (user.getUserStatus() == SysUserStatus.STATUS_LOCKED) {
-//            errorMessage = "登录失败,用户账号被锁定!";
-//            return ResponseResult.error(ErrorCodeEnum.INVALID_USER_STATUS, errorMessage);
-//        }
-//        if (BooleanUtil.isTrue(appConfig.getExcludeLogin())) {
-//            String deviceType = MyCommonUtil.getDeviceTypeWithString();
-//            LoginUserInfo userInfo = BeanUtil.copyProperties(user, LoginUserInfo.class);
-//            String loginId = SaTokenUtil.makeLoginId(userInfo);
-//            StpUtil.kickout(loginId, deviceType);
-//        }
-//        return ResponseResult.success(user);
-//    }
-
-    private JSONObject buildLoginDataAndLogin(SysUserWeb user) {
-        TokenData tokenData = this.loginAndCreateToken(user);
-        // 这里手动将TokenData存入request,便于OperationLogAspect统一处理操作日志。
-        TokenData.addToRequest(tokenData);
-        JSONObject jsonData = this.createResponseData(user);
-        Collection<SysMenu> allMenuList;
-        return jsonData;
-    }
-
-    private JSONObject buildMobileLoginDataAndLogin(SysUserWeb user) {
-        TokenData tokenData = this.loginAndCreateToken(user);
-        // 这里手动将TokenData存入request,便于OperationLogAspect统一处理操作日志。
-        TokenData.addToRequest(tokenData);
-        JSONObject jsonData = this.createResponseData(user);
-        List<MobileEntry> mobileEntryList;
-
-        return jsonData;
-    }
-
-    private TokenData loginAndCreateToken(SysUserWeb user) {
-        String deviceType = MyCommonUtil.getDeviceTypeWithString();
-        LoginUserInfo userInfo = BeanUtil.copyProperties(user, LoginUserInfo.class);
-        String loginId = SaTokenUtil.makeLoginIdByWeb(userInfo);
-        StpUtil.login(loginId, deviceType);
-        SaSession session = StpUtil.getTokenSession();
-        TokenData tokenData = this.buildTokenData(user, session.getId(), StpUtil.getLoginDevice());
-        String mySessionId = RedisKeyUtil.getSessionIdPrefix(tokenData, user.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 JSONObject createResponseData(SysUserWeb user) {
-        JSONObject jsonData = new JSONObject();
-        jsonData.put(TokenData.REQUEST_ATTRIBUTE_NAME, StpUtil.getTokenValue());
-        jsonData.put(SHOW_NAME_FIELD, user.getShowName());
-        if (StrUtil.isNotBlank(user.getHeadImageUrl())) {
-            jsonData.put(HEAD_IMAGE_URL_FIELD, user.getHeadImageUrl());
-        }
-        return jsonData;
-    }
-
-    private void appendResponseMenuAndPermCodeData(
-            JSONObject responseData, Collection<SysMenu> allMenuList, Collection<String> menuCodeList) {
-        allMenuList.stream()
-                .filter(m -> m.getExtraObject() != null && StrUtil.isNotBlank(m.getExtraObject().getMenuCode()))
-                .forEach(m -> CollUtil.addAll(menuCodeList, m.getExtraObject().getMenuCode()));
-        List<SysMenu> menuList = allMenuList.stream()
-                .filter(m -> m.getMenuType() <= SysMenuType.TYPE_MENU).collect(Collectors.toList());
-        responseData.put("menuList", menuList);
-        responseData.put("permCodeList", menuCodeList);
-    }
-
-    private TokenData buildTokenData(SysUserWeb user, String sessionId, String deviceType) {
-        TokenData tokenData = new TokenData();
-        tokenData.setSessionId(sessionId);
-        tokenData.setUserId(user.getUserId());
-        tokenData.setLoginName(user.getLoginName());
-        tokenData.setShowName(user.getShowName());
-        tokenData.setLoginIp(IpUtil.getRemoteIpAddress(ContextUtil.getHttpRequest()));
-        tokenData.setLoginTime(new Date());
-        tokenData.setDeviceType(deviceType);
-        tokenData.setHeadImageUrl(user.getHeadImageUrl());
-        return tokenData;
-    }
-
-    private void putUserSysPermCache(String sessionId, Collection<String> permUrlSet) {
-        if (CollUtil.isEmpty(permUrlSet)) {
-            return;
-        }
-        String sessionPermKey = RedisKeyUtil.makeSessionPermIdKey(sessionId);
-        RSet<String> redisPermSet = redissonClient.getSet(sessionPermKey);
-        redisPermSet.addAll(permUrlSet);
-        redisPermSet.expire(appConfig.getSessionExpiredSeconds(), TimeUnit.SECONDS);
-    }
-
-    private void putUserSysPermCodeCache(String sessionId, Collection<String> permCodeSet) {
-        if (CollUtil.isEmpty(permCodeSet)) {
-            return;
-        }
-        String sessionPermCodeKey = RedisKeyUtil.makeSessionPermCodeKey(sessionId);
-        RSet<String> redisPermSet = redissonClient.getSet(sessionPermCodeKey);
-        redisPermSet.addAll(permCodeSet);
-        redisPermSet.expire(appConfig.getSessionExpiredSeconds(), TimeUnit.SECONDS);
-    }
-
-    private OnlinePermData getOnlineMenuPermData(Collection<SysMenu> allMenuList) {
-        List<SysMenu> onlineMenuList = allMenuList.stream()
-                .filter(m -> m.getOnlineFormId() != null && m.getMenuType().equals(SysMenuType.TYPE_BUTTON))
-                .collect(Collectors.toList());
-        if (CollUtil.isEmpty(onlineMenuList)) {
-            return new OnlinePermData();
-        }
-        Set<Long> formIds = allMenuList.stream()
-                .filter(m -> m.getOnlineFormId() != null
-                        && m.getOnlineFlowEntryId() == null
-                        && m.getMenuType().equals(SysMenuType.TYPE_MENU))
-                .map(SysMenu::getOnlineFormId)
-                .collect(Collectors.toSet());
-        Set<Long> viewFormIds = onlineMenuList.stream()
-                .filter(m -> m.getOnlineMenuPermType() == SysOnlineMenuPermType.TYPE_VIEW)
-                .map(SysMenu::getOnlineFormId)
-                .collect(Collectors.toSet());
-        Set<Long> editFormIds = onlineMenuList.stream()
-                .filter(m -> m.getOnlineMenuPermType() == SysOnlineMenuPermType.TYPE_EDIT)
-                .map(SysMenu::getOnlineFormId)
-                .collect(Collectors.toSet());
-        Map<String, Object> permDataMap =
-                onlineOperationService.calculatePermData(formIds, viewFormIds, editFormIds);
-        OnlinePermData permData = BeanUtil.mapToBean(permDataMap, OnlinePermData.class, false, null);
-        permData.permUrlSet.addAll(permData.onlineWhitelistUrls);
-        return permData;
-    }
-
-    private OnlinePermData getFlowOnlineMenuPermData(Collection<SysMenu> allMenuList) {
-        List<SysMenu> flowOnlineMenuList = allMenuList.stream()
-                .filter(m -> m.getOnlineFlowEntryId() != null).collect(Collectors.toList());
-        Set<Long> entryIds = flowOnlineMenuList.stream()
-                .map(SysMenu::getOnlineFlowEntryId).collect(Collectors.toSet());
-        List<Map<String, Object>> flowPermDataList = flowOnlineOperationService.calculatePermData(entryIds);
-        List<OnlineFlowPermData> flowOnlinePermDataList =
-                MyModelUtil.mapToBeanList(flowPermDataList, OnlineFlowPermData.class);
-        OnlinePermData permData = new OnlinePermData();
-        flowOnlinePermDataList.forEach(perm -> {
-            permData.permCodeSet.addAll(perm.getPermCodeList());
-            permData.permUrlSet.addAll(perm.getPermList());
-        });
-        return permData;
-    }
-
-    private Set<String> getReportMenuPermData(Collection<SysMenu> allMenuList) {
-        Set<String> permSet = new HashSet<>();
-        List<SysMenu> reportMenuList = allMenuList.stream()
-                .filter(m -> m.getReportPageId() != null).collect(Collectors.toList());
-        if (CollUtil.isEmpty(reportMenuList)) {
-            return permSet;
-        }
-        Set<Long> pageIds = reportMenuList.stream().map(SysMenu::getReportPageId).collect(Collectors.toSet());
-        Map<Long, Set<String>> reportPermDataMap = reportOperationService.calculatePermData(pageIds);
-        for (Long pageId : pageIds) {
-            CollUtil.addAll(permSet, reportPermDataMap.get(pageId));
-        }
-        return permSet;
-    }
-
-    private Set<String> getOnlineMobileEntryPermData(Collection<MobileEntry> mobileEntryList) {
-        List<MobileEntry> onlineMobileEntryList = mobileEntryList.stream()
-                .filter(m -> m.getExtraObject() != null
-                        && m.getExtraObject().getOnlineFormId() != null
-                        && m.getExtraObject().getOnlineFlowEntryId() == null)
-                .collect(Collectors.toList());
-        if (CollUtil.isEmpty(onlineMobileEntryList)) {
-            return CollUtil.newHashSet();
-        }
-        Set<Long> onlineFormIds = onlineMobileEntryList.stream()
-                .map(m -> m.getExtraObject().getOnlineFormId()).collect(Collectors.toSet());
-        Map<String, Object> permDataMap =
-                onlineOperationService.calculatePermData(onlineFormIds, onlineFormIds, onlineFormIds);
-        OnlinePermData permData = BeanUtil.mapToBean(permDataMap, OnlinePermData.class, false, null);
-        permData.permUrlSet.addAll(permData.onlineWhitelistUrls);
-        return permData.permUrlSet;
-    }
-
-    private Set<String> getFlowOnlineMobileEntryPermData(Collection<MobileEntry> mobileEntryList) {
-        List<MobileEntry> flowOnlineMobileEntryList = mobileEntryList.stream()
-                .filter(m -> m.getExtraData() != null && m.getExtraObject().getOnlineFlowEntryId() != null)
-                .collect(Collectors.toList());
-        Set<Long> entryIds = flowOnlineMobileEntryList.stream()
-                .map(m -> m.getExtraObject().getOnlineFlowEntryId()).collect(Collectors.toSet());
-        List<Map<String, Object>> flowPermDataList = flowOnlineOperationService.calculatePermData(entryIds);
-        List<OnlineFlowPermData> flowOnlinePermDataList =
-                MyModelUtil.mapToBeanList(flowPermDataList, OnlineFlowPermData.class);
-        Set<String> permSet = new HashSet<>();
-        flowOnlinePermDataList.forEach(perm -> permSet.addAll(perm.getPermList()));
-        return permSet;
-    }
-
-    private Set<String> getReportMobileEntryPermData(Collection<MobileEntry> mobileEntryList) {
-        Set<String> permSet = new HashSet<>();
-        List<MobileEntry> reportMobileEntryList = mobileEntryList.stream()
-                .filter(m -> m.getExtraObject() != null && m.getExtraObject().getReportPageId() != null)
-                .collect(Collectors.toList());
-        if (CollUtil.isEmpty(reportMobileEntryList)) {
-            return permSet;
-        }
-        Set<Long> pageIds = reportMobileEntryList.stream()
-                .map(m -> m.getExtraObject().getReportPageId()).collect(Collectors.toSet());
-        Map<Long, Set<String>> reportPermDataMap = reportOperationService.calculatePermData(pageIds);
-        for (Long pageId : pageIds) {
-            CollUtil.addAll(permSet, reportPermDataMap.get(pageId));
-        }
-        return permSet;
-    }
-
-    static class OnlinePermData {
-        public final Set<String> permCodeSet = new HashSet<>();
-        public final Set<String> permUrlSet = new HashSet<>();
-        public final List<String> onlineWhitelistUrls = new LinkedList<>();
-    }
-
-    @Data
-    static class OnlineFlowPermData {
-        private List<String> permCodeList;
-        private List<String> permList;
-    }
-}

+ 0 - 189
application-webadmin/src/main/java/com/tourism/webadmin/back/dao/SysUserWebMapper.java

@@ -1,189 +0,0 @@
-package com.tourism.webadmin.back.dao;
-
-import com.tourism.common.core.base.dao.BaseDaoMapper;
-import com.tourism.webadmin.upms.model.SysUser;
-import com.tourism.webadmin.back.model.SysUserWeb;
-import org.apache.ibatis.annotations.Param;
-
-import java.util.*;
-
-/**
- * 用户管理数据操作访问接口。
- *
- * @author 吃饭睡觉
- * @date 2024-09-06
- */
-public interface SysUserWebMapper extends BaseDaoMapper<SysUserWeb> {
-
-    /**
-     * 批量插入对象列表。
-     *
-     * @param sysUserList 新增对象列表。
-     */
-    void insertList(List<SysUser> sysUserList);
-
-    /**
-     * 获取过滤后的对象列表。
-     *
-     * @param sysUserFilter 主表过滤对象。
-     * @param orderBy 排序字符串,order by从句的参数。
-     * @return 对象列表。
-     */
-    List<SysUser> getSysUserList(
-            @Param("sysUserFilter") SysUser sysUserFilter, @Param("orderBy") String orderBy);
-
-    /**
-     * 根据部门Id集合,获取关联的用户列表。
-     *
-     * @param deptIds       关联的部门Id集合。
-     * @param sysUserFilter 用户过滤条件对象。
-     * @param orderBy       order by从句的参数。
-     * @return 和部门Id集合关联的用户列表。
-     */
-    List<SysUser> getSysUserListByDeptIds(
-            @Param("deptIds") Set<Long> deptIds,
-            @Param("sysUserFilter") SysUser sysUserFilter,
-            @Param("orderBy") String orderBy);
-
-    /**
-     * 根据登录名集合,获取关联的用户列表。
-     * @param loginNames    登录名集合。
-     * @param sysUserFilter 用户过滤条件对象。
-     * @param orderBy       order by从句的参数。
-     * @return 和登录名集合关联的用户列表。
-     */
-    List<SysUser> getSysUserListByLoginNames(
-            @Param("loginNames") List<String> loginNames,
-            @Param("sysUserFilter") SysUser sysUserFilter,
-            @Param("orderBy") String orderBy);
-
-    /**
-     * 根据角色Id,获取关联的用户列表。
-     *
-     * @param roleId        关联的角色Id。
-     * @param sysUserFilter 用户过滤条件对象。
-     * @param orderBy       order by从句的参数。
-     * @return 和角色Id关联的用户列表。
-     */
-    List<SysUser> getSysUserListByRoleId(
-            @Param("roleId") Long roleId,
-            @Param("sysUserFilter") SysUser sysUserFilter,
-            @Param("orderBy") String orderBy);
-
-    /**
-     * 根据角色Id集合,获取去重后的用户Id列表。
-     *
-     * @param roleIds       关联的角色Id集合。
-     * @param sysUserFilter 用户过滤条件对象。
-     * @param orderBy       order by从句的参数。
-     * @return 和角色Id集合关联的去重后的用户Id列表。
-     */
-    List<Long> getUserIdListByRoleIds(
-            @Param("roleIds") Set<Long> roleIds,
-            @Param("sysUserFilter") SysUser sysUserFilter,
-            @Param("orderBy") String orderBy);
-
-    /**
-     * 根据角色Id,获取和当前角色Id没有建立多对多关联关系的用户列表。
-     *
-     * @param roleId        关联的角色Id。
-     * @param sysUserFilter 用户过滤条件对象。
-     * @param orderBy order by从句的参数。
-     * @return 和RoleId没有建立关联关系的用户列表。
-     */
-    List<SysUser> getNotInSysUserListByRoleId(
-            @Param("roleId") Long roleId,
-            @Param("sysUserFilter") SysUser sysUserFilter,
-            @Param("orderBy") String orderBy);
-
-    /**
-     * 根据数据权限Id,获取关联的用户列表。
-     *
-     * @param dataPermId    关联的数据权限Id。
-     * @param sysUserFilter 用户过滤条件对象。
-     * @param orderBy order by从句的参数。
-     * @return 和DataPermId关联的用户列表。
-     */
-    List<SysUser> getSysUserListByDataPermId(
-            @Param("dataPermId") Long dataPermId,
-            @Param("sysUserFilter") SysUser sysUserFilter,
-            @Param("orderBy") String orderBy);
-
-    /**
-     * 根据数据权限Id,获取和当前数据权限Id没有建立多对多关联关系的用户列表。
-     *
-     * @param dataPermId    关联的数据权限Id。
-     * @param sysUserFilter 用户过滤条件对象。
-     * @param orderBy order by从句的参数。
-     * @return 和DataPermId没有建立关联关系的用户列表。
-     */
-    List<SysUser> getNotInSysUserListByDataPermId(
-            @Param("dataPermId") Long dataPermId,
-            @Param("sysUserFilter") SysUser sysUserFilter,
-            @Param("orderBy") String orderBy);
-
-    /**
-     * 根据部门岗位Id集合,获取关联的去重后的用户Id列表。
-     *
-     * @param deptPostIds   关联的部门岗位Id集合。
-     * @param sysUserFilter 用户过滤条件对象。
-     * @param orderBy       order by从句的参数。
-     * @return 和部门岗位Id集合关联的去重后的用户Id列表。
-     */
-    List<Long> getUserIdListByDeptPostIds(
-            @Param("deptPostIds") Set<Long> deptPostIds,
-            @Param("sysUserFilter") SysUser sysUserFilter,
-            @Param("orderBy") String orderBy);
-
-    /**
-     * 根据部门岗位Id,获取关联的用户列表。
-     *
-     * @param deptPostId    关联的部门岗位Id。
-     * @param sysUserFilter 用户过滤条件对象。
-     * @param orderBy       order by从句的参数。
-     * @return 和部门岗位Id关联的用户列表。
-     */
-    List<SysUser> getSysUserListByDeptPostId(
-            @Param("deptPostId") Long deptPostId,
-            @Param("sysUserFilter") SysUser sysUserFilter,
-            @Param("orderBy") String orderBy);
-
-    /**
-     * 根据部门岗位Id,获取和当前部门岗位Id没有建立多对多关联关系的用户列表。
-     *
-     * @param deptPostId    关联的部门岗位Id。
-     * @param sysUserFilter 用户过滤条件对象。
-     * @param orderBy       order by从句的参数。
-     * @return 和deptPostId没有建立关联关系的用户列表。
-     */
-    List<SysUser> getNotInSysUserListByDeptPostId(
-            @Param("deptPostId") Long deptPostId,
-            @Param("sysUserFilter") SysUser sysUserFilter,
-            @Param("orderBy") String orderBy);
-
-    /**
-     * 根据岗位Id集合,获取关联的去重后的用户Id列表。
-     *
-     * @param postIds       关联的岗位Id集合。
-     * @param sysUserFilter 用户过滤条件对象。
-     * @param orderBy       order by从句的参数。
-     * @return 和岗位Id集合关联的去重后的用户Id列表。
-     */
-    List<Long> getUserIdListByPostIds(
-            @Param("postIds") Set<Long> postIds,
-            @Param("sysUserFilter") SysUser sysUserFilter,
-            @Param("orderBy") String orderBy);
-
-    /**
-     * 根据岗位Id,获取关联的用户列表。
-     *
-     * @param postId        关联的岗位Id。
-     * @param sysUserFilter 用户过滤条件对象。
-     * @param orderBy       order by从句的参数。
-     * @return 和岗位Id关联的用户列表。
-     */
-    List<SysUser> getSysUserListByPostId(
-            @Param("postId") Long postId,
-            @Param("sysUserFilter") SysUser sysUserFilter,
-            @Param("orderBy") String orderBy);
-}

+ 0 - 288
application-webadmin/src/main/java/com/tourism/webadmin/back/dao/mapper/SysUserWebMapper.xml

@@ -1,288 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="com.tourism.webadmin.back.dao.SysUserWebMapper">
-    <resultMap id="BaseResultMap" type="com.tourism.webadmin.back.model.SysUserWeb">
-        <id column="user_id" jdbcType="BIGINT" property="userId"/>
-        <result column="login_name" jdbcType="VARCHAR" property="loginName"/>
-        <result column="password" jdbcType="VARCHAR" property="password"/>
-        <result column="show_name" jdbcType="VARCHAR" property="showName"/>
-        <result column="head_image_url" jdbcType="VARCHAR" property="headImageUrl"/>
-        <result column="user_status" jdbcType="INTEGER" property="userStatus"/>
-        <result column="email" jdbcType="VARCHAR" property="email"/>
-        <result column="mobile" jdbcType="VARCHAR" property="mobile"/>
-        <result column="create_user_id" jdbcType="BIGINT" property="createUserId"/>
-        <result column="update_user_id" jdbcType="BIGINT" property="updateUserId"/>
-        <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
-        <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
-        <result column="deleted_flag" jdbcType="INTEGER" property="deletedFlag"/>
-    </resultMap>
-
-    <insert id="insertList">
-        INSERT INTO sys_user_web
-            (user_id,
-            login_name,
-            password,
-            show_name,
-            head_image_url,
-            user_status,
-            email,
-            mobile,
-            create_user_id,
-            update_user_id,
-            create_time,
-            update_time,
-            deleted_flag)
-        VALUES
-        <foreach collection="list" index="index" item="item" separator="," >
-            (#{item.userId},
-            #{item.loginName},
-            #{item.password},
-            #{item.showName},
-            #{item.headImageUrl},
-            #{item.userStatus},
-            #{item.email},
-            #{item.mobile},
-            #{item.createUserId},
-            #{item.updateUserId},
-            #{item.createTime},
-            #{item.updateTime},
-            #{item.deletedFlag})
-        </foreach>
-    </insert>
-
-    <!-- 如果有逻辑删除字段过滤,请写到这里 -->
-    <sql id="filterRef">
-        <!-- 这里必须加上全包名,否则当filterRef被其他Mapper.xml包含引用的时候,就会调用Mapper.xml中的该SQL片段 -->
-        <include refid="com.tourism.webadmin.upms.dao.SysUserMapper.inputFilterRef"/>
-        AND sys_user.deleted_flag = ${@com.tourism.common.core.constant.GlobalDeletedFlag@NORMAL}
-    </sql>
-
-    <!-- 这里仅包含调用接口输入的主表过滤条件 -->
-    <sql id="inputFilterRef">
-        <if test="sysUserFilter != null">
-            <if test="sysUserFilter.loginName != null and sysUserFilter.loginName != ''">
-                <bind name = "safeSysUserLoginName" value = "'%' + sysUserFilter.loginName + '%'" />
-                AND sys_user.login_name LIKE #{safeSysUserLoginName}
-            </if>
-            <if test="sysUserFilter.deptId != null">
-                AND (EXISTS (SELECT 1 FROM sys_dept_relation WHERE
-                        sys_dept_relation.parent_dept_id = #{sysUserFilter.deptId}
-                        AND sys_user.dept_id = sys_dept_relation.dept_id))
-            </if>
-            <if test="sysUserFilter.showName != null and sysUserFilter.showName != ''">
-                <bind name = "safeSysUserShowName" value = "'%' + sysUserFilter.showName + '%'" />
-                AND sys_user.show_name LIKE #{safeSysUserShowName}
-            </if>
-            <if test="sysUserFilter.userStatus != null">
-                AND sys_user.user_status = #{sysUserFilter.userStatus}
-            </if>
-            <if test="sysUserFilter.createTimeStart != null and sysUserFilter.createTimeStart != ''">
-                AND sys_user.create_time &gt;= #{sysUserFilter.createTimeStart}
-            </if>
-            <if test="sysUserFilter.createTimeEnd != null and sysUserFilter.createTimeEnd != ''">
-                AND sys_user.create_time &lt;= #{sysUserFilter.createTimeEnd}
-            </if>
-        </if>
-    </sql>
-
-    <select id="getSysUserList" resultMap="BaseResultMap" parameterType="com.tourism.webadmin.upms.model.SysUser">
-        SELECT * FROM sys_user
-        <where>
-            <include refid="filterRef"/>
-        </where>
-        <if test="orderBy != null and orderBy != ''">
-            ORDER BY ${orderBy}
-        </if>
-    </select>
-
-    <select id="getSysUserListByDeptIds" resultMap="BaseResultMap">
-        SELECT * FROM sys_user
-        <where>
-            <if test="deptIds != null">
-                AND (EXISTS (SELECT 1 FROM sys_dept_relation WHERE
-                        sys_dept_relation.parent_dept_id IN
-                        <foreach collection="deptIds" item="item" separator="," open="(" close=")">
-                            #{item}
-                        </foreach>
-                        AND sys_user.dept_id = sys_dept_relation.dept_id))
-            </if>
-            <include refid="filterRef"/>
-        </where>
-        <if test="orderBy != null and orderBy != ''">
-            ORDER BY ${orderBy}
-        </if>
-    </select>
-
-    <select id="getSysUserListByLoginNames" resultMap="BaseResultMap">
-        SELECT * FROM sys_user
-        <where>
-            <if test="loginNames != null">
-                AND sys_user.login_name IN
-                <foreach collection="loginNames" item="item" separator="," open="(" close=")">
-                    #{item}
-                </foreach>
-            </if>
-            <include refid="filterRef"/>
-        </where>
-        <if test="orderBy != null and orderBy != ''">
-            ORDER BY ${orderBy}
-        </if>
-    </select>
-
-    <select id="getUserIdListByRoleIds" resultType="java.lang.Long">
-        SELECT
-            DISTINCT sys_user.user_id
-        FROM
-            sys_user_role,
-            sys_user
-        <where>
-            AND sys_user_role.role_id IN
-            <foreach collection="roleIds" item="item" separator="," open="(" close=")">
-                #{item}
-            </foreach>
-            AND sys_user_role.user_id = sys_user.user_id
-            <include refid="filterRef"/>
-        </where>
-        <if test="orderBy != null and orderBy != ''">
-            ORDER BY ${orderBy}
-        </if>
-    </select>
-
-    <select id="getSysUserListByRoleId" resultMap="BaseResultMap">
-        SELECT
-            sys_user.*
-        FROM
-            sys_user_role,
-            sys_user
-        <where>
-            AND sys_user_role.role_id = #{roleId}
-            AND sys_user_role.user_id = sys_user.user_id
-            <include refid="filterRef"/>
-        </where>
-        <if test="orderBy != null and orderBy != ''">
-            ORDER BY ${orderBy}
-        </if>
-    </select>
-
-    <select id="getNotInSysUserListByRoleId" resultMap="BaseResultMap">
-        SELECT * FROM sys_user
-        <where>
-            NOT EXISTS (SELECT * FROM sys_user_role
-                WHERE sys_user_role.role_id = #{roleId} AND sys_user_role.user_id = sys_user.user_id)
-            <include refid="filterRef"/>
-        </where>
-        <if test="orderBy != null and orderBy != ''">
-            ORDER BY ${orderBy}
-        </if>
-    </select>
-
-    <select id="getSysUserListByDataPermId" resultMap="BaseResultMap">
-        SELECT
-            sys_user.*
-        FROM
-            sys_data_perm_user,
-            sys_user
-        <where>
-            AND sys_data_perm_user.data_perm_id = #{dataPermId}
-            AND sys_data_perm_user.user_id = sys_user.user_id
-            <include refid="filterRef"/>
-        </where>
-        <if test="orderBy != null and orderBy != ''">
-            ORDER BY ${orderBy}
-        </if>
-    </select>
-
-    <select id="getNotInSysUserListByDataPermId" resultMap="BaseResultMap">
-        SELECT * FROM sys_user
-        <where>
-            NOT EXISTS (SELECT * FROM sys_data_perm_user
-                WHERE sys_data_perm_user.data_perm_id = #{dataPermId} AND sys_data_perm_user.user_id = sys_user.user_id)
-            <include refid="filterRef"/>
-        </where>
-        <if test="orderBy != null and orderBy != ''">
-            ORDER BY ${orderBy}
-        </if>
-    </select>
-
-    <select id="getUserIdListByDeptPostIds" resultType="java.lang.Long">
-        SELECT
-            DISTINCT sys_user.user_id
-        FROM
-            sys_user_post,
-            sys_user
-        <where>
-            AND sys_user_post.dept_post_id IN
-            <foreach collection="deptPostIds" item="item" separator="," open="(" close=")">
-                #{item}
-            </foreach>
-            AND sys_user_post.user_id = sys_user.user_id
-            <include refid="filterRef"/>
-        </where>
-        <if test="orderBy != null and orderBy != ''">
-            ORDER BY ${orderBy}
-        </if>
-    </select>
-
-    <select id="getSysUserListByDeptPostId" resultMap="BaseResultMap">
-        SELECT
-            sys_user.*
-        FROM
-            sys_user_post,
-            sys_user
-        <where>
-            AND sys_user_post.dept_post_id = #{deptPostId}
-            AND sys_user_post.user_id = sys_user.user_id
-            <include refid="filterRef"/>
-        </where>
-        <if test="orderBy != null and orderBy != ''">
-            ORDER BY ${orderBy}
-        </if>
-    </select>
-
-    <select id="getNotInSysUserListByDeptPostId" resultMap="BaseResultMap">
-        SELECT * FROM sys_user
-        <where>
-            NOT EXISTS (SELECT * FROM sys_user_post
-                WHERE sys_user_post.dept_post_id = #{deptPostId} AND sys_user_post.user_id = sys_user.user_id)
-            <include refid="filterRef"/>
-        </where>
-        <if test="orderBy != null and orderBy != ''">
-            ORDER BY ${orderBy}
-        </if>
-    </select>
-
-    <select id="getUserIdListByPostIds" resultType="java.lang.Long">
-        SELECT
-            DISTINCT sys_user.user_id
-        FROM
-            sys_user_post,
-            sys_user
-        <where>
-            AND sys_user_post.post_id IN
-            <foreach collection="postIds" item="item" separator="," open="(" close=")">
-                #{item}
-            </foreach>
-            AND sys_user_post.user_id = sys_user.user_id
-            <include refid="filterRef"/>
-        </where>
-        <if test="orderBy != null and orderBy != ''">
-            ORDER BY ${orderBy}
-        </if>
-    </select>
-
-    <select id="getSysUserListByPostId" resultMap="BaseResultMap">
-        SELECT
-            sys_user.*
-        FROM
-            sys_user_post,
-            sys_user
-        <where>
-            AND sys_user_post.post_id = #{postId}
-            AND sys_user_post.user_id = sys_user.user_id
-            <include refid="filterRef"/>
-        </where>
-        <if test="orderBy != null and orderBy != ''">
-            ORDER BY ${orderBy}
-        </if>
-    </select>
-</mapper>

+ 0 - 68
application-webadmin/src/main/java/com/tourism/webadmin/back/dto/SysUserWebDto.java

@@ -1,68 +0,0 @@
-package com.tourism.webadmin.back.dto;
-
-import com.tourism.common.core.validator.AddGroup;
-import com.tourism.common.core.validator.UpdateGroup;
-import com.tourism.common.core.validator.ConstDictRef;
-import com.tourism.webadmin.upms.model.constant.SysUserType;
-import com.tourism.webadmin.upms.model.constant.SysUserStatus;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-
-import jakarta.validation.constraints.*;
-
-/**
- * 用户管理Dto对象。
- *
- * @author 吃饭睡觉
- * @date 2024-09-06
- */
-@Schema(description = "SysUserWebDto对象")
-@Data
-public class SysUserWebDto {
-
-    /**
-     * 登录用户名。
-     */
-    @NotBlank(message = "数据验证失败,登录用户名不能为空!")
-    private String loginName;
-
-    /**
-     * 用户密码。
-     */
-    @NotBlank(message = "数据验证失败,用户密码不能为空!", groups = {AddGroup.class})
-    private String password;
-
-
-    /**
-     * 用户显示名称。
-     */
-    @NotBlank(message = "数据验证失败,用户显示名称不能为空!")
-    private String showName;
-
-    /**
-     * 用户头像的Url。
-     */
-    @Schema(description = "用户头像的Url。")
-    private String headImageUrl;
-
-    /**
-     * 用户邮箱。
-     */
-    @Schema(description = "用户邮箱。")
-    private String email;
-
-    /**
-     * 用户手机。
-     */
-    @Schema(description = "用户手机。")
-    @NotBlank(message = "数据验证失败,用户手机不能为空!")
-    private String mobile;
-
-    /**
-     * 短信验证码。
-     */
-    @Schema(description = "短信验证码。")
-    @NotBlank(message = "数据验证失败,短信验证码不能为空!")
-    private String smsCode;
-}

+ 0 - 39
application-webadmin/src/main/java/com/tourism/webadmin/back/service/SysUserWebService.java

@@ -1,39 +0,0 @@
-package com.tourism.webadmin.back.service;
-
-import com.tourism.common.core.base.service.IBaseService;
-import com.tourism.webadmin.back.model.SysUserWeb;
-
-/**
- * 用户管理数据操作服务接口。
- *
- * @author 吃饭睡觉
- * @date 2024-09-06
- */
-public interface SysUserWebService extends IBaseService<SysUserWeb, Long> {
-
-    /**
-     * 获取指定登录名的用户对象。
-     *
-     * @param loginName 指定登录用户名。
-     * @return 用户对象。
-     */
-    SysUserWeb getSysUserByLoginName(String loginName);
-
-
-    SysUserWeb getSysUserByMobile(String mobile);
-
-    /**
-     * 保存新增的用户对象。
-     *
-     * @param user          新增的用户对象。
-     * @return 新增后的用户对象。
-     */
-    SysUserWeb saveNew(SysUserWeb user);
-    /**
-     * 修改用户密码。
-     * @param userId  用户主键Id。
-     * @param newPass 新密码。
-     * @return 成功返回true,否则false。
-     */
-    boolean changePassword(Long userId, String newPass);
-}

+ 0 - 94
application-webadmin/src/main/java/com/tourism/webadmin/back/service/impl/SysUserWebServiceImpl.java

@@ -1,94 +0,0 @@
-package com.tourism.webadmin.back.service.impl;
-
-import com.baomidou.mybatisplus.core.conditions.query.*;
-import com.tourism.webadmin.back.dao.SysUserWebMapper;
-import com.tourism.webadmin.back.model.SysUserWeb;
-import com.tourism.webadmin.back.service.SysUserWebService;
-import com.tourism.webadmin.upms.model.constant.SysUserStatus;
-import com.tourism.common.ext.base.BizWidgetDatasource;
-import com.tourism.common.core.base.dao.BaseDaoMapper;
-import com.tourism.common.core.constant.GlobalDeletedFlag;
-import com.tourism.common.core.object.*;
-import com.tourism.common.core.base.service.BaseService;
-import com.tourism.common.sequence.wrapper.IdGeneratorWrapper;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.transaction.annotation.Transactional;
-import org.springframework.security.crypto.password.PasswordEncoder;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-
-import java.util.*;
-
-/**
- * 用户管理数据操作服务类。
- *
- * @author 吃饭睡觉
- * @date 2024-09-06
- */
-@Slf4j
-@Service("sysUserWebService")
-public class SysUserWebServiceImpl extends BaseService<SysUserWeb, Long> implements SysUserWebService, BizWidgetDatasource {
-
-    @Autowired
-    private SysUserWebMapper sysUserWebMapper;
-    @Autowired
-    private IdGeneratorWrapper idGenerator;
-    @Autowired
-    private PasswordEncoder passwordEncoder;
-    /**
-     * 获取指定登录名的用户对象。
-     *
-     * @param loginName 指定登录用户名。
-     * @return 用户对象。
-     */
-    @Override
-    public SysUserWeb getSysUserByLoginName(String loginName) {
-        SysUserWeb filter = new SysUserWeb();
-        filter.setLoginName(loginName);
-        return sysUserWebMapper.selectOne(new QueryWrapper<>(filter));
-    }
-
-    @Override
-    public SysUserWeb getSysUserByMobile(String mobile) {
-        SysUserWeb filter = new SysUserWeb();
-        filter.setMobile(mobile);
-        return sysUserWebMapper.selectOne(new QueryWrapper<>(filter));
-    }
-
-    @Override
-    public SysUserWeb saveNew(SysUserWeb user) {
-        user.setUserId(idGenerator.nextLongId());
-        user.setPassword(user.getPassword());
-        user.setUserStatus(SysUserStatus.STATUS_NORMAL);
-        user.setDeletedFlag(GlobalDeletedFlag.NORMAL);
-        user.setCreateUserId(user.getUserId());
-        user.setCreateTime(new Date());
-        user.setUpdateUserId(user.getUserId());
-        user.setUpdateTime(new Date());
-        sysUserWebMapper.insert(user);
-        return user;
-    }
-
-    @Transactional(rollbackFor = Exception.class)
-    @Override
-    public boolean changePassword(Long userId, String newPass) {
-        SysUserWeb updatedUser = new SysUserWeb();
-        updatedUser.setUserId(userId);
-        updatedUser.setPassword(passwordEncoder.encode(newPass));
-        return sysUserWebMapper.updateById(updatedUser) == 1;
-    }
-    @Override
-    protected BaseDaoMapper<SysUserWeb> mapper() {
-        return null;
-    }
-
-    @Override
-    public MyPageData<Map<String, Object>> getDataList(String widgetType, Map<String, Object> filter, MyOrderParam orderParam, MyPageParam pageParam) {
-        return null;
-    }
-
-    @Override
-    public List<Map<String, Object>> getDataListWithInList(String widgetType, String fieldName, List<String> fieldValues) {
-        return null;
-    }
-}

+ 65 - 0
application-webadmin/src/main/java/com/tourism/webadmin/config/KaptchaConfig.java

@@ -0,0 +1,65 @@
+package com.tourism.webadmin.config;
+
+import com.google.code.kaptcha.Producer;
+import com.google.code.kaptcha.impl.DefaultKaptcha;
+import com.google.code.kaptcha.util.Config;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.Properties;
+
+/**
+ * 谷歌提供的图片验证码kaptcha
+ *
+ * @author 狐狸半面添
+ * @create 2023-01-29 11:02
+ */
+@Configuration
+public class KaptchaConfig {
+    @Bean
+    public Producer kaptcha() {
+        Properties properties = new Properties();
+        /*
+            设置图片有边框并且为蓝色
+            properties.setProperty("kaptcha.border", "no");
+            properties.setProperty("kaptcha.border.color", "blue");
+         */
+
+        // 设置图片无边框
+        properties.setProperty("kaptcha.border", "no");
+
+        // 背景颜色渐变开始,这里设置的是rgb值156,156,156
+        properties.put("kaptcha.background.clear.from", "156,156,156");
+        // 背景颜色渐变结束,这里设置以白色结束
+        properties.put("kaptcha.background.clear.to", "white");
+        // 字体颜色,这里设置为黑色
+        properties.put("kaptcha.textproducer.font.color", "black");
+        // 文字间隔,这里设置为10px
+        properties.put("kaptcha.textproducer.char.space", "10");
+
+        /*
+            如果需要去掉干扰线,则如此配置:
+            properties.put("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise");
+         */
+
+        // 干扰线颜色配置,这里设置成了idea的Darcula主题的背景色
+        properties.put("kaptcha.noise.color", "43,43,43");
+
+        // 字体
+        properties.put("kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑");
+        // 图片宽度,默认也是200px
+        properties.setProperty("kaptcha.image.width", "200");
+        // 图片高度,默认也是50px
+        properties.setProperty("kaptcha.image.height", "50");
+        // 从哪些字符中产生
+        properties.setProperty("kaptcha.textproducer.char.string", "0123456789abcdefghijklmnopqrsduvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
+        // 字符个数
+        properties.setProperty("kaptcha.textproducer.char.length", "5");
+
+        Config config = new Config(properties);
+        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
+        defaultKaptcha.setConfig(config);
+        return defaultKaptcha;
+    }
+}
+

+ 56 - 0
common/common-additional/pom.xml

@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>com.tourism</groupId>
+        <artifactId>common</artifactId>
+        <version>1.0.0</version>
+    </parent>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>7</source>
+                    <target>7</target>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>common-additional</artifactId>
+    <version>1.0.0</version>
+    <name>common-additional</name>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <!-- 常用工具 -->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>${guava.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.data</groupId>
+            <artifactId>spring-data-redis</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-api</artifactId>
+            <version>0.12.3</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.google.code.kaptcha</groupId>
+            <artifactId>kaptcha</artifactId>
+            <version>2.3</version>
+            <scope>compile</scope>
+        </dependency>
+    </dependencies>
+</project>

+ 87 - 0
common/common-additional/src/main/java/com/tourism/common/additional/config/CaptchaConfig.java

@@ -0,0 +1,87 @@
+package com.tourism.common.additional.config;
+
+import com.google.code.kaptcha.impl.DefaultKaptcha;
+import com.google.code.kaptcha.util.Config;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import java.util.Properties;
+
+import static com.google.code.kaptcha.Constants.*;
+
+
+/**
+ * 验证码配置
+ * 
+ * @author tourism
+ */
+@Configuration
+public class CaptchaConfig
+{
+
+    public static final String KAPTCHA_TEXTPRODUCER_CHAR_SPACE = "kaptcha.textproducer.char.space";
+    @Bean(name = "captchaProducer")
+    public DefaultKaptcha getKaptchaBean()
+    {
+        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
+        Properties properties = new Properties();
+        // 是否有边框 默认为true 我们可以自己设置yes,no
+        properties.setProperty(KAPTCHA_BORDER, "yes");
+        // 验证码文本字符颜色 默认为Color.BLACK
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black");
+        // 验证码图片宽度 默认为200
+        properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
+        // 验证码图片高度 默认为50
+        properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
+        // 验证码文本字符大小 默认为40
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38");
+        // KAPTCHA_SESSION_KEY
+        properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");
+        // 验证码文本字符长度 默认为5
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
+        // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
+        // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
+        properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
+        Config config = new Config(properties);
+        defaultKaptcha.setConfig(config);
+        return defaultKaptcha;
+    }
+
+    @Bean(name = "captchaProducerMath")
+    public DefaultKaptcha getKaptchaBeanMath()
+    {
+        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
+        Properties properties = new Properties();
+        // 是否有边框 默认为true 我们可以自己设置yes,no
+        properties.setProperty(KAPTCHA_BORDER, "yes");
+        // 边框颜色 默认为Color.BLACK
+        properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90");
+        // 验证码文本字符颜色 默认为Color.BLACK
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
+        // 验证码图片宽度 默认为200
+        properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
+        // 验证码图片高度 默认为50
+        properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
+        // 验证码文本字符大小 默认为40
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35");
+        // KAPTCHA_SESSION_KEY
+        properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath");
+        // 验证码文本生成器
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.tourism.common.additional.config.KaptchaTextCreator");
+        // 验证码文本字符间距 默认为2
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3");
+        // 验证码文本字符长度 默认为5
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6");
+        // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
+        // 验证码噪点颜色 默认为Color.BLACK
+        properties.setProperty(KAPTCHA_NOISE_COLOR, "white");
+        // 干扰实现类
+        properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
+        // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
+        properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
+        Config config = new Config(properties);
+        defaultKaptcha.setConfig(config);
+        return defaultKaptcha;
+    }
+}

+ 69 - 0
common/common-additional/src/main/java/com/tourism/common/additional/config/KaptchaTextCreator.java

@@ -0,0 +1,69 @@
+package com.tourism.common.additional.config;
+
+import com.google.code.kaptcha.text.impl.DefaultTextCreator;
+
+import java.util.Random;
+
+/**
+ * 验证码文本生成器
+ *
+ * @author tourism
+ */
+public class KaptchaTextCreator extends DefaultTextCreator
+{
+    private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(",");
+
+    @Override
+    public String getText()
+    {
+        Integer result = 0;
+        Random random = new Random();
+        int x = random.nextInt(10);
+        int y = random.nextInt(10);
+        StringBuilder suChinese = new StringBuilder();
+        int randomoperands = random.nextInt(3);
+        if (randomoperands == 0)
+        {
+            result = x * y;
+            suChinese.append(CNUMBERS[x]);
+            suChinese.append("*");
+            suChinese.append(CNUMBERS[y]);
+        }
+        else if (randomoperands == 1)
+        {
+            if ((x != 0) && y % x == 0)
+            {
+                result = y / x;
+                suChinese.append(CNUMBERS[y]);
+                suChinese.append("/");
+                suChinese.append(CNUMBERS[x]);
+            }
+            else
+            {
+                result = x + y;
+                suChinese.append(CNUMBERS[x]);
+                suChinese.append("+");
+                suChinese.append(CNUMBERS[y]);
+            }
+        }
+        else
+        {
+            if (x >= y)
+            {
+                result = x - y;
+                suChinese.append(CNUMBERS[x]);
+                suChinese.append("-");
+                suChinese.append(CNUMBERS[y]);
+            }
+            else
+            {
+                result = y - x;
+                suChinese.append(CNUMBERS[y]);
+                suChinese.append("-");
+                suChinese.append(CNUMBERS[x]);
+            }
+        }
+        suChinese.append("=?@" + result);
+        return suChinese.toString();
+    }
+}

+ 44 - 0
common/common-additional/src/main/java/com/tourism/common/additional/constant/CacheConstants.java

@@ -0,0 +1,44 @@
+package com.tourism.common.additional.constant;
+
+/**
+ * 缓存的key 常量
+ * 
+ * @author tourism
+ */
+public class CacheConstants
+{
+    /**
+     * 登录用户 redis key
+     */
+    public static final String LOGIN_TOKEN_KEY = "login_tokens:";
+
+    /**
+     * 验证码 redis key
+     */
+    public static final String CAPTCHA_CODE_KEY = "captcha_codes:";
+
+    /**
+     * 参数管理 cache key
+     */
+    public static final String SYS_CONFIG_KEY = "sys_config:";
+
+    /**
+     * 字典管理 cache key
+     */
+    public static final String SYS_DICT_KEY = "sys_dict:";
+
+    /**
+     * 防重提交 redis key
+     */
+    public static final String REPEAT_SUBMIT_KEY = "repeat_submit:";
+
+    /**
+     * 限流 redis key
+     */
+    public static final String RATE_LIMIT_KEY = "rate_limit:";
+
+    /**
+     * 登录账户密码错误次数 redis key
+     */
+    public static final String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
+}

+ 164 - 0
common/common-additional/src/main/java/com/tourism/common/additional/constant/Constants.java

@@ -0,0 +1,164 @@
+package com.tourism.common.additional.constant;
+
+import io.jsonwebtoken.Claims;
+
+import java.util.Locale;
+
+/**
+ * 通用常量信息
+ * 
+ * @author
+ */
+public class Constants
+{
+    /**
+     * UTF-8 字符集
+     */
+    public static final String UTF8 = "UTF-8";
+
+    /**
+     * GBK 字符集
+     */
+    public static final String GBK = "GBK";
+
+    /**
+     * 系统语言
+     */
+    public static final Locale DEFAULT_LOCALE = Locale.SIMPLIFIED_CHINESE;
+
+    /**
+     * www主域
+     */
+    public static final String WWW = "www.";
+
+    /**
+     * http请求
+     */
+    public static final String HTTP = "http://";
+
+    /**
+     * https请求
+     */
+    public static final String HTTPS = "https://";
+
+    /**
+     * 通用成功标识
+     */
+    public static final String SUCCESS = "0";
+
+    /**
+     * 通用失败标识
+     */
+    public static final String FAIL = "1";
+
+    /**
+     * 登录成功
+     */
+    public static final String LOGIN_SUCCESS = "Success";
+
+    /**
+     * 注销
+     */
+    public static final String LOGOUT = "Logout";
+
+    /**
+     * 注册
+     */
+    public static final String REGISTER = "Register";
+
+    /**
+     * 登录失败
+     */
+    public static final String LOGIN_FAIL = "Error";
+
+    /**
+     * 所有权限标识
+     */
+    public static final String ALL_PERMISSION = "*:*:*";
+
+    /**
+     * 管理员角色权限标识
+     */
+    public static final String SUPER_ADMIN = "admin";
+
+    /**
+     * 角色权限分隔符
+     */
+    public static final String ROLE_DELIMETER = ",";
+
+    /**
+     * 权限标识分隔符
+     */
+    public static final String PERMISSION_DELIMETER = ",";
+
+    /**
+     * 验证码有效期(分钟)
+     */
+    public static final Integer CAPTCHA_EXPIRATION = 2;
+
+    /**
+     * 令牌
+     */
+    public static final String TOKEN = "token";
+
+    /**
+     * 令牌前缀
+     */
+    public static final String TOKEN_PREFIX = "Bearer ";
+
+    /**
+     * 令牌前缀
+     */
+    public static final String LOGIN_USER_KEY = "login_user_key";
+
+    /**
+     * 用户ID
+     */
+    public static final String JWT_USERID = "userid";
+
+    /**
+     * 用户名称
+     */
+    public static final String JWT_USERNAME = Claims.SUBJECT;
+
+    /**
+     * 用户头像
+     */
+    public static final String JWT_AVATAR = "avatar";
+
+    /**
+     * 创建时间
+     */
+    public static final String JWT_CREATED = "created";
+
+    /**
+     * 用户权限
+     */
+    public static final String JWT_AUTHORITIES = "authorities";
+
+    /**
+     * 资源映射路径 前缀
+     */
+    public static final String RESOURCE_PREFIX = "/profile";
+
+    /**
+     * RMI 远程方法调用
+     */
+    public static final String LOOKUP_RMI = "rmi:";
+
+    /**
+     * LDAP 远程方法调用
+     */
+    public static final String LOOKUP_LDAP = "ldap:";
+
+    /**
+     * LDAPS 远程方法调用
+     */
+    public static final String LOOKUP_LDAPS = "ldaps:";
+
+    /**
+     * 自动识别json对象白名单配置(仅允许解析的包名,范围越小越安全)
+     */
+    public static final String[] JSON_WHITELIST_STR = { "org.springframework", "com.tourism" };
+
+}

+ 94 - 0
common/common-additional/src/main/java/com/tourism/common/additional/constant/HttpStatus.java

@@ -0,0 +1,94 @@
+package com.tourism.common.additional.constant;
+
+/**
+ * 返回状态码
+ * 
+ * @author tourism
+ */
+public class HttpStatus
+{
+    /**
+     * 操作成功
+     */
+    public static final int SUCCESS = 200;
+
+    /**
+     * 对象创建成功
+     */
+    public static final int CREATED = 201;
+
+    /**
+     * 请求已经被接受
+     */
+    public static final int ACCEPTED = 202;
+
+    /**
+     * 操作已经执行成功,但是没有返回数据
+     */
+    public static final int NO_CONTENT = 204;
+
+    /**
+     * 资源已被移除
+     */
+    public static final int MOVED_PERM = 301;
+
+    /**
+     * 重定向
+     */
+    public static final int SEE_OTHER = 303;
+
+    /**
+     * 资源没有被修改
+     */
+    public static final int NOT_MODIFIED = 304;
+
+    /**
+     * 参数列表错误(缺少,格式不匹配)
+     */
+    public static final int BAD_REQUEST = 400;
+
+    /**
+     * 未授权
+     */
+    public static final int UNAUTHORIZED = 401;
+
+    /**
+     * 访问受限,授权过期
+     */
+    public static final int FORBIDDEN = 403;
+
+    /**
+     * 资源,服务未找到
+     */
+    public static final int NOT_FOUND = 404;
+
+    /**
+     * 不允许的http方法
+     */
+    public static final int BAD_METHOD = 405;
+
+    /**
+     * 资源冲突,或者资源被锁
+     */
+    public static final int CONFLICT = 409;
+
+    /**
+     * 不支持的数据,媒体类型
+     */
+    public static final int UNSUPPORTED_TYPE = 415;
+
+    /**
+     * 系统内部错误
+     */
+    public static final int ERROR = 500;
+
+    /**
+     * 接口未实现
+     */
+    public static final int NOT_IMPLEMENTED = 501;
+
+    /**
+     * 系统警告消息
+     */
+    public static final int WARN = 601;
+}

+ 26 - 0
common/common-additional/src/main/java/com/tourism/common/additional/exception/UtilException.java

@@ -0,0 +1,26 @@
+package com.tourism.common.additional.exception;
+
+/**
+ * 工具类异常
+ * 
+ * @author tourism
+ */
+public class UtilException extends RuntimeException
+{
+    private static final long serialVersionUID = 8247610319171014183L;
+
+    public UtilException(Throwable e)
+    {
+        super(e.getMessage(), e);
+    }
+
+    public UtilException(String message)
+    {
+        super(message);
+    }
+
+    public UtilException(String message, Throwable throwable)
+    {
+        super(message, throwable);
+    }
+}

+ 217 - 0
common/common-additional/src/main/java/com/tourism/common/additional/model/AjaxResult.java

@@ -0,0 +1,217 @@
+package com.tourism.common.additional.model;
+
+import com.tourism.common.additional.constant.HttpStatus;
+import com.tourism.common.additional.utils.StringUtils;
+
+import java.util.HashMap;
+import java.util.Objects;
+
+/**
+ * 操作消息提醒
+ *
+ * @author tourism
+ */
+public class AjaxResult extends HashMap<String, Object>
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 状态码 */
+    public static final String CODE_TAG = "code";
+
+    /** 返回内容 */
+    public static final String MSG_TAG = "msg";
+
+    /** 数据对象 */
+    public static final String DATA_TAG = "data";
+
+    /**
+     * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
+     */
+    public AjaxResult()
+    {
+    }
+
+    /**
+     * 初始化一个新创建的 AjaxResult 对象
+     *
+     * @param code 状态码
+     * @param msg 返回内容
+     */
+    public AjaxResult(int code, String msg)
+    {
+        super.put(CODE_TAG, code);
+        super.put(MSG_TAG, msg);
+    }
+
+    /**
+     * 初始化一个新创建的 AjaxResult 对象
+     *
+     * @param code 状态码
+     * @param msg 返回内容
+     * @param data 数据对象
+     */
+    public AjaxResult(int code, String msg, Object data)
+    {
+        super.put(CODE_TAG, code);
+        super.put(MSG_TAG, msg);
+        if (StringUtils.isNotNull(data))
+        {
+            super.put(DATA_TAG, data);
+        }
+    }
+
+    /**
+     * 返回成功消息
+     *
+     * @return 成功消息
+     */
+    public static AjaxResult success()
+    {
+        return AjaxResult.success("操作成功");
+    }
+
+    /**
+     * 返回成功数据
+     *
+     * @return 成功消息
+     */
+    public static AjaxResult success(Object data)
+    {
+        return AjaxResult.success("操作成功", data);
+    }
+
+    /**
+     * 返回成功消息
+     *
+     * @param msg 返回内容
+     * @return 成功消息
+     */
+    public static AjaxResult success(String msg)
+    {
+        return AjaxResult.success(msg, null);
+    }
+
+    /**
+     * 返回成功消息
+     *
+     * @param msg 返回内容
+     * @param data 数据对象
+     * @return 成功消息
+     */
+    public static AjaxResult success(String msg, Object data)
+    {
+        return new AjaxResult(HttpStatus.SUCCESS, msg, data);
+    }
+
+    /**
+     * 返回警告消息
+     *
+     * @param msg 返回内容
+     * @return 警告消息
+     */
+    public static AjaxResult warn(String msg)
+    {
+        return AjaxResult.warn(msg, null);
+    }
+
+    /**
+     * 返回警告消息
+     *
+     * @param msg 返回内容
+     * @param data 数据对象
+     * @return 警告消息
+     */
+    public static AjaxResult warn(String msg, Object data)
+    {
+        return new AjaxResult(HttpStatus.WARN, msg, data);
+    }
+
+    /**
+     * 返回错误消息
+     *
+     * @return 错误消息
+     */
+    public static AjaxResult error()
+    {
+        return AjaxResult.error("操作失败");
+    }
+
+    /**
+     * 返回错误消息
+     *
+     * @param msg 返回内容
+     * @return 错误消息
+     */
+    public static AjaxResult error(String msg)
+    {
+        return AjaxResult.error(msg, null);
+    }
+
+    /**
+     * 返回错误消息
+     *
+     * @param msg 返回内容
+     * @param data 数据对象
+     * @return 错误消息
+     */
+    public static AjaxResult error(String msg, Object data)
+    {
+        return new AjaxResult(HttpStatus.ERROR, msg, data);
+    }
+
+    /**
+     * 返回错误消息
+     *
+     * @param code 状态码
+     * @param msg 返回内容
+     * @return 错误消息
+     */
+    public static AjaxResult error(int code, String msg)
+    {
+        return new AjaxResult(code, msg, null);
+    }
+
+    /**
+     * 是否为成功消息
+     *
+     * @return 结果
+     */
+    public boolean isSuccess()
+    {
+        return Objects.equals(HttpStatus.SUCCESS, this.get(CODE_TAG));
+    }
+
+    /**
+     * 是否为警告消息
+     *
+     * @return 结果
+     */
+    public boolean isWarn()
+    {
+        return Objects.equals(HttpStatus.WARN, this.get(CODE_TAG));
+    }
+
+    /**
+     * 是否为错误消息
+     *
+     * @return 结果
+     */
+    public boolean isError()
+    {
+        return Objects.equals(HttpStatus.ERROR, this.get(CODE_TAG));
+    }
+
+    /**
+     * 方便链式调用
+     *
+     * @param key 键
+     * @param value 值
+     * @return 数据对象
+     */
+    @Override
+    public AjaxResult put(String key, Object value)
+    {
+        super.put(key, value);
+        return this;
+    }
+}

+ 268 - 0
common/common-additional/src/main/java/com/tourism/common/additional/redis/RedisCache.java

@@ -0,0 +1,268 @@
+package com.tourism.common.additional.redis;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.BoundSetOperations;
+import org.springframework.data.redis.core.HashOperations;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.stereotype.Component;
+
+/**
+ * spring redis 工具类
+ *
+ * @author tourism
+ **/
+@SuppressWarnings(value = { "unchecked", "rawtypes" })
+@Component
+public class RedisCache
+{
+    @Autowired
+    public RedisTemplate redisTemplate;
+
+    /**
+     * 缓存基本的对象,Integer、String、实体类等
+     *
+     * @param key 缓存的键值
+     * @param value 缓存的值
+     */
+    public <T> void setCacheObject(final String key, final T value)
+    {
+        redisTemplate.opsForValue().set(key, value);
+    }
+
+    /**
+     * 缓存基本的对象,Integer、String、实体类等
+     *
+     * @param key 缓存的键值
+     * @param value 缓存的值
+     * @param timeout 时间
+     * @param timeUnit 时间颗粒度
+     */
+    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
+    {
+        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
+    }
+
+    /**
+     * 设置有效时间
+     *
+     * @param key Redis键
+     * @param timeout 超时时间
+     * @return true=设置成功;false=设置失败
+     */
+    public boolean expire(final String key, final long timeout)
+    {
+        return expire(key, timeout, TimeUnit.SECONDS);
+    }
+
+    /**
+     * 设置有效时间
+     *
+     * @param key Redis键
+     * @param timeout 超时时间
+     * @param unit 时间单位
+     * @return true=设置成功;false=设置失败
+     */
+    public boolean expire(final String key, final long timeout, final TimeUnit unit)
+    {
+        return redisTemplate.expire(key, timeout, unit);
+    }
+
+    /**
+     * 获取有效时间
+     *
+     * @param key Redis键
+     * @return 有效时间
+     */
+    public long getExpire(final String key)
+    {
+        return redisTemplate.getExpire(key);
+    }
+
+    /**
+     * 判断 key是否存在
+     *
+     * @param key 键
+     * @return true 存在 false不存在
+     */
+    public Boolean hasKey(String key)
+    {
+        return redisTemplate.hasKey(key);
+    }
+
+    /**
+     * 获得缓存的基本对象。
+     *
+     * @param key 缓存键值
+     * @return 缓存键值对应的数据
+     */
+    public <T> T getCacheObject(final String key)
+    {
+        ValueOperations<String, T> operation = redisTemplate.opsForValue();
+        return operation.get(key);
+    }
+
+    /**
+     * 删除单个对象
+     *
+     * @param key
+     */
+    public boolean deleteObject(final String key)
+    {
+        return redisTemplate.delete(key);
+    }
+
+    /**
+     * 删除集合对象
+     *
+     * @param collection 多个对象
+     * @return
+     */
+    public boolean deleteObject(final Collection collection)
+    {
+        return redisTemplate.delete(collection) > 0;
+    }
+
+    /**
+     * 缓存List数据
+     *
+     * @param key 缓存的键值
+     * @param dataList 待缓存的List数据
+     * @return 缓存的对象
+     */
+    public <T> long setCacheList(final String key, final List<T> dataList)
+    {
+        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
+        return count == null ? 0 : count;
+    }
+
+    /**
+     * 获得缓存的list对象
+     *
+     * @param key 缓存的键值
+     * @return 缓存键值对应的数据
+     */
+    public <T> List<T> getCacheList(final String key)
+    {
+        return redisTemplate.opsForList().range(key, 0, -1);
+    }
+
+    /**
+     * 缓存Set
+     *
+     * @param key 缓存键值
+     * @param dataSet 缓存的数据
+     * @return 缓存数据的对象
+     */
+    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
+    {
+        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
+        Iterator<T> it = dataSet.iterator();
+        while (it.hasNext())
+        {
+            setOperation.add(it.next());
+        }
+        return setOperation;
+    }
+
+    /**
+     * 获得缓存的set
+     *
+     * @param key
+     * @return
+     */
+    public <T> Set<T> getCacheSet(final String key)
+    {
+        return redisTemplate.opsForSet().members(key);
+    }
+
+    /**
+     * 缓存Map
+     *
+     * @param key
+     * @param dataMap
+     */
+    public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
+    {
+        if (dataMap != null) {
+            redisTemplate.opsForHash().putAll(key, dataMap);
+        }
+    }
+
+    /**
+     * 获得缓存的Map
+     *
+     * @param key
+     * @return
+     */
+    public <T> Map<String, T> getCacheMap(final String key)
+    {
+        return redisTemplate.opsForHash().entries(key);
+    }
+
+    /**
+     * 往Hash中存入数据
+     *
+     * @param key Redis键
+     * @param hKey Hash键
+     * @param value 值
+     */
+    public <T> void setCacheMapValue(final String key, final String hKey, final T value)
+    {
+        redisTemplate.opsForHash().put(key, hKey, value);
+    }
+
+    /**
+     * 获取Hash中的数据
+     *
+     * @param key Redis键
+     * @param hKey Hash键
+     * @return Hash中的对象
+     */
+    public <T> T getCacheMapValue(final String key, final String hKey)
+    {
+        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
+        return opsForHash.get(key, hKey);
+    }
+
+    /**
+     * 获取多个Hash中的数据
+     *
+     * @param key Redis键
+     * @param hKeys Hash键集合
+     * @return Hash对象集合
+     */
+    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
+    {
+        return redisTemplate.opsForHash().multiGet(key, hKeys);
+    }
+
+    /**
+     * 删除Hash中的某条数据
+     *
+     * @param key Redis键
+     * @param hKey Hash键
+     * @return 是否成功
+     */
+    public boolean deleteCacheMapValue(final String key, final String hKey)
+    {
+        return redisTemplate.opsForHash().delete(key, hKey) > 0;
+    }
+
+    /**
+     * 获得缓存的基本对象列表
+     *
+     * @param pattern 字符串前缀
+     * @return 对象列表
+     */
+    public Collection<String> keys(final String pattern)
+    {
+        return redisTemplate.keys(pattern);
+    }
+}

+ 291 - 0
common/common-additional/src/main/java/com/tourism/common/additional/sign/Base64.java

@@ -0,0 +1,291 @@
+package com.tourism.common.additional.sign;
+
+/**
+ * Base64工具类
+ * 
+ * @author tourism
+ */
+public final class Base64
+{
+    static private final int     BASELENGTH           = 128;
+    static private final int     LOOKUPLENGTH         = 64;
+    static private final int     TWENTYFOURBITGROUP   = 24;
+    static private final int     EIGHTBIT             = 8;
+    static private final int     SIXTEENBIT           = 16;
+    static private final int     FOURBYTE             = 4;
+    static private final int     SIGN                 = -128;
+    static private final char    PAD                  = '=';
+    static final private byte[]  base64Alphabet       = new byte[BASELENGTH];
+    static final private char[]  lookUpBase64Alphabet = new char[LOOKUPLENGTH];
+
+    static
+    {
+        for (int i = 0; i < BASELENGTH; ++i)
+        {
+            base64Alphabet[i] = -1;
+        }
+        for (int i = 'Z'; i >= 'A'; i--)
+        {
+            base64Alphabet[i] = (byte) (i - 'A');
+        }
+        for (int i = 'z'; i >= 'a'; i--)
+        {
+            base64Alphabet[i] = (byte) (i - 'a' + 26);
+        }
+
+        for (int i = '9'; i >= '0'; i--)
+        {
+            base64Alphabet[i] = (byte) (i - '0' + 52);
+        }
+
+        base64Alphabet['+'] = 62;
+        base64Alphabet['/'] = 63;
+
+        for (int i = 0; i <= 25; i++)
+        {
+            lookUpBase64Alphabet[i] = (char) ('A' + i);
+        }
+
+        for (int i = 26, j = 0; i <= 51; i++, j++)
+        {
+            lookUpBase64Alphabet[i] = (char) ('a' + j);
+        }
+
+        for (int i = 52, j = 0; i <= 61; i++, j++)
+        {
+            lookUpBase64Alphabet[i] = (char) ('0' + j);
+        }
+        lookUpBase64Alphabet[62] = (char) '+';
+        lookUpBase64Alphabet[63] = (char) '/';
+    }
+
+    private static boolean isWhiteSpace(char octect)
+    {
+        return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9);
+    }
+
+    private static boolean isPad(char octect)
+    {
+        return (octect == PAD);
+    }
+
+    private static boolean isData(char octect)
+    {
+        return (octect < BASELENGTH && base64Alphabet[octect] != -1);
+    }
+
+    /**
+     * Encodes hex octects into Base64
+     *
+     * @param binaryData Array containing binaryData
+     * @return Encoded Base64 array
+     */
+    public static String encode(byte[] binaryData)
+    {
+        if (binaryData == null)
+        {
+            return null;
+        }
+
+        int lengthDataBits = binaryData.length * EIGHTBIT;
+        if (lengthDataBits == 0)
+        {
+            return "";
+        }
+
+        int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP;
+        int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP;
+        int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 : numberTriplets;
+        char encodedData[] = null;
+
+        encodedData = new char[numberQuartet * 4];
+
+        byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0;
+
+        int encodedIndex = 0;
+        int dataIndex = 0;
+
+        for (int i = 0; i < numberTriplets; i++)
+        {
+            b1 = binaryData[dataIndex++];
+            b2 = binaryData[dataIndex++];
+            b3 = binaryData[dataIndex++];
+
+            l = (byte) (b2 & 0x0f);
+            k = (byte) (b1 & 0x03);
+
+            byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
+            byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0);
+            byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc);
+
+            encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
+            encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
+            encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3];
+            encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f];
+        }
+
+        // form integral number of 6-bit groups
+        if (fewerThan24bits == EIGHTBIT)
+        {
+            b1 = binaryData[dataIndex];
+            k = (byte) (b1 & 0x03);
+            byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
+            encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
+            encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4];
+            encodedData[encodedIndex++] = PAD;
+            encodedData[encodedIndex++] = PAD;
+        }
+        else if (fewerThan24bits == SIXTEENBIT)
+        {
+            b1 = binaryData[dataIndex];
+            b2 = binaryData[dataIndex + 1];
+            l = (byte) (b2 & 0x0f);
+            k = (byte) (b1 & 0x03);
+
+            byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
+            byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0);
+
+            encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
+            encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
+            encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2];
+            encodedData[encodedIndex++] = PAD;
+        }
+        return new String(encodedData);
+    }
+
+    /**
+     * Decodes Base64 data into octects
+     *
+     * @param encoded string containing Base64 data
+     * @return Array containind decoded data.
+     */
+    public static byte[] decode(String encoded)
+    {
+        if (encoded == null)
+        {
+            return null;
+        }
+
+        char[] base64Data = encoded.toCharArray();
+        // remove white spaces
+        int len = removeWhiteSpace(base64Data);
+
+        if (len % FOURBYTE != 0)
+        {
+            return null;// should be divisible by four
+        }
+
+        int numberQuadruple = (len / FOURBYTE);
+
+        if (numberQuadruple == 0)
+        {
+            return new byte[0];
+        }
+
+        byte decodedData[] = null;
+        byte b1 = 0, b2 = 0, b3 = 0, b4 = 0;
+        char d1 = 0, d2 = 0, d3 = 0, d4 = 0;
+
+        int i = 0;
+        int encodedIndex = 0;
+        int dataIndex = 0;
+        decodedData = new byte[(numberQuadruple) * 3];
+
+        for (; i < numberQuadruple - 1; i++)
+        {
+
+            if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))
+                    || !isData((d3 = base64Data[dataIndex++])) || !isData((d4 = base64Data[dataIndex++])))
+            {
+                return null;
+            } // if found "no data" just return null
+
+            b1 = base64Alphabet[d1];
+            b2 = base64Alphabet[d2];
+            b3 = base64Alphabet[d3];
+            b4 = base64Alphabet[d4];
+
+            decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
+            decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
+            decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
+        }
+
+        if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++])))
+        {
+            return null;// if found "no data" just return null
+        }
+
+        b1 = base64Alphabet[d1];
+        b2 = base64Alphabet[d2];
+
+        d3 = base64Data[dataIndex++];
+        d4 = base64Data[dataIndex++];
+        if (!isData((d3)) || !isData((d4)))
+        {// Check if they are PAD characters
+            if (isPad(d3) && isPad(d4))
+            {
+                if ((b2 & 0xf) != 0)// last 4 bits should be zero
+                {
+                    return null;
+                }
+                byte[] tmp = new byte[i * 3 + 1];
+                System.arraycopy(decodedData, 0, tmp, 0, i * 3);
+                tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
+                return tmp;
+            }
+            else if (!isPad(d3) && isPad(d4))
+            {
+                b3 = base64Alphabet[d3];
+                if ((b3 & 0x3) != 0)// last 2 bits should be zero
+                {
+                    return null;
+                }
+                byte[] tmp = new byte[i * 3 + 2];
+                System.arraycopy(decodedData, 0, tmp, 0, i * 3);
+                tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
+                tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
+                return tmp;
+            }
+            else
+            {
+                return null;
+            }
+        }
+        else
+        { // No PAD e.g 3cQl
+            b3 = base64Alphabet[d3];
+            b4 = base64Alphabet[d4];
+            decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
+            decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
+            decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
+
+        }
+        return decodedData;
+    }
+
+    /**
+     * remove WhiteSpace from MIME containing encoded Base64 data.
+     *
+     * @param data the byte array of base64 data (with WS)
+     * @return the new length
+     */
+    private static int removeWhiteSpace(char[] data)
+    {
+        if (data == null)
+        {
+            return 0;
+        }
+
+        // count characters that's not whitespace
+        int newSize = 0;
+        int len = data.length;
+        for (int i = 0; i < len; i++)
+        {
+            if (!isWhiteSpace(data[i]))
+            {
+                data[newSize++] = data[i];
+            }
+        }
+        return newSize;
+    }
+}

+ 68 - 0
common/common-additional/src/main/java/com/tourism/common/additional/sign/Md5Utils.java

@@ -0,0 +1,68 @@
+package com.tourism.common.additional.sign;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+
+/**
+ * Md5加密方法
+ * 
+ * @author tourism
+ */
+public class Md5Utils
+{
+    private static final Logger log = LoggerFactory.getLogger(Md5Utils.class);
+
+    private static byte[] md5(String s)
+    {
+        MessageDigest algorithm;
+        try
+        {
+            algorithm = MessageDigest.getInstance("MD5");
+            algorithm.reset();
+            algorithm.update(s.getBytes("UTF-8"));
+            byte[] messageDigest = algorithm.digest();
+            return messageDigest;
+        }
+        catch (Exception e)
+        {
+            log.error("MD5 Error...", e);
+        }
+        return null;
+    }
+
+    private static final String toHex(byte hash[])
+    {
+        if (hash == null)
+        {
+            return null;
+        }
+        StringBuffer buf = new StringBuffer(hash.length * 2);
+        int i;
+
+        for (i = 0; i < hash.length; i++)
+        {
+            if ((hash[i] & 0xff) < 0x10)
+            {
+                buf.append("0");
+            }
+            buf.append(Long.toString(hash[i] & 0xff, 16));
+        }
+        return buf.toString();
+    }
+
+    public static String hash(String s)
+    {
+        try
+        {
+            return new String(toHex(md5(s)).getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
+        }
+        catch (Exception e)
+        {
+            log.error("not supported charset...{}", e);
+            return s;
+        }
+    }
+}

+ 86 - 0
common/common-additional/src/main/java/com/tourism/common/additional/text/CharsetKit.java

@@ -0,0 +1,86 @@
+package com.tourism.common.additional.text;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import com.tourism.common.additional.utils.StringUtils;
+
+/**
+ * 字符集工具类
+ * 
+ * @author tourism
+ */
+public class CharsetKit
+{
+    /** ISO-8859-1 */
+    public static final String ISO_8859_1 = "ISO-8859-1";
+    /** UTF-8 */
+    public static final String UTF_8 = "UTF-8";
+    /** GBK */
+    public static final String GBK = "GBK";
+
+    /** ISO-8859-1 */
+    public static final Charset CHARSET_ISO_8859_1 = Charset.forName(ISO_8859_1);
+    /** UTF-8 */
+    public static final Charset CHARSET_UTF_8 = Charset.forName(UTF_8);
+    /** GBK */
+    public static final Charset CHARSET_GBK = Charset.forName(GBK);
+
+    /**
+     * 转换为Charset对象
+     * 
+     * @param charset 字符集,为空则返回默认字符集
+     * @return Charset
+     */
+    public static Charset charset(String charset)
+    {
+        return StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset);
+    }
+
+    /**
+     * 转换字符串的字符集编码
+     * 
+     * @param source 字符串
+     * @param srcCharset 源字符集,默认ISO-8859-1
+     * @param destCharset 目标字符集,默认UTF-8
+     * @return 转换后的字符集
+     */
+    public static String convert(String source, String srcCharset, String destCharset)
+    {
+        return convert(source, Charset.forName(srcCharset), Charset.forName(destCharset));
+    }
+
+    /**
+     * 转换字符串的字符集编码
+     * 
+     * @param source 字符串
+     * @param srcCharset 源字符集,默认ISO-8859-1
+     * @param destCharset 目标字符集,默认UTF-8
+     * @return 转换后的字符集
+     */
+    public static String convert(String source, Charset srcCharset, Charset destCharset)
+    {
+        if (null == srcCharset)
+        {
+            srcCharset = StandardCharsets.ISO_8859_1;
+        }
+
+        if (null == destCharset)
+        {
+            destCharset = StandardCharsets.UTF_8;
+        }
+
+        if (StringUtils.isEmpty(source) || srcCharset.equals(destCharset))
+        {
+            return source;
+        }
+        return new String(source.getBytes(srcCharset), destCharset);
+    }
+
+    /**
+     * @return 系统字符集编码
+     */
+    public static String systemCharset()
+    {
+        return Charset.defaultCharset().name();
+    }
+}

+ 1011 - 0
common/common-additional/src/main/java/com/tourism/common/additional/text/Convert.java

@@ -0,0 +1,1011 @@
+package com.tourism.common.additional.text;
+
+import com.tourism.common.additional.utils.StringUtils;
+import org.apache.commons.lang3.ArrayUtils;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.RoundingMode;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.text.NumberFormat;
+import java.util.Set;
+
+/**
+ * 类型转换器
+ *
+ * @author tourism
+ */
+public class Convert
+{
+    /**
+     * 转换为字符串<br>
+     * 如果给定的值为null,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static String toStr(Object value, String defaultValue)
+    {
+        if (null == value)
+        {
+            return defaultValue;
+        }
+        if (value instanceof String)
+        {
+            return (String) value;
+        }
+        return value.toString();
+    }
+
+    /**
+     * 转换为字符串<br>
+     * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static String toStr(Object value)
+    {
+        return toStr(value, null);
+    }
+
+    /**
+     * 转换为字符<br>
+     * 如果给定的值为null,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static Character toChar(Object value, Character defaultValue)
+    {
+        if (null == value)
+        {
+            return defaultValue;
+        }
+        if (value instanceof Character)
+        {
+            return (Character) value;
+        }
+
+        final String valueStr = toStr(value, null);
+        return StringUtils.isEmpty(valueStr) ? defaultValue : valueStr.charAt(0);
+    }
+
+    /**
+     * 转换为字符<br>
+     * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static Character toChar(Object value)
+    {
+        return toChar(value, null);
+    }
+
+    /**
+     * 转换为byte<br>
+     * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static Byte toByte(Object value, Byte defaultValue)
+    {
+        if (value == null)
+        {
+            return defaultValue;
+        }
+        if (value instanceof Byte)
+        {
+            return (Byte) value;
+        }
+        if (value instanceof Number)
+        {
+            return ((Number) value).byteValue();
+        }
+        final String valueStr = toStr(value, null);
+        if (StringUtils.isEmpty(valueStr))
+        {
+            return defaultValue;
+        }
+        try
+        {
+            return Byte.parseByte(valueStr);
+        }
+        catch (Exception e)
+        {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * 转换为byte<br>
+     * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static Byte toByte(Object value)
+    {
+        return toByte(value, null);
+    }
+
+    /**
+     * 转换为Short<br>
+     * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static Short toShort(Object value, Short defaultValue)
+    {
+        if (value == null)
+        {
+            return defaultValue;
+        }
+        if (value instanceof Short)
+        {
+            return (Short) value;
+        }
+        if (value instanceof Number)
+        {
+            return ((Number) value).shortValue();
+        }
+        final String valueStr = toStr(value, null);
+        if (StringUtils.isEmpty(valueStr))
+        {
+            return defaultValue;
+        }
+        try
+        {
+            return Short.parseShort(valueStr.trim());
+        }
+        catch (Exception e)
+        {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * 转换为Short<br>
+     * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static Short toShort(Object value)
+    {
+        return toShort(value, null);
+    }
+
+    /**
+     * 转换为Number<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static Number toNumber(Object value, Number defaultValue)
+    {
+        if (value == null)
+        {
+            return defaultValue;
+        }
+        if (value instanceof Number)
+        {
+            return (Number) value;
+        }
+        final String valueStr = toStr(value, null);
+        if (StringUtils.isEmpty(valueStr))
+        {
+            return defaultValue;
+        }
+        try
+        {
+            return NumberFormat.getInstance().parse(valueStr);
+        }
+        catch (Exception e)
+        {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * 转换为Number<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static Number toNumber(Object value)
+    {
+        return toNumber(value, null);
+    }
+
+    /**
+     * 转换为int<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static Integer toInt(Object value, Integer defaultValue)
+    {
+        if (value == null)
+        {
+            return defaultValue;
+        }
+        if (value instanceof Integer)
+        {
+            return (Integer) value;
+        }
+        if (value instanceof Number)
+        {
+            return ((Number) value).intValue();
+        }
+        final String valueStr = toStr(value, null);
+        if (StringUtils.isEmpty(valueStr))
+        {
+            return defaultValue;
+        }
+        try
+        {
+            return Integer.parseInt(valueStr.trim());
+        }
+        catch (Exception e)
+        {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * 转换为int<br>
+     * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static Integer toInt(Object value)
+    {
+        return toInt(value, null);
+    }
+
+    /**
+     * 转换为Integer数组<br>
+     *
+     * @param str 被转换的值
+     * @return 结果
+     */
+    public static Integer[] toIntArray(String str)
+    {
+        return toIntArray(",", str);
+    }
+
+    /**
+     * 转换为Long数组<br>
+     *
+     * @param str 被转换的值
+     * @return 结果
+     */
+    public static Long[] toLongArray(String str)
+    {
+        return toLongArray(",", str);
+    }
+
+    /**
+     * 转换为Integer数组<br>
+     *
+     * @param split 分隔符
+     * @param split 被转换的值
+     * @return 结果
+     */
+    public static Integer[] toIntArray(String split, String str)
+    {
+        if (StringUtils.isEmpty(str))
+        {
+            return new Integer[] {};
+        }
+        String[] arr = str.split(split);
+        final Integer[] ints = new Integer[arr.length];
+        for (int i = 0; i < arr.length; i++)
+        {
+            final Integer v = toInt(arr[i], 0);
+            ints[i] = v;
+        }
+        return ints;
+    }
+
+    /**
+     * 转换为Long数组<br>
+     *
+     * @param split 分隔符
+     * @param str 被转换的值
+     * @return 结果
+     */
+    public static Long[] toLongArray(String split, String str)
+    {
+        if (StringUtils.isEmpty(str))
+        {
+            return new Long[] {};
+        }
+        String[] arr = str.split(split);
+        final Long[] longs = new Long[arr.length];
+        for (int i = 0; i < arr.length; i++)
+        {
+            final Long v = toLong(arr[i], null);
+            longs[i] = v;
+        }
+        return longs;
+    }
+
+    /**
+     * 转换为String数组<br>
+     *
+     * @param str 被转换的值
+     * @return 结果
+     */
+    public static String[] toStrArray(String str)
+    {
+        if (StringUtils.isEmpty(str))
+        {
+            return new String[] {};
+        }
+        return toStrArray(",", str);
+    }
+
+    /**
+     * 转换为String数组<br>
+     *
+     * @param split 分隔符
+     * @param split 被转换的值
+     * @return 结果
+     */
+    public static String[] toStrArray(String split, String str)
+    {
+        return str.split(split);
+    }
+
+    /**
+     * 转换为long<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static Long toLong(Object value, Long defaultValue)
+    {
+        if (value == null)
+        {
+            return defaultValue;
+        }
+        if (value instanceof Long)
+        {
+            return (Long) value;
+        }
+        if (value instanceof Number)
+        {
+            return ((Number) value).longValue();
+        }
+        final String valueStr = toStr(value, null);
+        if (StringUtils.isEmpty(valueStr))
+        {
+            return defaultValue;
+        }
+        try
+        {
+            // 支持科学计数法
+            return new BigDecimal(valueStr.trim()).longValue();
+        }
+        catch (Exception e)
+        {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * 转换为long<br>
+     * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static Long toLong(Object value)
+    {
+        return toLong(value, null);
+    }
+
+    /**
+     * 转换为double<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static Double toDouble(Object value, Double defaultValue)
+    {
+        if (value == null)
+        {
+            return defaultValue;
+        }
+        if (value instanceof Double)
+        {
+            return (Double) value;
+        }
+        if (value instanceof Number)
+        {
+            return ((Number) value).doubleValue();
+        }
+        final String valueStr = toStr(value, null);
+        if (StringUtils.isEmpty(valueStr))
+        {
+            return defaultValue;
+        }
+        try
+        {
+            // 支持科学计数法
+            return new BigDecimal(valueStr.trim()).doubleValue();
+        }
+        catch (Exception e)
+        {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * 转换为double<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static Double toDouble(Object value)
+    {
+        return toDouble(value, null);
+    }
+
+    /**
+     * 转换为Float<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static Float toFloat(Object value, Float defaultValue)
+    {
+        if (value == null)
+        {
+            return defaultValue;
+        }
+        if (value instanceof Float)
+        {
+            return (Float) value;
+        }
+        if (value instanceof Number)
+        {
+            return ((Number) value).floatValue();
+        }
+        final String valueStr = toStr(value, null);
+        if (StringUtils.isEmpty(valueStr))
+        {
+            return defaultValue;
+        }
+        try
+        {
+            return Float.parseFloat(valueStr.trim());
+        }
+        catch (Exception e)
+        {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * 转换为Float<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static Float toFloat(Object value)
+    {
+        return toFloat(value, null);
+    }
+
+    /**
+     * 转换为boolean<br>
+     * String支持的值为:true、false、yes、ok、no,1,0 如果给定的值为空,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static Boolean toBool(Object value, Boolean defaultValue)
+    {
+        if (value == null)
+        {
+            return defaultValue;
+        }
+        if (value instanceof Boolean)
+        {
+            return (Boolean) value;
+        }
+        String valueStr = toStr(value, null);
+        if (StringUtils.isEmpty(valueStr))
+        {
+            return defaultValue;
+        }
+        valueStr = valueStr.trim().toLowerCase();
+        switch (valueStr)
+        {
+            case "true":
+            case "yes":
+            case "ok":
+            case "1":
+                return true;
+            case "false":
+            case "no":
+            case "0":
+                return false;
+            default:
+                return defaultValue;
+        }
+    }
+
+    /**
+     * 转换为boolean<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static Boolean toBool(Object value)
+    {
+        return toBool(value, null);
+    }
+
+    /**
+     * 转换为Enum对象<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<br>
+     *
+     * @param clazz Enum的Class
+     * @param value 值
+     * @param defaultValue 默认值
+     * @return Enum
+     */
+    public static <E extends Enum<E>> E toEnum(Class<E> clazz, Object value, E defaultValue)
+    {
+        if (value == null)
+        {
+            return defaultValue;
+        }
+        if (clazz.isAssignableFrom(value.getClass()))
+        {
+            @SuppressWarnings("unchecked")
+            E myE = (E) value;
+            return myE;
+        }
+        final String valueStr = toStr(value, null);
+        if (StringUtils.isEmpty(valueStr))
+        {
+            return defaultValue;
+        }
+        try
+        {
+            return Enum.valueOf(clazz, valueStr);
+        }
+        catch (Exception e)
+        {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * 转换为Enum对象<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
+     *
+     * @param clazz Enum的Class
+     * @param value 值
+     * @return Enum
+     */
+    public static <E extends Enum<E>> E toEnum(Class<E> clazz, Object value)
+    {
+        return toEnum(clazz, value, null);
+    }
+
+    /**
+     * 转换为BigInteger<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static BigInteger toBigInteger(Object value, BigInteger defaultValue)
+    {
+        if (value == null)
+        {
+            return defaultValue;
+        }
+        if (value instanceof BigInteger)
+        {
+            return (BigInteger) value;
+        }
+        if (value instanceof Long)
+        {
+            return BigInteger.valueOf((Long) value);
+        }
+        final String valueStr = toStr(value, null);
+        if (StringUtils.isEmpty(valueStr))
+        {
+            return defaultValue;
+        }
+        try
+        {
+            return new BigInteger(valueStr);
+        }
+        catch (Exception e)
+        {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * 转换为BigInteger<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static BigInteger toBigInteger(Object value)
+    {
+        return toBigInteger(value, null);
+    }
+
+    /**
+     * 转换为BigDecimal<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static BigDecimal toBigDecimal(Object value, BigDecimal defaultValue)
+    {
+        if (value == null)
+        {
+            return defaultValue;
+        }
+        if (value instanceof BigDecimal)
+        {
+            return (BigDecimal) value;
+        }
+        if (value instanceof Long)
+        {
+            return new BigDecimal((Long) value);
+        }
+        if (value instanceof Double)
+        {
+            return BigDecimal.valueOf((Double) value);
+        }
+        if (value instanceof Integer)
+        {
+            return new BigDecimal((Integer) value);
+        }
+        final String valueStr = toStr(value, null);
+        if (StringUtils.isEmpty(valueStr))
+        {
+            return defaultValue;
+        }
+        try
+        {
+            return new BigDecimal(valueStr);
+        }
+        catch (Exception e)
+        {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * 转换为BigDecimal<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static BigDecimal toBigDecimal(Object value)
+    {
+        return toBigDecimal(value, null);
+    }
+
+    /**
+     * 将对象转为字符串<br>
+     * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法
+     *
+     * @param obj 对象
+     * @return 字符串
+     */
+    public static String utf8Str(Object obj)
+    {
+        return str(obj, CharsetKit.CHARSET_UTF_8);
+    }
+
+    /**
+     * 将对象转为字符串<br>
+     * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法
+     *
+     * @param obj 对象
+     * @param charsetName 字符集
+     * @return 字符串
+     */
+    public static String str(Object obj, String charsetName)
+    {
+        return str(obj, Charset.forName(charsetName));
+    }
+
+    /**
+     * 将对象转为字符串<br>
+     * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法
+     *
+     * @param obj 对象
+     * @param charset 字符集
+     * @return 字符串
+     */
+    public static String str(Object obj, Charset charset)
+    {
+        if (null == obj)
+        {
+            return null;
+        }
+
+        if (obj instanceof String)
+        {
+            return (String) obj;
+        }
+        else if (obj instanceof byte[])
+        {
+            return str((byte[]) obj, charset);
+        }
+        else if (obj instanceof Byte[])
+        {
+            byte[] bytes = ArrayUtils.toPrimitive((Byte[]) obj);
+            return str(bytes, charset);
+        }
+        else if (obj instanceof ByteBuffer)
+        {
+            return str((ByteBuffer) obj, charset);
+        }
+        return obj.toString();
+    }
+
+    /**
+     * 将byte数组转为字符串
+     *
+     * @param bytes byte数组
+     * @param charset 字符集
+     * @return 字符串
+     */
+    public static String str(byte[] bytes, String charset)
+    {
+        return str(bytes, StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset));
+    }
+
+    /**
+     * 解码字节码
+     *
+     * @param data 字符串
+     * @param charset 字符集,如果此字段为空,则解码的结果取决于平台
+     * @return 解码后的字符串
+     */
+    public static String str(byte[] data, Charset charset)
+    {
+        if (data == null)
+        {
+            return null;
+        }
+
+        if (null == charset)
+        {
+            return new String(data);
+        }
+        return new String(data, charset);
+    }
+
+    /**
+     * 将编码的byteBuffer数据转换为字符串
+     *
+     * @param data 数据
+     * @param charset 字符集,如果为空使用当前系统字符集
+     * @return 字符串
+     */
+    public static String str(ByteBuffer data, String charset)
+    {
+        if (data == null)
+        {
+            return null;
+        }
+
+        return str(data, Charset.forName(charset));
+    }
+
+    /**
+     * 将编码的byteBuffer数据转换为字符串
+     *
+     * @param data 数据
+     * @param charset 字符集,如果为空使用当前系统字符集
+     * @return 字符串
+     */
+    public static String str(ByteBuffer data, Charset charset)
+    {
+        if (null == charset)
+        {
+            charset = Charset.defaultCharset();
+        }
+        return charset.decode(data).toString();
+    }
+
+    // ----------------------------------------------------------------------- 全角半角转换
+    /**
+     * 半角转全角
+     *
+     * @param input String.
+     * @return 全角字符串.
+     */
+    public static String toSBC(String input)
+    {
+        return toSBC(input, null);
+    }
+
+    /**
+     * 半角转全角
+     *
+     * @param input String
+     * @param notConvertSet 不替换的字符集合
+     * @return 全角字符串.
+     */
+    public static String toSBC(String input, Set<Character> notConvertSet)
+    {
+        char[] c = input.toCharArray();
+        for (int i = 0; i < c.length; i++)
+        {
+            if (null != notConvertSet && notConvertSet.contains(c[i]))
+            {
+                // 跳过不替换的字符
+                continue;
+            }
+
+            if (c[i] == ' ')
+            {
+                c[i] = '\u3000';
+            }
+            else if (c[i] < '\177')
+            {
+                c[i] = (char) (c[i] + 65248);
+
+            }
+        }
+        return new String(c);
+    }
+
+    /**
+     * 全角转半角
+     *
+     * @param input String.
+     * @return 半角字符串
+     */
+    public static String toDBC(String input)
+    {
+        return toDBC(input, null);
+    }
+
+    /**
+     * 替换全角为半角
+     *
+     * @param text 文本
+     * @param notConvertSet 不替换的字符集合
+     * @return 替换后的字符
+     */
+    public static String toDBC(String text, Set<Character> notConvertSet)
+    {
+        char[] c = text.toCharArray();
+        for (int i = 0; i < c.length; i++)
+        {
+            if (null != notConvertSet && notConvertSet.contains(c[i]))
+            {
+                // 跳过不替换的字符
+                continue;
+            }
+
+            if (c[i] == '\u3000')
+            {
+                c[i] = ' ';
+            }
+            else if (c[i] > '\uFF00' && c[i] < '\uFF5F')
+            {
+                c[i] = (char) (c[i] - 65248);
+            }
+        }
+        String returnString = new String(c);
+
+        return returnString;
+    }
+
+    /**
+     * 数字金额大写转换 先写个完整的然后将如零拾替换成零
+     *
+     * @param n 数字
+     * @return 中文大写数字
+     */
+    public static String digitUppercase(double n)
+    {
+        String[] fraction = { "角", "分" };
+        String[] digit = { "零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖" };
+        String[][] unit = { { "元", "万", "亿" }, { "", "拾", "佰", "仟" } };
+
+        String head = n < 0 ? "负" : "";
+        n = Math.abs(n);
+
+        String s = "";
+        for (int i = 0; i < fraction.length; i++)
+        {
+            // 优化double计算精度丢失问题
+            BigDecimal nNum = new BigDecimal(n);
+            BigDecimal decimal = new BigDecimal(10);
+            BigDecimal scale = nNum.multiply(decimal).setScale(2, RoundingMode.HALF_EVEN);
+            double d = scale.doubleValue();
+            s += (digit[(int) (Math.floor(d * Math.pow(10, i)) % 10)] + fraction[i]).replaceAll("(零.)+", "");
+        }
+        if (s.length() < 1)
+        {
+            s = "整";
+        }
+        int integerPart = (int) Math.floor(n);
+
+        for (int i = 0; i < unit[0].length && integerPart > 0; i++)
+        {
+            String p = "";
+            for (int j = 0; j < unit[1].length && n > 0; j++)
+            {
+                p = digit[integerPart % 10] + unit[1][j] + p;
+                integerPart = integerPart / 10;
+            }
+            s = p.replaceAll("(零.)*零$", "").replaceAll("^$", "零") + unit[0][i] + s;
+        }
+        return head + s.replaceAll("(零.)*零元", "元").replaceFirst("(零.)+", "").replaceAll("(零.)+", "零").replaceAll("^整$", "零元整");
+    }
+}

+ 92 - 0
common/common-additional/src/main/java/com/tourism/common/additional/text/StrFormatter.java

@@ -0,0 +1,92 @@
+package com.tourism.common.additional.text;
+
+import com.tourism.common.additional.utils.StringUtils;
+
+/**
+ * 字符串格式化
+ *
+ * @author tourism
+ */
+public class StrFormatter
+{
+    public static final String EMPTY_JSON = "{}";
+    public static final char C_BACKSLASH = '\\';
+    public static final char C_DELIM_START = '{';
+    public static final char C_DELIM_END = '}';
+
+    /**
+     * 格式化字符串<br>
+     * 此方法只是简单将占位符 {} 按照顺序替换为参数<br>
+     * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可<br>
+     * 例:<br>
+     * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b<br>
+     * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a<br>
+     * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b<br>
+     *
+     * @param strPattern 字符串模板
+     * @param argArray 参数列表
+     * @return 结果
+     */
+    public static String format(final String strPattern, final Object... argArray)
+    {
+        if (StringUtils.isEmpty(strPattern) || StringUtils.isEmpty(argArray))
+        {
+            return strPattern;
+        }
+        final int strPatternLength = strPattern.length();
+
+        // 初始化定义好的长度以获得更好的性能
+        StringBuilder sbuf = new StringBuilder(strPatternLength + 50);
+
+        int handledPosition = 0;
+        int delimIndex;// 占位符所在位置
+        for (int argIndex = 0; argIndex < argArray.length; argIndex++)
+        {
+            delimIndex = strPattern.indexOf(EMPTY_JSON, handledPosition);
+            if (delimIndex == -1)
+            {
+                if (handledPosition == 0)
+                {
+                    return strPattern;
+                }
+                else
+                { // 字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果
+                    sbuf.append(strPattern, handledPosition, strPatternLength);
+                    return sbuf.toString();
+                }
+            }
+            else
+            {
+                if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == C_BACKSLASH)
+                {
+                    if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == C_BACKSLASH)
+                    {
+                        // 转义符之前还有一个转义符,占位符依旧有效
+                        sbuf.append(strPattern, handledPosition, delimIndex - 1);
+                        sbuf.append(Convert.utf8Str(argArray[argIndex]));
+                        handledPosition = delimIndex + 2;
+                    }
+                    else
+                    {
+                        // 占位符被转义
+                        argIndex--;
+                        sbuf.append(strPattern, handledPosition, delimIndex - 1);
+                        sbuf.append(C_DELIM_START);
+                        handledPosition = delimIndex + 1;
+                    }
+                }
+                else
+                {
+                    // 正常占位符
+                    sbuf.append(strPattern, handledPosition, delimIndex);
+                    sbuf.append(Convert.utf8Str(argArray[argIndex]));
+                    handledPosition = delimIndex + 2;
+                }
+            }
+        }
+        // 加入最后一个占位符后所有的字符
+        sbuf.append(strPattern, handledPosition, strPattern.length());
+
+        return sbuf.toString();
+    }
+}

+ 680 - 0
common/common-additional/src/main/java/com/tourism/common/additional/utils/StringUtils.java

@@ -0,0 +1,680 @@
+package com.tourism.common.additional.utils;
+
+import com.tourism.common.additional.constant.Constants;
+import com.tourism.common.additional.text.StrFormatter;
+import org.springframework.util.AntPathMatcher;
+
+import java.util.*;
+
+/**
+ * 字符串工具类
+ *
+ * @author tourism
+ */
+public class StringUtils extends org.apache.commons.lang3.StringUtils
+{
+    /** 空字符串 */
+    private static final String NULLSTR = "";
+
+    /** 下划线 */
+    private static final char SEPARATOR = '_';
+
+    /** 星号 */
+    private static final char ASTERISK = '*';
+
+    /**
+     * 获取参数不为空值
+     *
+     * @param value defaultValue 要判断的value
+     * @return value 返回值
+     */
+    public static <T> T nvl(T value, T defaultValue)
+    {
+        return value != null ? value : defaultValue;
+    }
+
+    /**
+     * * 判断一个Collection是否为空, 包含List,Set,Queue
+     *
+     * @param coll 要判断的Collection
+     * @return true:为空 false:非空
+     */
+    public static boolean isEmpty(Collection<?> coll)
+    {
+        return isNull(coll) || coll.isEmpty();
+    }
+
+    /**
+     * * 判断一个Collection是否非空,包含List,Set,Queue
+     *
+     * @param coll 要判断的Collection
+     * @return true:非空 false:空
+     */
+    public static boolean isNotEmpty(Collection<?> coll)
+    {
+        return !isEmpty(coll);
+    }
+
+    /**
+     * * 判断一个对象数组是否为空
+     *
+     * @param objects 要判断的对象数组
+     ** @return true:为空 false:非空
+     */
+    public static boolean isEmpty(Object[] objects)
+    {
+        return isNull(objects) || (objects.length == 0);
+    }
+
+    /**
+     * * 判断一个对象数组是否非空
+     *
+     * @param objects 要判断的对象数组
+     * @return true:非空 false:空
+     */
+    public static boolean isNotEmpty(Object[] objects)
+    {
+        return !isEmpty(objects);
+    }
+
+    /**
+     * * 判断一个Map是否为空
+     *
+     * @param map 要判断的Map
+     * @return true:为空 false:非空
+     */
+    public static boolean isEmpty(Map<?, ?> map)
+    {
+        return isNull(map) || map.isEmpty();
+    }
+
+    /**
+     * * 判断一个Map是否为空
+     *
+     * @param map 要判断的Map
+     * @return true:非空 false:空
+     */
+    public static boolean isNotEmpty(Map<?, ?> map)
+    {
+        return !isEmpty(map);
+    }
+
+    /**
+     * * 判断一个字符串是否为空串
+     *
+     * @param str String
+     * @return true:为空 false:非空
+     */
+    public static boolean isEmpty(String str)
+    {
+        return isNull(str) || NULLSTR.equals(str.trim());
+    }
+
+    /**
+     * * 判断一个字符串是否为非空串
+     *
+     * @param str String
+     * @return true:非空串 false:空串
+     */
+    public static boolean isNotEmpty(String str)
+    {
+        return !isEmpty(str);
+    }
+
+    /**
+     * * 判断一个对象是否为空
+     *
+     * @param object Object
+     * @return true:为空 false:非空
+     */
+    public static boolean isNull(Object object)
+    {
+        return object == null;
+    }
+
+    /**
+     * * 判断一个对象是否非空
+     *
+     * @param object Object
+     * @return true:非空 false:空
+     */
+    public static boolean isNotNull(Object object)
+    {
+        return !isNull(object);
+    }
+
+    /**
+     * * 判断一个对象是否是数组类型(Java基本型别的数组)
+     *
+     * @param object 对象
+     * @return true:是数组 false:不是数组
+     */
+    public static boolean isArray(Object object)
+    {
+        return isNotNull(object) && object.getClass().isArray();
+    }
+
+    /**
+     * 去空格
+     */
+    public static String trim(String str)
+    {
+        return (str == null ? "" : str.trim());
+    }
+
+    /**
+     * 替换指定字符串的指定区间内字符为"*"
+     *
+     * @param str 字符串
+     * @param startInclude 开始位置(包含)
+     * @param endExclude 结束位置(不包含)
+     * @return 替换后的字符串
+     */
+    public static String hide(CharSequence str, int startInclude, int endExclude)
+    {
+        if (isEmpty(str))
+        {
+            return NULLSTR;
+        }
+        final int strLength = str.length();
+        if (startInclude > strLength)
+        {
+            return NULLSTR;
+        }
+        if (endExclude > strLength)
+        {
+            endExclude = strLength;
+        }
+        if (startInclude > endExclude)
+        {
+            // 如果起始位置大于结束位置,不替换
+            return NULLSTR;
+        }
+        final char[] chars = new char[strLength];
+        for (int i = 0; i < strLength; i++)
+        {
+            if (i >= startInclude && i < endExclude)
+            {
+                chars[i] = ASTERISK;
+            }
+            else
+            {
+                chars[i] = str.charAt(i);
+            }
+        }
+        return new String(chars);
+    }
+
+    /**
+     * 截取字符串
+     *
+     * @param str 字符串
+     * @param start 开始
+     * @return 结果
+     */
+    public static String substring(final String str, int start)
+    {
+        if (str == null)
+        {
+            return NULLSTR;
+        }
+
+        if (start < 0)
+        {
+            start = str.length() + start;
+        }
+
+        if (start < 0)
+        {
+            start = 0;
+        }
+        if (start > str.length())
+        {
+            return NULLSTR;
+        }
+
+        return str.substring(start);
+    }
+
+    /**
+     * 截取字符串
+     *
+     * @param str 字符串
+     * @param start 开始
+     * @param end 结束
+     * @return 结果
+     */
+    public static String substring(final String str, int start, int end)
+    {
+        if (str == null)
+        {
+            return NULLSTR;
+        }
+
+        if (end < 0)
+        {
+            end = str.length() + end;
+        }
+        if (start < 0)
+        {
+            start = str.length() + start;
+        }
+
+        if (end > str.length())
+        {
+            end = str.length();
+        }
+
+        if (start > end)
+        {
+            return NULLSTR;
+        }
+
+        if (start < 0)
+        {
+            start = 0;
+        }
+        if (end < 0)
+        {
+            end = 0;
+        }
+
+        return str.substring(start, end);
+    }
+
+    /**
+     * 判断是否为空,并且不是空白字符
+     *
+     * @param str 要判断的value
+     * @return 结果
+     */
+    public static boolean hasText(String str)
+    {
+        return (str != null && !str.isEmpty() && containsText(str));
+    }
+
+    private static boolean containsText(CharSequence str)
+    {
+        int strLen = str.length();
+        for (int i = 0; i < strLen; i++)
+        {
+            if (!Character.isWhitespace(str.charAt(i)))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 格式化文本, {} 表示占位符<br>
+     * 此方法只是简单将占位符 {} 按照顺序替换为参数<br>
+     * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可<br>
+     * 例:<br>
+     * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b<br>
+     * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a<br>
+     * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b<br>
+     *
+     * @param template 文本模板,被替换的部分用 {} 表示
+     * @param params 参数值
+     * @return 格式化后的文本
+     */
+    public static String format(String template, Object... params)
+    {
+        if (isEmpty(params) || isEmpty(template))
+        {
+            return template;
+        }
+        return StrFormatter.format(template, params);
+    }
+
+    /**
+     * 是否为http(s)://开头
+     *
+     * @param link 链接
+     * @return 结果
+     */
+    public static boolean ishttp(String link)
+    {
+        return StringUtils.startsWithAny(link, Constants.HTTP, Constants.HTTPS);
+    }
+
+    /**
+     * 字符串转set
+     *
+     * @param str 字符串
+     * @param sep 分隔符
+     * @return set集合
+     */
+    public static final Set<String> str2Set(String str, String sep)
+    {
+        return new HashSet<String>(str2List(str, sep, true, false));
+    }
+
+    /**
+     * 字符串转list
+     *
+     * @param str 字符串
+     * @param sep 分隔符
+     * @param filterBlank 过滤纯空白
+     * @param trim 去掉首尾空白
+     * @return list集合
+     */
+    public static final List<String> str2List(String str, String sep, boolean filterBlank, boolean trim)
+    {
+        List<String> list = new ArrayList<String>();
+        if (StringUtils.isEmpty(str))
+        {
+            return list;
+        }
+
+        // 过滤空白字符串
+        if (filterBlank && StringUtils.isBlank(str))
+        {
+            return list;
+        }
+        String[] split = str.split(sep);
+        for (String string : split)
+        {
+            if (filterBlank && StringUtils.isBlank(string))
+            {
+                continue;
+            }
+            if (trim)
+            {
+                string = string.trim();
+            }
+            list.add(string);
+        }
+
+        return list;
+    }
+
+    /**
+     * 判断给定的collection列表中是否包含数组array 判断给定的数组array中是否包含给定的元素value
+     *
+     * @param collection 给定的集合
+     * @param array 给定的数组
+     * @return boolean 结果
+     */
+    public static boolean containsAny(Collection<String> collection, String... array)
+    {
+        if (isEmpty(collection) || isEmpty(array))
+        {
+            return false;
+        }
+        else
+        {
+            for (String str : array)
+            {
+                if (collection.contains(str))
+                {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    /**
+     * 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写
+     *
+     * @param cs 指定字符串
+     * @param searchCharSequences 需要检查的字符串数组
+     * @return 是否包含任意一个字符串
+     */
+    public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences)
+    {
+        if (isEmpty(cs) || isEmpty(searchCharSequences))
+        {
+            return false;
+        }
+        for (CharSequence testStr : searchCharSequences)
+        {
+            if (containsIgnoreCase(cs, testStr))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 驼峰转下划线命名
+     */
+    public static String toUnderScoreCase(String str)
+    {
+        if (str == null)
+        {
+            return null;
+        }
+        StringBuilder sb = new StringBuilder();
+        // 前置字符是否大写
+        boolean preCharIsUpperCase = true;
+        // 当前字符是否大写
+        boolean curreCharIsUpperCase = true;
+        // 下一字符是否大写
+        boolean nexteCharIsUpperCase = true;
+        for (int i = 0; i < str.length(); i++)
+        {
+            char c = str.charAt(i);
+            if (i > 0)
+            {
+                preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1));
+            }
+            else
+            {
+                preCharIsUpperCase = false;
+            }
+
+            curreCharIsUpperCase = Character.isUpperCase(c);
+
+            if (i < (str.length() - 1))
+            {
+                nexteCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1));
+            }
+
+            if (preCharIsUpperCase && curreCharIsUpperCase && !nexteCharIsUpperCase)
+            {
+                sb.append(SEPARATOR);
+            }
+            else if ((i != 0 && !preCharIsUpperCase) && curreCharIsUpperCase)
+            {
+                sb.append(SEPARATOR);
+            }
+            sb.append(Character.toLowerCase(c));
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * 是否包含字符串
+     *
+     * @param str 验证字符串
+     * @param strs 字符串组
+     * @return 包含返回true
+     */
+    public static boolean inStringIgnoreCase(String str, String... strs)
+    {
+        if (str != null && strs != null)
+        {
+            for (String s : strs)
+            {
+                if (str.equalsIgnoreCase(trim(s)))
+                {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld
+     *
+     * @param name 转换前的下划线大写方式命名的字符串
+     * @return 转换后的驼峰式命名的字符串
+     */
+    public static String convertToCamelCase(String name)
+    {
+        StringBuilder result = new StringBuilder();
+        // 快速检查
+        if (name == null || name.isEmpty())
+        {
+            // 没必要转换
+            return "";
+        }
+        else if (!name.contains("_"))
+        {
+            // 不含下划线,仅将首字母大写
+            return name.substring(0, 1).toUpperCase() + name.substring(1);
+        }
+        // 用下划线将原始字符串分割
+        String[] camels = name.split("_");
+        for (String camel : camels)
+        {
+            // 跳过原始字符串中开头、结尾的下换线或双重下划线
+            if (camel.isEmpty())
+            {
+                continue;
+            }
+            // 首字母大写
+            result.append(camel.substring(0, 1).toUpperCase());
+            result.append(camel.substring(1).toLowerCase());
+        }
+        return result.toString();
+    }
+
+    /**
+     * 驼峰式命名法
+     * 例如:user_name->userName
+     */
+    public static String toCamelCase(String s)
+    {
+        if (s == null)
+        {
+            return null;
+        }
+        if (s.indexOf(SEPARATOR) == -1)
+        {
+            return s;
+        }
+        s = s.toLowerCase();
+        StringBuilder sb = new StringBuilder(s.length());
+        boolean upperCase = false;
+        for (int i = 0; i < s.length(); i++)
+        {
+            char c = s.charAt(i);
+
+            if (c == SEPARATOR)
+            {
+                upperCase = true;
+            }
+            else if (upperCase)
+            {
+                sb.append(Character.toUpperCase(c));
+                upperCase = false;
+            }
+            else
+            {
+                sb.append(c);
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串
+     *
+     * @param str 指定字符串
+     * @param strs 需要检查的字符串数组
+     * @return 是否匹配
+     */
+    public static boolean matches(String str, List<String> strs)
+    {
+        if (isEmpty(str) || isEmpty(strs))
+        {
+            return false;
+        }
+        for (String pattern : strs)
+        {
+            if (isMatch(pattern, str))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 判断url是否与规则配置:
+     * ? 表示单个字符;
+     * * 表示一层路径内的任意字符串,不可跨层级;
+     * ** 表示任意层路径;
+     *
+     * @param pattern 匹配规则
+     * @param url 需要匹配的url
+     * @return
+     */
+    public static boolean isMatch(String pattern, String url)
+    {
+        AntPathMatcher matcher = new AntPathMatcher();
+        return matcher.match(pattern, url);
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T cast(Object obj)
+    {
+        return (T) obj;
+    }
+
+    /**
+     * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。
+     *
+     * @param num 数字对象
+     * @param size 字符串指定长度
+     * @return 返回数字的字符串格式,该字符串为指定长度。
+     */
+    public static final String padl(final Number num, final int size)
+    {
+        return padl(num.toString(), size, '0');
+    }
+
+    /**
+     * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。
+     *
+     * @param s 原始字符串
+     * @param size 字符串指定长度
+     * @param c 用于补齐的字符
+     * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。
+     */
+    public static final String padl(final String s, final int size, final char c)
+    {
+        final StringBuilder sb = new StringBuilder(size);
+        if (s != null)
+        {
+            final int len = s.length();
+            if (s.length() <= size)
+            {
+                for (int i = size - len; i > 0; i--)
+                {
+                    sb.append(c);
+                }
+                sb.append(s);
+            }
+            else
+            {
+                return s.substring(len - size, len);
+            }
+        }
+        else
+        {
+            for (int i = size; i > 0; i--)
+            {
+                sb.append(c);
+            }
+        }
+        return sb.toString();
+    }
+}

+ 49 - 0
common/common-additional/src/main/java/com/tourism/common/additional/uuid/IdUtils.java

@@ -0,0 +1,49 @@
+package com.tourism.common.additional.uuid;
+
+/**
+ * ID生成器工具类
+ * 
+ * @author tourism
+ */
+public class IdUtils
+{
+    /**
+     * 获取随机UUID
+     * 
+     * @return 随机UUID
+     */
+    public static String randomUUID()
+    {
+        return UUID.randomUUID().toString();
+    }
+
+    /**
+     * 简化的UUID,去掉了横线
+     * 
+     * @return 简化的UUID,去掉了横线
+     */
+    public static String simpleUUID()
+    {
+        return UUID.randomUUID().toString(true);
+    }
+
+    /**
+     * 获取随机UUID,使用性能更好的ThreadLocalRandom生成UUID
+     * 
+     * @return 随机UUID
+     */
+    public static String fastUUID()
+    {
+        return UUID.fastUUID().toString();
+    }
+
+    /**
+     * 简化的UUID,去掉了横线,使用性能更好的ThreadLocalRandom生成UUID
+     * 
+     * @return 简化的UUID,去掉了横线
+     */
+    public static String fastSimpleUUID()
+    {
+        return UUID.fastUUID().toString(true);
+    }
+}

+ 485 - 0
common/common-additional/src/main/java/com/tourism/common/additional/uuid/UUID.java

@@ -0,0 +1,485 @@
+package com.tourism.common.additional.uuid;
+
+import com.tourism.common.additional.exception.UtilException;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * 提供通用唯一识别码(universally unique identifier)(UUID)实现
+ *
+ * @author tourism
+ */
+public final class UUID implements java.io.Serializable, Comparable<UUID>
+{
+    private static final long serialVersionUID = -1185015143654744140L;
+
+    /**
+     * SecureRandom 的单例
+     *
+     */
+    private static class Holder
+    {
+        static final SecureRandom numberGenerator = getSecureRandom();
+    }
+
+    /** 此UUID的最高64有效位 */
+    private final long mostSigBits;
+
+    /** 此UUID的最低64有效位 */
+    private final long leastSigBits;
+
+    /**
+     * 私有构造
+     * 
+     * @param data 数据
+     */
+    private UUID(byte[] data)
+    {
+        long msb = 0;
+        long lsb = 0;
+        assert data.length == 16 : "data must be 16 bytes in length";
+        for (int i = 0; i < 8; i++)
+        {
+            msb = (msb << 8) | (data[i] & 0xff);
+        }
+        for (int i = 8; i < 16; i++)
+        {
+            lsb = (lsb << 8) | (data[i] & 0xff);
+        }
+        this.mostSigBits = msb;
+        this.leastSigBits = lsb;
+    }
+
+    /**
+     * 使用指定的数据构造新的 UUID。
+     *
+     * @param mostSigBits 用于 {@code UUID} 的最高有效 64 位
+     * @param leastSigBits 用于 {@code UUID} 的最低有效 64 位
+     */
+    public UUID(long mostSigBits, long leastSigBits)
+    {
+        this.mostSigBits = mostSigBits;
+        this.leastSigBits = leastSigBits;
+    }
+
+    /**
+     * 获取类型 4(伪随机生成的)UUID 的静态工厂。
+     * 
+     * @return 随机生成的 {@code UUID}
+     */
+    public static UUID fastUUID()
+    {
+        return randomUUID(false);
+    }
+
+    /**
+     * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。
+     * 
+     * @return 随机生成的 {@code UUID}
+     */
+    public static UUID randomUUID()
+    {
+        return randomUUID(true);
+    }
+
+    /**
+     * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。
+     * 
+     * @param isSecure 是否使用{@link SecureRandom}如果是可以获得更安全的随机码,否则可以得到更好的性能
+     * @return 随机生成的 {@code UUID}
+     */
+    public static UUID randomUUID(boolean isSecure)
+    {
+        final Random ng = isSecure ? Holder.numberGenerator : getRandom();
+
+        byte[] randomBytes = new byte[16];
+        ng.nextBytes(randomBytes);
+        randomBytes[6] &= 0x0f; /* clear version */
+        randomBytes[6] |= 0x40; /* set to version 4 */
+        randomBytes[8] &= 0x3f; /* clear variant */
+        randomBytes[8] |= 0x80; /* set to IETF variant */
+        return new UUID(randomBytes);
+    }
+
+    /**
+     * 根据指定的字节数组获取类型 3(基于名称的)UUID 的静态工厂。
+     *
+     * @param name 用于构造 UUID 的字节数组。
+     *
+     * @return 根据指定数组生成的 {@code UUID}
+     */
+    public static UUID nameUUIDFromBytes(byte[] name)
+    {
+        MessageDigest md;
+        try
+        {
+            md = MessageDigest.getInstance("MD5");
+        }
+        catch (NoSuchAlgorithmException nsae)
+        {
+            throw new InternalError("MD5 not supported");
+        }
+        byte[] md5Bytes = md.digest(name);
+        md5Bytes[6] &= 0x0f; /* clear version */
+        md5Bytes[6] |= 0x30; /* set to version 3 */
+        md5Bytes[8] &= 0x3f; /* clear variant */
+        md5Bytes[8] |= 0x80; /* set to IETF variant */
+        return new UUID(md5Bytes);
+    }
+
+    /**
+     * 根据 {@link #toString()} 方法中描述的字符串标准表示形式创建{@code UUID}。
+     *
+     * @param name 指定 {@code UUID} 字符串
+     * @return 具有指定值的 {@code UUID}
+     * @throws IllegalArgumentException 如果 name 与 {@link #toString} 中描述的字符串表示形式不符抛出此异常
+     *
+     */
+    public static UUID fromString(String name)
+    {
+        String[] components = name.split("-");
+        if (components.length != 5)
+        {
+            throw new IllegalArgumentException("Invalid UUID string: " + name);
+        }
+        for (int i = 0; i < 5; i++)
+        {
+            components[i] = "0x" + components[i];
+        }
+
+        long mostSigBits = Long.decode(components[0]).longValue();
+        mostSigBits <<= 16;
+        mostSigBits |= Long.decode(components[1]).longValue();
+        mostSigBits <<= 16;
+        mostSigBits |= Long.decode(components[2]).longValue();
+
+        long leastSigBits = Long.decode(components[3]).longValue();
+        leastSigBits <<= 48;
+        leastSigBits |= Long.decode(components[4]).longValue();
+
+        return new UUID(mostSigBits, leastSigBits);
+    }
+
+    /**
+     * 返回此 UUID 的 128 位值中的最低有效 64 位。
+     *
+     * @return 此 UUID 的 128 位值中的最低有效 64 位。
+     */
+    public long getLeastSignificantBits()
+    {
+        return leastSigBits;
+    }
+
+    /**
+     * 返回此 UUID 的 128 位值中的最高有效 64 位。
+     *
+     * @return 此 UUID 的 128 位值中最高有效 64 位。
+     */
+    public long getMostSignificantBits()
+    {
+        return mostSigBits;
+    }
+
+    /**
+     * 与此 {@code UUID} 相关联的版本号. 版本号描述此 {@code UUID} 是如何生成的。
+     * <p>
+     * 版本号具有以下含意:
+     * <ul>
+     * <li>1 基于时间的 UUID
+     * <li>2 DCE 安全 UUID
+     * <li>3 基于名称的 UUID
+     * <li>4 随机生成的 UUID
+     * </ul>
+     *
+     * @return 此 {@code UUID} 的版本号
+     */
+    public int version()
+    {
+        // Version is bits masked by 0x000000000000F000 in MS long
+        return (int) ((mostSigBits >> 12) & 0x0f);
+    }
+
+    /**
+     * 与此 {@code UUID} 相关联的变体号。变体号描述 {@code UUID} 的布局。
+     * <p>
+     * 变体号具有以下含意:
+     * <ul>
+     * <li>0 为 NCS 向后兼容保留
+     * <li>2 <a href="http://www.ietf.org/rfc/rfc4122.txt">IETF&nbsp;RFC&nbsp;4122</a>(Leach-Salz), 用于此类
+     * <li>6 保留,微软向后兼容
+     * <li>7 保留供以后定义使用
+     * </ul>
+     *
+     * @return 此 {@code UUID} 相关联的变体号
+     */
+    public int variant()
+    {
+        // This field is composed of a varying number of bits.
+        // 0 - - Reserved for NCS backward compatibility
+        // 1 0 - The IETF aka Leach-Salz variant (used by this class)
+        // 1 1 0 Reserved, Microsoft backward compatibility
+        // 1 1 1 Reserved for future definition.
+        return (int) ((leastSigBits >>> (64 - (leastSigBits >>> 62))) & (leastSigBits >> 63));
+    }
+
+    /**
+     * 与此 UUID 相关联的时间戳值。
+     *
+     * <p>
+     * 60 位的时间戳值根据此 {@code UUID} 的 time_low、time_mid 和 time_hi 字段构造。<br>
+     * 所得到的时间戳以 100 毫微秒为单位,从 UTC(通用协调时间) 1582 年 10 月 15 日零时开始。
+     *
+     * <p>
+     * 时间戳值仅在在基于时间的 UUID(其 version 类型为 1)中才有意义。<br>
+     * 如果此 {@code UUID} 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。
+     *
+     * @throws UnsupportedOperationException 如果此 {@code UUID} 不是 version 为 1 的 UUID。
+     */
+    public long timestamp() throws UnsupportedOperationException
+    {
+        checkTimeBase();
+        return (mostSigBits & 0x0FFFL) << 48//
+                | ((mostSigBits >> 16) & 0x0FFFFL) << 32//
+                | mostSigBits >>> 32;
+    }
+
+    /**
+     * 与此 UUID 相关联的时钟序列值。
+     *
+     * <p>
+     * 14 位的时钟序列值根据此 UUID 的 clock_seq 字段构造。clock_seq 字段用于保证在基于时间的 UUID 中的时间唯一性。
+     * <p>
+     * {@code clockSequence} 值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。 如果此 UUID 不是基于时间的 UUID,则此方法抛出
+     * UnsupportedOperationException。
+     *
+     * @return 此 {@code UUID} 的时钟序列
+     *
+     * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1
+     */
+    public int clockSequence() throws UnsupportedOperationException
+    {
+        checkTimeBase();
+        return (int) ((leastSigBits & 0x3FFF000000000000L) >>> 48);
+    }
+
+    /**
+     * 与此 UUID 相关的节点值。
+     *
+     * <p>
+     * 48 位的节点值根据此 UUID 的 node 字段构造。此字段旨在用于保存机器的 IEEE 802 地址,该地址用于生成此 UUID 以保证空间唯一性。
+     * <p>
+     * 节点值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。<br>
+     * 如果此 UUID 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。
+     *
+     * @return 此 {@code UUID} 的节点值
+     *
+     * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1
+     */
+    public long node() throws UnsupportedOperationException
+    {
+        checkTimeBase();
+        return leastSigBits & 0x0000FFFFFFFFFFFFL;
+    }
+
+    /**
+     * 返回此{@code UUID} 的字符串表现形式。
+     *
+     * <p>
+     * UUID 的字符串表示形式由此 BNF 描述:
+     * 
+     * <pre>
+     * {@code
+     * UUID                   = <time_low>-<time_mid>-<time_high_and_version>-<variant_and_sequence>-<node>
+     * time_low               = 4*<hexOctet>
+     * time_mid               = 2*<hexOctet>
+     * time_high_and_version  = 2*<hexOctet>
+     * variant_and_sequence   = 2*<hexOctet>
+     * node                   = 6*<hexOctet>
+     * hexOctet               = <hexDigit><hexDigit>
+     * hexDigit               = [0-9a-fA-F]
+     * }
+     * </pre>
+     * 
+     * </blockquote>
+     *
+     * @return 此{@code UUID} 的字符串表现形式
+     * @see #toString(boolean)
+     */
+    @Override
+    public String toString()
+    {
+        return toString(false);
+    }
+
+    /**
+     * 返回此{@code UUID} 的字符串表现形式。
+     *
+     * <p>
+     * UUID 的字符串表示形式由此 BNF 描述:
+     * 
+     * <pre>
+     * {@code
+     * UUID                   = <time_low>-<time_mid>-<time_high_and_version>-<variant_and_sequence>-<node>
+     * time_low               = 4*<hexOctet>
+     * time_mid               = 2*<hexOctet>
+     * time_high_and_version  = 2*<hexOctet>
+     * variant_and_sequence   = 2*<hexOctet>
+     * node                   = 6*<hexOctet>
+     * hexOctet               = <hexDigit><hexDigit>
+     * hexDigit               = [0-9a-fA-F]
+     * }
+     * </pre>
+     * 
+     * </blockquote>
+     *
+     * @param isSimple 是否简单模式,简单模式为不带'-'的UUID字符串
+     * @return 此{@code UUID} 的字符串表现形式
+     */
+    public String toString(boolean isSimple)
+    {
+        final StringBuilder builder = new StringBuilder(isSimple ? 32 : 36);
+        // time_low
+        builder.append(digits(mostSigBits >> 32, 8));
+        if (!isSimple)
+        {
+            builder.append('-');
+        }
+        // time_mid
+        builder.append(digits(mostSigBits >> 16, 4));
+        if (!isSimple)
+        {
+            builder.append('-');
+        }
+        // time_high_and_version
+        builder.append(digits(mostSigBits, 4));
+        if (!isSimple)
+        {
+            builder.append('-');
+        }
+        // variant_and_sequence
+        builder.append(digits(leastSigBits >> 48, 4));
+        if (!isSimple)
+        {
+            builder.append('-');
+        }
+        // node
+        builder.append(digits(leastSigBits, 12));
+
+        return builder.toString();
+    }
+
+    /**
+     * 返回此 UUID 的哈希码。
+     *
+     * @return UUID 的哈希码值。
+     */
+    @Override
+    public int hashCode()
+    {
+        long hilo = mostSigBits ^ leastSigBits;
+        return ((int) (hilo >> 32)) ^ (int) hilo;
+    }
+
+    /**
+     * 将此对象与指定对象比较。
+     * <p>
+     * 当且仅当参数不为 {@code null}、而是一个 UUID 对象、具有与此 UUID 相同的 varriant、包含相同的值(每一位均相同)时,结果才为 {@code true}。
+     *
+     * @param obj 要与之比较的对象
+     *
+     * @return 如果对象相同,则返回 {@code true};否则返回 {@code false}
+     */
+    @Override
+    public boolean equals(Object obj)
+    {
+        if ((null == obj) || (obj.getClass() != UUID.class))
+        {
+            return false;
+        }
+        UUID id = (UUID) obj;
+        return (mostSigBits == id.mostSigBits && leastSigBits == id.leastSigBits);
+    }
+
+    // Comparison Operations
+
+    /**
+     * 将此 UUID 与指定的 UUID 比较。
+     *
+     * <p>
+     * 如果两个 UUID 不同,且第一个 UUID 的最高有效字段大于第二个 UUID 的对应字段,则第一个 UUID 大于第二个 UUID。
+     *
+     * @param val 与此 UUID 比较的 UUID
+     *
+     * @return 在此 UUID 小于、等于或大于 val 时,分别返回 -1、0 或 1。
+     *
+     */
+    @Override
+    public int compareTo(UUID val)
+    {
+        // The ordering is intentionally set up so that the UUIDs
+        // can simply be numerically compared as two numbers
+        return (this.mostSigBits < val.mostSigBits ? -1 : //
+                (this.mostSigBits > val.mostSigBits ? 1 : //
+                        (this.leastSigBits < val.leastSigBits ? -1 : //
+                                (this.leastSigBits > val.leastSigBits ? 1 : //
+                                        0))));
+    }
+
+    // -------------------------------------------------------------------------------------------------------------------
+    // Private method start
+    /**
+     * 返回指定数字对应的hex值
+     * 
+     * @param val 值
+     * @param digits 位
+     * @return 值
+     */
+    private static String digits(long val, int digits)
+    {
+        long hi = 1L << (digits * 4);
+        return Long.toHexString(hi | (val & (hi - 1))).substring(1);
+    }
+
+    /**
+     * 检查是否为time-based版本UUID
+     */
+    private void checkTimeBase()
+    {
+        if (version() != 1)
+        {
+            throw new UnsupportedOperationException("Not a time-based UUID");
+        }
+    }
+
+    /**
+     * 获取{@link SecureRandom},类提供加密的强随机数生成器 (RNG)
+     *
+     * @return {@link SecureRandom}
+     */
+    public static SecureRandom getSecureRandom()
+    {
+        try
+        {
+            return SecureRandom.getInstance("SHA1PRNG");
+        }
+        catch (NoSuchAlgorithmException e)
+        {
+            throw new UtilException(e);
+        }
+    }
+
+    /**
+     * 获取随机数生成器对象<br>
+     * ThreadLocalRandom是JDK 7之后提供并发产生随机数,能够解决多个线程发生的竞争争夺。
+     * 
+     * @return {@link ThreadLocalRandom}
+     */
+    public static ThreadLocalRandom getRandom()
+    {
+        return ThreadLocalRandom.current();
+    }
+}

+ 2 - 1
common/common-core/src/main/java/com/tourism/common/core/constant/ErrorCodeEnum.java

@@ -40,7 +40,8 @@ public enum ErrorCodeEnum {
     INVALID_TENANT_STATUS("当前租户为不可用状态,请刷新后重试!"),
     INVALID_USER_TENANT("当前用户并不属于当前租户,请刷新后重试!"),
     INVALID_MOBILE_CODE("手机号不存在!"),
-    SMSCODE_ERR("验证码错误,请重试!"),
+    SMSCODE_ERR("短信验证码错误,请重试!"),
+    PICTURE_CODE_ERR("图片验证码错误,请重试!"),
 
     HAS_CHILDREN_DATA("数据验证失败,子数据存在,请刷新后重试!"),
     DATA_VALIDATED_FAILED("数据验证失败,请核对!"),

+ 1 - 0
common/pom.xml

@@ -12,6 +12,7 @@
     <packaging>pom</packaging>
 
     <modules>
+        <module>common-additional</module>
         <module>common-dbutil</module>
         <module>common-ext</module>
         <module>common-core</module>