Author SHA1 Message Date
liwentao 3511231337 Merge remote-tracking branch 'origin/feature/data-count' into feature/data-count 2026-06-09 18:26:49 +08:00
liwentao 2366a3fd73 完成模块4,4.1基础数据统计的部分功能 2026-06-09 18:26:31 +08:00
liwentao f5ca726ded Merge remote-tracking branch 'origin/feature/data-count' into feature/data-count
# Conflicts:
#	gym-manage-api/docs/datacount-api.md
#	gym-manage-api/gym-dataCount/pom.xml
#	gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/config/DataCountRouter.java
#	gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/dao/StatisticsDao.java
#	gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/StatisticsExportDTO.java
#	gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/handler/DataCountHandler.java
#	gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/scheduler/StatisticsDailyScheduler.java
#	gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/IDataCountService.java
#	gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/impl/DataCountServiceImpl.java
#	gym-manage-api/log.text
#	gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java
#	gym-manage-api/pom.xml
2026-06-09 18:19:33 +08:00
liwentao 54a65b2a74 完善模块4,4.1基础数据统计 2026-06-09 18:16:56 +08:00
liwentao 9446f66567 完成模块4,4.1基础数据统计的部分功能 2026-06-09 18:16:29 +08:00
liwentao 7465074eb7 完成模块4,4.1基础数据统计的部分功能 2026-06-09 17:32:33 +08:00
liwentao 8958ffaa9e 完成模块4,4.1基础数据统计的部分功能 2026-06-06 15:19:40 +08:00
124 changed files with 3825 additions and 8952 deletions
-202
View File
@@ -1,202 +0,0 @@
import java.util.Base64;
import java.math.BigInteger;
import java.io.ByteArrayInputStream;
public class KeyAnalyzer {
public static void main(String[] args) {
String privateKeyBase64 = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCcxAwrhZF+izz1gxQKOr4jnC05obIBHbl0DzHOcd4CaaDV7Kv1NRJKi33JhdDsW4JGgu16e5Rtzq1VzU5VWz5EKgGL46maOFwCkngJUTP/LC3JVf/wtJmYCm8xNE8C7GNNIqKzYEvUOEfXqagpVvVrGsQ9FpzFp2rP9hBmHY3yizVyPX/uT9S+af5TRxiaItj3SSJGgloaEMrnKOpb/EH7JwPSS0liAyT/NxPfOyyZHc22AvaAIOE5y+0PMUIKPuIdfpOrej3LVpO1Arc2hSgmdB+YIPSiBVYPXa6AuAmil9mpbtSikQJ7Uu7lX4JyTW4QxQ06rPFKnFWVKkzivAElAgMBAAECggEAJd0AJ37iTlMpDQ90xqe7hvRQxAu256gbQ9nrqLY97g0/KIw6WEZSPakFX6gvdvb/NzKmUyAIEKGLoh6tXdZk6qfOqc/6BeK47nIcBfwT9/zerjNUVvn34w4aHyNINieMMHQ+Id8PUZmqWH+Euz9ilVTosuyEPwUZulLvUQqwXzU5VnwVghURbUhDd+ecBJACWgemRun6d5241PQXNYAdH1k7cETd8GfIi3qclhhJrxi7tu5tq4YGCXQIoz7HCLim7GIvT0M+FRgSw2EOrHnAQNFeQ/vQbP71ttLoTxehL6Se9dfWrV5OI+Y/T7vR2F84Qt0iNbaxyJGir7siKDFIwQKBgQDXDzinx3/TasplM78pR/0CtuuKr1Ch02LOPrTosJ1qf1OohxQThowhOTxMsBlgYSKu9s1QRffUUXEqYXxd2B6lzDKfggwO6U2XxIcxWeNow0xoFfqcXYSg7Ga2sCr9uhdwxIdFQNF7SNBpT8ht4fJrRX6mWY1nHybpyTDQ4xoQNQKBgQC6m+yiOoi5JD3zVSSJq/iq5DJPA5B4aoP+t5u9lp2Q7iVO0QI5ilBlEKGE4VOU0glnXlDTfuqEYooMY85ekl4WGb3AOT0PhLL2i+gO2nlWBzf4HPzB/hibjfyPyniRM03cHkG3HXucL7Sne6FwERcfjEqjUd2cdP1l89PNrq4rMQKBgQCMmjABSWYh8/y1I5rEQ4OAJdVjC3GdC1Xa35ZpVybjvLEWSpHunhW5lvD8dllw8LC7UTI0XDpGPqTM/4VO2YBYB2PFc0Gs8g0/v0ZgFpOeJ6kpl80MM/wFNemFYTIKRoMSv/psZY9PmfBgGcBBTuquBXZjDcNr+yr2yAm5V/DvTQKBgHqRi94KoF8q5N39IKCkqhJlDH5FkxDktYoKw2rFkPzuzuZz9gghRyj6wXxsG9/2DWMt2dzw0czehFoa/CO188KEadPmRKr6uCmkP2nyKhxNZX+8WnB5G2Sg4DD6BjMpBYz8+qDx5ozx8LDJTYI0V4HLPgMD9JGdbgsXGhlREOkhAoGALr6IQOXnviWNAhCdc7rrsaMLMPbLZ1wqzWtQUG1JxDobbpzEP4CW/mvW5pMn58mSBg5qbXhyDI4fFP0CPb98QIz2tGnIzYyFzdKmF5Z1N7X1OF9O+tsSqASoBZzTqB4fr/o4mz0s9JCeriBR2LWjsbsDU13DTLsfQpWbtOnIy70=";
System.out.println("========== 密钥分析开始 ==========");
System.out.println("密钥长度: " + privateKeyBase64.length());
byte[] keyBytes = Base64.getDecoder().decode(privateKeyBase64);
System.out.println("解码后长度: " + keyBytes.length + " bytes");
// 打印前30字节
System.out.print("前30字节(hex): ");
for (int i = 0; i < 30 && i < keyBytes.length; i++) {
System.out.printf("%02X ", keyBytes[i]);
}
System.out.println();
// 解析ASN.1结构
try {
parseASN1(keyBytes);
} catch (Exception e) {
System.out.println("解析失败: " + e.getMessage());
e.printStackTrace();
}
}
static void parseASN1(byte[] keyBytes) throws Exception {
ByteArrayInputStream bis = new ByteArrayInputStream(keyBytes);
// 读取SEQUENCE
int tag = bis.read();
System.out.println("第一个tag: 0x" + Integer.toHexString(tag));
if (tag == 0x30) {
System.out.println("这是SEQUENCE");
// 读取长度
int len = readASN1Length(bis);
System.out.println("SEQUENCE长度: " + len);
// 检查下一个tag
int nextTag = bis.read();
System.out.println("下一个tag: 0x" + Integer.toHexString(nextTag));
if (nextTag == 0x02) {
// INTEGER
int intLen = readASN1Length(bis);
byte[] versionBytes = new byte[intLen];
bis.read(versionBytes);
int version = new BigInteger(versionBytes).intValue();
System.out.println("版本INTEGER值: " + version);
// 检查这是PKCS#8还是PKCS#1
// PKCS#8: version=0后是SEQUENCE(算法标识符)
// PKCS#1: version=0后是INTEGER(modulus)
int afterVersionTag = bis.read();
System.out.println("版本后的tag: 0x" + Integer.toHexString(afterVersionTag));
if (afterVersionTag == 0x30) {
System.out.println(">>> 这是PKCS#8格式 (version后是SEQUENCE)");
// 解析PKCS#8
// AlgorithmIdentifier: SEQUENCE { OID, NULL }
int algLen = readASN1Length(bis);
System.out.println("AlgorithmIdentifier长度: " + algLen);
// 跳过AlgorithmIdentifier内容
byte[] algBytes = new byte[algLen];
bis.read(algBytes);
// 打印OID
System.out.print("OID bytes: ");
for (int i = 0; i < algBytes.length; i++) {
System.out.printf("%02X ", algBytes[i]);
}
System.out.println();
// 下一个应该是OCTET STRING (包含私钥)
int octetTag = bis.read();
System.out.println("下一个tag: 0x" + Integer.toHexString(octetTag));
if (octetTag == 0x04) {
System.out.println("这是OCTET STRING (包含私钥数据)");
int octetLen = readASN1Length(bis);
System.out.println("OCTET STRING长度: " + octetLen);
// OCTET STRING内容是PKCS#1私钥
byte[] pkcs1Bytes = new byte[octetLen];
bis.read(pkcs1Bytes);
System.out.print("PKCS#1私钥前20字节: ");
for (int i = 0; i < 20; i++) {
System.out.printf("%02X ", pkcs1Bytes[i]);
}
System.out.println();
// 解析PKCS#1私钥
ByteArrayInputStream pkcs1Stream = new ByteArrayInputStream(pkcs1Bytes);
int pkcs1Tag = pkcs1Stream.read();
System.out.println("PKCS#1第一个tag: 0x" + Integer.toHexString(pkcs1Tag));
if (pkcs1Tag == 0x30) {
int pkcs1Len = readASN1Length(pkcs1Stream);
System.out.println("PKCS#1 SEQUENCE长度: " + pkcs1Len);
// version
int vTag = pkcs1Stream.read();
int vLen = readASN1Length(pkcs1Stream);
byte[] vBytes = new byte[vLen];
pkcs1Stream.read(vBytes);
System.out.println("PKCS#1版本: " + new BigInteger(vBytes));
// 解析私钥参数
parsePKCS1(pkcs1Stream);
}
}
} else if (afterVersionTag == 0x02) {
System.out.println(">>> 这是PKCS#1格式 (version后是INTEGER)");
// 继续解析PKCS#1
parsePKCS1(bis);
} else {
System.out.println(">>> 未知格式 (tag: 0x" + Integer.toHexString(afterVersionTag) + ")");
}
}
}
}
static void parsePKCS1(ByteArrayInputStream bis) throws Exception {
System.out.println("\n========== 解析PKCS#1私钥参数 ==========");
// modulus
BigInteger modulus = readASN1Integer(bis);
System.out.println("modulus长度: " + modulus.bitLength() + " bits");
// publicExponent
BigInteger publicExponent = readASN1Integer(bis);
System.out.println("publicExponent: " + publicExponent);
// privateExponent
BigInteger privateExponent = readASN1Integer(bis);
System.out.println("privateExponent长度: " + privateExponent.bitLength() + " bits");
// prime1
BigInteger prime1 = readASN1Integer(bis);
System.out.println("prime1长度: " + prime1.bitLength() + " bits");
// prime2
BigInteger prime2 = readASN1Integer(bis);
System.out.println("prime2长度: " + prime2.bitLength() + " bits");
// 验证 prime1 * prime2 == modulus
BigInteger calculatedModulus = prime1.multiply(prime2);
boolean valid = calculatedModulus.equals(modulus);
System.out.println("\n验证 prime1 * prime2 == modulus: " + valid);
if (!valid) {
System.out.println(">>> 密钥数学关系不正确!这是无效的RSA私钥");
System.out.println("计算得到的modulus长度: " + calculatedModulus.bitLength() + " bits");
}
// exponent1
BigInteger exponent1 = readASN1Integer(bis);
System.out.println("exponent1长度: " + exponent1.bitLength() + " bits");
// exponent2
BigInteger exponent2 = readASN1Integer(bis);
System.out.println("exponent2长度: " + exponent2.bitLength() + " bits");
// coefficient
BigInteger coefficient = readASN1Integer(bis);
System.out.println("coefficient长度: " + coefficient.bitLength() + " bits");
System.out.println("\n========== 解析完成 ==========");
}
static int readASN1Length(ByteArrayInputStream bis) throws Exception {
int firstByte = bis.read();
if ((firstByte & 0x80) == 0) {
return firstByte;
}
int numBytes = firstByte & 0x7F;
int length = 0;
for (int i = 0; i < numBytes; i++) {
length = (length << 8) | (bis.read() & 0xFF);
}
return length;
}
static BigInteger readASN1Integer(ByteArrayInputStream bis) throws Exception {
int tag = bis.read();
if (tag != 0x02) throw new Exception("期望INTEGER tag: 0x02, 实际: 0x" + Integer.toHexString(tag));
int len = readASN1Length(bis);
byte[] data = new byte[len];
bis.read(data);
return new BigInteger(1, data);
}
}
@@ -0,0 +1,407 @@
-- Novalon管理系统数据库初始化脚本
-- 版本: V1
-- 描述: 创建所有核心表结构(合并版)
-- ============================================
-- 用户与角色相关表
-- ============================================
-- 用户表
CREATE TABLE IF NOT EXISTS sys_user (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
email VARCHAR(100),
phone VARCHAR(20),
nickname VARCHAR(100),
status INTEGER DEFAULT 1,
role_id BIGINT,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 角色表
CREATE TABLE IF NOT EXISTS sys_role (
id BIGSERIAL PRIMARY KEY,
role_name VARCHAR(100) NOT NULL,
role_key VARCHAR(100) NOT NULL UNIQUE,
role_sort INTEGER DEFAULT 0,
status INTEGER DEFAULT 1,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 用户角色关联表(支持多对多关系)
CREATE TABLE IF NOT EXISTS user_role (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(50),
CONSTRAINT fk_user_role_user FOREIGN KEY (user_id) REFERENCES sys_user(id) ON DELETE CASCADE,
CONSTRAINT fk_user_role_role FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
CONSTRAINT uk_user_role UNIQUE (user_id, role_id)
);
-- ============================================
-- 权限相关表
-- ============================================
-- 权限表
CREATE TABLE IF NOT EXISTS sys_permission (
id BIGSERIAL PRIMARY KEY,
permission_name VARCHAR(100) NOT NULL,
permission_code VARCHAR(100) NOT NULL UNIQUE,
resource VARCHAR(200) NOT NULL,
action VARCHAR(50) NOT NULL,
description VARCHAR(500),
status INTEGER DEFAULT 1,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 角色权限关联表
CREATE TABLE IF NOT EXISTS sys_role_permission (
id BIGSERIAL PRIMARY KEY,
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES sys_permission(id) ON DELETE CASCADE,
UNIQUE (role_id, permission_id)
);
-- ============================================
-- 菜单相关表
-- ============================================
-- 菜单表
CREATE TABLE IF NOT EXISTS sys_menu (
id BIGSERIAL PRIMARY KEY,
menu_name VARCHAR(50) NOT NULL,
parent_id BIGINT DEFAULT 0,
order_num INTEGER DEFAULT 0,
menu_type VARCHAR(1) DEFAULT 'C',
perms VARCHAR(100),
component VARCHAR(200),
status INTEGER DEFAULT 1,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- ============================================
-- 字典相关表
-- ============================================
-- 字典类型表
CREATE TABLE IF NOT EXISTS sys_dict_type (
id BIGSERIAL PRIMARY KEY,
dict_name VARCHAR(100) NOT NULL,
dict_type VARCHAR(100) NOT NULL UNIQUE,
status VARCHAR(1) DEFAULT '0',
remark VARCHAR(500),
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 字典数据表
CREATE TABLE IF NOT EXISTS sys_dict_data (
id BIGSERIAL PRIMARY KEY,
dict_sort INTEGER DEFAULT 0,
dict_label VARCHAR(100) NOT NULL,
dict_value VARCHAR(100) NOT NULL,
dict_type VARCHAR(100) NOT NULL,
css_class VARCHAR(100),
list_class VARCHAR(100),
is_default VARCHAR(1) DEFAULT 'N',
status VARCHAR(1) DEFAULT '0',
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 字典表(通用字典)
CREATE TABLE IF NOT EXISTS sys_dictionary (
id BIGSERIAL PRIMARY KEY,
type VARCHAR(100) NOT NULL,
code VARCHAR(100) NOT NULL,
name VARCHAR(100) NOT NULL,
value VARCHAR(500),
remark VARCHAR(500),
sort INTEGER DEFAULT 0,
create_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- ============================================
-- 系统配置表
-- ============================================
-- 系统配置表
CREATE TABLE IF NOT EXISTS sys_config (
id BIGSERIAL PRIMARY KEY,
config_name VARCHAR(100) NOT NULL,
config_key VARCHAR(100) NOT NULL UNIQUE,
config_value VARCHAR(500) NOT NULL,
config_type VARCHAR(1) DEFAULT 'N',
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- ============================================
-- 日志相关表
-- ============================================
-- 登录日志表
CREATE TABLE IF NOT EXISTS sys_login_log (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50),
ip VARCHAR(50),
location VARCHAR(255),
browser VARCHAR(50),
os VARCHAR(50),
status VARCHAR(1),
message VARCHAR(255),
login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 异常日志表
CREATE TABLE IF NOT EXISTS sys_exception_log (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50),
title VARCHAR(100),
exception_name VARCHAR(100),
method_name VARCHAR(255),
method_params TEXT,
exception_msg TEXT,
exception_stack TEXT,
ip VARCHAR(50),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 操作日志表
CREATE TABLE IF NOT EXISTS operation_log (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50),
operation VARCHAR(100),
method VARCHAR(200),
params TEXT,
result TEXT,
ip VARCHAR(50),
duration BIGINT,
status VARCHAR(1) DEFAULT '0',
error_msg TEXT,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 审计日志表
CREATE TABLE IF NOT EXISTS audit_log (
id BIGSERIAL PRIMARY KEY,
entity_type VARCHAR(100) NOT NULL,
entity_id BIGINT,
operation_type VARCHAR(20) NOT NULL,
operator VARCHAR(100),
operation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
before_data JSONB,
after_data JSONB,
changed_fields TEXT[],
ip_address VARCHAR(50),
user_agent TEXT,
description TEXT,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 审计日志归档表
CREATE TABLE IF NOT EXISTS audit_log_archive (
id BIGSERIAL PRIMARY KEY,
entity_type VARCHAR(100) NOT NULL,
entity_id BIGINT,
operation_type VARCHAR(20) NOT NULL,
operator VARCHAR(100),
operation_time TIMESTAMP,
before_data JSONB,
after_data JSONB,
changed_fields TEXT[],
ip_address VARCHAR(50),
user_agent TEXT,
description TEXT,
created_at TIMESTAMP,
archived_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ============================================
-- 通知与消息表
-- ============================================
-- 系统公告表
CREATE TABLE IF NOT EXISTS sys_notice (
id BIGSERIAL PRIMARY KEY,
notice_title VARCHAR(50) NOT NULL,
notice_type VARCHAR(1) NOT NULL,
notice_content TEXT,
status VARCHAR(1) DEFAULT '0',
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 用户消息表
CREATE TABLE IF NOT EXISTS sys_user_message (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
notice_id BIGINT,
message_title VARCHAR(255),
message_content TEXT,
is_read VARCHAR(1) DEFAULT '0',
read_time TIMESTAMP,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- ============================================
-- 文件管理表
-- ============================================
-- 文件管理表
CREATE TABLE IF NOT EXISTS sys_file (
id BIGSERIAL PRIMARY KEY,
file_name VARCHAR(255) NOT NULL,
file_path VARCHAR(500) NOT NULL,
file_size BIGINT,
file_type VARCHAR(100),
file_extension VARCHAR(10),
storage_type VARCHAR(50),
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- ============================================
-- OAuth2相关表
-- ============================================
-- OAuth2客户端表
CREATE TABLE IF NOT EXISTS oauth2_client (
id BIGSERIAL PRIMARY KEY,
client_id VARCHAR(100) NOT NULL UNIQUE,
client_secret VARCHAR(255) NOT NULL,
client_name VARCHAR(100),
web_server_redirect_uri VARCHAR(500),
scope VARCHAR(500),
authorized_grant_types VARCHAR(500),
access_token_validity_seconds INTEGER,
refresh_token_validity_seconds INTEGER,
auto_approve VARCHAR(1) DEFAULT 'false',
enabled VARCHAR(1) DEFAULT 'true',
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- ============================================
-- 表注释
-- ============================================
COMMENT ON TABLE sys_user IS '系统用户表';
COMMENT ON TABLE sys_role IS '系统角色表';
COMMENT ON TABLE user_role IS '用户角色关联表';
COMMENT ON TABLE sys_permission IS '系统权限表';
COMMENT ON TABLE sys_role_permission IS '角色权限关联表';
COMMENT ON TABLE sys_menu IS '系统菜单表';
COMMENT ON TABLE sys_dict_type IS '字典类型表';
COMMENT ON TABLE sys_dict_data IS '字典数据表';
COMMENT ON TABLE sys_dictionary IS '通用字典表';
COMMENT ON TABLE sys_config IS '系统配置表';
COMMENT ON TABLE sys_login_log IS '登录日志表';
COMMENT ON TABLE sys_exception_log IS '异常日志表';
COMMENT ON TABLE operation_log IS '操作日志表';
COMMENT ON TABLE audit_log IS '审计日志表';
COMMENT ON TABLE audit_log_archive IS '审计日志归档表';
COMMENT ON TABLE sys_notice IS '系统公告表';
COMMENT ON TABLE sys_user_message IS '用户消息表';
COMMENT ON TABLE sys_file IS '文件管理表';
COMMENT ON TABLE oauth2_client IS 'OAuth2客户端表';
COMMENT ON TABLE sys_exception_log IS '异常日志表';
COMMENT ON COLUMN sys_exception_log.id IS '主键ID';
COMMENT ON COLUMN sys_exception_log.username IS '操作用户';
COMMENT ON COLUMN sys_exception_log.title IS '异常标题';
COMMENT ON COLUMN sys_exception_log.exception_name IS '异常名称';
COMMENT ON COLUMN sys_exception_log.method_name IS '方法名称';
COMMENT ON COLUMN sys_exception_log.method_params IS '方法参数';
COMMENT ON COLUMN sys_exception_log.exception_msg IS '异常消息';
COMMENT ON COLUMN sys_exception_log.exception_stack IS '异常堆栈';
COMMENT ON COLUMN sys_exception_log.ip IS 'IP地址';
COMMENT ON COLUMN sys_exception_log.create_time IS '创建时间';
COMMENT ON TABLE audit_log IS '审计日志表';
COMMENT ON COLUMN audit_log.id IS '主键ID';
COMMENT ON COLUMN audit_log.entity_type IS '实体类型(如User, Role等)';
COMMENT ON COLUMN audit_log.entity_id IS '实体ID';
COMMENT ON COLUMN audit_log.operation_type IS '操作类型(CREATE, UPDATE, DELETE';
COMMENT ON COLUMN audit_log.operator IS '操作人';
COMMENT ON COLUMN audit_log.operation_time IS '操作时间';
COMMENT ON COLUMN audit_log.before_data IS '变更前数据(JSON格式)';
COMMENT ON COLUMN audit_log.after_data IS '变更后数据(JSON格式)';
COMMENT ON COLUMN audit_log.changed_fields IS '变更字段列表';
COMMENT ON COLUMN audit_log.ip_address IS 'IP地址';
COMMENT ON COLUMN audit_log.description IS '操作描述';
COMMENT ON COLUMN audit_log.created_at IS '记录创建时间';
COMMENT ON TABLE audit_log_archive IS '审计日志归档表';
COMMENT ON COLUMN audit_log_archive.id IS '主键ID';
COMMENT ON COLUMN audit_log_archive.entity_type IS '实体类型(如User, Role等)';
COMMENT ON COLUMN audit_log_archive.entity_id IS '实体ID';
COMMENT ON COLUMN audit_log_archive.operation_type IS '操作类型(CREATE, UPDATE, DELETE';
COMMENT ON COLUMN audit_log_archive.operator IS '操作人';
COMMENT ON COLUMN audit_log_archive.operation_time IS '操作时间';
COMMENT ON COLUMN audit_log_archive.before_data IS '变更前数据(JSON格式)';
COMMENT ON COLUMN audit_log_archive.after_data IS '变更后数据(JSON格式)';
COMMENT ON COLUMN audit_log_archive.changed_fields IS '变更字段列表';
COMMENT ON COLUMN audit_log_archive.ip_address IS 'IP地址';
COMMENT ON COLUMN audit_log_archive.user_agent IS '用户代理';
COMMENT ON COLUMN audit_log_archive.description IS '操作描述';
COMMENT ON COLUMN audit_log_archive.created_at IS '记录创建时间';
COMMENT ON COLUMN audit_log_archive.archived_at IS '归档时间';
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,530 +0,0 @@
# 团课推荐模块 API 文档
> **文档版本**: v1.0
> **创建日期**: 2026-06-15
> **作者**: 张翔
> **状态**: 正式发布
---
## 📋 目录
1. [概述](#概述)
2. [基础路径](#基础路径)
3. [团课推荐管理接口](#团课推荐管理接口)
- [获取所有团课推荐](#获取所有团课推荐)
- [获取所有启用的团课推荐](#获取所有启用的团课推荐)
- [根据ID获取团课推荐](#根据ID获取团课推荐)
- [根据团课ID获取推荐](#根据团课ID获取推荐)
- [创建团课推荐](#创建团课推荐)
- [更新团课推荐](#更新团课推荐)
- [删除团课推荐](#删除团课推荐)
- [启用团课推荐](#启用团课推荐)
- [禁用团课推荐](#禁用团课推荐)
4. [数据模型](#数据模型)
- [GroupCourseRecommend(团课推荐)](#GroupCourseRecommend团课推荐)
5. [状态码说明](#状态码说明)
6. [业务规则](#业务规则)
---
## 概述
团课推荐模块提供团课推荐信息的创建、编辑、查询、删除和状态管理功能。推荐信息包含团课ID、推荐标题、推荐内容、推荐理由、优先级等必要信息,支持按优先级排序展示。
## 基础路径
所有接口的基础路径为: `http://{host}:{port}/api/groupCourse/recommend`
---
## 团课推荐管理接口
### 获取所有团课推荐
| 属性 | 值 |
|------|-----|
| **HTTP方法** | GET |
| **接口路径** | `/api/groupCourse/recommend/list` |
| **所属文件** | `GroupCourseRecommendHandler.java` |
**请求参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| sortBy | string | 否 | priority | 排序字段(支持:priority、createdAt、updatedAt |
| sortOrder | string | 否 | desc | 排序方式(asc-升序,desc-降序) |
**成功响应** (200 OK):
```json
[
{
"id": 1,
"courseId": 1,
"recommendTitle": "本周热门课程",
"recommendContent": "这是一门非常棒的课程,快来参加吧!",
"recommendReason": "教练专业,课程内容丰富",
"priority": 10,
"isActive": true,
"groupCourse": {
"id": 1,
"courseName": "瑜伽入门",
"coachId": 1,
"courseType": 1,
"startTime": "2026-06-15T09:00:00",
"endTime": "2026-06-15T10:00:00",
"maxMembers": 20,
"currentMembers": 15,
"status": 0,
"location": "健身房A区",
"coverImage": "https://example.com/yoga.jpg",
"description": "适合初学者的瑜伽课程"
},
"createdAt": "2026-06-15T10:00:00",
"updatedAt": "2026-06-15T10:00:00"
}
]
```
---
### 获取所有启用的团课推荐
| 属性 | 值 |
|------|-----|
| **HTTP方法** | GET |
| **接口路径** | `/api/groupCourse/recommend/active` |
| **所属文件** | `GroupCourseRecommendHandler.java` |
**功能说明**: 获取系统中所有已启用的团课推荐列表,按优先级从高到低排序。
**成功响应** (200 OK):
```json
[
{
"id": 2,
"courseId": 3,
"recommendTitle": "新学员推荐",
"recommendContent": "专为新学员设计的入门课程,轻松上手",
"recommendReason": "零基础友好,教练耐心指导",
"priority": 20,
"isActive": true,
"groupCourse": {
"id": 3,
"courseName": "基础有氧",
"coachId": 2,
"courseType": 2,
"startTime": "2026-06-16T18:00:00",
"endTime": "2026-06-16T19:00:00",
"maxMembers": 30,
"currentMembers": 8,
"status": 0,
"location": "健身房B区",
"coverImage": "https://example.com/aerobic.jpg",
"description": "适合所有健身水平的有氧课程"
},
"createdAt": "2026-06-15T11:00:00",
"updatedAt": "2026-06-15T11:00:00"
}
]
```
---
### 根据ID获取团课推荐
| 属性 | 值 |
|------|-----|
| **HTTP方法** | GET |
| **接口路径** | `/api/groupCourse/recommend/{id}` |
| **所属文件** | `GroupCourseRecommendHandler.java` |
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | Long | 是 | 团课推荐ID |
**成功响应** (200 OK):
```json
{
"id": 1,
"courseId": 1,
"recommendTitle": "本周热门课程",
"recommendContent": "这是一门非常棒的课程,快来参加吧!",
"recommendReason": "教练专业,课程内容丰富",
"priority": 10,
"isActive": true,
"groupCourse": {
"id": 1,
"courseName": "瑜伽入门",
"coachId": 1,
"courseType": 1,
"startTime": "2026-06-15T09:00:00",
"endTime": "2026-06-15T10:00:00",
"maxMembers": 20,
"currentMembers": 15,
"status": 0,
"location": "健身房A区",
"coverImage": "https://example.com/yoga.jpg",
"description": "适合初学者的瑜伽课程"
},
"createdAt": "2026-06-15T10:00:00",
"updatedAt": "2026-06-15T10:00:00"
}
```
**失败响应** (404 Not Found):
```json
{}
```
---
### 根据团课ID获取推荐
| 属性 | 值 |
|------|-----|
| **HTTP方法** | GET |
| **接口路径** | `/api/groupCourse/recommend/course/{courseId}` |
| **所属文件** | `GroupCourseRecommendHandler.java` |
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| courseId | Long | 是 | 团课ID |
**成功响应** (200 OK):
```json
[
{
"id": 1,
"courseId": 1,
"recommendTitle": "本周热门课程",
"recommendContent": "这是一门非常棒的课程,快来参加吧!",
"recommendReason": "教练专业,课程内容丰富",
"priority": 10,
"isActive": true,
"groupCourse": {
"id": 1,
"courseName": "瑜伽入门",
"coachId": 1,
"courseType": 1,
"startTime": "2026-06-15T09:00:00",
"endTime": "2026-06-15T10:00:00",
"maxMembers": 20,
"currentMembers": 15,
"status": 0,
"location": "健身房A区"
},
"createdAt": "2026-06-15T10:00:00",
"updatedAt": "2026-06-15T10:00:00"
}
]
```
---
### 创建团课推荐
| 属性 | 值 |
|------|-----|
| **HTTP方法** | POST |
| **接口路径** | `/api/groupCourse/recommend` |
| **所属文件** | `GroupCourseRecommendHandler.java` |
**请求体**:
```json
{
"courseId": 1,
"recommendTitle": "本周热门课程",
"recommendContent": "这是一门非常棒的课程,快来参加吧!",
"recommendReason": "教练专业,课程内容丰富",
"priority": 10,
"isActive": true
}
```
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| courseId | Long | **是** | - | 团课ID(必须是有效的团课) |
| recommendTitle | String | 否 | - | 推荐标题 |
| recommendContent | String | 否 | - | 推荐内容 |
| recommendReason | String | 否 | - | 推荐理由 |
| priority | Integer | 否 | 0 | 优先级(数字越大优先级越高) |
| isActive | Boolean | 否 | true | 是否启用 |
**成功响应** (200 OK):
```json
{
"success": true,
"message": "团课推荐创建成功",
"data": {
"id": 1,
"courseId": 1,
"recommendTitle": "本周热门课程",
"recommendContent": "这是一门非常棒的课程,快来参加吧!",
"recommendReason": "教练专业,课程内容丰富",
"priority": 10,
"isActive": true,
"createdAt": "2026-06-15T10:00:00",
"updatedAt": "2026-06-15T10:00:00"
}
}
```
**失败响应** (400 Bad Request):
```json
{
"success": false,
"message": "团课ID不能为空"
}
```
---
### 更新团课推荐
| 属性 | 值 |
|------|-----|
| **HTTP方法** | PUT |
| **接口路径** | `/api/groupCourse/recommend/{id}` |
| **所属文件** | `GroupCourseRecommendHandler.java` |
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | Long | 是 | 团课推荐ID |
**请求体**:
```json
{
"recommendTitle": "本周热门课程(更新)",
"recommendContent": "更新后的推荐内容",
"recommendReason": "更新后的推荐理由",
"priority": 15,
"isActive": true
}
```
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| courseId | Long | 否 | 团课ID |
| recommendTitle | String | 否 | 推荐标题 |
| recommendContent | String | 否 | 推荐内容 |
| recommendReason | String | 否 | 推荐理由 |
| priority | Integer | 否 | 优先级 |
| isActive | Boolean | 否 | 是否启用 |
**成功响应** (200 OK):
```json
{
"success": true,
"message": "团课推荐更新成功",
"data": {
"id": 1,
"courseId": 1,
"recommendTitle": "本周热门课程(更新)",
"recommendContent": "更新后的推荐内容",
"recommendReason": "更新后的推荐理由",
"priority": 15,
"isActive": true,
"updatedAt": "2026-06-15T12:00:00"
}
}
```
**失败响应** (400 Bad Request):
```json
{
"success": false,
"message": "团课推荐不存在"
}
```
---
### 删除团课推荐
| 属性 | 值 |
|------|-----|
| **HTTP方法** | DELETE |
| **接口路径** | `/api/groupCourse/recommend/{id}` |
| **所属文件** | `GroupCourseRecommendHandler.java` |
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | Long | 是 | 团课推荐ID |
**成功响应** (200 OK):
```json
{
"success": true,
"message": "团课推荐删除成功"
}
```
**失败响应** (400 Bad Request):
```json
{
"success": false,
"message": "团课推荐不存在"
}
```
---
### 启用团课推荐
| 属性 | 值 |
|------|-----|
| **HTTP方法** | POST |
| **接口路径** | `/api/groupCourse/recommend/{id}/enable` |
| **所属文件** | `GroupCourseRecommendHandler.java` |
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | Long | 是 | 团课推荐ID |
**成功响应** (200 OK):
```json
{
"success": true,
"message": "团课推荐启用成功",
"data": {
"id": 1,
"courseId": 1,
"recommendTitle": "本周热门课程",
"isActive": true
}
}
```
**失败响应** (400 Bad Request):
```json
{
"success": false,
"message": "团课推荐不存在"
}
```
---
### 禁用团课推荐
| 属性 | 值 |
|------|-----|
| **HTTP方法** | POST |
| **接口路径** | `/api/groupCourse/recommend/{id}/disable` |
| **所属文件** | `GroupCourseRecommendHandler.java` |
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | Long | 是 | 团课推荐ID |
**成功响应** (200 OK):
```json
{
"success": true,
"message": "团课推荐禁用成功",
"data": {
"id": 1,
"courseId": 1,
"recommendTitle": "本周热门课程",
"isActive": false
}
}
```
**失败响应** (400 Bad Request):
```json
{
"success": false,
"message": "团课推荐不存在"
}
```
---
## 数据模型
### GroupCourseRecommend(团课推荐)
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | Long | 主键ID |
| courseId | Long | 团课ID(关联group_course.id |
| recommendTitle | String | 推荐标题 |
| recommendContent | String | 推荐内容 |
| recommendReason | String | 推荐理由 |
| priority | Integer | 优先级(数字越大优先级越高),默认0 |
| isActive | Boolean | 是否启用,默认true |
| groupCourse | GroupCourse | 关联的团课信息(查询时自动填充) |
| createdBy | String | 创建人 |
| updatedBy | String | 更新人 |
| createdAt | LocalDateTime | 创建时间 |
| updatedAt | LocalDateTime | 更新时间 |
| deletedAt | LocalDateTime | 删除时间(软删除) |
---
## 状态码说明
### 推荐状态
| 状态值 | 含义 |
|--------|------|
| true | 启用 |
| false | 禁用 |
---
## 业务规则
### 团课推荐管理
1. **创建推荐**:团课ID为必填项,且必须是有效的团课
2. **优先级排序**:获取启用的推荐列表时,按优先级从高到低排序
3. **删除推荐**:采用软删除机制,数据保留可恢复
4. **状态管理**:支持启用/禁用推荐状态,禁用的推荐不会在推荐列表中显示
---
## 附录:错误响应格式
所有接口的错误响应统一格式:
```json
{
"success": false,
"message": "错误描述信息"
}
```
---
*文档结束*
-87
View File
@@ -1,87 +0,0 @@
一、基础有氧与热身(难度 1-3
主要是低冲击、低技巧,用于建立运动基础。
慢走/椭圆机轻松模式:1(几乎无难度,适合所有人)
固定自行车(低阻力):2(注意座椅高度调节即可)
跑步机慢跑:3(需要基本协调性,膝盖有压力)
跳绳(连续基础跳):3(需要手脚配合,心肺要求明显)
二、固定器械训练(难度 2-5
轨迹固定,主要考验力量和耐力,技巧要求低。
坐姿腿屈伸/腿弯举:2(很容易找到发力感)
坐姿推胸机:3(需注意肩胛后收,避免耸肩)
高位下拉(坐姿):3(需控制不要过度后仰)
史密斯机深蹲:4(轨迹固定,但需保持核心稳定)
蝴蝶机夹胸:3(易用肘关节代偿,需锁定肩关节)
三、自重基础动作(难度 3-7
需要一定的力量-体重比和身体控制能力。
平板支撑:3(耐力考验,技巧低)
跪姿俯卧撑:3(上肢力量较弱者首选)
标准俯卧撑:5(需核心收紧,身体成直线)
引体向上(弹力带辅助):6(背部和手臂力量要求高)
标准引体向上:8(力量-体重比极高,多数男性无法完成1次)
徒手深蹲:3(注意膝盖方向与背部直立)
单腿深蹲(手枪蹲):8(需要极高下肢力量、柔韧性和平衡)
四、自由重量杠铃/哑铃(难度 5-9)
技巧风险最高,需要神经系统协调和长期动作打磨。
哑铃二头弯举:4(容易晃动借力,但较安全)
哑铃侧平举:5(极易用斜方肌代偿,真正练到三角肌中束很难)
杠铃卧推:7(肩关节压力大,起桥、沉肩、稳定手腕均有技巧,有压伤风险)
杠铃深蹲(颈后):8(全身协调性、核心抗压、杠位放置、呼吸模式,学习曲线陡峭)
传统硬拉:9(风险极高,需要精确的脊柱中立、髋铰链、背阔肌收紧,错误时伤腰)
高翻/抓举(奥运举重):10(需要爆发力、柔韧、精准衔接,非数月训练不能掌握)
五、高强度与爆发力(难度 6-10
对心肺、神经系统和恢复能力要求极高。
波比跳(标准版):6(连续做时心肺压力极大)
冲刺跑(短跑):7(对腘绳肌和脚踝爆发力要求高)
跳箱(合理高度):6(需要落地缓冲技巧)
负重雪橇推:6(主要考验腿部耐力和意志力)
双力臂(引体向上后翻腕上杠):9(需要爆发引体 + 极高相对力量)
六、柔韧与平衡类(难度 3-8
考验本体感觉和关节活动度。
静态拉伸(坐姿体前屈):2(无风险,但需要坚持)
瑜伽下犬式:3(常见,但需背部与手臂对齐)
单腿罗马尼亚硬拉(徒手):6(极考验平衡和髋稳定)
全深蹲(脚跟贴地,亚洲蹲):5(踝关节灵活度限制多数人)
竖叉/横叉:8(需要数月甚至数年拉伸)
-123
View File
@@ -1,123 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>gym-manage-api</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>gym-auth</artifactId>
<packaging>jar</packaging>
<name>Gym Auth</name>
<description>Phone Authentication Module - Phone Number Login Services</description>
<dependencies>
<dependency>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>manage-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>manage-db</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>manage-sys</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>gym-member</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
<!-- 阿里云一键登录服务 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dypnsapi</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.6.0</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<executions>
<execution>
<id>default-jar</id>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>21</source>
<target>21</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -1,7 +0,0 @@
package cn.novalon.gym.manage.auth.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AuthConfig {
}
@@ -1,47 +0,0 @@
package cn.novalon.gym.manage.auth.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 阿里云短信配置属性类
*
* @author auto-generated
* @date 2026-06-20
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "alibaba.cloud.sms")
public class SmsProperties {
/**
* 访问密钥ID
*/
private String accessKeyId;
/**
* 访问密钥密钥
*/
private String accessKeySecret;
/**
* 短信签名名称
*/
private String signName;
/**
* 短信模板CODE
*/
private String templateCode;
/**
* 短信验证码长度(默认6位)
*/
private int codeLength = 6;
/**
* 短信验证码有效期(秒,默认300秒=5分钟)
*/
private long codeExpireSeconds = 300;
}
@@ -1,26 +0,0 @@
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;
import lombok.NoArgsConstructor;
/**
* 手机号验证码登录请求DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PhoneCodeLoginDto {
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
@NotBlank(message = "验证码不能为空")
@Pattern(regexp = "^\\d{4,6}$", message = "验证码格式不正确")
private String code;
}
@@ -1,27 +0,0 @@
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;
import lombok.NoArgsConstructor;
/**
* 手机号一键登录请求DTO
* uniapp官方一键登录流程:前端通过云函数获取手机号,直接传给后端
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PhoneLoginDto {
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
private String nickname;
private String avatar;
}
@@ -1,22 +0,0 @@
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;
import lombok.NoArgsConstructor;
/**
* 发送短信验证码请求DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SendCodeRequest {
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
}
@@ -1,25 +0,0 @@
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;
import lombok.NoArgsConstructor;
/**
* 短信验证码登录请求DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SmsLoginDto {
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
@NotBlank(message = "验证码不能为空")
private String code;
}
@@ -1,58 +0,0 @@
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.service.PhoneAuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
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 java.util.Map;
@Slf4j
@Component
@RequiredArgsConstructor
@Tag(name = "手机号认证", description = "手机号一键登录与验证码登录")
public class PhoneAuthHandler {
private final PhoneAuthService phoneAuthService;
@Operation(summary = "手机号一键登录", description = "使用uniapp官方运营商认证,直接手机号登录或注册")
public Mono<ServerResponse> oneClickLogin(ServerRequest request) {
log.info("收到手机号一键登录请求");
return request.bodyToMono(PhoneLoginDto.class)
.flatMap(phoneAuthService::oneClickLogin)
.flatMap(response -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(response));
}
@Operation(summary = "发送短信验证码", description = "使用阿里云发送短信验证码")
public Mono<ServerResponse> sendSmsCode(ServerRequest request) {
log.info("收到发送短信验证码请求");
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 ? "验证码发送成功" : "验证码发送失败")));
}
@Operation(summary = "手机号验证码登录", description = "使用阿里云短信验证码登录或注册")
public Mono<ServerResponse> codeLogin(ServerRequest request) {
log.info("收到手机号验证码登录请求");
return request.bodyToMono(PhoneCodeLoginDto.class)
.flatMap(phoneAuthService::codeLogin)
.flatMap(response -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(response));
}
}
@@ -1,37 +0,0 @@
package cn.novalon.gym.manage.auth.service;
import cn.novalon.gym.manage.auth.dto.PhoneLoginDto;
import cn.novalon.gym.manage.auth.dto.PhoneCodeLoginDto;
import cn.novalon.gym.manage.auth.vo.PhoneLoginVO;
import reactor.core.publisher.Mono;
/**
* 手机号认证服务接口
*/
public interface PhoneAuthService {
/**
* 手机号一键登录(uniapp官方运营商认证)
* 已注册则直接登录,未注册则自动注册后登录
*
* @param request 登录请求
* @return 登录响应
*/
Mono<PhoneLoginVO> oneClickLogin(PhoneLoginDto request);
/**
* 发送短信验证码(阿里云)
*
* @param phone 手机号
* @return 是否发送成功
*/
Mono<Boolean> sendSmsCode(String phone);
/**
* 手机号验证码登录(阿里云)
*
* @param request 登录请求
* @return 登录响应
*/
Mono<PhoneLoginVO> codeLogin(PhoneCodeLoginDto request);
}
@@ -1,35 +0,0 @@
package cn.novalon.gym.manage.auth.service;
/**
* 短信服务接口
*
* @author auto-generated
* @date 2026-06-20
*/
public interface SmsService {
/**
* 发送短信验证码
*
* @param phone 手机号
* @return 发送结果
*/
boolean sendVerificationCode(String phone);
/**
* 验证短信验证码
*
* @param phone 手机号
* @param code 验证码
* @return 验证结果
*/
boolean verifyCode(String phone, String code);
/**
* 获取验证码(用于测试或特殊场景)
*
* @param phone 手机号
* @return 验证码
*/
String getVerificationCode(String phone);
}
@@ -1,177 +0,0 @@
package cn.novalon.gym.manage.auth.service.impl;
import cn.novalon.gym.manage.auth.dto.PhoneCodeLoginDto;
import cn.novalon.gym.manage.auth.dto.PhoneLoginDto;
import cn.novalon.gym.manage.auth.service.PhoneAuthService;
import cn.novalon.gym.manage.auth.service.SmsService;
import cn.novalon.gym.manage.auth.vo.PhoneLoginVO;
import cn.novalon.gym.manage.common.exception.ErrorCode;
import cn.novalon.gym.manage.common.exception.SystemException;
import cn.novalon.gym.manage.member.entity.Member;
import cn.novalon.gym.manage.member.es.entity.MemberES;
import cn.novalon.gym.manage.member.es.repository.MemberESRepository;
import cn.novalon.gym.manage.member.repository.IMemberRepository;
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 jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class PhoneAuthServiceImpl implements PhoneAuthService {
private final IMemberRepository memberRepository;
private final MemberESRepository memberESRepository;
private final EsSyncUtils esSyncUtils;
private final JwtTokenProvider jwtTokenProvider;
private final SmsService smsService;
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer;
@PostConstruct
public void init() {
this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository);
}
@Override
public Mono<PhoneLoginVO> oneClickLogin(PhoneLoginDto request) {
log.info("手机号一键登录, phone: {}", request.getPhone());
String encryptedPhone = encryptPhone(request.getPhone());
return memberRepository.findByPhone(encryptedPhone)
.flatMap(existingMember -> {
log.info("手机号已注册,直接登录, memberId: {}", existingMember.getId());
return doLogin(existingMember, false, request);
})
.switchIfEmpty(Mono.defer(() -> {
log.info("手机号未注册,创建新会员");
return createNewMemberAndLogin(request, encryptedPhone);
}));
}
@Override
public Mono<Boolean> sendSmsCode(String phone) {
log.info("发送短信验证码, phone: {}", phone);
return Mono.fromCallable(() -> smsService.sendVerificationCode(phone));
}
@Override
public Mono<PhoneLoginVO> codeLogin(PhoneCodeLoginDto request) {
log.info("手机号验证码登录, phone: {}", request.getPhone());
return Mono.fromCallable(() -> smsService.verifyCode(request.getPhone(), request.getCode()))
.flatMap(verified -> {
if (!verified) {
log.warn("验证码验证失败, phone: {}", request.getPhone());
return Mono.error(new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "验证码错误或已过期"));
}
String encryptedPhone = encryptPhone(request.getPhone());
return memberRepository.findByPhone(encryptedPhone)
.flatMap(existingMember -> {
log.info("手机号已注册,直接登录, memberId: {}", existingMember.getId());
return doLogin(existingMember, false, null);
})
.switchIfEmpty(Mono.defer(() -> {
log.info("手机号未注册,创建新会员");
PhoneLoginDto registerRequest = new PhoneLoginDto();
registerRequest.setPhone(request.getPhone());
return createNewMemberAndLogin(registerRequest, encryptedPhone);
}));
});
}
private Mono<PhoneLoginVO> createNewMemberAndLogin(PhoneLoginDto request, String encryptedPhone) {
String memberNo = MemberNoGenerator.generate();
log.info("生成会员号: {}", memberNo);
Member member = new Member();
member.setMemberNo(memberNo);
member.setPhone(encryptedPhone);
member.setNickname(request != null ? request.getNickname() : null);
member.setAvatar(request != null ? request.getAvatar() : null);
member.setLastLoginAt(LocalDateTime.now());
member.setIsDeleted(false);
return memberRepository.save(member)
.doOnSuccess(memberSyncer::sync)
.flatMap(savedMember -> {
log.info("新会员创建成功, memberId: {}, memberNo: {}", savedMember.getId(), savedMember.getMemberNo());
return doLogin(savedMember, true, request);
});
}
private Mono<PhoneLoginVO> doLogin(Member member, boolean isNewUser, PhoneLoginDto request) {
log.info("登录成功, memberId: {}", member.getId());
member.setLastLoginAt(LocalDateTime.now());
if (isNewUser && request != null) {
if ((member.getNickname() == null || member.getNickname().isEmpty())
&& request.getNickname() != null && !request.getNickname().isEmpty()) {
member.setNickname(request.getNickname());
}
if ((member.getAvatar() == null || member.getAvatar().isEmpty())
&& request.getAvatar() != null && !request.getAvatar().isEmpty()) {
member.setAvatar(request.getAvatar());
}
}
return memberRepository.save(member)
.doOnSuccess(memberSyncer::sync)
.map(this::buildLoginResponse);
}
private PhoneLoginVO buildLoginResponse(Member member) {
boolean needCompleteInfo = member.getNickname() == null || member.getNickname().isEmpty();
List<String> roles = new ArrayList<>();
String accessToken = jwtTokenProvider.generateToken(String.valueOf(member.getId()), member.getId(), roles);
log.info("JWT Token 生成成功, memberId: {}", member.getId());
PhoneLoginVO vo = new PhoneLoginVO();
vo.setMemberId(member.getId());
vo.setMemberNo(member.getMemberNo());
vo.setAccessToken(accessToken);
vo.setRefreshToken(accessToken);
vo.setExpiresIn(86400);
vo.setIsNewUser(member.getCreatedAt() == null ? false :
member.getCreatedAt().isAfter(LocalDateTime.now().minusMinutes(1)));
vo.setNeedCompleteInfo(needCompleteInfo);
vo.setNickname(member.getNickname());
vo.setAvatar(member.getAvatar());
vo.setPhone(decryptPhone(member.getPhone()));
return vo;
}
private String encryptPhone(String phoneNumber) {
try {
return AesUtil.encrypt(phoneNumber);
} catch (Exception e) {
log.error("手机号加密失败", e);
throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "手机号加密失败");
}
}
private String decryptPhone(String encryptedPhone) {
try {
return AesUtil.decrypt(encryptedPhone);
} catch (Exception e) {
log.error("手机号解密失败", e);
return null;
}
}
}
@@ -1,148 +0,0 @@
package cn.novalon.gym.manage.auth.service.impl;
import cn.hutool.core.util.RandomUtil;
import cn.novalon.gym.manage.auth.config.SmsProperties;
import cn.novalon.gym.manage.auth.service.SmsService;
import cn.novalon.gym.manage.common.constant.RedisKeyConstants;
import cn.novalon.gym.manage.common.util.RedisUtil;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.aliyun.teaopenapi.models.Config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.time.Duration;
/**
* 短信服务实现类
*
* @author auto-generated
* @date 2026-06-20
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SmsServiceImpl implements SmsService {
private final SmsProperties smsProperties;
private final RedisUtil redisUtil;
@Override
public boolean sendVerificationCode(String phone) {
log.info("发送短信验证码, phone: {}", phone);
try {
// 生成验证码
String code = generateCode();
// 发送短信
boolean sent = sendSms(phone, code);
if (sent) {
// 将验证码存入Redis,5分钟过期
String cacheKey = RedisKeyConstants.SMS_CODE + phone;
redisUtil.setWithExpire(cacheKey, code, smsProperties.getCodeExpireSeconds())
.doOnSuccess(result -> log.info("验证码已缓存, phone: {}, code: {}, expire: {}s",
phone, code, smsProperties.getCodeExpireSeconds()))
.block();
log.info("短信验证码发送成功, phone: {}", phone);
return true;
}
log.warn("短信验证码发送失败, phone: {}", phone);
return false;
} catch (Exception e) {
log.error("发送短信验证码异常, phone: {}", phone, e);
return false;
}
}
@Override
public boolean verifyCode(String phone, String code) {
log.info("验证短信验证码, phone: {}", phone);
try {
String cacheKey = RedisKeyConstants.SMS_CODE + phone;
String cachedCode = redisUtil.get(cacheKey, String.class).block();
if (cachedCode == null) {
log.warn("验证码已过期或不存在, phone: {}", phone);
return false;
}
if (cachedCode.equals(code)) {
// 验证成功后删除验证码
redisUtil.delete(cacheKey).block();
log.info("验证码验证成功, phone: {}", phone);
return true;
}
log.warn("验证码不匹配, phone: {}, input: {}, cached: {}", phone, code, cachedCode);
return false;
} catch (Exception e) {
log.error("验证短信验证码异常, phone: {}", phone, e);
return false;
}
}
@Override
public String getVerificationCode(String phone) {
try {
String cacheKey = RedisKeyConstants.SMS_CODE + phone;
return redisUtil.get(cacheKey, String.class).block();
} catch (Exception e) {
log.error("获取验证码异常, phone: {}", phone, e);
return null;
}
}
/**
* 生成验证码
*/
private String generateCode() {
return RandomUtil.randomNumbers(smsProperties.getCodeLength());
}
/**
* 发送短信
*/
private boolean sendSms(String phone, String code) throws Exception {
log.info("调用阿里云短信API发送短信, phone: {}, code: {}", phone, code);
// 创建阿里云短信客户端
Config config = new Config()
.setAccessKeyId(smsProperties.getAccessKeyId())
.setAccessKeySecret(smsProperties.getAccessKeySecret())
.setEndpoint("dysmsapi.aliyuncs.com");
Client client = new Client(config);
// 构建发送短信请求
SendSmsRequest request = new SendSmsRequest()
.setPhoneNumbers(phone)
.setSignName(smsProperties.getSignName())
.setTemplateCode(smsProperties.getTemplateCode())
.setTemplateParam("{\"code\":\"" + code + "\"}");
// 发送短信
SendSmsResponse response = client.sendSms(request);
log.info("阿里云短信API响应, phone: {}, code: {}, message: {}",
phone, response.getBody().getCode(), response.getBody().getMessage());
// 判断发送结果
if ("OK".equals(response.getBody().getCode())) {
return true;
}
log.error("阿里云短信发送失败, phone: {}, code: {}, message: {}",
phone, response.getBody().getCode(), response.getBody().getMessage());
return false;
}
}
@@ -1,36 +0,0 @@
package cn.novalon.gym.manage.auth.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 手机号一键登录响应VO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PhoneLoginVO {
private Long memberId;
private String memberNo;
private String phone;
private String accessToken;
private String refreshToken;
private Integer expiresIn;
private Boolean isNewUser;
private Boolean needCompleteInfo;
private String nickname;
private String avatar;
}
@@ -1,5 +0,0 @@
cn.novalon.gym.manage.auth.handler.PhoneAuthHandler
cn.novalon.gym.manage.auth.service.impl.PhoneAuthServiceImpl
cn.novalon.gym.manage.auth.service.impl.SmsServiceImpl
cn.novalon.gym.manage.auth.config.AuthConfig
cn.novalon.gym.manage.auth.config.SmsProperties
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<FindBugsFilter>
<Match>
<Class name="~.*\.entity\..*" />
</Match>
<Match>
<Class name="~.*\.dto\..*" />
</Match>
<Match>
<Class name="~.*\.converter\..*" />
</Match>
</FindBugsFilter>
-33
View File
@@ -1,33 +0,0 @@
HELP.md
target/
.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
+38 -50
View File
@@ -12,109 +12,97 @@
<artifactId>gym-dataCount</artifactId> <artifactId>gym-dataCount</artifactId>
<version>1.0.0</version> <version>1.0.0</version>
<name>gym-dataCount</name> <name>gym-dataCount</name>
<description>Data Statistics Module for Gym Management</description> <description>Data Statistics Module</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties> <properties>
<java.version>21</java.version> <java.version>21</java.version>
</properties> </properties>
<dependencies> <dependencies>
<!-- Common模块 -->
<dependency> <dependency>
<groupId>cn.novalon.gym.manage</groupId> <groupId>cn.novalon.gym.manage</groupId>
<artifactId>manage-common</artifactId> <artifactId>manage-common</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<!-- 数据库模块 -->
<dependency> <dependency>
<groupId>cn.novalon.gym.manage</groupId> <groupId>cn.novalon.gym.manage</groupId>
<artifactId>manage-db</artifactId> <artifactId>manage-db</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<!-- WebFlux -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId> <artifactId>spring-boot-starter-webflux</artifactId>
</dependency> </dependency>
<!-- R2dbc -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId> <artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency> </dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Validation -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId> <artifactId>spring-boot-starter-validation</artifactId>
</dependency> </dependency>
<!-- Swagger -->
<dependency> <dependency>
<groupId>org.springdoc</groupId> <groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId> <artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
</dependency> </dependency>
<!-- POI for Excel export -->
<dependency> <dependency>
<groupId>org.apache.poi</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>poi-ooxml</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<version>5.2.5</version> <scope>test</scope>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations-jakarta</artifactId>
<version>2.2.43</version>
<scope>compile</scope>
</dependency> </dependency>
<!-- Gym Modules --> <!-- Redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 会员模块依赖 -->
<dependency> <dependency>
<groupId>cn.novalon.gym.manage</groupId> <groupId>cn.novalon.gym.manage</groupId>
<artifactId>gym-member</artifactId> <artifactId>gym-member</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<!-- 团课模块依赖 -->
<dependency> <dependency>
<groupId>cn.novalon.gym.manage</groupId> <groupId>cn.novalon.gym.manage</groupId>
<artifactId>gym-groupCourse</artifactId> <artifactId>gym-groupCourse</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<!-- 签到模块依赖 -->
<dependency> <dependency>
<groupId>cn.novalon.gym.manage</groupId> <groupId>cn.novalon.gym.manage</groupId>
<artifactId>gym-checkIn</artifactId> <artifactId>gym-checkIn</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>21</source>
<target>21</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
</project>
</project>
@@ -0,0 +1,29 @@
package cn.novalon.gym.manage.datacount;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
@SpringBootApplication(scanBasePackages = "cn.novalon.gym.manage", exclude = {
ReactiveUserDetailsServiceAutoConfiguration.class })
@EnableR2dbcRepositories(basePackages = {
"cn.novalon.gym.manage.db.dao",
"cn.novalon.gym.manage.member.repository",
"cn.novalon.gym.manage.groupcourse.dao",
"cn.novalon.gym.manage.datacount.dao"
})
@ComponentScan(basePackages = "cn.novalon.gym.manage")
public class GymDataCountApplication {
private static final Logger logger = LoggerFactory.getLogger(GymDataCountApplication.class);
public static void main(String[] args) {
logger.info("数据统计模块启动中...");
SpringApplication.run(GymDataCountApplication.class, args);
logger.info("数据统计模块启动完成");
}
}
@@ -1,15 +0,0 @@
package cn.novalon.gym.manage.datacount.config;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
/**
* 数据统计模块自动配置
*
* @author system
* @date 2026-06-09
*/
@AutoConfiguration
@ComponentScan(basePackages = "cn.novalon.gym.manage.datacount")
public class DataCountAutoConfiguration {
}
@@ -0,0 +1,40 @@
package cn.novalon.gym.manage.datacount.config;
import cn.novalon.gym.manage.datacount.handler.DataCountHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
@Configuration
public class DataCountRouter {
@Bean
public RouterFunction<ServerResponse> dataCountRoutes(DataCountHandler dataCountHandler) {
return route()
// ========== 会员数据统计路由 ==========
.GET("/api/stats/member/date/{date}", dataCountHandler::getMemberStatisticsByDate)
.GET("/api/stats/member/range", dataCountHandler::getMemberStatisticsByDateRange)
.GET("/api/stats/member/summary", dataCountHandler::getMemberStatisticsSummary)
// ========== 预约数据统计路由 ==========
.GET("/api/stats/booking/date/{date}", dataCountHandler::getBookingStatisticsByDate)
.GET("/api/stats/booking/range", dataCountHandler::getBookingStatisticsByDateRange)
.GET("/api/stats/booking/summary", dataCountHandler::getBookingStatisticsSummary)
// ========== 签到数据统计路由 ==========
.GET("/api/stats/signin/date/{date}", dataCountHandler::getSignInStatisticsByDate)
.GET("/api/stats/signin/range", dataCountHandler::getSignInStatisticsByDateRange)
.GET("/api/stats/signin/summary", dataCountHandler::getSignInStatisticsSummary)
// ========== 数据导出路由 ==========
.GET("/api/stats/export/member", dataCountHandler::exportMemberStatistics)
.GET("/api/stats/export/booking", dataCountHandler::exportBookingStatistics)
.GET("/api/stats/export/signin", dataCountHandler::exportSignInStatistics)
.GET("/api/stats/export/all", dataCountHandler::exportAllStatistics)
.build();
}
}
@@ -1,183 +0,0 @@
package cn.novalon.gym.manage.datacount.dao;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
/**
* 数据统计 DAO - 使用 DatabaseClient 执行跨表聚合查询
*
* @author system
* @date 2026-06-09
*/
@Repository
public class DataStatisticsDao {
private final DatabaseClient databaseClient;
public DataStatisticsDao(DatabaseClient databaseClient) {
this.databaseClient = databaseClient;
}
/**
* 统计指定时间范围内新增会员数
*/
public Mono<Long> countNewMembers(LocalDateTime startTime, LocalDateTime endTime) {
return databaseClient.sql("SELECT COUNT(*) FROM member_user WHERE created_at >= :startTime AND created_at < :endTime AND is_deleted = false")
.bind("startTime", startTime)
.bind("endTime", endTime)
.map(row -> row.get(0, Long.class))
.one();
}
/**
* 统计总会员数
*/
public Mono<Long> countTotalMembers() {
return databaseClient.sql("SELECT COUNT(*) FROM member_user WHERE is_deleted = false")
.map(row -> row.get(0, Long.class))
.one();
}
/**
* 统计指定时间范围内的签到次数
*/
public Mono<Long> countSignIns(LocalDateTime startTime, LocalDateTime endTime) {
return databaseClient.sql("SELECT COUNT(*) FROM sign_in_record WHERE sign_in_time >= :startTime AND sign_in_time < :endTime AND is_delete = false")
.bind("startTime", startTime)
.bind("endTime", endTime)
.map(row -> row.get(0, Long.class))
.one();
}
/**
* 统计指定时间范围内的成功签到次数
*/
public Mono<Long> countSuccessSignIns(LocalDateTime startTime, LocalDateTime endTime) {
return databaseClient.sql("SELECT COUNT(*) FROM sign_in_record WHERE sign_in_time >= :startTime AND sign_in_time < :endTime AND sign_in_status = 'SUCCESS' AND is_delete = false")
.bind("startTime", startTime)
.bind("endTime", endTime)
.map(row -> row.get(0, Long.class))
.one();
}
/**
* 统计指定时间范围内签到的独立会员数
*/
public Mono<Long> countDistinctSignInMembers(LocalDateTime startTime, LocalDateTime endTime) {
return databaseClient.sql("SELECT COUNT(DISTINCT member_id) FROM sign_in_record WHERE sign_in_time >= :startTime AND sign_in_time < :endTime AND is_delete = false")
.bind("startTime", startTime)
.bind("endTime", endTime)
.map(row -> row.get(0, Long.class))
.one();
}
/**
* 统计指定时间范围内的预约数
*/
public Mono<Long> countBookings(LocalDateTime startTime, LocalDateTime endTime) {
return databaseClient.sql("SELECT COUNT(*) FROM group_course_booking WHERE booking_time >= :startTime AND booking_time < :endTime AND deleted_at IS NULL")
.bind("startTime", startTime)
.bind("endTime", endTime)
.map(row -> row.get(0, Long.class))
.one();
}
/**
* 统计指定时间范围内的取消预约数
*/
public Mono<Long> countCancelBookings(LocalDateTime startTime, LocalDateTime endTime) {
return databaseClient.sql("SELECT COUNT(*) FROM group_course_booking WHERE booking_time >= :startTime AND booking_time < :endTime AND status = '1' AND deleted_at IS NULL")
.bind("startTime", startTime)
.bind("endTime", endTime)
.map(row -> row.get(0, Long.class))
.one();
}
/**
* 统计指定时间范围内的出席预约数
*/
public Mono<Long> countAttendBookings(LocalDateTime startTime, LocalDateTime endTime) {
return databaseClient.sql("SELECT COUNT(*) FROM group_course_booking WHERE booking_time >= :startTime AND booking_time < :endTime AND status = '2' AND deleted_at IS NULL")
.bind("startTime", startTime)
.bind("endTime", endTime)
.map(row -> row.get(0, Long.class))
.one();
}
/**
* 统计指定时间范围内的缺席预约数
*/
public Mono<Long> countAbsentBookings(LocalDateTime startTime, LocalDateTime endTime) {
return databaseClient.sql("SELECT COUNT(*) FROM group_course_booking WHERE booking_time >= :startTime AND booking_time < :endTime AND status = '3' AND deleted_at IS NULL")
.bind("startTime", startTime)
.bind("endTime", endTime)
.map(row -> row.get(0, Long.class))
.one();
}
/**
* 统计指定时间范围内有预约的独立会员数
*/
public Mono<Long> countDistinctBookingMembers(LocalDateTime startTime, LocalDateTime endTime) {
return databaseClient.sql("SELECT COUNT(DISTINCT member_id) FROM group_course_booking WHERE booking_time >= :startTime AND booking_time < :endTime AND deleted_at IS NULL")
.bind("startTime", startTime)
.bind("endTime", endTime)
.map(row -> row.get(0, Long.class))
.one();
}
/**
* 统计指定时间范围内取消预约的独立会员数
*/
public Mono<Long> countDistinctCancelMembers(LocalDateTime startTime, LocalDateTime endTime) {
return databaseClient.sql("SELECT COUNT(DISTINCT member_id) FROM group_course_booking WHERE booking_time >= :startTime AND booking_time < :endTime AND status = '1' AND deleted_at IS NULL")
.bind("startTime", startTime)
.bind("endTime", endTime)
.map(row -> row.get(0, Long.class))
.one();
}
/**
* 按签到类型统计次数
*/
public Flux<SignInTypeCount> countSignInsByType(LocalDateTime startTime, LocalDateTime endTime) {
return databaseClient.sql("SELECT sign_in_type as type, COUNT(*) as count FROM sign_in_record WHERE sign_in_time >= :startTime AND sign_in_time < :endTime AND is_delete = false GROUP BY sign_in_type")
.bind("startTime", startTime)
.bind("endTime", endTime)
.map(row -> {
SignInTypeCount result = new SignInTypeCount();
result.setType(row.get("type", String.class));
result.setCount(row.get("count", Long.class));
return result;
})
.all();
}
/**
* 签到类型计数结果
*/
public static class SignInTypeCount {
private String type;
private Long count;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public Long getCount() {
return count;
}
public void setCount(Long count) {
this.count = count;
}
}
}
@@ -0,0 +1,270 @@
package cn.novalon.gym.manage.datacount.dao;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 统计数据访问层
* 使用 R2dbcEntityTemplate 执行原生SQL查询
*
* @author 数据统计模块
* @date 2026-06-06
*/
@Repository
public class StatisticsDao {
private final R2dbcEntityTemplate r2dbcEntityTemplate;
public StatisticsDao(R2dbcEntityTemplate r2dbcEntityTemplate) {
this.r2dbcEntityTemplate = r2dbcEntityTemplate;
}
// ========== 会员数据统计 ==========
public Mono<Integer> countTotalMembers() {
String sql = "SELECT COUNT(*) FROM member_user WHERE is_deleted = FALSE";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.map(row -> row.get(0, Integer.class))
.one();
}
public Mono<Integer> countNewMembersByDate(LocalDate date) {
String sql = "SELECT COUNT(*) FROM member_user WHERE is_deleted = FALSE AND DATE(created_at) = $1";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.bind(0, date)
.map(row -> row.get(0, Integer.class))
.one();
}
public Mono<Integer> countActiveMembersByDate(LocalDate date) {
String sql = "SELECT COUNT(DISTINCT m.id) FROM member_user m LEFT JOIN member_card_record r ON m.id = r.member_id WHERE m.is_deleted = FALSE AND r.deleted_at IS NULL AND DATE(r.created_at) = $1";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.bind(0, date)
.map(row -> row.get(0, Integer.class))
.one();
}
public Mono<Integer> countMaleMembers() {
String sql = "SELECT COUNT(*) FROM member_user WHERE is_deleted = FALSE AND gender = 1";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.map(row -> row.get(0, Integer.class))
.one();
}
public Mono<Integer> countFemaleMembers() {
String sql = "SELECT COUNT(*) FROM member_user WHERE is_deleted = FALSE AND gender = 2";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.map(row -> row.get(0, Integer.class))
.one();
}
public Mono<Integer> countCardPurchaseByDate(LocalDate date) {
String sql = "SELECT COUNT(DISTINCT r.member_id) FROM member_card_record r WHERE r.deleted_at IS NULL AND DATE(r.created_at) = $1";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.bind(0, date)
.map(row -> row.get(0, Integer.class))
.one();
}
public Mono<Double> calculateAverageAge() {
String sql = "SELECT AVG(EXTRACT(YEAR FROM AGE(CURRENT_DATE, birthday))) FROM member_user WHERE is_deleted = FALSE AND birthday IS NOT NULL";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.map(row -> row.get(0, Double.class))
.one();
}
public Mono<Integer> countActiveMembersByTimeRange(LocalDateTime startTime, LocalDateTime endTime) {
String sql = "SELECT COUNT(*) FROM member_user WHERE is_deleted = FALSE AND last_login_at >= $1 AND last_login_at < $2";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.bind(0, startTime)
.bind(1, endTime)
.map(row -> row.get(0, Integer.class))
.one();
}
// ========== 预约数据统计 ==========
public Mono<Integer> countTotalBookingsByDate(LocalDate date) {
String sql = "SELECT COUNT(*) FROM group_course_booking WHERE deleted_at IS NULL AND DATE(created_at) = $1";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.bind(0, date)
.map(row -> row.get(0, Integer.class))
.one();
}
public Mono<Integer> countCancelledBookingsByDate(LocalDate date) {
String sql = "SELECT COUNT(*) FROM group_course_booking WHERE deleted_at IS NULL AND status = '1' AND DATE(cancel_time) = $1";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.bind(0, date)
.map(row -> row.get(0, Integer.class))
.one();
}
public Mono<Integer> countAttendedBookingsByDate(LocalDate date) {
String sql = "SELECT COUNT(*) FROM group_course_booking WHERE deleted_at IS NULL AND status = '2' AND DATE(created_at) = $1";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.bind(0, date)
.map(row -> row.get(0, Integer.class))
.one();
}
public Mono<Integer> countTotalCoursesByDate(LocalDate date) {
String sql = "SELECT COUNT(*) FROM group_course WHERE deleted_at IS NULL AND DATE(start_time) = $1";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.bind(0, date)
.map(row -> row.get(0, Integer.class))
.one();
}
public Mono<Integer> countFullCoursesByDate(LocalDate date) {
String sql = "SELECT COUNT(*) FROM group_course WHERE deleted_at IS NULL AND current_members = max_members AND DATE(start_time) = $1";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.bind(0, date)
.map(row -> row.get(0, Integer.class))
.one();
}
public Mono<Object[]> findMostPopularCourseByDate(LocalDate date) {
String sql = "SELECT c.course_name, COUNT(b.id) as booking_count FROM group_course_booking b LEFT JOIN group_course c ON b.course_id = c.id WHERE b.deleted_at IS NULL AND c.deleted_at IS NULL AND DATE(b.created_at) = $1 GROUP BY c.id, c.course_name ORDER BY booking_count DESC LIMIT 1";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.bind(0, date)
.map(row -> new Object[]{row.get(0, String.class), row.get(1, Integer.class)})
.one();
}
// ========== 签到数据统计 ==========
public Mono<Integer> countTotalSignInsByDate(LocalDate date) {
String sql = "SELECT COUNT(*) FROM sign_in_record WHERE is_delete = FALSE AND DATE(sign_in_time) = $1";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.bind(0, date)
.map(row -> row.get(0, Integer.class))
.one();
}
public Mono<Integer> countSuccessSignInsByDate(LocalDate date) {
String sql = "SELECT COUNT(*) FROM sign_in_record WHERE is_delete = FALSE AND sign_in_status = 'SUCCESS' AND DATE(sign_in_time) = $1";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.bind(0, date)
.map(row -> row.get(0, Integer.class))
.one();
}
public Mono<Integer> countFailedSignInsByDate(LocalDate date) {
String sql = "SELECT COUNT(*) FROM sign_in_record WHERE is_delete = FALSE AND sign_in_status = 'FAILED' AND DATE(sign_in_time) = $1";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.bind(0, date)
.map(row -> row.get(0, Integer.class))
.one();
}
public Mono<Integer> countUniqueMembersByDate(LocalDate date) {
String sql = "SELECT COUNT(DISTINCT member_id) FROM sign_in_record WHERE is_delete = FALSE AND DATE(sign_in_time) = $1";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.bind(0, date)
.map(row -> row.get(0, Integer.class))
.one();
}
public Mono<Integer> countQrCodeSignInsByDate(LocalDate date) {
String sql = "SELECT COUNT(*) FROM sign_in_record WHERE is_delete = FALSE AND sign_in_type = 'QR_CODE' AND DATE(sign_in_time) = $1";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.bind(0, date)
.map(row -> row.get(0, Integer.class))
.one();
}
public Mono<Integer> countManualSignInsByDate(LocalDate date) {
String sql = "SELECT COUNT(*) FROM sign_in_record WHERE is_delete = FALSE AND sign_in_type = 'MANUAL' AND DATE(sign_in_time) = $1";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.bind(0, date)
.map(row -> row.get(0, Integer.class))
.one();
}
public Mono<Integer> countFaceSignInsByDate(LocalDate date) {
String sql = "SELECT COUNT(*) FROM sign_in_record WHERE is_delete = FALSE AND sign_in_type = 'FACE' AND DATE(sign_in_time) = $1";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.bind(0, date)
.map(row -> row.get(0, Integer.class))
.one();
}
public Flux<Object[]> findSignInStatisticsByDateRange(LocalDate startDate, LocalDate endDate) {
String sql = "SELECT DATE(s.sign_in_time) as date, " +
"COUNT(*) as total_sign_ins, " +
"SUM(CASE WHEN s.sign_in_status = 'SUCCESS' THEN 1 ELSE 0 END) as success_sign_ins, " +
"SUM(CASE WHEN s.sign_in_status = 'FAILED' THEN 1 ELSE 0 END) as failed_sign_ins, " +
"COUNT(DISTINCT s.member_id) as unique_members, " +
"SUM(CASE WHEN s.sign_in_type = 'QR_CODE' THEN 1 ELSE 0 END) as qr_code_sign_ins, " +
"SUM(CASE WHEN s.sign_in_type = 'MANUAL' THEN 1 ELSE 0 END) as manual_sign_ins, " +
"SUM(CASE WHEN s.sign_in_type = 'FACE' THEN 1 ELSE 0 END) as face_sign_ins " +
"FROM sign_in_record s " +
"WHERE s.is_delete = FALSE AND DATE(s.sign_in_time) BETWEEN $1 AND $2 " +
"GROUP BY DATE(s.sign_in_time) " +
"ORDER BY date";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.bind(0, startDate)
.bind(1, endDate)
.map(row -> new Object[]{
row.get(0, LocalDate.class),
row.get(1, Integer.class),
row.get(2, Integer.class),
row.get(3, Integer.class),
row.get(4, Integer.class),
row.get(5, Integer.class),
row.get(6, Integer.class),
row.get(7, Integer.class)
})
.all();
}
// ========== 日期范围查询 ==========
public Flux<Object[]> findMemberStatisticsByDateRange(LocalDate startDate, LocalDate endDate) {
String sql = "SELECT DATE(m.created_at) as date, COUNT(*) as new_members FROM member_user m WHERE m.is_deleted = FALSE AND DATE(m.created_at) BETWEEN $1 AND $2 GROUP BY DATE(m.created_at) ORDER BY date";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.bind(0, startDate)
.bind(1, endDate)
.map(row -> new Object[]{row.get(0, LocalDate.class), row.get(1, Integer.class)})
.all();
}
public Flux<Object[]> findBookingStatisticsByDateRange(LocalDate startDate, LocalDate endDate) {
String sql = "SELECT DATE(b.created_at) as date, COUNT(*) as total_bookings, SUM(CASE WHEN b.status = '1' THEN 1 ELSE 0 END) as cancelled_bookings, SUM(CASE WHEN b.status = '2' THEN 1 ELSE 0 END) as attended_bookings FROM group_course_booking b WHERE b.deleted_at IS NULL AND DATE(b.created_at) BETWEEN $1 AND $2 GROUP BY DATE(b.created_at) ORDER BY date";
return r2dbcEntityTemplate.getDatabaseClient()
.sql(sql)
.bind(0, startDate)
.bind(1, endDate)
.map(row -> new Object[]{row.get(0, LocalDate.class), row.get(1, Integer.class), row.get(2, Integer.class), row.get(3, Integer.class)})
.all();
}
}
@@ -1,64 +1,169 @@
package cn.novalon.gym.manage.datacount.domain; package cn.novalon.gym.manage.datacount.domain;
import lombok.AllArgsConstructor; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/** import java.time.LocalDate;
* 预约数据统计结果
* @Schema(description = "预约数据统计")
* @author system
* @date 2026-06-09
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BookingStatistics { public class BookingStatistics {
/** @Schema(description = "统计日期", example = "2026-01-01")
* 统计日期 private LocalDate date;
*/
private String statDate;
/** @Schema(description = "预约总数", example = "100")
* 新增预约数 private Integer totalBookings;
*/
private Long newBookings;
/** @Schema(description = "取消预约数", example = "10")
* 取消预约数 private Integer cancelledBookings;
*/
private Long cancelBookings;
/** @Schema(description = "实际到场数", example = "85")
* 出席预约数 private Integer attendedBookings;
*/
private Long attendBookings;
/** @Schema(description = "预约成功率", example = "0.85")
* 缺席预约数 private Double bookingSuccessRate;
*/
private Long absentBookings;
/** @Schema(description = "取消率", example = "0.10")
* 预约出席率 private Double cancellationRate;
*/
@Schema(description = "到场率", example = "0.85")
private Double attendanceRate; private Double attendanceRate;
/** @Schema(description = "热门课程名称", example = "动感单车")
* 取消率 private String popularCourseName;
*/
private Double cancelRate;
/** @Schema(description = "热门课程预约数", example = "25")
* 预约人数(独立会员数) private Integer popularCourseCount;
*/
private Long bookingMembers;
/** @Schema(description = "团课总数", example = "20")
* 取消人数(独立会员数) private Integer totalCourses;
*/
private Long cancelMembers; @Schema(description = "满员课程数", example = "5")
} private Integer fullCourses;
@Schema(description = "开始日期(用于汇总统计)")
private LocalDate startDate;
@Schema(description = "结束日期(用于汇总统计)")
private LocalDate endDate;
public BookingStatistics() {
}
public BookingStatistics(LocalDate date) {
this.date = date;
this.totalBookings = 0;
this.cancelledBookings = 0;
this.attendedBookings = 0;
this.bookingSuccessRate = 0.0;
this.cancellationRate = 0.0;
this.attendanceRate = 0.0;
this.popularCourseName = "";
this.popularCourseCount = 0;
this.totalCourses = 0;
this.fullCourses = 0;
}
public LocalDate getDate() {
return date;
}
public void setDate(LocalDate date) {
this.date = date;
}
public Integer getTotalBookings() {
return totalBookings;
}
public void setTotalBookings(Integer totalBookings) {
this.totalBookings = totalBookings;
}
public Integer getCancelledBookings() {
return cancelledBookings;
}
public void setCancelledBookings(Integer cancelledBookings) {
this.cancelledBookings = cancelledBookings;
}
public Integer getAttendedBookings() {
return attendedBookings;
}
public void setAttendedBookings(Integer attendedBookings) {
this.attendedBookings = attendedBookings;
}
public Double getBookingSuccessRate() {
return bookingSuccessRate;
}
public void setBookingSuccessRate(Double bookingSuccessRate) {
this.bookingSuccessRate = bookingSuccessRate;
}
public Double getCancellationRate() {
return cancellationRate;
}
public void setCancellationRate(Double cancellationRate) {
this.cancellationRate = cancellationRate;
}
public Double getAttendanceRate() {
return attendanceRate;
}
public void setAttendanceRate(Double attendanceRate) {
this.attendanceRate = attendanceRate;
}
public String getPopularCourseName() {
return popularCourseName;
}
public void setPopularCourseName(String popularCourseName) {
this.popularCourseName = popularCourseName;
}
public Integer getPopularCourseCount() {
return popularCourseCount;
}
public void setPopularCourseCount(Integer popularCourseCount) {
this.popularCourseCount = popularCourseCount;
}
public Integer getTotalCourses() {
return totalCourses;
}
public void setTotalCourses(Integer totalCourses) {
this.totalCourses = totalCourses;
}
public Integer getFullCourses() {
return fullCourses;
}
public void setFullCourses(Integer fullCourses) {
this.fullCourses = fullCourses;
}
public LocalDate getStartDate() {
return startDate;
}
public void setStartDate(LocalDate startDate) {
this.startDate = startDate;
}
public LocalDate getEndDate() {
return endDate;
}
public void setEndDate(LocalDate endDate) {
this.endDate = endDate;
}
}
@@ -1,79 +0,0 @@
package cn.novalon.gym.manage.datacount.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 数据统计结果域对象
*
* @author system
* @date 2026-06-09
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DataStatistics {
/**
* 统计类型:MEMBER-会员统计,BOOKING-预约统计,SIGN_IN-签到统计
*/
private String statType;
/**
* 统计周期:DAY-日统计,WEEK-周统计,MONTH-月统计
*/
private String periodType;
/**
* 统计日期(DAY时为具体日期,WEEK时为周开始日期,MONTH时为月份第一天)
*/
private LocalDateTime statDate;
/**
* 统计数据值
*/
private Long count;
/**
* 关联ID(如会员ID等)
*/
private Long relatedId;
/**
* 扩展数据(JSON格式存储额外信息)
*/
private String extraData;
/**
* 统计周期常量
*/
public static final class PeriodType {
/** 日统计 */
public static final String DAY = "DAY";
/** 周统计 */
public static final String WEEK = "WEEK";
/** 月统计 */
public static final String MONTH = "MONTH";
private PeriodType() {}
}
/**
* 统计类型常量
*/
public static final class StatType {
/** 会员统计 */
public static final String MEMBER = "MEMBER";
/** 预约统计 */
public static final String BOOKING = "BOOKING";
/** 签到统计 */
public static final String SIGN_IN = "SIGN_IN";
private StatType() {}
}
}
@@ -1,54 +1,145 @@
package cn.novalon.gym.manage.datacount.domain; package cn.novalon.gym.manage.datacount.domain;
import lombok.AllArgsConstructor; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/** import java.time.LocalDate;
* 会员数据统计结果
* @Schema(description = "会员数据统计")
* @author system
* @date 2026-06-09
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberStatistics { public class MemberStatistics {
/** @Schema(description = "统计日期", example = "2026-01-01")
* 统计日期 private LocalDate date;
*/
private String statDate;
/** @Schema(description = "新增会员数", example = "10")
* 新增会员数 private Integer newMembers;
*/
private Long newMembers;
/** @Schema(description = "活跃会员数", example = "50")
* 活跃会员数(当天有签到或预约的会员) private Integer activeMembers;
*/
private Long activeMembers;
/** @Schema(description = "留存会员数", example = "45")
* 累计会员总数 private Integer retainedMembers;
*/
private Long totalMembers;
/** @Schema(description = "总会员数", example = "200")
* 今日签到会员数 private Integer totalMembers;
*/
private Long signInMembers;
/** @Schema(description = "男性会员数", example = "100")
* 今日预约会员数 private Integer maleMembers;
*/
private Long bookingMembers;
/** @Schema(description = "女性会员数", example = "100")
* 今日取消预约会员数 private Integer femaleMembers;
*/
private Long cancelBookingMembers; @Schema(description = "会员卡购买人数", example = "30")
} private Integer cardPurchaseCount;
@Schema(description = "平均年龄", example = "28")
private Double averageAge;
@Schema(description = "开始日期(用于汇总统计)")
private LocalDate startDate;
@Schema(description = "结束日期(用于汇总统计)")
private LocalDate endDate;
public MemberStatistics() {
}
public MemberStatistics(LocalDate date) {
this.date = date;
this.newMembers = 0;
this.activeMembers = 0;
this.retainedMembers = 0;
this.totalMembers = 0;
this.maleMembers = 0;
this.femaleMembers = 0;
this.cardPurchaseCount = 0;
this.averageAge = 0.0;
}
public LocalDate getDate() {
return date;
}
public void setDate(LocalDate date) {
this.date = date;
}
public Integer getNewMembers() {
return newMembers;
}
public void setNewMembers(Integer newMembers) {
this.newMembers = newMembers;
}
public Integer getActiveMembers() {
return activeMembers;
}
public void setActiveMembers(Integer activeMembers) {
this.activeMembers = activeMembers;
}
public Integer getRetainedMembers() {
return retainedMembers;
}
public void setRetainedMembers(Integer retainedMembers) {
this.retainedMembers = retainedMembers;
}
public Integer getTotalMembers() {
return totalMembers;
}
public void setTotalMembers(Integer totalMembers) {
this.totalMembers = totalMembers;
}
public Integer getMaleMembers() {
return maleMembers;
}
public void setMaleMembers(Integer maleMembers) {
this.maleMembers = maleMembers;
}
public Integer getFemaleMembers() {
return femaleMembers;
}
public void setFemaleMembers(Integer femaleMembers) {
this.femaleMembers = femaleMembers;
}
public Integer getCardPurchaseCount() {
return cardPurchaseCount;
}
public void setCardPurchaseCount(Integer cardPurchaseCount) {
this.cardPurchaseCount = cardPurchaseCount;
}
public Double getAverageAge() {
return averageAge;
}
public void setAverageAge(Double averageAge) {
this.averageAge = averageAge;
}
public LocalDate getStartDate() {
return startDate;
}
public void setStartDate(LocalDate startDate) {
this.startDate = startDate;
}
public LocalDate getEndDate() {
return endDate;
}
public void setEndDate(LocalDate endDate) {
this.endDate = endDate;
}
}
@@ -1,64 +1,157 @@
package cn.novalon.gym.manage.datacount.domain; package cn.novalon.gym.manage.datacount.domain;
import lombok.AllArgsConstructor; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/** import java.time.LocalDate;
* 签到数据统计结果
* @Schema(description = "签到数据统计")
* @author system
* @date 2026-06-09
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SignInStatistics { public class SignInStatistics {
/** @Schema(description = "统计日期", example = "2026-01-01")
* 统计日期 private LocalDate date;
*/
private String statDate;
/** @Schema(description = "签到总次数", example = "100")
* 签到总次数 private Integer totalSignIns;
*/
private Long totalSignIns;
/** @Schema(description = "成功签到次数", example = "95")
* 成功签到次数 private Integer successSignIns;
*/
private Long successSignIns;
/** @Schema(description = "失败签到次数", example = "5")
* 失败签到次数 private Integer failedSignIns;
*/
private Long failedSignIns;
/** @Schema(description = "签到成功率", example = "0.95")
* 签到成功率
*/
private Double successRate; private Double successRate;
/** @Schema(description = "签到会员数(去重)", example = "80")
* 签到人数(独立会员数) private Integer uniqueMembers;
*/
private Long signInMembers;
/** @Schema(description = "平均到店频次(月均)", example = "4.5")
* 扫码签到次数 private Double averageFrequency;
*/
private Long qrCodeSignIns;
/** @Schema(description = "扫码签到次数", example = "80")
* 手动签到次数 private Integer qrCodeSignIns;
*/
private Long manualSignIns;
/** @Schema(description = "手动签到次数", example = "15")
* 人脸识别签到次数 private Integer manualSignIns;
*/
private Long faceSignIns; @Schema(description = "人脸识别签到次数", example = "5")
} private Integer faceSignIns;
@Schema(description = "开始日期(用于汇总统计)")
private LocalDate startDate;
@Schema(description = "结束日期(用于汇总统计)")
private LocalDate endDate;
public SignInStatistics() {
}
public SignInStatistics(LocalDate date) {
this.date = date;
this.totalSignIns = 0;
this.successSignIns = 0;
this.failedSignIns = 0;
this.successRate = 0.0;
this.uniqueMembers = 0;
this.averageFrequency = 0.0;
this.qrCodeSignIns = 0;
this.manualSignIns = 0;
this.faceSignIns = 0;
}
public LocalDate getDate() {
return date;
}
public void setDate(LocalDate date) {
this.date = date;
}
public Integer getTotalSignIns() {
return totalSignIns;
}
public void setTotalSignIns(Integer totalSignIns) {
this.totalSignIns = totalSignIns;
}
public Integer getSuccessSignIns() {
return successSignIns;
}
public void setSuccessSignIns(Integer successSignIns) {
this.successSignIns = successSignIns;
}
public Integer getFailedSignIns() {
return failedSignIns;
}
public void setFailedSignIns(Integer failedSignIns) {
this.failedSignIns = failedSignIns;
}
public Double getSuccessRate() {
return successRate;
}
public void setSuccessRate(Double successRate) {
this.successRate = successRate;
}
public Integer getUniqueMembers() {
return uniqueMembers;
}
public void setUniqueMembers(Integer uniqueMembers) {
this.uniqueMembers = uniqueMembers;
}
public Double getAverageFrequency() {
return averageFrequency;
}
public void setAverageFrequency(Double averageFrequency) {
this.averageFrequency = averageFrequency;
}
public Integer getQrCodeSignIns() {
return qrCodeSignIns;
}
public void setQrCodeSignIns(Integer qrCodeSignIns) {
this.qrCodeSignIns = qrCodeSignIns;
}
public Integer getManualSignIns() {
return manualSignIns;
}
public void setManualSignIns(Integer manualSignIns) {
this.manualSignIns = manualSignIns;
}
public Integer getFaceSignIns() {
return faceSignIns;
}
public void setFaceSignIns(Integer faceSignIns) {
this.faceSignIns = faceSignIns;
}
public LocalDate getStartDate() {
return startDate;
}
public void setStartDate(LocalDate startDate) {
this.startDate = startDate;
}
public LocalDate getEndDate() {
return endDate;
}
public void setEndDate(LocalDate endDate) {
this.endDate = endDate;
}
}
@@ -0,0 +1,101 @@
package cn.novalon.gym.manage.datacount.domain;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDate;
import java.util.List;
@Schema(description = "统计数据导出DTO")
public class StatisticsExportDTO {
@Schema(description = "导出文件名", example = "statistics_report_20260101")
private String fileName;
@Schema(description = "导出格式", example = "EXCEL")
private String format;
@Schema(description = "开始日期", example = "2026-01-01")
private LocalDate startDate;
@Schema(description = "结束日期", example = "2026-01-31")
private LocalDate endDate;
@Schema(description = "导出类型", example = "MEMBER")
private String exportType;
@Schema(description = "会员统计数据")
private List<MemberStatistics> memberStatisticsList;
@Schema(description = "预约统计数据")
private List<BookingStatistics> bookingStatisticsList;
@Schema(description = "签到统计数据")
private List<SignInStatistics> signInStatisticsList;
public StatisticsExportDTO() {
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getFormat() {
return format;
}
public void setFormat(String format) {
this.format = format;
}
public LocalDate getStartDate() {
return startDate;
}
public void setStartDate(LocalDate startDate) {
this.startDate = startDate;
}
public LocalDate getEndDate() {
return endDate;
}
public void setEndDate(LocalDate endDate) {
this.endDate = endDate;
}
public String getExportType() {
return exportType;
}
public void setExportType(String exportType) {
this.exportType = exportType;
}
public List<MemberStatistics> getMemberStatisticsList() {
return memberStatisticsList;
}
public void setMemberStatisticsList(List<MemberStatistics> memberStatisticsList) {
this.memberStatisticsList = memberStatisticsList;
}
public List<BookingStatistics> getBookingStatisticsList() {
return bookingStatisticsList;
}
public void setBookingStatisticsList(List<BookingStatistics> bookingStatisticsList) {
this.bookingStatisticsList = bookingStatisticsList;
}
public List<SignInStatistics> getSignInStatisticsList() {
return signInStatisticsList;
}
public void setSignInStatisticsList(List<SignInStatistics> signInStatisticsList) {
this.signInStatisticsList = signInStatisticsList;
}
}
@@ -1,52 +0,0 @@
package cn.novalon.gym.manage.datacount.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 统计数据查询请求
*
* @author system
* @date 2026-06-09
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class StatisticsQuery {
/**
* 统计类型:MEMBER-会员统计,BOOKING-预约统计,SIGN_IN-签到统计
* 空表示所有类型
*/
private String statType;
/**
* 统计周期:DAY-日统计,WEEK-周统计,MONTH-月统计
*/
private String periodType;
/**
* 开始时间
*/
private LocalDateTime startTime;
/**
* 结束时间
*/
private LocalDateTime endTime;
/**
* 分页页码
*/
private Integer page;
/**
* 每页大小
*/
private Integer size;
}
@@ -1,44 +0,0 @@
package cn.novalon.gym.manage.datacount.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 统计数据汇总
*
* @author system
* @date 2026-06-09
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class StatisticsSummary {
/**
* 统计日期
*/
private String statDate;
/**
* 会员统计数据
*/
private MemberStatistics memberStatistics;
/**
* 预约统计数据
*/
private BookingStatistics bookingStatistics;
/**
* 签到统计数据
*/
private SignInStatistics signInStatistics;
/**
* 统计数据生成时间
*/
private String generatedAt;
}
@@ -0,0 +1,283 @@
package cn.novalon.gym.manage.datacount.handler;
import cn.novalon.gym.manage.datacount.domain.BookingStatistics;
import cn.novalon.gym.manage.datacount.domain.MemberStatistics;
import cn.novalon.gym.manage.datacount.domain.SignInStatistics;
import cn.novalon.gym.manage.datacount.service.IDataCountService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
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 java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
@Component
@Tag(name = "数据统计", description = "数据统计相关操作")
public class DataCountHandler {
private final IDataCountService dataCountService;
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
public DataCountHandler(IDataCountService dataCountService) {
this.dataCountService = dataCountService;
}
@Operation(summary = "获取会员数据统计(按日期)", description = "获取指定日期的会员数据统计")
public Mono<ServerResponse> getMemberStatisticsByDate(ServerRequest request) {
String dateStr = request.pathVariable("date");
LocalDate date = LocalDate.parse(dateStr, DATE_FORMATTER);
return dataCountService.getMemberStatisticsByDate(date)
.flatMap(stats -> ServerResponse.ok().bodyValue(stats))
.switchIfEmpty(ServerResponse.notFound().build())
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
}
@Operation(summary = "获取会员数据统计(按日期范围)", description = "获取指定日期范围内的会员数据统计")
public Mono<ServerResponse> getMemberStatisticsByDateRange(ServerRequest request) {
String startDateStr = request.queryParam("startDate").orElseThrow(() -> new IllegalArgumentException("startDate必填"));
String endDateStr = request.queryParam("endDate").orElseThrow(() -> new IllegalArgumentException("endDate必填"));
LocalDate startDate = LocalDate.parse(startDateStr, DATE_FORMATTER);
LocalDate endDate = LocalDate.parse(endDateStr, DATE_FORMATTER);
return dataCountService.getMemberStatisticsByDateRange(startDate, endDate)
.collectList()
.flatMap(list -> ServerResponse.ok().bodyValue(list))
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
}
@Operation(summary = "获取会员数据统计汇总", description = "获取指定日期范围内的会员数据统计汇总")
public Mono<ServerResponse> getMemberStatisticsSummary(ServerRequest request) {
String startDateStr = request.queryParam("startDate").orElseThrow(() -> new IllegalArgumentException("startDate必填"));
String endDateStr = request.queryParam("endDate").orElseThrow(() -> new IllegalArgumentException("endDate必填"));
LocalDate startDate = LocalDate.parse(startDateStr, DATE_FORMATTER);
LocalDate endDate = LocalDate.parse(endDateStr, DATE_FORMATTER);
return dataCountService.getMemberStatisticsSummary(startDate, endDate)
.flatMap(summary -> ServerResponse.ok().bodyValue(summary))
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
}
@Operation(summary = "获取预约数据统计(按日期)", description = "获取指定日期的预约数据统计")
public Mono<ServerResponse> getBookingStatisticsByDate(ServerRequest request) {
String dateStr = request.pathVariable("date");
LocalDate date = LocalDate.parse(dateStr, DATE_FORMATTER);
return dataCountService.getBookingStatisticsByDate(date)
.flatMap(stats -> ServerResponse.ok().bodyValue(stats))
.switchIfEmpty(ServerResponse.notFound().build())
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
}
@Operation(summary = "获取预约数据统计(按日期范围)", description = "获取指定日期范围内的预约数据统计")
public Mono<ServerResponse> getBookingStatisticsByDateRange(ServerRequest request) {
String startDateStr = request.queryParam("startDate").orElseThrow(() -> new IllegalArgumentException("startDate必填"));
String endDateStr = request.queryParam("endDate").orElseThrow(() -> new IllegalArgumentException("endDate必填"));
LocalDate startDate = LocalDate.parse(startDateStr, DATE_FORMATTER);
LocalDate endDate = LocalDate.parse(endDateStr, DATE_FORMATTER);
return dataCountService.getBookingStatisticsByDateRange(startDate, endDate)
.collectList()
.flatMap(list -> ServerResponse.ok().bodyValue(list))
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
}
@Operation(summary = "获取预约数据统计汇总", description = "获取指定日期范围内的预约数据统计汇总")
public Mono<ServerResponse> getBookingStatisticsSummary(ServerRequest request) {
String startDateStr = request.queryParam("startDate").orElseThrow(() -> new IllegalArgumentException("startDate必填"));
String endDateStr = request.queryParam("endDate").orElseThrow(() -> new IllegalArgumentException("endDate必填"));
LocalDate startDate = LocalDate.parse(startDateStr, DATE_FORMATTER);
LocalDate endDate = LocalDate.parse(endDateStr, DATE_FORMATTER);
return dataCountService.getBookingStatisticsSummary(startDate, endDate)
.flatMap(summary -> ServerResponse.ok().bodyValue(summary))
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
}
@Operation(summary = "导出会员数据统计", description = "导出指定日期范围内的会员数据统计为CSV文件")
public Mono<ServerResponse> exportMemberStatistics(ServerRequest request) {
String startDateStr = request.queryParam("startDate").orElseThrow(() -> new IllegalArgumentException("startDate必填"));
String endDateStr = request.queryParam("endDate").orElseThrow(() -> new IllegalArgumentException("endDate必填"));
LocalDate startDate = LocalDate.parse(startDateStr, DATE_FORMATTER);
LocalDate endDate = LocalDate.parse(endDateStr, DATE_FORMATTER);
String fileName = "member_statistics_" + startDateStr + "_" + endDateStr + ".csv";
return dataCountService.exportMemberStatistics(startDate, endDate)
.flatMap(data -> ServerResponse.ok()
.header("Content-Disposition", "attachment; filename=" + fileName)
.header("Content-Type", "text/csv; charset=UTF-8")
.bodyValue(data))
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
}
@Operation(summary = "导出预约数据统计", description = "导出指定日期范围内的预约数据统计为CSV文件")
public Mono<ServerResponse> exportBookingStatistics(ServerRequest request) {
String startDateStr = request.queryParam("startDate").orElseThrow(() -> new IllegalArgumentException("startDate必填"));
String endDateStr = request.queryParam("endDate").orElseThrow(() -> new IllegalArgumentException("endDate必填"));
LocalDate startDate = LocalDate.parse(startDateStr, DATE_FORMATTER);
LocalDate endDate = LocalDate.parse(endDateStr, DATE_FORMATTER);
String fileName = "booking_statistics_" + startDateStr + "_" + endDateStr + ".csv";
return dataCountService.exportBookingStatistics(startDate, endDate)
.flatMap(data -> ServerResponse.ok()
.header("Content-Disposition", "attachment; filename=" + fileName)
.header("Content-Type", "text/csv; charset=UTF-8")
.bodyValue(data))
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
}
@Operation(summary = "导出全部统计数据", description = "导出指定日期范围内的会员和预约数据统计为CSV文件")
public Mono<ServerResponse> exportAllStatistics(ServerRequest request) {
String startDateStr = request.queryParam("startDate").orElseThrow(() -> new IllegalArgumentException("startDate必填"));
String endDateStr = request.queryParam("endDate").orElseThrow(() -> new IllegalArgumentException("endDate必填"));
LocalDate startDate = LocalDate.parse(startDateStr, DATE_FORMATTER);
LocalDate endDate = LocalDate.parse(endDateStr, DATE_FORMATTER);
String fileName = "all_statistics_" + startDateStr + "_" + endDateStr + ".csv";
return dataCountService.exportAllStatistics(startDate, endDate)
.flatMap(data -> ServerResponse.ok()
.header("Content-Disposition", "attachment; filename=" + fileName)
.header("Content-Type", "text/csv; charset=UTF-8")
.bodyValue(data))
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
}
// ========== 签到数据统计 ==========
@Operation(summary = "获取签到数据统计(按日期)", description = "获取指定日期的签到数据统计")
public Mono<ServerResponse> getSignInStatisticsByDate(ServerRequest request) {
String dateStr = request.pathVariable("date");
LocalDate date = LocalDate.parse(dateStr, DATE_FORMATTER);
return dataCountService.getSignInStatisticsByDate(date)
.flatMap(stats -> ServerResponse.ok().bodyValue(stats))
.switchIfEmpty(ServerResponse.notFound().build())
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
}
@Operation(summary = "获取签到数据统计(按日期范围)", description = "获取指定日期范围内的签到数据统计")
public Mono<ServerResponse> getSignInStatisticsByDateRange(ServerRequest request) {
String startDateStr = request.queryParam("startDate").orElseThrow(() -> new IllegalArgumentException("startDate必填"));
String endDateStr = request.queryParam("endDate").orElseThrow(() -> new IllegalArgumentException("endDate必填"));
LocalDate startDate = LocalDate.parse(startDateStr, DATE_FORMATTER);
LocalDate endDate = LocalDate.parse(endDateStr, DATE_FORMATTER);
return dataCountService.getSignInStatisticsByDateRange(startDate, endDate)
.collectList()
.flatMap(list -> ServerResponse.ok().bodyValue(list))
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
}
@Operation(summary = "获取签到数据统计汇总", description = "获取指定日期范围内的签到数据统计汇总")
public Mono<ServerResponse> getSignInStatisticsSummary(ServerRequest request) {
String startDateStr = request.queryParam("startDate").orElseThrow(() -> new IllegalArgumentException("startDate必填"));
String endDateStr = request.queryParam("endDate").orElseThrow(() -> new IllegalArgumentException("endDate必填"));
LocalDate startDate = LocalDate.parse(startDateStr, DATE_FORMATTER);
LocalDate endDate = LocalDate.parse(endDateStr, DATE_FORMATTER);
return dataCountService.getSignInStatisticsSummary(startDate, endDate)
.flatMap(summary -> ServerResponse.ok().bodyValue(summary))
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
}
@Operation(summary = "导出签到数据统计", description = "导出指定日期范围内的签到数据统计为CSV文件")
public Mono<ServerResponse> exportSignInStatistics(ServerRequest request) {
String startDateStr = request.queryParam("startDate").orElseThrow(() -> new IllegalArgumentException("startDate必填"));
String endDateStr = request.queryParam("endDate").orElseThrow(() -> new IllegalArgumentException("endDate必填"));
LocalDate startDate = LocalDate.parse(startDateStr, DATE_FORMATTER);
LocalDate endDate = LocalDate.parse(endDateStr, DATE_FORMATTER);
String fileName = "signin_statistics_" + startDateStr + "_" + endDateStr + ".csv";
return dataCountService.exportSignInStatistics(startDate, endDate)
.flatMap(data -> ServerResponse.ok()
.header("Content-Disposition", "attachment; filename=" + fileName)
.header("Content-Type", "text/csv; charset=UTF-8")
.bodyValue(data))
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
}
}
@@ -1,125 +0,0 @@
package cn.novalon.gym.manage.datacount.handler;
import cn.novalon.gym.manage.datacount.domain.*;
import cn.novalon.gym.manage.datacount.service.IDataStatisticsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
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 java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 数据统计 Handler
*
* @author system
* @date 2026-06-09
*/
@Component
@Tag(name = "数据统计", description = "数据统计相关操作")
public class DataStatisticsHandler {
@Autowired
private IDataStatisticsService dataStatisticsService;
@Operation(summary = "获取综合统计数据", description = "获取会员、预约、签到综合统计数据")
public Mono<ServerResponse> getStatisticsSummary(ServerRequest request) {
StatisticsQuery query = buildQueryFromRequest(request);
return dataStatisticsService.getStatisticsSummaryWithCache(query)
.flatMap(summary -> ServerResponse.ok().bodyValue(summary))
.onErrorResume(e -> {
StatisticsSummary errorSummary = StatisticsSummary.builder()
.statDate(LocalDateTime.now().toLocalDate().toString())
.generatedAt(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
.build();
return ServerResponse.ok().bodyValue(errorSummary);
});
}
@Operation(summary = "获取会员统计数据", description = "获取会员统计数据")
public Mono<ServerResponse> getMemberStatistics(ServerRequest request) {
StatisticsQuery query = buildQueryFromRequest(request);
return dataStatisticsService.getMemberStatistics(query)
.flatMap(stats -> ServerResponse.ok().bodyValue(stats))
.onErrorResume(e -> ServerResponse.ok().bodyValue(MemberStatistics.builder().statDate(query.getStartTime() != null ? query.getStartTime().toLocalDate().toString() : LocalDateTime.now().toLocalDate().toString()).build()));
}
@Operation(summary = "获取预约统计数据", description = "获取预约统计数据")
public Mono<ServerResponse> getBookingStatistics(ServerRequest request) {
StatisticsQuery query = buildQueryFromRequest(request);
return dataStatisticsService.getBookingStatistics(query)
.flatMap(stats -> ServerResponse.ok().bodyValue(stats))
.onErrorResume(e -> ServerResponse.ok().bodyValue(BookingStatistics.builder().statDate(query.getStartTime() != null ? query.getStartTime().toLocalDate().toString() : LocalDateTime.now().toLocalDate().toString()).build()));
}
@Operation(summary = "获取签到统计数据", description = "获取签到统计数据")
public Mono<ServerResponse> getSignInStatistics(ServerRequest request) {
StatisticsQuery query = buildQueryFromRequest(request);
return dataStatisticsService.getSignInStatistics(query)
.flatMap(stats -> ServerResponse.ok().bodyValue(stats))
.onErrorResume(e -> ServerResponse.ok().bodyValue(SignInStatistics.builder().statDate(query.getStartTime() != null ? query.getStartTime().toLocalDate().toString() : LocalDateTime.now().toLocalDate().toString()).build()));
}
@Operation(summary = "查询历史统计数据", description = "查询历史统计数据")
public Mono<ServerResponse> queryHistoricalStatistics(ServerRequest request) {
StatisticsQuery query = buildQueryFromRequest(request);
return dataStatisticsService.queryHistoricalStatistics(query)
.collectList()
.flatMap(stats -> ServerResponse.ok().bodyValue(stats));
}
@Operation(summary = "导出统计数据", description = "导出统计数据为Excel文件")
public Mono<ServerResponse> exportStatistics(ServerRequest request) {
StatisticsQuery query = buildQueryFromRequest(request);
return dataStatisticsService.exportStatistics(query)
.flatMap(bytes -> {
String filename = "statistics_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".xlsx";
return ServerResponse.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
.contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
.bodyValue(bytes);
})
.onErrorResume(e -> ServerResponse.ok().bodyValue("导出失败: " + e.getMessage()));
}
private StatisticsQuery buildQueryFromRequest(ServerRequest request) {
StatisticsQuery.StatisticsQueryBuilder builder = StatisticsQuery.builder();
request.queryParam("statType").ifPresent(builder::statType);
request.queryParam("periodType").ifPresent(builder::periodType);
request.queryParam("startTime").ifPresent(startTimeStr -> {
try {
builder.startTime(LocalDateTime.parse(startTimeStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
} catch (Exception e) {
try {
builder.startTime(LocalDateTime.parse(startTimeStr));
} catch (Exception ignored) {
}
}
});
request.queryParam("endTime").ifPresent(endTimeStr -> {
try {
builder.endTime(LocalDateTime.parse(endTimeStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
} catch (Exception e) {
try {
builder.endTime(LocalDateTime.parse(endTimeStr));
} catch (Exception ignored) {
}
}
});
return builder.build();
}
}
@@ -1,109 +0,0 @@
package cn.novalon.gym.manage.datacount.scheduler;
import cn.novalon.gym.manage.datacount.domain.DataStatistics;
import cn.novalon.gym.manage.datacount.service.IDataStatisticsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.time.LocalDate;
/**
* 数据统计定时任务
* 每日凌晨执行统计数据更新
*
* @author system
* @date 2026-06-09
*/
@Component
public class DataStatisticsScheduler {
private static final Logger log = LoggerFactory.getLogger(DataStatisticsScheduler.class);
@Autowired
private IDataStatisticsService dataStatisticsService;
/**
* 每日凌晨2点执行前一天的日统计数据
* 使用cron表达式: 0 0 2 * * ?
*/
@Scheduled(cron = "0 0 2 * * ?")
public void executeDailyStatistics() {
LocalDate yesterday = LocalDate.now().minusDays(1);
log.info("Starting daily statistics task for date: {}", yesterday);
dataStatisticsService.executeDailyStatistics(yesterday)
.subscribe(
null,
error -> log.error("Daily statistics task failed for date: {}", yesterday, error),
() -> log.info("Daily statistics task completed for date: {}", yesterday)
);
}
/**
* 每周一凌晨3点执行上周的周统计数据
* 使用cron表达式: 0 0 3 ? * MON
*/
@Scheduled(cron = "0 0 3 ? * MON")
public void executeWeeklyStatistics() {
LocalDate lastWeek = LocalDate.now().minusWeeks(1);
log.info("Starting weekly statistics task for week of: {}", lastWeek);
// 执行上周每天的统计数据汇总
LocalDate weekStart = lastWeek.with(java.time.temporal.TemporalAdjusters.previousOrSame(java.time.DayOfWeek.MONDAY));
LocalDate weekEnd = lastWeek.with(java.time.temporal.TemporalAdjusters.nextOrSame(java.time.DayOfWeek.SUNDAY));
for (LocalDate date = weekStart; !date.isAfter(weekEnd); date = date.plusDays(1)) {
final LocalDate statDate = date;
dataStatisticsService.executeDailyStatistics(statDate)
.block();
}
log.info("Weekly statistics task completed for week: {} - {}", weekStart, weekEnd);
}
/**
* 每月1日凌晨3点执行上个月的月统计数据
* 使用cron表达式: 0 0 3 1 * ?
*/
@Scheduled(cron = "0 0 3 1 * ?")
public void executeMonthlyStatistics() {
LocalDate lastMonth = LocalDate.now().minusMonths(1);
log.info("Starting monthly statistics task for month: {}", lastMonth.getMonth());
// 执行上个月每天的统计数据补全
LocalDate startOfMonth = lastMonth.withDayOfMonth(1);
LocalDate endOfMonth = lastMonth.with(java.time.temporal.TemporalAdjusters.lastDayOfMonth());
for (LocalDate date = startOfMonth; !date.isAfter(endOfMonth); date = date.plusDays(1)) {
final LocalDate statDate = date;
dataStatisticsService.executeDailyStatistics(statDate)
.block();
}
log.info("Monthly statistics task completed for month: {}", lastMonth.getMonth());
}
/**
* 每月15日凌晨4点清理30天前的旧统计数据
* 使用cron表达式: 0 0 4 15 * ?
*/
@Scheduled(cron = "0 0 4 15 * ?")
public void cleanupOldStatistics() {
LocalDate cutoffDate = LocalDate.now().minusDays(30);
log.info("Starting cleanup old statistics task, removing data before: {}", cutoffDate);
// 清理Redis中的旧统计数据
String pattern = "datacount:statistics:*:" + cutoffDate.toString();
cn.novalon.gym.manage.common.util.RedisUtil redisUtil = null;
try {
// 这里可以通过注入的service来清理,但当前实现使用Redis缓存30天自动过期
log.info("Old statistics cleanup completed, cutoff date: {}", cutoffDate);
} catch (Exception e) {
log.error("Failed to cleanup old statistics", e);
}
}
}
@@ -0,0 +1,138 @@
package cn.novalon.gym.manage.datacount.scheduler;
import cn.novalon.gym.manage.datacount.service.IDataCountService;
import cn.novalon.gym.manage.datacount.service.impl.DataCountServiceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
/**
* 数据统计定时任务
*
* 功能:每日凌晨1点自动计算前一天的数据统计并缓存到Redis
*
* @author 数据统计模块
* @date 2026-06-06
*/
@Component
public class StatisticsDailyScheduler {
private static final Logger logger = LoggerFactory.getLogger(StatisticsDailyScheduler.class);
private final IDataCountService dataCountService;
public StatisticsDailyScheduler(IDataCountService dataCountService) {
this.dataCountService = dataCountService;
}
/**
* 每日凌晨1点执行前一天的统计数据计算并缓存
* cron表达式:秒 分 时 日 月 周
*/
@Scheduled(cron = "0 0 1 * * ?")
public void calculateDailyStatistics() {
LocalDate yesterday = LocalDate.now().minusDays(1);
logger.info("定时任务开始计算 {} 的数据统计", yesterday);
dataCountService.getMemberStatisticsByDate(yesterday)
.doOnSuccess(stats -> logger.info("会员统计计算完成: date={}, newMembers={}, totalMembers={}",
yesterday, stats.getNewMembers(), stats.getTotalMembers()))
.doOnError(error -> logger.error("会员统计计算失败: date={}", yesterday, error))
.subscribe();
dataCountService.getBookingStatisticsByDate(yesterday)
.doOnSuccess(stats -> logger.info("预约统计计算完成: date={}, totalBookings={}, attendanceRate={}%",
yesterday, stats.getTotalBookings(), stats.getAttendanceRate()))
.doOnError(error -> logger.error("预约统计计算失败: date={}", yesterday, error))
.subscribe();
dataCountService.getSignInStatisticsByDate(yesterday)
.doOnSuccess(stats -> logger.info("签到统计计算完成: date={}, totalSignIns={}, successRate={}%",
yesterday, stats.getTotalSignIns(), stats.getSuccessRate()))
.doOnError(error -> logger.error("签到统计计算失败: date={}", yesterday, error))
.subscribe();
}
/**
* 每周一凌晨2点执行上周的数据汇总统计
*/
@Scheduled(cron = "0 0 2 * * 1")
public void calculateWeeklyStatistics() {
LocalDate lastWeekEnd = LocalDate.now().minusDays(1);
LocalDate lastWeekStart = lastWeekEnd.minusDays(6);
logger.info("定时任务开始计算上周汇总统计: {} ~ {}", lastWeekStart, lastWeekEnd);
dataCountService.getMemberStatisticsSummary(lastWeekStart, lastWeekEnd)
.doOnSuccess(stats -> logger.info("上周会员汇总统计完成: newMembers={}", stats.getNewMembers()))
.doOnError(error -> logger.error("上周会员汇总统计失败", error))
.subscribe();
dataCountService.getBookingStatisticsSummary(lastWeekStart, lastWeekEnd)
.doOnSuccess(stats -> logger.info("上周预约汇总统计完成: totalBookings={}", stats.getTotalBookings()))
.doOnError(error -> logger.error("上周预约汇总统计失败", error))
.subscribe();
dataCountService.getSignInStatisticsSummary(lastWeekStart, lastWeekEnd)
.doOnSuccess(stats -> logger.info("上周签到汇总统计完成: totalSignIns={}", stats.getTotalSignIns()))
.doOnError(error -> logger.error("上周签到汇总统计失败", error))
.subscribe();
}
/**
* 每月1号凌晨3点执行上月的数据汇总统计
*/
@Scheduled(cron = "0 0 3 1 * ?")
public void calculateMonthlyStatistics() {
LocalDate lastMonthEnd = LocalDate.now().minusDays(1);
LocalDate lastMonthStart = lastMonthEnd.withDayOfMonth(1);
logger.info("定时任务开始计算上月汇总统计: {} ~ {}", lastMonthStart, lastMonthEnd);
dataCountService.getMemberStatisticsSummary(lastMonthStart, lastMonthEnd)
.doOnSuccess(stats -> logger.info("上月会员汇总统计完成: newMembers={}", stats.getNewMembers()))
.doOnError(error -> logger.error("上月会员汇总统计失败", error))
.subscribe();
dataCountService.getBookingStatisticsSummary(lastMonthStart, lastMonthEnd)
.doOnSuccess(stats -> logger.info("上月预约汇总统计完成: totalBookings={}", stats.getTotalBookings()))
.doOnError(error -> logger.error("上月预约汇总统计失败", error))
.subscribe();
dataCountService.getSignInStatisticsSummary(lastMonthStart, lastMonthEnd)
.doOnSuccess(stats -> logger.info("上月签到汇总统计完成: totalSignIns={}", stats.getTotalSignIns()))
.doOnError(error -> logger.error("上月签到汇总统计失败", error))
.subscribe();
}
/**
* 每日凌晨4点清理30天前的统计缓存数据
* 业务规则:统计数据保留30天(用于趋势分析),业务记录永久保存
*/
@Scheduled(cron = "0 0 4 * * ?")
public void cleanOldStatisticsCache() {
LocalDate thirtyDaysAgo = LocalDate.now().minusDays(30);
logger.info("定时任务开始清理30天前的统计缓存数据: before {}", thirtyDaysAgo);
String thirtyDaysAgoStr = thirtyDaysAgo.toString();
// 清理会员统计缓存
String memberCacheKey = DataCountServiceImpl.MEMBER_STATS_CACHE_PREFIX + thirtyDaysAgoStr;
dataCountService.getRedisUtil().delete(memberCacheKey)
.doOnSuccess(deleted -> logger.info("清理会员统计缓存: key={}, deleted={}", memberCacheKey, deleted))
.subscribe();
// 清理预约统计缓存
String bookingCacheKey = DataCountServiceImpl.BOOKING_STATS_CACHE_PREFIX + thirtyDaysAgoStr;
dataCountService.getRedisUtil().delete(bookingCacheKey)
.doOnSuccess(deleted -> logger.info("清理预约统计缓存: key={}, deleted={}", bookingCacheKey, deleted))
.subscribe();
// 清理签到统计缓存
String signInCacheKey = DataCountServiceImpl.SIGN_IN_STATS_CACHE_PREFIX + thirtyDaysAgoStr;
dataCountService.getRedisUtil().delete(signInCacheKey)
.doOnSuccess(deleted -> logger.info("清理签到统计缓存: key={}, deleted={}", signInCacheKey, deleted))
.subscribe();
}
}
@@ -0,0 +1,44 @@
package cn.novalon.gym.manage.datacount.service;
import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.datacount.domain.BookingStatistics;
import cn.novalon.gym.manage.datacount.domain.MemberStatistics;
import cn.novalon.gym.manage.datacount.domain.SignInStatistics;
import cn.novalon.gym.manage.datacount.domain.StatisticsExportDTO;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDate;
public interface IDataCountService {
Mono<MemberStatistics> getMemberStatisticsByDate(LocalDate date);
Flux<MemberStatistics> getMemberStatisticsByDateRange(LocalDate startDate, LocalDate endDate);
Mono<MemberStatistics> getMemberStatisticsSummary(LocalDate startDate, LocalDate endDate);
Mono<BookingStatistics> getBookingStatisticsByDate(LocalDate date);
Flux<BookingStatistics> getBookingStatisticsByDateRange(LocalDate startDate, LocalDate endDate);
Mono<BookingStatistics> getBookingStatisticsSummary(LocalDate startDate, LocalDate endDate);
// ========== 签到数据统计 ==========
Mono<SignInStatistics> getSignInStatisticsByDate(LocalDate date);
Flux<SignInStatistics> getSignInStatisticsByDateRange(LocalDate startDate, LocalDate endDate);
Mono<SignInStatistics> getSignInStatisticsSummary(LocalDate startDate, LocalDate endDate);
Mono<byte[]> exportSignInStatistics(LocalDate startDate, LocalDate endDate);
Mono<byte[]> exportMemberStatistics(LocalDate startDate, LocalDate endDate);
Mono<byte[]> exportBookingStatistics(LocalDate startDate, LocalDate endDate);
Mono<byte[]> exportAllStatistics(LocalDate startDate, LocalDate endDate);
RedisUtil getRedisUtil();
}
@@ -1,78 +0,0 @@
package cn.novalon.gym.manage.datacount.service;
import cn.novalon.gym.manage.datacount.domain.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 数据统计服务接口
*
* @author system
* @date 2026-06-09
*/
public interface IDataStatisticsService {
/**
* 获取会员统计数据
*
* @param query 查询条件
* @return 会员统计数据
*/
Mono<MemberStatistics> getMemberStatistics(StatisticsQuery query);
/**
* 获取预约统计数据
*
* @param query 查询条件
* @return 预约统计数据
*/
Mono<BookingStatistics> getBookingStatistics(StatisticsQuery query);
/**
* 获取签到统计数据
*
* @param query 查询条件
* @return 签到统计数据
*/
Mono<SignInStatistics> getSignInStatistics(StatisticsQuery query);
/**
* 获取综合统计数据(包含会员、预约、签到)
*
* @param query 查询条件
* @return 综合统计数据
*/
Mono<StatisticsSummary> getStatisticsSummary(StatisticsQuery query);
/**
* 查询历史统计数据
*
* @param query 查询条件
* @return 历史统计数据列表
*/
Flux<DataStatistics> queryHistoricalStatistics(StatisticsQuery query);
/**
* 执行每日统计数据更新(定时任务调用)
*
* @param statDate 统计日期
* @return 更新结果
*/
Mono<Void> executeDailyStatistics(java.time.LocalDate statDate);
/**
* 导出统计数据为Excel
*
* @param query 查询条件
* @return Excel文件的字节数组
*/
Mono<byte[]> exportStatistics(StatisticsQuery query);
/**
* 获取统计数据(带缓存)
*
* @param query 查询条件
* @return 统计数据
*/
Mono<StatisticsSummary> getStatisticsSummaryWithCache(StatisticsQuery query);
}
@@ -0,0 +1,475 @@
package cn.novalon.gym.manage.datacount.service.impl;
import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.datacount.dao.StatisticsDao;
import cn.novalon.gym.manage.datacount.domain.BookingStatistics;
import cn.novalon.gym.manage.datacount.domain.MemberStatistics;
import cn.novalon.gym.manage.datacount.domain.SignInStatistics;
import cn.novalon.gym.manage.datacount.service.IDataCountService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.ByteArrayOutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Service
public class DataCountServiceImpl implements IDataCountService {
private final StatisticsDao statisticsDao;
private final RedisUtil redisUtil;
private final ObjectMapper objectMapper;
public static final String MEMBER_STATS_CACHE_PREFIX = "stats:member:";
public static final String BOOKING_STATS_CACHE_PREFIX = "stats:booking:";
public static final String SIGN_IN_STATS_CACHE_PREFIX = "stats:signin:";
private static final long CACHE_EXPIRE_SECONDS = 3600;
public DataCountServiceImpl(StatisticsDao statisticsDao, RedisUtil redisUtil, ObjectMapper objectMapper) {
this.statisticsDao = statisticsDao;
this.redisUtil = redisUtil;
this.objectMapper = objectMapper;
}
@Override
public Mono<MemberStatistics> getMemberStatisticsByDate(LocalDate date) {
String cacheKey = MEMBER_STATS_CACHE_PREFIX + date.toString();
return redisUtil.get(cacheKey)
.flatMap(cached -> {
if (cached != null) {
try {
MemberStatistics stats = objectMapper.readValue(cached.toString(), MemberStatistics.class);
log.debug("从缓存获取会员统计数据, date: {}", date);
return Mono.just(stats);
} catch (Exception e) {
log.warn("缓存数据解析失败, date: {}", date, e);
}
}
return Mono.empty();
})
.switchIfEmpty(fetchMemberStatisticsByDate(date)
.flatMap(stats -> {
try {
String json = objectMapper.writeValueAsString(stats);
return redisUtil.setWithExpire(cacheKey, json, CACHE_EXPIRE_SECONDS)
.then(Mono.just(stats));
} catch (Exception e) {
log.warn("缓存会员统计数据失败, date: {}", date, e);
return Mono.just(stats);
}
}));
}
private Mono<MemberStatistics> fetchMemberStatisticsByDate(LocalDate date) {
return Mono.zip(
statisticsDao.countTotalMembers(),
statisticsDao.countNewMembersByDate(date),
statisticsDao.countActiveMembersByDate(date),
statisticsDao.countMaleMembers(),
statisticsDao.countFemaleMembers(),
statisticsDao.countCardPurchaseByDate(date),
statisticsDao.calculateAverageAge()
).map(tuple -> {
MemberStatistics stats = new MemberStatistics(date);
stats.setTotalMembers(tuple.getT1());
stats.setNewMembers(tuple.getT2());
stats.setActiveMembers(tuple.getT3());
stats.setMaleMembers(tuple.getT4());
stats.setFemaleMembers(tuple.getT5());
stats.setCardPurchaseCount(tuple.getT6());
stats.setAverageAge(tuple.getT7() != null ? tuple.getT7() : 0.0);
return stats;
});
}
@Override
public Flux<MemberStatistics> getMemberStatisticsByDateRange(LocalDate startDate, LocalDate endDate) {
return statisticsDao.findMemberStatisticsByDateRange(startDate, endDate)
.map(row -> {
LocalDate date = (LocalDate) row[0];
MemberStatistics stats = new MemberStatistics(date);
stats.setNewMembers(((Number) row[1]).intValue());
return stats;
});
}
@Override
public Mono<MemberStatistics> getMemberStatisticsSummary(LocalDate startDate, LocalDate endDate) {
return getMemberStatisticsByDateRange(startDate, endDate)
.collectList()
.flatMap(list -> {
MemberStatistics summary = new MemberStatistics();
summary.setStartDate(startDate);
summary.setEndDate(endDate);
summary.setNewMembers(list.stream().mapToInt(MemberStatistics::getNewMembers).sum());
return Mono.zip(
statisticsDao.countTotalMembers(),
statisticsDao.countMaleMembers(),
statisticsDao.countFemaleMembers(),
statisticsDao.calculateAverageAge()
).map(tuple -> {
summary.setTotalMembers(tuple.getT1());
summary.setMaleMembers(tuple.getT2());
summary.setFemaleMembers(tuple.getT3());
summary.setAverageAge(tuple.getT4() != null ? tuple.getT4() : 0.0);
return summary;
});
});
}
@Override
public Mono<BookingStatistics> getBookingStatisticsByDate(LocalDate date) {
String cacheKey = BOOKING_STATS_CACHE_PREFIX + date.toString();
return redisUtil.get(cacheKey)
.flatMap(cached -> {
if (cached != null) {
try {
BookingStatistics stats = objectMapper.readValue(cached.toString(), BookingStatistics.class);
log.debug("从缓存获取预约统计数据, date: {}", date);
return Mono.just(stats);
} catch (Exception e) {
log.warn("缓存数据解析失败, date: {}", date, e);
}
}
return Mono.empty();
})
.switchIfEmpty(fetchBookingStatisticsByDate(date)
.flatMap(stats -> {
try {
String json = objectMapper.writeValueAsString(stats);
return redisUtil.setWithExpire(cacheKey, json, CACHE_EXPIRE_SECONDS)
.then(Mono.just(stats));
} catch (Exception e) {
log.warn("缓存预约统计数据失败, date: {}", date, e);
return Mono.just(stats);
}
}));
}
private Mono<BookingStatistics> fetchBookingStatisticsByDate(LocalDate date) {
return Mono.zip(
statisticsDao.countTotalBookingsByDate(date),
statisticsDao.countCancelledBookingsByDate(date),
statisticsDao.countAttendedBookingsByDate(date),
statisticsDao.countTotalCoursesByDate(date),
statisticsDao.countFullCoursesByDate(date),
statisticsDao.findMostPopularCourseByDate(date)
).map(tuple -> {
int totalBookings = tuple.getT1();
int cancelledBookings = tuple.getT2();
int attendedBookings = tuple.getT3();
BookingStatistics stats = new BookingStatistics(date);
stats.setTotalBookings(totalBookings);
stats.setCancelledBookings(cancelledBookings);
stats.setAttendedBookings(attendedBookings);
stats.setTotalCourses(tuple.getT4());
stats.setFullCourses(tuple.getT5());
if (totalBookings > 0) {
stats.setCancellationRate(Math.round(cancelledBookings * 10000.0 / totalBookings) / 100.0);
stats.setAttendanceRate(Math.round(attendedBookings * 10000.0 / totalBookings) / 100.0);
}
Object[] popularCourse = tuple.getT6();
if (popularCourse != null && popularCourse.length >= 2) {
stats.setPopularCourseName((String) popularCourse[0]);
stats.setPopularCourseCount(((Number) popularCourse[1]).intValue());
}
return stats;
});
}
@Override
public Flux<BookingStatistics> getBookingStatisticsByDateRange(LocalDate startDate, LocalDate endDate) {
return statisticsDao.findBookingStatisticsByDateRange(startDate, endDate)
.map(row -> {
LocalDate date = (LocalDate) row[0];
int totalBookings = ((Number) row[1]).intValue();
int cancelledBookings = ((Number) row[2]).intValue();
int attendedBookings = ((Number) row[3]).intValue();
BookingStatistics stats = new BookingStatistics(date);
stats.setTotalBookings(totalBookings);
stats.setCancelledBookings(cancelledBookings);
stats.setAttendedBookings(attendedBookings);
if (totalBookings > 0) {
stats.setCancellationRate(Math.round(cancelledBookings * 10000.0 / totalBookings) / 100.0);
stats.setAttendanceRate(Math.round(attendedBookings * 10000.0 / totalBookings) / 100.0);
}
return stats;
});
}
@Override
public Mono<BookingStatistics> getBookingStatisticsSummary(LocalDate startDate, LocalDate endDate) {
return getBookingStatisticsByDateRange(startDate, endDate)
.collectList()
.map(list -> {
BookingStatistics summary = new BookingStatistics();
summary.setStartDate(startDate);
summary.setEndDate(endDate);
summary.setTotalBookings(list.stream().mapToInt(BookingStatistics::getTotalBookings).sum());
summary.setCancelledBookings(list.stream().mapToInt(BookingStatistics::getCancelledBookings).sum());
summary.setAttendedBookings(list.stream().mapToInt(BookingStatistics::getAttendedBookings).sum());
int total = summary.getTotalBookings();
if (total > 0) {
summary.setCancellationRate(Math.round(summary.getCancelledBookings() * 10000.0 / total) / 100.0);
summary.setAttendanceRate(Math.round(summary.getAttendedBookings() * 10000.0 / total) / 100.0);
}
return summary;
});
}
@Override
public Mono<SignInStatistics> getSignInStatisticsByDate(LocalDate date) {
String cacheKey = SIGN_IN_STATS_CACHE_PREFIX + date.toString();
return redisUtil.get(cacheKey)
.flatMap(cached -> {
if (cached != null) {
try {
SignInStatistics stats = objectMapper.readValue(cached.toString(), SignInStatistics.class);
log.debug("从缓存获取签到统计数据, date: {}", date);
return Mono.just(stats);
} catch (Exception e) {
log.warn("缓存数据解析失败, date: {}", date, e);
}
}
return Mono.empty();
})
.switchIfEmpty(fetchSignInStatisticsByDate(date)
.flatMap(stats -> {
try {
String json = objectMapper.writeValueAsString(stats);
return redisUtil.setWithExpire(cacheKey, json, CACHE_EXPIRE_SECONDS)
.then(Mono.just(stats));
} catch (Exception e) {
log.warn("缓存签到统计数据失败, date: {}", date, e);
return Mono.just(stats);
}
}));
}
private Mono<SignInStatistics> fetchSignInStatisticsByDate(LocalDate date) {
return Mono.zip(
statisticsDao.countTotalSignInsByDate(date),
statisticsDao.countSuccessSignInsByDate(date),
statisticsDao.countFailedSignInsByDate(date),
statisticsDao.countUniqueMembersByDate(date),
statisticsDao.countQrCodeSignInsByDate(date),
statisticsDao.countManualSignInsByDate(date),
statisticsDao.countFaceSignInsByDate(date)
).map(tuple -> {
int totalSignIns = tuple.getT1();
int successSignIns = tuple.getT2();
int failedSignIns = tuple.getT3();
SignInStatistics stats = new SignInStatistics(date);
stats.setTotalSignIns(totalSignIns);
stats.setSuccessSignIns(successSignIns);
stats.setFailedSignIns(failedSignIns);
stats.setUniqueMembers(tuple.getT4());
stats.setQrCodeSignIns(tuple.getT5());
stats.setManualSignIns(tuple.getT6());
stats.setFaceSignIns(tuple.getT7());
if (totalSignIns > 0) {
stats.setSuccessRate(Math.round(successSignIns * 10000.0 / totalSignIns) / 100.0);
}
return stats;
});
}
@Override
public Flux<SignInStatistics> getSignInStatisticsByDateRange(LocalDate startDate, LocalDate endDate) {
return statisticsDao.findSignInStatisticsByDateRange(startDate, endDate)
.map(row -> {
LocalDate date = (LocalDate) row[0];
int totalSignIns = ((Number) row[1]).intValue();
int successSignIns = ((Number) row[2]).intValue();
int failedSignIns = ((Number) row[3]).intValue();
int uniqueMembers = ((Number) row[4]).intValue();
int qrCodeSignIns = ((Number) row[5]).intValue();
int manualSignIns = ((Number) row[6]).intValue();
int faceSignIns = ((Number) row[7]).intValue();
SignInStatistics stats = new SignInStatistics(date);
stats.setTotalSignIns(totalSignIns);
stats.setSuccessSignIns(successSignIns);
stats.setFailedSignIns(failedSignIns);
stats.setUniqueMembers(uniqueMembers);
stats.setQrCodeSignIns(qrCodeSignIns);
stats.setManualSignIns(manualSignIns);
stats.setFaceSignIns(faceSignIns);
if (totalSignIns > 0) {
stats.setSuccessRate(Math.round(successSignIns * 10000.0 / totalSignIns) / 100.0);
}
return stats;
});
}
@Override
public Mono<SignInStatistics> getSignInStatisticsSummary(LocalDate startDate, LocalDate endDate) {
return getSignInStatisticsByDateRange(startDate, endDate)
.collectList()
.map(list -> {
SignInStatistics summary = new SignInStatistics();
summary.setStartDate(startDate);
summary.setEndDate(endDate);
summary.setTotalSignIns(list.stream().mapToInt(SignInStatistics::getTotalSignIns).sum());
summary.setSuccessSignIns(list.stream().mapToInt(SignInStatistics::getSuccessSignIns).sum());
summary.setFailedSignIns(list.stream().mapToInt(SignInStatistics::getFailedSignIns).sum());
summary.setUniqueMembers(list.stream().mapToInt(SignInStatistics::getUniqueMembers).sum());
summary.setQrCodeSignIns(list.stream().mapToInt(SignInStatistics::getQrCodeSignIns).sum());
summary.setManualSignIns(list.stream().mapToInt(SignInStatistics::getManualSignIns).sum());
summary.setFaceSignIns(list.stream().mapToInt(SignInStatistics::getFaceSignIns).sum());
int total = summary.getTotalSignIns();
if (total > 0) {
summary.setSuccessRate(Math.round(summary.getSuccessSignIns() * 10000.0 / total) / 100.0);
}
return summary;
});
}
@Override
public Mono<byte[]> exportSignInStatistics(LocalDate startDate, LocalDate endDate) {
return getSignInStatisticsByDateRange(startDate, endDate)
.collectList()
.map(this::generateSignInStatisticsCSV);
}
private byte[] generateSignInStatisticsCSV(List<SignInStatistics> statsList) {
return generateSignInStatisticsCSVContent(statsList).getBytes(StandardCharsets.UTF_8);
}
private String generateSignInStatisticsCSVContent(List<SignInStatistics> statsList) {
StringBuilder sb = new StringBuilder();
sb.append("统计日期,签到总次数,成功签到次数,失败签到次数,签到成功率(%),签到会员数,扫码签到次数,手动签到次数,人脸识别签到次数\n");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (SignInStatistics stats : statsList) {
sb.append(stats.getDate().format(formatter)).append(",");
sb.append(stats.getTotalSignIns()).append(",");
sb.append(stats.getSuccessSignIns()).append(",");
sb.append(stats.getFailedSignIns()).append(",");
sb.append(String.format("%.2f", stats.getSuccessRate())).append(",");
sb.append(stats.getUniqueMembers()).append(",");
sb.append(stats.getQrCodeSignIns()).append(",");
sb.append(stats.getManualSignIns()).append(",");
sb.append(stats.getFaceSignIns()).append("\n");
}
return sb.toString();
}
@Override
public Mono<byte[]> exportMemberStatistics(LocalDate startDate, LocalDate endDate) {
return getMemberStatisticsByDateRange(startDate, endDate)
.collectList()
.map(this::generateMemberStatisticsCSV);
}
@Override
public Mono<byte[]> exportBookingStatistics(LocalDate startDate, LocalDate endDate) {
return getBookingStatisticsByDateRange(startDate, endDate)
.collectList()
.map(this::generateBookingStatisticsCSV);
}
@Override
public Mono<byte[]> exportAllStatistics(LocalDate startDate, LocalDate endDate) {
return Mono.zip(
getMemberStatisticsByDateRange(startDate, endDate).collectList(),
getBookingStatisticsByDateRange(startDate, endDate).collectList(),
getSignInStatisticsByDateRange(startDate, endDate).collectList()
).map(tuple -> {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(baos, StandardCharsets.UTF_8));
writer.println("=== 会员数据统计 ===");
writer.println(generateMemberStatisticsCSVContent(tuple.getT1()));
writer.println();
writer.println("=== 预约数据统计 ===");
writer.println(generateBookingStatisticsCSVContent(tuple.getT2()));
writer.println();
writer.println("=== 签到数据统计 ===");
writer.println(generateSignInStatisticsCSVContent(tuple.getT3()));
writer.flush();
return baos.toByteArray();
});
}
private byte[] generateMemberStatisticsCSV(List<MemberStatistics> statsList) {
return generateMemberStatisticsCSVContent(statsList).getBytes(StandardCharsets.UTF_8);
}
private String generateMemberStatisticsCSVContent(List<MemberStatistics> statsList) {
StringBuilder sb = new StringBuilder();
sb.append("统计日期,新增会员数,活跃会员数,总会员数,男性会员数,女性会员数,购卡人数,平均年龄\n");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (MemberStatistics stats : statsList) {
sb.append(stats.getDate().format(formatter)).append(",");
sb.append(stats.getNewMembers()).append(",");
sb.append(stats.getActiveMembers()).append(",");
sb.append(stats.getTotalMembers()).append(",");
sb.append(stats.getMaleMembers()).append(",");
sb.append(stats.getFemaleMembers()).append(",");
sb.append(stats.getCardPurchaseCount()).append(",");
sb.append(String.format("%.1f", stats.getAverageAge())).append("\n");
}
return sb.toString();
}
private byte[] generateBookingStatisticsCSV(List<BookingStatistics> statsList) {
return generateBookingStatisticsCSVContent(statsList).getBytes(StandardCharsets.UTF_8);
}
private String generateBookingStatisticsCSVContent(List<BookingStatistics> statsList) {
StringBuilder sb = new StringBuilder();
sb.append("统计日期,预约总数,取消预约数,实际到场数,取消率(%),到场率(%),热门课程,热门课程预约数,团课总数,满员课程数\n");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (BookingStatistics stats : statsList) {
sb.append(stats.getDate().format(formatter)).append(",");
sb.append(stats.getTotalBookings()).append(",");
sb.append(stats.getCancelledBookings()).append(",");
sb.append(stats.getAttendedBookings()).append(",");
sb.append(String.format("%.2f", stats.getCancellationRate())).append(",");
sb.append(String.format("%.2f", stats.getAttendanceRate())).append(",");
sb.append(stats.getPopularCourseName() != null ? stats.getPopularCourseName() : "").append(",");
sb.append(stats.getPopularCourseCount()).append(",");
sb.append(stats.getTotalCourses()).append(",");
sb.append(stats.getFullCourses()).append("\n");
}
return sb.toString();
}
@Override
public RedisUtil getRedisUtil() {
return redisUtil;
}
}
@@ -1,513 +0,0 @@
package cn.novalon.gym.manage.datacount.service.impl;
import cn.novalon.gym.manage.checkIn.entity.SignInRecord;
import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.datacount.dao.DataStatisticsDao;
import cn.novalon.gym.manage.datacount.domain.*;
import cn.novalon.gym.manage.datacount.service.IDataStatisticsService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.io.ByteArrayOutputStream;
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAdjusters;
import java.util.HashMap;
import java.util.Map;
/**
* 数据统计服务实现类
*
* @author system
* @date 2026-06-09
*/
@Service
public class DataStatisticsServiceImpl implements IDataStatisticsService {
private static final Logger log = LoggerFactory.getLogger(DataStatisticsServiceImpl.class);
private static final String CACHE_KEY_PREFIX = "datacount:statistics:";
private static final long CACHE_EXPIRE_SECONDS = 3600; // 1小时
@Autowired
private DataStatisticsDao dataStatisticsDao;
@Autowired
private RedisUtil redisUtil;
@Autowired
private ObjectMapper objectMapper;
@Value("${datacount.cache.expire-seconds:3600}")
private long cacheExpireSeconds = 3600;
@Override
public Mono<MemberStatistics> getMemberStatistics(StatisticsQuery query) {
LocalDateTime startTime = getStartTime(query);
LocalDateTime endTime = getEndTime(query);
Mono<Long> newMembersMono = dataStatisticsDao.countNewMembers(startTime, endTime);
Mono<Long> totalMembersMono = dataStatisticsDao.countTotalMembers();
Mono<Long> signInMembersMono = dataStatisticsDao.countDistinctSignInMembers(startTime, endTime);
Mono<Long> bookingMembersMono = dataStatisticsDao.countDistinctBookingMembers(startTime, endTime);
Mono<Long> cancelMembersMono = dataStatisticsDao.countDistinctCancelMembers(startTime, endTime);
return Mono.zip(newMembersMono, totalMembersMono, signInMembersMono, bookingMembersMono, cancelMembersMono)
.map(tuple -> {
long newMembers = tuple.getT1() != null ? tuple.getT1() : 0L;
long totalMembers = tuple.getT2() != null ? tuple.getT2() : 0L;
long signInMembers = tuple.getT3() != null ? tuple.getT3() : 0L;
long bookingMembers = tuple.getT4() != null ? tuple.getT4() : 0L;
long cancelMembers = tuple.getT5() != null ? tuple.getT5() : 0L;
// 活跃会员数 = 有签到的 + 有预约的(去重后大概值)
long activeMembers = signInMembers + bookingMembers;
return MemberStatistics.builder()
.statDate(startTime.toLocalDate().toString())
.newMembers(newMembers)
.activeMembers(activeMembers)
.totalMembers(totalMembers)
.signInMembers(signInMembers)
.bookingMembers(bookingMembers)
.cancelBookingMembers(cancelMembers)
.build();
});
}
@Override
public Mono<BookingStatistics> getBookingStatistics(StatisticsQuery query) {
LocalDateTime startTime = getStartTime(query);
LocalDateTime endTime = getEndTime(query);
Mono<Long> totalMono = dataStatisticsDao.countBookings(startTime, endTime);
Mono<Long> cancelMono = dataStatisticsDao.countCancelBookings(startTime, endTime);
Mono<Long> attendMono = dataStatisticsDao.countAttendBookings(startTime, endTime);
Mono<Long> absentMono = dataStatisticsDao.countAbsentBookings(startTime, endTime);
Mono<Long> bookingMembersMono = dataStatisticsDao.countDistinctBookingMembers(startTime, endTime);
Mono<Long> cancelMembersMono = dataStatisticsDao.countDistinctCancelMembers(startTime, endTime);
return Mono.zip(totalMono, cancelMono, attendMono, absentMono, bookingMembersMono, cancelMembersMono)
.map(tuple -> {
long total = tuple.getT1() != null ? tuple.getT1() : 0L;
long cancel = tuple.getT2() != null ? tuple.getT2() : 0L;
long attend = tuple.getT3() != null ? tuple.getT3() : 0L;
long absent = tuple.getT4() != null ? tuple.getT4() : 0L;
long bookingMembers = tuple.getT5() != null ? tuple.getT5() : 0L;
long cancelMembers = tuple.getT6() != null ? tuple.getT6() : 0L;
double attendanceRate = total > 0 ? (double) attend / total * 100 : 0;
double cancelRate = total > 0 ? (double) cancel / total * 100 : 0;
return BookingStatistics.builder()
.statDate(startTime.toLocalDate().toString())
.newBookings(total)
.cancelBookings(cancel)
.attendBookings(attend)
.absentBookings(absent)
.attendanceRate(Math.round(attendanceRate * 100.0) / 100.0)
.cancelRate(Math.round(cancelRate * 100.0) / 100.0)
.bookingMembers(bookingMembers)
.cancelMembers(cancelMembers)
.build();
});
}
@Override
public Mono<SignInStatistics> getSignInStatistics(StatisticsQuery query) {
LocalDateTime startTime = getStartTime(query);
LocalDateTime endTime = getEndTime(query);
Mono<Long> totalMono = dataStatisticsDao.countSignIns(startTime, endTime);
Mono<Long> successMono = dataStatisticsDao.countSuccessSignIns(startTime, endTime);
Mono<Long> distinctMembersMono = dataStatisticsDao.countDistinctSignInMembers(startTime, endTime);
return Mono.zip(totalMono, successMono, distinctMembersMono)
.flatMap(tuple -> {
long total = tuple.getT1() != null ? tuple.getT1() : 0L;
long success = tuple.getT2() != null ? tuple.getT2() : 0L;
long distinctMembers = tuple.getT3() != null ? tuple.getT3() : 0L;
double successRate = total > 0 ? (double) success / total * 100 : 0;
return dataStatisticsDao.countSignInsByType(startTime, endTime)
.collectMap(
DataStatisticsDao.SignInTypeCount::getType,
DataStatisticsDao.SignInTypeCount::getCount
)
.defaultIfEmpty(new HashMap<>())
.map(typeCountMap -> {
long qrCode = getCountByType(typeCountMap, SignInRecord.SignInType.QR_CODE);
long manual = getCountByType(typeCountMap, SignInRecord.SignInType.MANUAL);
long face = getCountByType(typeCountMap, SignInRecord.SignInType.FACE);
return SignInStatistics.builder()
.statDate(startTime.toLocalDate().toString())
.totalSignIns(total)
.successSignIns(success)
.failedSignIns(total - success)
.successRate(Math.round(successRate * 100.0) / 100.0)
.signInMembers(distinctMembers)
.qrCodeSignIns(qrCode)
.manualSignIns(manual)
.faceSignIns(face)
.build();
});
});
}
private long getCountByType(Map<String, Long> typeCountMap, String type) {
Long count = typeCountMap.get(type);
return count != null ? count : 0L;
}
@Override
public Mono<StatisticsSummary> getStatisticsSummary(StatisticsQuery query) {
Mono<MemberStatistics> memberStatsMono = getMemberStatistics(query);
Mono<BookingStatistics> bookingStatsMono = getBookingStatistics(query);
Mono<SignInStatistics> signInStatsMono = getSignInStatistics(query);
return Mono.zip(memberStatsMono, bookingStatsMono, signInStatsMono)
.map(tuple -> StatisticsSummary.builder()
.statDate(LocalDateTime.now().toLocalDate().toString())
.memberStatistics(tuple.getT1())
.bookingStatistics(tuple.getT2())
.signInStatistics(tuple.getT3())
.generatedAt(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
.build());
}
@Override
public reactor.core.publisher.Flux<DataStatistics> queryHistoricalStatistics(StatisticsQuery query) {
// 历史统计数据查询(从Redis缓存中获取)
String cacheKey = buildCacheKey(query);
return redisUtil.get(cacheKey, String.class)
.flatMapMany(json -> {
try {
java.util.List<DataStatistics> stats = objectMapper.readValue(json,
objectMapper.getTypeFactory().constructCollectionType(java.util.List.class, DataStatistics.class));
return reactor.core.publisher.Flux.fromIterable(stats);
} catch (Exception e) {
log.error("Failed to parse historical statistics from cache", e);
return reactor.core.publisher.Flux.empty();
}
})
.switchIfEmpty(reactor.core.publisher.Flux.empty());
}
@Override
public Mono<Void> executeDailyStatistics(LocalDate statDate) {
log.info("Executing daily statistics for date: {}", statDate);
LocalDateTime dayStart = statDate.atStartOfDay();
LocalDateTime dayEnd = statDate.plusDays(1).atStartOfDay();
StatisticsQuery query = StatisticsQuery.builder()
.startTime(dayStart)
.endTime(dayEnd)
.build();
Mono<MemberStatistics> memberStatsMono = getMemberStatistics(query);
Mono<BookingStatistics> bookingStatsMono = getBookingStatistics(query);
Mono<SignInStatistics> signInStatsMono = getSignInStatistics(query);
return Mono.zip(memberStatsMono, bookingStatsMono, signInStatsMono)
.flatMap(tuple -> {
MemberStatistics memberStats = tuple.getT1();
BookingStatistics bookingStats = tuple.getT2();
SignInStatistics signInStats = tuple.getT3();
String dateStr = statDate.toString();
String memberKey = CACHE_KEY_PREFIX + "member:" + dateStr;
String bookingKey = CACHE_KEY_PREFIX + "booking:" + dateStr;
String signInKey = CACHE_KEY_PREFIX + "signin:" + dateStr;
try {
String memberJson = objectMapper.writeValueAsString(memberStats);
String bookingJson = objectMapper.writeValueAsString(bookingStats);
String signInJson = objectMapper.writeValueAsString(signInStats);
return reactor.core.publisher.Flux.merge(
redisUtil.setWithExpire(memberKey, memberJson, Duration.ofDays(30).getSeconds()),
redisUtil.setWithExpire(bookingKey, bookingJson, Duration.ofDays(30).getSeconds()),
redisUtil.setWithExpire(signInKey, signInJson, Duration.ofDays(30).getSeconds())
).then();
} catch (Exception e) {
log.error("Failed to serialize statistics data", e);
return Mono.empty();
}
})
.then()
.doOnSuccess(v -> log.info("Daily statistics completed for date: {}", statDate))
.doOnError(e -> log.error("Failed to execute daily statistics for date: {}", statDate, e));
}
@Override
public Mono<byte[]> exportStatistics(StatisticsQuery query) {
return getStatisticsSummary(query)
.flatMap(summary -> {
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("数据统计报表");
CellStyle headerStyle = workbook.createCellStyle();
Font headerFont = workbook.createFont();
headerFont.setBold(true);
headerStyle.setFont(headerFont);
createMainSheet(sheet, summary, headerStyle);
Sheet memberSheet = workbook.createSheet("会员统计");
createMemberStatisticsSheet(memberSheet, summary.getMemberStatistics(), headerStyle);
Sheet bookingSheet = workbook.createSheet("预约统计");
createBookingStatisticsSheet(bookingSheet, summary.getBookingStatistics(), headerStyle);
Sheet signInSheet = workbook.createSheet("签到统计");
createSignInStatisticsSheet(signInSheet, summary.getSignInStatistics(), headerStyle);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
workbook.write(outputStream);
return Mono.just(outputStream.toByteArray());
} catch (Exception e) {
log.error("Failed to export statistics", e);
return Mono.error(e);
}
});
}
private void createMainSheet(Sheet sheet, StatisticsSummary summary, CellStyle headerStyle) {
int rowNum = 0;
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue("统计项");
row.createCell(1).setCellValue("数值");
row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue("统计日期");
row.createCell(1).setCellValue(summary.getStatDate());
row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue("新增会员数");
row.createCell(1).setCellValue(summary.getMemberStatistics().getNewMembers());
row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue("活跃会员数");
row.createCell(1).setCellValue(summary.getMemberStatistics().getActiveMembers());
row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue("累计会员总数");
row.createCell(1).setCellValue(summary.getMemberStatistics().getTotalMembers());
row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue("新增预约数");
row.createCell(1).setCellValue(summary.getBookingStatistics().getNewBookings());
row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue("预约出席率");
row.createCell(1).setCellValue(summary.getBookingStatistics().getAttendanceRate() + "%");
row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue("签到总次数");
row.createCell(1).setCellValue(summary.getSignInStatistics().getTotalSignIns());
row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue("签到成功率");
row.createCell(1).setCellValue(summary.getSignInStatistics().getSuccessRate() + "%");
sheet.autoSizeColumn(0);
sheet.autoSizeColumn(1);
}
private void createMemberStatisticsSheet(Sheet sheet, MemberStatistics stats, CellStyle headerStyle) {
int rowNum = 0;
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue("统计项");
row.createCell(1).setCellValue("数值");
String[][] data = {
{"统计日期", stats.getStatDate()},
{"新增会员数", String.valueOf(stats.getNewMembers())},
{"活跃会员数", String.valueOf(stats.getActiveMembers())},
{"累计会员总数", String.valueOf(stats.getTotalMembers())},
{"今日签到会员数", String.valueOf(stats.getSignInMembers())},
{"今日预约会员数", String.valueOf(stats.getBookingMembers())},
{"今日取消预约会员数", String.valueOf(stats.getCancelBookingMembers())}
};
for (String[] rowData : data) {
row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(rowData[0]);
row.createCell(1).setCellValue(rowData[1]);
}
sheet.autoSizeColumn(0);
sheet.autoSizeColumn(1);
}
private void createBookingStatisticsSheet(Sheet sheet, BookingStatistics stats, CellStyle headerStyle) {
int rowNum = 0;
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue("统计项");
row.createCell(1).setCellValue("数值");
String[][] data = {
{"统计日期", stats.getStatDate()},
{"新增预约数", String.valueOf(stats.getNewBookings())},
{"取消预约数", String.valueOf(stats.getCancelBookings())},
{"出席预约数", String.valueOf(stats.getAttendBookings())},
{"缺席预约数", String.valueOf(stats.getAbsentBookings())},
{"预约出席率", stats.getAttendanceRate() + "%"},
{"取消率", stats.getCancelRate() + "%"},
{"预约人数", String.valueOf(stats.getBookingMembers())},
{"取消人数", String.valueOf(stats.getCancelMembers())}
};
for (String[] rowData : data) {
row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(rowData[0]);
row.createCell(1).setCellValue(rowData[1]);
}
sheet.autoSizeColumn(0);
sheet.autoSizeColumn(1);
}
private void createSignInStatisticsSheet(Sheet sheet, SignInStatistics stats, CellStyle headerStyle) {
int rowNum = 0;
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue("统计项");
row.createCell(1).setCellValue("数值");
String[][] data = {
{"统计日期", stats.getStatDate()},
{"签到总次数", String.valueOf(stats.getTotalSignIns())},
{"成功签到次数", String.valueOf(stats.getSuccessSignIns())},
{"失败签到次数", String.valueOf(stats.getFailedSignIns())},
{"签到成功率", stats.getSuccessRate() + "%"},
{"签到人数", String.valueOf(stats.getSignInMembers())},
{"扫码签到次数", String.valueOf(stats.getQrCodeSignIns())},
{"手动签到次数", String.valueOf(stats.getManualSignIns())},
{"人脸识别签到次数", String.valueOf(stats.getFaceSignIns())}
};
for (String[] rowData : data) {
row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(rowData[0]);
row.createCell(1).setCellValue(rowData[1]);
}
sheet.autoSizeColumn(0);
sheet.autoSizeColumn(1);
}
@Override
public Mono<StatisticsSummary> getStatisticsSummaryWithCache(StatisticsQuery query) {
String cacheKey = buildCacheKey(query);
return redisUtil.get(cacheKey, StatisticsSummary.class)
.switchIfEmpty(
getStatisticsSummary(query)
.flatMap(summary -> {
try {
String json = objectMapper.writeValueAsString(summary);
return redisUtil.setWithExpire(cacheKey, json, cacheExpireSeconds)
.thenReturn(summary);
} catch (Exception e) {
log.error("Failed to serialize statistics summary", e);
return Mono.just(summary);
}
})
);
}
private String buildCacheKey(StatisticsQuery query) {
StringBuilder keyBuilder = new StringBuilder(CACHE_KEY_PREFIX);
keyBuilder.append("summary:");
if (query.getStartTime() != null) {
keyBuilder.append(query.getStartTime().toLocalDate().toString());
}
if (query.getEndTime() != null) {
keyBuilder.append("_").append(query.getEndTime().toLocalDate().toString());
}
if (query.getStatType() != null) {
keyBuilder.append("_").append(query.getStatType());
}
if (query.getPeriodType() != null) {
keyBuilder.append("_").append(query.getPeriodType());
}
return keyBuilder.toString();
}
private LocalDateTime getStartTime(StatisticsQuery query) {
if (query.getStartTime() != null) {
return query.getStartTime();
}
LocalDate today = LocalDate.now();
String periodType = query.getPeriodType();
if (DataStatistics.PeriodType.WEEK.equals(periodType)) {
// 周统计:本周一
return today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).atStartOfDay();
} else if (DataStatistics.PeriodType.MONTH.equals(periodType)) {
// 月统计:本月第一天
return today.withDayOfMonth(1).atStartOfDay();
} else {
// 日统计:当天零点
return today.atStartOfDay();
}
}
private LocalDateTime getEndTime(StatisticsQuery query) {
if (query.getEndTime() != null) {
return query.getEndTime();
}
LocalDate today = LocalDate.now();
String periodType = query.getPeriodType();
if (DataStatistics.PeriodType.WEEK.equals(periodType)) {
// 周统计:本周日 23:59:59
return today.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)).atTime(23, 59, 59);
} else if (DataStatistics.PeriodType.MONTH.equals(periodType)) {
// 月统计:本月最后一天 23:59:59
return today.with(TemporalAdjusters.lastDayOfMonth()).atTime(23, 59, 59);
} else {
// 日统计:当前时间
return LocalDateTime.now();
}
}
/**
* 根据周期类型调整时间范围
* 用于定时任务中的周期统计
*/
private LocalDateTime[] adjustTimeRangeByPeriod(LocalDate date, String periodType) {
LocalDateTime startTime;
LocalDateTime endTime;
if (DataStatistics.PeriodType.WEEK.equals(periodType)) {
startTime = date.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).atStartOfDay();
endTime = date.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)).atTime(23, 59, 59);
} else if (DataStatistics.PeriodType.MONTH.equals(periodType)) {
startTime = date.withDayOfMonth(1).atStartOfDay();
endTime = date.with(TemporalAdjusters.lastDayOfMonth()).atTime(23, 59, 59);
} else {
startTime = date.atStartOfDay();
endTime = date.plusDays(1).atStartOfDay();
}
return new LocalDateTime[]{startTime, endTime};
}
}
@@ -1 +0,0 @@
cn.novalon.gym.manage.datacount.config.DataCountAutoConfiguration
@@ -0,0 +1,3 @@
spring:
application:
name: gym-dataCount
@@ -0,0 +1,13 @@
package cn.novalon.gym.manage.datacount;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class GymDataCountApplicationTests {
@Test
void contextLoads() {
}
}
+2 -15
View File
@@ -81,26 +81,13 @@
<artifactId>gym-member</artifactId> <artifactId>gym-member</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<!-- ZXing QR Code依赖 -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.5.3</version>
</dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>maven-jar-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
<version>3.4.2</version>
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
@@ -4,10 +4,8 @@ package cn.novalon.gym.manage.groupcourse.converter;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse; import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseBooking; import cn.novalon.gym.manage.groupcourse.domain.GroupCourseBooking;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseType;
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseBookingEntity; import cn.novalon.gym.manage.groupcourse.entity.GroupCourseBookingEntity;
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseEntity; import cn.novalon.gym.manage.groupcourse.entity.GroupCourseEntity;
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseTypeEntity;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -126,33 +124,4 @@ public class GroupCourseConverter {
.map(this::toBookingEntity) .map(this::toBookingEntity)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
/**
* 将团课类型实体转换为领域模型
*/
public GroupCourseType toGroupCourseType(GroupCourseTypeEntity entity){
if(entity == null){
return null;
}
GroupCourseType groupCourseType = new GroupCourseType();
BeanUtil.copyProperties(entity, groupCourseType);
log.debug("转换团课类型实体到领域模型:typeId={}", entity.getId());
return groupCourseType;
}
/**
* 将团课类型领域模型转换为实体
*/
public GroupCourseTypeEntity toGroupCourseTypeEntity(GroupCourseType domain){
if(domain == null){
return null;
}
GroupCourseTypeEntity entity = new GroupCourseTypeEntity();
BeanUtil.copyProperties(domain, entity);
if (domain.getId() != null) {
entity.markNotNew();
}
log.debug("转换团课类型领域模型到实体:typeId={}", domain.getId());
return entity;
}
} }
@@ -1,27 +0,0 @@
package cn.novalon.gym.manage.groupcourse.dao;
import cn.novalon.gym.manage.groupcourse.entity.CourseLabelEntity;
import org.springframework.data.r2dbc.repository.Modifying;
import org.springframework.data.r2dbc.repository.Query;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
@Repository
public interface CourseLabelDao extends R2dbcRepository<CourseLabelEntity, Long> {
Mono<CourseLabelEntity> findByIdIsAndDeletedAtIsNull(Long id);
Flux<CourseLabelEntity> findAllByDeletedAtIsNull();
Flux<CourseLabelEntity> findByLabelNameContainingAndDeletedAtIsNull(String labelName);
Mono<CourseLabelEntity> findByLabelNameAndDeletedAtIsNull(String labelName);
@Modifying
@Query("UPDATE course_label SET deleted_at = :deletedAt WHERE id = :id")
Mono<Integer> softDelete(Long id, LocalDateTime deletedAt);
}
@@ -1,38 +0,0 @@
package cn.novalon.gym.manage.groupcourse.dao;
import cn.novalon.gym.manage.groupcourse.entity.CourseTypeLabelEntity;
import org.springframework.data.r2dbc.repository.Modifying;
import org.springframework.data.r2dbc.repository.Query;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.util.List;
@Repository
public interface CourseTypeLabelDao extends R2dbcRepository<CourseTypeLabelEntity, Long> {
Flux<CourseTypeLabelEntity> findByTypeIdAndDeletedAtIsNull(Long typeId);
Flux<CourseTypeLabelEntity> findByLabelIdAndDeletedAtIsNull(Long labelId);
Mono<CourseTypeLabelEntity> findByTypeIdAndLabelIdAndDeletedAtIsNull(Long typeId, Long labelId);
@Modifying
@Query("UPDATE course_type_label SET deleted_at = :deletedAt WHERE type_id = :typeId AND label_id = :labelId")
Mono<Integer> deleteByTypeIdAndLabelId(Long typeId, Long labelId, LocalDateTime deletedAt);
@Modifying
@Query("UPDATE course_type_label SET deleted_at = :deletedAt WHERE type_id = :typeId")
Mono<Integer> deleteByTypeId(Long typeId, LocalDateTime deletedAt);
@Modifying
@Query("DELETE FROM course_type_label WHERE type_id = :typeId AND label_id = :labelId")
Mono<Integer> physicalDeleteByTypeIdAndLabelId(Long typeId, Long labelId);
@Modifying
@Query("UPDATE course_type_label SET deleted_at = :deletedAt WHERE label_id = :labelId")
Mono<Integer> deleteByLabelId(Long labelId, LocalDateTime deletedAt);
}
@@ -1,19 +1,16 @@
package cn.novalon.gym.manage.groupcourse.dao; package cn.novalon.gym.manage.groupcourse.dao;
import cn.novalon.gym.manage.groupcourse.dto.GroupCourseQueryDto;
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseEntity; import cn.novalon.gym.manage.groupcourse.entity.GroupCourseEntity;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.repository.Modifying; import org.springframework.data.r2dbc.repository.Modifying;
import org.springframework.data.r2dbc.repository.Query; import org.springframework.data.r2dbc.repository.Query;
import org.springframework.data.r2dbc.repository.R2dbcRepository; import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Repository @Repository
public interface GroupCourseDao extends R2dbcRepository<GroupCourseEntity, Long> { public interface GroupCourseDao extends R2dbcRepository<GroupCourseEntity, Long> {
@@ -39,186 +36,4 @@ public interface GroupCourseDao extends R2dbcRepository<GroupCourseEntity, Long>
@Modifying @Modifying
@Query("UPDATE group_course SET deleted_at = :deletedAt WHERE id = :id") @Query("UPDATE group_course SET deleted_at = :deletedAt WHERE id = :id")
Mono<Integer> softDelete(Long id, LocalDateTime deletedAt); Mono<Integer> softDelete(Long id, LocalDateTime deletedAt);
}
Flux<GroupCourseEntity> findByCourseTypeAndDeletedAtIsNull(Long courseType);
/**
* 多条件查询团课(使用 DatabaseClient 构建动态 SQL
*/
default Flux<GroupCourseEntity> searchGroupCourses(DatabaseClient databaseClient, GroupCourseQueryDto query) {
StringBuilder sql = new StringBuilder("SELECT * FROM group_course WHERE deleted_at IS NULL");
List<String> conditions = new ArrayList<>();
// 默认不查询不可预约的团课(仅查询 status = '0'
conditions.add("status = '0'");
// 1. 团课名称模糊查询
if (query.getCourseName() != null && !query.getCourseName().isEmpty()) {
conditions.add("course_name ILIKE :courseName");
}
// 2. 基于团课类型查询
if (query.getCourseType() != null) {
conditions.add("course_type = :courseType");
}
// 3. 基于日期时间段查询
if (query.getStartDate() != null) {
conditions.add("start_time >= :startDate");
}
if (query.getEndDate() != null) {
conditions.add("start_time <= :endDate");
}
// 4. 基于早晨/下午/夜晚时间段查询
if (query.getTimePeriod() != null && !query.getTimePeriod().isEmpty()) {
switch (query.getTimePeriod().toLowerCase()) {
case "morning":
conditions.add("EXTRACT(HOUR FROM start_time) >= 6 AND EXTRACT(HOUR FROM start_time) < 12");
break;
case "afternoon":
conditions.add("EXTRACT(HOUR FROM start_time) >= 12 AND EXTRACT(HOUR FROM start_time) < 18");
break;
case "evening":
conditions.add("EXTRACT(HOUR FROM start_time) >= 18 AND EXTRACT(HOUR FROM start_time) < 24");
break;
default:
break;
}
}
sql.append(" AND ").append(String.join(" AND ", conditions));
// 5. 价格排序 / 6. 剩余名额最多排序
boolean hasPriceSort = query.getPriceSort() != null && !query.getPriceSort().isEmpty();
boolean hasRemainingMost = query.getRemainingMost() != null && query.getRemainingMost();
if (hasPriceSort || hasRemainingMost) {
sql.append(" ORDER BY");
List<String> orderClauses = new ArrayList<>();
if (hasRemainingMost) {
orderClauses.add(" (max_members - current_members) DESC");
}
if (hasPriceSort) {
if ("asc".equalsIgnoreCase(query.getPriceSort())) {
orderClauses.add(" stored_value_amount ASC");
} else if ("desc".equalsIgnoreCase(query.getPriceSort())) {
orderClauses.add(" stored_value_amount DESC");
}
}
sql.append(String.join(",", orderClauses));
} else {
sql.append(" ORDER BY start_time ASC");
}
// 分页
int page = query.getPage() != null ? query.getPage() : 0;
int size = query.getSize() != null ? query.getSize() : 10;
if (size < 1) size = 10;
if (size > 100) size = 100;
int offset = page * size;
sql.append(" LIMIT :limit OFFSET :offset");
DatabaseClient.GenericExecuteSpec spec = databaseClient.sql(sql.toString());
if (query.getCourseName() != null && !query.getCourseName().isEmpty()) {
spec = spec.bind("courseName", "%" + query.getCourseName() + "%");
}
if (query.getCourseType() != null) {
spec = spec.bind("courseType", query.getCourseType());
}
if (query.getStartDate() != null) {
spec = spec.bind("startDate", query.getStartDate());
}
if (query.getEndDate() != null) {
spec = spec.bind("endDate", query.getEndDate());
}
spec = spec.bind("limit", size);
spec = spec.bind("offset", offset);
return spec.map((row, meta) -> {
GroupCourseEntity entity = new GroupCourseEntity();
entity.setId(row.get("id", Long.class));
entity.setCourseName(row.get("course_name", String.class));
entity.setCoachId(row.get("coach_id", Long.class));
entity.setCourseType(row.get("course_type", Long.class));
entity.setStartTime(row.get("start_time", LocalDateTime.class));
entity.setEndTime(row.get("end_time", LocalDateTime.class));
entity.setMaxMembers(row.get("max_members", Integer.class));
entity.setCurrentMembers(row.get("current_members", Integer.class));
String statusStr = row.get("status", String.class);
entity.setStatus(statusStr != null ? Long.parseLong(statusStr) : null);
entity.setLocation(row.get("location", String.class));
entity.setCoverImage(row.get("cover_image", String.class));
entity.setDescription(row.get("description", String.class));
entity.setPointCardAmount(row.get("point_card_amount", Integer.class));
entity.setStoredValueAmount(row.get("stored_value_amount", java.math.BigDecimal.class));
entity.setCreateBy(row.get("create_by", String.class));
entity.setUpdateBy(row.get("update_by", String.class));
entity.setCreatedAt(row.get("created_at", LocalDateTime.class));
entity.setUpdatedAt(row.get("updated_at", LocalDateTime.class));
entity.setDeletedAt(row.get("deleted_at", LocalDateTime.class));
return entity;
}).all();
}
/**
* 多条件查询团课总数
*/
default Mono<Long> countSearchGroupCourses(DatabaseClient databaseClient, GroupCourseQueryDto query) {
StringBuilder sql = new StringBuilder("SELECT COUNT(*) FROM group_course WHERE deleted_at IS NULL");
List<String> conditions = new ArrayList<>();
conditions.add("status = '0'");
if (query.getCourseName() != null && !query.getCourseName().isEmpty()) {
conditions.add("course_name ILIKE :courseName");
}
if (query.getCourseType() != null) {
conditions.add("course_type = :courseType");
}
if (query.getStartDate() != null) {
conditions.add("start_time >= :startDate");
}
if (query.getEndDate() != null) {
conditions.add("start_time <= :endDate");
}
if (query.getTimePeriod() != null && !query.getTimePeriod().isEmpty()) {
switch (query.getTimePeriod().toLowerCase()) {
case "morning":
conditions.add("EXTRACT(HOUR FROM start_time) >= 6 AND EXTRACT(HOUR FROM start_time) < 12");
break;
case "afternoon":
conditions.add("EXTRACT(HOUR FROM start_time) >= 12 AND EXTRACT(HOUR FROM start_time) < 18");
break;
case "evening":
conditions.add("EXTRACT(HOUR FROM start_time) >= 18 AND EXTRACT(HOUR FROM start_time) < 24");
break;
default:
break;
}
}
sql.append(" AND ").append(String.join(" AND ", conditions));
DatabaseClient.GenericExecuteSpec spec = databaseClient.sql(sql.toString());
if (query.getCourseName() != null && !query.getCourseName().isEmpty()) {
spec = spec.bind("courseName", "%" + query.getCourseName() + "%");
}
if (query.getCourseType() != null) {
spec = spec.bind("courseType", query.getCourseType());
}
if (query.getStartDate() != null) {
spec = spec.bind("startDate", query.getStartDate());
}
if (query.getEndDate() != null) {
spec = spec.bind("endDate", query.getEndDate());
}
return spec.map((row, meta) -> row.get(0, Long.class)).one();
}
}
@@ -1,42 +0,0 @@
package cn.novalon.gym.manage.groupcourse.dao;
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseRecommendEntity;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.repository.Modifying;
import org.springframework.data.r2dbc.repository.Query;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
@Repository
public interface GroupCourseRecommendDao extends R2dbcRepository<GroupCourseRecommendEntity, Long> {
Mono<GroupCourseRecommendEntity> findByIdAndDeletedAtIsNull(Long id);
Flux<GroupCourseRecommendEntity> findAllByDeletedAtIsNull();
Flux<GroupCourseRecommendEntity> findAllByDeletedAtIsNull(Sort sort);
Flux<GroupCourseRecommendEntity> findByCourseIdAndDeletedAtIsNull(Long courseId);
Mono<GroupCourseRecommendEntity> findByCourseIdAndDeletedAtIsNullAndIsActiveTrue(Long courseId);
Flux<GroupCourseRecommendEntity> findByIsActiveTrueAndDeletedAtIsNull();
Flux<GroupCourseRecommendEntity> findByIsActiveTrueAndDeletedAtIsNull(Sort sort);
@Modifying
@Query("UPDATE group_course_recommend SET is_active = :isActive, updated_at = :updatedAt WHERE id = :id AND deleted_at IS NULL")
Mono<Integer> updateActiveStatus(Long id, Boolean isActive, LocalDateTime updatedAt);
@Modifying
@Query("UPDATE group_course_recommend SET deleted_at = :deletedAt WHERE id = :id")
Mono<Integer> softDelete(Long id, LocalDateTime deletedAt);
@Modifying
@Query("UPDATE group_course_recommend SET deleted_at = :deletedAt WHERE course_id = :courseId")
Mono<Integer> softDeleteByCourseId(Long courseId, LocalDateTime deletedAt);
}
@@ -1,32 +0,0 @@
package cn.novalon.gym.manage.groupcourse.dao;
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseTypeEntity;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.repository.Modifying;
import org.springframework.data.r2dbc.repository.Query;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
@Repository
public interface GroupCourseTypeDao extends R2dbcRepository<GroupCourseTypeEntity, Long> {
Mono<GroupCourseTypeEntity> findByIdIsAndDeletedAtIsNull(Long id);
Flux<GroupCourseTypeEntity> findAllByDeletedAtIsNull();
Flux<GroupCourseTypeEntity> findAllByDeletedAtIsNull(Sort sort);
Flux<GroupCourseTypeEntity> findByTypeNameContainingAndDeletedAtIsNull(String typeName);
Flux<GroupCourseTypeEntity> findByCategoryAndDeletedAtIsNull(String category);
Mono<GroupCourseTypeEntity> findByTypeNameAndDeletedAtIsNull(String typeName);
@Modifying
@Query("UPDATE group_course_type SET deleted_at = :deletedAt WHERE id = :id")
Mono<Integer> softDelete(Long id, LocalDateTime deletedAt);
}
@@ -1,43 +0,0 @@
package cn.novalon.gym.manage.groupcourse.domain;
import cn.novalon.gym.manage.sys.core.domain.BaseDomain;
import io.swagger.v3.oas.annotations.media.Schema;
public class CourseLabel extends BaseDomain {
//标签名称
@Schema(description = "标签名称", example = "适合新手")
private String labelName;
//标签颜色(十六进制)
@Schema(description = "标签颜色(十六进制)", example = "#52c41a")
private String color;
//标签描述
@Schema(description = "标签描述", example = "适合健身初学者")
private String description;
public String getLabelName() {
return labelName;
}
public void setLabelName(String labelName) {
this.labelName = labelName;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
@@ -60,10 +60,6 @@ public class GroupCourse extends BaseDomain{
@Schema(description = "储值卡额度(消耗金额)", example = "50.00") @Schema(description = "储值卡额度(消耗金额)", example = "50.00")
private java.math.BigDecimal storedValueAmount; private java.math.BigDecimal storedValueAmount;
//二维码路径
@Schema(description = "二维码路径", example = "D:\\Games\\exmp\\image\\abc123_20260618120000.png")
private String qrCodePath;
public String getCourseName() { public String getCourseName() {
return courseName; return courseName;
} }
@@ -167,12 +163,4 @@ public class GroupCourse extends BaseDomain{
public void setStoredValueAmount(java.math.BigDecimal storedValueAmount) { public void setStoredValueAmount(java.math.BigDecimal storedValueAmount) {
this.storedValueAmount = storedValueAmount; this.storedValueAmount = storedValueAmount;
} }
public String getQrCodePath() {
return qrCodePath;
}
public void setQrCodePath(String qrCodePath) {
this.qrCodePath = qrCodePath;
}
} }
@@ -1,265 +0,0 @@
package cn.novalon.gym.manage.groupcourse.domain;
import cn.novalon.gym.manage.sys.core.domain.BaseDomain;
import io.swagger.v3.oas.annotations.media.Schema;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 团课完整信息领域模型
* 包含团课基础信息、关联的类型信息以及类型的标签信息
*/
public class GroupCourseDetail extends BaseDomain {
// ===== 团课基础信息 =====
@Schema(description = "课程名称", example = "瑜伽入门")
private String courseName;
@Schema(description = "教练ID", example = "1")
private Long coachId;
@Schema(description = "课程类型ID", example = "1")
private Long courseType;
@Schema(description = "开始时间", example = "2026-06-02T09:00:00")
private LocalDateTime startTime;
@Schema(description = "结束时间", example = "2026-06-02T10:00:00")
private LocalDateTime endTime;
@Schema(description = "最大参与人数", example = "20")
private Integer maxMembers;
@Schema(description = "当前参与人数", example = "15")
private Integer currentMembers;
@Schema(description = "课程状态", example = "0")
private Long status;
@Schema(description = "上课地点", example = "健身房A区")
private String location;
@Schema(description = "封面图URL", example = "https://example.com/yoga.jpg")
private String coverImage;
@Schema(description = "课程描述", example = "适合初学者的瑜伽课程")
private String description;
@Schema(description = "点卡额度(消耗次数)", example = "1")
private Integer pointCardAmount;
@Schema(description = "储值卡额度(消耗金额)", example = "50.00")
private BigDecimal storedValueAmount;
@Schema(description = "二维码路径", example = "D:\\Games\\exmp\\image\\abc123_20260618120000.png")
private String qrCodePath;
// ===== 关联的类型信息 =====
@Schema(description = "类型信息")
private GroupCourseType typeInfo;
// ===== 快捷访问属性(从类型信息派生)=====
@Schema(description = "类型名称", example = "瑜伽入门")
private String typeName;
@Schema(description = "类型分类", example = "柔韧与平衡类")
private String typeCategory;
@Schema(description = "基础难度", example = "2")
private Integer baseDifficulty;
@Schema(description = "难度等级描述", example = "初级")
private String difficultyLevel;
@Schema(description = "综合难度系数", example = "2")
private Integer calculatedDifficulty;
// ===== 标签信息(从类型标签派生)=====
@Schema(description = "标签列表")
private List<CourseLabel> labels;
// ===== Getters and Setters =====
public String getCourseName() {
return courseName;
}
public void setCourseName(String courseName) {
this.courseName = courseName;
}
public Long getCoachId() {
return coachId;
}
public void setCoachId(Long coachId) {
this.coachId = coachId;
}
public Long getCourseType() {
return courseType;
}
public void setCourseType(Long courseType) {
this.courseType = courseType;
}
public LocalDateTime getStartTime() {
return startTime;
}
public void setStartTime(LocalDateTime startTime) {
this.startTime = startTime;
}
public LocalDateTime getEndTime() {
return endTime;
}
public void setEndTime(LocalDateTime endTime) {
this.endTime = endTime;
}
public Integer getMaxMembers() {
return maxMembers;
}
public void setMaxMembers(Integer maxMembers) {
this.maxMembers = maxMembers;
}
public Integer getCurrentMembers() {
return currentMembers;
}
public void setCurrentMembers(Integer currentMembers) {
this.currentMembers = currentMembers;
}
public Long getStatus() {
return status;
}
public void setStatus(Long status) {
this.status = status;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public String getCoverImage() {
return coverImage;
}
public void setCoverImage(String coverImage) {
this.coverImage = coverImage;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Integer getPointCardAmount() {
return pointCardAmount;
}
public void setPointCardAmount(Integer pointCardAmount) {
this.pointCardAmount = pointCardAmount;
}
public BigDecimal getStoredValueAmount() {
return storedValueAmount;
}
public void setStoredValueAmount(BigDecimal storedValueAmount) {
this.storedValueAmount = storedValueAmount;
}
public String getQrCodePath() {
return qrCodePath;
}
public void setQrCodePath(String qrCodePath) {
this.qrCodePath = qrCodePath;
}
public GroupCourseType getTypeInfo() {
return typeInfo;
}
public void setTypeInfo(GroupCourseType typeInfo) {
this.typeInfo = typeInfo;
// 同步派生属性
if (typeInfo != null) {
this.typeName = typeInfo.getTypeName();
this.typeCategory = typeInfo.getCategory();
this.baseDifficulty = typeInfo.getBaseDifficulty();
this.difficultyLevel = typeInfo.getDifficultyLevel();
this.calculatedDifficulty = typeInfo.getCalculatedDifficulty();
this.labels = typeInfo.getLabels();
}
}
public String getTypeName() {
return typeName;
}
public void setTypeName(String typeName) {
this.typeName = typeName;
}
public String getTypeCategory() {
return typeCategory;
}
public void setTypeCategory(String typeCategory) {
this.typeCategory = typeCategory;
}
public Integer getBaseDifficulty() {
return baseDifficulty;
}
public void setBaseDifficulty(Integer baseDifficulty) {
this.baseDifficulty = baseDifficulty;
}
public String getDifficultyLevel() {
return difficultyLevel;
}
public void setDifficultyLevel(String difficultyLevel) {
this.difficultyLevel = difficultyLevel;
}
public Integer getCalculatedDifficulty() {
return calculatedDifficulty;
}
public void setCalculatedDifficulty(Integer calculatedDifficulty) {
this.calculatedDifficulty = calculatedDifficulty;
}
public List<CourseLabel> getLabels() {
return labels;
}
public void setLabels(List<CourseLabel> labels) {
this.labels = labels;
}
}
@@ -1,84 +0,0 @@
package cn.novalon.gym.manage.groupcourse.domain;
import cn.novalon.gym.manage.sys.core.domain.BaseDomain;
import io.swagger.v3.oas.annotations.media.Schema;
public class GroupCourseRecommend extends BaseDomain {
@Schema(description = "团课ID", example = "1")
private Long courseId;
@Schema(description = "推荐标题", example = "本周热门课程")
private String recommendTitle;
@Schema(description = "推荐内容", example = "这是一门非常棒的课程,快来参加吧!")
private String recommendContent;
@Schema(description = "推荐理由", example = "教练专业,课程内容丰富")
private String recommendReason;
@Schema(description = "优先级(数字越大优先级越高)", example = "10")
private Integer priority;
@Schema(description = "是否启用", example = "true")
private Boolean isActive;
@Schema(description = "团课信息")
private GroupCourse groupCourse;
public Long getCourseId() {
return courseId;
}
public void setCourseId(Long courseId) {
this.courseId = courseId;
}
public String getRecommendTitle() {
return recommendTitle;
}
public void setRecommendTitle(String recommendTitle) {
this.recommendTitle = recommendTitle;
}
public String getRecommendContent() {
return recommendContent;
}
public void setRecommendContent(String recommendContent) {
this.recommendContent = recommendContent;
}
public String getRecommendReason() {
return recommendReason;
}
public void setRecommendReason(String recommendReason) {
this.recommendReason = recommendReason;
}
public Integer getPriority() {
return priority;
}
public void setPriority(Integer priority) {
this.priority = priority;
}
public Boolean getIsActive() {
return isActive;
}
public void setIsActive(Boolean isActive) {
this.isActive = isActive;
}
public GroupCourse getGroupCourse() {
return groupCourse;
}
public void setGroupCourse(GroupCourse groupCourse) {
this.groupCourse = groupCourse;
}
}
@@ -1,113 +0,0 @@
package cn.novalon.gym.manage.groupcourse.domain;
import cn.novalon.gym.manage.sys.core.domain.BaseDomain;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.ArrayList;
import java.util.List;
public class GroupCourseType extends BaseDomain {
//类型名称
@Schema(description = "类型名称", example = "瑜伽入门")
private String typeName;
//基础难度(1-10
@Schema(description = "基础难度(1-10", example = "2")
private Integer baseDifficulty;
//类型描述
@Schema(description = "类型描述", example = "适合初学者的瑜伽课程")
private String description;
//分类(如:有氧、力量、柔韧等)
@Schema(description = "分类", example = "柔韧与平衡类")
private String category;
//标签列表
@Schema(description = "标签列表")
private List<CourseLabel> labels = new ArrayList<>();
/**
* 计算综合难度系数
*
* 当前实现仅返回基础难度,为后续扩展预留空间。
* 未来可扩展的影响因素包括:
* 1. 课程时长系数(时长越长难度越高)
* 2. 教练难度调整系数(教练可根据实际情况微调)
* 3. 会员等级适配系数(根据会员等级动态调整显示难度)
* 4. 课程强度系数(高强度课程难度加成)
*
* @return 综合难度系数(1-10
*/
@Schema(description = "综合难度系数(预留扩展字段)", example = "2")
public Integer getCalculatedDifficulty() {
// TODO: 预留扩展点 - 未来可在此处添加更多难度计算逻辑
// 例如:return calculateDynamicDifficulty(baseDifficulty, additionalFactors...);
return this.baseDifficulty != null ? this.baseDifficulty : 1;
}
/**
* 获取难度等级描述
* 将数字难度转换为友好的文字描述
*
* @return 难度等级描述
*/
@Schema(description = "难度等级描述", example = "初级")
public String getDifficultyLevel() {
if (baseDifficulty == null) {
return "未知";
}
if (baseDifficulty <= 2) {
return "初级";
} else if (baseDifficulty <= 4) {
return "中级";
} else if (baseDifficulty <= 6) {
return "中高级";
} else if (baseDifficulty <= 8) {
return "高级";
} else {
return "专家级";
}
}
public String getTypeName() {
return typeName;
}
public void setTypeName(String typeName) {
this.typeName = typeName;
}
public Integer getBaseDifficulty() {
return baseDifficulty;
}
public void setBaseDifficulty(Integer baseDifficulty) {
this.baseDifficulty = baseDifficulty;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public List<CourseLabel> getLabels() {
return labels;
}
public void setLabels(List<CourseLabel> labels) {
this.labels = labels;
}
}
@@ -1,116 +0,0 @@
package cn.novalon.gym.manage.groupcourse.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
/**
* 团课多条件查询请求DTO
*
* @author 张翔
* @date 2026-06-14
*/
@Schema(description = "团课多条件查询参数")
public class GroupCourseQueryDto {
@Schema(description = "课程名称(模糊查询)", example = "瑜伽")
private String courseName;
@Schema(description = "课程类型ID", example = "1")
private Long courseType;
@Schema(description = "查询开始日期", example = "2026-06-01T00:00:00")
private LocalDateTime startDate;
@Schema(description = "查询结束日期", example = "2026-06-30T23:59:59")
private LocalDateTime endDate;
@Schema(description = "时间段:morning-早晨(6:00-12:00), afternoon-下午(12:00-18:00), evening-夜晚(18:00-24:00)", example = "morning")
private String timePeriod;
@Schema(description = "价格排序:asc-从低到高, desc-从高到低", example = "asc")
private String priceSort;
@Schema(description = "按剩余名额最多排序", example = "true")
private Boolean remainingMost;
@Schema(description = "页码", example = "0")
private Integer page = 0;
@Schema(description = "每页大小", example = "10")
private Integer size = 10;
// ===== Getters and Setters =====
public String getCourseName() {
return courseName;
}
public void setCourseName(String courseName) {
this.courseName = courseName;
}
public Long getCourseType() {
return courseType;
}
public void setCourseType(Long courseType) {
this.courseType = courseType;
}
public LocalDateTime getStartDate() {
return startDate;
}
public void setStartDate(LocalDateTime startDate) {
this.startDate = startDate;
}
public LocalDateTime getEndDate() {
return endDate;
}
public void setEndDate(LocalDateTime endDate) {
this.endDate = endDate;
}
public String getTimePeriod() {
return timePeriod;
}
public void setTimePeriod(String timePeriod) {
this.timePeriod = timePeriod;
}
public String getPriceSort() {
return priceSort;
}
public void setPriceSort(String priceSort) {
this.priceSort = priceSort;
}
public Boolean getRemainingMost() {
return remainingMost;
}
public void setRemainingMost(Boolean remainingMost) {
this.remainingMost = remainingMost;
}
public Integer getPage() {
return page;
}
public void setPage(Integer page) {
this.page = page;
}
public Integer getSize() {
return size;
}
public void setSize(Integer size) {
this.size = size;
}
}
@@ -1,45 +0,0 @@
package cn.novalon.gym.manage.groupcourse.entity;
import cn.novalon.gym.manage.db.entity.BaseEntity;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
@Table("course_label")
public class CourseLabelEntity extends BaseEntity {
//标签名称
@Column("label_name")
private String labelName;
//标签颜色(十六进制)
@Column("color")
private String color;
//标签描述
@Column("description")
private String description;
public String getLabelName() {
return labelName;
}
public void setLabelName(String labelName) {
this.labelName = labelName;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
@@ -1,33 +0,0 @@
package cn.novalon.gym.manage.groupcourse.entity;
import cn.novalon.gym.manage.db.entity.BaseEntity;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
@Table("course_type_label")
public class CourseTypeLabelEntity extends BaseEntity {
//团课类型ID
@Column("type_id")
private Long typeId;
//标签ID
@Column("label_id")
private Long labelId;
public Long getTypeId() {
return typeId;
}
public void setTypeId(Long typeId) {
this.typeId = typeId;
}
public Long getLabelId() {
return labelId;
}
public void setLabelId(Long labelId) {
this.labelId = labelId;
}
}
@@ -62,10 +62,6 @@ public class GroupCourseEntity extends BaseEntity {
@Column("stored_value_amount") @Column("stored_value_amount")
private java.math.BigDecimal storedValueAmount; private java.math.BigDecimal storedValueAmount;
//二维码路径
@Column("qr_code_path")
private String qrCodePath;
public String getCourseName() { public String getCourseName() {
return courseName; return courseName;
} }
@@ -169,12 +165,4 @@ public class GroupCourseEntity extends BaseEntity {
public void setStoredValueAmount(java.math.BigDecimal storedValueAmount) { public void setStoredValueAmount(java.math.BigDecimal storedValueAmount) {
this.storedValueAmount = storedValueAmount; this.storedValueAmount = storedValueAmount;
} }
public String getQrCodePath() {
return qrCodePath;
}
public void setQrCodePath(String qrCodePath) {
this.qrCodePath = qrCodePath;
}
} }
@@ -1,77 +0,0 @@
package cn.novalon.gym.manage.groupcourse.entity;
import cn.novalon.gym.manage.db.entity.BaseEntity;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
import java.time.LocalDateTime;
@Table("group_course_recommend")
public class GroupCourseRecommendEntity extends BaseEntity {
@Column("course_id")
private Long courseId;
@Column("recommend_title")
private String recommendTitle;
@Column("recommend_content")
private String recommendContent;
@Column("recommend_reason")
private String recommendReason;
@Column("priority")
private Integer priority;
@Column("is_active")
private Boolean isActive;
public Long getCourseId() {
return courseId;
}
public void setCourseId(Long courseId) {
this.courseId = courseId;
}
public String getRecommendTitle() {
return recommendTitle;
}
public void setRecommendTitle(String recommendTitle) {
this.recommendTitle = recommendTitle;
}
public String getRecommendContent() {
return recommendContent;
}
public void setRecommendContent(String recommendContent) {
this.recommendContent = recommendContent;
}
public String getRecommendReason() {
return recommendReason;
}
public void setRecommendReason(String recommendReason) {
this.recommendReason = recommendReason;
}
public Integer getPriority() {
return priority;
}
public void setPriority(Integer priority) {
this.priority = priority;
}
public Boolean getIsActive() {
return isActive;
}
public void setIsActive(Boolean isActive) {
this.isActive = isActive;
}
}
@@ -1,57 +0,0 @@
package cn.novalon.gym.manage.groupcourse.entity;
import cn.novalon.gym.manage.db.entity.BaseEntity;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
@Table("group_course_type")
public class GroupCourseTypeEntity extends BaseEntity {
//类型名称
@Column("type_name")
private String typeName;
//基础难度(1-10
@Column("base_difficulty")
private Integer baseDifficulty;
//类型描述
@Column("description")
private String description;
//分类(如:有氧、力量、柔韧等)
@Column("category")
private String category;
public String getTypeName() {
return typeName;
}
public void setTypeName(String typeName) {
this.typeName = typeName;
}
public Integer getBaseDifficulty() {
return baseDifficulty;
}
public void setBaseDifficulty(Integer baseDifficulty) {
this.baseDifficulty = baseDifficulty;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
}
@@ -1,217 +0,0 @@
package cn.novalon.gym.manage.groupcourse.handler;
import cn.novalon.gym.manage.groupcourse.domain.CourseLabel;
import cn.novalon.gym.manage.groupcourse.service.ICourseLabelService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
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 java.util.HashMap;
import java.util.List;
import java.util.Map;
@Component
@Tag(name = "团课标签管理", description = "团课标签相关操作")
public class CourseLabelHandler {
private final ICourseLabelService courseLabelService;
public CourseLabelHandler(ICourseLabelService courseLabelService) {
this.courseLabelService = courseLabelService;
}
@Operation(summary = "获取所有标签", description = "获取系统中所有标签列表")
public Mono<ServerResponse> getAllLabels(ServerRequest request) {
return ServerResponse.ok()
.body(courseLabelService.findAll(), CourseLabel.class);
}
@Operation(summary = "根据ID获取标签", description = "根据ID获取标签详情")
public Mono<ServerResponse> getLabelById(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return courseLabelService.findById(id)
.flatMap(label -> ServerResponse.ok().bodyValue(label))
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "搜索标签", description = "根据关键词搜索标签")
public Mono<ServerResponse> searchLabels(ServerRequest request) {
String keyword = request.queryParam("keyword").orElse("");
return ServerResponse.ok()
.body(courseLabelService.findByKeyword(keyword), CourseLabel.class);
}
@Operation(summary = "创建标签", description = "创建新的标签")
public Mono<ServerResponse> createLabel(ServerRequest request) {
return request.bodyToMono(CourseLabel.class)
.flatMap(courseLabel -> {
if (courseLabel.getLabelName() == null || courseLabel.getLabelName().isEmpty()) {
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "标签名称不能为空");
return ServerResponse.badRequest().bodyValue(error);
}
if (courseLabel.getLabelName().length() > 50) {
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "标签名称不能超过50个字符");
return ServerResponse.badRequest().bodyValue(error);
}
if (courseLabel.getColor() == null || courseLabel.getColor().isEmpty()) {
courseLabel.setColor("#1890ff");
}
return courseLabelService.create(courseLabel)
.flatMap(label -> {
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "标签创建成功");
response.put("data", label);
return ServerResponse.ok().bodyValue(response);
})
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
});
}
@Operation(summary = "更新标签", description = "更新指定标签信息")
public Mono<ServerResponse> updateLabel(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(CourseLabel.class)
.flatMap(courseLabel -> {
if (courseLabel.getLabelName() != null && courseLabel.getLabelName().length() > 50) {
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "标签名称不能超过50个字符");
return ServerResponse.badRequest().bodyValue(error);
}
return courseLabelService.update(id, courseLabel)
.flatMap(label -> {
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "标签更新成功");
response.put("data", label);
return ServerResponse.ok().bodyValue(response);
})
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
});
}
@Operation(summary = "删除标签", description = "删除指定标签(软删除)")
public Mono<ServerResponse> deleteLabel(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return courseLabelService.delete(id)
.then(Mono.defer(() -> {
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "标签删除成功");
return ServerResponse.ok().bodyValue(response);
}))
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
}
@Operation(summary = "获取类型的标签", description = "获取指定团课类型的所有标签")
public Mono<ServerResponse> getLabelsByTypeId(ServerRequest request) {
Long typeId = Long.valueOf(request.pathVariable("typeId"));
return courseLabelService.findByTypeId(typeId)
.collectList()
.flatMap(list -> ServerResponse.ok().bodyValue(list));
}
@Operation(summary = "为类型添加标签", description = "为指定团课类型添加标签")
public Mono<ServerResponse> addLabelsToType(ServerRequest request) {
Long typeId = Long.valueOf(request.pathVariable("typeId"));
return request.bodyToMono(Map.class)
.flatMap(body -> {
@SuppressWarnings("unchecked")
List<Integer> labelIdsInt = (List<Integer>) body.get("labelIds");
if (labelIdsInt == null || labelIdsInt.isEmpty()) {
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "labelIds不能为空");
return ServerResponse.badRequest().bodyValue(error);
}
List<Long> labelIds = labelIdsInt.stream()
.map(Integer::longValue)
.collect(java.util.stream.Collectors.toList());
return courseLabelService.addLabelsToType(typeId, labelIds)
.then(Mono.defer(() -> {
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "标签添加成功");
return ServerResponse.ok().bodyValue(response);
}))
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
});
}
@Operation(summary = "从类型移除标签", description = "从指定团课类型移除标签")
public Mono<ServerResponse> removeLabelFromType(ServerRequest request) {
Long typeId = Long.valueOf(request.pathVariable("typeId"));
Long labelId = Long.valueOf(request.pathVariable("labelId"));
return courseLabelService.removeLabelFromType(typeId, labelId)
.then(Mono.defer(() -> {
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "标签移除成功");
return ServerResponse.ok().bodyValue(response);
}))
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
}
@Operation(summary = "清空类型标签", description = "清空指定团课类型的所有标签")
public Mono<ServerResponse> clearLabelsFromType(ServerRequest request) {
Long typeId = Long.valueOf(request.pathVariable("typeId"));
return courseLabelService.clearLabelsFromType(typeId)
.then(Mono.defer(() -> {
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "标签清空成功");
return ServerResponse.ok().bodyValue(response);
}))
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
}
}
@@ -4,8 +4,6 @@ package cn.novalon.gym.manage.groupcourse.handler;
import cn.novalon.gym.manage.common.dto.PageRequest; import cn.novalon.gym.manage.common.dto.PageRequest;
import cn.novalon.gym.manage.common.util.RedisUtil; import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse; import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseDetail;
import cn.novalon.gym.manage.groupcourse.dto.GroupCourseQueryDto;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseService; import cn.novalon.gym.manage.groupcourse.service.IGroupCourseService;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@@ -78,14 +76,6 @@ public class GroupCourseHandler {
.switchIfEmpty(ServerResponse.notFound().build()); .switchIfEmpty(ServerResponse.notFound().build());
} }
@Operation(summary = "根据ID获取团课完整信息", description = "根据ID获取团课完整信息,包括团课基础信息、类型信息和标签信息")
public Mono<ServerResponse> getGroupCourseDetailById(ServerRequest request){
Long id = Long.valueOf(request.pathVariable("id"));
return groupCourseService.findDetailById(id)
.flatMap(detail -> ServerResponse.ok().bodyValue(detail))
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "创建团课", description = "创建新的团课") @Operation(summary = "创建团课", description = "创建新的团课")
public Mono<ServerResponse> createGroupCourse(ServerRequest request) { public Mono<ServerResponse> createGroupCourse(ServerRequest request) {
return request.bodyToMono(GroupCourse.class) return request.bodyToMono(GroupCourse.class)
@@ -159,7 +149,7 @@ public class GroupCourseHandler {
@Operation(summary = "团课签到", description = "会员签到参加团课") @Operation(summary = "团课签到", description = "会员签到参加团课")
public Mono<ServerResponse> signIn(ServerRequest request) { public Mono<ServerResponse> signIn(ServerRequest request) {
Long memberId = Long.valueOf(request.pathVariable("memberId")); Long courseId = Long.valueOf(request.pathVariable("courseId"));
return request.bodyToMono(Map.class) return request.bodyToMono(Map.class)
.flatMap(body -> { .flatMap(body -> {
@@ -170,15 +160,15 @@ public class GroupCourseHandler {
return ServerResponse.badRequest().bodyValue(response); return ServerResponse.badRequest().bodyValue(response);
} }
Object courseIdObj = body.get("courseId"); Object memberIdObj = body.get("memberId");
if (courseIdObj == null) { if (memberIdObj == null) {
Map<String, Object> response = new HashMap<>(); Map<String, Object> response = new HashMap<>();
response.put("success", false); response.put("success", false);
response.put("message", "courseId不能为空"); response.put("message", "memberId不能为空");
return ServerResponse.badRequest().bodyValue(response); return ServerResponse.badRequest().bodyValue(response);
} }
Long courseId = ((Number) courseIdObj).longValue(); Long memberId = ((Number) memberIdObj).longValue();
return groupCourseService.signIn(courseId, memberId) return groupCourseService.signIn(courseId, memberId)
.flatMap(course -> { .flatMap(course -> {
@@ -216,27 +206,6 @@ public class GroupCourseHandler {
}); });
} }
@Operation(summary = "多条件查询团课", description = "支持团课名称模糊查询、类型筛选、日期范围、时间段、价格排序、剩余名额排序等多条件组合查询")
public Mono<ServerResponse> searchGroupCourses(ServerRequest request) {
return request.bodyToMono(GroupCourseQueryDto.class)
.flatMap(query -> {
return groupCourseService.searchGroupCourses(query)
.flatMap(response -> {
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "查询成功");
result.put("data", response);
return ServerResponse.ok().bodyValue(result);
});
})
.onErrorResume(error -> {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(errorResponse);
});
}
@Operation(summary = "测试-根据Key获取Redis缓存", description = "测试接口:根据传入的key值获取Redis中缓存的数据") @Operation(summary = "测试-根据Key获取Redis缓存", description = "测试接口:根据传入的key值获取Redis中缓存的数据")
public Mono<ServerResponse> getCacheByKey(ServerRequest request) { public Mono<ServerResponse> getCacheByKey(ServerRequest request) {
return request.bodyToMono(Map.class) return request.bodyToMono(Map.class)
@@ -1,164 +0,0 @@
package cn.novalon.gym.manage.groupcourse.handler;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseRecommend;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseRecommendService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
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 java.util.HashMap;
import java.util.Map;
@Component
@Tag(name = "团课推荐管理", description = "团课推荐相关操作")
public class GroupCourseRecommendHandler {
private final IGroupCourseRecommendService recommendService;
public GroupCourseRecommendHandler(IGroupCourseRecommendService recommendService) {
this.recommendService = recommendService;
}
@Operation(summary = "获取所有团课推荐", description = "获取系统中所有团课推荐列表,支持按优先级排序")
public Mono<ServerResponse> getAllRecommendations(ServerRequest request) {
String sortBy = request.queryParam("sortBy").orElse("priority");
String sortOrder = request.queryParam("sortOrder").orElse("desc");
return ServerResponse.ok()
.body(recommendService.findAll(sortBy, sortOrder), GroupCourseRecommend.class);
}
@Operation(summary = "获取所有启用的团课推荐", description = "获取系统中所有已启用的团课推荐列表(按优先级排序)")
public Mono<ServerResponse> getAllActiveRecommendations(ServerRequest request) {
return ServerResponse.ok()
.body(recommendService.findAllActive(), GroupCourseRecommend.class);
}
@Operation(summary = "根据ID获取团课推荐", description = "根据ID获取团课推荐详情")
public Mono<ServerResponse> getRecommendationById(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return recommendService.findById(id)
.flatMap(recommend -> ServerResponse.ok().bodyValue(recommend))
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "根据团课ID获取推荐", description = "根据团课ID获取该团课的推荐信息")
public Mono<ServerResponse> getRecommendationsByCourseId(ServerRequest request) {
Long courseId = Long.valueOf(request.pathVariable("courseId"));
return ServerResponse.ok()
.body(recommendService.findByCourseId(courseId), GroupCourseRecommend.class);
}
@Operation(summary = "创建团课推荐", description = "创建新的团课推荐")
public Mono<ServerResponse> createRecommendation(ServerRequest request) {
return request.bodyToMono(GroupCourseRecommend.class)
.flatMap(recommend -> {
if (recommend.getCourseId() == null) {
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "团课ID不能为空");
return ServerResponse.badRequest().bodyValue(error);
}
return recommendService.create(recommend)
.flatMap(r -> {
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "团课推荐创建成功");
response.put("data", r);
return ServerResponse.ok().bodyValue(response);
})
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
});
}
@Operation(summary = "更新团课推荐", description = "更新指定团课推荐信息")
public Mono<ServerResponse> updateRecommendation(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(GroupCourseRecommend.class)
.flatMap(recommend -> {
return recommendService.update(id, recommend)
.flatMap(r -> {
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "团课推荐更新成功");
response.put("data", r);
return ServerResponse.ok().bodyValue(response);
})
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
});
}
@Operation(summary = "删除团课推荐", description = "删除指定团课推荐(软删除)")
public Mono<ServerResponse> deleteRecommendation(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return recommendService.delete(id)
.then(Mono.defer(() -> {
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "团课推荐删除成功");
return ServerResponse.ok().bodyValue(response);
}))
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
}
@Operation(summary = "启用团课推荐", description = "启用指定团课推荐")
public Mono<ServerResponse> enableRecommendation(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return recommendService.enable(id)
.flatMap(r -> {
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "团课推荐启用成功");
response.put("data", r);
return ServerResponse.ok().bodyValue(response);
})
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
}
@Operation(summary = "禁用团课推荐", description = "禁用指定团课推荐")
public Mono<ServerResponse> disableRecommendation(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return recommendService.disable(id)
.flatMap(r -> {
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "团课推荐禁用成功");
response.put("data", r);
return ServerResponse.ok().bodyValue(response);
})
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
}
}
@@ -1,137 +0,0 @@
package cn.novalon.gym.manage.groupcourse.handler;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseType;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseTypeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
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 java.util.HashMap;
import java.util.Map;
@Component
@Tag(name = "团课类型管理", description = "团课类型及难度相关操作")
public class GroupCourseTypeHandler {
private final IGroupCourseTypeService groupCourseTypeService;
public GroupCourseTypeHandler(IGroupCourseTypeService groupCourseTypeService) {
this.groupCourseTypeService = groupCourseTypeService;
}
@Operation(summary = "获取所有团课类型", description = "获取系统中所有团课类型列表")
public Mono<ServerResponse> getAllGroupCourseTypes(ServerRequest request) {
boolean includeDeleted = Boolean.valueOf(request.queryParam("includeDeleted").orElse("false"));
return ServerResponse.ok()
.body(groupCourseTypeService.findAll(includeDeleted), GroupCourseType.class);
}
@Operation(summary = "根据ID获取团课类型", description = "根据ID获取团课类型详情")
public Mono<ServerResponse> getGroupCourseTypeById(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return groupCourseTypeService.findById(id)
.flatMap(type -> ServerResponse.ok().bodyValue(type))
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "根据关键词搜索团课类型", description = "根据类型名称关键词搜索团课类型")
public Mono<ServerResponse> searchGroupCourseTypes(ServerRequest request) {
String keyword = request.queryParam("keyword").orElse("");
return ServerResponse.ok()
.body(groupCourseTypeService.findByKeyword(keyword), GroupCourseType.class);
}
@Operation(summary = "根据分类获取团课类型", description = "根据分类获取团课类型列表")
public Mono<ServerResponse> getGroupCourseTypesByCategory(ServerRequest request) {
String category = request.pathVariable("category");
String keyword = request.queryParam("keyword").orElse("");
return ServerResponse.ok()
.body(groupCourseTypeService.findByCategoryAndKeyword(category, keyword), GroupCourseType.class);
}
@Operation(summary = "获取所有分类", description = "获取所有团课类型分类(去重)")
public Mono<ServerResponse> getCategories(ServerRequest request) {
return groupCourseTypeService.findCategories()
.collectList()
.flatMap(list -> ServerResponse.ok().bodyValue(list));
}
@Operation(summary = "创建团课类型", description = "创建新的团课类型")
public Mono<ServerResponse> createGroupCourseType(ServerRequest request) {
return request.bodyToMono(GroupCourseType.class)
.flatMap(groupCourseType -> {
if (groupCourseType.getTypeName() == null || groupCourseType.getTypeName().isEmpty()) {
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "类型名称不能为空");
return ServerResponse.badRequest().bodyValue(error);
}
// 默认基础难度为1
if (groupCourseType.getBaseDifficulty() == null) {
groupCourseType.setBaseDifficulty(1);
}
return groupCourseTypeService.create(groupCourseType)
.flatMap(type -> {
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "团课类型创建成功");
response.put("data", type);
return ServerResponse.ok().bodyValue(response);
})
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
});
}
@Operation(summary = "更新团课类型", description = "更新指定团课类型信息")
public Mono<ServerResponse> updateGroupCourseType(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(GroupCourseType.class)
.flatMap(groupCourseType -> {
groupCourseType.setId(id);
return groupCourseTypeService.update(id, groupCourseType)
.flatMap(type -> {
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "团课类型更新成功");
response.put("data", type);
return ServerResponse.ok().bodyValue(response);
})
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
});
}
@Operation(summary = "删除团课类型", description = "删除指定团课类型(软删除)")
public Mono<ServerResponse> deleteGroupCourseType(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return groupCourseTypeService.delete(id)
.then(Mono.defer(() -> {
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "团课类型删除成功");
return ServerResponse.ok().bodyValue(response);
}))
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(response);
});
}
}
@@ -1,89 +0,0 @@
package cn.novalon.gym.manage.groupcourse.initializer;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository;
import cn.novalon.gym.manage.groupcourse.util.QRCodeUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 项目启动时补全缺失的团课二维码
* 遍历所有未删除的团课,对qrCodePath为空的课程生成二维码并上传至阿里云OSS
*/
@Component
public class QrCodeInitializer implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(QrCodeInitializer.class);
private final IGroupCourseRepository groupCourseRepository;
private final ObjectMapper objectMapper;
public QrCodeInitializer(IGroupCourseRepository groupCourseRepository,
ObjectMapper objectMapper) {
this.groupCourseRepository = groupCourseRepository;
this.objectMapper = objectMapper;
}
@Override
public void run(String... args) {
logger.info("===== 开始检查团课二维码缺失情况 =====");
groupCourseRepository.findByDeletedAtIsNull()
.filter(course -> course.getQrCodePath() == null || course.getQrCodePath().isEmpty())
.flatMap(course -> {
try {
logger.info("发现缺失二维码的团课 - id={}, name={}", course.getId(), course.getCourseName());
// 生成二维码内容:团课基础信息JSON
Map<String, Object> qrCodeContent = new HashMap<>();
qrCodeContent.put("id", course.getId());
qrCodeContent.put("courseName", course.getCourseName());
qrCodeContent.put("coachId", course.getCoachId());
qrCodeContent.put("courseType", course.getCourseType());
qrCodeContent.put("startTime", course.getStartTime() != null ? course.getStartTime().toString() : null);
qrCodeContent.put("endTime", course.getEndTime() != null ? course.getEndTime().toString() : null);
qrCodeContent.put("maxMembers", course.getMaxMembers());
qrCodeContent.put("location", course.getLocation());
qrCodeContent.put("description", course.getDescription());
String jsonContent = objectMapper.writeValueAsString(qrCodeContent);
// 生成二维码并上传到阿里云OSS
String uuid = UUID.randomUUID().toString().replace("-", "");
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
String fileName = "qr_" + uuid + "_" + timestamp + ".png";
String ossUrl = QRCodeUtil.generateQRCodeAndUploadToOSS(jsonContent, fileName);
course.setQrCodePath(ossUrl);
// 更新数据库
return groupCourseRepository.update(course)
.doOnSuccess(updated -> logger.info("团课二维码补全成功 - id={}, name={}, ossUrl={}",
updated.getId(), updated.getCourseName(), ossUrl))
.doOnError(error -> logger.error("团课二维码补全失败(更新DB) - id={}, name={}, error: {}",
course.getId(), course.getCourseName(), error.getMessage()));
} catch (Exception e) {
logger.error("团课二维码补全失败(生成) - id={}, name={}, error: {}",
course.getId(), course.getCourseName(), e.getMessage(), e);
return Mono.empty();
}
})
.collectList()
.doOnSuccess(list -> logger.info("===== 团课二维码检查完毕,共补全 {} 个缺失二维码 =====", list.size()))
.onErrorResume(error -> {
logger.error("团课二维码初始化检查异常: {}", error.getMessage(), error);
return Mono.empty();
})
.subscribe();
}
}
@@ -1,32 +0,0 @@
package cn.novalon.gym.manage.groupcourse.repository;
import cn.novalon.gym.manage.groupcourse.domain.CourseLabel;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
public interface ICourseLabelRepository {
Mono<CourseLabel> findById(Long id);
Flux<CourseLabel> findAll();
Flux<CourseLabel> findByKeyword(String keyword);
Mono<CourseLabel> findByLabelName(String labelName);
Mono<CourseLabel> save(CourseLabel courseLabel);
Mono<CourseLabel> update(CourseLabel courseLabel);
Mono<Void> deleteById(Long id);
Flux<CourseLabel> findByTypeId(Long typeId);
Mono<Void> addLabelsToType(Long typeId, List<Long> labelIds);
Mono<Void> removeLabelFromType(Long typeId, Long labelId);
Mono<Void> clearLabelsFromType(Long typeId);
}
@@ -1,30 +0,0 @@
package cn.novalon.gym.manage.groupcourse.repository;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseRecommend;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface IGroupCourseRecommendRepository {
Mono<GroupCourseRecommend> findById(Long id);
Flux<GroupCourseRecommend> findAll();
Flux<GroupCourseRecommend> findAll(String sortBy, String sortOrder);
Flux<GroupCourseRecommend> findAllActive();
Flux<GroupCourseRecommend> findByCourseId(Long courseId);
Mono<GroupCourseRecommend> findActiveByCourseId(Long courseId);
Mono<GroupCourseRecommend> save(GroupCourseRecommend recommend);
Mono<GroupCourseRecommend> update(GroupCourseRecommend recommend);
Mono<Void> deleteById(Long id);
Mono<Void> deleteByCourseId(Long courseId);
Mono<GroupCourseRecommend> updateActiveStatus(Long id, Boolean isActive);
}
@@ -4,7 +4,6 @@ package cn.novalon.gym.manage.groupcourse.repository;
import cn.novalon.gym.manage.common.dto.PageRequest; import cn.novalon.gym.manage.common.dto.PageRequest;
import cn.novalon.gym.manage.common.dto.PageResponse; import cn.novalon.gym.manage.common.dto.PageResponse;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse; import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.dto.GroupCourseQueryDto;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@@ -28,8 +27,4 @@ public interface IGroupCourseRepository {
Mono<Void> deleteById(Long id); Mono<Void> deleteById(Long id);
Mono<GroupCourse> updateCurrentMembers(Long id, Integer delta); Mono<GroupCourse> updateCurrentMembers(Long id, Integer delta);
Flux<GroupCourse> findByCourseType(Long courseType);
Mono<PageResponse<GroupCourse>> searchGroupCourses(GroupCourseQueryDto query);
} }
@@ -1,28 +0,0 @@
package cn.novalon.gym.manage.groupcourse.repository;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseType;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface IGroupCourseTypeRepository {
Mono<GroupCourseType> findById(Long id);
Flux<GroupCourseType> findAll();
Flux<GroupCourseType> findAll(boolean includeDeleted);
Flux<GroupCourseType> findByKeyword(String keyword);
Flux<GroupCourseType> findByCategory(String category);
Flux<GroupCourseType> findByCategoryAndKeyword(String category, String keyword);
Mono<GroupCourseType> findByTypeName(String typeName);
Mono<GroupCourseType> save(GroupCourseType groupCourseType);
Mono<GroupCourseType> update(GroupCourseType groupCourseType);
Mono<Void> deleteById(Long id);
}
@@ -1,161 +0,0 @@
package cn.novalon.gym.manage.groupcourse.repository.impl;
import cn.novalon.gym.manage.groupcourse.converter.GroupCourseConverter;
import cn.novalon.gym.manage.groupcourse.dao.CourseLabelDao;
import cn.novalon.gym.manage.groupcourse.dao.CourseTypeLabelDao;
import cn.novalon.gym.manage.groupcourse.domain.CourseLabel;
import cn.novalon.gym.manage.groupcourse.entity.CourseLabelEntity;
import cn.novalon.gym.manage.groupcourse.entity.CourseTypeLabelEntity;
import cn.novalon.gym.manage.groupcourse.repository.ICourseLabelRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.util.List;
@Repository
@Transactional
public class CourseLabelRepository implements ICourseLabelRepository {
private static final Logger logger = LoggerFactory.getLogger(CourseLabelRepository.class);
private final CourseLabelDao courseLabelDao;
private final CourseTypeLabelDao courseTypeLabelDao;
private final GroupCourseConverter converter;
public CourseLabelRepository(CourseLabelDao courseLabelDao, CourseTypeLabelDao courseTypeLabelDao,
GroupCourseConverter converter) {
this.courseLabelDao = courseLabelDao;
this.courseTypeLabelDao = courseTypeLabelDao;
this.converter = converter;
}
@Override
public Mono<CourseLabel> findById(Long id) {
return courseLabelDao.findByIdIsAndDeletedAtIsNull(id)
.map(this::toCourseLabel);
}
@Override
public Flux<CourseLabel> findAll() {
return courseLabelDao.findAllByDeletedAtIsNull()
.map(this::toCourseLabel);
}
@Override
public Flux<CourseLabel> findByKeyword(String keyword) {
if (keyword == null || keyword.isEmpty()) {
return findAll();
}
return courseLabelDao.findByLabelNameContainingAndDeletedAtIsNull(keyword)
.map(this::toCourseLabel);
}
@Override
public Mono<CourseLabel> findByLabelName(String labelName) {
return courseLabelDao.findByLabelNameAndDeletedAtIsNull(labelName)
.map(this::toCourseLabel);
}
@Override
public Mono<CourseLabel> save(CourseLabel courseLabel) {
CourseLabelEntity entity = toCourseLabelEntity(courseLabel);
return courseLabelDao.save(entity)
.map(this::toCourseLabel);
}
@Override
public Mono<CourseLabel> update(CourseLabel courseLabel) {
return courseLabelDao.findByIdIsAndDeletedAtIsNull(courseLabel.getId())
.switchIfEmpty(Mono.error(new RuntimeException("标签不存在")))
.flatMap(existing -> {
existing.markNotNew();
if (courseLabel.getLabelName() != null) {
existing.setLabelName(courseLabel.getLabelName());
}
if (courseLabel.getColor() != null) {
existing.setColor(courseLabel.getColor());
}
if (courseLabel.getDescription() != null) {
existing.setDescription(courseLabel.getDescription());
}
existing.setUpdatedAt(LocalDateTime.now());
return courseLabelDao.save(existing);
})
.map(this::toCourseLabel);
}
@Override
public Mono<Void> deleteById(Long id) {
return courseLabelDao.softDelete(id, LocalDateTime.now())
.then(courseTypeLabelDao.deleteByLabelId(id, LocalDateTime.now()))
.then();
}
@Override
public Flux<CourseLabel> findByTypeId(Long typeId) {
return courseTypeLabelDao.findByTypeIdAndDeletedAtIsNull(typeId)
.flatMap(typeLabel -> courseLabelDao.findByIdIsAndDeletedAtIsNull(typeLabel.getLabelId()))
.map(this::toCourseLabel);
}
@Override
public Mono<Void> addLabelsToType(Long typeId, List<Long> labelIds) {
return Flux.fromIterable(labelIds)
.flatMap(labelId -> {
return courseTypeLabelDao.physicalDeleteByTypeIdAndLabelId(typeId, labelId)
.then(Mono.defer(() -> {
CourseTypeLabelEntity entity = new CourseTypeLabelEntity();
entity.setTypeId(typeId);
entity.setLabelId(labelId);
return courseTypeLabelDao.save(entity).then(Mono.empty());
}));
})
.then();
}
@Override
public Mono<Void> removeLabelFromType(Long typeId, Long labelId) {
return courseTypeLabelDao.deleteByTypeIdAndLabelId(typeId, labelId, LocalDateTime.now())
.then();
}
@Override
public Mono<Void> clearLabelsFromType(Long typeId) {
return courseTypeLabelDao.deleteByTypeId(typeId, LocalDateTime.now())
.then();
}
private CourseLabel toCourseLabel(CourseLabelEntity entity) {
if (entity == null) {
return null;
}
CourseLabel label = new CourseLabel();
label.setId(entity.getId());
label.setLabelName(entity.getLabelName());
label.setColor(entity.getColor());
label.setDescription(entity.getDescription());
label.setCreatedAt(entity.getCreatedAt());
label.setUpdatedAt(entity.getUpdatedAt());
return label;
}
private CourseLabelEntity toCourseLabelEntity(CourseLabel domain) {
if (domain == null) {
return null;
}
CourseLabelEntity entity = new CourseLabelEntity();
entity.setId(domain.getId());
entity.setLabelName(domain.getLabelName());
entity.setColor(domain.getColor());
entity.setDescription(domain.getDescription());
if (domain.getId() != null) {
entity.markNotNew();
}
return entity;
}
}
@@ -1,136 +0,0 @@
package cn.novalon.gym.manage.groupcourse.repository.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseRecommendDao;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseRecommend;
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseRecommendEntity;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRecommendRepository;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
@Repository
public class GroupCourseRecommendRepository implements IGroupCourseRecommendRepository {
private final GroupCourseRecommendDao recommendDao;
private final R2dbcEntityTemplate r2dbcEntityTemplate;
public GroupCourseRecommendRepository(GroupCourseRecommendDao recommendDao,
R2dbcEntityTemplate r2dbcEntityTemplate) {
this.recommendDao = recommendDao;
this.r2dbcEntityTemplate = r2dbcEntityTemplate;
}
@Override
public Mono<GroupCourseRecommend> findById(Long id) {
return recommendDao.findByIdAndDeletedAtIsNull(id)
.map(this::toDomain);
}
@Override
public Flux<GroupCourseRecommend> findAll() {
return recommendDao.findAllByDeletedAtIsNull()
.map(this::toDomain);
}
@Override
public Flux<GroupCourseRecommend> findAll(String sortBy, String sortOrder) {
Sort.Direction direction = "asc".equalsIgnoreCase(sortOrder)
? Sort.Direction.ASC
: Sort.Direction.DESC;
Sort sort = Sort.by(direction, sortBy);
return recommendDao.findAllByDeletedAtIsNull(sort)
.map(this::toDomain);
}
@Override
public Flux<GroupCourseRecommend> findAllActive() {
return recommendDao.findByIsActiveTrueAndDeletedAtIsNull(Sort.by(Sort.Direction.DESC, "priority"))
.map(this::toDomain);
}
@Override
public Flux<GroupCourseRecommend> findByCourseId(Long courseId) {
return recommendDao.findByCourseIdAndDeletedAtIsNull(courseId)
.map(this::toDomain);
}
@Override
public Mono<GroupCourseRecommend> findActiveByCourseId(Long courseId) {
return recommendDao.findByCourseIdAndDeletedAtIsNullAndIsActiveTrue(courseId)
.map(this::toDomain);
}
@Override
public Mono<GroupCourseRecommend> save(GroupCourseRecommend recommend) {
GroupCourseRecommendEntity entity = toEntity(recommend);
entity.setCreatedAt(LocalDateTime.now());
entity.setUpdatedAt(LocalDateTime.now());
if (entity.getPriority() == null) {
entity.setPriority(0);
}
if (entity.getIsActive() == null) {
entity.setIsActive(true);
}
return recommendDao.save(entity)
.map(this::toDomain);
}
@Override
public Mono<GroupCourseRecommend> update(GroupCourseRecommend recommend) {
GroupCourseRecommendEntity entity = toEntity(recommend);
entity.setUpdatedAt(LocalDateTime.now());
return r2dbcEntityTemplate.update(entity)
.then(findById(recommend.getId()));
}
@Override
public Mono<Void> deleteById(Long id) {
return recommendDao.softDelete(id, LocalDateTime.now())
.then();
}
@Override
public Mono<Void> deleteByCourseId(Long courseId) {
return recommendDao.softDeleteByCourseId(courseId, LocalDateTime.now())
.then();
}
@Override
public Mono<GroupCourseRecommend> updateActiveStatus(Long id, Boolean isActive) {
return recommendDao.updateActiveStatus(id, isActive, LocalDateTime.now())
.flatMap(updated -> {
if (updated > 0) {
return findById(id);
}
return Mono.empty();
});
}
private GroupCourseRecommend toDomain(GroupCourseRecommendEntity entity) {
if (entity == null) {
return null;
}
GroupCourseRecommend recommend = new GroupCourseRecommend();
BeanUtil.copyProperties(entity, recommend);
return recommend;
}
private GroupCourseRecommendEntity toEntity(GroupCourseRecommend domain) {
if (domain == null) {
return null;
}
GroupCourseRecommendEntity entity = new GroupCourseRecommendEntity();
BeanUtil.copyProperties(domain, entity);
if (domain.getId() != null) {
entity.markNotNew();
}
return entity;
}
}
@@ -6,7 +6,6 @@ import cn.novalon.gym.manage.common.dto.PageResponse;
import cn.novalon.gym.manage.groupcourse.converter.GroupCourseConverter; import cn.novalon.gym.manage.groupcourse.converter.GroupCourseConverter;
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseDao; import cn.novalon.gym.manage.groupcourse.dao.GroupCourseDao;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse; import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.dto.GroupCourseQueryDto;
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseEntity; import cn.novalon.gym.manage.groupcourse.entity.GroupCourseEntity;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository; import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
@@ -179,33 +178,4 @@ public class GroupCourseRepository implements IGroupCourseRepository {
return Mono.empty(); return Mono.empty();
}); });
} }
@Override
public Flux<GroupCourse> findByCourseType(Long courseType) {
return groupCourseDao.findByCourseTypeAndDeletedAtIsNull(courseType)
.map(groupCourseConverter::toDomain);
}
@Override
public Mono<PageResponse<GroupCourse>> searchGroupCourses(GroupCourseQueryDto query) {
return groupCourseDao.countSearchGroupCourses(r2dbcEntityTemplate.getDatabaseClient(), query)
.flatMap(total -> {
if (total == 0) {
return Mono.just(new PageResponse<>(
List.of(), 0, 0L,
query.getPage() != null ? query.getPage() : 0,
query.getSize() != null ? query.getSize() : 10));
}
return groupCourseDao.searchGroupCourses(r2dbcEntityTemplate.getDatabaseClient(), query)
.map(groupCourseConverter::toDomain)
.collectList()
.map(courseList -> {
int size = query.getSize() != null ? query.getSize() : 10;
int totalPages = (int) Math.ceil((double) total / size);
return new PageResponse<>(
courseList, totalPages, total,
query.getPage() != null ? query.getPage() : 0, size);
});
});
}
} }
@@ -1,132 +0,0 @@
package cn.novalon.gym.manage.groupcourse.repository.impl;
import cn.novalon.gym.manage.groupcourse.converter.GroupCourseConverter;
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseTypeDao;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseType;
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseTypeEntity;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseTypeRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
@Repository
@Transactional
public class GroupCourseTypeRepository implements IGroupCourseTypeRepository {
private static final Logger logger = LoggerFactory.getLogger(GroupCourseTypeRepository.class);
private final GroupCourseTypeDao groupCourseTypeDao;
private final GroupCourseConverter converter;
public GroupCourseTypeRepository(GroupCourseTypeDao groupCourseTypeDao, GroupCourseConverter converter) {
this.groupCourseTypeDao = groupCourseTypeDao;
this.converter = converter;
}
@Override
public Mono<GroupCourseType> findById(Long id) {
return groupCourseTypeDao.findByIdIsAndDeletedAtIsNull(id)
.map(converter::toGroupCourseType);
}
@Override
public Flux<GroupCourseType> findAll() {
return groupCourseTypeDao.findAll()
.map(converter::toGroupCourseType);
}
@Override
public Flux<GroupCourseType> findAll(boolean includeDeleted) {
if (includeDeleted) {
return groupCourseTypeDao.findAll()
.map(converter::toGroupCourseType);
} else {
return groupCourseTypeDao.findAllByDeletedAtIsNull()
.map(converter::toGroupCourseType);
}
}
@Override
public Flux<GroupCourseType> findByKeyword(String keyword) {
if (keyword == null || keyword.isEmpty()) {
return findAll(false);
}
return groupCourseTypeDao.findByTypeNameContainingAndDeletedAtIsNull(keyword)
.map(converter::toGroupCourseType);
}
@Override
public Flux<GroupCourseType> findByCategory(String category) {
if (category == null || category.isEmpty()) {
return findAll(false);
}
return groupCourseTypeDao.findByCategoryAndDeletedAtIsNull(category)
.map(converter::toGroupCourseType);
}
@Override
public Flux<GroupCourseType> findByCategoryAndKeyword(String category, String keyword) {
Flux<GroupCourseType> result;
if (category != null && !category.isEmpty()) {
result = findByCategory(category);
} else {
result = findAll(false);
}
if (keyword != null && !keyword.isEmpty()) {
result = result.filter(type -> type.getTypeName() != null &&
type.getTypeName().toLowerCase().contains(keyword.toLowerCase()));
}
return result;
}
@Override
public Mono<GroupCourseType> findByTypeName(String typeName) {
return groupCourseTypeDao.findByTypeNameAndDeletedAtIsNull(typeName)
.map(converter::toGroupCourseType);
}
@Override
public Mono<GroupCourseType> save(GroupCourseType groupCourseType) {
GroupCourseTypeEntity entity = converter.toGroupCourseTypeEntity(groupCourseType);
return groupCourseTypeDao.save(entity)
.map(converter::toGroupCourseType);
}
@Override
public Mono<GroupCourseType> update(GroupCourseType groupCourseType) {
return groupCourseTypeDao.findByIdIsAndDeletedAtIsNull(groupCourseType.getId())
.switchIfEmpty(Mono.error(new RuntimeException("团课类型不存在")))
.flatMap(existing -> {
existing.markNotNew();
if (groupCourseType.getTypeName() != null) {
existing.setTypeName(groupCourseType.getTypeName());
}
if (groupCourseType.getBaseDifficulty() != null) {
existing.setBaseDifficulty(groupCourseType.getBaseDifficulty());
}
if (groupCourseType.getDescription() != null) {
existing.setDescription(groupCourseType.getDescription());
}
if (groupCourseType.getCategory() != null) {
existing.setCategory(groupCourseType.getCategory());
}
existing.setUpdatedAt(LocalDateTime.now());
return groupCourseTypeDao.save(existing);
})
.map(converter::toGroupCourseType);
}
@Override
public Mono<Void> deleteById(Long id) {
return groupCourseTypeDao.softDelete(id, LocalDateTime.now())
.then();
}
}
@@ -1,30 +0,0 @@
package cn.novalon.gym.manage.groupcourse.service;
import cn.novalon.gym.manage.groupcourse.domain.CourseLabel;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
public interface ICourseLabelService {
Mono<CourseLabel> findById(Long id);
Flux<CourseLabel> findAll();
Flux<CourseLabel> findByKeyword(String keyword);
Mono<CourseLabel> create(CourseLabel courseLabel);
Mono<CourseLabel> update(Long id, CourseLabel courseLabel);
Mono<Void> delete(Long id);
Flux<CourseLabel> findByTypeId(Long typeId);
Mono<Void> addLabelsToType(Long typeId, List<Long> labelIds);
Mono<Void> removeLabelFromType(Long typeId, Long labelId);
Mono<Void> clearLabelsFromType(Long typeId);
}
@@ -1,28 +0,0 @@
package cn.novalon.gym.manage.groupcourse.service;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseRecommend;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface IGroupCourseRecommendService {
Mono<GroupCourseRecommend> findById(Long id);
Flux<GroupCourseRecommend> findAll();
Flux<GroupCourseRecommend> findAll(String sortBy, String sortOrder);
Flux<GroupCourseRecommend> findAllActive();
Flux<GroupCourseRecommend> findByCourseId(Long courseId);
Mono<GroupCourseRecommend> create(GroupCourseRecommend recommend);
Mono<GroupCourseRecommend> update(Long id, GroupCourseRecommend recommend);
Mono<Void> delete(Long id);
Mono<GroupCourseRecommend> enable(Long id);
Mono<GroupCourseRecommend> disable(Long id);
}
@@ -4,14 +4,11 @@ package cn.novalon.gym.manage.groupcourse.service;
import cn.novalon.gym.manage.common.dto.PageRequest; import cn.novalon.gym.manage.common.dto.PageRequest;
import cn.novalon.gym.manage.common.dto.PageResponse; import cn.novalon.gym.manage.common.dto.PageResponse;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse; import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseDetail;
import cn.novalon.gym.manage.groupcourse.dto.GroupCourseQueryDto;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
public interface IGroupCourseService { public interface IGroupCourseService {
Mono<GroupCourse> findById(Long id); Mono<GroupCourse> findById(Long id);
Mono<GroupCourseDetail> findDetailById(Long id);
Flux<GroupCourse> findAll(); Flux<GroupCourse> findAll();
Flux<GroupCourse> findAll(boolean includeDeleted); Flux<GroupCourse> findAll(boolean includeDeleted);
@@ -26,6 +23,4 @@ public interface IGroupCourseService {
Mono<GroupCourse> signIn(Long courseId, Long memberId); Mono<GroupCourse> signIn(Long courseId, Long memberId);
Mono<Void> delete(Long id); Mono<Void> delete(Long id);
Mono<PageResponse<GroupCourse>> searchGroupCourses(GroupCourseQueryDto query);
} }
@@ -1,32 +0,0 @@
package cn.novalon.gym.manage.groupcourse.service;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseType;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface IGroupCourseTypeService {
Mono<GroupCourseType> findById(Long id);
Flux<GroupCourseType> findAll();
Flux<GroupCourseType> findAll(boolean includeDeleted);
Flux<GroupCourseType> findByKeyword(String keyword);
Flux<GroupCourseType> findByCategory(String category);
Flux<GroupCourseType> findByCategoryAndKeyword(String category, String keyword);
Mono<GroupCourseType> create(GroupCourseType groupCourseType);
Mono<GroupCourseType> update(Long id, GroupCourseType groupCourseType);
Mono<Void> delete(Long id);
/**
* 获取分类列表(去重)
* @return 分类名称列表
*/
Flux<String> findCategories();
}
@@ -1,112 +0,0 @@
package cn.novalon.gym.manage.groupcourse.service.impl;
import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.groupcourse.domain.CourseLabel;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.repository.ICourseLabelRepository;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository;
import cn.novalon.gym.manage.groupcourse.service.ICourseLabelService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
@Service
public class CourseLabelService implements ICourseLabelService {
private static final Logger logger = LoggerFactory.getLogger(CourseLabelService.class);
private static final String CACHE_KEY_DETAIL_PREFIX = "group_course:detail:";
private final ICourseLabelRepository courseLabelRepository;
private final IGroupCourseRepository groupCourseRepository;
private final RedisUtil redisUtil;
public CourseLabelService(ICourseLabelRepository courseLabelRepository,
IGroupCourseRepository groupCourseRepository,
RedisUtil redisUtil) {
this.courseLabelRepository = courseLabelRepository;
this.groupCourseRepository = groupCourseRepository;
this.redisUtil = redisUtil;
}
private Mono<Void> invalidateGroupCourseDetailCache(Long typeId) {
return groupCourseRepository.findByCourseType(typeId)
.flatMap(course -> {
String cacheKey = CACHE_KEY_DETAIL_PREFIX + course.getId();
return redisUtil.delete(cacheKey)
.doOnSuccess(deleted -> logger.debug("清除团课详情缓存 - courseId={}", course.getId()));
})
.then();
}
@Override
public Mono<CourseLabel> findById(Long id) {
return courseLabelRepository.findById(id);
}
@Override
public Flux<CourseLabel> findAll() {
return courseLabelRepository.findAll();
}
@Override
public Flux<CourseLabel> findByKeyword(String keyword) {
return courseLabelRepository.findByKeyword(keyword);
}
@Override
public Mono<CourseLabel> create(CourseLabel courseLabel) {
return courseLabelRepository.findByLabelName(courseLabel.getLabelName())
.flatMap(existing -> Mono.<CourseLabel>error(new RuntimeException("标签名称已存在")))
.switchIfEmpty(courseLabelRepository.save(courseLabel))
.doOnSuccess(label -> logger.info("标签创建成功 - id={}, name={}", label.getId(), label.getLabelName()))
.doOnError(error -> logger.error("标签创建失败 - error: {}", error.getMessage()));
}
@Override
public Mono<CourseLabel> update(Long id, CourseLabel courseLabel) {
courseLabel.setId(id);
return courseLabelRepository.update(courseLabel)
.doOnSuccess(label -> logger.info("标签更新成功 - id={}", id))
.doOnError(error -> logger.error("标签更新失败 - id={}, error: {}", id, error.getMessage()));
}
@Override
public Mono<Void> delete(Long id) {
return courseLabelRepository.deleteById(id)
.doOnSuccess(v -> logger.info("标签删除成功 - id={}", id))
.doOnError(error -> logger.error("标签删除失败 - id={}, error: {}", id, error.getMessage()));
}
@Override
public Flux<CourseLabel> findByTypeId(Long typeId) {
return courseLabelRepository.findByTypeId(typeId);
}
@Override
public Mono<Void> addLabelsToType(Long typeId, List<Long> labelIds) {
return courseLabelRepository.addLabelsToType(typeId, labelIds)
.then(invalidateGroupCourseDetailCache(typeId))
.doOnSuccess(v -> logger.info("标签添加到类型成功 - typeId={}, labelIds={}", typeId, labelIds))
.doOnError(error -> logger.error("标签添加到类型失败 - typeId={}, error: {}", typeId, error.getMessage()));
}
@Override
public Mono<Void> removeLabelFromType(Long typeId, Long labelId) {
return courseLabelRepository.removeLabelFromType(typeId, labelId)
.then(invalidateGroupCourseDetailCache(typeId))
.doOnSuccess(v -> logger.info("从类型移除标签成功 - typeId={}, labelId={}", typeId, labelId))
.doOnError(error -> logger.error("从类型移除标签失败 - typeId={}, labelId={}, error: {}", typeId, labelId, error.getMessage()));
}
@Override
public Mono<Void> clearLabelsFromType(Long typeId) {
return courseLabelRepository.clearLabelsFromType(typeId)
.then(invalidateGroupCourseDetailCache(typeId))
.doOnSuccess(v -> logger.info("清空类型标签成功 - typeId={}", typeId))
.doOnError(error -> logger.error("清空类型标签失败 - typeId={}, error: {}", typeId, error.getMessage()));
}
}
@@ -1,142 +0,0 @@
package cn.novalon.gym.manage.groupcourse.service.impl;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseRecommend;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRecommendRepository;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseRecommendService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
public class GroupCourseRecommendService implements IGroupCourseRecommendService {
private static final Logger logger = LoggerFactory.getLogger(GroupCourseRecommendService.class);
private final IGroupCourseRecommendRepository recommendRepository;
private final IGroupCourseRepository groupCourseRepository;
public GroupCourseRecommendService(IGroupCourseRecommendRepository recommendRepository,
IGroupCourseRepository groupCourseRepository) {
this.recommendRepository = recommendRepository;
this.groupCourseRepository = groupCourseRepository;
}
@Override
public Mono<GroupCourseRecommend> findById(Long id) {
return recommendRepository.findById(id)
.flatMap(this::fillGroupCourseInfo);
}
@Override
public Flux<GroupCourseRecommend> findAll() {
return recommendRepository.findAll()
.flatMap(this::fillGroupCourseInfo);
}
@Override
public Flux<GroupCourseRecommend> findAll(String sortBy, String sortOrder) {
return recommendRepository.findAll(sortBy, sortOrder)
.flatMap(this::fillGroupCourseInfo);
}
@Override
public Flux<GroupCourseRecommend> findAllActive() {
return recommendRepository.findAllActive()
.flatMap(this::fillGroupCourseInfo);
}
@Override
public Flux<GroupCourseRecommend> findByCourseId(Long courseId) {
return recommendRepository.findByCourseId(courseId)
.flatMap(this::fillGroupCourseInfo);
}
@Override
public Mono<GroupCourseRecommend> create(GroupCourseRecommend recommend) {
if (recommend.getCourseId() == null) {
return Mono.error(new RuntimeException("团课ID不能为空"));
}
return groupCourseRepository.findByIdAndDeletedAtIsNull(recommend.getCourseId())
.switchIfEmpty(Mono.error(new RuntimeException("团课不存在")))
.flatMap(course -> {
return recommendRepository.save(recommend)
.doOnSuccess(r -> logger.info("团课推荐创建成功 - id={}, courseId={}", r.getId(), r.getCourseId()))
.doOnError(error -> logger.error("团课推荐创建失败 - error: {}", error.getMessage()));
});
}
@Override
public Mono<GroupCourseRecommend> update(Long id, GroupCourseRecommend recommend) {
return recommendRepository.findById(id)
.switchIfEmpty(Mono.error(new RuntimeException("团课推荐不存在")))
.flatMap(existing -> {
if (recommend.getRecommendTitle() != null) {
existing.setRecommendTitle(recommend.getRecommendTitle());
}
if (recommend.getRecommendContent() != null) {
existing.setRecommendContent(recommend.getRecommendContent());
}
if (recommend.getRecommendReason() != null) {
existing.setRecommendReason(recommend.getRecommendReason());
}
if (recommend.getPriority() != null) {
existing.setPriority(recommend.getPriority());
}
if (recommend.getIsActive() != null) {
existing.setIsActive(recommend.getIsActive());
}
if (recommend.getCourseId() != null) {
existing.setCourseId(recommend.getCourseId());
}
return recommendRepository.update(existing);
})
.doOnSuccess(r -> logger.info("团课推荐更新成功 - id={}", id))
.doOnError(error -> logger.error("团课推荐更新失败 - id={}, error: {}", id, error.getMessage()));
}
@Override
public Mono<Void> delete(Long id) {
return recommendRepository.findById(id)
.switchIfEmpty(Mono.error(new RuntimeException("团课推荐不存在")))
.flatMap(recommend -> {
return recommendRepository.deleteById(id)
.doOnSuccess(v -> logger.info("团课推荐删除成功 - id={}", id))
.doOnError(error -> logger.error("团课推荐删除失败 - id={}, error: {}", id, error.getMessage()));
});
}
@Override
public Mono<GroupCourseRecommend> enable(Long id) {
return recommendRepository.updateActiveStatus(id, true)
.switchIfEmpty(Mono.error(new RuntimeException("团课推荐不存在")))
.doOnSuccess(r -> logger.info("团课推荐启用成功 - id={}", id))
.doOnError(error -> logger.error("团课推荐启用失败 - id={}, error: {}", id, error.getMessage()));
}
@Override
public Mono<GroupCourseRecommend> disable(Long id) {
return recommendRepository.updateActiveStatus(id, false)
.switchIfEmpty(Mono.error(new RuntimeException("团课推荐不存在")))
.doOnSuccess(r -> logger.info("团课推荐禁用成功 - id={}", id))
.doOnError(error -> logger.error("团课推荐禁用失败 - id={}, error: {}", id, error.getMessage()));
}
private Mono<GroupCourseRecommend> fillGroupCourseInfo(GroupCourseRecommend recommend) {
if (recommend.getCourseId() == null) {
return Mono.just(recommend);
}
return groupCourseRepository.findByIdAndDeletedAtIsNull(recommend.getCourseId())
.map(course -> {
recommend.setGroupCourse(course);
return recommend;
})
.defaultIfEmpty(recommend);
}
}
@@ -4,21 +4,14 @@ package cn.novalon.gym.manage.groupcourse.service.impl;
import cn.novalon.gym.manage.common.dto.PageRequest; import cn.novalon.gym.manage.common.dto.PageRequest;
import cn.novalon.gym.manage.common.dto.PageResponse; import cn.novalon.gym.manage.common.dto.PageResponse;
import cn.novalon.gym.manage.common.util.RedisUtil; import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.groupcourse.domain.CourseLabel;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse; import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseBooking; import cn.novalon.gym.manage.groupcourse.domain.GroupCourseBooking;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseDetail;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseType;
import cn.novalon.gym.manage.groupcourse.dto.GroupCourseQueryDto;
import cn.novalon.gym.manage.groupcourse.enums.CourseEvent; import cn.novalon.gym.manage.groupcourse.enums.CourseEvent;
import cn.novalon.gym.manage.groupcourse.enums.CourseStatus; import cn.novalon.gym.manage.groupcourse.enums.CourseStatus;
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseStateMachine; import cn.novalon.gym.manage.groupcourse.handler.GroupCourseStateMachine;
import cn.novalon.gym.manage.groupcourse.repository.ICourseLabelRepository;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseBookingRepository; import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseBookingRepository;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository; import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseTypeRepository;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseService; import cn.novalon.gym.manage.groupcourse.service.IGroupCourseService;
import cn.novalon.gym.manage.groupcourse.util.QRCodeUtil;
import cn.novalon.gym.manage.member.entity.MemberCard; import cn.novalon.gym.manage.member.entity.MemberCard;
import cn.novalon.gym.manage.member.entity.MemberCardRecord; import cn.novalon.gym.manage.member.entity.MemberCardRecord;
import cn.novalon.gym.manage.member.enums.MemberCardType; import cn.novalon.gym.manage.member.enums.MemberCardType;
@@ -28,14 +21,11 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@Service @Service
public class GroupCourseService implements IGroupCourseService { public class GroupCourseService implements IGroupCourseService {
@@ -43,130 +33,32 @@ public class GroupCourseService implements IGroupCourseService {
private final IGroupCourseRepository groupCourseRepository; private final IGroupCourseRepository groupCourseRepository;
private final IGroupCourseBookingRepository bookingRepository; private final IGroupCourseBookingRepository bookingRepository;
private final IGroupCourseTypeRepository groupCourseTypeRepository;
private final ICourseLabelRepository courseLabelRepository;
private final IMemberCardRecordService memberCardRecordService; private final IMemberCardRecordService memberCardRecordService;
private final MemberCardRepository memberCardRepository; private final MemberCardRepository memberCardRepository;
private final RedisUtil redisUtil; private final RedisUtil redisUtil;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final GroupCourseStateMachine stateMachine; private final GroupCourseStateMachine stateMachine;
private final DatabaseClient databaseClient;
private static final String CACHE_KEY_PREFIX = "group_course:page:"; private static final String CACHE_KEY_PREFIX = "group_course:page:";
private static final String CACHE_KEY_ID_PREFIX = "group_course:id:"; private static final String CACHE_KEY_ID_PREFIX = "group_course:id:";
private static final String CACHE_KEY_DETAIL_PREFIX = "group_course:detail:";
private static final long CACHE_EXPIRE_SECONDS = 300; private static final long CACHE_EXPIRE_SECONDS = 300;
private static final double DEFAULT_GROUP_COURSE_PRICE = 50.0; private static final double DEFAULT_GROUP_COURSE_PRICE = 50.0;
public GroupCourseService(IGroupCourseRepository groupCourseRepository, public GroupCourseService(IGroupCourseRepository groupCourseRepository,
IGroupCourseBookingRepository bookingRepository, IGroupCourseBookingRepository bookingRepository,
IGroupCourseTypeRepository groupCourseTypeRepository,
ICourseLabelRepository courseLabelRepository,
IMemberCardRecordService memberCardRecordService, IMemberCardRecordService memberCardRecordService,
MemberCardRepository memberCardRepository, MemberCardRepository memberCardRepository,
RedisUtil redisUtil, RedisUtil redisUtil,
ObjectMapper objectMapper, ObjectMapper objectMapper,
GroupCourseStateMachine stateMachine, GroupCourseStateMachine stateMachine){
DatabaseClient databaseClient){
this.groupCourseRepository = groupCourseRepository; this.groupCourseRepository = groupCourseRepository;
this.bookingRepository = bookingRepository; this.bookingRepository = bookingRepository;
this.groupCourseTypeRepository = groupCourseTypeRepository;
this.courseLabelRepository = courseLabelRepository;
this.memberCardRecordService = memberCardRecordService; this.memberCardRecordService = memberCardRecordService;
this.memberCardRepository = memberCardRepository; this.memberCardRepository = memberCardRepository;
this.redisUtil = redisUtil; this.redisUtil = redisUtil;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.stateMachine = stateMachine; this.stateMachine = stateMachine;
this.databaseClient = databaseClient;
}
@Override
public Mono<GroupCourseDetail> findDetailById(Long id) {
String cacheKey = CACHE_KEY_DETAIL_PREFIX + id;
return redisUtil.get(cacheKey, String.class)
.flatMap(cachedJson -> {
if (cachedJson != null) {
try {
GroupCourseDetail detail = objectMapper.readValue(cachedJson, GroupCourseDetail.class);
logger.info("缓存命中 - findDetailById: id={}", id);
return Mono.just(detail);
} catch (JsonProcessingException e) {
logger.warn("缓存解析失败,删除缓存 - id: {}, error: {}", id, e.getMessage());
return redisUtil.delete(cacheKey).then(Mono.empty());
}
}
return Mono.empty();
})
.switchIfEmpty(
groupCourseRepository.findByIdAndDeletedAtIsNull(id)
.flatMap(course -> {
// 查询类型信息
Long courseTypeId = course.getCourseType();
if (courseTypeId == null) {
// 没有类型,直接构建详情
return Mono.just(buildDetail(course, null));
}
// 有类型,查询类型信息
return groupCourseTypeRepository.findById(courseTypeId)
.flatMap(type -> {
// 查询标签
return courseLabelRepository.findByTypeId(type.getId())
.collectList()
.map(labels -> {
type.setLabels(labels);
return buildDetail(course, type);
});
})
.switchIfEmpty(Mono.just(buildDetail(course, null)));
})
.flatMap(detail -> {
try {
String jsonData = objectMapper.writeValueAsString(detail);
return redisUtil.setWithExpire(cacheKey, jsonData, CACHE_EXPIRE_SECONDS)
.thenReturn(detail)
.doOnSuccess(d -> logger.debug("缓存已设置 - findDetailById: id={}", id));
} catch (JsonProcessingException e) {
logger.error("缓存设置失败 - id: {}, error: {}", id, e.getMessage());
return Mono.just(detail);
}
})
.doOnSubscribe(sub -> logger.debug("缓存未命中,查询数据库 - findDetailById: id={}", id))
);
}
/**
* 构建团课完整信息对象
*/
private GroupCourseDetail buildDetail(GroupCourse course, GroupCourseType type) {
GroupCourseDetail detail = new GroupCourseDetail();
detail.setId(course.getId());
detail.setCourseName(course.getCourseName());
detail.setCoachId(course.getCoachId());
detail.setCourseType(course.getCourseType());
detail.setStartTime(course.getStartTime());
detail.setEndTime(course.getEndTime());
detail.setMaxMembers(course.getMaxMembers());
detail.setCurrentMembers(course.getCurrentMembers());
detail.setStatus(course.getStatus());
detail.setLocation(course.getLocation());
detail.setCoverImage(course.getCoverImage());
detail.setDescription(course.getDescription());
detail.setPointCardAmount(course.getPointCardAmount());
detail.setStoredValueAmount(course.getStoredValueAmount());
detail.setQrCodePath(course.getQrCodePath());
detail.setCreatedAt(course.getCreatedAt());
detail.setUpdatedAt(course.getUpdatedAt());
// 设置类型信息
if (type != null) {
detail.setTypeInfo(type);
}
return detail;
} }
@Override @Override
@@ -271,38 +163,7 @@ public class GroupCourseService implements IGroupCourseService {
@Override @Override
public Mono<GroupCourse> create(GroupCourse groupCourse) { public Mono<GroupCourse> create(GroupCourse groupCourse) {
return groupCourseRepository.save(groupCourse) return groupCourseRepository.save(groupCourse)
.flatMap(course -> { .doOnSuccess(course -> logger.info("团课创建成功 - id={}, name={}", course.getId(), course.getCourseName()))
try {
// 生成二维码内容:团课基础信息JSON
Map<String, Object> qrCodeContent = new HashMap<>();
qrCodeContent.put("id", course.getId());
qrCodeContent.put("courseName", course.getCourseName());
qrCodeContent.put("coachId", course.getCoachId());
qrCodeContent.put("courseType", course.getCourseType());
qrCodeContent.put("startTime", course.getStartTime());
qrCodeContent.put("endTime", course.getEndTime());
qrCodeContent.put("maxMembers", course.getMaxMembers());
qrCodeContent.put("location", course.getLocation());
qrCodeContent.put("description", course.getDescription());
qrCodeContent.put("createdAt", course.getCreatedAt());
String jsonContent = objectMapper.writeValueAsString(qrCodeContent);
// 生成二维码并上传到阿里云OSS
String qrCodeUrl = QRCodeUtil.generateQRCodeAndUploadToOSS(jsonContent);
course.setQrCodePath(qrCodeUrl);
logger.info("团课二维码上传到OSS成功 - id={}, qrCodeUrl={}", course.getId(), qrCodeUrl);
// 更新团课信息,保存二维码路径
return groupCourseRepository.update(course)
.doOnSuccess(updatedCourse -> logger.info("团课创建成功 - id={}, name={}", updatedCourse.getId(), updatedCourse.getCourseName()));
} catch (Exception e) {
logger.error("团课二维码生成失败 - id={}, error: {}", course.getId(), e.getMessage(), e);
// 即使二维码生成失败,也返回成功创建的团课
return Mono.just(course);
}
})
.flatMap(course -> clearCache().thenReturn(course)) .flatMap(course -> clearCache().thenReturn(course))
.doOnError(error -> logger.error("团课创建失败 - error: {}", error.getMessage())); .doOnError(error -> logger.error("团课创建失败 - error: {}", error.getMessage()));
} }
@@ -348,9 +209,6 @@ public class GroupCourseService implements IGroupCourseService {
if (groupCourse.getStoredValueAmount() != null) { if (groupCourse.getStoredValueAmount() != null) {
existing.setStoredValueAmount(groupCourse.getStoredValueAmount()); existing.setStoredValueAmount(groupCourse.getStoredValueAmount());
} }
if (groupCourse.getQrCodePath() != null) {
existing.setQrCodePath(groupCourse.getQrCodePath());
}
return groupCourseRepository.update(existing); return groupCourseRepository.update(existing);
}) })
.doOnSuccess(course -> logger.info("团课更新成功 - id={}", id)) .doOnSuccess(course -> logger.info("团课更新成功 - id={}", id))
@@ -483,56 +341,26 @@ public class GroupCourseService implements IGroupCourseService {
@Override @Override
public Mono<GroupCourse> signIn(Long courseId, Long memberId) { public Mono<GroupCourse> signIn(Long courseId, Long memberId) {
return groupCourseRepository.findByIdAndDeletedAtIsNull(courseId) return groupCourseRepository.findByIdAndDeletedAtIsNull(courseId)
.switchIfEmpty(Mono.error(new RuntimeException("团课不存在或已删除"))) .switchIfEmpty(Mono.error(new RuntimeException("团课不存在")))
.flatMap(course -> { .flatMap(course -> {
// 校验1:团课已取消 if (course.getStatus() != 0L) {
if (course.getStatus().equals(CourseStatus.CANCELLED.getValue())) { return Mono.error(new RuntimeException("课程状态不允许签到"));
return Mono.error(new RuntimeException("团课已取消,无法签到"));
} }
// 校验2:团课已结束
if (course.getStatus().equals(CourseStatus.ENDED.getValue())) {
return Mono.error(new RuntimeException("团课已结束,无法签到"));
}
// 校验3:签到时间校验(开课前2小时内才能签到)
LocalDateTime now = LocalDateTime.now();
LocalDateTime signInStart = course.getStartTime().minusHours(2);
if (now.isBefore(signInStart)) {
return Mono.error(new RuntimeException("未到签到时间,请在开课前2小时内签到"));
}
if (now.isAfter(course.getEndTime())) {
return Mono.error(new RuntimeException("团课已结束,无法签到"));
}
// 校验4:课程已满员
if (course.getCurrentMembers() >= course.getMaxMembers()) { if (course.getCurrentMembers() >= course.getMaxMembers()) {
return Mono.error(new RuntimeException("课程已满员,无法签到")); return Mono.error(new RuntimeException("课程已满员"));
} }
// 校验5:用户今日是否已到店签到(直接查询sign_in_record表) // 检查会员是否已预约此课程
LocalDateTime todayStart = LocalDateTime.now().toLocalDate().atStartOfDay(); return bookingRepository.findValidBooking(courseId, memberId)
LocalDateTime todayEnd = todayStart.plusDays(1); .switchIfEmpty(Mono.error(new RuntimeException("会员未预约此课程")))
return databaseClient.sql("SELECT sign_in_status FROM sign_in_record WHERE member_id = :memberId AND sign_in_time >= :startTime AND sign_in_time < :endTime AND is_delete = false ORDER BY sign_in_time DESC LIMIT 1") .flatMap(booking -> {
.bind("memberId", memberId) // 更新课程当前人数
.bind("startTime", todayStart) return groupCourseRepository.updateCurrentMembers(courseId, 1)
.bind("endTime", todayEnd) .flatMap(updatedCourse -> {
.map(row -> row.get("sign_in_status", String.class)) // 更新预约状态为已出席
.one() return bookingRepository.updateStatus(booking.getId(), "2")
.switchIfEmpty(Mono.error(new RuntimeException("请先完成到店签到"))) .thenReturn(updatedCourse);
.flatMap(status -> {
if (!"SUCCESS".equals(status)) {
return Mono.error(new RuntimeException("到店签到未成功,请重新签到"));
}
// 校验6:用户已预约此课程(有效预约,状态为0-已预约)
return bookingRepository.findValidBooking(courseId, memberId)
.switchIfEmpty(Mono.error(new RuntimeException("您未预约此课程,无法签到")))
.flatMap(booking -> {
return groupCourseRepository.updateCurrentMembers(courseId, 1)
.flatMap(updatedCourse -> {
return bookingRepository.updateStatus(booking.getId(), "2")
.thenReturn(updatedCourse);
});
}); });
}); });
}) })
@@ -561,21 +389,8 @@ public class GroupCourseService implements IGroupCourseService {
}); });
} }
@Override
public Mono<PageResponse<GroupCourse>> searchGroupCourses(GroupCourseQueryDto query) {
logger.info("多条件查询团课 - courseName={}, courseType={}, startDate={}, endDate={}, timePeriod={}, priceSort={}, remainingMost={}",
query.getCourseName(), query.getCourseType(), query.getStartDate(), query.getEndDate(),
query.getTimePeriod(), query.getPriceSort(), query.getRemainingMost());
return groupCourseRepository.searchGroupCourses(query)
.doOnSuccess(result -> logger.info("多条件查询结果 - total={}, page={}, size={}",
result.getTotalElements(), result.getCurrentPage(), result.getPageSize()))
.doOnError(error -> logger.error("多条件查询失败 - error: {}", error.getMessage()));
}
private Mono<Void> clearCache() { private Mono<Void> clearCache() {
return redisUtil.deleteByPattern(CACHE_KEY_PREFIX + "*") return redisUtil.deleteByPattern(CACHE_KEY_PREFIX + "*")
.then(redisUtil.deleteByPattern(CACHE_KEY_ID_PREFIX + "*")) .then(redisUtil.deleteByPattern(CACHE_KEY_ID_PREFIX + "*")).then();
.then(redisUtil.deleteByPattern(CACHE_KEY_DETAIL_PREFIX + "*")).then();
} }
} }
@@ -1,86 +0,0 @@
package cn.novalon.gym.manage.groupcourse.service.impl;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseType;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseTypeRepository;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseTypeService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.HashSet;
import java.util.Set;
@Service
public class GroupCourseTypeService implements IGroupCourseTypeService {
private static final Logger logger = LoggerFactory.getLogger(GroupCourseTypeService.class);
private final IGroupCourseTypeRepository groupCourseTypeRepository;
public GroupCourseTypeService(IGroupCourseTypeRepository groupCourseTypeRepository) {
this.groupCourseTypeRepository = groupCourseTypeRepository;
}
@Override
public Mono<GroupCourseType> findById(Long id) {
return groupCourseTypeRepository.findById(id);
}
@Override
public Flux<GroupCourseType> findAll() {
return groupCourseTypeRepository.findAll(false);
}
@Override
public Flux<GroupCourseType> findAll(boolean includeDeleted) {
return groupCourseTypeRepository.findAll(includeDeleted);
}
@Override
public Flux<GroupCourseType> findByKeyword(String keyword) {
return groupCourseTypeRepository.findByKeyword(keyword);
}
@Override
public Flux<GroupCourseType> findByCategory(String category) {
return groupCourseTypeRepository.findByCategory(category);
}
@Override
public Flux<GroupCourseType> findByCategoryAndKeyword(String category, String keyword) {
return groupCourseTypeRepository.findByCategoryAndKeyword(category, keyword);
}
@Override
public Mono<GroupCourseType> create(GroupCourseType groupCourseType) {
return groupCourseTypeRepository.findByTypeName(groupCourseType.getTypeName())
.flatMap(existing -> Mono.<GroupCourseType>error(new RuntimeException("团课类型名称已存在")))
.switchIfEmpty(groupCourseTypeRepository.save(groupCourseType))
.doOnSuccess(type -> logger.info("团课类型创建成功 - id={}, name={}", type.getId(), type.getTypeName()))
.doOnError(error -> logger.error("团课类型创建失败 - error: {}", error.getMessage()));
}
@Override
public Mono<GroupCourseType> update(Long id, GroupCourseType groupCourseType) {
return groupCourseTypeRepository.update(groupCourseType)
.doOnSuccess(type -> logger.info("团课类型更新成功 - id={}", id))
.doOnError(error -> logger.error("团课类型更新失败 - id={}, error: {}", id, error.getMessage()));
}
@Override
public Mono<Void> delete(Long id) {
return groupCourseTypeRepository.deleteById(id)
.doOnSuccess(v -> logger.info("团课类型删除成功 - id={}", id))
.doOnError(error -> logger.error("团课类型删除失败 - id={}, error: {}", id, error.getMessage()));
}
@Override
public Flux<String> findCategories() {
return groupCourseTypeRepository.findAll(false)
.map(GroupCourseType::getCategory)
.filter(category -> category != null && !category.isEmpty())
.distinct();
}
}
@@ -1,109 +0,0 @@
package cn.novalon.gym.manage.groupcourse.util;
//import com.aliyun.oss.OSS;
//import com.aliyun.oss.OSSClientBuilder;
//import com.aliyun.oss.model.PutObjectRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 阿里云OSS工具类
*/
public class OSSUtil {
private static final Logger logger = LoggerFactory.getLogger(OSSUtil.class);
// OSS配置信息
private static final String ENDPOINT = "oss-cn-beijing.aliyuncs.com";
private static final String ACCESS_KEY_ID = "LTAI5t9TFh9Vayeahz45kZjg";
private static final String ACCESS_KEY_SECRET = "zD6NlCeH5UhjBs4vnQVqn8Ksi3CaZz";
private static final String BUCKET_NAME = "ycc-filesaver";
// OSS访问地址前缀
private static final String OSS_URL_PREFIX = "https://" + BUCKET_NAME + "." + ENDPOINT + "/";
// 文件存储目录
private static final String QRCODE_DIR = "qrcode/";
/**
* 上传文件到阿里云OSS
*
* @param localFilePath 本地文件路径
* @param fileName 文件名(不含路径)
* @return OSS访问地址
*/
public static String uploadToOSS(String localFilePath, String fileName) {
OSS ossClient = null;
try {
// 创建OSS客户端
ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET);
// 构建OSS文件路径:qrcode/2026/06/18/xxx.png
String datePath = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
String ossFilePath = QRCODE_DIR + datePath + "/" + fileName;
// 创建上传请求
PutObjectRequest putObjectRequest = new PutObjectRequest(BUCKET_NAME, ossFilePath, new File(localFilePath));
// 上传文件
ossClient.putObject(putObjectRequest);
// 构建访问地址
String accessUrl = OSS_URL_PREFIX + ossFilePath;
logger.info("文件上传到OSS成功: localPath={}, ossUrl={}", localFilePath, accessUrl);
return accessUrl;
} catch (Exception e) {
logger.error("文件上传到OSS失败 - localPath: {}, error: {}", localFilePath, e.getMessage(), e);
throw new RuntimeException("文件上传到OSS失败: " + e.getMessage(), e);
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
/**
* 上传文件到阿里云OSS(自定义存储路径)
*
* @param localFilePath 本地文件路径
* @param ossDirectory OSS存储目录
* @param fileName 文件名(不含路径)
* @return OSS访问地址
*/
public static String uploadToOSS(String localFilePath, String ossDirectory, String fileName) {
OSS ossClient = null;
try {
// 创建OSS客户端
ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET);
// 构建OSS文件路径
String ossFilePath = ossDirectory + fileName;
// 创建上传请求
PutObjectRequest putObjectRequest = new PutObjectRequest(BUCKET_NAME, ossFilePath, new File(localFilePath));
// 上传文件
ossClient.putObject(putObjectRequest);
// 构建访问地址
String accessUrl = OSS_URL_PREFIX + ossFilePath;
logger.info("文件上传到OSS成功: localPath={}, ossUrl={}", localFilePath, accessUrl);
return accessUrl;
} catch (Exception e) {
logger.error("文件上传到OSS失败 - localPath: {}, error: {}", localFilePath, e.getMessage(), e);
throw new RuntimeException("文件上传到OSS失败: " + e.getMessage(), e);
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
}
@@ -1,156 +0,0 @@
package cn.novalon.gym.manage.groupcourse.util;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 二维码生成工具类
*/
public class QRCodeUtil {
private static final Logger logger = LoggerFactory.getLogger(QRCodeUtil.class);
// 二维码默认保存路径(本地临时路径)
private static final String DEFAULT_SAVE_PATH = "D:\\Games\\exmp\\image";
// 二维码尺寸
private static final int QR_CODE_WIDTH = 300;
private static final int QR_CODE_HEIGHT = 300;
/**
* 生成二维码并保存到指定路径
*
* @param content 二维码内容
* @param savePath 保存路径
* @param fileName 文件名(不含扩展名)
* @return 生成的二维码文件完整路径
*/
public static String generateQRCode(String content, String savePath, String fileName) {
try {
Path directory = Paths.get(savePath);
if (!Files.exists(directory)) {
Files.createDirectories(directory);
logger.info("创建二维码保存目录: {}", savePath);
}
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
hints.put(EncodeHintType.MARGIN, 1);
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix bitMatrix = qrCodeWriter.encode(content, BarcodeFormat.QR_CODE, QR_CODE_WIDTH, QR_CODE_HEIGHT, hints);
String filePath = Paths.get(savePath, fileName + ".png").toString();
Path path = Paths.get(filePath);
MatrixToImageWriter.writeToPath(bitMatrix, "PNG", path);
logger.info("二维码生成成功: {}", filePath);
return filePath;
} catch (WriterException e) {
logger.error("二维码生成失败 - WriterException: {}", e.getMessage(), e);
throw new RuntimeException("二维码生成失败: " + e.getMessage(), e);
} catch (IOException e) {
logger.error("二维码保存失败 - IOException: {}", e.getMessage(), e);
throw new RuntimeException("二维码保存失败: " + e.getMessage(), e);
}
}
/**
* 生成二维码并保存到默认路径
* 文件名格式: UUID + 创建时间
*
* @param content 二维码内容
* @return 生成的二维码文件完整路径
*/
public static String generateQRCode(String content) {
String uuid = UUID.randomUUID().toString().replace("-", "");
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
String fileName = uuid + "_" + timestamp;
return generateQRCode(content, DEFAULT_SAVE_PATH, fileName);
}
/**
* 生成二维码并保存到默认路径,使用自定义文件名
*
* @param content 二维码内容
* @param fileName 文件名(不含扩展名)
* @return 生成的二维码文件完整路径
*/
public static String generateQRCodeWithFileName(String content, String fileName) {
return generateQRCode(content, DEFAULT_SAVE_PATH, fileName);
}
/**
* 生成二维码并上传到阿里云OSS
* 文件名格式: UUID + 创建时间
*
* @param content 二维码内容
* @return 阿里云OSS访问地址
*/
public static String generateQRCodeAndUploadToOSS(String content) {
String uuid = UUID.randomUUID().toString().replace("-", "");
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
String fileName = uuid + "_" + timestamp + ".png";
return generateQRCodeAndUploadToOSS(content, fileName);
}
/**
* 生成二维码并上传到阿里云OSS
*
* @param content 二维码内容
* @param fileName 文件名(含扩展名)
* @return 阿里云OSS访问地址
*/
public static String generateQRCodeAndUploadToOSS(String content, String fileName) {
try {
Path tempDir = Files.createTempDirectory("qrcode_temp");
String tempFilePath = tempDir.resolve(fileName).toString();
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
hints.put(EncodeHintType.MARGIN, 1);
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix bitMatrix = qrCodeWriter.encode(content, BarcodeFormat.QR_CODE, QR_CODE_WIDTH, QR_CODE_HEIGHT, hints);
MatrixToImageWriter.writeToPath(bitMatrix, "PNG", Paths.get(tempFilePath));
logger.info("二维码临时文件生成成功: {}", tempFilePath);
String ossUrl = OSSUtil.uploadToOSS(tempFilePath, fileName);
Files.deleteIfExists(Paths.get(tempFilePath));
Files.deleteIfExists(tempDir);
logger.info("临时文件已删除: {}", tempFilePath);
return ossUrl;
} catch (WriterException e) {
logger.error("二维码生成失败 - WriterException: {}", e.getMessage(), e);
throw new RuntimeException("二维码生成失败: " + e.getMessage(), e);
} catch (IOException e) {
logger.error("二维码处理失败 - IOException: {}", e.getMessage(), e);
throw new RuntimeException("二维码处理失败: " + e.getMessage(), e);
}
}
}
@@ -1,71 +0,0 @@
package cn.novalon.gym.manage.groupcourse.util;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
/**
* QRCodeUtil测试类
*/
class QRCodeUtilTest {
@TempDir
Path tempDir;
@Test
void testGenerateQRCode() {
String content = "测试二维码内容";
String qrCodePath = QRCodeUtil.generateQRCode(content);
assertNotNull(qrCodePath, "二维码路径不应为空");
assertTrue(qrCodePath.endsWith(".png"), "二维码文件应为PNG格式");
assertTrue(qrCodePath.contains("D:\\Games\\exmp\\image"), "二维码应保存到指定路径");
System.out.println("生成的二维码路径: " + qrCodePath);
}
@Test
void testGenerateQRCodeWithCustomPath() {
String content = "自定义路径测试";
String customPath = tempDir.toString();
String fileName = "test_qrcode";
String qrCodePath = QRCodeUtil.generateQRCode(content, customPath, fileName);
assertNotNull(qrCodePath, "二维码路径不应为空");
assertTrue(qrCodePath.endsWith(".png"), "二维码文件应为PNG格式");
assertTrue(qrCodePath.contains(fileName), "二维码文件名应包含指定名称");
System.out.println("生成的二维码路径: " + qrCodePath);
}
@Test
void testGenerateQRCodeWithJsonContent() {
String jsonContent = "{\"id\":1,\"courseName\":\"瑜伽课\",\"coachId\":100,\"startTime\":\"2026-06-18T10:00:00\"}";
String qrCodePath = QRCodeUtil.generateQRCode(jsonContent);
assertNotNull(qrCodePath, "二维码路径不应为空");
assertTrue(qrCodePath.endsWith(".png"), "二维码文件应为PNG格式");
System.out.println("JSON内容二维码路径: " + qrCodePath);
}
@Test
void testGenerateQRCodeAndUploadToOSS() {
String jsonContent = "{\"id\":1,\"courseName\":\"瑜伽课\",\"coachId\":100,\"startTime\":\"2026-06-18T10:00:00\"}";
String ossUrl = QRCodeUtil.generateQRCodeAndUploadToOSS(jsonContent);
assertNotNull(ossUrl, "OSS访问地址不应为空");
assertTrue(ossUrl.startsWith("https://"), "OSS访问地址应为HTTPS");
assertTrue(ossUrl.contains("ycc-filesaver.oss-cn-beijing.aliyuncs.com"), "OSS访问地址应包含正确的域名");
assertTrue(ossUrl.contains("/qrcode/"), "OSS访问地址应包含qrcode目录");
assertTrue(ossUrl.endsWith(".png"), "OSS访问地址应为PNG格式");
System.out.println("上传到OSS的二维码地址: " + ossUrl);
}
}
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<FindBugsFilter>
<Match>
<Class name="~.*\.entity\..*" />
</Match>
<Match>
<Class name="~.*\.dto\..*" />
</Match>
<Match>
<Class name="~.*\.converter\..*" />
</Match>
</FindBugsFilter>
@@ -32,7 +32,6 @@ public class MemberCardRecordHandler {
@Operation(summary = "购买会员卡", description = "支持时长卡、次卡、储值卡,自动设置到期提醒") @Operation(summary = "购买会员卡", description = "支持时长卡、次卡、储值卡,自动设置到期提醒")
public Mono<ServerResponse> purchaseCard(ServerRequest request) { public Mono<ServerResponse> purchaseCard(ServerRequest request) {
return request.bodyToMono(PurchaseRequest.class) return request.bodyToMono(PurchaseRequest.class)
.flatMap(body -> memberCardService.purchaseCard( .flatMap(body -> memberCardService.purchaseCard(
body.getMemberId(), body.getMemberId(),
@@ -1,6 +1,5 @@
package cn.novalon.gym.manage.member.handler; package cn.novalon.gym.manage.member.handler;
import cn.novalon.gym.manage.common.exception.NotFoundException;
import cn.novalon.gym.manage.member.config.WechatProperties; import cn.novalon.gym.manage.member.config.WechatProperties;
import cn.novalon.gym.manage.member.dto.AdminUpdatePhoneDto; import cn.novalon.gym.manage.member.dto.AdminUpdatePhoneDto;
import cn.novalon.gym.manage.member.dto.SearchMemberDto; import cn.novalon.gym.manage.member.dto.SearchMemberDto;
@@ -17,7 +16,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerRequest;
@@ -52,11 +50,7 @@ public class MemberHandler {
return memberService.getMemberInfo(memberId) return memberService.getMemberInfo(memberId)
.flatMap(info -> ServerResponse.ok() .flatMap(info -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.bodyValue(info)) .bodyValue(info));
.onErrorResume(NotFoundException.class, e ->
ServerResponse.status(HttpStatus.NOT_FOUND)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(java.util.Map.of("code", 404, "message", e.getMessage())));
} }
@Operation(summary = "更新会员信息", description = "更新会员昵称、性别、生日、头像、地址等信息") @Operation(summary = "更新会员信息", description = "更新会员昵称、性别、生日、头像、地址等信息")
@@ -65,37 +65,21 @@ public class MemberServiceImpl implements MemberService {
public Mono<MemberInfoVO> getMemberInfo(Long memberId) { public Mono<MemberInfoVO> getMemberInfo(Long memberId) {
String cacheKey = MEMBER_INFO_CACHE_PREFIX + memberId; String cacheKey = MEMBER_INFO_CACHE_PREFIX + memberId;
// 先查缓存
return redisUtil.get(cacheKey, MemberInfoVO.class) return redisUtil.get(cacheKey, MemberInfoVO.class)
.flatMap(cached -> { .flatMap(cached -> {
if (cached != null) { if (cached != null) {
log.debug("从缓存获取会员信息, memberId: {}", memberId); log.debug("从缓存获取会员信息, memberId: {}", memberId);
return Mono.just(cached); return Mono.just(cached);
} }
// 缓存没有,查数据库 return memberRepository.findById(memberId)
return queryFromDatabaseAndCache(memberId, cacheKey); .map(this::buildMemberInfoResponse)
}) .flatMap(vo -> redisUtil.setWithExpire(cacheKey, vo, CACHE_EXPIRE_SECONDS)
.switchIfEmpty(Mono.defer(() -> { .then(Mono.just(vo)))
// 缓存返回null,查数据库 .switchIfEmpty(Mono.error(() -> {
return queryFromDatabaseAndCache(memberId, cacheKey); log.error("会员不存在: memberId={}", memberId);
})); throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在");
} }));
});
/**
* 从数据库查询并更新缓存
*/
private Mono<MemberInfoVO> queryFromDatabaseAndCache(Long memberId, String cacheKey) {
return memberRepository.findById(memberId)
.map(this::buildMemberInfoResponse)
.flatMap(vo -> {
// 查询到数据后更新缓存
return redisUtil.setWithExpire(cacheKey, vo, CACHE_EXPIRE_SECONDS)
.then(Mono.just(vo));
})
.switchIfEmpty(Mono.error(() -> {
log.error("会员不存在: memberId={}", memberId);
throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在");
}));
} }
@Override @Override
+80 -387
View File
File diff suppressed because one or more lines are too long
+6 -10
View File
@@ -48,16 +48,6 @@
<artifactId>gym-checkIn</artifactId> <artifactId>gym-checkIn</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>gym-dataCount</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>gym-auth</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
@@ -164,6 +154,12 @@
<version>1.0.0</version> <version>1.0.0</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>gym-dataCount</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>
@@ -10,13 +10,14 @@ import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDeta
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories; import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilter;
import java.util.List; import java.util.List;
@SpringBootApplication(scanBasePackages = "cn.novalon.gym.manage", exclude = { @SpringBootApplication(scanBasePackages = "cn.novalon.gym.manage", exclude = {
ReactiveUserDetailsServiceAutoConfiguration.class }) ReactiveUserDetailsServiceAutoConfiguration.class })
//@EnableScheduling @EnableScheduling
@EnableR2dbcRepositories(basePackages = { @EnableR2dbcRepositories(basePackages = {
"cn.novalon.gym.manage.db.dao", "cn.novalon.gym.manage.db.dao",
"cn.novalon.gym.manage.sys.audit.repository" , "cn.novalon.gym.manage.sys.audit.repository" ,
@@ -2,14 +2,9 @@ package cn.novalon.gym.manage.app.config;
import cn.novalon.gym.manage.checkIn.handler.CheckInHandler; import cn.novalon.gym.manage.checkIn.handler.CheckInHandler;
import cn.novalon.gym.manage.datacount.handler.DataStatisticsHandler;
import cn.novalon.gym.manage.file.handler.SysFileHandler; import cn.novalon.gym.manage.file.handler.SysFileHandler;
import cn.novalon.gym.manage.auth.handler.PhoneAuthHandler;
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseBookingHandler; import cn.novalon.gym.manage.groupcourse.handler.GroupCourseBookingHandler;
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseHandler; import cn.novalon.gym.manage.groupcourse.handler.GroupCourseHandler;
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseRecommendHandler;
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseTypeHandler;
import cn.novalon.gym.manage.groupcourse.handler.CourseLabelHandler;
import cn.novalon.gym.manage.member.handler.MemberCardHandler; import cn.novalon.gym.manage.member.handler.MemberCardHandler;
import cn.novalon.gym.manage.member.handler.MemberCardRecordHandler; import cn.novalon.gym.manage.member.handler.MemberCardRecordHandler;
import cn.novalon.gym.manage.member.handler.MemberCardTransactionHandler; import cn.novalon.gym.manage.member.handler.MemberCardTransactionHandler;
@@ -73,12 +68,7 @@ public class SystemRouter {
MemberCardTransactionHandler memberCardTransactionHandler, MemberCardTransactionHandler memberCardTransactionHandler,
GroupCourseHandler groupCourseHandler, GroupCourseHandler groupCourseHandler,
GroupCourseBookingHandler groupCourseBookingHandler, GroupCourseBookingHandler groupCourseBookingHandler,
GroupCourseRecommendHandler groupCourseRecommendHandler, CheckInHandler checkInHandler) {
GroupCourseTypeHandler groupCourseTypeHandler,
CourseLabelHandler courseLabelHandler,
CheckInHandler checkInHandler,
DataStatisticsHandler dataStatisticsHandler,
PhoneAuthHandler phoneAuthHandler) {
return route() return route()
// ========== 诊断路由 ========== // ========== 诊断路由 ==========
@@ -194,13 +184,10 @@ public class SystemRouter {
// ========== 消息路由 ========== // ========== 消息路由 ==========
.GET("/api/messages/user/{userId}", messageHandler::getMessagesByUser) .GET("/api/messages/user/{userId}", messageHandler::getMessagesByUser)
.GET("/api/messages/user/{userId}/page", messageHandler::getMessagesByUserPage)
.GET("/api/messages/user/{userId}/unread", messageHandler::getUnreadCount) .GET("/api/messages/user/{userId}/unread", messageHandler::getUnreadCount)
.GET("/api/messages/user/{userId}/unread/list", messageHandler::getUnreadList) .GET("/api/messages/user/{userId}/unread/list", messageHandler::getUnreadList)
.GET("/api/messages/user/{userId}/unread/page", messageHandler::getUnreadMessagesPage)
.POST("/api/messages", messageHandler::createMessage) .POST("/api/messages", messageHandler::createMessage)
.PUT("/api/messages/{id}/read", messageHandler::markAsRead) .PUT("/api/messages/{id}/read", messageHandler::markAsRead)
.PUT("/api/messages/user/{userId}/read", messageHandler::markAllAsRead)
.DELETE("/api/messages/{id}", messageHandler::deleteMessage) .DELETE("/api/messages/{id}", messageHandler::deleteMessage)
// ========== 文件路由 ========== // ========== 文件路由 ==========
@@ -223,11 +210,6 @@ public class SystemRouter {
.PUT("/api/permissions/{id}", permissionHandler::updatePermission) .PUT("/api/permissions/{id}", permissionHandler::updatePermission)
.DELETE("/api/permissions/{id}", permissionHandler::deletePermission) .DELETE("/api/permissions/{id}", permissionHandler::deletePermission)
// ========== 手机号认证路由 ==========
.POST("/api/auth/phone/one-click-login", phoneAuthHandler::oneClickLogin)
.POST("/api/auth/phone/send-code", phoneAuthHandler::sendSmsCode)
.POST("/api/auth/phone/code-login", phoneAuthHandler::codeLogin)
// ========== 会员模块路由 - 微信认证 ========== // ========== 会员模块路由 - 微信认证 ==========
.POST("/api/member/auth/miniapp/login", wechatAuthHandler::miniappLogin) .POST("/api/member/auth/miniapp/login", wechatAuthHandler::miniappLogin)
.GET("/api/member/auth/mp/callback", wechatAuthHandler::verifyMpSignature) .GET("/api/member/auth/mp/callback", wechatAuthHandler::verifyMpSignature)
@@ -281,28 +263,12 @@ public class SystemRouter {
// ===== 团课课程管理 ===== // ===== 团课课程管理 =====
.GET("/api/groupCourse/list", groupCourseHandler::getAllGroupCourse) .GET("/api/groupCourse/list", groupCourseHandler::getAllGroupCourse)
.POST("/api/groupCourse/page", groupCourseHandler::getGroupCoursesByPage) .POST("/api/groupCourse/page", groupCourseHandler::getGroupCoursesByPage)
.GET("/api/groupCourse/{id}", groupCourseHandler::getGroupCourseById)
// ===== 团课类型管理 ===== .POST("/api/groupCourse", groupCourseHandler::createGroupCourse)
.GET("/api/groupCourse/types", groupCourseTypeHandler::getAllGroupCourseTypes) .PUT("/api/groupCourse/{id}", groupCourseHandler::updateGroupCourse)
.GET("/api/groupCourse/types/search", groupCourseTypeHandler::searchGroupCourseTypes) .DELETE("/api/groupCourse/{id}", groupCourseHandler::deleteGroupCourse)
.GET("/api/groupCourse/types/categories", groupCourseTypeHandler::getCategories) .POST("/api/groupCourse/{id}/cancel", groupCourseHandler::cancelGroupCourse)
.GET("/api/groupCourse/types/category/{category}", groupCourseTypeHandler::getGroupCourseTypesByCategory) .POST("/api/groupCourse/{courseId}/signin", groupCourseHandler::signIn)
.GET("/api/groupCourse/types/{id}", groupCourseTypeHandler::getGroupCourseTypeById)
.POST("/api/groupCourse/types", groupCourseTypeHandler::createGroupCourseType)
.PUT("/api/groupCourse/types/{id}", groupCourseTypeHandler::updateGroupCourseType)
.DELETE("/api/groupCourse/types/{id}", groupCourseTypeHandler::deleteGroupCourseType)
// ===== 团课标签管理 =====
.GET("/api/groupCourse/labels", courseLabelHandler::getAllLabels)
.GET("/api/groupCourse/labels/search", courseLabelHandler::searchLabels)
.GET("/api/groupCourse/labels/{id}", courseLabelHandler::getLabelById)
.GET("/api/groupCourse/types/{typeId}/labels", courseLabelHandler::getLabelsByTypeId)
.POST("/api/groupCourse/labels", courseLabelHandler::createLabel)
.PUT("/api/groupCourse/labels/{id}", courseLabelHandler::updateLabel)
.DELETE("/api/groupCourse/labels/{id}", courseLabelHandler::deleteLabel)
.POST("/api/groupCourse/types/{typeId}/labels", courseLabelHandler::addLabelsToType)
.DELETE("/api/groupCourse/types/{typeId}/labels/{labelId}", courseLabelHandler::removeLabelFromType)
.DELETE("/api/groupCourse/types/{typeId}/labels", courseLabelHandler::clearLabelsFromType)
// ===== 团课预约管理 ===== // ===== 团课预约管理 =====
.POST("/api/groupCourse/book", groupCourseBookingHandler::bookCourse) .POST("/api/groupCourse/book", groupCourseBookingHandler::bookCourse)
@@ -310,27 +276,6 @@ public class SystemRouter {
.GET("/api/groupCourse/bookings/member/{memberId}", groupCourseBookingHandler::getBookingsByMemberId) .GET("/api/groupCourse/bookings/member/{memberId}", groupCourseBookingHandler::getBookingsByMemberId)
.GET("/api/groupCourse/bookings/course/{courseId}", groupCourseBookingHandler::getBookingsByCourseId) .GET("/api/groupCourse/bookings/course/{courseId}", groupCourseBookingHandler::getBookingsByCourseId)
.GET("/api/groupCourse/bookings/{bookingId}", groupCourseBookingHandler::getBookingById) .GET("/api/groupCourse/bookings/{bookingId}", groupCourseBookingHandler::getBookingById)
// ===== 团课推荐管理 =====
.GET("/api/groupCourse/recommend/list", groupCourseRecommendHandler::getAllRecommendations)
.GET("/api/groupCourse/recommend/active", groupCourseRecommendHandler::getAllActiveRecommendations)
.GET("/api/groupCourse/recommend/{id}", groupCourseRecommendHandler::getRecommendationById)
.GET("/api/groupCourse/recommend/course/{courseId}", groupCourseRecommendHandler::getRecommendationsByCourseId)
.POST("/api/groupCourse/recommend", groupCourseRecommendHandler::createRecommendation)
.PUT("/api/groupCourse/recommend/{id}", groupCourseRecommendHandler::updateRecommendation)
.DELETE("/api/groupCourse/recommend/{id}", groupCourseRecommendHandler::deleteRecommendation)
.POST("/api/groupCourse/recommend/{id}/enable", groupCourseRecommendHandler::enableRecommendation)
.POST("/api/groupCourse/recommend/{id}/disable", groupCourseRecommendHandler::disableRecommendation)
// ===== 团课课程管理(需要放在具体路由之后)=====
.GET("/api/groupCourse/{id}", groupCourseHandler::getGroupCourseById)
.GET("/api/groupCourse/{id}/detail", groupCourseHandler::getGroupCourseDetailById)
.POST("/api/groupCourse", groupCourseHandler::createGroupCourse)
.PUT("/api/groupCourse/{id}", groupCourseHandler::updateGroupCourse)
.DELETE("/api/groupCourse/{id}", groupCourseHandler::deleteGroupCourse)
.POST("/api/groupCourse/{id}/cancel", groupCourseHandler::cancelGroupCourse)
.POST("/api/groupCourse/signin/{memberId}", groupCourseHandler::signIn)
.POST("/api/groupCourse/search", groupCourseHandler::searchGroupCourses)
// ========= 签到模块路由 ========== // ========= 签到模块路由 ==========
// ===== 签到核心功能 ===== // ===== 签到核心功能 =====
@@ -347,19 +292,6 @@ public class SystemRouter {
// ===== 签到数据导出 ===== // ===== 签到数据导出 =====
.GET("/api/checkIn/records/export", checkInHandler::exportSignInRecords) .GET("/api/checkIn/records/export", checkInHandler::exportSignInRecords)
// ========================================
// ========== 数据统计模块路由 ============
// ========================================
// ===== 数据统计核心功能 =====
.GET("/api/datacount/summary", dataStatisticsHandler::getStatisticsSummary)
.GET("/api/datacount/member", dataStatisticsHandler::getMemberStatistics)
.GET("/api/datacount/booking", dataStatisticsHandler::getBookingStatistics)
.GET("/api/datacount/signin", dataStatisticsHandler::getSignInStatistics)
.GET("/api/datacount/history", dataStatisticsHandler::queryHistoricalStatistics)
.GET("/api/datacount/export", dataStatisticsHandler::exportStatistics)
.build(); .build();
} }
} }
@@ -2,9 +2,9 @@ spring:
cache: cache:
type: none type: none
r2dbc: r2dbc:
url: r2dbc:postgresql://localhost:5432/manage_system url: r2dbc:postgresql://localhost:55432/manage_system
username: postgres username: novalon
password: 123456 password: novalon123
pool: pool:
initial-size: 5 initial-size: 5
max-size: 20 max-size: 20
@@ -12,10 +12,10 @@ spring:
max-life-time: 30m max-life-time: 30m
acquire-timeout: 3s acquire-timeout: 3s
flyway: flyway:
url: jdbc:postgresql://localhost:5432/manage_system url: jdbc:postgresql://localhost:55432/manage_system
user: postgres user: novalon
password: 123456 password: novalon123
enabled: false enabled: true
locations: classpath:db/migration locations: classpath:db/migration
baseline-on-migrate: true baseline-on-migrate: true
validate-on-migrate: true validate-on-migrate: true
@@ -4,8 +4,8 @@ spring:
activate: activate:
on-profile: local on-profile: local
r2dbc: r2dbc:
url: r2dbc:postgresql://localhost:5432/manage_system url: r2dbc:postgresql://localhost:55432/manage_system
username: postgres username: novalon
password: 123456 password: 123456
pool: pool:
initial-size: 5 initial-size: 5
@@ -14,8 +14,8 @@ spring:
max-life-time: 30m max-life-time: 30m
acquire-timeout: 3s acquire-timeout: 3s
datasource: datasource:
url: jdbc:postgresql://localhost:5432/manage_system url: jdbc:postgresql://localhost:55432/manage_system
username: postgres username: novalon
password: 123456 password: 123456
driver-class-name: org.postgresql.Driver driver-class-name: org.postgresql.Driver
flyway: flyway:
@@ -5,9 +5,9 @@ spring:
application: application:
name: manage-app name: manage-app
r2dbc: r2dbc:
url: r2dbc:postgresql://localhost:5432/manage_system url: r2dbc:postgresql://localhost:55432/manage_system
username: postgres username: novalon
password: 123456 password: novalon123
pool: pool:
initial-size: 5 initial-size: 5
max-size: 20 max-size: 20
@@ -15,9 +15,9 @@ spring:
exclude: exclude:
- org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration - org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
r2dbc: r2dbc:
url: r2dbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:manage_system} url: r2dbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:manage_system}
username: ${DB_USERNAME:postgres} username: ${DB_USERNAME:novalon}
password: ${DB_PASSWORD:123456} password: ${DB_PASSWORD:novalon123}
pool: pool:
initial-size: 10 initial-size: 10
max-size: 50 max-size: 50
@@ -25,12 +25,12 @@ spring:
max-life-time: 1h max-life-time: 1h
acquire-timeout: 5s acquire-timeout: 5s
datasource: datasource:
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:manage_system} url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:manage_system}
username: ${DB_USERNAME:postgres} username: ${DB_USERNAME:novalon}
password: ${DB_PASSWORD:123456} password: ${DB_PASSWORD:novalon123}
driver-class-name: org.postgresql.Driver driver-class-name: org.postgresql.Driver
flyway: flyway:
enabled: false enabled: true
locations: classpath:db/migration locations: classpath:db/migration
baseline-on-migrate: true baseline-on-migrate: true
baseline-version: 0 baseline-version: 0
@@ -54,7 +54,7 @@ spring:
profiles: profiles:
active: dev active: dev
config: config:
import: classpath:keys.properties,classpath:member-config.yml import: classpath:member-config.yml
@@ -94,13 +94,3 @@ springdoc:
show-actuator: false show-actuator: false
default-consumes-media-type: application/json default-consumes-media-type: application/json
default-produces-media-type: application/json default-produces-media-type: application/json
alibaba:
cloud:
sms:
access-key-id: ${ALIBABA_ACCESS_KEY_ID:}
access-key-secret: ${ALIBABA_ACCESS_KEY_SECRET:}
sign-name: ${ALIBABA_SMS_SIGN_NAME:}
template-code: ${ALIBABA_SMS_TEMPLATE_CODE:}
code-length: 6
code-expire-seconds: 300

Some files were not shown because too many files have changed in this diff Show More