修复协议错误

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区")
private String location;
//封面图URL(非DB字段,由 Service 从 GroupCourse 填充)
@Schema(description = "封面图URL", example = "https://example.com/cover.jpg")
private String coverImage;
public Long getCourseId() {
return courseId;
}
@@ -132,4 +136,12 @@ public class GroupCourseBooking extends BaseDomain {
public void setLocation(String 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.IGroupCourseRepository;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseBookingService;
import cn.novalon.gym.manage.groupcourse.util.OSSUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.r2dbc.core.DatabaseClient;
@@ -335,6 +336,18 @@ public class GroupCourseBookingService implements IGroupCourseBookingService {
public Flux<GroupCourseBooking> getBookingsByMemberId(Long memberId) {
logger.debug("查询会员预约记录:memberId={}", 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));
}
@@ -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.IGroupCourseRepository;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseRecommendService;
import cn.novalon.gym.manage.groupcourse.util.OSSUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@@ -134,6 +135,8 @@ public class GroupCourseRecommendService implements IGroupCourseRecommendService
return groupCourseRepository.findByIdAndDeletedAtIsNull(recommend.getCourseId())
.map(course -> {
// 将 OSS Key 转换为预签名URL,前端可直接加载
course.setCoverImage(OSSUtil.toCoverPresignedUrl(course.getCoverImage()));
recommend.setGroupCourse(course);
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.service.IGroupCourseService;
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.MemberCardRecord;
import cn.novalon.gym.manage.member.enums.MemberCardType;
@@ -32,6 +33,7 @@ import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.time.LocalDateTime;
import java.util.HashMap;
@@ -153,7 +155,7 @@ public class GroupCourseService implements IGroupCourseService {
detail.setCurrentMembers(course.getCurrentMembers());
detail.setStatus(course.getStatus());
detail.setLocation(course.getLocation());
detail.setCoverImage(course.getCoverImage());
detail.setCoverImage(OSSUtil.toCoverPresignedUrl(course.getCoverImage()));
detail.setDescription(course.getDescription());
detail.setStoredValueAmount(course.getStoredValueAmount());
detail.setQrCodePath(course.getQrCodePath());
@@ -178,7 +180,7 @@ public class GroupCourseService implements IGroupCourseService {
try {
GroupCourse groupCourse = objectMapper.readValue(cachedJson, GroupCourse.class);
logger.info("缓存命中 - findById: id={}", id);
return Mono.just(groupCourse);
return Mono.just(fillCoverPresignedUrl(groupCourse));
} catch (JsonProcessingException e) {
logger.warn("缓存解析失败,删除缓存 - id: {}, error: {}", id, e.getMessage());
return redisUtil.delete(cacheKey).then(Mono.empty());
@@ -188,6 +190,7 @@ public class GroupCourseService implements IGroupCourseService {
})
.switchIfEmpty(
groupCourseRepository.findByIdAndDeletedAtIsNull(id)
.map(this::fillCoverPresignedUrl)
.flatMap(groupCourse -> {
try {
String jsonData = objectMapper.writeValueAsString(groupCourse);
@@ -205,16 +208,19 @@ public class GroupCourseService implements IGroupCourseService {
@Override
public Flux<GroupCourse> findAll() {
return groupCourseRepository.findAll();
return groupCourseRepository.findAll()
.map(this::fillCoverPresignedUrl);
}
@Override
public Flux<GroupCourse> findAll(boolean includeDeleted) {
Flux<GroupCourse> flux;
if(includeDeleted){
return groupCourseRepository.findAll();
flux = groupCourseRepository.findAll();
}else{
return groupCourseRepository.findByDeletedAtIsNull();
flux = groupCourseRepository.findByDeletedAtIsNull();
}
return flux.map(this::fillCoverPresignedUrl);
}
@Override
@@ -234,6 +240,7 @@ public class GroupCourseService implements IGroupCourseService {
PageResponse<GroupCourse> pageResponse = objectMapper.readValue(cachedJson,
objectMapper.getTypeFactory().constructParametricType(PageResponse.class, GroupCourse.class));
logger.info("缓存命中 - findByPage: key={}", cacheKey);
fillCoverPresignedUrl(pageResponse);
return Mono.just(pageResponse);
} catch (JsonProcessingException e) {
logger.warn("缓存解析失败,删除缓存 - key: {}, error: {}", cacheKey, e.getMessage());
@@ -254,6 +261,7 @@ public class GroupCourseService implements IGroupCourseService {
return resultMono.flatMap(pageResponse -> {
try {
fillCoverPresignedUrl(pageResponse);
String jsonData = objectMapper.writeValueAsString(pageResponse);
return redisUtil.setWithExpire(cacheKey, jsonData, CACHE_EXPIRE_SECONDS)
.thenReturn(pageResponse)
@@ -572,8 +580,11 @@ public class GroupCourseService implements IGroupCourseService {
query.getTimePeriod(), query.getPriceSort(), query.getRemainingMost());
return groupCourseRepository.searchGroupCourses(query)
.doOnSuccess(result -> logger.info("多条件查询结果 - total={}, page={}, size={}",
result.getTotalElements(), result.getCurrentPage(), result.getPageSize()))
.doOnSuccess(result -> {
fillCoverPresignedUrl(result);
logger.info("多条件查询结果 - total={}, page={}, size={}",
result.getTotalElements(), result.getCurrentPage(), result.getPageSize());
})
.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_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);
// 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_SECRET = "isAfz1IFGAnV13LOIrVg19aPhY8aRq";
private static final String BUCKET_NAME = "ycc-filesaver";
@@ -37,6 +37,8 @@ public class OSSUtil {
// 预签名URL有效期(秒)
private static final long PRESIGN_EXPIRE_SECONDS = 300;
// 封面图预签名URL有效期(秒)- 1小时,确保前端列表页足够展示
private static final long COVER_PRESIGN_EXPIRE_SECONDS = 3600;
/**
* 上传文件到阿里云OSS(文件默认继承Bucket权限,不设置ACL)
@@ -171,4 +173,21 @@ public class OSSUtil {
public static String getPublicUrl(String 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',
confirmButtonText: '确定',
cancelButtonText: '取消',
}).catch(() => {})
}).catch(() => ({ value: '' }))
if (!pwd) {
ElMessage.warning('请输入验证密码')
return
@@ -94,7 +94,7 @@ async function handleSave() {
inputType: 'password',
confirmButtonText: '确定',
cancelButtonText: '取消',
}).catch(() => {})
}).catch(() => ({ value: '' }))
if (!pwd) {
ElMessage.warning('请输入验证密码')
return
@@ -132,7 +132,7 @@ async function handleAssignRoles() {
inputType: 'password',
confirmButtonText: '确定',
cancelButtonText: '取消',
}).catch(() => {})
}).catch(() => ({ value: '' }))
if (!pwd) {
ElMessage.warning('请输入验证密码')
return
@@ -89,8 +89,8 @@ async function fetchData() {
newMemberData[i] = res?.newMembers ?? 0
activeMemberData[i] = res?.activeMembers ?? 0
if (i === todayIdx) {
statCards.value[0].value = res?.totalMembers ?? 0
statCards.value[1].value = res?.newMembers ?? 0
statCards.value[0]!.value = res?.totalMembers ?? 0
statCards.value[1]!.value = res?.newMembers ?? 0
}
}).catch(() => {}),
@@ -98,14 +98,14 @@ async function fetchData() {
bookingData[i] = res?.newBookings ?? 0
cancelData[i] = res?.cancelBookings ?? 0
if (i === todayIdx) {
statCards.value[3].value = res?.newBookings ?? 0
statCards.value[3]!.value = res?.newBookings ?? 0
}
}).catch(() => {}),
getSignInStatistics(range).then((res: any) => {
signinData[i] = res?.totalSignIns ?? 0
if (i === todayIdx) {
statCards.value[2].value = res?.totalSignIns ?? 0
statCards.value[2]!.value = res?.totalSignIns ?? 0
}
}).catch(() => {}),
)
@@ -116,7 +116,7 @@ async function fetchData() {
getRevenueStatistics().then((res: any) => {
const data = res?.data
if (data) {
statCards.value[4].value = (data.todayIncome ?? 0).toFixed(2)
statCards.value[4]!.value = (data.todayIncome ?? 0).toFixed(2)
}
}).catch(() => {}),
)
@@ -123,7 +123,7 @@ async function fetchData() {
const cache: Record<number, LabelItem[]> = {}
labelResults.forEach((result, i) => {
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
+1 -1
View File
@@ -147,7 +147,7 @@ function handleSearch() {
function resetFilters() {
keyword.value = ''
genderFilter.value = null
sortBy.value = sortOptions[0]
sortBy.value = sortOptions[0]!
currentPage.value = 1
}
+3 -3
View File
@@ -107,7 +107,7 @@ async function handleDelete(row: any) {
inputType: 'password',
confirmButtonText: '确定',
cancelButtonText: '取消',
}).catch(() => {})
}).catch(() => ({ value: '' }))
if (!pwd) {
ElMessage.warning('请输入验证密码')
return
@@ -151,7 +151,7 @@ async function handleSave() {
inputType: 'password',
confirmButtonText: '确定',
cancelButtonText: '取消',
}).catch(() => {})
}).catch(() => ({ value: '' }))
if (!pwd) {
ElMessage.warning('请输入验证密码')
return
@@ -189,7 +189,7 @@ async function handleAssignPerms() {
inputType: 'password',
confirmButtonText: '确定',
cancelButtonText: '取消',
}).catch(() => {})
}).catch(() => ({ value: '' }))
if (!pwd) {
ElMessage.warning('请输入验证密码')
return
+1
View File
@@ -6,6 +6,7 @@ import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
base: '/cuit/',
plugins: [
vue(),
vueDevTools(),