16 changed files with 39 additions and 244 deletions
@@ -1,6 +1,7 @@
package cn.novalon.gym.manage.auth.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -8,7 +9,7 @@ import lombok.NoArgsConstructor;
/**
* 手机号一键登录请求DTO
* uniapp官方一键登录流程:前端通过云函数获取access_token和openid传给后端换取手机号
* uniapp官方一键登录流程:前端通过云函数获取手机号,直接传给后端
*/
@Data
@Builder
@@ -16,11 +17,9 @@ import lombok.NoArgsConstructor;
@AllArgsConstructor
public class PhoneLoginDto {
@NotBlank(message = "access_token不能为空")
private String accessToken;
@NotBlank(message = "openid不能为空")
private String openid;
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
private String nickname;
@@ -2,7 +2,6 @@ package cn.novalon.gym.manage.auth.handler;
import cn.novalon.gym.manage.auth.dto.PhoneCodeLoginDto;
import cn.novalon.gym.manage.auth.dto.PhoneLoginDto;
import cn.novalon.gym.manage.auth.dto.SendCodeRequest;
import cn.novalon.gym.manage.auth.service.PhoneAuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -39,8 +38,8 @@ public class PhoneAuthHandler {
public Mono<ServerResponse> sendSmsCode(ServerRequest request) {
log.info("收到发送短信验证码请求");
return request.bodyToMono(SendCodeRequest.class)
.flatMap(req -> phoneAuthService.sendSmsCode(req.getPhone()))
return request.bodyToMono(PhoneLoginDto.class)
.flatMap(dto -> phoneAuthService.sendSmsCode(dto.getPhone()))
.flatMap(success -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(Map.of("success", success, "message", success ? "验证码发送成功" : "验证码发送失败")));
@@ -1,6 +1,5 @@
package cn.novalon.gym.manage.auth.service.impl;
import cn.novalon.gym.manage.auth.config.SmsProperties;
import cn.novalon.gym.manage.auth.dto.PhoneCodeLoginDto;
import cn.novalon.gym.manage.auth.dto.PhoneLoginDto;
import cn.novalon.gym.manage.auth.service.PhoneAuthService;
@@ -16,13 +15,6 @@ import cn.novalon.gym.manage.member.util.AesUtil;
import cn.novalon.gym.manage.member.util.EsSyncUtils;
import cn.novalon.gym.manage.member.util.MemberNoGenerator;
import cn.novalon.gym.manage.sys.security.JwtTokenProvider;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dypnsapi.model.v20170525.GetMobileRequest;
import com.aliyuncs.dypnsapi.model.v20170525.GetMobileResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.profile.DefaultProfile;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -43,7 +35,6 @@ public class PhoneAuthServiceImpl implements PhoneAuthService {
private final EsSyncUtils esSyncUtils;
private final JwtTokenProvider jwtTokenProvider;
private final SmsService smsService;
private final SmsProperties smsProperties;
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer;
@@ -54,12 +45,9 @@ public class PhoneAuthServiceImpl implements PhoneAuthService {
@Override
public Mono<PhoneLoginVO> oneClickLogin(PhoneLoginDto request) {
log.info("手机号一键登录请求, accessToken: {}, openid: {}", request.getAccessToken(), request.getOpenid());
log.info("手机号一键登录, phone: {}", request.getPhone());
return Mono.fromCallable(() -> getPhoneByToken(request.getAccessToken(), request.getOpenid()))
.flatMap(phone -> {
log.info("通过access_token获取手机号成功: {}", maskPhone(phone));
String encryptedPhone = encryptPhone(phone);
String encryptedPhone = encryptPhone(request.getPhone());
return memberRepository.findByPhone(encryptedPhone)
.flatMap(existingMember -> {
@@ -70,47 +58,6 @@ public class PhoneAuthServiceImpl implements PhoneAuthService {
log.info("手机号未注册,创建新会员");
return createNewMemberAndLogin(request, encryptedPhone);
}));
})
.onErrorResume(e -> {
log.error("一键登录失败", e);
return Mono.error(new SystemException(ErrorCode.AUTH_PHONE_ERROR, "手机号获取失败: " + e.getMessage()));
});
}
/**
* 通过access_token和openid获取手机号
* 使用阿里云DYPNS API
*/
private String getPhoneByToken(String accessToken, String openid) throws Exception {
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", smsProperties.getAccessKeyId(), smsProperties.getAccessKeySecret());
IAcsClient client = new DefaultAcsClient(profile);
GetMobileRequest request = new GetMobileRequest();
request.setAccessToken(accessToken);
try {
GetMobileResponse response = client.getAcsResponse(request);
if ("OK".equals(response.getCode())) {
return response.getGetMobileResultDTO().getMobile();
}
log.warn("DYPNS API返回错误: code={}, message={}", response.getCode(), response.getMessage());
throw new SystemException(ErrorCode.AUTH_PHONE_ERROR, "手机号获取失败: " + response.getMessage());
} catch (ServerException e) {
log.error("阿里云DYPNS API服务端异常", e);
throw new SystemException(ErrorCode.AUTH_PHONE_ERROR, "手机号获取失败");
} catch (ClientException e) {
log.error("阿里云DYPNS API客户端异常", e);
throw new SystemException(ErrorCode.AUTH_PHONE_ERROR, "手机号获取失败");
}
}
/**
* 手机号脱敏显示
*/
private String maskPhone(String phone) {
if (phone == null || phone.length() < 11) {
return phone;
}
return phone.substring(0, 3) + "****" + phone.substring(7);
}
@Override
@@ -140,6 +87,7 @@ public class PhoneAuthServiceImpl implements PhoneAuthService {
.switchIfEmpty(Mono.defer(() -> {
log.info("手机号未注册,创建新会员");
PhoneLoginDto registerRequest = new PhoneLoginDto();
registerRequest.setPhone(request.getPhone());
return createNewMemberAndLogin(registerRequest, encryptedPhone);
}));
});
@@ -205,7 +153,7 @@ public class PhoneAuthServiceImpl implements PhoneAuthService {
vo.setNeedCompleteInfo(needCompleteInfo);
vo.setNickname(member.getNickname());
vo.setAvatar(member.getAvatar());
vo.setPhone(member.getPhone() != null ? decryptPhone(member.getPhone()) : null);
vo.setPhone(decryptPhone(member.getPhone()));
return vo;
}
@@ -1,43 +0,0 @@
package cn.novalon.gym.manage.checkIn.config;
import cn.novalon.gym.manage.checkIn.websocket.MyWebSocketHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class WebSocketConfig {
@Autowired
private MyWebSocketHandler myWebSocketHandler;
/**
* 注册 WebSocket 路由映射
* 路径对应前端连接的 ws://xxx/webSocket/checkIn
*/
@Bean
public HandlerMapping webSocketMapping() {
Map<String, WebSocketHandler> map = new HashMap<>();
map.put("/webSocket/checkIn", myWebSocketHandler);
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setUrlMap(map);
mapping.setOrder(10); // 设置优先级
return mapping;
}
/**
* 注册 WebSocket 处理器适配器(必须)
*/
@Bean
public WebSocketHandlerAdapter handlerAdapter() {
return new WebSocketHandlerAdapter();
}
}
@@ -2,8 +2,6 @@ package cn.novalon.gym.manage.checkIn.handler;
import cn.novalon.gym.manage.checkIn.service.impl.CheckServiceImpl;
import cn.novalon.gym.manage.checkIn.websocket.MyWebSocketHandler;
import cn.novalon.gym.manage.checkIn.vo.SignInRecordVO;
import cn.novalon.gym.manage.checkIn.vo.SignInStatsVO;
import cn.novalon.gym.manage.sys.util.AuthUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -14,7 +12,6 @@ import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import cn.novalon.gym.manage.checkIn.websocket.MyWebSocketHandler;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
@@ -39,7 +36,6 @@ public class CheckInHandler {
public Mono<ServerResponse> checkIn(ServerRequest request) {
Long memberId = authUtil.getMemberIdOrThrow(request);
return request.bodyToMono(Map.class)
.flatMap(body -> {
String qrContent = (String) body.get("qrContent");
@@ -2,8 +2,6 @@ package cn.novalon.gym.manage.checkIn.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.json.JSONUtil;
import cn.hutool.extra.qrcode.QrCodeUtil;
import cn.hutool.extra.qrcode.QrConfig;
import cn.novalon.gym.manage.checkIn.config.QRCodeConfig;
import cn.novalon.gym.manage.checkIn.constant.QRRedisKey;
import cn.novalon.gym.manage.checkIn.entity.SignInRecord;
@@ -21,6 +19,8 @@ import cn.novalon.gym.manage.member.entity.MemberCardRecord;
import cn.novalon.gym.manage.member.enums.MemberCardType;
import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
import cn.novalon.gym.manage.member.repository.MemberCardRepository;
import cn.hutool.extra.qrcode.QrCodeUtil;
import cn.hutool.extra.qrcode.QrConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
-6
View File
@@ -93,12 +93,6 @@
<artifactId>javase</artifactId>
<version>3.5.3</version>
</dependency>
<!-- 阿里云OSS SDK -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.4</version>
</dependency>
</dependencies>
<build>
@@ -1,8 +1,8 @@
package cn.novalon.gym.manage.groupcourse.util;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.PutObjectRequest;
//import com.aliyun.oss.OSS;
//import com.aliyun.oss.OSSClientBuilder;
//import com.aliyun.oss.model.PutObjectRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -1,6 +1,5 @@
package cn.novalon.gym.manage.member.service.impl;
import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.member.entity.MemberCard;
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
import cn.novalon.gym.manage.member.entity.MemberCardTransaction;
@@ -16,6 +15,7 @@ import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
import cn.novalon.gym.manage.member.repository.MemberCardRepository;
import cn.novalon.gym.manage.member.service.IMemberCardService;
import cn.novalon.gym.manage.member.service.IMemberCardTransactionService;
import cn.novalon.gym.manage.common.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
@@ -1,99 +0,0 @@
-- ============================================
-- 签到记录表(sign_in_record
-- ============================================
-- Step 1: 创建 sign_in_record 表
CREATE TABLE IF NOT EXISTS sign_in_record (
-- ========== 主键和基础字段 ==========
id BIGSERIAL PRIMARY KEY, -- 自增主键
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 记录创建时间
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 记录更新时间
-- ========== 会员相关字段 ==========
member_id BIGINT NOT NULL, -- 会员ID,关联member_user表
member_card_id BIGINT, -- 签到时使用的会员卡ID
-- ========== 签到核心字段 ==========
sign_in_time TIMESTAMP NOT NULL, -- 签到入场时间
sign_in_type VARCHAR(20) NOT NULL, -- 签到方式:QR_CODE-扫码签到,MANUAL-手动签到,FACE-人脸识别
sign_in_status VARCHAR(20) NOT NULL DEFAULT 'SUCCESS', -- 签到状态:SUCCESS-成功,FAILED-失败
-- ========== 验证和错误信息 ==========
verification_details TEXT, -- JSONB格式,存储会员卡验证时的快照数据
fail_reason VARCHAR(500), -- 失败时的具体原因文案
-- ========== 操作人信息 ==========
operator_id BIGINT, -- 操作人ID(前台人员),自助签到时为NULL
operator_name VARCHAR(100), -- 操作人姓名冗余
-- ========== 设备和环境信息 ==========
device_info VARCHAR(200), -- 签到设备标识或型号
ip_address VARCHAR(50), -- 客户端IP地址
source VARCHAR(20) NOT NULL, -- 签到来源:MINI_PROGRAM-小程序扫码,PC_BACKEND-后台管理端
-- ========== 软删除字段 ==========
is_delete BOOLEAN DEFAULT FALSE -- 软删除标识:false-未删除,true-已删除
);
-- Step 2: 创建索引
-- 会员ID索引(加速按会员查询签到记录)
CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_id ON sign_in_record(member_id);
-- 签到时间索引(加速按时间范围查询)
CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_time ON sign_in_record(sign_in_time);
-- 签到状态索引(加速按状态筛选)
CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_status ON sign_in_record(sign_in_status);
-- 会员卡ID索引(加速按会员卡查询)
CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_card_id ON sign_in_record(member_card_id);
-- 操作人ID索引(加速按操作人查询)
CREATE INDEX IF NOT EXISTS idx_sign_in_record_operator_id ON sign_in_record(operator_id);
-- 签到来源索引(加速按来源统计)
CREATE INDEX IF NOT EXISTS idx_sign_in_record_source ON sign_in_record(source);
-- 软删除索引(加速查询未删除的记录)
CREATE INDEX IF NOT EXISTS idx_sign_in_record_is_delete ON sign_in_record(is_delete);
-- 复合索引:会员ID + 签到时间(加速会员签到历史查询)
CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_time ON sign_in_record(member_id, sign_in_time);
-- 复合索引:签到状态 + 签到时间(加速统计数据查询)
CREATE INDEX IF NOT EXISTS idx_sign_in_record_status_time ON sign_in_record(sign_in_status, sign_in_time);
-- 复合索引:签到日期(用于每日统计,使用表达式索引)
CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_date ON sign_in_record(DATE(sign_in_time));
-- Step 3: 添加外键约束(可选,根据业务需求决定)
-- 注意:如果member_user表使用了软删除,外键约束可能需要谨慎使用
-- ALTER TABLE sign_in_record
-- ADD CONSTRAINT fk_sign_in_record_member
-- FOREIGN KEY (member_id) REFERENCES member_user(id) ON DELETE SET NULL;
-- Step 4: 添加注释
COMMENT ON TABLE sign_in_record IS '会员到店签到记录表';
COMMENT ON COLUMN sign_in_record.id IS '自增主键';
COMMENT ON COLUMN sign_in_record.created_at IS '记录创建时间';
COMMENT ON COLUMN sign_in_record.updated_at IS '记录更新时间';
COMMENT ON COLUMN sign_in_record.member_id IS '会员ID,关联member_user表';
COMMENT ON COLUMN sign_in_record.member_card_id IS '签到时使用的会员卡ID';
COMMENT ON COLUMN sign_in_record.sign_in_time IS '签到入场时间';
COMMENT ON COLUMN sign_in_record.sign_in_type IS '签到方式:QR_CODE-扫码签到,MANUAL-手动签到,FACE-人脸识别';
COMMENT ON COLUMN sign_in_record.sign_in_status IS '签到状态:SUCCESS-成功,FAILED-失败';
COMMENT ON COLUMN sign_in_record.verification_details IS 'JSON格式,存储会员卡验证时的快照数据(包含卡类型、剩余次数/金额、有效期等)';
COMMENT ON COLUMN sign_in_record.fail_reason IS '失败时的具体原因文案';
COMMENT ON COLUMN sign_in_record.operator_id IS '操作人ID(前台人员),自助签到时为NULL';
COMMENT ON COLUMN sign_in_record.operator_name IS '操作人姓名冗余(避免关联查询)';
COMMENT ON COLUMN sign_in_record.device_info IS '签到设备标识或型号';
COMMENT ON COLUMN sign_in_record.ip_address IS '客户端IP地址';
COMMENT ON COLUMN sign_in_record.source IS '签到来源:MINI_PROGRAM-小程序扫码,PC_BACKEND-后台管理端';
COMMENT ON COLUMN sign_in_record.is_delete IS '软删除标识:false-未删除,true-已删除';
@@ -64,7 +64,7 @@ signature:
max-age-minutes: ${SIGNATURE_MAX_AGE_MINUTES:5}
nonce-cache-size: ${SIGNATURE_NONCE_CACHE_SIZE:10000}
whitelist:
paths: ${SIGNATURE_WHITELIST_PATHS:/actuator/health,/actuator/info,/api/auth/login,/api/auth/register,/api/member/auth/miniapp/login,/api/member/auth/mp/callback,/api/groupCourse/**}
paths: ${SIGNATURE_WHITELIST_PATHS:/actuator/health,/actuator/info,/api/auth/login,/api/auth/register,/api/member/auth/miniapp/login,/api/member/auth/mp/callback}
resilience:
enabled: ${RESILIENCE_ENABLED:true}
@@ -1,4 +1,4 @@
package cn.novalon.gym.manage.sys.config;
package cn.novalon.gym.manage.sys.config;
import cn.novalon.gym.manage.sys.audit.OperationLogWebFilter;
import cn.novalon.gym.manage.sys.security.JwtAuthenticationFilter;
@@ -66,6 +66,7 @@ public class SecurityConfig {
.pathMatchers("/api/payment/notify").permitAll()
.pathMatchers("/api/payment/refund").permitAll();
if (isDevOrTest) {
spec.pathMatchers("/swagger-ui.html").permitAll()
.pathMatchers("/swagger-ui/**").permitAll()