增加短信验证码登录方式

master
15004070936 3 weeks ago
parent 6112c7f639
commit 9ca0321317

@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.im.util.BeanUtils;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ruoyi.RestResponse;
@ -16,6 +17,7 @@ import com.ruoyi.common.core.domain.model.AppLoginUser;
import com.ruoyi.common.core.domain.model.AppUserLoginBody;
import com.ruoyi.common.utils.JpushClientUtil;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.sms.SmsCodeAuthenticationToken;
import com.ruoyi.framework.web.service.AppLoginService;
import com.ruoyi.basic.domain.YjStore;
import com.ruoyi.basic.service.impl.YjStoreServiceImpl;
@ -25,9 +27,11 @@ import com.ruoyi.web.controller.basic.dto.AppLoginUserForGetInfor;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -51,12 +55,32 @@ public class AppLoginController {
@Autowired
private SysPermissionService permissionService;
/**
*
*/
@PostMapping("/sms/login")
public AjaxResult smsLogin(@RequestBody AppUserLoginBody loginBody) {
AjaxResult ajax = AjaxResult.success();
String token=appLoginService.loginBySms(loginBody.getPhonenumber(),loginBody.getSmsCode(),loginBody.getRegisterId(),loginBody.getMiniAppCode());
ajax.put(Constants.TOKEN, token);
// 保存设备id绑定别名便于推送消息
if (StrUtil.isNotEmpty(loginBody.getRegisterId())){
appUserService.update(new UpdateWrapper<AppUser>().eq("phone_number",loginBody.getPhonenumber())
.set("register_id",loginBody.getRegisterId())
.set(StrUtil.isNotEmpty(loginBody.getLoginSysPlatform()),"login_sys_platform",loginBody.getLoginSysPlatform())
);
// 极光推送注册设备id绑定别名便于推送消息
JpushClientUtil.bindAlias(loginBody.getRegisterId(),loginBody.getPhonenumber());
}
return ajax;
}
/**
*
* @param loginBody
* @return
*/
@PostMapping("/register")
// @PostMapping("/register")
public RestResponse register(@RequestBody AppUserLoginBody loginBody){
appLoginService.validateCaptcha(loginBody.getCode(), loginBody.getUuid());
if (!loginBody.getPassword().equals(loginBody.getPasswordSecond())){
@ -87,7 +111,6 @@ public class AppLoginController {
AjaxResult ajax = AjaxResult.success();
String token=appLoginService.login(loginBody.getPhonenumber(),loginBody.getPassword(),loginBody.getCode(),loginBody.getUuid(),loginBody.getRegisterId(),loginBody.getMiniAppCode());
ajax.put(Constants.TOKEN, token);
// 保存设备id绑定别名便于推送消息
if (StrUtil.isNotEmpty(loginBody.getRegisterId())){
appUserService.update(new UpdateWrapper<AppUser>().eq("phone_number",loginBody.getPhonenumber())
@ -131,6 +154,37 @@ public class AppLoginController {
return ajax;
}
@ApiOperation(value = "注销用户")
@GetMapping("/cancel")
public RestResponse cancel()
{
AppLoginUser user = SecurityUtils.getAppLoginUser();
if (ObjectUtil.isEmpty(user)){
return new RestResponse().setSuccess(false).setMessage("登录已过期,请重新登录");
}
AppUser appUser=new AppUser();
appUser.setId(user.getAppUser().getId());
appUser.setNickName("用户已注销");
appUser.setStatus(0);
appUser.setPhoneNumber(null);
appUser.setUserName(null);
appUser.setPassword(null);
appUser.setAvatar(null);
appUser.setSex(null);
appUser.setCity(null);
appUser.setProvince(null);
appUser.setCountry(null);
appUser.setBirthday(null);
appUser.setRegisterId(null);
appUser.setUnionid(null);
appUser.setOpenid(null);
appUser.setLoginIp(null);
appUser.setSignature(null);
appUser.setLoginSysPlatform(null);
appUser.setCancelTime(new Date());
appUserService.updateById(appUser);
return new RestResponse().setSuccess(true).setMessage("注销成功");
}
@Resource
private YjStoreServiceImpl storeService;
@ -175,4 +229,5 @@ public class AppLoginController {
}

@ -1,8 +1,12 @@
package com.ruoyi.web.controller.login;
import cn.hutool.core.date.DateUtil;
import com.google.code.kaptcha.Producer;
import com.ruoyi.RestResponse;
import com.ruoyi.basic.domain.AppAgreement;
import com.ruoyi.basic.domain.YjStore;
import com.ruoyi.basic.mapper.YjStoreMapper;
import com.ruoyi.basic.service.AppAgreementService;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.constant.Constants;
@ -11,16 +15,27 @@ import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.JpushClientUtil;
import com.ruoyi.common.utils.sign.Base64;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.framework.sms.SmsService;
import com.ruoyi.system.service.ISysConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.Pattern;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
@ -30,7 +45,7 @@ import java.util.concurrent.TimeUnit;
* @author ruoyi
*/
@RestController
public class CaptchaController
public class CaptchaController
{
@Resource(name = "captchaProducer")
private Producer captchaProducer;
@ -44,6 +59,59 @@ public class CaptchaController
@Autowired
private ISysConfigService configService;
@Autowired
private SmsService smsService;
@Autowired
private RedisTemplate<String, String> redisTemplate; // 注入 StringRedisTemplate
private static final String SMS_CODE_PREFIX = "sms_code:";
/**
*
*/
@PostMapping("/sms/sendCode")
public AjaxResult sendSmsCode(@RequestBody Map<String,String> map ) {
String phone=map.get("phone");
// 1. 验证手机号格式(可交由前端或参数校验)
if (!phone.matches("^1[3-9]\\d{9}$")) {
return AjaxResult.error("手机号格式不正确");
}
// 2. 限流检查60秒内同一手机号只能发送一次原子操作
String limitKey = CacheConstants.SMS_SEND_LIMIT_KEY + phone;
// 尝试设置key如果已存在则返回false说明60秒内已发送过
Boolean absent = redisTemplate.opsForValue()
.setIfAbsent(limitKey, "1", Duration.ofSeconds(60));
if (Boolean.FALSE.equals(absent)) {
// 获取剩余时间(可选,用于提示)
Long expire = redisTemplate.getExpire(limitKey, TimeUnit.SECONDS);
String msg = expire != null && expire > 0
? "操作过于频繁,请" + expire + "秒后再试"
: "操作过于频繁,请稍后再试";
return AjaxResult.error(msg);
}
// 1. 生成6位随机验证码
String code = String.format("%06d", new Random().nextInt(999999));
// 2. 存入Redis有效期1分钟
String codeKey = CacheConstants.SMS_CODE_KEY + phone;
redisCache.setCacheObject(codeKey, code, 1, TimeUnit.MINUTES);
// redisTemplate.opsForValue().set(codeKey, code, Duration.ofMinutes(1));
// 3. 发送短信
try {
smsService.sendSmsCode(phone, code);
return AjaxResult.success("验证码发送成功");
} catch (Exception e) {
// 发送失败:删除验证码(避免无效验证码被使用)
redisTemplate.delete(codeKey);
// 删除限流key允许用户立即重试
redisTemplate.delete(limitKey);
return AjaxResult.error(e.getMessage());
}
}
/**
*
@ -97,7 +165,333 @@ public class CaptchaController
ajax.put("img", Base64.encode(os.toByteArray()));
return ajax;
}
@Autowired
private AppAgreementService appAgreementService;
/**
*
* @param type 1-2-
* @return
*/
@GetMapping(value ="/api/termsOfService", produces = MediaType.TEXT_HTML_VALUE)
public ResponseEntity<String> agreementOfService(HttpServletResponse response) throws IOException {
AppAgreement agreement = appAgreementService.getLatestByType(1);
String html = buildHtmlPage(agreement, agreement.getTitle());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.valueOf("text/html; charset=UTF-8"));
return new ResponseEntity<>(html, headers, HttpStatus.OK);
}
/**
*
* @param response
* @return
* @throws IOException
*/
@GetMapping(value ="/api/privacyPolicy", produces = MediaType.TEXT_HTML_VALUE)
public ResponseEntity<String> privacyPolicy(HttpServletResponse response) throws IOException {
AppAgreement agreement = appAgreementService.getLatestByType(2);
String html = buildHtmlPage(agreement, agreement.getTitle());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.valueOf("text/html; charset=UTF-8"));
return new ResponseEntity<>(html, headers, HttpStatus.OK);
}
/**
*
* @param response
* @return
* @throws IOException
*/
@GetMapping(value ="/api/technicalSupport", produces = MediaType.TEXT_HTML_VALUE)
public ResponseEntity<String> technicalSupport(HttpServletResponse response) throws IOException {
AppAgreement agreement = appAgreementService.getLatestByType(3);
String html = buildHtmlPage(agreement, agreement.getTitle());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.valueOf("text/html; charset=UTF-8"));
return new ResponseEntity<>(html, headers, HttpStatus.OK);
}
/**
*
* @param response
* @return
* @throws IOException
*/
@GetMapping(value ="/api/marketing", produces = MediaType.TEXT_HTML_VALUE)
public ResponseEntity<String> marketing(HttpServletResponse response) throws IOException {
AppAgreement agreement = appAgreementService.getLatestByType(5);
String html = buildHtmlPage(agreement, agreement.getTitle());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.valueOf("text/html; charset=UTF-8"));
return new ResponseEntity<>(html, headers, HttpStatus.OK);
}
/**
*
* @param response
* @return
* @throws IOException
*/
@GetMapping(value ="/api/companyInformation", produces = MediaType.TEXT_HTML_VALUE)
public ResponseEntity<String> companyInformation(HttpServletResponse response) throws IOException {
AppAgreement agreement = appAgreementService.getLatestByType(4);
String html = buildHtmlPage(agreement, agreement.getTitle());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.valueOf("text/html; charset=UTF-8"));
return new ResponseEntity<>(html, headers, HttpStatus.OK);
}
/**
* HTML
*/
private String buildHtmlPage(AppAgreement agreement, String title) {
String content = agreement != null ? agreement.getContent() : "<p>协议内容暂未发布,请联系客服。</p>";
String updateDate = agreement != null ? DateUtil.format(agreement.getEffectiveDate(), "yyyy-MM-dd") : "";
Integer type = agreement != null ? agreement.getType() : 0;
// 判断是否为官网页面type=5使用更宽的容器
boolean isCompanyPage = type == 4;
return "<!DOCTYPE html>\n" +
"<html lang=\"zh-CN\">\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes\">\n" +
" <title>" + escapeHtml(title) + "</title>\n" +
" <style>\n" +
" * { margin: 0; padding: 0; box-sizing: border-box; }\n" +
" html, body {\n" +
" width: 100%;\n" +
" overflow-x: hidden;\n" +
" }\n" +
" body {\n" +
" font-family: -apple-system, BlinkMacSystemFont, \"PingFang SC\", \"Helvetica Neue\", Arial, sans-serif;\n" +
" background-color: #f5f7fa;\n" +
" padding: 20px 12px 40px;\n" +
" color: #333;\n" +
" line-height: 1.8;\n" +
" font-size: 16px;\n" +
" max-width: 100%;\n" +
" }\n" +
" .agreement-box {\n" +
" max-width: " + (isCompanyPage ? "1200px" : "820px") + ";\n" +
" width: 100%;\n" +
" margin: 0 auto;\n" +
" background: #ffffff;\n" +
" border-radius: 12px;\n" +
" padding: " + (isCompanyPage ? "16px 12px" : "28px 22px") + ";\n" +
" box-shadow: 0 2px 12px rgba(0,0,0,0.06);\n" +
" overflow: hidden;\n" +
" word-wrap: break-word;\n" +
" word-break: break-word;\n" +
" }\n" +
" /* 如果 content 内部有独立的 html 结构(如公司官网),则重置内部样式 */\n" +
" .agreement-box .page-wrapper {\n" +
" width: 100%;\n" +
" max-width: 100%;\n" +
" overflow: hidden;\n" +
" }\n" +
" .agreement-box h1 {\n" +
" font-size: 22px;\n" +
" text-align: center;\n" +
" margin-bottom: 12px;\n" +
" font-weight: 600;\n" +
" color: #1a1a1a;\n" +
" }\n" +
" .agreement-box .update-date {\n" +
" text-align: center;\n" +
" color: #999;\n" +
" font-size: 14px;\n" +
" margin-bottom: 20px;\n" +
" border-bottom: 1px solid #eee;\n" +
" padding-bottom: 12px;\n" +
" }\n" +
" .agreement-box h2 {\n" +
" font-size: 20px;\n" +
" margin-top: 24px;\n" +
" margin-bottom: 12px;\n" +
" font-weight: 600;\n" +
" color: #1a3a5c;\n" +
" }\n" +
" .agreement-box h3 {\n" +
" font-size: 18px;\n" +
" margin-top: 20px;\n" +
" margin-bottom: 10px;\n" +
" font-weight: 600;\n" +
" color: #1a3a5c;\n" +
" }\n" +
" .agreement-box h4 {\n" +
" font-size: 16px;\n" +
" margin-top: 16px;\n" +
" margin-bottom: 8px;\n" +
" font-weight: 600;\n" +
" color: #2a6a9c;\n" +
" }\n" +
" .agreement-box p {\n" +
" margin-bottom: 10px;\n" +
" text-align: justify;\n" +
" }\n" +
" .agreement-box ul, .agreement-box ol {\n" +
" padding-left: 22px;\n" +
" margin-bottom: 12px;\n" +
" }\n" +
" .agreement-box ul li, .agreement-box ol li {\n" +
" margin-bottom: 6px;\n" +
" }\n" +
" .agreement-box strong {\n" +
" color: #1a1a1a;\n" +
" }\n" +
" .agreement-box a {\n" +
" color: #2b7cff;\n" +
" text-decoration: none;\n" +
" word-break: break-all;\n" +
" }\n" +
" .agreement-box a:hover {\n" +
" text-decoration: underline;\n" +
" }\n" +
" /* 图片自适应 */\n" +
" .agreement-box img {\n" +
" max-width: 100%;\n" +
" height: auto;\n" +
" display: block;\n" +
" margin: 10px 0;\n" +
" }\n" +
" /* 表格自适应 */\n" +
" .agreement-box table {\n" +
" width: 100%;\n" +
" border-collapse: collapse;\n" +
" margin: 12px 0;\n" +
" font-size: 14px;\n" +
" display: block;\n" +
" overflow-x: auto;\n" +
" -webkit-overflow-scrolling: touch;\n" +
" }\n" +
" .agreement-box table thead,\n" +
" .agreement-box table tbody,\n" +
" .agreement-box table tr {\n" +
" display: table;\n" +
" width: 100%;\n" +
" table-layout: fixed;\n" +
" }\n" +
" .agreement-box table th,\n" +
" .agreement-box table td {\n" +
" padding: 8px 10px;\n" +
" border: 1px solid #ddd;\n" +
" text-align: left;\n" +
" word-break: break-word;\n" +
" }\n" +
" .agreement-box table th {\n" +
" background: #f5f7fa;\n" +
" font-weight: 600;\n" +
" }\n" +
" /* 卡片和网格容器自适应(用于公司官网) */\n" +
" .agreement-box .grid-2,\n" +
" .agreement-box .grid-3 {\n" +
" display: grid;\n" +
" gap: 16px;\n" +
" width: 100%;\n" +
" }\n" +
" .agreement-box .grid-2 { grid-template-columns: 1fr 1fr; }\n" +
" .agreement-box .grid-3 { grid-template-columns: 1fr 1fr 1fr; }\n" +
" .agreement-box .banner {\n" +
" width: 100%;\n" +
" overflow: hidden;\n" +
" }\n" +
" .agreement-box .partner-grid {\n" +
" display: grid;\n" +
" grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));\n" +
" gap: 12px;\n" +
" width: 100%;\n" +
" }\n" +
" .agreement-box .stat-item {\n" +
" background: #f8fafc;\n" +
" border-radius: 8px;\n" +
" padding: 14px 12px;\n" +
" text-align: center;\n" +
" }\n" +
" .agreement-box .stat-item .num {\n" +
" font-size: 24px;\n" +
" font-weight: 700;\n" +
" color: #1a3a5c;\n" +
" }\n" +
" .agreement-box .stat-item .label {\n" +
" font-size: 13px;\n" +
" color: #888;\n" +
" }\n" +
" .agreement-box .badge {\n" +
" display: inline-block;\n" +
" background: #e8f0fe;\n" +
" color: #1a3a5c;\n" +
" border-radius: 16px;\n" +
" padding: 2px 14px;\n" +
" font-size: 13px;\n" +
" }\n" +
" .agreement-box .badge-blue {\n" +
" background: #1a3a5c;\n" +
" color: #fff;\n" +
" }\n" +
" .agreement-box .tag {\n" +
" display: inline-block;\n" +
" background: rgba(26, 58, 92, 0.12);\n" +
" border-radius: 20px;\n" +
" padding: 2px 16px;\n" +
" font-size: 13px;\n" +
" margin: 2px 4px;\n" +
" }\n" +
" .agreement-box .footer {\n" +
" margin-top: 24px;\n" +
" border-top: 1px solid #eee;\n" +
" padding-top: 16px;\n" +
" font-size: 14px;\n" +
" color: #999;\n" +
" text-align: center;\n" +
" }\n" +
" /* 移动端适配 */\n" +
" @media (max-width: 768px) {\n" +
" body { padding: 12px 8px 30px; font-size: 15px; }\n" +
" .agreement-box { padding: 16px 12px !important; }\n" +
" .agreement-box .grid-2,\n" +
" .agreement-box .grid-3 { grid-template-columns: 1fr !important; }\n" +
" .agreement-box h1 { font-size: 20px; }\n" +
" .agreement-box h2 { font-size: 18px; }\n" +
" .agreement-box h3 { font-size: 16px; }\n" +
" .agreement-box .partner-grid { grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); }\n" +
" .agreement-box table { font-size: 12px; }\n" +
" .agreement-box table th,\n" +
" .agreement-box table td { padding: 5px 6px; }\n" +
" }\n" +
" @media (max-width: 480px) {\n" +
" body { padding: 8px 4px 20px; font-size: 14px; }\n" +
" .agreement-box { padding: 12px 8px !important; border-radius: 8px; }\n" +
" .agreement-box .partner-grid { grid-template-columns: 1fr 1fr; }\n" +
" }\n" +
" </style>\n" +
"</head>\n" +
"<body>\n" +
" <div class=\"agreement-box\">\n" +
(isCompanyPage ? "" : "<h1>" + escapeHtml(title) + "</h1>\n") +
(agreement != null && !isCompanyPage ? "<div class=\"update-date\">更新日期:" + updateDate + "  生效日期:" + updateDate + "</div>" : "") +
" <div class=\"" + (isCompanyPage ? "page-wrapper" : "") + "\">\n" +
content + "\n" +
" </div>\n" +
(!isCompanyPage ? "<div class=\"footer\">" + (agreement != null ? agreement.getVersion() : "") + "</div>" : "") +
" </div>\n" +
"</body>\n" +
"</html>";
}
/**
* HTML XSS
*/
private String escapeHtml(String text) {
if (text == null) return "";
return text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#39;");
}
public void testBindAlias() {

@ -18,6 +18,12 @@ public class CacheConstants
* redis key
*/
public static final String CAPTCHA_CODE_KEY = "captcha_codes:";
public static final String SMS_CODE_KEY= "sms_codes:";
/**
* key60
*/
public static final String SMS_SEND_LIMIT_KEY = "sms_send_limit:";
/**
* cache key

@ -1,9 +1,6 @@
package com.ruoyi.common.core.domain.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.utils.SecurityUtils;
@ -36,52 +33,66 @@ public class AppUser{
private String nickName;
@ApiModelProperty("名称")
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String userName;
@ApiModelProperty("密码")
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String password;
@ApiModelProperty("手机号")
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String phoneNumber;
@ApiModelProperty("帐号启用状态:0->禁用1->启用")
private Integer status;
@ApiModelProperty("头像")
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String avatar;
@ApiModelProperty("性别0->未知1->女2->男")
@TableField(updateStrategy = FieldStrategy.IGNORED)
private Integer sex;
@ApiModelProperty("用户所在城市")
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String city;
@ApiModelProperty("用户所在省份")
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String province;
@ApiModelProperty("用户所在国家")
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String country;
@ApiModelProperty("生日")
@Excel(name = "生日", width = 30, dateFormat = "yyyy-MM-dd")
@TableField(updateStrategy = FieldStrategy.IGNORED)
private LocalDate birthday;
@ApiModelProperty("设备id")
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String registerId;
@ApiModelProperty("访问/浏览门店id")
private Long visitStore;
@ApiModelProperty("小程序unionid")
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String unionid;
@ApiModelProperty("小程序openid")
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String openid;
/** 最后登录IP */
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String loginIp;
//个性签名140字
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String signature;
/** 最后登录时间 */
@ -98,9 +109,10 @@ public class AppUser{
//iOSios
//鸿蒙harmonyos
//微信小程序weixin
@TableField("login_sys_platform")
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String loginSysPlatform;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date cancelTime;
}

@ -41,6 +41,7 @@ public class AppUserLoginBody
*
*/
private String code;
/**
* id
*/

@ -47,7 +47,16 @@
</exclusions>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.6.3</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>2.2.1</version>
</dependency>
<!-- 获取系统信息 -->
<dependency>
<groupId>com.github.oshi</groupId>
@ -61,4 +70,4 @@
</dependencies>
</project>
</project>

@ -1,5 +1,6 @@
package com.ruoyi.framework.config;
import com.ruoyi.framework.sms.SmsCodeAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
@ -73,6 +74,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
@Autowired
private PermitAllUrlProperties permitAllUrl;
@Autowired
private SmsCodeAuthenticationProvider smsCodeAuthenticationProvider;
/**
* AuthenticationManager
*
@ -141,9 +144,17 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
.antMatchers("/login",
"/register",
"/captchaImage",
"/sms/sendCode",
"/getStoreList",
"/api/login",
"/api/register" ).anonymous()
"/api/sms/login",
"/api/register" ,
"/api/termsOfService" ,
"/api/privacyPolicy",
"/api/technicalSupport",
"/api/companyInformation",
"/api/marketing"
).anonymous()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
@ -179,6 +190,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.authenticationProvider(smsCodeAuthenticationProvider);
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}

@ -0,0 +1,69 @@
package com.ruoyi.framework.sms;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.exception.ServiceException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
@Autowired
private RedisCache redisCache;
@Autowired
private SmsUserDetailsService smsUserDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 1. 获取手机号和验证码
String phoneNumber = (String) authentication.getPrincipal();
String smsCode = (String) authentication.getCredentials();
// 2. 从Redis中校验验证码
String cacheKey = CacheConstants.SMS_CODE_KEY + phoneNumber;
String cachedCode = redisCache.getCacheObject(cacheKey);
if (cachedCode == null) {
throw new BadCredentialsException("验证码已过期,请重新获取");
}
if (!cachedCode.equals(smsCode)) {
throw new BadCredentialsException("验证码错误");
}
// 3. 验证通过,删除验证码(一次性)
redisCache.deleteObject(cacheKey);
// 4. 加载用户信息
UserDetails userDetails;
try {
userDetails = smsUserDetailsService.loadUserByUsername(phoneNumber);
} catch (UsernameNotFoundException e) {
throw new ServiceException("该手机号未注册,请先注册");
}
// 5. 构建已认证的Token
SmsCodeAuthenticationToken authenticatedToken = new SmsCodeAuthenticationToken(
userDetails,
userDetails.getAuthorities()
);
// 设置详细信息IP等
authenticatedToken.setDetails(authentication.getDetails());
return authenticatedToken;
}
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}

@ -0,0 +1,52 @@
package com.ruoyi.framework.sms;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 1L;
private final Object principal; // 手机号
private Object credentials; // 验证码(认证后清空)
/**
*
*/
public SmsCodeAuthenticationToken(String phoneNumber, String smsCode) {
super(null);
this.principal = phoneNumber;
this.credentials = smsCode;
setAuthenticated(false);
}
/**
*
*/
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = null;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return credentials;
}
@Override
public Object getPrincipal() {
return principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
}

@ -0,0 +1,59 @@
package com.ruoyi.framework.sms;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class SmsService {
private static final Logger log = LoggerFactory.getLogger(SmsService.class);
private String accessKeyId = "LTAI5tABg9V5VaXFDcZW4LLk";
private String accessKeySecret = "xp5k5fwnqfCqZe2K5IBI1h5xPEN45V";
private String signName = "沈阳润芃智能科技";
private String templateCode = "SMS_337755625";
/**
*
* @param phone
* @param code
* @throws RuntimeException
*/
public void sendSmsCode(String phone, String code) {
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
IAcsClient client = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest();
request.setPhoneNumbers(phone);
request.setSignName(signName);
request.setTemplateCode(templateCode);
request.setTemplateParam("{\"code\":\"" + code + "\"}");
try {
SendSmsResponse response = client.getAcsResponse(request);
if (!"OK".equals(response.getCode())) {
log.error("短信发送失败code={}, message={}", response.getCode(), response.getMessage());
throw new RuntimeException("短信发送失败:" + response.getMessage());
}
log.info("短信发送成功,手机号:{},验证码:{}", phone, code);
} catch (ClientException e) {
// } catch (Exception e) {
log.error("调用阿里云短信API异常", e);
throw new RuntimeException("短信服务异常,请稍后重试");
}
}
}

@ -0,0 +1,87 @@
package com.ruoyi.framework.sms;
import cn.hutool.core.util.ObjectUtil;
import com.ruoyi.basic.service.impl.YjAppUserServiceImpl;
import com.ruoyi.common.core.domain.entity.AppUser;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.AppLoginUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.course.service.IScStudentService;
import com.ruoyi.system.service.ISysUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class SmsUserDetailsService implements UserDetailsService {
private static final Logger log = LoggerFactory.getLogger(SmsUserDetailsService.class);
@Autowired
private YjAppUserServiceImpl userService;
@Autowired
private ISysUserService sysUserService;
@Autowired
private IScStudentService studentService;
@Override
public UserDetails loadUserByUsername(String phonenumber) throws UsernameNotFoundException {
// 通过手机号查询用户需在SysUserService中增加按手机号查询的方法
AppUser user = userService.selectAppUserByPhone(phonenumber);
if (StringUtils.isNull(user))
{
log.info("登录用户:{} 不存在.", phonenumber);
throw new ServiceException("登录用户:" + phonenumber + " 不存在");
}
else if (user.getStatus() == 0)
{
log.info("登录用户:{} 已被停用.", phonenumber);
throw new ServiceException("对不起,您的账号:" + phonenumber + " 已停用");
}
List<Long> roles=new ArrayList<>();
roles.add(107l);
if (ObjectUtil.isNotEmpty(user.getManageAccountId())){
//todo 角色ID顾问103 教练104 店长105 普通用户107
SysUser sysUser= sysUserService.selectUserById(user.getManageAccountId());
if (ObjectUtil.isEmpty(sysUser)){
log.info("登录用户:{} 已被停用.", phonenumber);
throw new ServiceException("对不起,您的账号:" + phonenumber + " 所属租户异常,请联系管理员!");
}
List<SysRole> roles1=sysUser.getRoles();
for (SysRole sysRole : roles1) {
roles.add(sysRole.getRoleId());
}
}
Long studentId=studentService.getStudentByAppUserId(user.getId());
// Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext();
// String password = usernamePasswordAuthenticationToken.getCredentials().toString();
// if (!SecurityUtils.matchesPassword(password,user.getPassword())){
// throw new RuntimeException(Constants.LOGIN_INFO.WRONG);
// }
// SysUser user1=new SysUser();
// user1.setPassword(user.getPassword());
// passwordService.validate(user1);
return createLoginUser(user,roles.toArray(new Long[roles.size()]) ,studentId);
}
public UserDetails createLoginUser(AppUser user, Long[] roles,Long studentId)
{
return new AppLoginUser(user.getId(),user.getUserName(), user.getManageAccountId(),roles,studentId, user);
}
}

@ -24,6 +24,7 @@ import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.framework.security.context.AuthenticationContextHolder;
import com.ruoyi.framework.sms.SmsCodeAuthenticationToken;
import com.ruoyi.mall.util.PayConstants;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.ISysUserService;
@ -66,6 +67,8 @@ public class AppLoginService
@Qualifier("AppUseAuthenticationManager")
private AuthenticationManager appUserAuthenticationManager;
@Autowired
private AuthenticationManager authenticationManager;
/**
*
*
@ -130,6 +133,54 @@ public class AppLoginService
return tokenService.createAppToken(loginUser);
}
/**
* App
*
* @param phoneNumber
* @param smsCode
* @param registerId ID
* @param miniAppCode openId
* @return Token
*/
public String loginBySms(String phoneNumber, String smsCode, String registerId, String miniAppCode) {
// ========== 1. 校验短信验证码 ==========
// 1. 构造未认证的Token
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(phoneNumber, smsCode);
// 2. 调用AuthenticationManager认证
Authentication authentication = authenticationManager.authenticate(authRequest);
// 3. 若认证成功生成Token
AppLoginUser loginUser = (AppLoginUser) authentication.getPrincipal();
boolean update=false;
UpdateWrapper wrapper=new UpdateWrapper<AppUser>().eq("app_user_id",loginUser.getAppUserId());
//设备id
if (StrUtil.isNotEmpty(loginUser.getAppUser().getRegisterId())&&!loginUser.getAppUser().getRegisterId().equals(registerId)){
wrapper.set("register_id",registerId);
update=true;
}
//小程序登录
if (StrUtil.isNotEmpty(miniAppCode) && StrUtil.isEmpty(loginUser.getAppUser().getOpenid())){
HashMap<String, Object> map=getOpenId(miniAppCode);
wrapper.set(ObjectUtil.isNotEmpty(map.get("openId")),"openid",map.get("openId").toString());
wrapper.set(ObjectUtil.isNotEmpty(map.get("unionId")),"unionid",map.get("unionId").toString());
update=true;
}
if (update){
appUserService.update(wrapper);
}
//将登录信息存入到数据库
wrapper.set("login_ip",IpUtils.getIpAddr(ServletUtils.getRequest()));
wrapper.set("login_date",DateUtils.getNowDate());
appUserService.update(wrapper);
// ========== 6. 生成并返回Token ==========
return tokenService.createAppToken(loginUser);
}
/**
*
*

@ -0,0 +1,48 @@
package com.ruoyi.basic.domain;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
*
* @TableName
*/
@Data
@TableName("app_agreements")
public class AppAgreement {
@TableId(type = IdType.AUTO)
private Integer id;
/**
* 1-2-
*/
private Integer type;
/**
*
*/
private String title;
/**
* HTML
*/
private String content;
/**
* v3.2
*/
private String version;
/**
*
*/
private LocalDateTime effectiveDate;
/**
*
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
}

@ -0,0 +1,10 @@
package com.ruoyi.basic.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.basic.domain.AppAgreement;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AppAgreementMapper extends BaseMapper<AppAgreement> {
// 无需额外方法,使用 MyBatis-Plus 提供的条件构造器即可
}

@ -0,0 +1,14 @@
package com.ruoyi.basic.service;
import com.ruoyi.basic.domain.AppAgreement;
public interface AppAgreementService {
/**
*
* @param type 1-2-
* @return null
*/
AppAgreement getLatestByType(Integer type);
}

@ -0,0 +1,28 @@
package com.ruoyi.basic.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.basic.domain.AppAgreement;
import com.ruoyi.basic.mapper.AppAgreementMapper;
import com.ruoyi.basic.service.AppAgreementService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
@Service
public class AppAgreementServiceImpl implements AppAgreementService {
@Autowired
private AppAgreementMapper appAgreementMapper;
@Override
public AppAgreement getLatestByType(Integer type) {
LambdaQueryWrapper<AppAgreement> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AppAgreement::getType, type)
.le(AppAgreement::getEffectiveDate, LocalDateTime.now()) // 仅查询已生效的
.orderByDesc(AppAgreement::getEffectiveDate) // 按生效日期降序
.orderByDesc(AppAgreement::getVersion) // 辅助按版本号降序(可选)
.last("LIMIT 1"); // 只取一条
return appAgreementMapper.selectOne(wrapper);
}
}

@ -740,6 +740,10 @@ public class ScClaTimeServiceImpl extends ServiceImpl<ScClaTimeMapper, ScClaTime
//判断是否有学员未签到
Long notCheckIn=bookCourses.stream().filter(l->l.getCheckIn()==0).count();
if (notCheckIn>0){
//todo 排除无需签到的学员
//确认上课时 直接扣费
//但要在预约的时候 判断余额 算上已预约的课程 不能超额
//在后台修改余额的时候 算上已预约的课程 不能透支 提醒:当前会员卡余额或被预约占用,剩余额度不足; 无法修改
return new RestResponse().setSuccess(false).setMessage("请确认学员签到情况,如无法到课请联系学员取消预约!");
}
//签到学员
@ -921,39 +925,6 @@ public class ScClaTimeServiceImpl extends ServiceImpl<ScClaTimeMapper, ScClaTime
// 保存上课记录
attendService.save(addClaTimeAttend);
// 上课日志
// StringBuffer sb = new StringBuffer("");
// sb.append("上课[").append(ClaTimeAttendStatusEnums.getNameByStatus("1")).append("],上课时间")
// .append(DateUtil.now()).append(",");
// ScStudentCourseLog studentCourseLog = ScStudentCourseLog.builder()
// .studentId(bookCourse.getStudentId())
// .logType(LogTypeEnum.ATTEND_CLA.getLogType())
// .courseId(scCourse.getCourseId())
// .courseName(scCourse.getCourseName())
// .claId(dbCla.getClaId())
// .claName(dbCla.getClaName())
// .deptName(sysDept.getDeptName())
// .createUser(loginUser.getAppUserId())
// .createUserType("app")
// .createUserName(loginUser.getAppUser().getUserName())
// .createTime(new Date())
// .build();
// //count:课时 days:时间 total_fee:储值
// if (chargeType.equals("count")) {
// studentCourseLog.setChangeHour(addClaTimeAttend.getPayHour());
// studentCourseLog.setAfterBalanceHour(addClaTimeAttend.getCountAfter());
// sb.append("扣减").append(addClaTimeAttend.getPayHour()).append("课时;");
// }
// if (chargeType.equals("total_fee")) {
// studentCourseLog.setChangeFee(addClaTimeAttend.getPayFee());
// studentCourseLog.setAfterBalanceHour(addClaTimeAttend.getFeeAfter());
// sb.append("扣减").append(addClaTimeAttend.getPayHour()).append("元;");
// }
// studentCourseLog.setMemo(sb.toString());
// }
// 保存学生日志
// studentCourseLogMapper.insert(studentCourseLog);
return new RestResponse().setSuccess(true).setMessage("签到成功!");
}

@ -24,6 +24,10 @@ public interface SysUserMapper
public SysUser selectUserByUserName(String userName);
public SysUser selectUserByPhonenumber(String phonenumber);
/**
* ID
*

@ -28,6 +28,6 @@ public interface ISysUserService
*/
public SysUser selectUserById(Long userId);
SysUser selectUserByPhonenumber (String phonenumber);
}

@ -64,6 +64,13 @@ public class SysUserServiceImpl implements ISysUserService
return userMapper.selectUserByUserName(userName);
}
@Override
public SysUser selectUserByPhonenumber (String phonenumber)
{
return userMapper.selectUserByPhonenumber(phonenumber);
}
/**
* ID
*

@ -56,7 +56,7 @@
</select>
<select id="getAppIndex" resultType="SysAppIndex">
select * from sys_app_index order by sort
select * from sys_app_index where display = 1 order by sort
</select>
</mapper>

@ -58,6 +58,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where u.user_name = #{userName} and u.delete_flag = '0'
</select>
<select id="selectUserByPhonenumber" parameterType="String" resultMap="SysUserResult">
<include refid="selectUserVo"/>
where u.phonenumber = #{phonenumber} and u.delete_flag = '0'
</select>
<select id="selectUserById" parameterType="Long" resultMap="SysUserResult">
<include refid="selectUserVo"/>
where

Loading…
Cancel
Save