Merge branch 'dev' of https://git.yfgame.vip/r/hangtag into dev

# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
This commit is contained in:
yf 2024-12-14 17:53:14 +08:00
commit d5b05b6d79
49 changed files with 2462 additions and 708 deletions

View File

@ -24,11 +24,20 @@ public class ValidationUtils {
private static final Pattern PATTERN_XML_NCNAME = Pattern.compile("[a-zA-Z_][\\-_.0-9_a-zA-Z$]*");
private static final Pattern PATTERN_EMAIL = Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");
public static boolean isMobile(String mobile) {
return StringUtils.hasText(mobile)
&& PATTERN_MOBILE.matcher(mobile).matches();
}
public static boolean isEmail(String email) {
return StringUtils.hasText(email)
&& PATTERN_EMAIL.matcher(email).matches();
}
public static boolean isURL(String url) {
return StringUtils.hasText(url)
&& PATTERN_URL.matcher(url).matches();

View File

@ -0,0 +1,28 @@
package cn.hangtag.framework.common.validation;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Target({
ElementType.METHOD,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR,
ElementType.PARAMETER,
ElementType.TYPE_USE
})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = EmailValidator.class
)
public @interface Email {
String message() default "邮箱格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,25 @@
package cn.hangtag.framework.common.validation;
import cn.hangtag.framework.common.util.validation.ValidationUtils;
import cn.hutool.core.util.StrUtil;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class EmailValidator implements ConstraintValidator<Email, String> {
@Override
public void initialize(Email annotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 如果手机号为空默认不校验即校验通过
if (StrUtil.isEmpty(value)) {
return true;
}
// 校验手机
return ValidationUtils.isEmail(value);
}
}

View File

@ -24,5 +24,6 @@ public interface ErrorCodeConstants extends cn.hangtag.module.system.enums.Erro
ErrorCode PRODUCE_ORDER_IMPORT_LIST_IS_EMPTY = new ErrorCode(4003, "导入生产制单数据不能为空");
ErrorCode SALE_CONTRACT_NOT_EXISTS = new ErrorCode(5000, "OMS销售合约不存在");
ErrorCode PRODUCT_PRICE_NOT_EXISTS = new ErrorCode(600, "产品单价记录不存在");
ErrorCode CUSTOMER_EMAIL_EXISTS = new ErrorCode(600, "已存在重复的客户邮箱号");
}

View File

@ -13,6 +13,7 @@ import cn.hangtag.module.oms.controller.admin.customer.front.vo.AddressInfoVO;
import cn.hangtag.module.oms.controller.admin.customer.front.vo.CustomerInfoVO;
import cn.hangtag.module.oms.serialnumber.CodingRulesUtils;
import cn.hangtag.module.system.controller.admin.user.vo.user.UserSaveReqVO;
import cn.hangtag.module.system.dal.dataobject.user.AdminUserDO;
import cn.hangtag.module.system.service.permission.PermissionService;
import cn.hangtag.module.system.service.user.AdminUserService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@ -74,9 +75,15 @@ public class CustomerServiceImpl implements CustomerService {
customer.setNumber(getNewCode());
}
// 判断是否存在重复的邮箱账户
AdminUserDO user = userService.getUserByMail(createReqVO.getEmail(), 999999);
if(user!=null){
throw exception(CUSTOMER_EMAIL_EXISTS);
}
//新增用户账号
UserSaveReqVO userSaveReqVO = new UserSaveReqVO();
userSaveReqVO.setUsername(createReqVO.getPhone());
userSaveReqVO.setUsername(createReqVO.getEmail());
userSaveReqVO.setNickname(createReqVO.getName());
userSaveReqVO.setMobile(createReqVO.getPhone());
userSaveReqVO.setPassword(userInitPassword);
@ -94,8 +101,6 @@ public class CustomerServiceImpl implements CustomerService {
// 插入子表
createCustomerAddressList(customer.getId(), createReqVO.getCustomerAddresss());
// 返回
return customer.getId();
}

View File

@ -0,0 +1,43 @@
package cn.hangtag.module.system.api.mail;
import cn.hangtag.framework.common.exception.ServiceException;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeSendReqDTO;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeUseReqDTO;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeValidateReqDTO;
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeValidateReqDTO;
import javax.validation.Valid;
/**
* 短信验证码 API 接口
*
* @author 芋道源码
*/
public interface MailCodeApi {
/**
* 创建邮箱验证码并进行发送
*
* @param reqDTO 发送请求
*/
void sendMailCode(@Valid MailCodeSendReqDTO reqDTO);
/**
* 验证邮箱验证码并进行使用
* 如果正确则将验证码标记成已使用
* 如果错误则抛出 {@link ServiceException} 异常
*
* @param reqDTO 使用请求
*/
void useMailCode(@Valid MailCodeUseReqDTO reqDTO);
/**
* 检查验证码是否有效
*
* @param reqDTO 校验请求
*/
void validateMailCode(@Valid MailCodeValidateReqDTO reqDTO);
}

View File

@ -7,7 +7,7 @@ import javax.validation.constraints.NotNull;
import java.util.Map;
/**
* 发送 Request DTO
* 发送 Request DTO
*
* @author wangjingqi
*/

View File

@ -0,0 +1,37 @@
package cn.hangtag.module.system.api.mail.dto.code;
import cn.hangtag.framework.common.validation.Email;
import cn.hangtag.framework.common.validation.InEnum;
import cn.hangtag.module.system.enums.mail.MailSceneEnum;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
/**
* 邮箱验证码的发送 Request DTO
*
* @author 芋道源码
*/
@Data
public class MailCodeSendReqDTO {
/**
* 邮箱
*/
@Email
@NotEmpty(message = "邮箱不能为空")
private String mail;
/**
* 发送场景
*/
@NotNull(message = "发送场景不能为空")
@InEnum(MailSceneEnum.class)
private Integer scene;
/**
* 发送 IP
*/
@NotEmpty(message = "发送 IP 不能为空")
private String createIp;
}

View File

@ -0,0 +1,43 @@
package cn.hangtag.module.system.api.mail.dto.code;
import cn.hangtag.framework.common.validation.Email;
import cn.hangtag.framework.common.validation.InEnum;
import cn.hangtag.framework.common.validation.Mobile;
import cn.hangtag.module.system.enums.sms.SmsSceneEnum;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
/**
* 邮箱验证码的使用 Request DTO
*
* @author 芋道源码
*/
@Data
public class MailCodeUseReqDTO {
/**
* 邮箱
*/
@Email
@NotEmpty(message = "邮箱不能为空")
private String mail;
/**
* 发送场景
*/
@NotNull(message = "发送场景不能为空")
@InEnum(SmsSceneEnum.class)
private Integer scene;
/**
* 验证码
*/
@NotEmpty(message = "验证码")
private String code;
/**
* 使用 IP
*/
@NotEmpty(message = "使用 IP 不能为空")
private String usedIp;
}

View File

@ -0,0 +1,37 @@
package cn.hangtag.module.system.api.mail.dto.code;
import cn.hangtag.framework.common.validation.InEnum;
import cn.hangtag.framework.common.validation.Mobile;
import cn.hangtag.module.system.enums.sms.SmsSceneEnum;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
/**
* 邮箱验证码的校验 Request DTO
*
* @author 芋道源码
*/
@Data
public class MailCodeValidateReqDTO {
/**
* 手机号
*/
@Mobile
@NotEmpty(message = "邮箱不能为空")
private String mail;
/**
* 发送场景
*/
@NotNull(message = "发送场景不能为空")
@InEnum(SmsSceneEnum.class)
private Integer scene;
/**
* 验证码
*/
@NotEmpty(message = "验证码")
private String code;
}

View File

@ -0,0 +1,51 @@
package cn.hangtag.module.system.enums.mail;
import cn.hangtag.framework.common.core.IntArrayValuable;
import cn.hutool.core.util.ArrayUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 用户邮箱验证码发送场景的枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum MailSceneEnum implements IntArrayValuable {
MEMBER_LOGIN(1, "user-mail-login", "会员用户 - 邮箱登陆"),
MEMBER_UPDATE_MOBILE(2, "user-update-mail", "会员用户 - 修改手机"),
MEMBER_UPDATE_PASSWORD(3, "user-update-password", "会员用户 - 修改密码"),
MEMBER_RESET_PASSWORD(4, "user-reset-password", "会员用户 - 忘记密码"),
ADMIN_MEMBER_LOGIN(21, "admin-mail-login", "后台用户 - 邮箱登录");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(MailSceneEnum::getScene).toArray();
/**
* 验证场景的编号
*/
private final Integer scene;
/**
* 模版编码
*/
private final String templateCode;
/**
* 描述
*/
private final String description;
@Override
public int[] array() {
return ARRAYS;
}
public static MailSceneEnum getCodeByScene(Integer scene) {
return ArrayUtil.firstMatch(sceneEnum -> sceneEnum.getScene().equals(scene),
values());
}
}

View File

@ -0,0 +1,44 @@
package cn.hangtag.module.system.api.mail;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeSendReqDTO;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeUseReqDTO;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeValidateReqDTO;
import cn.hangtag.module.system.api.sms.SmsCodeApi;
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeValidateReqDTO;
import cn.hangtag.module.system.service.mail.MailCodeService;
import cn.hangtag.module.system.service.sms.SmsCodeService;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
/**
* 短信验证码 API 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class MailCodeApiImpl implements MailCodeApi {
@Resource
private MailCodeService mailCodeService;
@Override
public void sendMailCode(MailCodeSendReqDTO reqDTO) {
mailCodeService.sendMailCode(reqDTO);
}
@Override
public void useMailCode(MailCodeUseReqDTO reqDTO) {
mailCodeService.useMailCode(reqDTO);
}
@Override
public void validateMailCode(MailCodeValidateReqDTO reqDTO) {
mailCodeService.validateMailCode(reqDTO);
}
}

View File

@ -146,6 +146,27 @@ public class AuthController {
return success(true);
}
// ========== 邮箱相关 ==========
@PostMapping("/send-mail-code")
@PermitAll
@Operation(summary = "发送邮箱验证码")
public CommonResult<Boolean> sendForgetPasswordEmailCode(@RequestBody @Valid AuthMailSendReqVO reqVO) {
authService.sendMailCode(reqVO);
return success(true);
}
@PostMapping("/mail-modifypwd")
@PermitAll
@Operation(summary = "使用邮箱验证码修改密码")
public CommonResult<Boolean> smsLogin(@RequestBody @Valid AuthMailModifyPwdReqVO reqVO) {
authService.mailResetPwd(reqVO);
return success(true);
}
// ========== 社交登录相关 ==========
@GetMapping("/social-auth-redirect")

View File

@ -23,8 +23,8 @@ public class AuthLoginReqVO {
@Schema(description = "账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "hangtagyuanma")
@NotEmpty(message = "登录账号不能为空")
@Length(min = 4, max = 16, message = "账号长度为 4-16 位")
@Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
//@Length(min = 4, max = 16, message = "账号长度为 4-16 位")
//@Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
private String username;
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao")

View File

@ -0,0 +1,41 @@
package cn.hangtag.module.system.controller.admin.auth.vo;
import cn.hangtag.framework.common.validation.Email;
import cn.hangtag.framework.common.validation.Mobile;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotEmpty;
@Schema(description = "管理后台 - 邮箱验证码的重置密码 Request VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AuthMailModifyPwdReqVO {
@Schema(description = "邮箱账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "xxxxx@qq.com")
@NotEmpty(message = "邮箱账号不能为空")
@Email
private String mail;
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "xxxxx@qq.com")
@NotEmpty(message = "密码不能为空")
private String password;
@Schema(description = "确认密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "xxxxx@qq.com")
@NotEmpty(message = "确认密码不能为空")
private String checkpassword;
@Schema(description = "短信验证码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotEmpty(message = "验证码不能为空")
private String code;
@Schema(description = "账户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotEmpty(message = "账户类型不能为空")
private Integer type;
}

View File

@ -0,0 +1,36 @@
package cn.hangtag.module.system.controller.admin.auth.vo;
import cn.hangtag.framework.common.validation.Email;
import cn.hangtag.framework.common.validation.InEnum;
import cn.hangtag.framework.common.validation.Mobile;
import cn.hangtag.module.system.enums.mail.MailSceneEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@Schema(description = "管理后台 - 发送手机验证码 Request VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AuthMailSendReqVO {
@Schema(description = "邮箱账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "xxxxx@qq.com")
@NotEmpty(message = "邮箱账号不能为空")
@Email
private String mail;
@Schema(description = "邮箱场景", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "发送场景不能为空")
@InEnum(MailSceneEnum.class)
private Integer scene;
@Schema(description = "账户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "账户类型不能为空")
private Integer type;
}

View File

@ -1,5 +1,7 @@
package cn.hangtag.module.system.convert.auth;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeSendReqDTO;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeUseReqDTO;
import cn.hutool.core.collection.CollUtil;
import cn.hangtag.framework.common.util.object.BeanUtils;
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
@ -85,4 +87,8 @@ public interface AuthConvert {
SmsCodeUseReqDTO convert(AuthSmsLoginReqVO reqVO, Integer scene, String usedIp);
MailCodeSendReqDTO convert(AuthMailSendReqVO reqVO);
MailCodeUseReqDTO convert(AuthMailModifyPwdReqVO reqVO, Integer scene, String usedIp);
}

View File

@ -0,0 +1,65 @@
package cn.hangtag.module.system.dal.dataobject.mail;
import cn.hangtag.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
* 邮箱验证码 DO
*
* idx_mobile 索引基于 {@link #mobile} 字段
*
* @author 芋道源码
*/
@TableName("system_mail_code")
@KeySequence("system_mail_code_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MailCodeDO extends BaseDO {
/**
* 编号
*/
private Long id;
/**
* 邮箱号
*/
private String mail;
/**
* 验证码
*/
private String code;
/**
* 发送场景
*
* 枚举 {@link MailCodeDO}
*/
private Integer scene;
/**
* 创建 IP
*/
private String createIp;
/**
* 今日发送的第几条
*/
private Integer todayIndex;
/**
* 是否使用
*/
private Boolean used;
/**
* 使用时间
*/
private LocalDateTime usedTime;
/**
* 使用 IP
*/
private String usedIp;
}

View File

@ -0,0 +1,29 @@
package cn.hangtag.module.system.dal.mysql.mail;
import cn.hangtag.framework.mybatis.core.mapper.BaseMapperX;
import cn.hangtag.framework.mybatis.core.query.QueryWrapperX;
import cn.hangtag.module.system.dal.dataobject.mail.MailCodeDO;
import cn.hangtag.module.system.dal.dataobject.sms.SmsCodeDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MailCodeMapper extends BaseMapperX<MailCodeDO> {
/**
* 获得手机号的最后一个手机验证码
*
* @param mail 手机号
* @param scene 发送场景选填
* @param code 验证码 选填
* @return 手机验证码
*/
default MailCodeDO selectLastByMail(String mail, String code, Integer scene) {
return selectOne(new QueryWrapperX<MailCodeDO>()
.eq("mail", mail)
.eqIfPresent("scene", scene)
.eqIfPresent("code", code)
.orderByDesc("id")
.limitN(1));
}
}

View File

@ -25,6 +25,10 @@ public interface AdminUserMapper extends BaseMapperX<AdminUserDO> {
return selectOne(AdminUserDO::getEmail, email);
}
default AdminUserDO selectByEmail(String email,Integer type) {
return selectOne(AdminUserDO::getEmail, email,AdminUserDO::getDeptId,type);
}
default AdminUserDO selectByMobile(String mobile) {
return selectOne(AdminUserDO::getMobile, mobile);
}

View File

@ -70,4 +70,14 @@ public interface AdminAuthService {
*/
AuthLoginRespVO refreshToken(String refreshToken);
/**
* 邮箱验证码发送
*
* @param reqVO 发送请求
*/
void sendMailCode(AuthMailSendReqVO reqVO);
boolean mailResetPwd(AuthMailModifyPwdReqVO reqVO);
}

View File

@ -1,5 +1,8 @@
package cn.hangtag.module.system.service.auth;
import cn.hangtag.module.system.api.mail.MailCodeApi;
import cn.hangtag.module.system.dal.mysql.user.AdminUserMapper;
import cn.hangtag.module.system.enums.mail.MailSceneEnum;
import cn.hutool.core.util.ObjectUtil;
import cn.hangtag.framework.common.enums.CommonStatusEnum;
import cn.hangtag.framework.common.enums.UserTypeEnum;
@ -29,6 +32,7 @@ import com.xingyuv.captcha.model.vo.CaptchaVO;
import com.xingyuv.captcha.service.CaptchaService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@ -64,6 +68,13 @@ public class AdminAuthServiceImpl implements AdminAuthService {
private CaptchaService captchaService;
@Resource
private SmsCodeApi smsCodeApi;
@Resource
private MailCodeApi mailCodeApi;
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private AdminUserMapper userMapper;
/**
* 验证码的开关默认为 true
@ -247,4 +258,43 @@ public class AdminAuthServiceImpl implements AdminAuthService {
return UserTypeEnum.ADMIN;
}
@Override
public void sendMailCode(AuthMailSendReqVO reqVO) {
// 登录场景验证是否存在
if (userService.getUserByMail(reqVO.getMail(),reqVO.getType()) == null) {
throw exception(MAIL_ACCOUNT_NOT_EXISTS);
}
// 发送验证码
mailCodeApi.sendMailCode(AuthConvert.INSTANCE.convert(reqVO).setCreateIp(getClientIP()));
}
@Override
public boolean mailResetPwd(AuthMailModifyPwdReqVO reqVO) {
// 校验验证码
mailCodeApi.useMailCode(AuthConvert.INSTANCE.convert(reqVO, MailSceneEnum.MEMBER_RESET_PASSWORD.getScene(), getClientIP()));
// 获得用户信息
AdminUserDO user = userService.getUserByMail(reqVO.getMail(),reqVO.getType());
if (user == null) {
throw exception(USER_NOT_EXISTS);
}
// 执行更新
user.setPassword(encodePassword(reqVO.getPassword())); // 加密密码
int i = userMapper.updateById(user);
return true;
}
/**
* 对密码进行加密
*
* @param password 密码
* @return 加密后的密码
*/
private String encodePassword(String password) {
return passwordEncoder.encode(password);
}
}

View File

@ -0,0 +1,42 @@
package cn.hangtag.module.system.service.mail;
import cn.hangtag.framework.common.exception.ServiceException;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeSendReqDTO;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeUseReqDTO;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeValidateReqDTO;
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeValidateReqDTO;
import javax.validation.Valid;
/**
* 短信验证码 Service 接口
*
* @author 芋道源码
*/
public interface MailCodeService {
/**
* 创建邮件验证码并进行发送
*
* @param reqDTO 发送请求
*/
void sendMailCode(@Valid MailCodeSendReqDTO reqDTO);
/**
* 验证邮件验证码并进行使用
* 如果正确则将验证码标记成已使用
* 如果错误则抛出 {@link ServiceException} 异常
*
* @param reqDTO 使用请求
*/
void useMailCode(@Valid MailCodeUseReqDTO reqDTO);
/**
* 检查验证码是否有效
*
* @param reqDTO 校验请求
*/
void validateMailCode(@Valid MailCodeValidateReqDTO reqDTO);
}

View File

@ -0,0 +1,122 @@
package cn.hangtag.module.system.service.mail;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeSendReqDTO;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeUseReqDTO;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeValidateReqDTO;
import cn.hangtag.module.system.dal.dataobject.mail.MailCodeDO;
import cn.hangtag.module.system.dal.dataobject.sms.SmsCodeDO;
import cn.hangtag.module.system.dal.mysql.mail.MailCodeMapper;
import cn.hangtag.module.system.enums.mail.MailSceneEnum;
import cn.hangtag.module.system.enums.sms.SmsSceneEnum;
import cn.hangtag.module.system.framework.sms.config.SmsCodeProperties;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import static cn.hangtag.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.hangtag.framework.common.util.date.DateUtils.isToday;
import static cn.hangtag.module.system.enums.ErrorCodeConstants.*;
import static cn.hutool.core.util.RandomUtil.randomInt;
/**
* 邮箱验证码 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class MailCodeServiceImpl implements MailCodeService {
@Resource
private SmsCodeProperties smsCodeProperties;
@Resource
private MailCodeMapper mailCodeMapper;
@Resource
private MailSendService mailSendService;
@Override
public void sendMailCode(MailCodeSendReqDTO reqDTO) {
MailSceneEnum sceneEnum = MailSceneEnum.getCodeByScene(reqDTO.getScene());
Assert.notNull(sceneEnum, "验证码场景({}) 查找不到配置", reqDTO.getScene());
// 创建验证码
String code = createMailCode(reqDTO.getMail(), reqDTO.getScene(), reqDTO.getCreateIp());
LinkedHashMap<String, Object> params = new LinkedHashMap<>();
params.put("key1",reqDTO.getMail());
params.put("key2",reqDTO.getMail());
params.put("key3",code);
String templateCode = sceneEnum.getTemplateCode();
// 发送验证码
mailSendService.sendSingleMail(reqDTO.getMail(), null, null,
sceneEnum.getTemplateCode(), params);
}
private String createMailCode(String mobile, Integer scene, String ip) {
// 校验是否可以发送验证码不用筛选场景
MailCodeDO lastMailCode = mailCodeMapper.selectLastByMail(mobile, null, null);
if (lastMailCode != null) {
if (LocalDateTimeUtil.between(lastMailCode.getCreateTime(), LocalDateTime.now()).toMillis()
< smsCodeProperties.getSendFrequency().toMillis()) { // 发送过于频繁
throw exception(SMS_CODE_SEND_TOO_FAST);
}
if (isToday(lastMailCode.getCreateTime()) && // 必须是今天才能计算超过当天的上限
lastMailCode.getTodayIndex() >= smsCodeProperties.getSendMaximumQuantityPerDay()) { // 超过当天发送的上限
throw exception(SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY);
}
// TODO 芋艿提升每个 IP 每天可发送数量
// TODO 芋艿提升每个 IP 每小时可发送数量
}
// 创建验证码记录
String code = String.format("%0" + smsCodeProperties.getEndCode().toString().length() + "d",
randomInt(smsCodeProperties.getBeginCode(), smsCodeProperties.getEndCode() + 1));
MailCodeDO newSmsCode = MailCodeDO.builder().mail(mobile).code(code).scene(scene)
.todayIndex(lastMailCode != null && isToday(lastMailCode.getCreateTime()) ? lastMailCode.getTodayIndex() + 1 : 1)
.createIp(ip).used(false).build();
mailCodeMapper.insert(newSmsCode);
return code;
}
@Override
public void useMailCode(MailCodeUseReqDTO reqDTO) {
// 检测验证码是否有效
MailCodeDO lastMailCode = validateMailCode0(reqDTO.getMail(), reqDTO.getCode(), reqDTO.getScene());
// 使用验证码
mailCodeMapper.updateById(MailCodeDO.builder().id(lastMailCode.getId())
.used(true).usedTime(LocalDateTime.now()).usedIp(reqDTO.getUsedIp()).build());
}
@Override
public void validateMailCode(MailCodeValidateReqDTO reqDTO) {
validateMailCode0(reqDTO.getMail(), reqDTO.getCode(), reqDTO.getScene());
}
private MailCodeDO validateMailCode0(String mobile, String code, Integer scene) {
// 校验验证码
MailCodeDO lastMailCode = mailCodeMapper.selectLastByMail(mobile, code, scene);
// 若验证码不存在抛出异常
if (lastMailCode == null) {
throw exception(SMS_CODE_NOT_FOUND);
}
// 超过时间
if (LocalDateTimeUtil.between(lastMailCode.getCreateTime(), LocalDateTime.now()).toMillis()
>= smsCodeProperties.getExpireTimes().toMillis()) { // 验证码已过期
throw exception(SMS_CODE_EXPIRED);
}
// 判断验证码是否已被使用
if (Boolean.TRUE.equals(lastMailCode.getUsed())) {
throw exception(SMS_CODE_USED);
}
return lastMailCode;
}
}

View File

@ -12,6 +12,7 @@ import cn.hangtag.module.system.dal.mysql.mail.MailTemplateMapper;
import cn.hangtag.module.system.dal.redis.RedisKeyConstants;
import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@ -41,7 +42,7 @@ public class MailTemplateServiceImpl implements MailTemplateService {
/**
* 正则表达式匹配 {} 中的变量
*/
private static final Pattern PATTERN_PARAMS = Pattern.compile("\\{(.*?)}");
private static final Pattern PATTERN_PARAMS = Pattern.compile("\\{\\{(.*?)}}");
@Resource
private MailTemplateMapper mailTemplateMapper;
@ -122,6 +123,12 @@ public class MailTemplateServiceImpl implements MailTemplateService {
@Override
public String formatMailTemplateContent(String content, Map<String, Object> params) {
if(StringUtils.isNotBlank(content)){
/* content = content.replaceFirst("<p>", "").replace("</p>", "");
content = content.replaceAll("&lt;", "<").replaceAll("&gt;", ">");
content = content.replaceAll("&nbsp;", "");*/
content = content.replaceAll("\\{\\{", "{").replaceAll("}}","}");
}
return StrUtil.format(content, params);
}

View File

@ -201,4 +201,15 @@ public interface AdminUserService {
*/
boolean isPasswordMatch(String rawPassword, String encodedPassword);
/**
* 通过邮箱号获取用户
*
* @param mail 邮箱号
* @return 用户对象信息
*/
AdminUserDO getUserByMail(String mail,Integer type);
}

View File

@ -496,6 +496,11 @@ public class AdminUserServiceImpl implements AdminUserService {
return passwordEncoder.matches(rawPassword, encodedPassword);
}
@Override
public AdminUserDO getUserByMail(String mail,Integer type) {
return userMapper.selectByEmail(mail,type);
}
/**
* 对密码进行加密
*

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -46,7 +46,7 @@
<div class="flex flex-col">
<div>
<el-form label-width="180px">
<el-form label-width="200px">
<el-form-item label="Style" v-show="that.draftDesignList.length > 1">
<div class="flex ml-3">
<div>
@ -66,7 +66,7 @@
</div>
<div class="w-full" v-if="that.propOrderByList && that.propOrderByList.length > 0">
<el-scrollbar height="600px">
<el-form label-width="180px">
<el-form label-width="200px">
<el-form-item
v-for="(tmp,orderIndex) in that.propOrderByList"
@ -134,8 +134,9 @@
:class="{
'error_tip': errorItem(tmp.key,i)
}" v-if="that.propInfo[tmp.key].canInput">
<el-autocomplete
style="min-width: 200px;width: 60%"
style="min-width: 320px;width: 100%"
v-model="that.propInfo[tmp.key].dataInfo[i].showLabel"
:fetch-suggestions="querySearch"
clearable
@ -500,6 +501,7 @@ const changeIconData = (index: number, key: string) => {
for (let i = 0; i < infoList.length; i++) {
mapping[infoList[i].label] = infoList[i].langMapping;
}
that.propInfo[key].processType = findProcessTypeIndex(index);
//
const allData = [...that.propInfo[key].dataInfo]
const fLang = allData[0].locale;
@ -563,7 +565,7 @@ const findProcessTypeIndex = (index) => {
const washingInfoListByType = (type, index = -1) => {
const typeIndex = findProcessTypeIndex(index);
return that.washingInfoList.filter(item => {
if (item.type === type && (typeIndex < 0 || (item.processType === `${typeIndex}`))) {
if (`${item.type}` === `${type}` && (typeIndex < 0 || (`${item.processType}` === `${typeIndex}`))) {
return item
}
})
@ -896,18 +898,17 @@ const checkPropInfo = (info) => {
for (let i = 0; i < that.propOrderByList.length; i++) {
keys.push(that.propOrderByList[i].key);
}
endloop: for (let i = 0; i < keys.length; i++) {
console.log("keys",keys)
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
//
let newArr = [];
//
const allLang = findGroupAllLang(key, newInfo2);
const firstLang = newInfo[key].dataInfo[0].locale;
const linkChar = newInfo[key].linkChar || '';
const dataInfoArr = newInfo[key].dataInfo;
let langList = newInfo[key].langList || [];
console.log("newInfo[key].groupType",newInfo[key])
if (newInfo[key].groupType === GroupTypeEnum.RATIO) {
//
//
let langSort = newInfo[key].langSort || [];
@ -937,6 +938,8 @@ const checkPropInfo = (info) => {
for (let j = 0; j < langSort.length; j++) {
const labelInfo = []
let allRatio = 0; //
const langLabel = [];
let linkChar = ","
for (let k = 0; k < langList.length; k++) {
if (langSort[j] === langList[k].locale) {
let tmpValue = `${langList[k].value}`.replaceAll('${r}', `${str}`)
@ -946,9 +949,17 @@ const checkPropInfo = (info) => {
if (str === null || str === undefined || str === -1) {
str = ''
}
mergeLabel.push(tmpValue)
console.log("langList[k]",langList[k])
if(langList[k].linkChar && linkChar != ','){
linkChar = langList[k]
}
langLabel.push(tmpValue)
}
}
if(langLabel.length > 0){
mergeLabel.push(langLabel.join(linkChar))
}
let flag = false;
//
for (let k = 0; k < dataInfoArr.length; k++) {
@ -1006,7 +1017,6 @@ const checkPropInfo = (info) => {
if (newInfo[kKey].groupName === newInfo[key].parentId) {
newKey = kKey;
parentStr = newInfo[kKey].dataInfo[0].label + '<br/>'
break;
}
}
@ -1021,24 +1031,28 @@ const checkPropInfo = (info) => {
//
if (newInfo[key].multiLanguage) {
const showLabel1 = newInfo[key].dataInfo[0].showLabel
if (newInfo[key].groupType === GroupTypeEnum.ICON) {
const infoList = washingInfoListByType(newInfo[key].groupType)
const infos = newInfo[key].dataInfo;
for (let j = 1; j < infos.length; j++) {
outerLoop: for (let i = 0; i < infoList.length; i++) {
const row1 = infoList[i];
for (let k = 0; k < row1.langMapping.length; k++) {
// icon url
if (infos[j].locale === row1.langMapping[k].locale) {
infos[j].label = row1.langMapping[k].value
break outerLoop;
if(`${row1.processType}` === `${ newInfo[key].processType}`){
for (let k = 0; k < row1.langMapping.length; k++) {
// icon url
if (infos[j].locale === row1.langMapping[k].locale && showLabel1 === row1.value) {
infos[j].label = row1.langMapping[k].value
break outerLoop;
}
}
}
}
}
console.log("newArr33",infos)
newInfo[key].dataInfo = infos;
break;
}else {
const infoList = washingInfoListByType(newInfo[key].groupType)
const infos = newInfo[key].dataInfo;
@ -1054,41 +1068,55 @@ const checkPropInfo = (info) => {
}
}
}
console.log("newArr33",infos)
console.log("newArr99",infos)
newInfo[key].dataInfo = infos;
break;
}
} else {
//
//
if (newInfo[key].groupType === GroupTypeEnum.TEXT) {
let langSort = newInfo[key].langSort || [];
if (langSort.length === 0) {
langSort = allLang
}
let mergeLabel = [];
for (let j = 0; j < langSort.length; j++) {
const labelInfo = []
for (let k = 0; k < langList.length; k++) {
if (langSort[j] === langList[k].locale && newInfo[key].dataInfo[k].showLabel) {
mergeLabel.push(langList[k].value)
if(newInfo[key].canInput){
for (let j = 0; j < dataInfoArr.length; j++) {
if (dataInfoArr[j].showLabel) {
if(mergeLabel.includes(dataInfoArr[j].showLabel)){
useMessage().notifyWarning(`${newInfo[key].groupName}中,第${j + 1}项重复${dataInfoArr[j].showLabel}`)
}
mergeLabel.push(dataInfoArr[j].showLabel)
}
}
//
for (let k = 0; k < dataInfoArr.length; k++) {
if (langSort[j] === newInfo[key].dataInfo[k].locale) {
if (newInfo[key].dataInfo[k].showLabel) {
if (labelInfo.includes(newInfo[key].dataInfo[k].showLabel)) {
useMessage().notifyError(`${newInfo[key].groupName}中第${labelInfo.length + 1}项重复`);
that.errorList.push({
key: `${key}_${k}`,
message: "数据重复"
})
reject("数据重复")
return
}else {
for (let j = 0; j < langSort.length; j++) {
const labelInfo = []
for (let k = 0; k < langList.length; k++) {
console.log("newInfo[key].dataInfo[k]",newInfo[key].dataInfo[k])
if (langSort[j] === langList[k].locale) {
mergeLabel.push(langList[k].value)
}
}
//
for (let k = 0; k < dataInfoArr.length; k++) {
if (langSort[j] === newInfo[key].dataInfo[k].locale) {
if (newInfo[key].dataInfo[k].showLabel) {
if (labelInfo.includes(newInfo[key].dataInfo[k].showLabel)) {
useMessage().notifyError(`${newInfo[key].groupName}中第${labelInfo.length + 1}项重复`);
that.errorList.push({
key: `${key}_${k}`,
message: "数据重复"
})
reject("数据重复")
return
}
labelInfo.push(newInfo[key].dataInfo[k].showLabel)
}
labelInfo.push(newInfo[key].dataInfo[k].showLabel)
}
}
}
@ -1374,7 +1402,7 @@ defineExpose({
}
.error_tip {
box-shadow: rgb(255, 0, 0) 0px 0px 2px 1px;
box-shadow: rgb(255, 0, 0) 0 0 2px 1px;
}
</style>

View File

@ -49,7 +49,7 @@
<el-row>
<el-col :span="6">
<div style="width: 120px;display: flex">
<el-checkbox
<el-checkbox :disabled="disableInput"
v-model="that.configInfo.canChange">
<span>允许调整数量</span>
</el-checkbox>

View File

@ -1462,6 +1462,7 @@ const handlerGroupList = (cells, isCombo = false, min, max) => {
canChange = that.pageConfig.propList[i].canChange === true
canInput = that.pageConfig.propList[i].canInput === true
langSort = that.pageConfig.propList[i].langSort || []
linkChar = that.pageConfig.propList[i].linkChar || ','
break;
}
}

View File

@ -37,7 +37,7 @@
<el-row>
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="formData.email" placeholder="请输入邮箱" />
<el-input v-model="formData.email" placeholder="请输入邮箱" :disabled="formType=='update'" />
</el-form-item>
</el-col>
<el-col :span="12">
@ -175,6 +175,7 @@ const formRules = reactive({
}
],
email: [
{ required: true, message: '邮箱不能为空', trigger: 'blur' },
{
type: 'email',
message: '请输入正确的邮箱地址',

View File

@ -95,6 +95,9 @@ importers:
diagram-js:
specifier: ^12.8.0
version: 12.8.1
dom-to-image:
specifier: ^2.6.0
version: 2.6.0
driver.js:
specifier: ^1.3.1
version: 1.3.1
@ -2802,6 +2805,9 @@ packages:
dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
dom-to-image@2.6.0:
resolution: {integrity: sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA==}
dom-walk@0.1.2:
resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==}
@ -7410,7 +7416,7 @@ snapshots:
'@types/web-bluetooth': 0.0.16
'@vueuse/metadata': 9.13.0
'@vueuse/shared': 9.13.0(vue@3.4.21(typescript@5.3.3))
vue-demi: 0.14.7(vue@3.4.21(typescript@5.3.3))
vue-demi: 0.14.10(vue@3.4.21(typescript@5.3.3))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
@ -7421,14 +7427,14 @@ snapshots:
'@vueuse/shared@10.9.0(vue@3.4.21(typescript@5.3.3))':
dependencies:
vue-demi: 0.14.7(vue@3.4.21(typescript@5.3.3))
vue-demi: 0.14.10(vue@3.4.21(typescript@5.3.3))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@vueuse/shared@9.13.0(vue@3.4.21(typescript@5.3.3))':
dependencies:
vue-demi: 0.14.7(vue@3.4.21(typescript@5.3.3))
vue-demi: 0.14.10(vue@3.4.21(typescript@5.3.3))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
@ -8217,6 +8223,8 @@ snapshots:
domhandler: 5.0.3
entities: 4.5.0
dom-to-image@2.6.0: {}
dom-walk@0.1.2: {}
dom7@3.0.0:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -12,6 +12,21 @@ export interface SmsLoginVO {
code: string
}
export interface MailCodeVO {
mail: string
scene: number
type: number
}
export interface MailModifyPwdVO {
mail: string
password: string
checkpassword: string
code: string
type: number
}
// 登录
export const login = (data: UserLoginVO) => {
return request.post({ url: '/system/auth/login', data })
@ -47,6 +62,16 @@ export const sendSmsCode = (data: SmsCodeVO) => {
return request.post({ url: '/system/auth/send-sms-code', data })
}
//获取邮箱登录验证码
export const sendMailCode = (data: MailCodeVO) => {
return request.post({ url: '/system/auth/send-mail-code', data })
}
// 邮箱码修改密码
export const mailModifyPwd = (data: MailModifyPwdVO) => {
return request.post({ url: '/system/auth/mail-modifypwd', data })
}
// 短信验证码登录
export const smsLogin = (data: SmsLoginVO) => {
return request.post({ url: '/system/auth/sms-login', data })

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -46,7 +46,7 @@
<div class="flex flex-col">
<div>
<el-form label-width="180px">
<el-form label-width="200px">
<el-form-item label="Style" v-show="that.draftDesignList.length > 1">
<div class="flex ml-3">
<div>
@ -66,7 +66,7 @@
</div>
<div class="w-full" v-if="that.propOrderByList && that.propOrderByList.length > 0">
<el-scrollbar height="600px">
<el-form label-width="180px">
<el-form label-width="200px">
<el-form-item
v-for="(tmp,orderIndex) in that.propOrderByList"
@ -134,8 +134,9 @@
:class="{
'error_tip': errorItem(tmp.key,i)
}" v-if="that.propInfo[tmp.key].canInput">
<el-autocomplete
style="min-width: 200px;width: 60%"
style="min-width: 320px;width: 100%"
v-model="that.propInfo[tmp.key].dataInfo[i].showLabel"
:fetch-suggestions="querySearch"
clearable
@ -500,6 +501,7 @@ const changeIconData = (index: number, key: string) => {
for (let i = 0; i < infoList.length; i++) {
mapping[infoList[i].label] = infoList[i].langMapping;
}
that.propInfo[key].processType = findProcessTypeIndex(index);
//
const allData = [...that.propInfo[key].dataInfo]
const fLang = allData[0].locale;
@ -563,7 +565,7 @@ const findProcessTypeIndex = (index) => {
const washingInfoListByType = (type, index = -1) => {
const typeIndex = findProcessTypeIndex(index);
return that.washingInfoList.filter(item => {
if (item.type === type && (typeIndex < 0 || (item.processType === `${typeIndex}`))) {
if (`${item.type}` === `${type}` && (typeIndex < 0 || (`${item.processType}` === `${typeIndex}`))) {
return item
}
})
@ -896,18 +898,17 @@ const checkPropInfo = (info) => {
for (let i = 0; i < that.propOrderByList.length; i++) {
keys.push(that.propOrderByList[i].key);
}
endloop: for (let i = 0; i < keys.length; i++) {
console.log("keys",keys)
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
//
let newArr = [];
//
const allLang = findGroupAllLang(key, newInfo2);
const firstLang = newInfo[key].dataInfo[0].locale;
const linkChar = newInfo[key].linkChar || '';
const dataInfoArr = newInfo[key].dataInfo;
let langList = newInfo[key].langList || [];
console.log("newInfo[key].groupType",newInfo[key])
if (newInfo[key].groupType === GroupTypeEnum.RATIO) {
//
//
let langSort = newInfo[key].langSort || [];
@ -937,6 +938,8 @@ const checkPropInfo = (info) => {
for (let j = 0; j < langSort.length; j++) {
const labelInfo = []
let allRatio = 0; //
const langLabel = [];
let linkChar = ","
for (let k = 0; k < langList.length; k++) {
if (langSort[j] === langList[k].locale) {
let tmpValue = `${langList[k].value}`.replaceAll('${r}', `${str}`)
@ -946,9 +949,17 @@ const checkPropInfo = (info) => {
if (str === null || str === undefined || str === -1) {
str = ''
}
mergeLabel.push(tmpValue)
console.log("langList[k]",langList[k])
if(langList[k].linkChar && linkChar != ','){
linkChar = langList[k]
}
langLabel.push(tmpValue)
}
}
if(langLabel.length > 0){
mergeLabel.push(langLabel.join(linkChar))
}
let flag = false;
//
for (let k = 0; k < dataInfoArr.length; k++) {
@ -1006,7 +1017,6 @@ const checkPropInfo = (info) => {
if (newInfo[kKey].groupName === newInfo[key].parentId) {
newKey = kKey;
parentStr = newInfo[kKey].dataInfo[0].label + '<br/>'
break;
}
}
@ -1021,24 +1031,28 @@ const checkPropInfo = (info) => {
//
if (newInfo[key].multiLanguage) {
const showLabel1 = newInfo[key].dataInfo[0].showLabel
if (newInfo[key].groupType === GroupTypeEnum.ICON) {
const infoList = washingInfoListByType(newInfo[key].groupType)
const infos = newInfo[key].dataInfo;
for (let j = 1; j < infos.length; j++) {
outerLoop: for (let i = 0; i < infoList.length; i++) {
const row1 = infoList[i];
for (let k = 0; k < row1.langMapping.length; k++) {
// icon url
if (infos[j].locale === row1.langMapping[k].locale) {
infos[j].label = row1.langMapping[k].value
break outerLoop;
if(`${row1.processType}` === `${ newInfo[key].processType}`){
for (let k = 0; k < row1.langMapping.length; k++) {
// icon url
if (infos[j].locale === row1.langMapping[k].locale && showLabel1 === row1.value) {
infos[j].label = row1.langMapping[k].value
break outerLoop;
}
}
}
}
}
console.log("newArr33",infos)
newInfo[key].dataInfo = infos;
break;
}else {
const infoList = washingInfoListByType(newInfo[key].groupType)
const infos = newInfo[key].dataInfo;
@ -1054,41 +1068,55 @@ const checkPropInfo = (info) => {
}
}
}
console.log("newArr33",infos)
console.log("newArr99",infos)
newInfo[key].dataInfo = infos;
break;
}
} else {
//
//
if (newInfo[key].groupType === GroupTypeEnum.TEXT) {
let langSort = newInfo[key].langSort || [];
if (langSort.length === 0) {
langSort = allLang
}
let mergeLabel = [];
for (let j = 0; j < langSort.length; j++) {
const labelInfo = []
for (let k = 0; k < langList.length; k++) {
if (langSort[j] === langList[k].locale && newInfo[key].dataInfo[k].showLabel) {
mergeLabel.push(langList[k].value)
if(newInfo[key].canInput){
for (let j = 0; j < dataInfoArr.length; j++) {
if (dataInfoArr[j].showLabel) {
if(mergeLabel.includes(dataInfoArr[j].showLabel)){
useMessage().notifyWarning(`${newInfo[key].groupName}中,第${j + 1}项重复${dataInfoArr[j].showLabel}`)
}
mergeLabel.push(dataInfoArr[j].showLabel)
}
}
//
for (let k = 0; k < dataInfoArr.length; k++) {
if (langSort[j] === newInfo[key].dataInfo[k].locale) {
if (newInfo[key].dataInfo[k].showLabel) {
if (labelInfo.includes(newInfo[key].dataInfo[k].showLabel)) {
useMessage().notifyError(`${newInfo[key].groupName}中第${labelInfo.length + 1}项重复`);
that.errorList.push({
key: `${key}_${k}`,
message: "数据重复"
})
reject("数据重复")
return
}else {
for (let j = 0; j < langSort.length; j++) {
const labelInfo = []
for (let k = 0; k < langList.length; k++) {
console.log("newInfo[key].dataInfo[k]",newInfo[key].dataInfo[k])
if (langSort[j] === langList[k].locale) {
mergeLabel.push(langList[k].value)
}
}
//
for (let k = 0; k < dataInfoArr.length; k++) {
if (langSort[j] === newInfo[key].dataInfo[k].locale) {
if (newInfo[key].dataInfo[k].showLabel) {
if (labelInfo.includes(newInfo[key].dataInfo[k].showLabel)) {
useMessage().notifyError(`${newInfo[key].groupName}中第${labelInfo.length + 1}项重复`);
that.errorList.push({
key: `${key}_${k}`,
message: "数据重复"
})
reject("数据重复")
return
}
labelInfo.push(newInfo[key].dataInfo[k].showLabel)
}
labelInfo.push(newInfo[key].dataInfo[k].showLabel)
}
}
}
@ -1374,7 +1402,7 @@ defineExpose({
}
.error_tip {
box-shadow: rgb(255, 0, 0) 0px 0px 2px 1px;
box-shadow: rgb(255, 0, 0) 0 0 2px 1px;
}
</style>

View File

@ -49,7 +49,7 @@
<el-row>
<el-col :span="6">
<div style="width: 120px;display: flex">
<el-checkbox
<el-checkbox :disabled="disableInput"
v-model="that.configInfo.canChange">
<span>允许调整数量</span>
</el-checkbox>

View File

@ -1462,6 +1462,7 @@ const handlerGroupList = (cells, isCombo = false, min, max) => {
canChange = that.pageConfig.propList[i].canChange === true
canInput = that.pageConfig.propList[i].canInput === true
langSort = that.pageConfig.propList[i].langSort || []
linkChar = that.pageConfig.propList[i].linkChar || ','
break;
}
}

View File

@ -114,7 +114,7 @@ export default {
},
login: {
welcome: 'Welcome to the system',
message: 'Backstage management system',
message: '',
tenantname: 'TenantName',
username: 'Username',
password: 'Password',
@ -133,14 +133,20 @@ export default {
codePlaceholder: 'Please Enter Verification Code',
mobileTitle: 'Mobile sign in',
mobileNumber: 'Mobile Number',
mobileNumberPlaceholder: 'Plaease Enter Mobile Number',
mobileNumberPlaceholder: 'Plaease Enter Mobile Code',
emailNumberPlaceholder: 'Plaease Enter Email Code',
backLogin: 'back',
getSmsCode: 'Get SMS Code',
getMailCode: 'Get Mail Code',
btnMobile: 'Mobile sign in',
btnQRCode: 'QR code sign in',
qrcode: 'Scan the QR code to log in',
btnRegister: 'Sign up',
SmsSendMsg: 'code has been sent'
SmsSendMsg: 'code has been sent',
pwdResetSuccess: 'password reset success'
},
forgetpassword: {
resetpwd: 'reset pwd',
},
captcha: {
verification: 'Please complete security verification',

View File

@ -134,13 +134,19 @@ export default {
mobileTitle: '手机登录',
mobileNumber: '手机号码',
mobileNumberPlaceholder: '请输入手机号码',
emailNumberPlaceholder: '请输入邮箱号码',
backLogin: '返回',
getSmsCode: '获取验证码',
getMailCode: '获取验证码',
btnMobile: '手机登录',
btnQRCode: '二维码登录',
qrcode: '扫描二维码登录',
btnRegister: '注册',
SmsSendMsg: '验证码已发送'
SmsSendMsg: '验证码已发送',
pwdResetSuccess: '密码重置成功'
},
forgetpassword: {
resetpwd: '重置密码',
},
captcha: {
verification: '请完成安全验证',

View File

@ -29,7 +29,7 @@
</div>
<div class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px">
<!-- 右上角的主题语言选择 -->
<!-- <div
<div
class="flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end"
>
<div class="flex items-center at-2xl:hidden at-xl:hidden">
@ -40,7 +40,7 @@
<ThemeSwitch />
<LocaleDropdown class="dark:text-white lt-xl:text-white" />
</div>
</div>-->
</div>
<!-- 右边的登录界面 -->
<Transition appear enter-active-class="animate__animated animate__bounceInRight">
<div
@ -56,6 +56,8 @@
<RegisterForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
<!-- 三方登录 -->
<SSOLoginVue class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
<!-- 忘记密码 -->
<ForgetPasswordForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
</div>
</Transition>
</div>
@ -70,7 +72,7 @@ import { useAppStore } from '@/store/modules/app'
import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue } from './components'
import { LoginForm, MobileForm, ForgetPasswordForm, QrCodeForm, RegisterForm, SSOLoginVue } from './components'
defineOptions({ name: 'Login' })

View File

@ -0,0 +1,264 @@
<template>
<el-form
v-show="getShow"
ref="formSmsLogin"
:model="loginData.loginForm"
:rules="rules"
class="login-form"
label-position="top"
label-width="120px"
size="large"
autocomplete="off"
>
<el-row style="margin-right: -10px; margin-left: -10px">
<!-- 租户名 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<LoginFormTitle style="width: 100%" />
</el-form-item>
</el-col>
<!-- <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
<el-input
v-model="loginData.loginForm.tenantName"
:placeholder="t('login.tenantNamePlaceholder')"
:prefix-icon="iconHouse"
type="primary"
link
/>
</el-form-item>
</el-col>-->
<!-- 手机号 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="mailNumber">
<el-input
v-model="loginData.loginForm.mailNumber"
:placeholder="t('login.emailNumberPlaceholder')"
:prefix-icon="iconCellemail"
/>
</el-form-item>
</el-col>
<!-- 密码 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="password">
<el-input
type="password"
disableautocomplete
autocomplete="off"
show-password="true"
v-model="loginData.loginForm.password"
:placeholder="t('login.password')"
/>
</el-form-item>
</el-col>
<!-- 确认密码 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="checkPassword">
<el-input
type="password"
autocomplete="off"
disableautocomplete
show-password="true"
v-model="loginData.loginForm.checkPassword"
:placeholder="t('login.checkPassword')"
/>
</el-form-item>
</el-col>
<!-- 验证码 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="code">
<el-row :gutter="5" justify="space-between" style="width: 100%">
<el-col :span="24">
<el-input
v-model="loginData.loginForm.code"
:placeholder="t('login.codePlaceholder')"
:prefix-icon="iconCircleCheck"
>
<!-- <el-button class="w-[100%]"> -->
<template #append>
<span
v-if="mailCodeTimer <= 0"
class="getMobileCode"
style="cursor: pointer"
@click="getMailCode"
>
{{ t('login.getMailCode') }}
</span>
<span v-if="mailCodeTimer > 0" class="getMobileCode" style="cursor: pointer">
{{ mailCodeTimer }}秒后可重新获取
</span>
</template>
</el-input>
<!-- </el-button> -->
</el-col>
</el-row>
</el-form-item>
</el-col>
<!-- 登录按钮 / 返回按钮 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<XButton
:loading="loginLoading"
:title="t('forgetpassword.resetpwd')"
class="w-[100%]"
type="primary"
@click="modifyPwdIn()"
/>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<XButton
:loading="loginLoading"
:title="t('login.backLogin')"
class="w-[100%]"
@click="handleBackLogin()"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script lang="ts" setup>
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { useIcon } from '@/hooks/web/useIcon'
import { setTenantId, setToken } from '@/utils/auth'
import { usePermissionStore } from '@/store/modules/permission'
import { getTenantIdByName, sendMailCode, mailModifyPwd } from '@/api/login'
import LoginFormTitle from './LoginFormTitle.vue'
import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
import { ElLoading } from 'element-plus'
defineOptions({ name: 'ForgetPasswordForm' })
const { t } = useI18n()
const message = useMessage()
const permissionStore = usePermissionStore()
const { currentRoute, push } = useRouter()
const formSmsLogin = ref()
const loginLoading = ref(false)
const iconHouse = useIcon({ icon: 'ep:house' })
const iconCellphone = useIcon({ icon: 'ep:cellphone' })
const iconCellemail= useIcon({ icon: 'ep:cellphone' })
const iconCircleCheck = useIcon({ icon: 'ep:circle-check' })
const { validForm } = useFormValid(formSmsLogin)
const { handleBackLogin, getLoginState } = useLoginState()
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.RESET_PASSWORD)
const rules = {
tenantName: [required],
mobileNumber: [required],
code: [required]
}
const loginData = reactive({
codeImg: '',
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
token: '',
loading: {
modifyPwdIn: false
},
loginForm: {
uuid: '',
tenantName: '芋道源码',
mobileNumber: '',
code: ''
}
})
const mailVO = reactive({
mailCode: {
mail: '',
scene: 4,
type: 999999
},
loginMail: {
mail: '',
code: '',
type: 999999
}
})
const mailCodeTimer = ref(0)
const redirect = ref<string>('')
const getMailCode = async () => {
await getTenantId()
mailVO.mailCode.mail = loginData.loginForm.mailNumber
await sendMailCode(mailVO.mailCode).then(async () => {
message.success(t('login.SmsSendMsg'))
//
mailCodeTimer.value = 60
let msgTimer = setInterval(() => {
mailCodeTimer.value = mailCodeTimer.value - 1
if (mailCodeTimer.value <= 0) {
clearInterval(msgTimer)
}
}, 1000)
})
}
watch(
() => currentRoute.value,
(route: RouteLocationNormalizedLoaded) => {
redirect.value = route?.query?.redirect as string
},
{
immediate: true
}
)
// ID
const getTenantId = async () => {
if (loginData.tenantEnable === 'true') {
const res = await getTenantIdByName(loginData.loginForm.tenantName)
setTenantId(res)
}
}
//
const modifyPwdIn = async () => {
await getTenantId()
const data = await validForm()
if (!data) return
/* ElLoading.service({
lock: true,
text: '正在加载系统中...',
background: 'rgba(0, 0, 0, 0.7)'
})*/
loginLoading.value = true
mailVO.loginMail.mail = loginData.loginForm.mailNumber
mailVO.loginMail.code = loginData.loginForm.code
mailVO.loginMail.password = loginData.loginForm.password
mailVO.loginMail.checkpassword = loginData.loginForm.checkPassword
await mailModifyPwd(mailVO.loginMail)
.then(async (res) => {
/* setToken(res)
if (!redirect.value) {
redirect.value = '/'
}
push({ path: redirect.value || permissionStore.addRouters[0].path })*/
await message.alertSuccess(t('login.pwdResetSuccess'))
handleBackLogin()
})
.catch(() => {})
.finally(() => {
loginLoading.value = false
setTimeout(() => {
const loadingInstance = ElLoading.service()
loadingInstance.close()
}, 400)
})
}
</script>
<style lang="scss" scoped>
:deep(.anticon) {
&:hover {
color: var(--el-color-primary) !important;
}
}
.smsbtn {
margin-top: 33px;
}
</style>

View File

@ -58,9 +58,9 @@
{{ t('login.remember') }}
</el-checkbox>
</el-col>
<!-- <el-col :offset="6" :span="12">
<el-link style="float: right" type="primary">{{ t('login.forgetPassword') }}</el-link>
</el-col>-->
<el-col :offset="6" :span="12">
<el-link style="float: right" type="primary" @click="setLoginState(LoginStateEnum.RESET_PASSWORD)">{{ t('login.forgetPassword') }}</el-link>
</el-col>
</el-row>
</el-form-item>
</el-col>

View File

@ -1,8 +1,9 @@
import LoginForm from './LoginForm.vue'
import MobileForm from './MobileForm.vue'
import ForgetPasswordForm from './ForgetPasswordForm.vue'
import LoginFormTitle from './LoginFormTitle.vue'
import RegisterForm from './RegisterForm.vue'
import QrCodeForm from './QrCodeForm.vue'
import SSOLoginVue from './SSOLogin.vue'
export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue }
export { LoginForm, MobileForm, ForgetPasswordForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue }