feat: 添加签到模块和数据库表结构
- 新增gym-checkIn签到模块 - 创建sign_in_record签到记录表(V13迁移脚本) - 添加签到相关路由配置 - 更新应用配置以支持签到模块 - 添加Redis密钥常量
This commit is contained in:
+4
@@ -206,4 +206,8 @@ public class SignInRecord {
|
|||||||
public void restore() {
|
public void restore() {
|
||||||
this.isDelete = false;
|
this.isDelete = false;
|
||||||
}
|
}
|
||||||
|
<<<<<<< HEAD
|
||||||
}
|
}
|
||||||
|
=======
|
||||||
|
}
|
||||||
|
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
|
||||||
|
|||||||
+4
@@ -78,4 +78,8 @@ public interface ICheckInService {
|
|||||||
* @return 签到统计VO
|
* @return 签到统计VO
|
||||||
*/
|
*/
|
||||||
Mono<SignInStatsVO> getDailySignInStats(LocalDate date);
|
Mono<SignInStatsVO> getDailySignInStats(LocalDate date);
|
||||||
|
<<<<<<< HEAD
|
||||||
}
|
}
|
||||||
|
=======
|
||||||
|
}
|
||||||
|
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
|
||||||
|
|||||||
+82
@@ -2,8 +2,11 @@ package cn.novalon.gym.manage.checkIn.service.impl;
|
|||||||
|
|
||||||
import cn.hutool.core.bean.BeanUtil;
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
import cn.hutool.json.JSONUtil;
|
import cn.hutool.json.JSONUtil;
|
||||||
|
<<<<<<< HEAD
|
||||||
import cn.hutool.extra.qrcode.QrCodeUtil;
|
import cn.hutool.extra.qrcode.QrCodeUtil;
|
||||||
import cn.hutool.extra.qrcode.QrConfig;
|
import cn.hutool.extra.qrcode.QrConfig;
|
||||||
|
=======
|
||||||
|
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
|
||||||
import cn.novalon.gym.manage.checkIn.config.QRCodeConfig;
|
import cn.novalon.gym.manage.checkIn.config.QRCodeConfig;
|
||||||
import cn.novalon.gym.manage.checkIn.constant.QRRedisKey;
|
import cn.novalon.gym.manage.checkIn.constant.QRRedisKey;
|
||||||
import cn.novalon.gym.manage.checkIn.entity.SignInRecord;
|
import cn.novalon.gym.manage.checkIn.entity.SignInRecord;
|
||||||
@@ -21,6 +24,11 @@ import cn.novalon.gym.manage.member.entity.MemberCardRecord;
|
|||||||
import cn.novalon.gym.manage.member.enums.MemberCardType;
|
import cn.novalon.gym.manage.member.enums.MemberCardType;
|
||||||
import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
|
import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
|
||||||
import cn.novalon.gym.manage.member.repository.MemberCardRepository;
|
import cn.novalon.gym.manage.member.repository.MemberCardRepository;
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
import cn.hutool.extra.qrcode.QrCodeUtil;
|
||||||
|
import cn.hutool.extra.qrcode.QrConfig;
|
||||||
|
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -84,6 +92,10 @@ public class CheckServiceImpl implements ICheckInService {
|
|||||||
public Mono<String> checkIn(Long memberId, String qrContent) {
|
public Mono<String> checkIn(Long memberId, String qrContent) {
|
||||||
String key = RedisKeyConstants.QRCODE_USER_DAILY + memberId + LocalDate.now();
|
String key = RedisKeyConstants.QRCODE_USER_DAILY + memberId + LocalDate.now();
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
// 先检查当天是否已经签到(从数据库查询,作为重复签到的额外保障)
|
||||||
|
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
|
||||||
return checkTodayAlreadySignedIn(memberId)
|
return checkTodayAlreadySignedIn(memberId)
|
||||||
.flatMap(existingRecord -> {
|
.flatMap(existingRecord -> {
|
||||||
String checkInTime = existingRecord.getSignInTime().format(DATE_FORMATTER);
|
String checkInTime = existingRecord.getSignInTime().format(DATE_FORMATTER);
|
||||||
@@ -125,6 +137,14 @@ public class CheckServiceImpl implements ICheckInService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
/**
|
||||||
|
* 检查会员当天是否已经签到
|
||||||
|
* @param memberId 会员ID
|
||||||
|
* @return 如果已签到返回签到记录,否则返回空
|
||||||
|
*/
|
||||||
|
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
|
||||||
private Mono<SignInRecord> checkTodayAlreadySignedIn(Long memberId) {
|
private Mono<SignInRecord> checkTodayAlreadySignedIn(Long memberId) {
|
||||||
LocalDateTime startOfDay = LocalDate.now().atStartOfDay();
|
LocalDateTime startOfDay = LocalDate.now().atStartOfDay();
|
||||||
LocalDateTime endOfDay = LocalDate.now().atTime(LocalTime.MAX);
|
LocalDateTime endOfDay = LocalDate.now().atTime(LocalTime.MAX);
|
||||||
@@ -132,9 +152,19 @@ public class CheckServiceImpl implements ICheckInService {
|
|||||||
return signInRecordRepository.findByMemberIdAndDate(memberId, startOfDay, endOfDay);
|
return signInRecordRepository.findByMemberIdAndDate(memberId, startOfDay, endOfDay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
private Mono<String> processCheckIn(Long memberId, Long memberCardRecordId, Map<String, Object> redisMap, String qrContent) {
|
private Mono<String> processCheckIn(Long memberId, Long memberCardRecordId, Map<String, Object> redisMap, String qrContent) {
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
|
=======
|
||||||
|
/**
|
||||||
|
* 处理签到逻辑
|
||||||
|
*/
|
||||||
|
private Mono<String> processCheckIn(Long memberId, Long memberCardRecordId, Map<String, Object> redisMap, String qrContent) {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
|
// 发送实时进度通知
|
||||||
|
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
|
||||||
MyWebSocketHandler.sendProgress(qrContent, "VALIDATE_CARD", "正在验证会员卡...");
|
MyWebSocketHandler.sendProgress(qrContent, "VALIDATE_CARD", "正在验证会员卡...");
|
||||||
|
|
||||||
return memberCardRecordRepository.findById(memberCardRecordId)
|
return memberCardRecordRepository.findById(memberCardRecordId)
|
||||||
@@ -153,8 +183,15 @@ public class CheckServiceImpl implements ICheckInService {
|
|||||||
return Mono.error(new RuntimeException("会员卡已过期"));
|
return Mono.error(new RuntimeException("会员卡已过期"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
MyWebSocketHandler.sendProgress(qrContent, "VALIDATE_BOOKING", "会员卡验证通过,正在检查预约信息...");
|
MyWebSocketHandler.sendProgress(qrContent, "VALIDATE_BOOKING", "会员卡验证通过,正在检查预约信息...");
|
||||||
|
|
||||||
|
=======
|
||||||
|
// 发送实时进度通知
|
||||||
|
MyWebSocketHandler.sendProgress(qrContent, "VALIDATE_BOOKING", "会员卡验证通过,正在检查预约信息...");
|
||||||
|
|
||||||
|
// 检查是否有需要签到的团课预约
|
||||||
|
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
|
||||||
return validateBooking(memberId, now)
|
return validateBooking(memberId, now)
|
||||||
.then(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(cardRecord.getMemberCardId())
|
.then(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(cardRecord.getMemberCardId())
|
||||||
.switchIfEmpty(Mono.defer(() -> {
|
.switchIfEmpty(Mono.defer(() -> {
|
||||||
@@ -162,6 +199,10 @@ public class CheckServiceImpl implements ICheckInService {
|
|||||||
return Mono.error(new RuntimeException("会员卡类型不存在"));
|
return Mono.error(new RuntimeException("会员卡类型不存在"));
|
||||||
}))
|
}))
|
||||||
.flatMap(card -> {
|
.flatMap(card -> {
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
// 发送实时进度通知
|
||||||
|
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
|
||||||
MyWebSocketHandler.sendProgress(qrContent, "DEDUCT_USAGE", "正在扣减会员卡次数...");
|
MyWebSocketHandler.sendProgress(qrContent, "DEDUCT_USAGE", "正在扣减会员卡次数...");
|
||||||
|
|
||||||
return deductCardUsage(cardRecord, card)
|
return deductCardUsage(cardRecord, card)
|
||||||
@@ -182,6 +223,12 @@ public class CheckServiceImpl implements ICheckInService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
/**
|
||||||
|
* 验证预约信息
|
||||||
|
*/
|
||||||
|
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
|
||||||
private Mono<Void> validateBooking(Long memberId, LocalDateTime now) {
|
private Mono<Void> validateBooking(Long memberId, LocalDateTime now) {
|
||||||
return groupCourseBookingService.getBookingsByMemberId(memberId)
|
return groupCourseBookingService.getBookingsByMemberId(memberId)
|
||||||
.filter(booking -> {
|
.filter(booking -> {
|
||||||
@@ -213,6 +260,12 @@ public class CheckServiceImpl implements ICheckInService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
/**
|
||||||
|
* 扣减会员卡使用次数/金额
|
||||||
|
*/
|
||||||
|
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
|
||||||
private Mono<MemberCardRecord> deductCardUsage(MemberCardRecord record, MemberCard card) {
|
private Mono<MemberCardRecord> deductCardUsage(MemberCardRecord record, MemberCard card) {
|
||||||
MemberCardType cardType = MemberCardType.valueOf(card.getMemberCardType());
|
MemberCardType cardType = MemberCardType.valueOf(card.getMemberCardType());
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
@@ -248,6 +301,12 @@ public class CheckServiceImpl implements ICheckInService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
/**
|
||||||
|
* 保存签到记录
|
||||||
|
*/
|
||||||
|
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
|
||||||
private Mono<Void> saveSignInRecord(Long memberId, Long memberCardRecordId, Long memberCardId) {
|
private Mono<Void> saveSignInRecord(Long memberId, Long memberCardRecordId, Long memberCardId) {
|
||||||
SignInRecord record = SignInRecord.builder()
|
SignInRecord record = SignInRecord.builder()
|
||||||
.memberId(memberId)
|
.memberId(memberId)
|
||||||
@@ -262,6 +321,12 @@ public class CheckServiceImpl implements ICheckInService {
|
|||||||
return signInRecordRepository.save(record).then();
|
return signInRecordRepository.save(record).then();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
/**
|
||||||
|
* 构建成功响应
|
||||||
|
*/
|
||||||
|
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
|
||||||
private String buildSuccessResponse(LocalDateTime dateTime) {
|
private String buildSuccessResponse(LocalDateTime dateTime) {
|
||||||
Map<String, Object> res = new HashMap<>();
|
Map<String, Object> res = new HashMap<>();
|
||||||
res.put("message", "签到成功");
|
res.put("message", "签到成功");
|
||||||
@@ -269,6 +334,12 @@ public class CheckServiceImpl implements ICheckInService {
|
|||||||
return JSONUtil.toJsonStr(res);
|
return JSONUtil.toJsonStr(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
/**
|
||||||
|
* 查找会员的有效会员卡(优先选择有效期最早到期的)
|
||||||
|
*/
|
||||||
|
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
|
||||||
private Mono<MemberCardRecord> findValidMemberCard(Long memberId) {
|
private Mono<MemberCardRecord> findValidMemberCard(Long memberId) {
|
||||||
return memberCardRecordRepository.findActiveCardsByMemberId(memberId)
|
return memberCardRecordRepository.findActiveCardsByMemberId(memberId)
|
||||||
.filter(record -> {
|
.filter(record -> {
|
||||||
@@ -286,6 +357,11 @@ public class CheckServiceImpl implements ICheckInService {
|
|||||||
.next();
|
.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
// ==================== 签到记录管理功能 ====================
|
||||||
|
|
||||||
|
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
|
||||||
@Override
|
@Override
|
||||||
public Flux<SignInRecordVO> getSignInRecords(Long memberId, LocalDate startTime, LocalDate endTime) {
|
public Flux<SignInRecordVO> getSignInRecords(Long memberId, LocalDate startTime, LocalDate endTime) {
|
||||||
LocalDateTime start = startTime.atStartOfDay();
|
LocalDateTime start = startTime.atStartOfDay();
|
||||||
@@ -383,6 +459,12 @@ public class CheckServiceImpl implements ICheckInService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
/**
|
||||||
|
* 转换实体到VO
|
||||||
|
*/
|
||||||
|
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
|
||||||
private SignInRecordVO convertToVO(SignInRecord record) {
|
private SignInRecordVO convertToVO(SignInRecord record) {
|
||||||
SignInRecordVO vo = new SignInRecordVO();
|
SignInRecordVO vo = new SignInRecordVO();
|
||||||
vo.setId(record.getId());
|
vo.setId(record.getId());
|
||||||
|
|||||||
+79
@@ -77,7 +77,11 @@ public class MyWebSocketHandler implements WebSocketHandler {
|
|||||||
.doOnError(e -> {
|
.doOnError(e -> {
|
||||||
log.error("接收消息出错,sessionId={}", sessionId, e);
|
log.error("接收消息出错,sessionId={}", sessionId, e);
|
||||||
})
|
})
|
||||||
|
<<<<<<< HEAD
|
||||||
.subscribe();
|
.subscribe();
|
||||||
|
=======
|
||||||
|
.subscribe(); // 必须订阅,否则不会执行
|
||||||
|
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
|
||||||
|
|
||||||
// 发送流给客户端
|
// 发送流给客户端
|
||||||
return session.send(sink.asFlux().map(session::textMessage))
|
return session.send(sink.asFlux().map(session::textMessage))
|
||||||
@@ -100,6 +104,10 @@ public class MyWebSocketHandler implements WebSocketHandler {
|
|||||||
* @return 是否发送成功
|
* @return 是否发送成功
|
||||||
*/
|
*/
|
||||||
public static boolean sendMessageToClient(String qrContent, String message) {
|
public static boolean sendMessageToClient(String qrContent, String message) {
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
// 先清理超时连接
|
||||||
|
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
|
||||||
cleanupTimeoutConnections();
|
cleanupTimeoutConnections();
|
||||||
|
|
||||||
Sinks.Many<String> sink = qrContentToSink.get(qrContent);
|
Sinks.Many<String> sink = qrContentToSink.get(qrContent);
|
||||||
@@ -129,6 +137,77 @@ public class MyWebSocketHandler implements WebSocketHandler {
|
|||||||
String progressMessage = buildMessage("PROGRESS", message);
|
String progressMessage = buildMessage("PROGRESS", message);
|
||||||
sendMessageToClient(qrContent, progressMessage);
|
sendMessageToClient(qrContent, progressMessage);
|
||||||
log.debug("发送进度消息: qrContent={}, step={}, message={}", qrContent, step, message);
|
log.debug("发送进度消息: qrContent={}, step={}, message={}", qrContent, step, message);
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送签到成功消息
|
||||||
|
*
|
||||||
|
* @param qrContent 二维码内容
|
||||||
|
* @param memberId 会员ID
|
||||||
|
* @param signInTime 签到时间
|
||||||
|
*/
|
||||||
|
public static void sendSuccess(String qrContent, Long memberId, String signInTime) {
|
||||||
|
String successMessage = buildMessage("SUCCESS", "签到成功!欢迎光临\n会员ID: " + memberId + "\n签到时间: " + signInTime);
|
||||||
|
sendMessageToClient(qrContent, successMessage);
|
||||||
|
log.info("发送成功消息: qrContent={}, memberId={}", qrContent, memberId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送签到失败消息
|
||||||
|
*
|
||||||
|
* @param qrContent 二维码内容
|
||||||
|
* @param reason 失败原因
|
||||||
|
*/
|
||||||
|
public static void sendFailure(String qrContent, String reason) {
|
||||||
|
String failureMessage = buildMessage("FAILURE", "签到失败:" + reason);
|
||||||
|
sendMessageToClient(qrContent, failureMessage);
|
||||||
|
log.warn("发送失败消息: qrContent={}, reason={}", qrContent, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建标准消息格式
|
||||||
|
*
|
||||||
|
* @param type 消息类型
|
||||||
|
* @param content 消息内容
|
||||||
|
* @return 格式化后的消息字符串
|
||||||
|
*/
|
||||||
|
private static String buildMessage(String type, String content) {
|
||||||
|
return JSONUtil.toJsonStr(Map.of(
|
||||||
|
"type", type,
|
||||||
|
"content", content,
|
||||||
|
"timestamp", System.currentTimeMillis()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理超时连接
|
||||||
|
*/
|
||||||
|
private static void cleanupTimeoutConnections() {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
qrContentToCreateTime.entrySet().removeIf(entry -> {
|
||||||
|
LocalDateTime createTime = entry.getValue();
|
||||||
|
long secondsDiff = java.time.Duration.between(createTime, now).getSeconds();
|
||||||
|
if (secondsDiff > TIMEOUT_SECONDS) {
|
||||||
|
String qrContent = entry.getKey();
|
||||||
|
qrContentToSink.remove(qrContent);
|
||||||
|
log.debug("清理超时连接: qrContent={}, 超时时间={}秒", qrContent, secondsDiff);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前连接数
|
||||||
|
*
|
||||||
|
* @return 连接数
|
||||||
|
*/
|
||||||
|
public static int getConnectionCount() {
|
||||||
|
cleanupTimeoutConnections();
|
||||||
|
return qrContentToSink.size();
|
||||||
|
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+4
@@ -25,6 +25,10 @@ import reactor.test.StepVerifier;
|
|||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
import java.time.LocalTime;
|
||||||
|
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
|||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 签到记录表(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-已删除';
|
||||||
Reference in New Issue
Block a user