修复协议错误

This commit is contained in:
2026-06-29 20:34:25 +08:00
parent 44da3cab6e
commit 836f0e1cbf
12 changed files with 2094 additions and 93 deletions
@@ -53,6 +53,10 @@ public class GroupCourseBooking extends BaseDomain {
@Schema(description = "上课地点", example = "健身房A区") @Schema(description = "上课地点", example = "健身房A区")
private String location; private String location;
//封面图URL(非DB字段,由 Service 从 GroupCourse 填充)
@Schema(description = "封面图URL", example = "https://example.com/cover.jpg")
private String coverImage;
public Long getCourseId() { public Long getCourseId() {
return courseId; return courseId;
} }
@@ -132,4 +136,12 @@ public class GroupCourseBooking extends BaseDomain {
public void setLocation(String location) { public void setLocation(String location) {
this.location = location; this.location = location;
} }
public String getCoverImage() {
return coverImage;
}
public void setCoverImage(String coverImage) {
this.coverImage = coverImage;
}
} }
@@ -7,6 +7,7 @@ import cn.novalon.gym.manage.groupcourse.handler.BookingSagaHandler;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseBookingRepository; import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseBookingRepository;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository; import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseBookingService; import cn.novalon.gym.manage.groupcourse.service.IGroupCourseBookingService;
import cn.novalon.gym.manage.groupcourse.util.OSSUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.r2dbc.core.DatabaseClient; import org.springframework.r2dbc.core.DatabaseClient;
@@ -335,6 +336,18 @@ public class GroupCourseBookingService implements IGroupCourseBookingService {
public Flux<GroupCourseBooking> getBookingsByMemberId(Long memberId) { public Flux<GroupCourseBooking> getBookingsByMemberId(Long memberId) {
logger.debug("查询会员预约记录:memberId={}", memberId); logger.debug("查询会员预约记录:memberId={}", memberId);
return bookingRepository.findByMemberId(memberId) return bookingRepository.findByMemberId(memberId)
.flatMap(booking -> {
// 从关联的 GroupCourse 获取封面图并转为预签名URL
if (booking.getCourseId() != null) {
return courseRepository.findByIdAndDeletedAtIsNull(booking.getCourseId())
.map(course -> {
booking.setCoverImage(OSSUtil.toCoverPresignedUrl(course.getCoverImage()));
return booking;
})
.defaultIfEmpty(booking);
}
return Mono.just(booking);
})
.doOnComplete(() -> logger.debug("查询完成:memberId={}", memberId)); .doOnComplete(() -> logger.debug("查询完成:memberId={}", memberId));
} }
@@ -5,6 +5,7 @@ import cn.novalon.gym.manage.groupcourse.domain.GroupCourseRecommend;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRecommendRepository; import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRecommendRepository;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository; import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseRecommendService; import cn.novalon.gym.manage.groupcourse.service.IGroupCourseRecommendService;
import cn.novalon.gym.manage.groupcourse.util.OSSUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -134,6 +135,8 @@ public class GroupCourseRecommendService implements IGroupCourseRecommendService
return groupCourseRepository.findByIdAndDeletedAtIsNull(recommend.getCourseId()) return groupCourseRepository.findByIdAndDeletedAtIsNull(recommend.getCourseId())
.map(course -> { .map(course -> {
// 将 OSS Key 转换为预签名URL,前端可直接加载
course.setCoverImage(OSSUtil.toCoverPresignedUrl(course.getCoverImage()));
recommend.setGroupCourse(course); recommend.setGroupCourse(course);
return recommend; return recommend;
}) })
@@ -19,6 +19,7 @@ import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseTypeRepository; import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseTypeRepository;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseService; import cn.novalon.gym.manage.groupcourse.service.IGroupCourseService;
import cn.novalon.gym.manage.groupcourse.util.QRCodeUtil; import cn.novalon.gym.manage.groupcourse.util.QRCodeUtil;
import cn.novalon.gym.manage.groupcourse.util.OSSUtil;
import cn.novalon.gym.manage.member.entity.MemberCard; import cn.novalon.gym.manage.member.entity.MemberCard;
import cn.novalon.gym.manage.member.entity.MemberCardRecord; import cn.novalon.gym.manage.member.entity.MemberCardRecord;
import cn.novalon.gym.manage.member.enums.MemberCardType; import cn.novalon.gym.manage.member.enums.MemberCardType;
@@ -32,6 +33,7 @@ import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.HashMap; import java.util.HashMap;
@@ -153,7 +155,7 @@ public class GroupCourseService implements IGroupCourseService {
detail.setCurrentMembers(course.getCurrentMembers()); detail.setCurrentMembers(course.getCurrentMembers());
detail.setStatus(course.getStatus()); detail.setStatus(course.getStatus());
detail.setLocation(course.getLocation()); detail.setLocation(course.getLocation());
detail.setCoverImage(course.getCoverImage()); detail.setCoverImage(OSSUtil.toCoverPresignedUrl(course.getCoverImage()));
detail.setDescription(course.getDescription()); detail.setDescription(course.getDescription());
detail.setStoredValueAmount(course.getStoredValueAmount()); detail.setStoredValueAmount(course.getStoredValueAmount());
detail.setQrCodePath(course.getQrCodePath()); detail.setQrCodePath(course.getQrCodePath());
@@ -178,7 +180,7 @@ public class GroupCourseService implements IGroupCourseService {
try { try {
GroupCourse groupCourse = objectMapper.readValue(cachedJson, GroupCourse.class); GroupCourse groupCourse = objectMapper.readValue(cachedJson, GroupCourse.class);
logger.info("缓存命中 - findById: id={}", id); logger.info("缓存命中 - findById: id={}", id);
return Mono.just(groupCourse); return Mono.just(fillCoverPresignedUrl(groupCourse));
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
logger.warn("缓存解析失败,删除缓存 - id: {}, error: {}", id, e.getMessage()); logger.warn("缓存解析失败,删除缓存 - id: {}, error: {}", id, e.getMessage());
return redisUtil.delete(cacheKey).then(Mono.empty()); return redisUtil.delete(cacheKey).then(Mono.empty());
@@ -188,6 +190,7 @@ public class GroupCourseService implements IGroupCourseService {
}) })
.switchIfEmpty( .switchIfEmpty(
groupCourseRepository.findByIdAndDeletedAtIsNull(id) groupCourseRepository.findByIdAndDeletedAtIsNull(id)
.map(this::fillCoverPresignedUrl)
.flatMap(groupCourse -> { .flatMap(groupCourse -> {
try { try {
String jsonData = objectMapper.writeValueAsString(groupCourse); String jsonData = objectMapper.writeValueAsString(groupCourse);
@@ -205,16 +208,19 @@ public class GroupCourseService implements IGroupCourseService {
@Override @Override
public Flux<GroupCourse> findAll() { public Flux<GroupCourse> findAll() {
return groupCourseRepository.findAll(); return groupCourseRepository.findAll()
.map(this::fillCoverPresignedUrl);
} }
@Override @Override
public Flux<GroupCourse> findAll(boolean includeDeleted) { public Flux<GroupCourse> findAll(boolean includeDeleted) {
Flux<GroupCourse> flux;
if(includeDeleted){ if(includeDeleted){
return groupCourseRepository.findAll(); flux = groupCourseRepository.findAll();
}else{ }else{
return groupCourseRepository.findByDeletedAtIsNull(); flux = groupCourseRepository.findByDeletedAtIsNull();
} }
return flux.map(this::fillCoverPresignedUrl);
} }
@Override @Override
@@ -234,6 +240,7 @@ public class GroupCourseService implements IGroupCourseService {
PageResponse<GroupCourse> pageResponse = objectMapper.readValue(cachedJson, PageResponse<GroupCourse> pageResponse = objectMapper.readValue(cachedJson,
objectMapper.getTypeFactory().constructParametricType(PageResponse.class, GroupCourse.class)); objectMapper.getTypeFactory().constructParametricType(PageResponse.class, GroupCourse.class));
logger.info("缓存命中 - findByPage: key={}", cacheKey); logger.info("缓存命中 - findByPage: key={}", cacheKey);
fillCoverPresignedUrl(pageResponse);
return Mono.just(pageResponse); return Mono.just(pageResponse);
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
logger.warn("缓存解析失败,删除缓存 - key: {}, error: {}", cacheKey, e.getMessage()); logger.warn("缓存解析失败,删除缓存 - key: {}, error: {}", cacheKey, e.getMessage());
@@ -254,6 +261,7 @@ public class GroupCourseService implements IGroupCourseService {
return resultMono.flatMap(pageResponse -> { return resultMono.flatMap(pageResponse -> {
try { try {
fillCoverPresignedUrl(pageResponse);
String jsonData = objectMapper.writeValueAsString(pageResponse); String jsonData = objectMapper.writeValueAsString(pageResponse);
return redisUtil.setWithExpire(cacheKey, jsonData, CACHE_EXPIRE_SECONDS) return redisUtil.setWithExpire(cacheKey, jsonData, CACHE_EXPIRE_SECONDS)
.thenReturn(pageResponse) .thenReturn(pageResponse)
@@ -572,8 +580,11 @@ public class GroupCourseService implements IGroupCourseService {
query.getTimePeriod(), query.getPriceSort(), query.getRemainingMost()); query.getTimePeriod(), query.getPriceSort(), query.getRemainingMost());
return groupCourseRepository.searchGroupCourses(query) return groupCourseRepository.searchGroupCourses(query)
.doOnSuccess(result -> logger.info("多条件查询结果 - total={}, page={}, size={}", .doOnSuccess(result -> {
result.getTotalElements(), result.getCurrentPage(), result.getPageSize())) fillCoverPresignedUrl(result);
logger.info("多条件查询结果 - total={}, page={}, size={}",
result.getTotalElements(), result.getCurrentPage(), result.getPageSize());
})
.doOnError(error -> logger.error("多条件查询失败 - error: {}", error.getMessage())); .doOnError(error -> logger.error("多条件查询失败 - error: {}", error.getMessage()));
} }
@@ -582,4 +593,23 @@ public class GroupCourseService implements IGroupCourseService {
.then(redisUtil.deleteByPattern(CACHE_KEY_ID_PREFIX + "*")) .then(redisUtil.deleteByPattern(CACHE_KEY_ID_PREFIX + "*"))
.then(redisUtil.deleteByPattern(CACHE_KEY_DETAIL_PREFIX + "*")).then(); .then(redisUtil.deleteByPattern(CACHE_KEY_DETAIL_PREFIX + "*")).then();
} }
/**
* 将单个 GroupCourse 的 coverImage 从 OSS Key 转换为预签名URL
*/
private GroupCourse fillCoverPresignedUrl(GroupCourse course) {
if (course != null) {
course.setCoverImage(OSSUtil.toCoverPresignedUrl(course.getCoverImage()));
}
return course;
}
/**
* 将分页结果中所有 GroupCourse 的 coverImage 从 OSS Key 转换为预签名URL
*/
private void fillCoverPresignedUrl(PageResponse<GroupCourse> pageResponse) {
if (pageResponse != null && pageResponse.getContent() != null) {
pageResponse.getContent().forEach(this::fillCoverPresignedUrl);
}
}
} }
@@ -23,7 +23,7 @@ public class OSSUtil {
private static final Logger logger = LoggerFactory.getLogger(OSSUtil.class); private static final Logger logger = LoggerFactory.getLogger(OSSUtil.class);
// OSS配置信息 // OSS配置信息
private static final String ENDPOINT = "oss-cn-beijing.aliyuncs.com"; private static final String ENDPOINT = "https://oss-cn-beijing.aliyuncs.com";
private static final String ACCESS_KEY_ID = "LTAI5t9wHCiH68Xjxg64Xx4Y"; private static final String ACCESS_KEY_ID = "LTAI5t9wHCiH68Xjxg64Xx4Y";
private static final String ACCESS_KEY_SECRET = "isAfz1IFGAnV13LOIrVg19aPhY8aRq"; private static final String ACCESS_KEY_SECRET = "isAfz1IFGAnV13LOIrVg19aPhY8aRq";
private static final String BUCKET_NAME = "ycc-filesaver"; private static final String BUCKET_NAME = "ycc-filesaver";
@@ -37,6 +37,8 @@ public class OSSUtil {
// 预签名URL有效期(秒) // 预签名URL有效期(秒)
private static final long PRESIGN_EXPIRE_SECONDS = 300; private static final long PRESIGN_EXPIRE_SECONDS = 300;
// 封面图预签名URL有效期(秒)- 1小时,确保前端列表页足够展示
private static final long COVER_PRESIGN_EXPIRE_SECONDS = 3600;
/** /**
* 上传文件到阿里云OSS(文件默认继承Bucket权限,不设置ACL) * 上传文件到阿里云OSS(文件默认继承Bucket权限,不设置ACL)
@@ -171,4 +173,21 @@ public class OSSUtil {
public static String getPublicUrl(String ossKey) { public static String getPublicUrl(String ossKey) {
return OSS_URL_PREFIX + ossKey; return OSS_URL_PREFIX + ossKey;
} }
/**
* 将OSS Key(相对路径)转换为封面图预签名URL(1小时有效期)
* 如果已经是HTTP(S)完整URL则直接返回
*
* @param ossKey OSS对象Key或完整URL
* @return 可访问的预签名URLossKey为空时返回null
*/
public static String toCoverPresignedUrl(String ossKey) {
if (ossKey == null || ossKey.isBlank()) {
return null;
}
if (ossKey.startsWith("http://") || ossKey.startsWith("https://")) {
return ossKey;
}
return generatePresignedUrl(ossKey, COVER_PRESIGN_EXPIRE_SECONDS);
}
} }
+1995 -72
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -65,7 +65,7 @@ async function handleDelete(row: any) {
inputType: 'password', inputType: 'password',
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
}).catch(() => {}) }).catch(() => ({ value: '' }))
if (!pwd) { if (!pwd) {
ElMessage.warning('请输入验证密码') ElMessage.warning('请输入验证密码')
return return
@@ -94,7 +94,7 @@ async function handleSave() {
inputType: 'password', inputType: 'password',
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
}).catch(() => {}) }).catch(() => ({ value: '' }))
if (!pwd) { if (!pwd) {
ElMessage.warning('请输入验证密码') ElMessage.warning('请输入验证密码')
return return
@@ -132,7 +132,7 @@ async function handleAssignRoles() {
inputType: 'password', inputType: 'password',
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
}).catch(() => {}) }).catch(() => ({ value: '' }))
if (!pwd) { if (!pwd) {
ElMessage.warning('请输入验证密码') ElMessage.warning('请输入验证密码')
return return
@@ -89,8 +89,8 @@ async function fetchData() {
newMemberData[i] = res?.newMembers ?? 0 newMemberData[i] = res?.newMembers ?? 0
activeMemberData[i] = res?.activeMembers ?? 0 activeMemberData[i] = res?.activeMembers ?? 0
if (i === todayIdx) { if (i === todayIdx) {
statCards.value[0].value = res?.totalMembers ?? 0 statCards.value[0]!.value = res?.totalMembers ?? 0
statCards.value[1].value = res?.newMembers ?? 0 statCards.value[1]!.value = res?.newMembers ?? 0
} }
}).catch(() => {}), }).catch(() => {}),
@@ -98,14 +98,14 @@ async function fetchData() {
bookingData[i] = res?.newBookings ?? 0 bookingData[i] = res?.newBookings ?? 0
cancelData[i] = res?.cancelBookings ?? 0 cancelData[i] = res?.cancelBookings ?? 0
if (i === todayIdx) { if (i === todayIdx) {
statCards.value[3].value = res?.newBookings ?? 0 statCards.value[3]!.value = res?.newBookings ?? 0
} }
}).catch(() => {}), }).catch(() => {}),
getSignInStatistics(range).then((res: any) => { getSignInStatistics(range).then((res: any) => {
signinData[i] = res?.totalSignIns ?? 0 signinData[i] = res?.totalSignIns ?? 0
if (i === todayIdx) { if (i === todayIdx) {
statCards.value[2].value = res?.totalSignIns ?? 0 statCards.value[2]!.value = res?.totalSignIns ?? 0
} }
}).catch(() => {}), }).catch(() => {}),
) )
@@ -116,7 +116,7 @@ async function fetchData() {
getRevenueStatistics().then((res: any) => { getRevenueStatistics().then((res: any) => {
const data = res?.data const data = res?.data
if (data) { if (data) {
statCards.value[4].value = (data.todayIncome ?? 0).toFixed(2) statCards.value[4]!.value = (data.todayIncome ?? 0).toFixed(2)
} }
}).catch(() => {}), }).catch(() => {}),
) )
@@ -123,7 +123,7 @@ async function fetchData() {
const cache: Record<number, LabelItem[]> = {} const cache: Record<number, LabelItem[]> = {}
labelResults.forEach((result, i) => { labelResults.forEach((result, i) => {
if (result.status === 'fulfilled' && Array.isArray(result.value)) { if (result.status === 'fulfilled' && Array.isArray(result.value)) {
cache[arr[i].id] = result.value as LabelItem[] cache[arr[i]!.id] = result.value as LabelItem[]
} }
}) })
labelCache.value = cache labelCache.value = cache
+1 -1
View File
@@ -147,7 +147,7 @@ function handleSearch() {
function resetFilters() { function resetFilters() {
keyword.value = '' keyword.value = ''
genderFilter.value = null genderFilter.value = null
sortBy.value = sortOptions[0] sortBy.value = sortOptions[0]!
currentPage.value = 1 currentPage.value = 1
} }
+3 -3
View File
@@ -107,7 +107,7 @@ async function handleDelete(row: any) {
inputType: 'password', inputType: 'password',
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
}).catch(() => {}) }).catch(() => ({ value: '' }))
if (!pwd) { if (!pwd) {
ElMessage.warning('请输入验证密码') ElMessage.warning('请输入验证密码')
return return
@@ -151,7 +151,7 @@ async function handleSave() {
inputType: 'password', inputType: 'password',
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
}).catch(() => {}) }).catch(() => ({ value: '' }))
if (!pwd) { if (!pwd) {
ElMessage.warning('请输入验证密码') ElMessage.warning('请输入验证密码')
return return
@@ -189,7 +189,7 @@ async function handleAssignPerms() {
inputType: 'password', inputType: 'password',
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
}).catch(() => {}) }).catch(() => ({ value: '' }))
if (!pwd) { if (!pwd) {
ElMessage.warning('请输入验证密码') ElMessage.warning('请输入验证密码')
return return
+1
View File
@@ -6,6 +6,7 @@ import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
base: '/cuit/',
plugins: [ plugins: [
vue(), vue(),
vueDevTools(), vueDevTools(),