Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db517a2da8 | |||
| 5237dfc1cb | |||
| 981d8ef211 | |||
| 244c599a82 | |||
| c822719f51 | |||
| 9753d7ebf5 | |||
| 5c5bc6419a | |||
| 47e9a65497 | |||
| 1a58ee63d2 | |||
| 0e7918b31e | |||
| 0e73bd4520 | |||
| f66ff5c8f8 | |||
| 005c09c99c | |||
| 08cf82ac83 | |||
| 174e33053e | |||
| 29b73c1f67 |
@@ -5,7 +5,7 @@ test.describe('认证和授权测试', () => {
|
|||||||
let userId: number;
|
let userId: number;
|
||||||
|
|
||||||
test.beforeAll(async ({ request }) => {
|
test.beforeAll(async ({ request }) => {
|
||||||
const response = await request.post('http://localhost:8080/api/auth/login', {
|
const response = await request.post('http://localhost:8080/api/admin/auth/login', {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
@@ -28,7 +28,7 @@ test.describe('认证和授权测试', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await test.step('发送登录请求', async () => {
|
await test.step('发送登录请求', async () => {
|
||||||
const response = await page.request.post('http://localhost:8080/api/auth/login', {
|
const response = await page.request.post('http://localhost:8080/api/admin/auth/login', {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
@@ -78,7 +78,7 @@ test.describe('认证和授权测试', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await test.step('查询指定用户信息', async () => {
|
await test.step('查询指定用户信息', async () => {
|
||||||
const response = await page.request.get(`http://localhost:8080/api/users/${userId}`, {
|
const response = await page.request.get(`http://localhost:8080/api/admin/users/${userId}`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${authToken}`
|
'Authorization': `Bearer ${authToken}`
|
||||||
}
|
}
|
||||||
@@ -98,10 +98,10 @@ test.describe('认证和授权测试', () => {
|
|||||||
test('权限验证测试', async ({ page }) => {
|
test('权限验证测试', async ({ page }) => {
|
||||||
await test.step('测试访问受保护的API', async () => {
|
await test.step('测试访问受保护的API', async () => {
|
||||||
const protectedEndpoints = [
|
const protectedEndpoints = [
|
||||||
'/api/users',
|
'/api/admin/users',
|
||||||
'/api/roles',
|
'/api/admin/roles',
|
||||||
'/api/menus',
|
'/api/admin/menus',
|
||||||
'/api/config'
|
'/api/admin/config'
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const endpoint of protectedEndpoints) {
|
for (const endpoint of protectedEndpoints) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ test.describe('参数配置功能测试', () => {
|
|||||||
let authToken: string;
|
let authToken: string;
|
||||||
|
|
||||||
test.beforeAll(async ({ request }) => {
|
test.beforeAll(async ({ request }) => {
|
||||||
const response = await request.post('http://localhost:8080/api/auth/login', {
|
const response = await request.post('http://localhost:8080/api/admin/auth/login', {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ test.describe('字典管理功能测试', () => {
|
|||||||
let authToken: string;
|
let authToken: string;
|
||||||
|
|
||||||
test.beforeAll(async ({ request }) => {
|
test.beforeAll(async ({ request }) => {
|
||||||
const response = await request.post('http://localhost:8080/api/auth/login', {
|
const response = await request.post('http://localhost:8080/api/admin/auth/login', {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ async function verifyAllServices(): Promise<void> {
|
|||||||
|
|
||||||
console.log(' 验证网关到后端的连通性...');
|
console.log(' 验证网关到后端的连通性...');
|
||||||
try {
|
try {
|
||||||
const response = await fetch('http://localhost:8080/api/auth/login', {
|
const response = await fetch('http://localhost:8080/api/admin/auth/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ username: 'admin', password: 'Test@123' }),
|
body: JSON.stringify({ username: 'admin', password: 'Test@123' }),
|
||||||
@@ -316,7 +316,7 @@ async function waitForBackendReady(): Promise<void> {
|
|||||||
console.log(`✅ 后端服务健康检查通过 (尝试 ${i + 1}/${maxRetries})`);
|
console.log(`✅ 后端服务健康检查通过 (尝试 ${i + 1}/${maxRetries})`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const loginTest = await fetch('http://localhost:8084/api/auth/login', {
|
const loginTest = await fetch('http://localhost:8084/api/admin/auth/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ username: 'admin', password: 'Test@123' }),
|
body: JSON.stringify({ username: 'admin', password: 'Test@123' }),
|
||||||
@@ -364,7 +364,7 @@ async function waitForGatewayReady(): Promise<void> {
|
|||||||
console.log(`✅ 网关服务健康检查通过 (尝试 ${i + 1}/${maxRetries})`);
|
console.log(`✅ 网关服务健康检查通过 (尝试 ${i + 1}/${maxRetries})`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const loginTest = await fetch('http://localhost:8080/api/auth/login', {
|
const loginTest = await fetch('http://localhost:8080/api/admin/auth/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ username: 'admin', password: 'Test@123' }),
|
body: JSON.stringify({ username: 'admin', password: 'Test@123' }),
|
||||||
@@ -425,7 +425,7 @@ async function waitForFrontendReady(): Promise<void> {
|
|||||||
async function cleanupTestData(): Promise<void> {
|
async function cleanupTestData(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// 登录获取token(通过网关)
|
// 登录获取token(通过网关)
|
||||||
const loginResponse = await fetch('http://localhost:8080/api/auth/login', {
|
const loginResponse = await fetch('http://localhost:8080/api/admin/auth/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -458,7 +458,7 @@ async function cleanupTestData(): Promise<void> {
|
|||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
if (user.id > 10) {
|
if (user.id > 10) {
|
||||||
try {
|
try {
|
||||||
await fetch(`http://localhost:8080/api/users/${user.id}`, {
|
await fetch(`http://localhost:8080/api/admin/users/${user.id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`
|
'Authorization': `Bearer ${token}`
|
||||||
@@ -486,7 +486,7 @@ async function cleanupTestData(): Promise<void> {
|
|||||||
for (const role of roles) {
|
for (const role of roles) {
|
||||||
if (role.id > 4) {
|
if (role.id > 4) {
|
||||||
try {
|
try {
|
||||||
await fetch(`http://localhost:8080/api/roles/${role.id}`, {
|
await fetch(`http://localhost:8080/api/admin/roles/${role.id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`
|
'Authorization': `Bearer ${token}`
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ test.describe('管理员完整工作流', () => {
|
|||||||
await test.step('提交表单', async () => {
|
await test.step('提交表单', async () => {
|
||||||
const [response] = await Promise.all([
|
const [response] = await Promise.all([
|
||||||
page.waitForResponse(resp =>
|
page.waitForResponse(resp =>
|
||||||
resp.url().includes('/api/roles') && resp.request().method() === 'POST',
|
resp.url().includes('/api/admin/roles') && resp.request().method() === 'POST',
|
||||||
{ timeout: 10000 }
|
{ timeout: 10000 }
|
||||||
).catch(() => null),
|
).catch(() => null),
|
||||||
page.locator('.el-dialog button:has-text("确定")').click()
|
page.locator('.el-dialog button:has-text("确定")').click()
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ test.describe('用户权限边界验证', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await test.step('尝试访问受限API', async () => {
|
await test.step('尝试访问受限API', async () => {
|
||||||
const response = await page.request.get('/api/users?page=0&size=10');
|
const response = await page.request.get('/api/admin/users?page=0&size=10');
|
||||||
expect([200, 401, 403]).toContain(response.status());
|
expect([200, 401, 403]).toContain(response.status());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ test.describe('菜单管理功能测试', () => {
|
|||||||
let authToken: string;
|
let authToken: string;
|
||||||
|
|
||||||
test.beforeAll(async ({ request }) => {
|
test.beforeAll(async ({ request }) => {
|
||||||
const response = await request.post('http://localhost:8080/api/auth/login', {
|
const response = await request.post('http://localhost:8080/api/admin/auth/login', {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async login(username: string, password: string): Promise<{ token: string; userId: number }> {
|
async login(username: string, password: string): Promise<{ token: string; userId: number }> {
|
||||||
const response = await this.request.post(`${this.baseURL}/api/auth/login`, {
|
const response = await this.request.post(`${this.baseURL}/api/admin/auth/login`, {
|
||||||
data: {
|
data: {
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
@@ -29,7 +29,7 @@ export class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async logout(token: string): Promise<void> {
|
async logout(token: string): Promise<void> {
|
||||||
await this.request.post(`${this.baseURL}/api/auth/logout`, {
|
await this.request.post(`${this.baseURL}/api/admin/auth/logout`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
@@ -37,7 +37,7 @@ export class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getUsers(token: string): Promise<any[]> {
|
async getUsers(token: string): Promise<any[]> {
|
||||||
const response = await this.request.get(`${this.baseURL}/api/users`, {
|
const response = await this.request.get(`${this.baseURL}/api/admin/users`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
@@ -51,7 +51,7 @@ export class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createUser(token: string, userData: any): Promise<any> {
|
async createUser(token: string, userData: any): Promise<any> {
|
||||||
const response = await this.request.post(`${this.baseURL}/api/users`, {
|
const response = await this.request.post(`${this.baseURL}/api/admin/users`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
@@ -66,7 +66,7 @@ export class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateUser(token: string, userId: number, userData: any): Promise<any> {
|
async updateUser(token: string, userId: number, userData: any): Promise<any> {
|
||||||
const response = await this.request.put(`${this.baseURL}/api/users/${userId}`, {
|
const response = await this.request.put(`${this.baseURL}/api/admin/users/${userId}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
@@ -81,7 +81,7 @@ export class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteUser(token: string, userId: number): Promise<void> {
|
async deleteUser(token: string, userId: number): Promise<void> {
|
||||||
const response = await this.request.delete(`${this.baseURL}/api/users/${userId}`, {
|
const response = await this.request.delete(`${this.baseURL}/api/admin/users/${userId}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
@@ -93,7 +93,7 @@ export class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getRoles(token: string): Promise<any[]> {
|
async getRoles(token: string): Promise<any[]> {
|
||||||
const response = await this.request.get(`${this.baseURL}/api/roles`, {
|
const response = await this.request.get(`${this.baseURL}/api/admin/roles`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
@@ -107,7 +107,7 @@ export class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createRole(token: string, roleData: any): Promise<any> {
|
async createRole(token: string, roleData: any): Promise<any> {
|
||||||
const response = await this.request.post(`${this.baseURL}/api/roles`, {
|
const response = await this.request.post(`${this.baseURL}/api/admin/roles`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
@@ -122,7 +122,7 @@ export class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteRole(token: string, roleId: number): Promise<void> {
|
async deleteRole(token: string, roleId: number): Promise<void> {
|
||||||
const response = await this.request.delete(`${this.baseURL}/api/roles/${roleId}`, {
|
const response = await this.request.delete(`${this.baseURL}/api/admin/roles/${roleId}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
@@ -134,7 +134,7 @@ export class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getMenus(token: string): Promise<any[]> {
|
async getMenus(token: string): Promise<any[]> {
|
||||||
const response = await this.request.get(`${this.baseURL}/api/menus`, {
|
const response = await this.request.get(`${this.baseURL}/api/admin/menus`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export class TestDataManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async createTestUser(request: APIRequestContext, userData: TestUser): Promise<any> {
|
static async createTestUser(request: APIRequestContext, userData: TestUser): Promise<any> {
|
||||||
const response = await request.post(`${this.apiBaseUrl}/api/users`, {
|
const response = await request.post(`${this.apiBaseUrl}/api/admin/users`, {
|
||||||
data: userData,
|
data: userData,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ export class TestDataManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async createTestRole(request: APIRequestContext, roleData: TestRole): Promise<any> {
|
static async createTestRole(request: APIRequestContext, roleData: TestRole): Promise<any> {
|
||||||
const response = await request.post(`${this.apiBaseUrl}/api/roles`, {
|
const response = await request.post(`${this.apiBaseUrl}/api/admin/roles`, {
|
||||||
data: roleData,
|
data: roleData,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ export class TestDataManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await request.delete(`${this.apiBaseUrl}/api/users/${userData.id}`);
|
const response = await request.delete(`${this.apiBaseUrl}/api/admin/users/${userData.id}`);
|
||||||
if (!response.ok()) {
|
if (!response.ok()) {
|
||||||
console.warn(`Failed to delete test user ${username}: ${await response.text()}`);
|
console.warn(`Failed to delete test user ${username}: ${await response.text()}`);
|
||||||
}
|
}
|
||||||
@@ -114,7 +114,7 @@ export class TestDataManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await request.delete(`${this.apiBaseUrl}/api/roles/${roleData.id}`);
|
const response = await request.delete(`${this.apiBaseUrl}/api/admin/roles/${roleData.id}`);
|
||||||
if (!response.ok()) {
|
if (!response.ok()) {
|
||||||
console.warn(`Failed to delete test role ${roleKey}: ${await response.text()}`);
|
console.warn(`Failed to delete test role ${roleKey}: ${await response.text()}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Compiled class file
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Log file
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# BlueJ files
|
||||||
|
*.ctxt
|
||||||
|
|
||||||
|
# Mobile Tools for Java (J2ME)
|
||||||
|
.mtj.tmp/
|
||||||
|
|
||||||
|
# Package Files
|
||||||
|
*.jar
|
||||||
|
*.war
|
||||||
|
*.nar
|
||||||
|
*.ear
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.rar
|
||||||
|
|
||||||
|
# Virtual machine crash logs
|
||||||
|
hs_err_pid*
|
||||||
|
replay_pid*
|
||||||
|
|
||||||
|
# Maven
|
||||||
|
target/
|
||||||
|
pom.xml.tag
|
||||||
|
pom.xml.releaseBackup
|
||||||
|
pom.xml.versionsBackup
|
||||||
|
pom.xml.next
|
||||||
|
release.properties
|
||||||
|
dependency-reduced-pom.xml
|
||||||
|
buildNumber.properties
|
||||||
|
.mvn/timing.properties
|
||||||
|
.mvn/wrapper/maven-wrapper.jar
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
.vscode/
|
||||||
|
.settings/
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
<?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-checkIn</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<name>Gym CheckIn</name>
|
||||||
|
<description>Check-In Management Module - Member Attendance 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>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-aop</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>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.projectreactor</groupId>
|
||||||
|
<artifactId>reactor-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.resilience4j</groupId>
|
||||||
|
<artifactId>resilience4j-spring-boot3</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.resilience4j</groupId>
|
||||||
|
<artifactId>resilience4j-reactor</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.testcontainers</groupId>
|
||||||
|
<artifactId>testcontainers</artifactId>
|
||||||
|
<version>1.21.4</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.testcontainers</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
|
<version>1.21.4</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.testcontainers</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<version>1.21.4</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.h2database</groupId>
|
||||||
|
<artifactId>h2</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.r2dbc</groupId>
|
||||||
|
<artifactId>r2dbc-h2</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.postgresql</groupId>
|
||||||
|
<artifactId>r2dbc-postgresql</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.hutool</groupId>
|
||||||
|
<artifactId>hutool-all</artifactId>
|
||||||
|
<version>5.8.25</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.zxing</groupId>
|
||||||
|
<artifactId>core</artifactId>
|
||||||
|
<version>3.5.1</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 添加 ZXing JavaSE 扩展(用于生成图片) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.zxing</groupId>
|
||||||
|
<artifactId>javase</artifactId>
|
||||||
|
<version>3.5.1</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||||
|
</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.mapstruct</groupId>
|
||||||
|
<artifactId>mapstruct-processor</artifactId>
|
||||||
|
<version>1.5.5.Final</version>
|
||||||
|
</path>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>${lombok.version}</version>
|
||||||
|
</path>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok-mapstruct-binding</artifactId>
|
||||||
|
<version>0.2.0</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.jacoco</groupId>
|
||||||
|
<artifactId>jacoco-maven-plugin</artifactId>
|
||||||
|
<version>0.8.12</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>prepare-agent</id>
|
||||||
|
<goals>
|
||||||
|
<goal>prepare-agent</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>report</id>
|
||||||
|
<phase>verify</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>report</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>check</id>
|
||||||
|
<phase>verify</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>check</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<rules>
|
||||||
|
<rule>
|
||||||
|
<element>BUNDLE</element>
|
||||||
|
<limits>
|
||||||
|
<limit>
|
||||||
|
<counter>INSTRUCTION</counter>
|
||||||
|
<value>COVEREDRATIO</value>
|
||||||
|
<minimum>0.60</minimum>
|
||||||
|
</limit>
|
||||||
|
</limits>
|
||||||
|
</rule>
|
||||||
|
</rules>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>com.github.spotbugs</groupId>
|
||||||
|
<artifactId>spotbugs-maven-plugin</artifactId>
|
||||||
|
<version>4.8.6.0</version>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.spotbugs</groupId>
|
||||||
|
<artifactId>spotbugs</artifactId>
|
||||||
|
<version>4.8.6</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>spotbugs-check</id>
|
||||||
|
<phase>verify</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>check</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<effort>Max</effort>
|
||||||
|
<threshold>High</threshold>
|
||||||
|
<failOnError>true</failOnError>
|
||||||
|
<excludeFilterFile>spotbugs-exclude.xml</excludeFilterFile>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
package cn.novalon.gym.manage.checkIn.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "qr.config")
|
||||||
|
public class QRCodeConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 二维码宽度(像素)
|
||||||
|
*/
|
||||||
|
private Integer width = 300;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 二维码高度(像素)
|
||||||
|
*/
|
||||||
|
private Integer height = 300;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 白边宽度
|
||||||
|
*/
|
||||||
|
private Integer margin = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 容错率:L, M, Q, H
|
||||||
|
*/
|
||||||
|
private String errorCorrection = "M";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片格式:png, jpg
|
||||||
|
*/
|
||||||
|
private String format = "png";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用Logo
|
||||||
|
*/
|
||||||
|
private Boolean logoEnabled = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logo路径
|
||||||
|
*/
|
||||||
|
private String logoPath = "";
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
package cn.novalon.gym.manage.checkIn.config;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.checkIn.websocket.MyWebSocketHandler;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.reactive.HandlerMapping;
|
||||||
|
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
|
||||||
|
import org.springframework.web.reactive.socket.WebSocketHandler;
|
||||||
|
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class WebSocketConfig {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MyWebSocketHandler myWebSocketHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册 WebSocket 路由映射
|
||||||
|
* 路径对应前端连接的 ws://xxx/webSocket/checkIn
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public HandlerMapping webSocketMapping() {
|
||||||
|
Map<String, WebSocketHandler> map = new HashMap<>();
|
||||||
|
map.put("/webSocket/checkIn", myWebSocketHandler);
|
||||||
|
|
||||||
|
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
|
||||||
|
mapping.setUrlMap(map);
|
||||||
|
mapping.setOrder(10); // 设置优先级
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册 WebSocket 处理器适配器(必须)
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public WebSocketHandlerAdapter handlerAdapter() {
|
||||||
|
return new WebSocketHandlerAdapter();
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
package cn.novalon.gym.manage.checkIn.constant;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打卡模块 Redis 键常量
|
||||||
|
*
|
||||||
|
* @author 付嘉
|
||||||
|
* @date 2026-05-30
|
||||||
|
*/
|
||||||
|
public final class QRRedisKey {
|
||||||
|
|
||||||
|
private static final String SEPARATOR = ":";
|
||||||
|
private static final String QRCODE_USER_DAILY = "qrcode:user:daily";
|
||||||
|
private static final String QRCODE_CONTENT = "QR_";
|
||||||
|
private QRRedisKey() {
|
||||||
|
// 私有构造,防止实例化
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户当日二维码
|
||||||
|
* 格式:qrcode:user:daily:{userId}:{date}
|
||||||
|
* 示例:qrcode:user:daily:1001:2026-05-30
|
||||||
|
*/
|
||||||
|
public static String qrcodeUserDaily(Long userId, LocalDate date) {
|
||||||
|
return QRCODE_USER_DAILY + SEPARATOR + userId + SEPARATOR + date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户当日二维码(今天)
|
||||||
|
*/
|
||||||
|
public static String qrcodeUserToday(Long userId) {
|
||||||
|
return qrcodeUserDaily(userId, LocalDate.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成二维码内容(每个用户每次调用都不同)
|
||||||
|
*/
|
||||||
|
public static String generateQrcodeContent() {
|
||||||
|
return QRCODE_CONTENT + UUID.randomUUID().toString().replace("-", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
package cn.novalon.gym.manage.checkIn.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class QRCodeDto {
|
||||||
|
|
||||||
|
private String qrContent;
|
||||||
|
|
||||||
|
private boolean isUsed;
|
||||||
|
}
|
||||||
+5
-2
@@ -1,6 +1,9 @@
|
|||||||
package cn.novalon.gym.manage.member.entity;
|
package cn.novalon.gym.manage.checkIn.entity;
|
||||||
|
|
||||||
import lombok.*;
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
import org.springframework.data.annotation.CreatedDate;
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
import org.springframework.data.annotation.Id;
|
import org.springframework.data.annotation.Id;
|
||||||
import org.springframework.data.relational.core.mapping.Column;
|
import org.springframework.data.relational.core.mapping.Column;
|
||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
package cn.novalon.gym.manage.checkIn.handler;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.checkIn.service.impl.CheckServiceImpl;
|
||||||
|
import cn.novalon.gym.manage.sys.util.AuthUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
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
|
||||||
|
public class CheckInHandler {
|
||||||
|
|
||||||
|
private final AuthUtil authUtil;
|
||||||
|
private final CheckServiceImpl checkService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签到
|
||||||
|
*
|
||||||
|
* POST /api/checkIn
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public Mono<ServerResponse> checkIn(ServerRequest request) {
|
||||||
|
|
||||||
|
Long memberId = 1L;
|
||||||
|
// authUtil.getMemberIdOrThrow(request);
|
||||||
|
return request.bodyToMono(Map.class)
|
||||||
|
.flatMap(body -> {
|
||||||
|
String qrContent = (String) body.get("qrContent");
|
||||||
|
log.info("收到签到请求, memberId: {}, qrContent: {}", memberId, qrContent);
|
||||||
|
|
||||||
|
return checkService.checkIn(memberId, qrContent)
|
||||||
|
.flatMap(result -> ServerResponse.ok()
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.bodyValue(Map.of("code", 200, "message", "签到成功")));
|
||||||
|
})
|
||||||
|
.onErrorResume(e -> {
|
||||||
|
log.error("签到失败", e);
|
||||||
|
return ServerResponse.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.bodyValue(Map.of("code", 400, "message", e.getMessage()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取二维码
|
||||||
|
*
|
||||||
|
* GET /api/checkin/qrcode
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public Mono<ServerResponse> getQRCode(ServerRequest request) {
|
||||||
|
|
||||||
|
Long memberId = 1L;
|
||||||
|
// authUtil.getMemberIdOrThrow(request);
|
||||||
|
|
||||||
|
log.info("收到用户{}获取二维码请求", memberId);
|
||||||
|
|
||||||
|
return checkService.getQRCode(memberId)
|
||||||
|
.flatMap(qrCodeVo -> ServerResponse.ok()
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.bodyValue(qrCodeVo));
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package cn.novalon.gym.manage.checkIn.service;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.checkIn.vo.QRCodeVo;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
public interface ICheckInService {
|
||||||
|
|
||||||
|
Mono<QRCodeVo> getQRCode(Long memberId);
|
||||||
|
|
||||||
|
Mono<String> checkIn(Long memberId, String qrContent);
|
||||||
|
}
|
||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
package cn.novalon.gym.manage.checkIn.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
|
import cn.hutool.extra.qrcode.QrCodeUtil;
|
||||||
|
import cn.hutool.extra.qrcode.QrConfig;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import cn.novalon.gym.manage.checkIn.config.QRCodeConfig;
|
||||||
|
import cn.novalon.gym.manage.checkIn.constant.QRRedisKey;
|
||||||
|
import cn.novalon.gym.manage.checkIn.service.ICheckInService;
|
||||||
|
import cn.novalon.gym.manage.checkIn.vo.QRCodeVo;
|
||||||
|
import cn.novalon.gym.manage.common.constant.RedisKeyConstants;
|
||||||
|
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CheckServiceImpl implements ICheckInService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private final QRCodeConfig qrCodeConfig;
|
||||||
|
|
||||||
|
private final RedisUtil redisUtil;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<QRCodeVo> getQRCode(Long memberId) {
|
||||||
|
log.info("开始查询会员信息");
|
||||||
|
// TODO: 获取会员信息 - 查会员卡有效期/剩余次数,过期返回,先查缓存,缓存不存在则查数据库
|
||||||
|
// if (member有效期过了) throw new RuntimeException("会员有效期已过,拒绝生成二维码");
|
||||||
|
log.info("会员信息查询完成");
|
||||||
|
|
||||||
|
log.info("开始生成二维码");
|
||||||
|
String qrContent = QRRedisKey.generateQrcodeContent();
|
||||||
|
Map<String, Object> redisMap = new HashMap<>();
|
||||||
|
redisMap.put("qrContent", qrContent);
|
||||||
|
redisMap.put("isUsed", false);
|
||||||
|
|
||||||
|
return redisUtil.setWithExpire(
|
||||||
|
RedisKeyConstants.QRCODE_USER_DAILY+memberId+LocalDate.now(),
|
||||||
|
redisMap,
|
||||||
|
getSecondsUntilEndOfDay()
|
||||||
|
)
|
||||||
|
.then(Mono.fromSupplier(() -> {
|
||||||
|
String qrCodeBase64 = QrCodeUtil.generateAsBase64(qrContent,
|
||||||
|
BeanUtil.copyProperties(qrCodeConfig, QrConfig.class), "png");
|
||||||
|
return new QRCodeVo(qrCodeBase64,false,qrCodeConfig.getWidth(),qrCodeConfig.getHeight());
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<String> checkIn(Long memberId, String qrContent) {
|
||||||
|
String key = RedisKeyConstants.QRCODE_USER_DAILY+memberId+LocalDate.now();
|
||||||
|
|
||||||
|
return redisUtil.get(key)
|
||||||
|
.flatMap(cachedQrContent -> {
|
||||||
|
if (cachedQrContent != null) {
|
||||||
|
// 匹配成功,执行签到逻辑
|
||||||
|
Map<String, Object> map = JSONUtil.parseObj(cachedQrContent);
|
||||||
|
if(map.get("qrContent").equals(qrContent)){
|
||||||
|
if((boolean)map.get("isUsed")){
|
||||||
|
log.error("重复签到");
|
||||||
|
throw new RuntimeException("您已经在"+map.get("checkInTime")+"完成签到,请勿重复签到");
|
||||||
|
}
|
||||||
|
log.info("二维码匹配成功,memberId: {}", memberId);
|
||||||
|
// TODO查会员卡缓存,按照卡有效期进行扣减次数,没有缓存查数据库
|
||||||
|
map.put("isUsed", true);
|
||||||
|
map.put("checkInTime", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||||
|
|
||||||
|
return redisUtil.set(key,map).
|
||||||
|
then(Mono.just("签到成功"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new RuntimeException("二维码无效");
|
||||||
|
})
|
||||||
|
.switchIfEmpty(Mono.error(new RuntimeException("二维码已过期或不存在")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getSecondsUntilEndOfDay() {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
LocalDateTime endOfDay = now.toLocalDate().atTime(23, 59, 59);
|
||||||
|
|
||||||
|
if (now.isAfter(endOfDay)) return 1;
|
||||||
|
|
||||||
|
return ChronoUnit.SECONDS.between(now, endOfDay);
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package cn.novalon.gym.manage.checkIn.vo;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class QRCodeVo {
|
||||||
|
|
||||||
|
private String qrCodeBase64;
|
||||||
|
|
||||||
|
private boolean isUsed;
|
||||||
|
|
||||||
|
private Integer width;
|
||||||
|
|
||||||
|
private Integer height;
|
||||||
|
}
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
package cn.novalon.gym.manage.checkIn.websocket;
|
||||||
|
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import cn.novalon.gym.manage.checkIn.dto.QRCodeDto;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.reactive.socket.WebSocketHandler;
|
||||||
|
import org.springframework.web.reactive.socket.WebSocketMessage;
|
||||||
|
import org.springframework.web.reactive.socket.WebSocketSession;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class MyWebSocketHandler implements WebSocketHandler {
|
||||||
|
|
||||||
|
// 存储所有连接
|
||||||
|
private static final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> handle(WebSocketSession session) {
|
||||||
|
String sessionId = session.getId();
|
||||||
|
|
||||||
|
// 连接建立
|
||||||
|
sessions.put(sessionId, session);
|
||||||
|
log.info("WebSocket 连接建立,sessionId:{},当前连接数:{}", sessionId, sessions.size());
|
||||||
|
|
||||||
|
// 处理接收到的消息
|
||||||
|
Flux<WebSocketMessage> output = session.receive()
|
||||||
|
.doOnNext(message -> {
|
||||||
|
String payload = message.getPayloadAsText();
|
||||||
|
log.info("收到消息:{}", payload);
|
||||||
|
})
|
||||||
|
.map(message -> {
|
||||||
|
String payload = message.getPayloadAsText();
|
||||||
|
String response = processMessage(payload, sessionId);
|
||||||
|
return session.textMessage(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 连接关闭时清理
|
||||||
|
return session.send(output)
|
||||||
|
.doFinally(signalType -> {
|
||||||
|
sessions.remove(sessionId);
|
||||||
|
log.info("WebSocket 连接关闭,sessionId:{},剩余连接数:{}", sessionId, sessions.size());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理消息逻辑
|
||||||
|
*/
|
||||||
|
private String processMessage(String message, String sessionId) {
|
||||||
|
try {
|
||||||
|
// 解析 QRCodeDto
|
||||||
|
QRCodeDto qrCodeDto = JSONUtil.toBean(message, QRCodeDto.class);
|
||||||
|
|
||||||
|
String response;
|
||||||
|
|
||||||
|
// 判断二维码是否有效
|
||||||
|
if (qrCodeDto.getQrContent() != null
|
||||||
|
&& !qrCodeDto.getQrContent().isEmpty()
|
||||||
|
&& !qrCodeDto.isUsed()) {
|
||||||
|
// 有效:qrContent 有值且 isUsed 为 false
|
||||||
|
response = "正在进行签到";
|
||||||
|
|
||||||
|
// 可选:将二维码标记为已使用(需要调用后端服务)
|
||||||
|
// checkInService.handleCheckIn(qrCodeDto.getQrContent());
|
||||||
|
|
||||||
|
log.info("二维码有效,sessionId:{},qrContent:{}", sessionId, qrCodeDto.getQrContent());
|
||||||
|
} else {
|
||||||
|
// 无效:qrContent 为空 或 isUsed 为 true
|
||||||
|
String reason = "";
|
||||||
|
if (qrCodeDto.getQrContent() == null || qrCodeDto.getQrContent().isEmpty()) {
|
||||||
|
reason = "二维码内容为空";
|
||||||
|
} else if (qrCodeDto.isUsed()) {
|
||||||
|
reason = "二维码已被使用";
|
||||||
|
}
|
||||||
|
response = "二维码无效:" + reason;
|
||||||
|
log.warn("二维码无效,sessionId:{},原因:{}", sessionId, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("解析消息失败,sessionId:{}", sessionId, e);
|
||||||
|
return "消息格式错误";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前在线连接数
|
||||||
|
*/
|
||||||
|
public static int getOnlineCount() {
|
||||||
|
return sessions.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# 二维码配置
|
||||||
|
qr:
|
||||||
|
config:
|
||||||
|
width: 300 # 二维码宽度(像素)
|
||||||
|
height: 300 # 二维码高度(像素)
|
||||||
|
margin: 1 # 白边宽度(像素)
|
||||||
|
format: png # 图片格式:png / jpg
|
||||||
|
error-correction: L #容错率:L, M, Q, H,如果启用Logo(logo-enabled: true),必须设置为 H
|
||||||
|
logo-enabled: false # 是否启用Logo(启用时error-correction必须为H)
|
||||||
|
# logo-path: static/logo.png # Logo图片路径(支持相对路径或绝对路径)
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package cn.novalon.gym.manage.checkin;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
public class CheckInModuleTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void contextLoads() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Test Configuration
|
||||||
-3
@@ -12,9 +12,6 @@ public class SearchMemberDto {
|
|||||||
// 搜索字段 - 包括 会员号、昵称、手机号
|
// 搜索字段 - 包括 会员号、昵称、手机号
|
||||||
private String searchValue;
|
private String searchValue;
|
||||||
|
|
||||||
// 排序
|
|
||||||
private String filter;
|
|
||||||
|
|
||||||
// 页码
|
// 页码
|
||||||
private Integer pageNum = 1;
|
private Integer pageNum = 1;
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -1,7 +1,9 @@
|
|||||||
package cn.novalon.gym.manage.member.dto;
|
package cn.novalon.gym.manage.member.dto;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.member.enums.GenderEnum;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,10 +19,10 @@ public class UpdateMemberInfoDto {
|
|||||||
private String nickname;
|
private String nickname;
|
||||||
|
|
||||||
// 性别
|
// 性别
|
||||||
private Integer gender;
|
private GenderEnum gender;
|
||||||
|
|
||||||
// 生日
|
// 生日
|
||||||
private Date birthday;
|
private LocalDate birthday;
|
||||||
|
|
||||||
// 头像
|
// 头像
|
||||||
private String avatar;
|
private String avatar;
|
||||||
|
|||||||
+3
@@ -33,6 +33,9 @@ public abstract class BaseEntity implements Persistable<Long> {
|
|||||||
@Column("updated_at")
|
@Column("updated_at")
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@Column("deleted_at")
|
||||||
|
private LocalDateTime deletedAt;
|
||||||
|
|
||||||
// 判断当前实体是否是新建的
|
// 判断当前实体是否是新建的
|
||||||
@Override
|
@Override
|
||||||
public boolean isNew() {
|
public boolean isNew() {
|
||||||
|
|||||||
+3
-6
@@ -1,13 +1,10 @@
|
|||||||
package cn.novalon.gym.manage.member.entity;
|
package cn.novalon.gym.manage.member.entity;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.*;
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import org.springframework.data.relational.core.mapping.Column;
|
import org.springframework.data.relational.core.mapping.Column;
|
||||||
import org.springframework.data.relational.core.mapping.Table;
|
import org.springframework.data.relational.core.mapping.Table;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
@@ -44,7 +41,7 @@ public class Member extends BaseEntity {
|
|||||||
|
|
||||||
//生日
|
//生日
|
||||||
@Column("birthday")
|
@Column("birthday")
|
||||||
private Date birthday;
|
private LocalDate birthday;
|
||||||
|
|
||||||
//地址
|
//地址
|
||||||
@Column("address")
|
@Column("address")
|
||||||
|
|||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
package cn.novalon.gym.manage.member.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 性别枚举
|
||||||
|
*
|
||||||
|
* @author 付嘉
|
||||||
|
* @date 2026-05-29
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum GenderEnum {
|
||||||
|
|
||||||
|
UNKNOWN(0, "未知"),
|
||||||
|
MALE(1, "男"),
|
||||||
|
FEMALE(2, "女");
|
||||||
|
|
||||||
|
private final Integer code;
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
GenderEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GenderEnum fromCode(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return UNKNOWN;
|
||||||
|
}
|
||||||
|
for (GenderEnum gender : values()) {
|
||||||
|
if (gender.code.equals(code)) {
|
||||||
|
return gender;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -1,12 +1,18 @@
|
|||||||
package cn.novalon.gym.manage.member.es.entity;
|
package cn.novalon.gym.manage.member.es.entity;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
import org.springframework.data.annotation.Id;
|
import org.springframework.data.annotation.Id;
|
||||||
import org.springframework.data.elasticsearch.annotations.Document;
|
import org.springframework.data.elasticsearch.annotations.Document;
|
||||||
import org.springframework.data.elasticsearch.annotations.Field;
|
import org.springframework.data.elasticsearch.annotations.Field;
|
||||||
import org.springframework.data.elasticsearch.annotations.FieldType;
|
import org.springframework.data.elasticsearch.annotations.FieldType;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
@Document(indexName = "gym_members")
|
@Document(indexName = "gym_members")
|
||||||
public class MemberES {
|
public class MemberES {
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -2,6 +2,7 @@ package cn.novalon.gym.manage.member.es.repository;
|
|||||||
|
|
||||||
import cn.novalon.gym.manage.member.es.entity.MemberES;
|
import cn.novalon.gym.manage.member.es.entity.MemberES;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.elasticsearch.annotations.Query;
|
||||||
import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository;
|
import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
@@ -15,6 +16,6 @@ public interface MemberESRepository extends ReactiveElasticsearchRepository<Memb
|
|||||||
/**
|
/**
|
||||||
* 前台通用搜索:会员号(精确匹配) 或 昵称(模糊匹配) 或 手机号(精确匹配)并且 性别筛选(精确匹配)
|
* 前台通用搜索:会员号(精确匹配) 或 昵称(模糊匹配) 或 手机号(精确匹配)并且 性别筛选(精确匹配)
|
||||||
*/
|
*/
|
||||||
Flux<MemberES> findByMemberNoOrPhoneOrNicknameContainingAndGender(
|
Flux<MemberES> findByMemberNoOrPhoneOrNicknameContaining(
|
||||||
String memberNo, String phone, String nickname,String gender, Pageable pageable);
|
String memberNo, String phone, String nickname, Pageable pageable);
|
||||||
}
|
}
|
||||||
+53
-99
@@ -8,8 +8,11 @@ import cn.novalon.gym.manage.member.service.MemberService;
|
|||||||
import cn.novalon.gym.manage.member.service.WechatAuthService;
|
import cn.novalon.gym.manage.member.service.WechatAuthService;
|
||||||
import cn.novalon.gym.manage.member.service.WechatOfficialService;
|
import cn.novalon.gym.manage.member.service.WechatOfficialService;
|
||||||
import cn.novalon.gym.manage.member.util.AesUtil;
|
import cn.novalon.gym.manage.member.util.AesUtil;
|
||||||
|
import cn.novalon.gym.manage.member.util.WechatPhoneUtil;
|
||||||
import cn.novalon.gym.manage.sys.util.AuthUtil;
|
import cn.novalon.gym.manage.sys.util.AuthUtil;
|
||||||
import cn.novalon.gym.manage.sys.security.JwtTokenProvider;
|
import cn.novalon.gym.manage.sys.security.JwtTokenProvider;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
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;
|
||||||
@@ -29,24 +32,18 @@ import reactor.core.publisher.Mono;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@Tag(name = "会员管理", description = "会员信息管理、微信绑定、服务号关注等")
|
||||||
public class MemberHandler {
|
public class MemberHandler {
|
||||||
|
|
||||||
private final MemberService memberService;
|
private final MemberService memberService;
|
||||||
private final WechatAuthService wechatAuthService;
|
private final WechatAuthService wechatAuthService;
|
||||||
private final WechatOfficialService wechatOfficialService;
|
private final WechatOfficialService wechatOfficialService;
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
|
||||||
private final WechatProperties wechatProperties;
|
|
||||||
private final AuthUtil authUtil;
|
private final AuthUtil authUtil;
|
||||||
|
|
||||||
/**
|
@Operation(summary = "获取会员信息", description = "根据当前登录用户获取会员基本信息")
|
||||||
* 获取会员信息
|
|
||||||
*
|
|
||||||
* GET /api/member/info
|
|
||||||
* header: { "Authorization": "Bearer xxx" }
|
|
||||||
*/
|
|
||||||
public Mono<ServerResponse> getMemberInfo(ServerRequest request) {
|
public Mono<ServerResponse> getMemberInfo(ServerRequest request) {
|
||||||
|
|
||||||
Long memberId = authUtil.getMemberIdOrThrow(request);
|
Long memberId = authUtil.getMemberUserIdOrThrow(request);
|
||||||
|
|
||||||
log.info("获取会员信息, memberId: {}", memberId);
|
log.info("获取会员信息, memberId: {}", memberId);
|
||||||
|
|
||||||
@@ -56,22 +53,10 @@ public class MemberHandler {
|
|||||||
.bodyValue(info));
|
.bodyValue(info));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Operation(summary = "更新会员信息", description = "更新会员昵称、性别、生日、头像、地址等信息")
|
||||||
* 更新会员信息
|
|
||||||
*
|
|
||||||
* PUT /api/member/info
|
|
||||||
* header: { "Authorization": "Bearer xxx" }
|
|
||||||
* Body: {
|
|
||||||
* "nickname": "新昵称",
|
|
||||||
* "gender": 1,
|
|
||||||
* "birthday": "2000-01-01",
|
|
||||||
* "avatar": "https://example.com/avatar.jpg",
|
|
||||||
* "address": "北京市朝阳区"
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
public Mono<ServerResponse> updateMemberInfo(ServerRequest request) {
|
public Mono<ServerResponse> updateMemberInfo(ServerRequest request) {
|
||||||
|
|
||||||
Long memberId = authUtil.getMemberIdOrThrow(request);
|
Long memberId = authUtil.getMemberUserIdOrThrow(request);
|
||||||
|
|
||||||
log.info("更新会员信息, memberId: {}", memberId);
|
log.info("更新会员信息, memberId: {}", memberId);
|
||||||
|
|
||||||
@@ -82,14 +67,10 @@ public class MemberHandler {
|
|||||||
.bodyValue(info));
|
.bodyValue(info));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Operation(summary = "绑定手机号", description = "通过微信小程序手机号code绑定会员手机号")
|
||||||
* 绑定手机号(微信小程序)
|
|
||||||
* header: { "Authorization": "Bearer xxx" }
|
|
||||||
* POST /api/member/phone/bind?code=PHONE_CODE
|
|
||||||
*/
|
|
||||||
public Mono<ServerResponse> bindPhone(ServerRequest request) {
|
public Mono<ServerResponse> bindPhone(ServerRequest request) {
|
||||||
|
|
||||||
Long memberId = authUtil.getMemberIdOrThrow(request);
|
Long memberId = authUtil.getMemberUserIdOrThrow(request);
|
||||||
|
|
||||||
String phoneCode = request.queryParam("phoneCode").orElse("");
|
String phoneCode = request.queryParam("phoneCode").orElse("");
|
||||||
|
|
||||||
@@ -103,15 +84,10 @@ public class MemberHandler {
|
|||||||
.bodyValue(success));
|
.bodyValue(success));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Operation(summary = "查询服务号关注状态", description = "查询会员是否关注微信服务号")
|
||||||
* 查询服务号关注状态
|
|
||||||
*
|
|
||||||
* GET /api/member/subscribe/status
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public Mono<ServerResponse> checkSubscribeStatus(ServerRequest request) {
|
public Mono<ServerResponse> checkSubscribeStatus(ServerRequest request) {
|
||||||
|
|
||||||
Long memberId = authUtil.getMemberIdOrThrow(request);
|
Long memberId = authUtil.getMemberUserIdOrThrow(request);
|
||||||
|
|
||||||
log.info("查询服务号关注状态, memberId: {}", memberId);
|
log.info("查询服务号关注状态, memberId: {}", memberId);
|
||||||
|
|
||||||
@@ -123,17 +99,10 @@ public class MemberHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Operation(summary = "管理员更新手机号", description = "后台管理员为会员更新手机号")
|
||||||
* 管理员更新手机号
|
|
||||||
*
|
|
||||||
* POST /api/admin/member/123/phone
|
|
||||||
* header: { "Authorization": "Bearer xxx" }
|
|
||||||
* Body: { "phone": "13800138000" }
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public Mono<ServerResponse> adminUpdatePhone(ServerRequest request) {
|
public Mono<ServerResponse> adminUpdatePhone(ServerRequest request) {
|
||||||
|
|
||||||
Long adminId = authUtil.getMemberIdOrThrow(request);
|
Long adminId = authUtil.getAdminUserIdOrThrow(request);
|
||||||
|
|
||||||
String memberIdStr = request.pathVariable("id");
|
String memberIdStr = request.pathVariable("id");
|
||||||
long memberId = NumberUtils.toLong(memberIdStr, 0L);
|
long memberId = NumberUtils.toLong(memberIdStr, 0L);
|
||||||
@@ -162,16 +131,10 @@ public class MemberHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Operation(summary = "管理员查看会员详情", description = "后台管理员查看指定会员的详细信息")
|
||||||
* 前台查看会员信息
|
|
||||||
*
|
|
||||||
* GET /api/admin/member/{id}
|
|
||||||
* header: { "Authorization": "xxx" }
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public Mono<ServerResponse> adminGetMemberInfo(ServerRequest request) {
|
public Mono<ServerResponse> adminGetMemberInfo(ServerRequest request) {
|
||||||
|
|
||||||
Long adminId = authUtil.getMemberIdOrThrow(request);
|
Long adminId = authUtil.getAdminUserIdOrThrow(request);
|
||||||
|
|
||||||
String memberIdStr = request.pathVariable("id");
|
String memberIdStr = request.pathVariable("id");
|
||||||
long memberId = NumberUtils.toLong(memberIdStr, 0L);
|
long memberId = NumberUtils.toLong(memberIdStr, 0L);
|
||||||
@@ -179,64 +142,61 @@ public class MemberHandler {
|
|||||||
|
|
||||||
log.info("前台查看会员信息, adminId: {}, memberId: {}", adminId, memberId);
|
log.info("前台查看会员信息, adminId: {}, memberId: {}", adminId, memberId);
|
||||||
|
|
||||||
// TODO 多表查询:会员信息、团课信息、会员卡信息
|
return memberService.getMemberDetail(memberId)
|
||||||
|
.flatMap(detail -> {
|
||||||
return ServerResponse.ok()
|
if (detail.getPhone() != null && !detail.getPhone().isEmpty()) {
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
try {
|
||||||
.bodyValue("成功");
|
String decryptedPhone = AesUtil.decrypt(detail.getPhone());
|
||||||
|
detail.setPhone(WechatPhoneUtil.maskPhone(decryptedPhone));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("手机号解密失败, memberId: {}", detail.getId(), e);
|
||||||
|
detail.setPhone(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ServerResponse.ok()
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.bodyValue(detail);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Operation(summary = "管理员编辑会员信息", description = "后台管理员编辑会员信息")
|
||||||
* 前台编辑会员信息
|
|
||||||
*
|
|
||||||
* PUT /api/admin/member/{id}
|
|
||||||
* header: { "Authorization": "xxx" }
|
|
||||||
* Body:{"字段","值"}
|
|
||||||
*/
|
|
||||||
public Mono<ServerResponse> adminUpdateMemberInfo(ServerRequest request) {
|
public Mono<ServerResponse> adminUpdateMemberInfo(ServerRequest request) {
|
||||||
|
|
||||||
Long adminId = authUtil.getMemberIdOrThrow(request);
|
Long adminId = authUtil.getAdminUserIdOrThrow(request);
|
||||||
|
|
||||||
String memberIdStr = request.pathVariable("id");
|
String memberIdStr = request.pathVariable("id");
|
||||||
long memberId = NumberUtils.toLong(memberIdStr, 0L);
|
long memberId = NumberUtils.toLong(memberIdStr, 0L);
|
||||||
if(memberId <= 0L) throw new IllegalArgumentException("会员ID格式错误");
|
if(memberId <= 0L) throw new IllegalArgumentException("会员ID格式错误");
|
||||||
|
|
||||||
|
// TODO: 补充签到记录
|
||||||
log.info("前台编辑会员信息, adminId: {}, memberId: {}", adminId, memberId);
|
log.info("前台编辑会员信息, adminId: {}, memberId: {}", adminId, memberId);
|
||||||
|
|
||||||
// TODO 多表查询:会员信息、团课信息、会员卡信息
|
return request.bodyToMono(UpdateMemberInfoDto.class)
|
||||||
|
.flatMap(updateDto -> memberService.adminUpdateMemberInfo(memberId, updateDto))
|
||||||
return ServerResponse.ok()
|
.flatMap(detail -> ServerResponse.ok()
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.bodyValue("成功");
|
.bodyValue(detail));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Operation(summary = "搜索会员列表", description = "后台管理员按关键词搜索会员,支持性别筛选和分页")
|
||||||
* 前台搜索会员列表
|
|
||||||
*
|
|
||||||
* GET /api/admin/members?searchValue=手机号/姓名/会员号&filter=男/女&pageNum=1&pageSize=10
|
|
||||||
* header: { "Authorization": "Bearer xxx" }
|
|
||||||
*/
|
|
||||||
public Mono<ServerResponse> searchMembers(ServerRequest request) {
|
public Mono<ServerResponse> searchMembers(ServerRequest request) {
|
||||||
|
|
||||||
Long adminId = authUtil.getMemberIdOrThrow(request);
|
Long adminId = authUtil.getAdminUserIdOrThrow(request);
|
||||||
|
|
||||||
String keyword = request.queryParam("searchValue").orElse(null);
|
String keyword = request.queryParam("searchValue").orElse(null);
|
||||||
String filter = request.queryParam("filter").orElse(null);
|
Integer pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1);
|
||||||
int pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1);
|
Integer pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10);
|
||||||
int pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10);
|
|
||||||
|
|
||||||
log.info("前台搜索会员列表, adminId: {}, keyword: {}, filter: {}, pageNum: {}, pageSize: {}",
|
log.info("前台搜索会员列表, adminId: {}, keyword: {},pageNum: {}, pageSize: {}",
|
||||||
adminId, keyword, filter, pageNum, pageSize);
|
adminId, keyword, pageNum, pageSize);
|
||||||
|
|
||||||
return memberService.searchMember(new SearchMemberDto(keyword, filter, pageNum, pageSize))
|
return memberService.searchMember(new SearchMemberDto(keyword, pageNum, pageSize))
|
||||||
.map(member -> {
|
.map(member -> {
|
||||||
// 解密手机号
|
// 解密手机号
|
||||||
if (member.getPhone() != null && !member.getPhone().isEmpty()) {
|
if (member.getPhone() != null && !member.getPhone().isEmpty()) {
|
||||||
try {
|
try {
|
||||||
String secretKey = wechatProperties.getPhoneEncryption().getSecretKey();
|
String decryptedPhone = AesUtil.decrypt(member.getPhone());
|
||||||
String iv = wechatProperties.getPhoneEncryption().getIv();
|
member.setPhone(WechatPhoneUtil.maskPhone(decryptedPhone));
|
||||||
String decryptedPhone = AesUtil.decrypt(member.getPhone(), secretKey, iv);
|
|
||||||
member.setPhone(decryptedPhone);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("手机号解密失败, memberId: {}", member.getId(), e);
|
log.error("手机号解密失败, memberId: {}", member.getId(), e);
|
||||||
member.setPhone(null);
|
member.setPhone(null);
|
||||||
@@ -249,30 +209,24 @@ public class MemberHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
@Operation(summary = "查看会员列表", description = "后台管理员分页查看所有会员列表")
|
||||||
* 前台查看会员列表
|
|
||||||
*
|
|
||||||
* GET /api/admin/members/all?pageNum=1&pageSize=10
|
|
||||||
* header: { "Authorization": "Bearer xxx" }
|
|
||||||
*/
|
|
||||||
public Mono<ServerResponse> getAllMembers(ServerRequest request) {
|
public Mono<ServerResponse> getAllMembers(ServerRequest request) {
|
||||||
|
|
||||||
Long adminId = authUtil.getMemberIdOrThrow(request);
|
Long adminId = authUtil.getAdminUserIdOrThrow(request);
|
||||||
|
|
||||||
int pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1);
|
int pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1);
|
||||||
int pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10);
|
int pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10);
|
||||||
|
|
||||||
log.info("前台查看会员列表, adminId: {}, pageNum: {}, pageSize: {}", adminId, pageNum, pageSize);
|
log.info("前台查看会员列表, adminId: {}, pageNum: {}, pageSize: {}", adminId, pageNum, pageSize);
|
||||||
|
// TODO: 补充签到记录
|
||||||
|
|
||||||
return memberService.findAll(pageNum, pageSize)
|
return memberService.findAll(pageNum, pageSize)
|
||||||
.map(member -> {
|
.map(member -> {
|
||||||
// 解密手机号
|
// 解密手机号
|
||||||
if (member.getPhone() != null && !member.getPhone().isEmpty()) {
|
if (member.getPhone() != null && !member.getPhone().isEmpty()) {
|
||||||
try {
|
try {
|
||||||
String secretKey = wechatProperties.getPhoneEncryption().getSecretKey();
|
String decryptedPhone = AesUtil.decrypt(member.getPhone());
|
||||||
String iv = wechatProperties.getPhoneEncryption().getIv();
|
member.setPhone(WechatPhoneUtil.maskPhone(decryptedPhone));
|
||||||
String decryptedPhone = AesUtil.decrypt(member.getPhone(), secretKey, iv);
|
|
||||||
member.setPhone(decryptedPhone);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("手机号解密失败, memberId: {}", member.getId(), e);
|
log.error("手机号解密失败, memberId: {}", member.getId(), e);
|
||||||
member.setPhone(null);
|
member.setPhone(null);
|
||||||
|
|||||||
+6
-17
@@ -2,6 +2,8 @@ package cn.novalon.gym.manage.member.handler;
|
|||||||
|
|
||||||
import cn.novalon.gym.manage.member.dto.WechatLoginDto;
|
import cn.novalon.gym.manage.member.dto.WechatLoginDto;
|
||||||
import cn.novalon.gym.manage.member.service.WechatAuthService;
|
import cn.novalon.gym.manage.member.service.WechatAuthService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
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.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@@ -20,20 +22,13 @@ import reactor.core.publisher.Mono;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@Tag(name = "微信认证", description = "微信小程序登录、公众号回调等")
|
||||||
public class WechatAuthHandler {
|
public class WechatAuthHandler {
|
||||||
|
|
||||||
private final WechatAuthService wechatAuthService;
|
private final WechatAuthService wechatAuthService;
|
||||||
private final WechatOfficialEventHandler wechatOfficialEventHandler;
|
private final WechatOfficialEventHandler wechatOfficialEventHandler;
|
||||||
|
|
||||||
/**
|
@Operation(summary = "微信小程序登录", description = "通过微信小程序code获取session_key,完成会员登录或注册")
|
||||||
* 小程序更新
|
|
||||||
*
|
|
||||||
* POST /api/member/auth/miniapp/login
|
|
||||||
* Body: {"code": "wx_login_code"}
|
|
||||||
*
|
|
||||||
* @param request ServerRequest
|
|
||||||
* @return Mono<ServerResponse> 登录响应
|
|
||||||
*/
|
|
||||||
public Mono<ServerResponse> miniappLogin(ServerRequest request) {
|
public Mono<ServerResponse> miniappLogin(ServerRequest request) {
|
||||||
log.info("收到小程序登录请求");
|
log.info("收到小程序登录请求");
|
||||||
|
|
||||||
@@ -50,18 +45,12 @@ public class WechatAuthHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Operation(summary = "微信公众号回调", description = "处理微信公众号事件(关注、取消关注等)")
|
||||||
* 公众号回调
|
|
||||||
*
|
|
||||||
* POST /api/member/auth/mp/callback
|
|
||||||
* Body: <xml><Event>subscribe</Event><FromUserName>openid</FromUserName></xml>
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public Mono<ServerResponse> mpCallback(ServerRequest request) {
|
public Mono<ServerResponse> mpCallback(ServerRequest request) {
|
||||||
return wechatOfficialEventHandler.handleEvent(request);
|
return wechatOfficialEventHandler.handleEvent(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证微信公众号签名
|
@Operation(summary = "验证微信公众号签名", description = "微信公众号服务器验证,返回echostr")
|
||||||
public Mono<ServerResponse> verifyMpSignature(ServerRequest request) {
|
public Mono<ServerResponse> verifyMpSignature(ServerRequest request) {
|
||||||
return wechatOfficialEventHandler.verifySignature(request);
|
return wechatOfficialEventHandler.verifySignature(request);
|
||||||
}
|
}
|
||||||
|
|||||||
-3
@@ -40,9 +40,6 @@ public class WechatOfficialEventHandler {
|
|||||||
.flatMap(xmlBody -> {
|
.flatMap(xmlBody -> {
|
||||||
log.info("收到微信公众号事件 {}", xmlBody);
|
log.info("收到微信公众号事件 {}", xmlBody);
|
||||||
|
|
||||||
// TODO: 将XML解析为WechatOfficialEventDto
|
|
||||||
// 目前简化处理直接获取openId和event
|
|
||||||
|
|
||||||
String openId = extractOpenId(xmlBody);
|
String openId = extractOpenId(xmlBody);
|
||||||
String event = extractEvent(xmlBody);
|
String event = extractEvent(xmlBody);
|
||||||
|
|
||||||
|
|||||||
+40
-1
@@ -1,7 +1,9 @@
|
|||||||
package cn.novalon.gym.manage.member.repository;
|
package cn.novalon.gym.manage.member.repository;
|
||||||
|
|
||||||
import cn.novalon.gym.manage.member.entity.Member;
|
import cn.novalon.gym.manage.member.entity.Member;
|
||||||
|
import cn.novalon.gym.manage.member.vo.MemberCardInfoVO;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.r2dbc.repository.Query;
|
||||||
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
@@ -30,7 +32,44 @@ public interface IMemberRepository extends R2dbcRepository<Member, Long> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询所有会员
|
* 分页查询所有会员
|
||||||
* 方法名 findAllBy 是 Spring Data 的约定,表示按条件查询所有
|
|
||||||
*/
|
*/
|
||||||
Flux<Member> findAllBy(Pageable pageable);
|
Flux<Member> findAllBy(Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询会员的所有卡片
|
||||||
|
*/
|
||||||
|
@Query("SELECT " +
|
||||||
|
" r.id, " +
|
||||||
|
" r.member_card_record_id, " +
|
||||||
|
" r.member_id, " +
|
||||||
|
" r.member_card_id, " +
|
||||||
|
" r.status, " +
|
||||||
|
" r.remaining_times, " +
|
||||||
|
" r.remaining_amount, " +
|
||||||
|
" r.expire_time, " +
|
||||||
|
" r.purchase_time, " +
|
||||||
|
" r.source_order_id, " +
|
||||||
|
" r.created_at, " +
|
||||||
|
" r.updated_at, " +
|
||||||
|
" r.version, " +
|
||||||
|
" r.card_composition, " +
|
||||||
|
" c.id AS card_id, " +
|
||||||
|
" c.member_card_id, " +
|
||||||
|
" c.member_card_name, " +
|
||||||
|
" c.member_card_type, " +
|
||||||
|
" c.member_card_price, " +
|
||||||
|
" c.member_card_validity_days, " +
|
||||||
|
" c.member_card_total_times, " +
|
||||||
|
" c.member_card_amount, " +
|
||||||
|
" c.member_card_status, " +
|
||||||
|
" c.extra_config, " +
|
||||||
|
" c.created_at AS card_created_at, " +
|
||||||
|
" c.updated_at AS card_updated_at " +
|
||||||
|
"FROM member_card_record r " +
|
||||||
|
"LEFT JOIN member_card c ON r.member_card_id = c.id " +
|
||||||
|
"WHERE r.member_id = :memberId " +
|
||||||
|
"AND r.deleted_at IS NULL " +
|
||||||
|
"AND c.deleted_at IS NULL " +
|
||||||
|
"ORDER BY r.created_at DESC")
|
||||||
|
Flux<MemberCardInfoVO> findCardRecordsWithCardInfoByMemberId(Long memberId);
|
||||||
}
|
}
|
||||||
|
|||||||
+18
@@ -4,6 +4,7 @@ import cn.novalon.gym.manage.member.dto.SearchMemberDto;
|
|||||||
import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto;
|
import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto;
|
||||||
import cn.novalon.gym.manage.member.entity.Member;
|
import cn.novalon.gym.manage.member.entity.Member;
|
||||||
import cn.novalon.gym.manage.member.es.entity.MemberES;
|
import cn.novalon.gym.manage.member.es.entity.MemberES;
|
||||||
|
import cn.novalon.gym.manage.member.vo.MemberDetailVO;
|
||||||
import cn.novalon.gym.manage.member.vo.MemberInfoVO;
|
import cn.novalon.gym.manage.member.vo.MemberInfoVO;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
@@ -58,4 +59,21 @@ public interface MemberService {
|
|||||||
* @return 所有会员信息
|
* @return 所有会员信息
|
||||||
*/
|
*/
|
||||||
Flux<Member> findAll(Integer pageNum, Integer pageSize);
|
Flux<Member> findAll(Integer pageNum, Integer pageSize);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 前台管理端获取会员详情(含会员卡信息)
|
||||||
|
*
|
||||||
|
* @param memberId 会员ID
|
||||||
|
* @return 会员详情
|
||||||
|
*/
|
||||||
|
Mono<MemberDetailVO> getMemberDetail(Long memberId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 前台管理端编辑会员信息
|
||||||
|
*
|
||||||
|
* @param memberId 会员ID
|
||||||
|
* @param updateDto 更新信息DTO
|
||||||
|
* @return 更新后的会员详情
|
||||||
|
*/
|
||||||
|
Mono<Boolean> adminUpdateMemberInfo(Long memberId, UpdateMemberInfoDto updateDto);
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-5
@@ -3,6 +3,8 @@ package cn.novalon.gym.manage.member.service.impl;
|
|||||||
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
|
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
|
||||||
import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
|
import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
|
||||||
import cn.novalon.gym.manage.member.service.IMemberCardRecordService;
|
import cn.novalon.gym.manage.member.service.IMemberCardRecordService;
|
||||||
|
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
@@ -16,17 +18,35 @@ import java.time.LocalDateTime;
|
|||||||
* @author 付嘉
|
* @author 付嘉
|
||||||
* @date 2026-05-27
|
* @date 2026-05-27
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class MemberCardRecordServiceImpl implements IMemberCardRecordService {
|
public class MemberCardRecordServiceImpl implements IMemberCardRecordService {
|
||||||
private final MemberCardRecordRepository memberCardRecordRepository;
|
private final MemberCardRecordRepository memberCardRecordRepository;
|
||||||
|
private final RedisUtil redisUtil;
|
||||||
|
|
||||||
public MemberCardRecordServiceImpl(MemberCardRecordRepository memberCardRecordRepository) {
|
private static final String MEMBER_CARD_RECORD_CACHE_PREFIX = "member:card:record:";
|
||||||
|
private static final long CACHE_EXPIRE_SECONDS = 300;
|
||||||
|
|
||||||
|
public MemberCardRecordServiceImpl(MemberCardRecordRepository memberCardRecordRepository, RedisUtil redisUtil) {
|
||||||
this.memberCardRecordRepository = memberCardRecordRepository;
|
this.memberCardRecordRepository = memberCardRecordRepository;
|
||||||
|
this.redisUtil = redisUtil;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<MemberCardRecord> findById(Long recordId) {
|
public Mono<MemberCardRecord> findById(Long recordId) {
|
||||||
return memberCardRecordRepository.findById(recordId);
|
String cacheKey = MEMBER_CARD_RECORD_CACHE_PREFIX + recordId;
|
||||||
|
Object cached = redisUtil.get(cacheKey);
|
||||||
|
if (cached != null && cached instanceof MemberCardRecord) {
|
||||||
|
log.debug("从缓存获取会员卡记录, recordId: {}", recordId);
|
||||||
|
return Mono.just((MemberCardRecord) cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
return memberCardRecordRepository.findById(recordId)
|
||||||
|
.doOnSuccess(record -> {
|
||||||
|
if (record != null) {
|
||||||
|
redisUtil.setWithExpire(cacheKey, record, CACHE_EXPIRE_SECONDS);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -53,17 +73,32 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<Integer> deductUsage(Long recordId, Integer deductTimes, Double deductAmount) {
|
public Mono<Integer> deductUsage(Long recordId, Integer deductTimes, Double deductAmount) {
|
||||||
return memberCardRecordRepository.deductUsage(recordId, deductTimes, deductAmount);
|
return memberCardRecordRepository.deductUsage(recordId, deductTimes, deductAmount)
|
||||||
|
.doOnSuccess(updated -> {
|
||||||
|
if (updated > 0) {
|
||||||
|
clearRecordCache(recordId);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<Integer> renewCard(Long recordId, Integer addTimes, Double addAmount, LocalDateTime newExpireTime) {
|
public Mono<Integer> renewCard(Long recordId, Integer addTimes, Double addAmount, LocalDateTime newExpireTime) {
|
||||||
return memberCardRecordRepository.renewCard(recordId, addTimes, addAmount, newExpireTime);
|
return memberCardRecordRepository.renewCard(recordId, addTimes, addAmount, newExpireTime)
|
||||||
|
.doOnSuccess(updated -> {
|
||||||
|
if (updated > 0) {
|
||||||
|
clearRecordCache(recordId);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<Integer> updateStatus(Long recordId, String status) {
|
public Mono<Integer> updateStatus(Long recordId, String status) {
|
||||||
return memberCardRecordRepository.updateStatus(recordId, status);
|
return memberCardRecordRepository.updateStatus(recordId, status)
|
||||||
|
.doOnSuccess(updated -> {
|
||||||
|
if (updated > 0) {
|
||||||
|
clearRecordCache(recordId);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -80,4 +115,10 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService {
|
|||||||
public Flux<MemberCardRecord> findExpiredCards() {
|
public Flux<MemberCardRecord> findExpiredCards() {
|
||||||
return memberCardRecordRepository.findExpiredCards();
|
return memberCardRecordRepository.findExpiredCards();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void clearRecordCache(Long recordId) {
|
||||||
|
String cacheKey = MEMBER_CARD_RECORD_CACHE_PREFIX + recordId;
|
||||||
|
redisUtil.delete(cacheKey);
|
||||||
|
log.debug("清除会员卡记录缓存, recordId: {}", recordId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-3
@@ -1,5 +1,6 @@
|
|||||||
package cn.novalon.gym.manage.member.service.impl;
|
package cn.novalon.gym.manage.member.service.impl;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||||
import cn.novalon.gym.manage.member.entity.MemberCard;
|
import cn.novalon.gym.manage.member.entity.MemberCard;
|
||||||
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
|
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
|
||||||
import cn.novalon.gym.manage.member.entity.MemberCardTransaction;
|
import cn.novalon.gym.manage.member.entity.MemberCardTransaction;
|
||||||
@@ -15,6 +16,7 @@ import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
|
|||||||
import cn.novalon.gym.manage.member.repository.MemberCardRepository;
|
import cn.novalon.gym.manage.member.repository.MemberCardRepository;
|
||||||
import cn.novalon.gym.manage.member.service.IMemberCardService;
|
import cn.novalon.gym.manage.member.service.IMemberCardService;
|
||||||
import cn.novalon.gym.manage.member.service.IMemberCardTransactionService;
|
import cn.novalon.gym.manage.member.service.IMemberCardTransactionService;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -39,6 +41,10 @@ public class MemberCardServiceImpl implements IMemberCardService {
|
|||||||
private final DistributedLockService distributedLockService;
|
private final DistributedLockService distributedLockService;
|
||||||
private final ExpirationReminderService expirationReminderService;
|
private final ExpirationReminderService expirationReminderService;
|
||||||
private final RefundSagaHandler refundSagaHandler;
|
private final RefundSagaHandler refundSagaHandler;
|
||||||
|
private final RedisUtil redisUtil;
|
||||||
|
|
||||||
|
private static final String MEMBER_CARD_CACHE_PREFIX = "member:card:";
|
||||||
|
private static final long CACHE_EXPIRE_SECONDS = 300;
|
||||||
|
|
||||||
public MemberCardServiceImpl(MemberCardRepository memberCardRepository,
|
public MemberCardServiceImpl(MemberCardRepository memberCardRepository,
|
||||||
MemberCardRecordRepository recordRepository,
|
MemberCardRecordRepository recordRepository,
|
||||||
@@ -46,7 +52,8 @@ public class MemberCardServiceImpl implements IMemberCardService {
|
|||||||
MemberCardStateMachine stateMachine,
|
MemberCardStateMachine stateMachine,
|
||||||
DistributedLockService distributedLockService,
|
DistributedLockService distributedLockService,
|
||||||
ExpirationReminderService expirationReminderService,
|
ExpirationReminderService expirationReminderService,
|
||||||
RefundSagaHandler refundSagaHandler) {
|
RefundSagaHandler refundSagaHandler,
|
||||||
|
RedisUtil redisUtil) {
|
||||||
this.memberCardRepository = memberCardRepository;
|
this.memberCardRepository = memberCardRepository;
|
||||||
this.recordRepository = recordRepository;
|
this.recordRepository = recordRepository;
|
||||||
this.transactionService = transactionService;
|
this.transactionService = transactionService;
|
||||||
@@ -54,11 +61,24 @@ public class MemberCardServiceImpl implements IMemberCardService {
|
|||||||
this.distributedLockService = distributedLockService;
|
this.distributedLockService = distributedLockService;
|
||||||
this.expirationReminderService = expirationReminderService;
|
this.expirationReminderService = expirationReminderService;
|
||||||
this.refundSagaHandler = refundSagaHandler;
|
this.refundSagaHandler = refundSagaHandler;
|
||||||
|
this.redisUtil = redisUtil;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<MemberCard> findByMemberCardIdAndDeletedAtIsNull(Long memberCardId) {
|
public Mono<MemberCard> findByMemberCardIdAndDeletedAtIsNull(Long memberCardId) {
|
||||||
return memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(memberCardId);
|
String cacheKey = MEMBER_CARD_CACHE_PREFIX + memberCardId;
|
||||||
|
Object cached = redisUtil.get(cacheKey);
|
||||||
|
if (cached != null && cached instanceof MemberCard) {
|
||||||
|
log.debug("从缓存获取会员卡信息, memberCardId: {}", memberCardId);
|
||||||
|
return Mono.just((MemberCard) cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
return memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(memberCardId)
|
||||||
|
.doOnSuccess(card -> {
|
||||||
|
if (card != null) {
|
||||||
|
redisUtil.setWithExpire(cacheKey, card, CACHE_EXPIRE_SECONDS);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -95,7 +115,12 @@ public class MemberCardServiceImpl implements IMemberCardService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<MemberCard> save(MemberCard entity) {
|
public Mono<MemberCard> save(MemberCard entity) {
|
||||||
return memberCardRepository.save(entity);
|
return memberCardRepository.save(entity)
|
||||||
|
.doOnSuccess(saved -> {
|
||||||
|
if (saved.getMemberCardId() != null) {
|
||||||
|
clearCardCache(saved.getMemberCardId());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -329,4 +354,10 @@ public class MemberCardServiceImpl implements IMemberCardService {
|
|||||||
|
|
||||||
return transactionService.createTransaction(transaction);
|
return transactionService.createTransaction(transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void clearCardCache(Long memberCardId) {
|
||||||
|
String cacheKey = MEMBER_CARD_CACHE_PREFIX + memberCardId;
|
||||||
|
redisUtil.delete(cacheKey);
|
||||||
|
log.debug("清除会员卡缓存, memberCardId: {}", memberCardId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+168
-27
@@ -4,20 +4,32 @@ import cn.novalon.gym.manage.common.exception.ConflictException;
|
|||||||
import cn.novalon.gym.manage.common.exception.ErrorCode;
|
import cn.novalon.gym.manage.common.exception.ErrorCode;
|
||||||
import cn.novalon.gym.manage.common.exception.NotFoundException;
|
import cn.novalon.gym.manage.common.exception.NotFoundException;
|
||||||
import cn.novalon.gym.manage.common.exception.SystemException;
|
import cn.novalon.gym.manage.common.exception.SystemException;
|
||||||
import cn.novalon.gym.manage.member.config.WechatProperties;
|
import cn.novalon.gym.manage.common.util.HtmlEscapeUtil;
|
||||||
import cn.novalon.gym.manage.member.dto.SearchMemberDto;
|
import cn.novalon.gym.manage.member.dto.SearchMemberDto;
|
||||||
import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto;
|
import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto;
|
||||||
import cn.novalon.gym.manage.member.entity.Member;
|
import cn.novalon.gym.manage.member.entity.Member;
|
||||||
|
import cn.novalon.gym.manage.member.enums.GenderEnum;
|
||||||
|
import cn.novalon.gym.manage.member.enums.MemberCardType;
|
||||||
import cn.novalon.gym.manage.member.es.entity.MemberES;
|
import cn.novalon.gym.manage.member.es.entity.MemberES;
|
||||||
import cn.novalon.gym.manage.member.es.repository.MemberESRepository;
|
import cn.novalon.gym.manage.member.es.repository.MemberESRepository;
|
||||||
import cn.novalon.gym.manage.member.repository.IMemberRepository;
|
import cn.novalon.gym.manage.member.repository.IMemberRepository;
|
||||||
import cn.novalon.gym.manage.member.service.MemberService;
|
import cn.novalon.gym.manage.member.service.MemberService;
|
||||||
import cn.novalon.gym.manage.member.util.AesUtil;
|
import cn.novalon.gym.manage.member.util.AesUtil;
|
||||||
|
import cn.novalon.gym.manage.member.util.BeanConvertUtil;
|
||||||
import cn.novalon.gym.manage.member.util.EsSyncUtils;
|
import cn.novalon.gym.manage.member.util.EsSyncUtils;
|
||||||
|
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||||
|
import cn.novalon.gym.manage.member.vo.MemberCardInfoVO;
|
||||||
|
import cn.novalon.gym.manage.member.vo.MemberDetailVO;
|
||||||
import cn.novalon.gym.manage.member.vo.MemberInfoVO;
|
import cn.novalon.gym.manage.member.vo.MemberInfoVO;
|
||||||
|
import co.elastic.clients.elasticsearch.ElasticsearchClient;
|
||||||
|
import co.elastic.clients.elasticsearch.core.IndexResponse;
|
||||||
|
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
|
||||||
|
import co.elastic.clients.transport.rest_client.RestClientTransport;
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.http.HttpHost;
|
||||||
|
import org.elasticsearch.client.RestClient;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
@@ -26,6 +38,10 @@ 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.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 会员服务实现
|
* 会员服务实现
|
||||||
@@ -41,10 +57,14 @@ public class MemberServiceImpl implements MemberService {
|
|||||||
private final IMemberRepository memberRepository;
|
private final IMemberRepository memberRepository;
|
||||||
private final MemberESRepository memberESRepository;
|
private final MemberESRepository memberESRepository;
|
||||||
private final EsSyncUtils esSyncUtils;
|
private final EsSyncUtils esSyncUtils;
|
||||||
private final WechatProperties wechatProperties;
|
private final RedisUtil redisUtil;
|
||||||
|
|
||||||
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer;
|
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer;
|
||||||
|
|
||||||
|
private static final String MEMBER_INFO_CACHE_PREFIX = "member:info:";
|
||||||
|
private static final String MEMBER_DETAIL_CACHE_PREFIX = "member:detail:";
|
||||||
|
private static final long CACHE_EXPIRE_SECONDS = 300;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository);
|
this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository);
|
||||||
@@ -52,12 +72,23 @@ public class MemberServiceImpl implements MemberService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<MemberInfoVO> getMemberInfo(Long memberId) {
|
public Mono<MemberInfoVO> getMemberInfo(Long memberId) {
|
||||||
return memberRepository.findById(memberId)
|
String cacheKey = MEMBER_INFO_CACHE_PREFIX + memberId;
|
||||||
.map(this::buildMemberInfoResponse)
|
|
||||||
.switchIfEmpty(Mono.error(() -> {
|
return redisUtil.get(cacheKey, MemberInfoVO.class)
|
||||||
log.error("会员不存在: memberId={}", memberId);
|
.flatMap(cached -> {
|
||||||
throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在");
|
if (cached != null) {
|
||||||
}));
|
log.debug("从缓存获取会员信息, memberId: {}", memberId);
|
||||||
|
return Mono.just(cached);
|
||||||
|
}
|
||||||
|
return memberRepository.findById(memberId)
|
||||||
|
.map(this::buildMemberInfoResponse)
|
||||||
|
.flatMap(vo -> 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
|
||||||
@@ -67,10 +98,10 @@ public class MemberServiceImpl implements MemberService {
|
|||||||
return memberRepository.findById(memberId)
|
return memberRepository.findById(memberId)
|
||||||
.flatMap(member -> {
|
.flatMap(member -> {
|
||||||
if (updateDto.getNickname() != null) {
|
if (updateDto.getNickname() != null) {
|
||||||
member.setNickname(updateDto.getNickname());
|
member.setNickname(HtmlEscapeUtil.escape(updateDto.getNickname()));
|
||||||
}
|
}
|
||||||
if (updateDto.getGender() != null) {
|
if (updateDto.getGender() != null) {
|
||||||
member.setGender(updateDto.getGender());
|
member.setGender(updateDto.getGender().getCode());
|
||||||
}
|
}
|
||||||
if (updateDto.getBirthday() != null) {
|
if (updateDto.getBirthday() != null) {
|
||||||
member.setBirthday(updateDto.getBirthday());
|
member.setBirthday(updateDto.getBirthday());
|
||||||
@@ -79,12 +110,16 @@ public class MemberServiceImpl implements MemberService {
|
|||||||
member.setAvatar(updateDto.getAvatar());
|
member.setAvatar(updateDto.getAvatar());
|
||||||
}
|
}
|
||||||
if (updateDto.getAddress() != null) {
|
if (updateDto.getAddress() != null) {
|
||||||
member.setAddress(updateDto.getAddress());
|
member.setAddress(HtmlEscapeUtil.escape(updateDto.getAddress()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return memberRepository.save(member);
|
return memberRepository.save(member);
|
||||||
})
|
})
|
||||||
.doOnSuccess(memberSyncer::sync)
|
.flatMap(savedMember -> {
|
||||||
|
memberSyncer.sync(savedMember);
|
||||||
|
return clearMemberCache(memberId)
|
||||||
|
.then(Mono.just(savedMember));
|
||||||
|
})
|
||||||
.map(savedMember -> {
|
.map(savedMember -> {
|
||||||
log.info("会员信息更新成功, memberId: {}", savedMember.getId());
|
log.info("会员信息更新成功, memberId: {}", savedMember.getId());
|
||||||
return buildMemberInfoResponse(savedMember);
|
return buildMemberInfoResponse(savedMember);
|
||||||
@@ -99,11 +134,14 @@ public class MemberServiceImpl implements MemberService {
|
|||||||
String phone = member.getPhone();
|
String phone = member.getPhone();
|
||||||
String maskedPhone = phone != null ? phone.replace(phone.substring(3, 7), "****") : null;
|
String maskedPhone = phone != null ? phone.replace(phone.substring(3, 7), "****") : null;
|
||||||
|
|
||||||
|
GenderEnum genderEnum = GenderEnum.fromCode(member.getGender());
|
||||||
|
|
||||||
return MemberInfoVO.builder()
|
return MemberInfoVO.builder()
|
||||||
.id(member.getId())
|
.id(member.getId())
|
||||||
.nickname(member.getNickname())
|
.nickname(member.getNickname())
|
||||||
.phone(maskedPhone)
|
.phone(maskedPhone)
|
||||||
.gender(member.getGender())
|
.gender(genderEnum)
|
||||||
|
.genderDesc(genderEnum.getDesc())
|
||||||
.birthday(member.getBirthday())
|
.birthday(member.getBirthday())
|
||||||
.avatar(member.getAvatar())
|
.avatar(member.getAvatar())
|
||||||
.hasPhone(phone != null)
|
.hasPhone(phone != null)
|
||||||
@@ -117,9 +155,7 @@ public class MemberServiceImpl implements MemberService {
|
|||||||
|
|
||||||
String encryptedPhone;
|
String encryptedPhone;
|
||||||
try {
|
try {
|
||||||
String secretKey = wechatProperties.getPhoneEncryption().getSecretKey();
|
encryptedPhone = AesUtil.encrypt(phone);
|
||||||
String iv = wechatProperties.getPhoneEncryption().getIv();
|
|
||||||
encryptedPhone = AesUtil.encrypt(phone, secretKey, iv);
|
|
||||||
log.info("手机号加密成功");
|
log.info("手机号加密成功");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("手机号加密失败", e);
|
log.error("手机号加密失败", e);
|
||||||
@@ -145,32 +181,32 @@ public class MemberServiceImpl implements MemberService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Flux<MemberES> searchMember(SearchMemberDto searchMemberDto) {
|
public Flux<MemberES> searchMember(SearchMemberDto searchMemberDto) {
|
||||||
log.info("搜索会员, searchValue: {}, filter: {}, pageNum: {}, pageSize: {}",
|
log.info("搜索会员, searchValue: {}, pageNum: {}, pageSize: {}",
|
||||||
searchMemberDto.getSearchValue(),
|
searchMemberDto.getSearchValue(),
|
||||||
searchMemberDto.getFilter(),
|
|
||||||
searchMemberDto.getPageNum(),
|
searchMemberDto.getPageNum(),
|
||||||
searchMemberDto.getPageSize());
|
searchMemberDto.getPageSize());
|
||||||
|
|
||||||
String searchValue = searchMemberDto.getSearchValue();
|
String searchValue = searchMemberDto.getSearchValue();
|
||||||
|
|
||||||
if(searchValue != null && searchValue.matches("^1[3-9]\\d{9}$")){
|
if (searchValue != null && searchValue.matches("^1[3-9]\\d{9}$")) {
|
||||||
log.debug("搜索值为手机号格式,进行加密处理");
|
log.debug("搜索值为手机号格式,进行加密处理");
|
||||||
String secretKey = wechatProperties.getPhoneEncryption().getSecretKey();
|
searchValue = AesUtil.encrypt(searchValue);
|
||||||
String iv = wechatProperties.getPhoneEncryption().getIv();
|
|
||||||
searchValue = AesUtil.encrypt(searchValue,secretKey,iv);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Pageable pageable = PageRequest.of(
|
Pageable pageable = PageRequest.of(
|
||||||
searchMemberDto.getPageNum() - 1,
|
searchMemberDto.getPageNum() - 1,
|
||||||
searchMemberDto.getPageSize(),
|
searchMemberDto.getPageSize()
|
||||||
Sort.by(Sort.Direction.DESC, "update_at")
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return memberESRepository.findByMemberNoOrPhoneOrNicknameContainingAndGender(
|
if (searchValue == null) {
|
||||||
|
log.warn("搜索值为空,返回空结果");
|
||||||
|
return Flux.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return memberESRepository.findByMemberNoOrPhoneOrNicknameContaining(
|
||||||
searchValue,
|
searchValue,
|
||||||
searchValue,
|
searchValue,
|
||||||
searchValue,
|
searchValue,
|
||||||
searchMemberDto.getFilter() ,
|
|
||||||
pageable
|
pageable
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -187,6 +223,99 @@ public class MemberServiceImpl implements MemberService {
|
|||||||
return memberRepository.findAllBy(pageable);
|
return memberRepository.findAllBy(pageable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<MemberDetailVO> getMemberDetail(Long memberId) {
|
||||||
|
log.info("查询会员详情, memberId: {}", memberId);
|
||||||
|
|
||||||
|
String cacheKey = MEMBER_DETAIL_CACHE_PREFIX + memberId;
|
||||||
|
|
||||||
|
return redisUtil.get(cacheKey, MemberDetailVO.class)
|
||||||
|
.flatMap(cached -> {
|
||||||
|
if (cached != null) {
|
||||||
|
log.debug("从缓存获取会员详情, memberId: {}", memberId);
|
||||||
|
return Mono.just(cached);
|
||||||
|
}
|
||||||
|
return memberRepository.findById(memberId)
|
||||||
|
.zipWith(
|
||||||
|
memberRepository.findCardRecordsWithCardInfoByMemberId(memberId)
|
||||||
|
.collectList(),
|
||||||
|
(baseInfo, cardList) -> {
|
||||||
|
MemberDetailVO memberDetailVO = BeanConvertUtil.toBean(baseInfo, MemberDetailVO.class);
|
||||||
|
|
||||||
|
GenderEnum genderEnum = GenderEnum.fromCode(baseInfo.getGender());
|
||||||
|
memberDetailVO.setGenderDesc(genderEnum.getDesc());
|
||||||
|
|
||||||
|
List<MemberCardInfoVO> enrichedCards = cardList.stream()
|
||||||
|
.peek(vo -> {
|
||||||
|
if (vo.getMemberCardType() != null) {
|
||||||
|
try {
|
||||||
|
MemberCardType cardType = MemberCardType.valueOf(vo.getMemberCardType());
|
||||||
|
vo.setMemberCardTypeDesc(cardType.getDesc());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
vo.setMemberCardTypeDesc(vo.getMemberCardType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (vo.getMemberCardStatus() != null) {
|
||||||
|
vo.setMemberCardStatusDesc(vo.getMemberCardStatus() == 1 ? "上架" : "下架");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
memberDetailVO.setMemberCards(enrichedCards);
|
||||||
|
|
||||||
|
long activeCount = enrichedCards.stream()
|
||||||
|
.filter(card -> card.getMemberCardStatus() != null && card.getMemberCardStatus() == 1)
|
||||||
|
.count();
|
||||||
|
memberDetailVO.setActiveCardCount((int) activeCount);
|
||||||
|
memberDetailVO.setInactiveCardCount(enrichedCards.size() - (int) activeCount);
|
||||||
|
|
||||||
|
return memberDetailVO;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.flatMap(vo -> redisUtil.setWithExpire(cacheKey, vo, CACHE_EXPIRE_SECONDS)
|
||||||
|
.then(Mono.just(vo)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Boolean> adminUpdateMemberInfo(Long memberId, UpdateMemberInfoDto updateDto) {
|
||||||
|
log.info("前台管理端编辑会员信息, memberId: {}", memberId);
|
||||||
|
|
||||||
|
return memberRepository.findById(memberId)
|
||||||
|
.switchIfEmpty(Mono.error(() -> {
|
||||||
|
log.error("会员不存在: memberId={}", memberId);
|
||||||
|
throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在");
|
||||||
|
}))
|
||||||
|
.flatMap(member -> {
|
||||||
|
if (updateDto.getNickname() != null) {
|
||||||
|
member.setNickname(HtmlEscapeUtil.escape(updateDto.getNickname()));
|
||||||
|
}
|
||||||
|
if (updateDto.getGender() != null) {
|
||||||
|
member.setGender(updateDto.getGender().getCode());
|
||||||
|
}
|
||||||
|
if (updateDto.getBirthday() != null) {
|
||||||
|
member.setBirthday(updateDto.getBirthday());
|
||||||
|
}
|
||||||
|
if (updateDto.getAvatar() != null) {
|
||||||
|
member.setAvatar(updateDto.getAvatar());
|
||||||
|
}
|
||||||
|
if (updateDto.getAddress() != null) {
|
||||||
|
member.setAddress(HtmlEscapeUtil.escape(updateDto.getAddress()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return memberRepository.save(member);
|
||||||
|
})
|
||||||
|
.flatMap(savedMember -> {
|
||||||
|
memberSyncer.sync(savedMember);
|
||||||
|
return clearMemberCache(memberId)
|
||||||
|
.then(Mono.just(true));
|
||||||
|
})
|
||||||
|
.onErrorResume(e -> {
|
||||||
|
log.error("编辑会员信息失败, memberId: {}, error: {}", memberId, e.getMessage(), e);
|
||||||
|
return Mono.just(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private Mono<Boolean> updateMemberPhone(Long memberId, String encryptedPhone) {
|
private Mono<Boolean> updateMemberPhone(Long memberId, String encryptedPhone) {
|
||||||
return memberRepository.findById(memberId)
|
return memberRepository.findById(memberId)
|
||||||
.flatMap(member -> {
|
.flatMap(member -> {
|
||||||
@@ -194,7 +323,11 @@ public class MemberServiceImpl implements MemberService {
|
|||||||
member.setLastLoginAt(LocalDateTime.now());
|
member.setLastLoginAt(LocalDateTime.now());
|
||||||
|
|
||||||
return memberRepository.save(member)
|
return memberRepository.save(member)
|
||||||
.doOnSuccess(memberSyncer::sync)
|
.flatMap(savedMember -> {
|
||||||
|
memberSyncer.sync(savedMember);
|
||||||
|
return clearMemberCache(memberId)
|
||||||
|
.then(Mono.just(savedMember));
|
||||||
|
})
|
||||||
.map(savedMember -> {
|
.map(savedMember -> {
|
||||||
log.info("手机号录入成功, memberId: {}", savedMember.getId());
|
log.info("手机号录入成功, memberId: {}", savedMember.getId());
|
||||||
return true;
|
return true;
|
||||||
@@ -205,4 +338,12 @@ public class MemberServiceImpl implements MemberService {
|
|||||||
throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在");
|
throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在");
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Mono<Long> clearMemberCache(Long memberId) {
|
||||||
|
String infoCacheKey = MEMBER_INFO_CACHE_PREFIX + memberId;
|
||||||
|
String detailCacheKey = MEMBER_DETAIL_CACHE_PREFIX + memberId;
|
||||||
|
return redisUtil.delete(infoCacheKey)
|
||||||
|
.then(redisUtil.delete(detailCacheKey))
|
||||||
|
.doOnSuccess(result -> log.debug("清除会员缓存, memberId: {}", memberId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+52
-15
@@ -1,9 +1,11 @@
|
|||||||
package cn.novalon.gym.manage.member.service.impl;
|
package cn.novalon.gym.manage.member.service.impl;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.common.util.HtmlEscapeUtil;
|
||||||
import cn.novalon.gym.manage.member.entity.RefundApplication;
|
import cn.novalon.gym.manage.member.entity.RefundApplication;
|
||||||
import cn.novalon.gym.manage.member.enums.RefundStatus;
|
import cn.novalon.gym.manage.member.enums.RefundStatus;
|
||||||
import cn.novalon.gym.manage.member.repository.RefundApplicationRepository;
|
import cn.novalon.gym.manage.member.repository.RefundApplicationRepository;
|
||||||
import cn.novalon.gym.manage.member.service.IRefundApplicationService;
|
import cn.novalon.gym.manage.member.service.IRefundApplicationService;
|
||||||
|
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
@@ -21,9 +23,14 @@ import java.time.LocalDateTime;
|
|||||||
public class RefundApplicationServiceImpl implements IRefundApplicationService {
|
public class RefundApplicationServiceImpl implements IRefundApplicationService {
|
||||||
|
|
||||||
private final RefundApplicationRepository refundApplicationRepository;
|
private final RefundApplicationRepository refundApplicationRepository;
|
||||||
|
private final RedisUtil redisUtil;
|
||||||
|
|
||||||
public RefundApplicationServiceImpl(RefundApplicationRepository refundApplicationRepository) {
|
private static final String REFUND_APPLICATION_CACHE_PREFIX = "member:refund:";
|
||||||
|
private static final long CACHE_EXPIRE_SECONDS = 300;
|
||||||
|
|
||||||
|
public RefundApplicationServiceImpl(RefundApplicationRepository refundApplicationRepository, RedisUtil redisUtil) {
|
||||||
this.refundApplicationRepository = refundApplicationRepository;
|
this.refundApplicationRepository = refundApplicationRepository;
|
||||||
|
this.redisUtil = redisUtil;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -38,14 +45,16 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService {
|
|||||||
.then(Mono.defer(() -> {
|
.then(Mono.defer(() -> {
|
||||||
RefundApplication application = RefundApplication.builder()
|
RefundApplication application = RefundApplication.builder()
|
||||||
.recordId(recordId)
|
.recordId(recordId)
|
||||||
.reason(reason)
|
.reason(HtmlEscapeUtil.escape(reason))
|
||||||
.status(RefundStatus.PENDING)
|
.status(RefundStatus.PENDING)
|
||||||
.applyTime(LocalDateTime.now())
|
.applyTime(LocalDateTime.now())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
return refundApplicationRepository.save(application)
|
return refundApplicationRepository.save(application)
|
||||||
.doOnSuccess(app -> log.info("创建退款申请成功: applicationId={}, recordId={}",
|
.doOnSuccess(app -> {
|
||||||
app.getId(), recordId));
|
log.info("创建退款申请成功: applicationId={}, recordId={}", app.getId(), recordId);
|
||||||
|
clearRefundCache(recordId);
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,14 +63,19 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService {
|
|||||||
return refundApplicationRepository.findById(applicationId)
|
return refundApplicationRepository.findById(applicationId)
|
||||||
.switchIfEmpty(Mono.error(new RuntimeException("退款申请不存在")))
|
.switchIfEmpty(Mono.error(new RuntimeException("退款申请不存在")))
|
||||||
.flatMap(application -> {
|
.flatMap(application -> {
|
||||||
if (!"PENDING".equals(application.getStatus())) {
|
if (application.getStatus() != RefundStatus.PENDING) {
|
||||||
return Mono.error(new RuntimeException("退款申请状态不正确,当前状态: " + application.getStatus()));
|
return Mono.error(new RuntimeException("退款申请状态不正确,当前状态: " + application.getStatus()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return refundApplicationRepository.approve(applicationId, "APPROVED", auditorId, remark)
|
return refundApplicationRepository.approve(applicationId, "APPROVED", auditorId, HtmlEscapeUtil.escape(remark))
|
||||||
.thenReturn(application)
|
.flatMap(updatedRows -> {
|
||||||
.doOnSuccess(app -> log.info("批准退款申请成功: applicationId={}, auditorId={}",
|
if (updatedRows == 0) {
|
||||||
applicationId, auditorId));
|
return Mono.error(new RuntimeException("批准退款申请失败"));
|
||||||
|
}
|
||||||
|
clearRefundCache(application.getRecordId());
|
||||||
|
log.info("批准退款申请成功: applicationId={}, auditorId={}", applicationId, auditorId);
|
||||||
|
return refundApplicationRepository.findById(applicationId);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,19 +84,42 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService {
|
|||||||
return refundApplicationRepository.findById(applicationId)
|
return refundApplicationRepository.findById(applicationId)
|
||||||
.switchIfEmpty(Mono.error(new RuntimeException("退款申请不存在")))
|
.switchIfEmpty(Mono.error(new RuntimeException("退款申请不存在")))
|
||||||
.flatMap(application -> {
|
.flatMap(application -> {
|
||||||
if (!"PENDING".equals(application.getStatus())) {
|
if (application.getStatus() != RefundStatus.PENDING) {
|
||||||
return Mono.error(new RuntimeException("退款申请状态不正确,当前状态: " + application.getStatus()));
|
return Mono.error(new RuntimeException("退款申请状态不正确,当前状态: " + application.getStatus()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return refundApplicationRepository.approve(applicationId, "REJECTED", auditorId, remark)
|
return refundApplicationRepository.approve(applicationId, "REJECTED", auditorId, HtmlEscapeUtil.escape(remark))
|
||||||
.thenReturn(application)
|
.flatMap(updatedRows -> {
|
||||||
.doOnSuccess(app -> log.info("拒绝退款申请成功: applicationId={}, auditorId={}",
|
if (updatedRows == 0) {
|
||||||
applicationId, auditorId));
|
return Mono.error(new RuntimeException("拒绝退款申请失败"));
|
||||||
|
}
|
||||||
|
clearRefundCache(application.getRecordId());
|
||||||
|
log.info("拒绝退款申请成功: applicationId={}, auditorId={}", applicationId, auditorId);
|
||||||
|
return refundApplicationRepository.findById(applicationId);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<RefundApplication> findByRecordId(Long recordId) {
|
public Mono<RefundApplication> findByRecordId(Long recordId) {
|
||||||
return refundApplicationRepository.findByRecordId(recordId);
|
String cacheKey = REFUND_APPLICATION_CACHE_PREFIX + recordId;
|
||||||
|
Object cached = redisUtil.get(cacheKey);
|
||||||
|
if (cached != null && cached instanceof RefundApplication) {
|
||||||
|
log.debug("从缓存获取退款申请, recordId: {}", recordId);
|
||||||
|
return Mono.just((RefundApplication) cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
return refundApplicationRepository.findByRecordId(recordId)
|
||||||
|
.doOnSuccess(application -> {
|
||||||
|
if (application != null) {
|
||||||
|
redisUtil.setWithExpire(cacheKey, application, CACHE_EXPIRE_SECONDS);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearRefundCache(Long recordId) {
|
||||||
|
String cacheKey = REFUND_APPLICATION_CACHE_PREFIX + recordId;
|
||||||
|
redisUtil.delete(cacheKey);
|
||||||
|
log.debug("清除退款申请缓存, recordId: {}", recordId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-27
@@ -4,6 +4,7 @@ import cn.novalon.gym.manage.common.exception.ErrorCode;
|
|||||||
import cn.novalon.gym.manage.common.exception.SystemException;
|
import cn.novalon.gym.manage.common.exception.SystemException;
|
||||||
import cn.novalon.gym.manage.member.config.WechatProperties;
|
import cn.novalon.gym.manage.member.config.WechatProperties;
|
||||||
import cn.novalon.gym.manage.member.service.WechatApiService;
|
import cn.novalon.gym.manage.member.service.WechatApiService;
|
||||||
|
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@@ -30,6 +31,10 @@ import java.util.Map;
|
|||||||
public class WechatApiServiceImpl implements WechatApiService {
|
public class WechatApiServiceImpl implements WechatApiService {
|
||||||
|
|
||||||
private final WechatProperties wechatProperties;
|
private final WechatProperties wechatProperties;
|
||||||
|
private final RedisUtil redisUtil;
|
||||||
|
|
||||||
|
private static final String ACCESS_TOKEN_CACHE_PREFIX = "wechat:access_token:";
|
||||||
|
private static final long ACCESS_TOKEN_EXPIRE_SECONDS = 7000; // 比官方过期时间短100秒
|
||||||
|
|
||||||
private final WebClient webClient = WebClient.builder()
|
private final WebClient webClient = WebClient.builder()
|
||||||
.baseUrl("https://api.weixin.qq.com")
|
.baseUrl("https://api.weixin.qq.com")
|
||||||
@@ -147,35 +152,46 @@ public class WechatApiServiceImpl implements WechatApiService {
|
|||||||
public Mono<String> getAccessToken(String appType) {
|
public Mono<String> getAccessToken(String appType) {
|
||||||
log.debug("获取access_token, appType: {}", appType);
|
log.debug("获取access_token, appType: {}", appType);
|
||||||
|
|
||||||
String appId, appSecret;
|
String cacheKey = ACCESS_TOKEN_CACHE_PREFIX + appType;
|
||||||
if ("miniapp".equals(appType)) {
|
|
||||||
appId = wechatProperties.getMiniapp().getAppId();
|
|
||||||
appSecret = wechatProperties.getMiniapp().getAppSecret();
|
|
||||||
} else {
|
|
||||||
appId = wechatProperties.getMp().getAppId();
|
|
||||||
appSecret = wechatProperties.getMp().getAppSecret();
|
|
||||||
}
|
|
||||||
|
|
||||||
return webClient.get()
|
return redisUtil.get(cacheKey, String.class)
|
||||||
.uri(uriBuilder -> uriBuilder
|
.flatMap(cachedToken -> {
|
||||||
.path("/cgi-bin/token")
|
if (cachedToken != null) {
|
||||||
.queryParam("grant_type", "client_credential")
|
log.debug("从缓存获取access_token, appType: {}", appType);
|
||||||
.queryParam("appid", appId)
|
return Mono.just(cachedToken);
|
||||||
.queryParam("secret", appSecret)
|
|
||||||
.build())
|
|
||||||
.retrieve()
|
|
||||||
.bodyToMono(Map.class)
|
|
||||||
.map(response -> {
|
|
||||||
if (response.containsKey("access_token")) {
|
|
||||||
String accessToken = (String) response.get("access_token");
|
|
||||||
Integer expiresIn = (Integer) response.get("expires_in");
|
|
||||||
log.info("获取access_token成功, expires_in: {}s", expiresIn);
|
|
||||||
return accessToken;
|
|
||||||
} else {
|
|
||||||
String errmsg = (String) response.get("errmsg");
|
|
||||||
log.error("获取access_token失败: {}", errmsg);
|
|
||||||
throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "获取access_token失败: " + errmsg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String appId, appSecret;
|
||||||
|
if ("miniapp".equals(appType)) {
|
||||||
|
appId = wechatProperties.getMiniapp().getAppId();
|
||||||
|
appSecret = wechatProperties.getMiniapp().getAppSecret();
|
||||||
|
} else {
|
||||||
|
appId = wechatProperties.getMp().getAppId();
|
||||||
|
appSecret = wechatProperties.getMp().getAppSecret();
|
||||||
|
}
|
||||||
|
|
||||||
|
return webClient.get()
|
||||||
|
.uri(uriBuilder -> uriBuilder
|
||||||
|
.path("/cgi-bin/token")
|
||||||
|
.queryParam("grant_type", "client_credential")
|
||||||
|
.queryParam("appid", appId)
|
||||||
|
.queryParam("secret", appSecret)
|
||||||
|
.build())
|
||||||
|
.retrieve()
|
||||||
|
.bodyToMono(Map.class)
|
||||||
|
.flatMap(response -> {
|
||||||
|
if (response.containsKey("access_token")) {
|
||||||
|
String accessToken = (String) response.get("access_token");
|
||||||
|
Integer expiresIn = (Integer) response.get("expires_in");
|
||||||
|
log.info("获取access_token成功, expires_in: {}s", expiresIn);
|
||||||
|
return redisUtil.setWithExpire(cacheKey, accessToken, ACCESS_TOKEN_EXPIRE_SECONDS)
|
||||||
|
.then(Mono.just(accessToken));
|
||||||
|
} else {
|
||||||
|
String errmsg = (String) response.get("errmsg");
|
||||||
|
log.error("获取access_token失败: {}", errmsg);
|
||||||
|
throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "获取access_token失败: " + errmsg);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+37
-15
@@ -4,6 +4,7 @@ import cn.novalon.gym.manage.common.exception.ConflictException;
|
|||||||
import cn.novalon.gym.manage.common.exception.ErrorCode;
|
import cn.novalon.gym.manage.common.exception.ErrorCode;
|
||||||
import cn.novalon.gym.manage.common.exception.NotFoundException;
|
import cn.novalon.gym.manage.common.exception.NotFoundException;
|
||||||
import cn.novalon.gym.manage.common.exception.SystemException;
|
import cn.novalon.gym.manage.common.exception.SystemException;
|
||||||
|
import cn.novalon.gym.manage.common.util.HtmlEscapeUtil;
|
||||||
import cn.novalon.gym.manage.member.config.WechatProperties;
|
import cn.novalon.gym.manage.member.config.WechatProperties;
|
||||||
import cn.novalon.gym.manage.member.dto.WechatLoginDto;
|
import cn.novalon.gym.manage.member.dto.WechatLoginDto;
|
||||||
import cn.novalon.gym.manage.member.entity.Member;
|
import cn.novalon.gym.manage.member.entity.Member;
|
||||||
@@ -15,6 +16,7 @@ import cn.novalon.gym.manage.member.service.WechatAuthService;
|
|||||||
import cn.novalon.gym.manage.member.util.AesUtil;
|
import cn.novalon.gym.manage.member.util.AesUtil;
|
||||||
import cn.novalon.gym.manage.member.util.EsSyncUtils;
|
import cn.novalon.gym.manage.member.util.EsSyncUtils;
|
||||||
import cn.novalon.gym.manage.member.util.MemberNoGenerator;
|
import cn.novalon.gym.manage.member.util.MemberNoGenerator;
|
||||||
|
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||||
import cn.novalon.gym.manage.member.util.WechatPhoneUtil;
|
import cn.novalon.gym.manage.member.util.WechatPhoneUtil;
|
||||||
import cn.novalon.gym.manage.member.vo.WechatLoginVO;
|
import cn.novalon.gym.manage.member.vo.WechatLoginVO;
|
||||||
import cn.novalon.gym.manage.sys.security.JwtTokenProvider;
|
import cn.novalon.gym.manage.sys.security.JwtTokenProvider;
|
||||||
@@ -42,14 +44,17 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
|||||||
|
|
||||||
private final WechatApiService wechatApiService;
|
private final WechatApiService wechatApiService;
|
||||||
private final IMemberRepository memberRepository;
|
private final IMemberRepository memberRepository;
|
||||||
private final WechatProperties wechatProperties;
|
|
||||||
private final WechatPhoneUtil wechatPhoneUtil;
|
private final WechatPhoneUtil wechatPhoneUtil;
|
||||||
private final MemberESRepository memberESRepository;
|
private final MemberESRepository memberESRepository;
|
||||||
private final EsSyncUtils esSyncUtils;
|
private final EsSyncUtils esSyncUtils;
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
|
private final RedisUtil redisUtil;
|
||||||
|
|
||||||
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer;
|
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer;
|
||||||
|
|
||||||
|
private static final String MEMBER_INFO_CACHE_PREFIX = "member:info:";
|
||||||
|
private static final long CACHE_EXPIRE_SECONDS = 300;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository);
|
this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository);
|
||||||
@@ -79,7 +84,10 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
|||||||
member.setLastLoginAt(LocalDateTime.now());
|
member.setLastLoginAt(LocalDateTime.now());
|
||||||
|
|
||||||
return memberRepository.save(member)
|
return memberRepository.save(member)
|
||||||
.doOnSuccess(memberSyncer::sync)
|
.doOnSuccess(saved -> {
|
||||||
|
memberSyncer.sync(saved);
|
||||||
|
clearMemberCache(saved.getId());
|
||||||
|
})
|
||||||
.flatMap(savedMember -> {
|
.flatMap(savedMember -> {
|
||||||
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
|
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
|
||||||
return Mono.just(response);
|
return Mono.just(response);
|
||||||
@@ -89,7 +97,10 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
|||||||
member.setLastLoginAt(LocalDateTime.now());
|
member.setLastLoginAt(LocalDateTime.now());
|
||||||
|
|
||||||
return memberRepository.save(member)
|
return memberRepository.save(member)
|
||||||
.doOnSuccess(memberSyncer::sync)
|
.doOnSuccess(saved -> {
|
||||||
|
memberSyncer.sync(saved);
|
||||||
|
clearMemberCache(saved.getId());
|
||||||
|
})
|
||||||
.flatMap(savedMember -> {
|
.flatMap(savedMember -> {
|
||||||
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
|
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
|
||||||
return Mono.just(response);
|
return Mono.just(response);
|
||||||
@@ -105,7 +116,10 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
|||||||
member.setLastLoginAt(LocalDateTime.now());
|
member.setLastLoginAt(LocalDateTime.now());
|
||||||
|
|
||||||
return memberRepository.save(member)
|
return memberRepository.save(member)
|
||||||
.doOnSuccess(memberSyncer::sync)
|
.doOnSuccess(saved -> {
|
||||||
|
memberSyncer.sync(saved);
|
||||||
|
clearMemberCache(saved.getId());
|
||||||
|
})
|
||||||
.flatMap(savedMember -> {
|
.flatMap(savedMember -> {
|
||||||
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
|
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
|
||||||
return Mono.just(response);
|
return Mono.just(response);
|
||||||
@@ -124,7 +138,10 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
|||||||
member.setLastLoginAt(LocalDateTime.now());
|
member.setLastLoginAt(LocalDateTime.now());
|
||||||
|
|
||||||
return memberRepository.save(member)
|
return memberRepository.save(member)
|
||||||
.doOnSuccess(memberSyncer::sync)
|
.doOnSuccess(saved -> {
|
||||||
|
memberSyncer.sync(saved);
|
||||||
|
clearMemberCache(saved.getId());
|
||||||
|
})
|
||||||
.flatMap(savedMember -> {
|
.flatMap(savedMember -> {
|
||||||
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
|
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
|
||||||
return Mono.just(response);
|
return Mono.just(response);
|
||||||
@@ -185,7 +202,10 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
|||||||
member.setPhone(encryptedPhone);
|
member.setPhone(encryptedPhone);
|
||||||
member.setLastLoginAt(LocalDateTime.now());
|
member.setLastLoginAt(LocalDateTime.now());
|
||||||
return memberRepository.save(member)
|
return memberRepository.save(member)
|
||||||
.doOnSuccess(memberSyncer::sync)
|
.doOnSuccess(saved -> {
|
||||||
|
memberSyncer.sync(saved);
|
||||||
|
clearMemberCache(saved.getId());
|
||||||
|
})
|
||||||
.map(savedMember -> {
|
.map(savedMember -> {
|
||||||
log.info("更新会员手机号成功, memberId: {}", savedMember.getId());
|
log.info("更新会员手机号成功, memberId: {}", savedMember.getId());
|
||||||
return true;
|
return true;
|
||||||
@@ -197,12 +217,15 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void clearMemberCache(Long memberId) {
|
||||||
|
String cacheKey = MEMBER_INFO_CACHE_PREFIX + memberId;
|
||||||
|
redisUtil.delete(cacheKey);
|
||||||
|
log.debug("清除会员缓存, memberId: {}", memberId);
|
||||||
|
}
|
||||||
|
|
||||||
private String encryptPhone(String phoneNumber) {
|
private String encryptPhone(String phoneNumber) {
|
||||||
try {
|
try {
|
||||||
String secretKey = wechatProperties.getPhoneEncryption().getSecretKey();
|
String encryptedPhone = AesUtil.encrypt(phoneNumber);
|
||||||
String iv = wechatProperties.getPhoneEncryption().getIv();
|
|
||||||
|
|
||||||
String encryptedPhone = AesUtil.encrypt(phoneNumber, secretKey, iv);
|
|
||||||
|
|
||||||
log.debug("手机号加密成功");
|
log.debug("手机号加密成功");
|
||||||
return encryptedPhone;
|
return encryptedPhone;
|
||||||
@@ -214,10 +237,8 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
|||||||
|
|
||||||
public String decryptPhone(String encryptedPhone) {
|
public String decryptPhone(String encryptedPhone) {
|
||||||
try {
|
try {
|
||||||
String secretKey = wechatProperties.getPhoneEncryption().getSecretKey();
|
|
||||||
String iv = wechatProperties.getPhoneEncryption().getIv();
|
|
||||||
|
|
||||||
String phoneNumber = AesUtil.decrypt(encryptedPhone, secretKey, iv);
|
String phoneNumber = AesUtil.decrypt(encryptedPhone);
|
||||||
|
|
||||||
log.debug("手机号解密成功");
|
log.debug("手机号解密成功");
|
||||||
return phoneNumber;
|
return phoneNumber;
|
||||||
@@ -282,9 +303,9 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<String> roles = new ArrayList<>();
|
List<String> roles = new ArrayList<>();
|
||||||
String accessToken = jwtTokenProvider.generateToken(String.valueOf(member.getId()), member.getId(), roles);
|
String accessToken = jwtTokenProvider.generateToken(String.valueOf(member.getId()), member.getId(), roles, "MEMBER");
|
||||||
|
|
||||||
log.info("JWT Token 生成成功, memberId: {}", member.getId());
|
log.info("JWT Token 生成成功, memberId: {}, userType=MEMBER", member.getId());
|
||||||
|
|
||||||
int expiresIn = 86400;
|
int expiresIn = 86400;
|
||||||
|
|
||||||
@@ -295,6 +316,7 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
|||||||
.expiresIn(expiresIn)
|
.expiresIn(expiresIn)
|
||||||
.isNewUser(isNewUser)
|
.isNewUser(isNewUser)
|
||||||
.needCompleteInfo(needCompleteInfo)
|
.needCompleteInfo(needCompleteInfo)
|
||||||
|
.userType("MEMBER")
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-30
@@ -1,5 +1,6 @@
|
|||||||
package cn.novalon.gym.manage.member.service.impl;
|
package cn.novalon.gym.manage.member.service.impl;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.common.util.HtmlEscapeUtil;
|
||||||
import cn.novalon.gym.manage.member.config.WechatProperties;
|
import cn.novalon.gym.manage.member.config.WechatProperties;
|
||||||
import cn.novalon.gym.manage.member.entity.Member;
|
import cn.novalon.gym.manage.member.entity.Member;
|
||||||
import cn.novalon.gym.manage.member.es.entity.MemberES;
|
import cn.novalon.gym.manage.member.es.entity.MemberES;
|
||||||
@@ -7,6 +8,7 @@ import cn.novalon.gym.manage.member.es.repository.MemberESRepository;
|
|||||||
import cn.novalon.gym.manage.member.repository.IMemberRepository;
|
import cn.novalon.gym.manage.member.repository.IMemberRepository;
|
||||||
import cn.novalon.gym.manage.member.service.WechatOfficialService;
|
import cn.novalon.gym.manage.member.service.WechatOfficialService;
|
||||||
import cn.novalon.gym.manage.member.util.EsSyncUtils;
|
import cn.novalon.gym.manage.member.util.EsSyncUtils;
|
||||||
|
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||||
import cn.novalon.gym.manage.member.vo.WechatUserInfoVO;
|
import cn.novalon.gym.manage.member.vo.WechatUserInfoVO;
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
@@ -40,11 +42,16 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
|||||||
private final MemberESRepository memberESRepository;
|
private final MemberESRepository memberESRepository;
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper()
|
private final ObjectMapper objectMapper = new ObjectMapper()
|
||||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||||
|
|
||||||
private final EsSyncUtils esSyncUtils;
|
private final EsSyncUtils esSyncUtils;
|
||||||
|
private final RedisUtil redisUtil;
|
||||||
|
|
||||||
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer;
|
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer;
|
||||||
|
|
||||||
|
private static final String ACCESS_TOKEN_CACHE_PREFIX = "wechat:access_token:";
|
||||||
|
private static final String MEMBER_INFO_CACHE_PREFIX = "member:info:";
|
||||||
|
private static final long ACCESS_TOKEN_EXPIRE_SECONDS = 7000;
|
||||||
|
private static final long CACHE_EXPIRE_SECONDS = 300;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository);
|
this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository);
|
||||||
@@ -74,24 +81,30 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
|||||||
existingMember.setOfficialOpenId(openId);
|
existingMember.setOfficialOpenId(openId);
|
||||||
|
|
||||||
if (existingMember.getNickname() == null || existingMember.getNickname().isEmpty()) {
|
if (existingMember.getNickname() == null || existingMember.getNickname().isEmpty()) {
|
||||||
existingMember.setNickname(userInfo.getNickname());
|
existingMember.setNickname(HtmlEscapeUtil.escape(userInfo.getNickname()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingMember.getAvatar() == null || existingMember.getAvatar().isEmpty()) {
|
if (existingMember.getAvatar() == null || existingMember.getAvatar().isEmpty()) {
|
||||||
existingMember.setAvatar(userInfo.getHeadimgurl());
|
existingMember.setAvatar(HtmlEscapeUtil.escape(userInfo.getHeadimgurl()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return memberRepository.save(existingMember)
|
return memberRepository.save(existingMember)
|
||||||
.doOnSuccess(memberSyncer::sync)
|
.flatMap(saved -> {
|
||||||
.then(sendWelcomeMessage(openId));
|
memberSyncer.sync(saved);
|
||||||
|
return clearMemberCache(saved.getId())
|
||||||
|
.then(sendWelcomeMessage(openId));
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
log.info("老用户关注服务号: memberId={}", existingMember.getId());
|
log.info("老用户关注服务号: memberId={}", existingMember.getId());
|
||||||
existingMember.setSubscribed(true);
|
existingMember.setSubscribed(true);
|
||||||
existingMember.setLastLoginAt(LocalDateTime.now());
|
existingMember.setLastLoginAt(LocalDateTime.now());
|
||||||
|
|
||||||
return memberRepository.save(existingMember)
|
return memberRepository.save(existingMember)
|
||||||
.doOnSuccess(memberSyncer::sync)
|
.flatMap(saved -> {
|
||||||
.then(sendWelcomeMessage(openId));
|
memberSyncer.sync(saved);
|
||||||
|
return clearMemberCache(saved.getId())
|
||||||
|
.then(sendWelcomeMessage(openId));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.switchIfEmpty(Mono.defer(() -> {
|
.switchIfEmpty(Mono.defer(() -> {
|
||||||
@@ -104,7 +117,10 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
|||||||
existingMember.setLastLoginAt(LocalDateTime.now());
|
existingMember.setLastLoginAt(LocalDateTime.now());
|
||||||
|
|
||||||
return memberRepository.save(existingMember)
|
return memberRepository.save(existingMember)
|
||||||
.doOnSuccess(memberSyncer::sync)
|
.doOnSuccess(saved -> {
|
||||||
|
memberSyncer.sync(saved);
|
||||||
|
clearMemberCache(saved.getId());
|
||||||
|
})
|
||||||
.then(sendWelcomeMessage(openId));
|
.then(sendWelcomeMessage(openId));
|
||||||
})
|
})
|
||||||
.switchIfEmpty(Mono.defer(() -> {
|
.switchIfEmpty(Mono.defer(() -> {
|
||||||
@@ -122,7 +138,10 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
|||||||
existingMember.setLastLoginAt(LocalDateTime.now());
|
existingMember.setLastLoginAt(LocalDateTime.now());
|
||||||
|
|
||||||
return memberRepository.save(existingMember)
|
return memberRepository.save(existingMember)
|
||||||
.doOnSuccess(memberSyncer::sync)
|
.doOnSuccess(saved -> {
|
||||||
|
memberSyncer.sync(saved);
|
||||||
|
clearMemberCache(saved.getId());
|
||||||
|
})
|
||||||
.then(sendWelcomeMessage(openId));
|
.then(sendWelcomeMessage(openId));
|
||||||
})
|
})
|
||||||
.switchIfEmpty(Mono.defer(() -> {
|
.switchIfEmpty(Mono.defer(() -> {
|
||||||
@@ -148,8 +167,10 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
|||||||
member.setSubscribed(false);
|
member.setSubscribed(false);
|
||||||
member.setLastLoginAt(LocalDateTime.now());
|
member.setLastLoginAt(LocalDateTime.now());
|
||||||
return memberRepository.save(member)
|
return memberRepository.save(member)
|
||||||
.doOnSuccess(memberSyncer::sync)
|
.flatMap(saved -> {
|
||||||
.then();
|
memberSyncer.sync(saved);
|
||||||
|
return clearMemberCache(saved.getId()).then();
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.then()
|
.then()
|
||||||
.switchIfEmpty(Mono.defer(() -> {
|
.switchIfEmpty(Mono.defer(() -> {
|
||||||
@@ -213,7 +234,11 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
|||||||
member.setOfficialOpenId(officialOpenId);
|
member.setOfficialOpenId(officialOpenId);
|
||||||
}
|
}
|
||||||
return memberRepository.save(member)
|
return memberRepository.save(member)
|
||||||
.doOnSuccess(memberSyncer::sync)
|
.flatMap(saved -> {
|
||||||
|
memberSyncer.sync(saved);
|
||||||
|
return clearMemberCache(saved.getId())
|
||||||
|
.then(Mono.just(saved));
|
||||||
|
})
|
||||||
.map(savedMember -> {
|
.map(savedMember -> {
|
||||||
log.info("关联成功, memberId: {}", savedMember.getId());
|
log.info("关联成功, memberId: {}", savedMember.getId());
|
||||||
return true;
|
return true;
|
||||||
@@ -268,28 +293,38 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取微信AccessToken
|
* 获取微信AccessToken
|
||||||
*
|
|
||||||
* TODO: 应该使用缓存,避免频繁请求
|
|
||||||
*/
|
*/
|
||||||
private Mono<String> getAccessToken() {
|
private Mono<String> getAccessToken() {
|
||||||
String appId = wechatProperties.getMp().getAppId();
|
String cacheKey = ACCESS_TOKEN_CACHE_PREFIX + "mp";
|
||||||
String appSecret = wechatProperties.getMp().getAppSecret();
|
|
||||||
|
|
||||||
String url = "https://api.weixin.qq.com/cgi-bin/token"
|
return redisUtil.get(cacheKey, String.class)
|
||||||
+ "?grant_type=client_credential"
|
.flatMap(cachedToken -> {
|
||||||
+ "&appid=" + appId
|
if (cachedToken != null) {
|
||||||
+ "&secret=" + appSecret;
|
log.debug("从缓存获取服务号access_token");
|
||||||
|
return Mono.just(cachedToken);
|
||||||
return webClient.get()
|
|
||||||
.uri(url)
|
|
||||||
.accept(MediaType.APPLICATION_JSON)
|
|
||||||
.retrieve()
|
|
||||||
.bodyToMono(Map.class)
|
|
||||||
.map(response -> {
|
|
||||||
if (response.containsKey("errcode")) {
|
|
||||||
throw new RuntimeException("获取AccessToken失败: " + response.get("errmsg"));
|
|
||||||
}
|
}
|
||||||
return (String) response.get("access_token");
|
|
||||||
|
String appId = wechatProperties.getMp().getAppId();
|
||||||
|
String appSecret = wechatProperties.getMp().getAppSecret();
|
||||||
|
|
||||||
|
String url = "https://api.weixin.qq.com/cgi-bin/token"
|
||||||
|
+ "?grant_type=client_credential"
|
||||||
|
+ "&appid=" + appId
|
||||||
|
+ "&secret=" + appSecret;
|
||||||
|
|
||||||
|
return webClient.get()
|
||||||
|
.uri(url)
|
||||||
|
.accept(MediaType.APPLICATION_JSON)
|
||||||
|
.retrieve()
|
||||||
|
.bodyToMono(Map.class)
|
||||||
|
.flatMap(response -> {
|
||||||
|
if (response.containsKey("errcode")) {
|
||||||
|
throw new RuntimeException("获取AccessToken失败: " + response.get("errmsg"));
|
||||||
|
}
|
||||||
|
String accessToken = (String) response.get("access_token");
|
||||||
|
return redisUtil.setWithExpire(cacheKey, accessToken, ACCESS_TOKEN_EXPIRE_SECONDS)
|
||||||
|
.then(Mono.just(accessToken));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,4 +370,10 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
|||||||
return Mono.empty(); // 即使发送失败也不影响主流程
|
return Mono.empty(); // 即使发送失败也不影响主流程
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Mono<Long> clearMemberCache(Long memberId) {
|
||||||
|
String cacheKey = MEMBER_INFO_CACHE_PREFIX + memberId;
|
||||||
|
return redisUtil.delete(cacheKey)
|
||||||
|
.doOnSuccess(result -> log.debug("清除会员缓存, memberId: {}", memberId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-10
@@ -1,12 +1,24 @@
|
|||||||
package cn.novalon.gym.manage.member.util;
|
package cn.novalon.gym.manage.member.util;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.member.config.WechatProperties;
|
||||||
|
import co.elastic.clients.elasticsearch.ElasticsearchClient;
|
||||||
|
import co.elastic.clients.elasticsearch.core.IndexResponse;
|
||||||
|
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
|
||||||
|
import co.elastic.clients.transport.rest_client.RestClientTransport;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.http.HttpHost;
|
||||||
|
import org.checkerframework.checker.units.qual.K;
|
||||||
|
import org.elasticsearch.client.RestClient;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AES加密工具类
|
* AES加密工具类
|
||||||
@@ -16,24 +28,35 @@ import java.util.Base64;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@Component
|
||||||
public class AesUtil {
|
public class AesUtil {
|
||||||
|
|
||||||
private static final String ALGORITHM = "AES";
|
private static final String ALGORITHM = "AES";
|
||||||
private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding";
|
private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding";
|
||||||
|
|
||||||
|
private static String KEY;
|
||||||
|
private static String IV;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public void setWechatProperties(WechatProperties props) {
|
||||||
|
KEY = props.getPhoneEncryption().getSecretKey(); // 从配置类读取
|
||||||
|
IV = props.getPhoneEncryption().getIv();
|
||||||
|
if(KEY == null || IV == null) throw new RuntimeException("请配置AES密钥和偏移量");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AES 解密
|
* AES 解密
|
||||||
*
|
*
|
||||||
* @param encryptedData 加密数据,Base64编码
|
* @param encryptedData 加密数据,Base64编码
|
||||||
* @param key AES密钥,Base64编码(32字节)
|
|
||||||
* @param iv 初始化向量IV,Base64编码(16字节)
|
|
||||||
* @return 解密后的字符串
|
* @return 解密后的字符串
|
||||||
*/
|
*/
|
||||||
public static String decrypt(String encryptedData, String key, String iv) {
|
public static String decrypt(String encryptedData) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
byte[] dataByte = Base64.getDecoder().decode(encryptedData);
|
byte[] dataByte = Base64.getDecoder().decode(encryptedData);
|
||||||
byte[] keyByte = Base64.getDecoder().decode(key);
|
byte[] keyByte = Base64.getDecoder().decode(KEY);
|
||||||
byte[] ivByte = Base64.getDecoder().decode(iv);
|
byte[] ivByte = Base64.getDecoder().decode(IV);
|
||||||
|
|
||||||
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||||
SecretKeySpec secretKeySpec = new SecretKeySpec(keyByte, ALGORITHM);
|
SecretKeySpec secretKeySpec = new SecretKeySpec(keyByte, ALGORITHM);
|
||||||
@@ -52,15 +75,13 @@ public class AesUtil {
|
|||||||
* AES 加密
|
* AES 加密
|
||||||
*
|
*
|
||||||
* @param data 原始数据
|
* @param data 原始数据
|
||||||
* @param key AES密钥,Base64编码(32字节)
|
|
||||||
* @param iv 初始化向量IV,Base64编码(16字节)
|
|
||||||
* @return Base64编码的加密数据
|
* @return Base64编码的加密数据
|
||||||
*/
|
*/
|
||||||
public static String encrypt(String data, String key, String iv) {
|
public static String encrypt(String data) {
|
||||||
try {
|
try {
|
||||||
byte[] dataByte = data.getBytes(StandardCharsets.UTF_8);
|
byte[] dataByte = data.getBytes(StandardCharsets.UTF_8);
|
||||||
byte[] keyByte = Base64.getDecoder().decode(key);
|
byte[] keyByte = Base64.getDecoder().decode(KEY);
|
||||||
byte[] ivByte = Base64.getDecoder().decode(iv);
|
byte[] ivByte = Base64.getDecoder().decode(IV);
|
||||||
|
|
||||||
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||||
SecretKeySpec secretKeySpec = new SecretKeySpec(keyByte, ALGORITHM);
|
SecretKeySpec secretKeySpec = new SecretKeySpec(keyByte, ALGORITHM);
|
||||||
|
|||||||
+1
-1
@@ -56,7 +56,7 @@ public class WechatPhoneUtil {
|
|||||||
* @param phone 手机号
|
* @param phone 手机号
|
||||||
* @return 脱敏后的手机号,如:138****8000
|
* @return 脱敏后的手机号,如:138****8000
|
||||||
*/
|
*/
|
||||||
private String maskPhone(String phone) {
|
public static String maskPhone(String phone) {
|
||||||
if (phone == null || phone.length() < 7) {
|
if (phone == null || phone.length() < 7) {
|
||||||
return "***";
|
return "***";
|
||||||
}
|
}
|
||||||
|
|||||||
+92
@@ -0,0 +1,92 @@
|
|||||||
|
package cn.novalon.gym.manage.member.vo;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员卡类型响应 VO
|
||||||
|
*
|
||||||
|
* @author 付嘉
|
||||||
|
* @date 2026-05-27
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class MemberCardInfoVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键ID
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员卡ID
|
||||||
|
*/
|
||||||
|
private Long memberCardId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员卡名称
|
||||||
|
*/
|
||||||
|
private String memberCardName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员卡类型:TIME_CARD-时长卡, COUNT_CARD-次卡, STORED_VALUE_CARD-储值卡
|
||||||
|
*/
|
||||||
|
private String memberCardType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卡类型描述
|
||||||
|
*/
|
||||||
|
private String memberCardTypeDesc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员卡价格
|
||||||
|
*/
|
||||||
|
private BigDecimal memberCardPrice;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 有效天数(时长卡用)
|
||||||
|
*/
|
||||||
|
private Integer memberCardValidityDays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总次数(次卡用)
|
||||||
|
*/
|
||||||
|
private Integer memberCardTotalTimes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 面额(储值卡用)
|
||||||
|
*/
|
||||||
|
private BigDecimal memberCardAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态:0-下架, 1-上架
|
||||||
|
*/
|
||||||
|
private Integer memberCardStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态描述
|
||||||
|
*/
|
||||||
|
private String memberCardStatusDesc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扩展配置(JSON格式)
|
||||||
|
*/
|
||||||
|
private String extraConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
+96
@@ -0,0 +1,96 @@
|
|||||||
|
package cn.novalon.gym.manage.member.vo;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员详情 VO(管理端使用)
|
||||||
|
*
|
||||||
|
* @author 付嘉
|
||||||
|
* @date 2026-05-27
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class MemberDetailVO {
|
||||||
|
|
||||||
|
// ==================== 会员基础信息 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员ID
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员编号
|
||||||
|
*/
|
||||||
|
private String memberNo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 昵称
|
||||||
|
*/
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号(脱敏显示)
|
||||||
|
*/
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 性别描述
|
||||||
|
*/
|
||||||
|
private String genderDesc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生日
|
||||||
|
*/
|
||||||
|
private Date birthday;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 地址
|
||||||
|
*/
|
||||||
|
private String address;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 头像URL
|
||||||
|
*/
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否关注服务号
|
||||||
|
*/
|
||||||
|
private Boolean subscribed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最后登录时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime lastLoginAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
// ==================== 会员卡信息 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员持有的卡列表
|
||||||
|
*/
|
||||||
|
private List<MemberCardInfoVO> memberCards;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 有效会员卡数量
|
||||||
|
*/
|
||||||
|
private Integer activeCardCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过期/用完会员卡数量
|
||||||
|
*/
|
||||||
|
private Integer inactiveCardCount;
|
||||||
|
}
|
||||||
+7
-2
@@ -1,10 +1,12 @@
|
|||||||
package cn.novalon.gym.manage.member.vo;
|
package cn.novalon.gym.manage.member.vo;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.member.enums.GenderEnum;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,10 +32,13 @@ public class MemberInfoVO {
|
|||||||
private String phone;
|
private String phone;
|
||||||
|
|
||||||
// 性别
|
// 性别
|
||||||
private Integer gender;
|
private GenderEnum gender;
|
||||||
|
|
||||||
|
// 性别描述
|
||||||
|
private String genderDesc;
|
||||||
|
|
||||||
// 生日
|
// 生日
|
||||||
private Date birthday;
|
private LocalDate birthday;
|
||||||
|
|
||||||
// 头像
|
// 头像
|
||||||
private String avatar;
|
private String avatar;
|
||||||
|
|||||||
+3
@@ -35,4 +35,7 @@ public class WechatLoginVO {
|
|||||||
|
|
||||||
// 是否需要补全信息(昵称、手机号等)
|
// 是否需要补全信息(昵称、手机号等)
|
||||||
private Boolean needCompleteInfo;
|
private Boolean needCompleteInfo;
|
||||||
|
|
||||||
|
// 用户类型(MEMBER)
|
||||||
|
private String userType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ CREATE TABLE IF NOT EXISTS member_user (
|
|||||||
nickname VARCHAR(100), -- 昵称
|
nickname VARCHAR(100), -- 昵称
|
||||||
phone VARCHAR(255), -- 手机号(AES加密存储)
|
phone VARCHAR(255), -- 手机号(AES加密存储)
|
||||||
gender INTEGER DEFAULT 0, -- 性别:0-未知,1-男,2-女
|
gender INTEGER DEFAULT 0, -- 性别:0-未知,1-男,2-女
|
||||||
birthday TIMESTAMP, -- 生日
|
birthday DATE, -- 生日
|
||||||
address VARCHAR(500), -- 地址
|
address VARCHAR(500), -- 地址
|
||||||
avatar VARCHAR(500), -- 头像URL
|
avatar VARCHAR(500), -- 头像URL
|
||||||
subscribed BOOLEAN DEFAULT FALSE, -- 是否关注服务号
|
subscribed BOOLEAN DEFAULT FALSE, -- 是否关注服务号
|
||||||
|
|||||||
@@ -2,21 +2,23 @@
|
|||||||
wechat:
|
wechat:
|
||||||
# Mock模式:true=使用模拟数据(开发测试),false=调用真实微信API(生产环境)
|
# Mock模式:true=使用模拟数据(开发测试),false=调用真实微信API(生产环境)
|
||||||
mock-enabled: false
|
mock-enabled: false
|
||||||
|
|
||||||
miniapp:
|
miniapp:
|
||||||
app-id: wx4d480112b426100b
|
app-id: ${WECHAT_MINIAPP_APP_ID}
|
||||||
app-secret: 78548f0c0ff66c73d3e8b071897eb1e5
|
app-secret: ${WECHAT_MINIAPP_SECRET}
|
||||||
|
|
||||||
mp:
|
mp:
|
||||||
app-id: wx6f138c9aacc8a0e8
|
app-id: ${WECHAT_MP_APP_ID}
|
||||||
app-secret: 5df2e315e9268e96a43bb2cce1d2270b
|
app-secret: ${WECHAT_MP_SECRET}
|
||||||
token: test_token
|
token: ${WECHAT_MP_TOKEN}
|
||||||
aes-key: ${WECHAT_MP_AESKEY:test_aes_key}
|
aes-key: ${WECHAT_MP_AESKEY}
|
||||||
# 服务器回调地址(微信服务器推送事件的URL)
|
callback-url: ${WECHAT_MP_CALLBACK_URL}
|
||||||
callback-url: https://1me240209tk74.vicp.fun/api/member/auth/mp/callback
|
|
||||||
# 手机号加密配置
|
# 手机号加密配置
|
||||||
phone-encryption:
|
phone-encryption:
|
||||||
secret-key: nVnA99iBfyK0IE6SkcUYdVAaVrezyn2sLRdLfkIyWnY=
|
secret-key: ${PHONE_ENCRYPTION_SECRET_KEY}
|
||||||
iv: LMpG6Ih9mmfEAALOCeIJBw==
|
iv: ${PHONE_ENCRYPTION_IV}
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
elasticsearch:
|
elasticsearch:
|
||||||
uris: http://localhost:9200 # ES 服务器地址(支持多个,逗号分隔)
|
uris: http://localhost:9200
|
||||||
@@ -43,6 +43,11 @@
|
|||||||
<artifactId>gym-member</artifactId>
|
<artifactId>gym-member</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.novalon.gym.manage</groupId>
|
||||||
|
<artifactId>gym-checkIn</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
@@ -139,6 +144,10 @@
|
|||||||
<groupId>org.springdoc</groupId>
|
<groupId>org.springdoc</groupId>
|
||||||
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
|
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
+8
-2
@@ -10,6 +10,8 @@ import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDeta
|
|||||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.ComponentScan;
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
|
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
|
||||||
|
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.web.server.WebFilter;
|
import org.springframework.web.server.WebFilter;
|
||||||
|
|
||||||
@@ -17,9 +19,13 @@ import java.util.List;
|
|||||||
|
|
||||||
@SpringBootApplication(scanBasePackages = "cn.novalon.gym.manage", exclude = {
|
@SpringBootApplication(scanBasePackages = "cn.novalon.gym.manage", exclude = {
|
||||||
ReactiveUserDetailsServiceAutoConfiguration.class })
|
ReactiveUserDetailsServiceAutoConfiguration.class })
|
||||||
@EnableR2dbcRepositories(basePackages = { "cn.novalon.gym.manage.db.dao",
|
@EnableR2dbcRepositories(basePackages = {
|
||||||
|
"cn.novalon.gym.manage.db.dao",
|
||||||
"cn.novalon.gym.manage.sys.audit.repository" ,
|
"cn.novalon.gym.manage.sys.audit.repository" ,
|
||||||
"cn.novalon.gym.manage.gymmembercard.dao"})
|
"cn.novalon.gym.manage.gymmembercard.dao",
|
||||||
|
"cn.novalon.gym.manage.member.repository"
|
||||||
|
})
|
||||||
|
@EnableReactiveElasticsearchRepositories(basePackages = "cn.novalon.gym.manage.member.es.repository")
|
||||||
public class ManageApplication {
|
public class ManageApplication {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ManageApplication.class);
|
private static final Logger logger = LoggerFactory.getLogger(ManageApplication.class);
|
||||||
|
|||||||
+152
-147
@@ -1,6 +1,7 @@
|
|||||||
package cn.novalon.gym.manage.app.config;
|
package cn.novalon.gym.manage.app.config;
|
||||||
|
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.checkIn.handler.CheckInHandler;
|
||||||
import cn.novalon.gym.manage.file.handler.SysFileHandler;
|
import cn.novalon.gym.manage.file.handler.SysFileHandler;
|
||||||
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;
|
||||||
@@ -33,7 +34,7 @@ import static org.springframework.web.reactive.function.server.RouterFunctions.r
|
|||||||
*
|
*
|
||||||
* 文件定义:配置WebFlux函数式路由,将HTTP请求映射到对应的Handler方法
|
* 文件定义:配置WebFlux函数式路由,将HTTP请求映射到对应的Handler方法
|
||||||
* 涉及业务:用户、角色、字典、菜单、公告、文件等所有RESTful API路由
|
* 涉及业务:用户、角色、字典、菜单、公告、文件等所有RESTful API路由
|
||||||
* 算法:使用RouterFunctions.route()构建函数式路由规则
|
* 路由规范:后台管理 API 统一前缀 /api/admin/**,前台会员 API 统一前缀 /api/member/**
|
||||||
*
|
*
|
||||||
* @author 张翔
|
* @author 张翔
|
||||||
* @date 2026-03-13
|
* @date 2026-03-13
|
||||||
@@ -62,154 +63,155 @@ public class SystemRouter {
|
|||||||
PasswordDiagnosticHandler passwordDiagnosticHandler,
|
PasswordDiagnosticHandler passwordDiagnosticHandler,
|
||||||
MemberCardHandler memberCardHandler,
|
MemberCardHandler memberCardHandler,
|
||||||
MemberCardRecordHandler memberCardRecordHandler,
|
MemberCardRecordHandler memberCardRecordHandler,
|
||||||
MemberCardTransactionHandler memberCardTransactionHandler) {
|
MemberCardTransactionHandler memberCardTransactionHandler,
|
||||||
|
CheckInHandler checkInHandler) {
|
||||||
|
|
||||||
return route()
|
return route()
|
||||||
// ========== 诊断路由 ==========
|
// ========== 诊断路由(管理端) ==========
|
||||||
.GET("/api/diagnostic/password", passwordDiagnosticHandler::diagnose)
|
.GET("/api/admin/diagnostic/password", passwordDiagnosticHandler::diagnose)
|
||||||
|
|
||||||
// ========== 字典路由 ==========
|
// ========== 字典路由(管理端) ==========
|
||||||
.GET("/api/dictionaries", dictionaryHandler::getAllDictionaries)
|
.GET("/api/admin/dictionaries", dictionaryHandler::getAllDictionaries)
|
||||||
.GET("/api/dictionaries/{id}", dictionaryHandler::getDictionaryById)
|
.GET("/api/admin/dictionaries/{id}", dictionaryHandler::getDictionaryById)
|
||||||
.GET("/api/dictionaries/type/{type}", dictionaryHandler::getDictionariesByType)
|
.GET("/api/admin/dictionaries/type/{type}", dictionaryHandler::getDictionariesByType)
|
||||||
.GET("/api/dictionaries/check/exists", dictionaryHandler::checkTypeAndCodeExists)
|
.GET("/api/admin/dictionaries/check/exists", dictionaryHandler::checkTypeAndCodeExists)
|
||||||
.POST("/api/dictionaries", dictionaryHandler::createDictionary)
|
.POST("/api/admin/dictionaries", dictionaryHandler::createDictionary)
|
||||||
.PUT("/api/dictionaries/{id}", dictionaryHandler::updateDictionary)
|
.PUT("/api/admin/dictionaries/{id}", dictionaryHandler::updateDictionary)
|
||||||
.DELETE("/api/dictionaries/{id}", dictionaryHandler::deleteDictionary)
|
.DELETE("/api/admin/dictionaries/{id}", dictionaryHandler::deleteDictionary)
|
||||||
|
|
||||||
// ========== 用户路由 ==========
|
// ========== 用户路由(管理端) ==========
|
||||||
.GET("/api/users", userHandler::getAllUsers)
|
.GET("/api/admin/users", userHandler::getAllUsers)
|
||||||
.GET("/api/users/page", userHandler::getUsersByPage)
|
.GET("/api/admin/users/page", userHandler::getUsersByPage)
|
||||||
.GET("/api/users/count", userHandler::getUserCount)
|
.GET("/api/admin/users/count", userHandler::getUserCount)
|
||||||
.GET("/api/users/username/{username}", userHandler::getUserByUsername)
|
.GET("/api/admin/users/username/{username}", userHandler::getUserByUsername)
|
||||||
.GET("/api/users/check/username", userHandler::checkUsernameExists)
|
.GET("/api/admin/users/check/username", userHandler::checkUsernameExists)
|
||||||
.GET("/api/users/check/email", userHandler::checkEmailExists)
|
.GET("/api/admin/users/check/email", userHandler::checkEmailExists)
|
||||||
.POST("/api/users", userHandler::createUser)
|
.POST("/api/admin/users", userHandler::createUser)
|
||||||
.GET("/api/users/{id}", userHandler::getUserById)
|
.GET("/api/admin/users/{id}", userHandler::getUserById)
|
||||||
.PUT("/api/users/{id}", userHandler::updateUser)
|
.PUT("/api/admin/users/{id}", userHandler::updateUser)
|
||||||
.DELETE("/api/users/{id}", userHandler::deleteUser)
|
.DELETE("/api/admin/users/{id}", userHandler::deleteUser)
|
||||||
.POST("/api/users/{id}/action/change-password", userHandler::changePassword)
|
.POST("/api/admin/users/{id}/action/change-password", userHandler::changePassword)
|
||||||
.POST("/api/users/{id}/action/logical-delete", userHandler::logicalDeleteUser)
|
.POST("/api/admin/users/{id}/action/logical-delete", userHandler::logicalDeleteUser)
|
||||||
.POST("/api/users/logical-delete", userHandler::logicalDeleteUsers)
|
.POST("/api/admin/users/logical-delete", userHandler::logicalDeleteUsers)
|
||||||
.POST("/api/users/action/restore", userHandler::restoreUsers)
|
.POST("/api/admin/users/action/restore", userHandler::restoreUsers)
|
||||||
.POST("/api/users/{id}/action/restore", userHandler::restoreUser)
|
.POST("/api/admin/users/{id}/action/restore", userHandler::restoreUser)
|
||||||
.GET("/api/users/{id}/roles", userHandler::getUserRoles)
|
.GET("/api/admin/users/{id}/roles", userHandler::getUserRoles)
|
||||||
.POST("/api/users/{id}/roles", userHandler::assignRoles)
|
.POST("/api/admin/users/{id}/roles", userHandler::assignRoles)
|
||||||
|
|
||||||
// ========== 菜单路由 ==========
|
// ========== 菜单路由(管理端) ==========
|
||||||
.GET("/api/menus", menuHandler::getAllMenus)
|
.GET("/api/admin/menus", menuHandler::getAllMenus)
|
||||||
.GET("/api/menus/tree", menuHandler::getMenuTree)
|
.GET("/api/admin/menus/tree", menuHandler::getMenuTree)
|
||||||
.GET("/api/menus/{id}", menuHandler::getMenuById)
|
.GET("/api/admin/menus/{id}", menuHandler::getMenuById)
|
||||||
.POST("/api/menus", menuHandler::createMenu)
|
.POST("/api/admin/menus", menuHandler::createMenu)
|
||||||
.PUT("/api/menus/{id}", menuHandler::updateMenu)
|
.PUT("/api/admin/menus/{id}", menuHandler::updateMenu)
|
||||||
.DELETE("/api/menus/{id}", menuHandler::deleteMenu)
|
.DELETE("/api/admin/menus/{id}", menuHandler::deleteMenu)
|
||||||
|
|
||||||
// ========== 角色路由 ==========
|
// ========== 角色路由(管理端) ==========
|
||||||
.GET("/api/roles", roleHandler::getAllRoles)
|
.GET("/api/admin/roles", roleHandler::getAllRoles)
|
||||||
.GET("/api/roles/page", roleHandler::getRolesByPage)
|
.GET("/api/admin/roles/page", roleHandler::getRolesByPage)
|
||||||
.GET("/api/roles/count", roleHandler::getRoleCount)
|
.GET("/api/admin/roles/count", roleHandler::getRoleCount)
|
||||||
.GET("/api/roles/name/{roleName}", roleHandler::getRoleByName)
|
.GET("/api/admin/roles/name/{roleName}", roleHandler::getRoleByName)
|
||||||
.GET("/api/roles/check-name", roleHandler::checkNameExists)
|
.GET("/api/admin/roles/check-name", roleHandler::checkNameExists)
|
||||||
.GET("/api/roles/{id}", roleHandler::getRoleById)
|
.GET("/api/admin/roles/{id}", roleHandler::getRoleById)
|
||||||
.POST("/api/roles", roleHandler::createRole)
|
.POST("/api/admin/roles", roleHandler::createRole)
|
||||||
.PUT("/api/roles/{id}", roleHandler::updateRole)
|
.PUT("/api/admin/roles/{id}", roleHandler::updateRole)
|
||||||
.DELETE("/api/roles/{id}", roleHandler::deleteRole)
|
.DELETE("/api/admin/roles/{id}", roleHandler::deleteRole)
|
||||||
.POST("/api/roles/{id}/restore", roleHandler::restoreRole)
|
.POST("/api/admin/roles/{id}/restore", roleHandler::restoreRole)
|
||||||
.GET("/api/roles/{id}/permissions", permissionHandler::getPermissionsByRoleId)
|
.GET("/api/admin/roles/{id}/permissions", permissionHandler::getPermissionsByRoleId)
|
||||||
.POST("/api/roles/{id}/permissions", permissionHandler::assignPermissionsToRole)
|
.POST("/api/admin/roles/{id}/permissions", permissionHandler::assignPermissionsToRole)
|
||||||
|
|
||||||
// ========== 配置路由 ==========
|
// ========== 配置路由(管理端) ==========
|
||||||
.GET("/api/config", configHandler::getAllConfigs)
|
.GET("/api/admin/config", configHandler::getAllConfigs)
|
||||||
.GET("/api/config/{id}", configHandler::getConfigById)
|
.GET("/api/admin/config/{id}", configHandler::getConfigById)
|
||||||
.GET("/api/config/key/{configKey}", configHandler::getConfigByKey)
|
.GET("/api/admin/config/key/{configKey}", configHandler::getConfigByKey)
|
||||||
.POST("/api/config", configHandler::createConfig)
|
.POST("/api/admin/config", configHandler::createConfig)
|
||||||
.PUT("/api/config/{id}", configHandler::updateConfig)
|
.PUT("/api/admin/config/{id}", configHandler::updateConfig)
|
||||||
.DELETE("/api/config/{id}", configHandler::deleteConfig)
|
.DELETE("/api/admin/config/{id}", configHandler::deleteConfig)
|
||||||
|
|
||||||
// ========== 日志路由 ==========
|
// ========== 日志路由(管理端) ==========
|
||||||
.GET("/api/logs/login", logHandler::getAllLoginLogs)
|
.GET("/api/admin/logs/login", logHandler::getAllLoginLogs)
|
||||||
.GET("/api/logs/login/page", logHandler::getLoginLogsByPage)
|
.GET("/api/admin/logs/login/page", logHandler::getLoginLogsByPage)
|
||||||
.GET("/api/logs/login/count", logHandler::getLoginLogCount)
|
.GET("/api/admin/logs/login/count", logHandler::getLoginLogCount)
|
||||||
.GET("/api/logs/login/today/count", logHandler::getTodayLoginCount)
|
.GET("/api/admin/logs/login/today/count", logHandler::getTodayLoginCount)
|
||||||
.GET("/api/logs/login/recent", logHandler::getRecentLoginLogs)
|
.GET("/api/admin/logs/login/recent", logHandler::getRecentLoginLogs)
|
||||||
.GET("/api/logs/login/{id}", logHandler::getLoginLogById)
|
.GET("/api/admin/logs/login/{id}", logHandler::getLoginLogById)
|
||||||
.POST("/api/logs/login", logHandler::createLoginLog)
|
.POST("/api/admin/logs/login", logHandler::createLoginLog)
|
||||||
.GET("/api/logs/exception", logHandler::getAllExceptionLogs)
|
.GET("/api/admin/logs/exception", logHandler::getAllExceptionLogs)
|
||||||
.GET("/api/logs/exception/page", logHandler::getExceptionLogsByPage)
|
.GET("/api/admin/logs/exception/page", logHandler::getExceptionLogsByPage)
|
||||||
.GET("/api/logs/exception/count", logHandler::getExceptionLogCount)
|
.GET("/api/admin/logs/exception/count", logHandler::getExceptionLogCount)
|
||||||
.GET("/api/logs/exception/{id}", logHandler::getExceptionLogById)
|
.GET("/api/admin/logs/exception/{id}", logHandler::getExceptionLogById)
|
||||||
.POST("/api/logs/exception", logHandler::createExceptionLog)
|
.POST("/api/admin/logs/exception", logHandler::createExceptionLog)
|
||||||
.GET("/api/logs/operation", operationLogHandler::getAllOperationLogs)
|
.GET("/api/admin/logs/operation", operationLogHandler::getAllOperationLogs)
|
||||||
.GET("/api/logs/operation/export", operationLogHandler::exportOperationLogs)
|
.GET("/api/admin/logs/operation/export", operationLogHandler::exportOperationLogs)
|
||||||
.GET("/api/logs/operation/page", operationLogHandler::getOperationLogsByPage)
|
.GET("/api/admin/logs/operation/page", operationLogHandler::getOperationLogsByPage)
|
||||||
.GET("/api/logs/operation/count", operationLogHandler::getOperationLogCount)
|
.GET("/api/admin/logs/operation/count", operationLogHandler::getOperationLogCount)
|
||||||
.GET("/api/logs/operation/{id}", operationLogHandler::getOperationLogById)
|
.GET("/api/admin/logs/operation/{id}", operationLogHandler::getOperationLogById)
|
||||||
.POST("/api/logs/operation", operationLogHandler::createOperationLog)
|
.POST("/api/admin/logs/operation", operationLogHandler::createOperationLog)
|
||||||
|
|
||||||
// ========== 认证路由 ==========
|
// ========== 认证路由(管理端) ==========
|
||||||
.POST("/api/auth/login", authHandler::login)
|
.POST("/api/admin/auth/login", authHandler::login)
|
||||||
.POST("/api/auth/register", authHandler::register)
|
.POST("/api/admin/auth/register", authHandler::register)
|
||||||
.POST("/api/auth/logout", authHandler::logout)
|
.POST("/api/admin/auth/logout", authHandler::logout)
|
||||||
|
|
||||||
// ========== 统计路由 ==========
|
// ========== 统计路由(管理端) ==========
|
||||||
.GET("/api/stats/overview", statsHandler::getOverview)
|
.GET("/api/admin/stats/overview", statsHandler::getOverview)
|
||||||
|
|
||||||
// ========== 数据字典路由 ==========
|
// ========== 数据字典路由(管理端) ==========
|
||||||
.GET("/api/dict/types", dictHandler::getAllDictTypes)
|
.GET("/api/admin/dict/types", dictHandler::getAllDictTypes)
|
||||||
.GET("/api/dict/types/{id}", dictHandler::getDictTypeById)
|
.GET("/api/admin/dict/types/{id}", dictHandler::getDictTypeById)
|
||||||
.GET("/api/dict/types/type/{dictType}", dictHandler::getDictTypeByType)
|
.GET("/api/admin/dict/types/type/{dictType}", dictHandler::getDictTypeByType)
|
||||||
.POST("/api/dict/types", dictHandler::createDictType)
|
.POST("/api/admin/dict/types", dictHandler::createDictType)
|
||||||
.PUT("/api/dict/types/{id}", dictHandler::updateDictType)
|
.PUT("/api/admin/dict/types/{id}", dictHandler::updateDictType)
|
||||||
.DELETE("/api/dict/types/{id}", dictHandler::deleteDictType)
|
.DELETE("/api/admin/dict/types/{id}", dictHandler::deleteDictType)
|
||||||
.GET("/api/dict/data", dictHandler::getAllDictData)
|
.GET("/api/admin/dict/data", dictHandler::getAllDictData)
|
||||||
.GET("/api/dict/data/type/{dictType}", dictHandler::getDictDataByType)
|
.GET("/api/admin/dict/data/type/{dictType}", dictHandler::getDictDataByType)
|
||||||
.GET("/api/dict/data/{id}", dictHandler::getDictDataById)
|
.GET("/api/admin/dict/data/{id}", dictHandler::getDictDataById)
|
||||||
.POST("/api/dict/data", dictHandler::createDictData)
|
.POST("/api/admin/dict/data", dictHandler::createDictData)
|
||||||
.PUT("/api/dict/data/{id}", dictHandler::updateDictData)
|
.PUT("/api/admin/dict/data/{id}", dictHandler::updateDictData)
|
||||||
.DELETE("/api/dict/data/{id}", dictHandler::deleteDictData)
|
.DELETE("/api/admin/dict/data/{id}", dictHandler::deleteDictData)
|
||||||
|
|
||||||
// ========== 公告路由 ==========
|
// ========== 公告路由(管理端) ==========
|
||||||
.GET("/api/notices", noticeHandler::getAllNotices)
|
.GET("/api/admin/notices", noticeHandler::getAllNotices)
|
||||||
.GET("/api/notices/{id}", noticeHandler::getNoticeById)
|
.GET("/api/admin/notices/{id}", noticeHandler::getNoticeById)
|
||||||
.GET("/api/notices/status/{status}", noticeHandler::getNoticesByStatus)
|
.GET("/api/admin/notices/status/{status}", noticeHandler::getNoticesByStatus)
|
||||||
.POST("/api/notices", noticeHandler::createNotice)
|
.POST("/api/admin/notices", noticeHandler::createNotice)
|
||||||
.PUT("/api/notices/{id}", noticeHandler::updateNotice)
|
.PUT("/api/admin/notices/{id}", noticeHandler::updateNotice)
|
||||||
.DELETE("/api/notices/{id}", noticeHandler::deleteNotice)
|
.DELETE("/api/admin/notices/{id}", noticeHandler::deleteNotice)
|
||||||
|
|
||||||
// ========== 消息路由 ==========
|
// ========== 消息路由(管理端) ==========
|
||||||
.GET("/api/messages/user/{userId}", messageHandler::getMessagesByUser)
|
.GET("/api/admin/messages/user/{userId}", messageHandler::getMessagesByUser)
|
||||||
.GET("/api/messages/user/{userId}/unread", messageHandler::getUnreadCount)
|
.GET("/api/admin/messages/user/{userId}/unread", messageHandler::getUnreadCount)
|
||||||
.GET("/api/messages/user/{userId}/unread/list", messageHandler::getUnreadList)
|
.GET("/api/admin/messages/user/{userId}/unread/list", messageHandler::getUnreadList)
|
||||||
.POST("/api/messages", messageHandler::createMessage)
|
.POST("/api/admin/messages", messageHandler::createMessage)
|
||||||
.PUT("/api/messages/{id}/read", messageHandler::markAsRead)
|
.PUT("/api/admin/messages/{id}/read", messageHandler::markAsRead)
|
||||||
.DELETE("/api/messages/{id}", messageHandler::deleteMessage)
|
.DELETE("/api/admin/messages/{id}", messageHandler::deleteMessage)
|
||||||
|
|
||||||
// ========== 文件路由 ==========
|
// ========== 文件路由(管理端) ==========
|
||||||
.GET("/api/files", fileHandler::getAllFiles)
|
.GET("/api/admin/files", fileHandler::getAllFiles)
|
||||||
.GET("/api/files/{id}", fileHandler::getFileById)
|
.GET("/api/admin/files/{id}", fileHandler::getFileById)
|
||||||
.POST("/api/files/upload", fileHandler::uploadFile)
|
.POST("/api/admin/files/upload", fileHandler::uploadFile)
|
||||||
.GET("/api/files/{id}/download", fileHandler::downloadFile)
|
.GET("/api/admin/files/{id}/download", fileHandler::downloadFile)
|
||||||
.GET("/api/files/download/{fileName}", fileHandler::downloadFileByName)
|
.GET("/api/admin/files/download/{fileName}", fileHandler::downloadFileByName)
|
||||||
.GET("/api/files/{id}/preview", fileHandler::previewFile)
|
.GET("/api/admin/files/{id}/preview", fileHandler::previewFile)
|
||||||
.GET("/api/files/preview/{fileName}", fileHandler::previewFileByName)
|
.GET("/api/admin/files/preview/{fileName}", fileHandler::previewFileByName)
|
||||||
.DELETE("/api/files/{id}", fileHandler::deleteFile)
|
.DELETE("/api/admin/files/{id}", fileHandler::deleteFile)
|
||||||
|
|
||||||
// ========== 权限路由 ==========
|
// ========== 权限路由(管理端) ==========
|
||||||
.GET("/api/permissions", permissionHandler::getAllPermissions)
|
.GET("/api/admin/permissions", permissionHandler::getAllPermissions)
|
||||||
.GET("/api/permissions/{id}", permissionHandler::getPermissionById)
|
.GET("/api/admin/permissions/{id}", permissionHandler::getPermissionById)
|
||||||
.GET("/api/permissions/code/{code}", permissionHandler::getPermissionByCode)
|
.GET("/api/admin/permissions/code/{code}", permissionHandler::getPermissionByCode)
|
||||||
.GET("/api/permissions/check-code", permissionHandler::checkCodeExists)
|
.GET("/api/admin/permissions/check-code", permissionHandler::checkCodeExists)
|
||||||
.GET("/api/permissions/count", permissionHandler::getPermissionCount)
|
.GET("/api/admin/permissions/count", permissionHandler::getPermissionCount)
|
||||||
.POST("/api/permissions", permissionHandler::createPermission)
|
.POST("/api/admin/permissions", permissionHandler::createPermission)
|
||||||
.PUT("/api/permissions/{id}", permissionHandler::updatePermission)
|
.PUT("/api/admin/permissions/{id}", permissionHandler::updatePermission)
|
||||||
.DELETE("/api/permissions/{id}", permissionHandler::deletePermission)
|
.DELETE("/api/admin/permissions/{id}", permissionHandler::deletePermission)
|
||||||
|
|
||||||
// ========== 会员模块路由 - 微信认证 ==========
|
// ========== 会员模块路由 - 微信认证(前台公开) ==========
|
||||||
.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)
|
||||||
.POST("/api/member/auth/mp/callback", wechatAuthHandler::mpCallback)
|
.POST("/api/member/auth/mp/callback", wechatAuthHandler::mpCallback)
|
||||||
|
|
||||||
// ========== 会员模块路由 - 会员信息 ==========
|
// ========== 会员模块路由 - 会员信息(前台) ==========
|
||||||
.GET("/api/member/info", memberHandler::getMemberInfo)
|
.GET("/api/member/info", memberHandler::getMemberInfo)
|
||||||
.PUT("/api/member/info", memberHandler::updateMemberInfo)
|
.PUT("/api/member/info", memberHandler::updateMemberInfo)
|
||||||
.POST("/api/member/phone/bind", memberHandler::bindPhone)
|
.POST("/api/member/phone/bind", memberHandler::bindPhone)
|
||||||
@@ -224,32 +226,35 @@ public class SystemRouter {
|
|||||||
|
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// ========== 会员卡管理路由 ==============
|
// ========== 会员卡管理路由(管理端) ==============
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
// ===== 会员卡类型管理 =====
|
// ===== 会员卡类型管理 =====
|
||||||
.GET("/api/member-cards/active", memberCardHandler::getActiveCards)
|
.GET("/api/admin/member-cards/active", memberCardHandler::getActiveCards)
|
||||||
.GET("/api/member-cards/{memberCardId}", memberCardHandler::getMemberCardById)
|
.GET("/api/admin/member-cards/{memberCardId}", memberCardHandler::getMemberCardById)
|
||||||
.POST("/api/member-cards", memberCardHandler::createMemberCard)
|
.POST("/api/admin/member-cards", memberCardHandler::createMemberCard)
|
||||||
|
|
||||||
// ===== 会员卡记录管理(核心业务)=====
|
// ===== 会员卡记录管理(核心业务)=====
|
||||||
.POST("/api/member-card-records/purchase", memberCardRecordHandler::purchaseCard)
|
.POST("/api/admin/member-card-records/purchase", memberCardRecordHandler::purchaseCard)
|
||||||
.POST("/api/member-card-records/{recordId}/renew", memberCardRecordHandler::renewCard)
|
.POST("/api/admin/member-card-records/{recordId}/renew", memberCardRecordHandler::renewCard)
|
||||||
.POST("/api/member-card-records/{recordId}/use", memberCardRecordHandler::useCard)
|
.POST("/api/admin/member-card-records/{recordId}/use", memberCardRecordHandler::useCard)
|
||||||
.POST("/api/member-card-records/{recordId}/refund", memberCardRecordHandler::refundCard)
|
.POST("/api/admin/member-card-records/{recordId}/refund", memberCardRecordHandler::refundCard)
|
||||||
.GET("/api/member-card-records/my-cards/{memberId}", memberCardRecordHandler::getMyCards)
|
.GET("/api/admin/member-card-records/my-cards/{memberId}", memberCardRecordHandler::getMyCards)
|
||||||
.GET("/api/member-card-records/{recordId}", memberCardRecordHandler::getMemberCardRecordById)
|
.GET("/api/admin/member-card-records/{recordId}", memberCardRecordHandler::getMemberCardRecordById)
|
||||||
.POST("/api/member-card-records/process-expired", memberCardRecordHandler::processExpiredCards)
|
.POST("/api/admin/member-card-records/process-expired", memberCardRecordHandler::processExpiredCards)
|
||||||
|
|
||||||
// ===== 会员卡交易流水管理 =====
|
// ===== 会员卡交易流水管理 =====
|
||||||
.POST("/api/member-card-transactions", memberCardTransactionHandler::insertTransaction)
|
.POST("/api/admin/member-card-transactions", memberCardTransactionHandler::insertTransaction)
|
||||||
.GET("/api/member-card-transactions", memberCardTransactionHandler::getTransactionsWithConditions)
|
.GET("/api/admin/member-card-transactions", memberCardTransactionHandler::getTransactionsWithConditions)
|
||||||
.GET("/api/member-card-transactions/member/{memberId}", memberCardTransactionHandler::getMemberTransactions)
|
.GET("/api/admin/member-card-transactions/member/{memberId}", memberCardTransactionHandler::getMemberTransactions)
|
||||||
.GET("/api/member-card-transactions/card/{cardId}", memberCardTransactionHandler::getTransactionsByCardId)
|
.GET("/api/admin/member-card-transactions/card/{cardId}", memberCardTransactionHandler::getTransactionsByCardId)
|
||||||
.GET("/api/member-card-transactions/statistics/deduct/{cardId}", memberCardTransactionHandler::getDeductCountByCardId)
|
.GET("/api/admin/member-card-transactions/statistics/deduct/{cardId}", memberCardTransactionHandler::getDeductCountByCardId)
|
||||||
.GET("/api/member-card-transactions/statistics/renew", memberCardTransactionHandler::getRenewAmountByTimeRange)
|
.GET("/api/admin/member-card-transactions/statistics/renew", memberCardTransactionHandler::getRenewAmountByTimeRange)
|
||||||
.GET("/api/member-card-transactions/statistics/purchase/{memberId}", memberCardTransactionHandler::getPurchaseAmountByMember)
|
.GET("/api/admin/member-card-transactions/statistics/purchase/{memberId}", memberCardTransactionHandler::getPurchaseAmountByMember)
|
||||||
|
|
||||||
|
// ========= 签到路由(前台会员) ==========
|
||||||
|
.POST("/api/member/checkIn", checkInHandler::checkIn)
|
||||||
|
.GET("/api/member/checkIn/qrcode", checkInHandler::getQRCode)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,10 @@
|
|||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.data</groupId>
|
||||||
|
<artifactId>spring-data-redis</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
package cn.novalon.gym.manage.common.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
|
||||||
|
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||||
|
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||||
|
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||||
|
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis 配置类(响应式版本)
|
||||||
|
*
|
||||||
|
* @author 付嘉
|
||||||
|
* @date 2026-05-29
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class RedisConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置 ReactiveRedisTemplate
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public ReactiveRedisTemplate<String, Object> reactiveRedisTemplate(
|
||||||
|
ReactiveRedisConnectionFactory connectionFactory) {
|
||||||
|
|
||||||
|
// 配置序列化上下文
|
||||||
|
RedisSerializationContext<String, Object> serializationContext =
|
||||||
|
RedisSerializationContext.<String, Object>newSerializationContext()
|
||||||
|
.key(StringRedisSerializer.UTF_8)
|
||||||
|
.value(new GenericJackson2JsonRedisSerializer())
|
||||||
|
.hashKey(StringRedisSerializer.UTF_8)
|
||||||
|
.hashValue(new GenericJackson2JsonRedisSerializer())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return new ReactiveRedisTemplate<>(connectionFactory, serializationContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
package cn.novalon.gym.manage.common.constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis 缓存 Key 常量类
|
||||||
|
* 统一管理项目中所有 Redis 缓存的 key 前缀
|
||||||
|
*
|
||||||
|
* @author auto-generated
|
||||||
|
* @date 2026-05-30
|
||||||
|
*/
|
||||||
|
public final class RedisKeyConstants {
|
||||||
|
|
||||||
|
private RedisKeyConstants() {
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 会员模块 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员信息缓存
|
||||||
|
* 格式:member:info:{memberId}
|
||||||
|
*/
|
||||||
|
public static final String MEMBER_INFO = "member:info:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员详情缓存
|
||||||
|
* 格式:member:detail:{memberId}
|
||||||
|
*/
|
||||||
|
public static final String MEMBER_DETAIL = "member:detail:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员卡类型缓存
|
||||||
|
* 格式:member:card:{memberCardId}
|
||||||
|
*/
|
||||||
|
public static final String MEMBER_CARD = "member:card:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员卡记录缓存(包含剩余次数/金额)
|
||||||
|
* 格式:member:card:record:{recordId}
|
||||||
|
*/
|
||||||
|
public static final String MEMBER_CARD_RECORD = "member:card:record:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员退款申请缓存
|
||||||
|
* 格式:member:refund:{recordId}
|
||||||
|
*/
|
||||||
|
public static final String MEMBER_REFUND = "member:refund:";
|
||||||
|
|
||||||
|
// ==================== 签到模块 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户当日二维码缓存
|
||||||
|
* 格式:qrcode:user:daily:{userId}:{date}
|
||||||
|
* 示例:qrcode:user:daily:1:2026-05-30
|
||||||
|
*/
|
||||||
|
public static final String QRCODE_USER_DAILY = "qrcode:user:daily:";
|
||||||
|
|
||||||
|
// ==================== 微信模块 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信 access_token 缓存
|
||||||
|
* 格式:wechat:access_token:{appType}
|
||||||
|
* appType: miniapp(小程序), mp(公众号)
|
||||||
|
*/
|
||||||
|
public static final String WECHAT_ACCESS_TOKEN = "wechat:access_token:";
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package cn.novalon.gym.manage.common.constants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户类型枚举
|
||||||
|
* 用于区分后台管理用户和前台会员用户
|
||||||
|
*/
|
||||||
|
public enum UserType {
|
||||||
|
ADMIN,
|
||||||
|
MEMBER;
|
||||||
|
|
||||||
|
public static UserType fromString(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
throw new IllegalArgumentException("userType 不能为空");
|
||||||
|
}
|
||||||
|
for (UserType type : values()) {
|
||||||
|
if (type.name().equalsIgnoreCase(value)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("未知的用户类型: " + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
package cn.novalon.gym.manage.common.util;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTML 转义工具类
|
||||||
|
* 防止 XSS 注入攻击
|
||||||
|
*
|
||||||
|
* @author 付嘉
|
||||||
|
* @date 2026-05-29
|
||||||
|
*/
|
||||||
|
public class HtmlEscapeUtil {
|
||||||
|
|
||||||
|
private static final Map<Character, String> ESCAPE_MAP = new HashMap<>();
|
||||||
|
private static final Map<String, Character> UNESCAPE_MAP = new HashMap<>();
|
||||||
|
private static final Pattern HTML_PATTERN = Pattern.compile("<[^>]*>");
|
||||||
|
|
||||||
|
static {
|
||||||
|
// HTML 特殊字符转义映射
|
||||||
|
ESCAPE_MAP.put('&', "&");
|
||||||
|
ESCAPE_MAP.put('<', "<");
|
||||||
|
ESCAPE_MAP.put('>', ">");
|
||||||
|
ESCAPE_MAP.put('"', """);
|
||||||
|
ESCAPE_MAP.put('\'', "'");
|
||||||
|
|
||||||
|
// 反向映射
|
||||||
|
UNESCAPE_MAP.put("&", '&');
|
||||||
|
UNESCAPE_MAP.put("<", '<');
|
||||||
|
UNESCAPE_MAP.put(">", '>');
|
||||||
|
UNESCAPE_MAP.put(""", '"');
|
||||||
|
UNESCAPE_MAP.put("'", '\'');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转义 HTML 特殊字符
|
||||||
|
*
|
||||||
|
* @param input 原始字符串
|
||||||
|
* @return 转义后的字符串
|
||||||
|
*/
|
||||||
|
public static String escape(String input) {
|
||||||
|
if (input == null || input.isEmpty()) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
for (char c : input.toCharArray()) {
|
||||||
|
String escaped = ESCAPE_MAP.get(c);
|
||||||
|
if (escaped != null) {
|
||||||
|
result.append(escaped);
|
||||||
|
} else {
|
||||||
|
result.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 反转义 HTML 特殊字符
|
||||||
|
*
|
||||||
|
* @param input 转义后的字符串
|
||||||
|
* @return 原始字符串
|
||||||
|
*/
|
||||||
|
public static String unescape(String input) {
|
||||||
|
if (input == null || input.isEmpty()) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
String result = input;
|
||||||
|
for (Map.Entry<String, Character> entry : UNESCAPE_MAP.entrySet()) {
|
||||||
|
result = result.replace(entry.getKey(), String.valueOf(entry.getValue()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除所有 HTML 标签
|
||||||
|
*
|
||||||
|
* @param input 原始字符串
|
||||||
|
* @return 移除标签后的字符串
|
||||||
|
*/
|
||||||
|
public static String stripHtmlTags(String input) {
|
||||||
|
if (input == null || input.isEmpty()) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
return HTML_PATTERN.matcher(input).replaceAll("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全转义(转义 + 移除标签)
|
||||||
|
*
|
||||||
|
* @param input 原始字符串
|
||||||
|
* @return 安全字符串
|
||||||
|
*/
|
||||||
|
public static String sanitize(String input) {
|
||||||
|
if (input == null || input.isEmpty()) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
// 先移除 HTML 标签,再转义特殊字符
|
||||||
|
String noTags = stripHtmlTags(input);
|
||||||
|
return escape(noTags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否包含 HTML 标签
|
||||||
|
*
|
||||||
|
* @param input 原始字符串
|
||||||
|
* @return true-包含, false-不包含
|
||||||
|
*/
|
||||||
|
public static boolean containsHtmlTags(String input) {
|
||||||
|
if (input == null || input.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return HTML_PATTERN.matcher(input).find();
|
||||||
|
}
|
||||||
|
}
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
package cn.novalon.gym.manage.common.util;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis 工具类(响应式版本)
|
||||||
|
*
|
||||||
|
* @author liwentao
|
||||||
|
* @date 2026/5/15
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class RedisUtil {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ReactiveRedisTemplate<String, Object> reactiveRedisTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置值
|
||||||
|
*/
|
||||||
|
public Mono<Boolean> set(String key, Object value) {
|
||||||
|
return reactiveRedisTemplate.opsForValue().set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置值并指定过期时间(秒)
|
||||||
|
*/
|
||||||
|
public Mono<Boolean> setWithExpire(String key, Object value, long timeoutSeconds) {
|
||||||
|
return reactiveRedisTemplate.opsForValue().set(key, value, Duration.ofSeconds(timeoutSeconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取值
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> Mono<T> get(String key, Class<T> clazz) {
|
||||||
|
return reactiveRedisTemplate.opsForValue().get(key)
|
||||||
|
.map(obj -> clazz.isInstance(obj) ? (T) obj : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取值(返回 Object)
|
||||||
|
*/
|
||||||
|
public Mono<Object> get(String key) {
|
||||||
|
return reactiveRedisTemplate.opsForValue().get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除key
|
||||||
|
*/
|
||||||
|
public Mono<Long> delete(String key) {
|
||||||
|
return reactiveRedisTemplate.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断key是否存在
|
||||||
|
*/
|
||||||
|
public Mono<Boolean> hasKey(String key) {
|
||||||
|
return reactiveRedisTemplate.hasKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置过期时间(秒)
|
||||||
|
*/
|
||||||
|
public Mono<Boolean> expire(String key, long timeoutSeconds) {
|
||||||
|
return reactiveRedisTemplate.expire(key, Duration.ofSeconds(timeoutSeconds));
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
-4
@@ -42,6 +42,13 @@ public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory<JwtAut
|
|||||||
return exchange.getResponse().setComplete();
|
return exchange.getResponse().setComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 路径-userType 校验:防止越权访问
|
||||||
|
String userType = jwtUtil.getUserTypeFromToken(token);
|
||||||
|
if (!isUserTypeAllowedForPath(path, userType)) {
|
||||||
|
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
|
||||||
|
return exchange.getResponse().setComplete();
|
||||||
|
}
|
||||||
|
|
||||||
String username = jwtUtil.getUsernameFromToken(token);
|
String username = jwtUtil.getUsernameFromToken(token);
|
||||||
Long userId = jwtUtil.getUserIdFromToken(token);
|
Long userId = jwtUtil.getUserIdFromToken(token);
|
||||||
|
|
||||||
@@ -49,18 +56,34 @@ public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory<JwtAut
|
|||||||
.header("X-User-Id", String.valueOf(userId))
|
.header("X-User-Id", String.valueOf(userId))
|
||||||
.header("X-Member-Id", String.valueOf(userId))
|
.header("X-Member-Id", String.valueOf(userId))
|
||||||
.header("X-Username", username)
|
.header("X-Username", username)
|
||||||
|
.header("X-User-Type", userType != null ? userType : "UNKNOWN")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
return chain.filter(exchange.mutate().request(modifiedRequest).build());
|
return chain.filter(exchange.mutate().request(modifiedRequest).build());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验路径与 userType 是否匹配
|
||||||
|
* - /api/admin/** 路径只允许 userType=ADMIN
|
||||||
|
* - /api/member/** 路径只允许 userType=MEMBER
|
||||||
|
* - 其他路径不做 userType 校验
|
||||||
|
*/
|
||||||
|
private boolean isUserTypeAllowedForPath(String path, String userType) {
|
||||||
|
if (path.startsWith("/api/admin/")) {
|
||||||
|
return "ADMIN".equals(userType);
|
||||||
|
}
|
||||||
|
if (path.startsWith("/api/member/")) {
|
||||||
|
return "MEMBER".equals(userType);
|
||||||
|
}
|
||||||
|
// 非特定前缀路径不做 userType 校验
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isPublicPath(String path) {
|
private boolean isPublicPath(String path) {
|
||||||
return path.startsWith("/api/auth/") ||
|
return path.startsWith("/api/admin/auth/") ||
|
||||||
path.equals("/actuator/health") ||
|
path.equals("/actuator/health") ||
|
||||||
path.equals("/api/member/auth/miniapp/login") ||
|
path.startsWith("/api/member/auth/") ||
|
||||||
path.equals("/api/member/auth/mp/callback") ||
|
|
||||||
path.equals("/api/auth/login") ||
|
|
||||||
path.startsWith("/actuator/info");
|
path.startsWith("/actuator/info");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -63,7 +63,8 @@ public class RbacAuthorizationFilter extends AbstractGatewayFilterFactory<RbacAu
|
|||||||
private boolean isPublicPath(String path) {
|
private boolean isPublicPath(String path) {
|
||||||
return path.startsWith("/api/auth/") ||
|
return path.startsWith("/api/auth/") ||
|
||||||
path.equals("/actuator/health") ||
|
path.equals("/actuator/health") ||
|
||||||
path.startsWith("/actuator/info");
|
path.startsWith("/actuator/info") ||
|
||||||
|
path.startsWith("/api/checkIn/");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Config {
|
public static class Config {
|
||||||
|
|||||||
+11
-1
@@ -28,6 +28,10 @@ public class JwtUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String generateToken(String username, Long userId) {
|
public String generateToken(String username, Long userId) {
|
||||||
|
return generateToken(username, userId, "ADMIN");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String generateToken(String username, Long userId, String userType) {
|
||||||
Date now = new Date();
|
Date now = new Date();
|
||||||
Date expiryDate = new Date(now.getTime() + expiration);
|
Date expiryDate = new Date(now.getTime() + expiration);
|
||||||
|
|
||||||
@@ -35,13 +39,14 @@ public class JwtUtil {
|
|||||||
String token = Jwts.builder()
|
String token = Jwts.builder()
|
||||||
.setSubject(username)
|
.setSubject(username)
|
||||||
.claim("userId", userId)
|
.claim("userId", userId)
|
||||||
|
.claim("userType", userType)
|
||||||
.claim("keyVersion", jwtKeyService.getCurrentKeyVersion())
|
.claim("keyVersion", jwtKeyService.getCurrentKeyVersion())
|
||||||
.setIssuedAt(now)
|
.setIssuedAt(now)
|
||||||
.setExpiration(expiryDate)
|
.setExpiration(expiryDate)
|
||||||
.signWith(getSigningKey())
|
.signWith(getSigningKey())
|
||||||
.compact();
|
.compact();
|
||||||
|
|
||||||
logger.debug("Generated JWT token for user: {}, userId: {}", username, userId);
|
logger.debug("Generated JWT token for user: {}, userId: {}, userType: {}", username, userId, userType);
|
||||||
return token;
|
return token;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -74,6 +79,11 @@ public class JwtUtil {
|
|||||||
return claims.get("userId", Long.class);
|
return claims.get("userId", Long.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getUserTypeFromToken(String token) {
|
||||||
|
Claims claims = parseToken(token);
|
||||||
|
return claims.get("userType", String.class);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean validateToken(String token) {
|
public boolean validateToken(String token) {
|
||||||
try {
|
try {
|
||||||
parseToken(token);
|
parseToken(token);
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ signature:
|
|||||||
max-age-minutes: ${SIGNATURE_MAX_AGE_MINUTES:5}
|
max-age-minutes: ${SIGNATURE_MAX_AGE_MINUTES:5}
|
||||||
nonce-cache-size: ${SIGNATURE_NONCE_CACHE_SIZE:10000}
|
nonce-cache-size: ${SIGNATURE_NONCE_CACHE_SIZE:10000}
|
||||||
whitelist:
|
whitelist:
|
||||||
paths: ${SIGNATURE_WHITELIST_PATHS:/actuator/health,/actuator/info,/api/auth/login,/api/auth/register,/api/member/auth/miniapp/login,/api/member/auth/mp/callback}
|
paths: ${SIGNATURE_WHITELIST_PATHS:/actuator/health,/actuator/info,/api/admin/auth/login,/api/admin/auth/register,/api/member/auth/miniapp/login,/api/member/auth/mp/callback}
|
||||||
|
|
||||||
resilience:
|
resilience:
|
||||||
enabled: ${RESILIENCE_ENABLED:true}
|
enabled: ${RESILIENCE_ENABLED:true}
|
||||||
|
|||||||
+164
-4
@@ -39,7 +39,7 @@ class GatewayJwtAuthenticationFilterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testPublicPath_AllowAccess() {
|
void testPublicPath_AllowAccess() {
|
||||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/auth/login").build();
|
MockServerHttpRequest request = MockServerHttpRequest.get("/api/admin/auth/login").build();
|
||||||
exchange = MockServerWebExchange.from(request);
|
exchange = MockServerWebExchange.from(request);
|
||||||
|
|
||||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||||
@@ -56,7 +56,7 @@ class GatewayJwtAuthenticationFilterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testPublicPath_Register() {
|
void testPublicPath_Register() {
|
||||||
MockServerHttpRequest request = MockServerHttpRequest.post("/api/auth/register").build();
|
MockServerHttpRequest request = MockServerHttpRequest.post("/api/admin/auth/register").build();
|
||||||
exchange = MockServerWebExchange.from(request);
|
exchange = MockServerWebExchange.from(request);
|
||||||
|
|
||||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||||
@@ -105,6 +105,40 @@ class GatewayJwtAuthenticationFilterTest {
|
|||||||
verify(jwtUtil, never()).validateToken(anyString());
|
verify(jwtUtil, never()).validateToken(anyString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPublicPath_MemberAuth() {
|
||||||
|
MockServerHttpRequest request = MockServerHttpRequest.post("/api/member/auth/miniapp/login").build();
|
||||||
|
exchange = MockServerWebExchange.from(request);
|
||||||
|
|
||||||
|
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||||
|
.filter(exchange, chain);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(chain).filter(any(ServerWebExchange.class));
|
||||||
|
verify(jwtUtil, never()).validateToken(anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPublicPath_AdminAuthPrefix() {
|
||||||
|
MockServerHttpRequest request = MockServerHttpRequest.post("/api/admin/auth/refresh").build();
|
||||||
|
exchange = MockServerWebExchange.from(request);
|
||||||
|
|
||||||
|
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||||
|
.filter(exchange, chain);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(chain).filter(any(ServerWebExchange.class));
|
||||||
|
verify(jwtUtil, never()).validateToken(anyString());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testProtectedPath_NoAuthHeader() {
|
void testProtectedPath_NoAuthHeader() {
|
||||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users").build();
|
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users").build();
|
||||||
@@ -152,6 +186,7 @@ class GatewayJwtAuthenticationFilterTest {
|
|||||||
when(jwtUtil.isTokenExpired(validToken)).thenReturn(false);
|
when(jwtUtil.isTokenExpired(validToken)).thenReturn(false);
|
||||||
when(jwtUtil.getUsernameFromToken(validToken)).thenReturn("testuser");
|
when(jwtUtil.getUsernameFromToken(validToken)).thenReturn("testuser");
|
||||||
when(jwtUtil.getUserIdFromToken(validToken)).thenReturn(1L);
|
when(jwtUtil.getUserIdFromToken(validToken)).thenReturn(1L);
|
||||||
|
when(jwtUtil.getUserTypeFromToken(validToken)).thenReturn("ADMIN");
|
||||||
|
|
||||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||||
.filter(exchange, chain);
|
.filter(exchange, chain);
|
||||||
@@ -163,6 +198,7 @@ class GatewayJwtAuthenticationFilterTest {
|
|||||||
verify(jwtUtil).isTokenExpired(validToken);
|
verify(jwtUtil).isTokenExpired(validToken);
|
||||||
verify(jwtUtil).getUsernameFromToken(validToken);
|
verify(jwtUtil).getUsernameFromToken(validToken);
|
||||||
verify(jwtUtil).getUserIdFromToken(validToken);
|
verify(jwtUtil).getUserIdFromToken(validToken);
|
||||||
|
verify(jwtUtil).getUserTypeFromToken(validToken);
|
||||||
verify(chain).filter(any(ServerWebExchange.class));
|
verify(chain).filter(any(ServerWebExchange.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,6 +260,7 @@ class GatewayJwtAuthenticationFilterTest {
|
|||||||
when(jwtUtil.isTokenExpired(validToken)).thenReturn(false);
|
when(jwtUtil.isTokenExpired(validToken)).thenReturn(false);
|
||||||
when(jwtUtil.getUsernameFromToken(validToken)).thenReturn("testuser");
|
when(jwtUtil.getUsernameFromToken(validToken)).thenReturn("testuser");
|
||||||
when(jwtUtil.getUserIdFromToken(validToken)).thenReturn(1L);
|
when(jwtUtil.getUserIdFromToken(validToken)).thenReturn(1L);
|
||||||
|
when(jwtUtil.getUserTypeFromToken(validToken)).thenReturn("ADMIN");
|
||||||
|
|
||||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||||
.filter(exchange, chain);
|
.filter(exchange, chain);
|
||||||
@@ -235,6 +272,7 @@ class GatewayJwtAuthenticationFilterTest {
|
|||||||
verify(jwtUtil).isTokenExpired(validToken);
|
verify(jwtUtil).isTokenExpired(validToken);
|
||||||
verify(jwtUtil).getUsernameFromToken(validToken);
|
verify(jwtUtil).getUsernameFromToken(validToken);
|
||||||
verify(jwtUtil).getUserIdFromToken(validToken);
|
verify(jwtUtil).getUserIdFromToken(validToken);
|
||||||
|
verify(jwtUtil).getUserTypeFromToken(validToken);
|
||||||
verify(chain).filter(any(ServerWebExchange.class));
|
verify(chain).filter(any(ServerWebExchange.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +289,7 @@ class GatewayJwtAuthenticationFilterTest {
|
|||||||
when(jwtUtil.isTokenExpired(validToken)).thenReturn(false);
|
when(jwtUtil.isTokenExpired(validToken)).thenReturn(false);
|
||||||
when(jwtUtil.getUsernameFromToken(validToken)).thenReturn("testuser");
|
when(jwtUtil.getUsernameFromToken(validToken)).thenReturn("testuser");
|
||||||
when(jwtUtil.getUserIdFromToken(validToken)).thenReturn(1L);
|
when(jwtUtil.getUserIdFromToken(validToken)).thenReturn(1L);
|
||||||
|
when(jwtUtil.getUserTypeFromToken(validToken)).thenReturn("ADMIN");
|
||||||
|
|
||||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||||
.filter(exchange, chain);
|
.filter(exchange, chain);
|
||||||
@@ -263,11 +302,12 @@ class GatewayJwtAuthenticationFilterTest {
|
|||||||
ServerHttpRequest modifiedRequest = exchangeCaptor.getValue().getRequest();
|
ServerHttpRequest modifiedRequest = exchangeCaptor.getValue().getRequest();
|
||||||
assert modifiedRequest.getHeaders().getFirst("X-User-Id").equals("1");
|
assert modifiedRequest.getHeaders().getFirst("X-User-Id").equals("1");
|
||||||
assert modifiedRequest.getHeaders().getFirst("X-Username").equals("testuser");
|
assert modifiedRequest.getHeaders().getFirst("X-Username").equals("testuser");
|
||||||
|
assert modifiedRequest.getHeaders().getFirst("X-User-Type").equals("ADMIN");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testMixedPath_AuthPath() {
|
void testMixedPath_AdminAuthPath() {
|
||||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/auth/logout").build();
|
MockServerHttpRequest request = MockServerHttpRequest.get("/api/admin/auth/logout").build();
|
||||||
exchange = MockServerWebExchange.from(request);
|
exchange = MockServerWebExchange.from(request);
|
||||||
|
|
||||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||||
@@ -295,6 +335,7 @@ class GatewayJwtAuthenticationFilterTest {
|
|||||||
when(jwtUtil.isTokenExpired(validToken)).thenReturn(false);
|
when(jwtUtil.isTokenExpired(validToken)).thenReturn(false);
|
||||||
when(jwtUtil.getUsernameFromToken(validToken)).thenReturn("testuser");
|
when(jwtUtil.getUsernameFromToken(validToken)).thenReturn("testuser");
|
||||||
when(jwtUtil.getUserIdFromToken(validToken)).thenReturn(1L);
|
when(jwtUtil.getUserIdFromToken(validToken)).thenReturn(1L);
|
||||||
|
when(jwtUtil.getUserTypeFromToken(validToken)).thenReturn("ADMIN");
|
||||||
|
|
||||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||||
.filter(exchange, chain);
|
.filter(exchange, chain);
|
||||||
@@ -308,4 +349,123 @@ class GatewayJwtAuthenticationFilterTest {
|
|||||||
verify(jwtUtil).getUserIdFromToken(validToken);
|
verify(jwtUtil).getUserIdFromToken(validToken);
|
||||||
verify(chain).filter(any(ServerWebExchange.class));
|
verify(chain).filter(any(ServerWebExchange.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== userType 路径校验测试 ==========
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAdminPath_WithAdminToken_ShouldPass() {
|
||||||
|
String adminToken = "admin.jwt.token";
|
||||||
|
MockServerHttpRequest request = MockServerHttpRequest.get("/api/admin/users")
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, "Bearer " + adminToken)
|
||||||
|
.build();
|
||||||
|
exchange = MockServerWebExchange.from(request);
|
||||||
|
|
||||||
|
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||||
|
when(jwtUtil.validateToken(adminToken)).thenReturn(true);
|
||||||
|
when(jwtUtil.isTokenExpired(adminToken)).thenReturn(false);
|
||||||
|
when(jwtUtil.getUsernameFromToken(adminToken)).thenReturn("admin");
|
||||||
|
when(jwtUtil.getUserIdFromToken(adminToken)).thenReturn(1L);
|
||||||
|
when(jwtUtil.getUserTypeFromToken(adminToken)).thenReturn("ADMIN");
|
||||||
|
|
||||||
|
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||||
|
.filter(exchange, chain);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(chain).filter(any(ServerWebExchange.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAdminPath_WithMemberToken_ShouldBeForbidden() {
|
||||||
|
String memberToken = "member.jwt.token";
|
||||||
|
MockServerHttpRequest request = MockServerHttpRequest.get("/api/admin/users")
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, "Bearer " + memberToken)
|
||||||
|
.build();
|
||||||
|
exchange = MockServerWebExchange.from(request);
|
||||||
|
|
||||||
|
when(jwtUtil.validateToken(memberToken)).thenReturn(true);
|
||||||
|
when(jwtUtil.isTokenExpired(memberToken)).thenReturn(false);
|
||||||
|
when(jwtUtil.getUserTypeFromToken(memberToken)).thenReturn("MEMBER");
|
||||||
|
|
||||||
|
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||||
|
.filter(exchange, chain);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
assert exchange.getResponse().getStatusCode() == HttpStatus.FORBIDDEN;
|
||||||
|
verify(chain, never()).filter(any(ServerWebExchange.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMemberPath_WithMemberToken_ShouldPass() {
|
||||||
|
String memberToken = "member.jwt.token";
|
||||||
|
MockServerHttpRequest request = MockServerHttpRequest.get("/api/member/info")
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, "Bearer " + memberToken)
|
||||||
|
.build();
|
||||||
|
exchange = MockServerWebExchange.from(request);
|
||||||
|
|
||||||
|
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||||
|
when(jwtUtil.validateToken(memberToken)).thenReturn(true);
|
||||||
|
when(jwtUtil.isTokenExpired(memberToken)).thenReturn(false);
|
||||||
|
when(jwtUtil.getUsernameFromToken(memberToken)).thenReturn("123");
|
||||||
|
when(jwtUtil.getUserIdFromToken(memberToken)).thenReturn(123L);
|
||||||
|
when(jwtUtil.getUserTypeFromToken(memberToken)).thenReturn("MEMBER");
|
||||||
|
|
||||||
|
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||||
|
.filter(exchange, chain);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(chain).filter(any(ServerWebExchange.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMemberPath_WithAdminToken_ShouldBeForbidden() {
|
||||||
|
String adminToken = "admin.jwt.token";
|
||||||
|
MockServerHttpRequest request = MockServerHttpRequest.get("/api/member/info")
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, "Bearer " + adminToken)
|
||||||
|
.build();
|
||||||
|
exchange = MockServerWebExchange.from(request);
|
||||||
|
|
||||||
|
when(jwtUtil.validateToken(adminToken)).thenReturn(true);
|
||||||
|
when(jwtUtil.isTokenExpired(adminToken)).thenReturn(false);
|
||||||
|
when(jwtUtil.getUserTypeFromToken(adminToken)).thenReturn("ADMIN");
|
||||||
|
|
||||||
|
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||||
|
.filter(exchange, chain);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
assert exchange.getResponse().getStatusCode() == HttpStatus.FORBIDDEN;
|
||||||
|
verify(chain, never()).filter(any(ServerWebExchange.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNonPrefixedPath_NoUserTypeCheck() {
|
||||||
|
String adminToken = "admin.jwt.token";
|
||||||
|
// /api/users 不以 /api/admin/ 或 /api/member/ 开头,不做 userType 校验
|
||||||
|
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, "Bearer " + adminToken)
|
||||||
|
.build();
|
||||||
|
exchange = MockServerWebExchange.from(request);
|
||||||
|
|
||||||
|
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||||
|
when(jwtUtil.validateToken(adminToken)).thenReturn(true);
|
||||||
|
when(jwtUtil.isTokenExpired(adminToken)).thenReturn(false);
|
||||||
|
when(jwtUtil.getUsernameFromToken(adminToken)).thenReturn("admin");
|
||||||
|
when(jwtUtil.getUserIdFromToken(adminToken)).thenReturn(1L);
|
||||||
|
when(jwtUtil.getUserTypeFromToken(adminToken)).thenReturn("ADMIN");
|
||||||
|
|
||||||
|
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||||
|
.filter(exchange, chain);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(chain).filter(any(ServerWebExchange.class));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -47,7 +47,8 @@ public class SecurityConfig {
|
|||||||
.addFilterBefore(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
|
.addFilterBefore(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
|
||||||
.addFilterAfter(operationLogWebFilter, SecurityWebFiltersOrder.AUTHORIZATION)
|
.addFilterAfter(operationLogWebFilter, SecurityWebFiltersOrder.AUTHORIZATION)
|
||||||
.authorizeExchange(spec -> {
|
.authorizeExchange(spec -> {
|
||||||
spec.pathMatchers("/api/auth/**").permitAll()
|
spec.pathMatchers("/api/admin/auth/**").permitAll()
|
||||||
|
.pathMatchers("/api/member/auth/**").permitAll()
|
||||||
.pathMatchers("/api/public/**").permitAll()
|
.pathMatchers("/api/public/**").permitAll()
|
||||||
.pathMatchers("/ws/**").permitAll()
|
.pathMatchers("/ws/**").permitAll()
|
||||||
.pathMatchers("/actuator/**").permitAll();
|
.pathMatchers("/actuator/**").permitAll();
|
||||||
@@ -59,7 +60,7 @@ public class SecurityConfig {
|
|||||||
.pathMatchers("/v3/api-docs/**").permitAll()
|
.pathMatchers("/v3/api-docs/**").permitAll()
|
||||||
.pathMatchers("/swagger-resources/**").permitAll()
|
.pathMatchers("/swagger-resources/**").permitAll()
|
||||||
.pathMatchers("/webjars/**").permitAll()
|
.pathMatchers("/webjars/**").permitAll()
|
||||||
.pathMatchers("/api/diagnostic/**").permitAll();
|
.pathMatchers("/api/admin/diagnostic/**").permitAll();
|
||||||
logger.info("SecurityConfig: Swagger路径和诊断端点已放行");
|
logger.info("SecurityConfig: Swagger路径和诊断端点已放行");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+19
@@ -20,6 +20,9 @@ public class AuthResponse {
|
|||||||
@Schema(description = "用户名", example = "admin")
|
@Schema(description = "用户名", example = "admin")
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
|
@Schema(description = "用户类型", example = "ADMIN")
|
||||||
|
private String userType;
|
||||||
|
|
||||||
public AuthResponse() {
|
public AuthResponse() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +30,14 @@ public class AuthResponse {
|
|||||||
this.token = token;
|
this.token = token;
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
this.username = username;
|
this.username = username;
|
||||||
|
this.userType = "ADMIN";
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthResponse(String token, Long userId, String username, String userType) {
|
||||||
|
this.token = token;
|
||||||
|
this.userId = userId;
|
||||||
|
this.username = username;
|
||||||
|
this.userType = userType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getToken() {
|
public String getToken() {
|
||||||
@@ -52,4 +63,12 @@ public class AuthResponse {
|
|||||||
public void setUsername(String username) {
|
public void setUsername(String username) {
|
||||||
this.username = username;
|
this.username = username;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getUserType() {
|
||||||
|
return userType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserType(String userType) {
|
||||||
|
this.userType = userType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-3
@@ -133,8 +133,9 @@ public class SysAuthHandler {
|
|||||||
.generateToken(
|
.generateToken(
|
||||||
user.getUsername(),
|
user.getUsername(),
|
||||||
user.getId(),
|
user.getId(),
|
||||||
roleKeys);
|
roleKeys,
|
||||||
logger.info("用户登录成功: username={}, userId={}, roles={}",
|
"ADMIN");
|
||||||
|
logger.info("用户登录成功: username={}, userId={}, roles={}, userType=ADMIN",
|
||||||
user.getUsername(),
|
user.getUsername(),
|
||||||
user.getId(),
|
user.getId(),
|
||||||
roleKeys);
|
roleKeys);
|
||||||
@@ -146,7 +147,8 @@ public class SysAuthHandler {
|
|||||||
AuthResponse response = new AuthResponse(
|
AuthResponse response = new AuthResponse(
|
||||||
token,
|
token,
|
||||||
user.getId(),
|
user.getId(),
|
||||||
user.getUsername());
|
user.getUsername(),
|
||||||
|
"ADMIN");
|
||||||
return ServerResponse.ok()
|
return ServerResponse.ok()
|
||||||
.bodyValue(response);
|
.bodyValue(response);
|
||||||
});
|
});
|
||||||
|
|||||||
+4
@@ -37,6 +37,7 @@ public class JwtAuthenticationFilter implements WebFilter {
|
|||||||
String username = jwtTokenProvider.getUsernameFromToken(token);
|
String username = jwtTokenProvider.getUsernameFromToken(token);
|
||||||
jwtTokenProvider.getUserIdFromToken(token);
|
jwtTokenProvider.getUserIdFromToken(token);
|
||||||
List<String> roles = jwtTokenProvider.getRolesFromToken(token);
|
List<String> roles = jwtTokenProvider.getRolesFromToken(token);
|
||||||
|
String userType = jwtTokenProvider.getUserTypeFromToken(token);
|
||||||
|
|
||||||
List<SimpleGrantedAuthority> authorities = roles.stream()
|
List<SimpleGrantedAuthority> authorities = roles.stream()
|
||||||
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
|
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
|
||||||
@@ -53,6 +54,9 @@ public class JwtAuthenticationFilter implements WebFilter {
|
|||||||
authorities
|
authorities
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 将 userType 存入 authentication details,供后续 AuthUtil 使用
|
||||||
|
authentication.setDetails(userType);
|
||||||
|
|
||||||
return chain.filter(exchange)
|
return chain.filter(exchange)
|
||||||
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication));
|
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication));
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-11
@@ -32,24 +32,19 @@ public class JwtTokenProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String generateToken(String username, Long userId) {
|
public String generateToken(String username, Long userId) {
|
||||||
Map<String, Object> claims = new HashMap<>();
|
return generateToken(username, userId, java.util.Collections.emptyList(), "ADMIN");
|
||||||
claims.put("userId", userId);
|
|
||||||
claims.put("username", username);
|
|
||||||
|
|
||||||
return Jwts.builder()
|
|
||||||
.setClaims(claims)
|
|
||||||
.setSubject(username)
|
|
||||||
.setIssuedAt(new Date())
|
|
||||||
.setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration()))
|
|
||||||
.signWith(getSigningKey())
|
|
||||||
.compact();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String generateToken(String username, Long userId, java.util.List<String> roles) {
|
public String generateToken(String username, Long userId, java.util.List<String> roles) {
|
||||||
|
return generateToken(username, userId, roles, "ADMIN");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String generateToken(String username, Long userId, java.util.List<String> roles, String userType) {
|
||||||
Map<String, Object> claims = new HashMap<>();
|
Map<String, Object> claims = new HashMap<>();
|
||||||
claims.put("userId", userId);
|
claims.put("userId", userId);
|
||||||
claims.put("username", username);
|
claims.put("username", username);
|
||||||
claims.put("roles", roles);
|
claims.put("roles", roles);
|
||||||
|
claims.put("userType", userType);
|
||||||
|
|
||||||
return Jwts.builder()
|
return Jwts.builder()
|
||||||
.setClaims(claims)
|
.setClaims(claims)
|
||||||
@@ -85,6 +80,10 @@ public class JwtTokenProvider {
|
|||||||
return java.util.Collections.emptyList();
|
return java.util.Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getUserTypeFromToken(String token) {
|
||||||
|
return getClaimsFromToken(token).get("userType", String.class);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean validateToken(String token) {
|
public boolean validateToken(String token) {
|
||||||
try {
|
try {
|
||||||
getClaimsFromToken(token);
|
getClaimsFromToken(token);
|
||||||
|
|||||||
@@ -29,4 +29,38 @@ public class AuthUtil {
|
|||||||
if (jwtTokenProvider.getUserIdFromToken(token) <= 0L) throw new IllegalArgumentException("ID无效");
|
if (jwtTokenProvider.getUserIdFromToken(token) <= 0L) throw new IllegalArgumentException("ID无效");
|
||||||
return jwtTokenProvider.getUserIdFromToken(token);
|
return jwtTokenProvider.getUserIdFromToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前 ADMIN 用户 ID,校验 userType 必须为 ADMIN
|
||||||
|
*/
|
||||||
|
public Long getAdminUserIdOrThrow(ServerRequest request) {
|
||||||
|
String token = extractToken(request);
|
||||||
|
if (token == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "缺少 Token");
|
||||||
|
if (!jwtTokenProvider.validateToken(token)) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Token 无效或已过期");
|
||||||
|
String userType = jwtTokenProvider.getUserTypeFromToken(token);
|
||||||
|
if (!"ADMIN".equals(userType)) {
|
||||||
|
log.warn("非管理员用户尝试访问管理端接口, userType={}", userType);
|
||||||
|
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "无权访问管理端接口");
|
||||||
|
}
|
||||||
|
Long userId = jwtTokenProvider.getUserIdFromToken(token);
|
||||||
|
if (userId <= 0L) throw new IllegalArgumentException("ID无效");
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前 MEMBER 用户 ID,校验 userType 必须为 MEMBER
|
||||||
|
*/
|
||||||
|
public Long getMemberUserIdOrThrow(ServerRequest request) {
|
||||||
|
String token = extractToken(request);
|
||||||
|
if (token == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "缺少 Token");
|
||||||
|
if (!jwtTokenProvider.validateToken(token)) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Token 无效或已过期");
|
||||||
|
String userType = jwtTokenProvider.getUserTypeFromToken(token);
|
||||||
|
if (!"MEMBER".equals(userType)) {
|
||||||
|
log.warn("非会员用户尝试访问会员接口, userType={}", userType);
|
||||||
|
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "无权访问会员接口");
|
||||||
|
}
|
||||||
|
Long userId = jwtTokenProvider.getUserIdFromToken(token);
|
||||||
|
if (userId <= 0L) throw new IllegalArgumentException("ID无效");
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+2
-2
@@ -80,7 +80,7 @@ class SysAuthHandlerTest {
|
|||||||
// 配置密码编码器Mock来验证密码
|
// 配置密码编码器Mock来验证密码
|
||||||
when(passwordEncoder.matches(rawPassword, realEncodedPassword)).thenReturn(true);
|
when(passwordEncoder.matches(rawPassword, realEncodedPassword)).thenReturn(true);
|
||||||
|
|
||||||
when(jwtTokenProvider.generateToken(eq("testuser"), eq(1L), anyList())).thenReturn("test_token");
|
when(jwtTokenProvider.generateToken(eq("testuser"), eq(1L), anyList(), eq("ADMIN"))).thenReturn("test_token");
|
||||||
|
|
||||||
// 使用测试数据工厂创建角色
|
// 使用测试数据工厂创建角色
|
||||||
SysRole mockRole = TestDataFactory.createUserRole();
|
SysRole mockRole = TestDataFactory.createUserRole();
|
||||||
@@ -103,7 +103,7 @@ class SysAuthHandlerTest {
|
|||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
|
|
||||||
verify(userService).findByUsername("testuser");
|
verify(userService).findByUsername("testuser");
|
||||||
verify(jwtTokenProvider).generateToken(eq("testuser"), eq(1L), anyList());
|
verify(jwtTokenProvider).generateToken(eq("testuser"), eq(1L), anyList(), eq("ADMIN"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+48
@@ -108,4 +108,52 @@ class JwtTokenProviderTest {
|
|||||||
|
|
||||||
assertThat(isValid).isFalse();
|
assertThat(isValid).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGenerateTokenWithUserType() {
|
||||||
|
when(jwtProperties.getSecret()).thenReturn("test-secret-key-for-testing-purposes-only-1234567890");
|
||||||
|
when(jwtProperties.getExpiration()).thenReturn(3600000L);
|
||||||
|
|
||||||
|
String token = jwtTokenProvider.generateToken("testuser", 1L, java.util.List.of("admin"), "ADMIN");
|
||||||
|
|
||||||
|
assertThat(token).isNotNull();
|
||||||
|
assertThat(token).isNotEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetUserTypeFromToken() {
|
||||||
|
when(jwtProperties.getSecret()).thenReturn("test-secret-key-for-testing-purposes-only-1234567890");
|
||||||
|
when(jwtProperties.getExpiration()).thenReturn(3600000L);
|
||||||
|
|
||||||
|
String token = jwtTokenProvider.generateToken("testuser", 1L, java.util.List.of("admin"), "ADMIN");
|
||||||
|
|
||||||
|
String userType = jwtTokenProvider.getUserTypeFromToken(token);
|
||||||
|
|
||||||
|
assertThat(userType).isEqualTo("ADMIN");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetUserTypeFromToken_Member() {
|
||||||
|
when(jwtProperties.getSecret()).thenReturn("test-secret-key-for-testing-purposes-only-1234567890");
|
||||||
|
when(jwtProperties.getExpiration()).thenReturn(3600000L);
|
||||||
|
|
||||||
|
String token = jwtTokenProvider.generateToken("123", 123L, java.util.List.of(), "MEMBER");
|
||||||
|
|
||||||
|
String userType = jwtTokenProvider.getUserTypeFromToken(token);
|
||||||
|
|
||||||
|
assertThat(userType).isEqualTo("MEMBER");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetUserTypeFromToken_DefaultIsAdmin() {
|
||||||
|
when(jwtProperties.getSecret()).thenReturn("test-secret-key-for-testing-purposes-only-1234567890");
|
||||||
|
when(jwtProperties.getExpiration()).thenReturn(3600000L);
|
||||||
|
|
||||||
|
// 使用旧的两参数方法生成的 token 默认 userType 为 ADMIN
|
||||||
|
String token = jwtTokenProvider.generateToken("testuser", 1L);
|
||||||
|
|
||||||
|
String userType = jwtTokenProvider.getUserTypeFromToken(token);
|
||||||
|
|
||||||
|
assertThat(userType).isEqualTo("ADMIN");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -43,6 +43,7 @@
|
|||||||
<module>manage-notify</module>
|
<module>manage-notify</module>
|
||||||
<module>manage-file</module>
|
<module>manage-file</module>
|
||||||
<module>gym-member</module>
|
<module>gym-member</module>
|
||||||
|
<module>gym-checkIn</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
|
|||||||
Reference in New Issue
Block a user