fix:多项前端修复与功能对齐

- 修复评委端进入评审contestId为NaN(record.id→record.contestId)
- 修复评委评审详情403(活动名称改为路由传参,跳过需要contest:read权限的接口)
- 已发布活动隐藏编辑按钮
- 添加评委成功提示去重(移除子组件重复message)
- 用户端活动阶段判断修复(报名与提交重叠时优先显示提交阶段)
- 用户端作品提交支持submitRule(once/resubmit)重新提交
- 后端公共API补充submitRule字段返回
- 报名统计接口增加租户隔离,修复统计与列表数据不一致

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
zhonghua 2026-04-03 20:29:28 +08:00
parent 3ef05de193
commit 1003776dd3
8 changed files with 70 additions and 12 deletions

View File

@ -37,7 +37,9 @@ public class ContestRegistrationController {
@RequirePermission("contest:read") @RequirePermission("contest:read")
@Operation(summary = "获取报名统计") @Operation(summary = "获取报名统计")
public Result<Map<String, Object>> getStats(@RequestParam(required = false) Long contestId) { public Result<Map<String, Object>> getStats(@RequestParam(required = false) Long contestId) {
return Result.success(registrationService.getStats(contestId, SecurityUtil.getCurrentTenantId())); Long tenantId = SecurityUtil.getCurrentTenantId();
boolean isSuperAdmin = SecurityUtil.isSuperAdmin();
return Result.success(registrationService.getStats(contestId, tenantId, isSuperAdmin));
} }
@GetMapping @GetMapping

View File

@ -15,7 +15,7 @@ public interface IContestRegistrationService extends IService<BizContestRegistra
PageResult<Map<String, Object>> findAll(QueryRegistrationDto dto, Long tenantId, boolean isSuperTenant); PageResult<Map<String, Object>> findAll(QueryRegistrationDto dto, Long tenantId, boolean isSuperTenant);
Map<String, Object> getStats(Long contestId, Long tenantId); Map<String, Object> getStats(Long contestId, Long tenantId, boolean isSuperAdmin);
Map<String, Object> findDetail(Long id, Long tenantId); Map<String, Object> findDetail(Long id, Long tenantId);

View File

@ -123,20 +123,26 @@ public class ContestRegistrationServiceImpl extends ServiceImpl<ContestRegistrat
} }
@Override @Override
public Map<String, Object> getStats(Long contestId, Long tenantId) { public Map<String, Object> getStats(Long contestId, Long tenantId, boolean isSuperAdmin) {
log.info("获取报名统计赛事ID{}", contestId); log.info("获取报名统计赛事ID{}租户ID{},超管:{}", contestId, tenantId, isSuperAdmin);
// 非超管需要按租户过滤与列表查询保持一致
LambdaQueryWrapper<BizContestRegistration> baseWrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<BizContestRegistration> baseWrapper = new LambdaQueryWrapper<>();
if (contestId != null) { if (contestId != null) {
baseWrapper.eq(BizContestRegistration::getContestId, contestId); baseWrapper.eq(BizContestRegistration::getContestId, contestId);
} }
if (!isSuperAdmin && tenantId != null) {
baseWrapper.eq(BizContestRegistration::getTenantId, tenantId);
}
long total = count(baseWrapper); long total = count(baseWrapper);
LambdaQueryWrapper<BizContestRegistration> pendingWrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<BizContestRegistration> pendingWrapper = new LambdaQueryWrapper<>();
if (contestId != null) { if (contestId != null) {
pendingWrapper.eq(BizContestRegistration::getContestId, contestId); pendingWrapper.eq(BizContestRegistration::getContestId, contestId);
} }
if (!isSuperAdmin && tenantId != null) {
pendingWrapper.eq(BizContestRegistration::getTenantId, tenantId);
}
pendingWrapper.eq(BizContestRegistration::getRegistrationState, "pending"); pendingWrapper.eq(BizContestRegistration::getRegistrationState, "pending");
long pending = count(pendingWrapper); long pending = count(pendingWrapper);
@ -144,6 +150,9 @@ public class ContestRegistrationServiceImpl extends ServiceImpl<ContestRegistrat
if (contestId != null) { if (contestId != null) {
passedWrapper.eq(BizContestRegistration::getContestId, contestId); passedWrapper.eq(BizContestRegistration::getContestId, contestId);
} }
if (!isSuperAdmin && tenantId != null) {
passedWrapper.eq(BizContestRegistration::getTenantId, tenantId);
}
passedWrapper.eq(BizContestRegistration::getRegistrationState, "passed"); passedWrapper.eq(BizContestRegistration::getRegistrationState, "passed");
long passed = count(passedWrapper); long passed = count(passedWrapper);
@ -151,6 +160,9 @@ public class ContestRegistrationServiceImpl extends ServiceImpl<ContestRegistrat
if (contestId != null) { if (contestId != null) {
rejectedWrapper.eq(BizContestRegistration::getContestId, contestId); rejectedWrapper.eq(BizContestRegistration::getContestId, contestId);
} }
if (!isSuperAdmin && tenantId != null) {
rejectedWrapper.eq(BizContestRegistration::getTenantId, tenantId);
}
rejectedWrapper.eq(BizContestRegistration::getRegistrationState, "rejected"); rejectedWrapper.eq(BizContestRegistration::getRegistrationState, "rejected");
long rejected = count(rejectedWrapper); long rejected = count(rejectedWrapper);

View File

@ -85,6 +85,7 @@ public class PublicActivityService {
result.put("registerState", contest.getRegisterState()); result.put("registerState", contest.getRegisterState());
result.put("submitStartTime", contest.getSubmitStartTime()); result.put("submitStartTime", contest.getSubmitStartTime());
result.put("submitEndTime", contest.getSubmitEndTime()); result.put("submitEndTime", contest.getSubmitEndTime());
result.put("submitRule", contest.getSubmitRule());
result.put("reviewStartTime", contest.getReviewStartTime()); result.put("reviewStartTime", contest.getReviewStartTime());
result.put("reviewEndTime", contest.getReviewEndTime()); result.put("reviewEndTime", contest.getReviewEndTime());
result.put("workType", contest.getWorkType()); result.put("workType", contest.getWorkType());
@ -116,10 +117,11 @@ public class PublicActivityService {
result.put("registrationState", reg.getRegistrationState()); result.put("registrationState", reg.getRegistrationState());
result.put("registrationTime", reg.getRegistrationTime()); result.put("registrationTime", reg.getRegistrationTime());
// 查询是否已提交作品 // 查询是否已提交作品只统计最新版本
Long workCount = contestWorkMapper.selectCount( Long workCount = contestWorkMapper.selectCount(
new LambdaQueryWrapper<BizContestWork>() new LambdaQueryWrapper<BizContestWork>()
.eq(BizContestWork::getRegistrationId, reg.getId())); .eq(BizContestWork::getRegistrationId, reg.getId())
.eq(BizContestWork::getIsLatest, true));
result.put("hasSubmittedWork", workCount > 0); result.put("hasSubmittedWork", workCount > 0);
result.put("workCount", workCount); result.put("workCount", workCount);
@ -318,6 +320,30 @@ public class PublicActivityService {
throw new BusinessException(400, "未报名或报名未通过"); throw new BusinessException(400, "未报名或报名未通过");
} }
// 查询活动提交规则
BizContest contest = contestMapper.selectById(contestId);
String submitRule = contest != null ? contest.getSubmitRule() : "once";
// 查询已有作品
BizContestWork existingWork = contestWorkMapper.selectOne(
new LambdaQueryWrapper<BizContestWork>()
.eq(BizContestWork::getContestId, contestId)
.eq(BizContestWork::getRegistrationId, reg.getId())
.eq(BizContestWork::getIsLatest, true)
.orderByDesc(BizContestWork::getVersion)
.last("LIMIT 1"));
if (existingWork != null) {
if ("once".equals(submitRule)) {
throw new BusinessException(400, "该活动仅允许提交一次作品");
}
// resubmit 模式将旧作品标记为非最新
existingWork.setIsLatest(false);
contestWorkMapper.updateById(existingWork);
}
int nextVersion = (existingWork != null) ? existingWork.getVersion() + 1 : 1;
BizContestWork work = new BizContestWork(); BizContestWork work = new BizContestWork();
work.setContestId(contestId); work.setContestId(contestId);
work.setRegistrationId(reg.getId()); work.setRegistrationId(reg.getId());
@ -328,7 +354,7 @@ public class PublicActivityService {
work.setSubmitterUserId(userId); work.setSubmitterUserId(userId);
work.setStatus("submitted"); work.setStatus("submitted");
work.setSubmitTime(LocalDateTime.now()); work.setSubmitTime(LocalDateTime.now());
work.setVersion(1); work.setVersion(nextVersion);
work.setIsLatest(true); work.setIsLatest(true);
if (dto.get("userWorkId") != null) { if (dto.get("userWorkId") != null) {
work.setUserWorkId(Long.valueOf(dto.get("userWorkId").toString())); work.setUserWorkId(Long.valueOf(dto.get("userWorkId").toString()));

View File

@ -214,6 +214,7 @@ export interface PublicActivity {
registerEndTime: string registerEndTime: string
submitStartTime: string submitStartTime: string
submitEndTime: string submitEndTime: string
submitRule: string
reviewStartTime: string reviewStartTime: string
reviewEndTime: string reviewEndTime: string
organizers: any organizers: any

View File

@ -155,7 +155,6 @@
<template v-else> <template v-else>
<a-button type="link" size="small" @click="router.push(`/${tenantCode}/contests/${record.id}`)">查看</a-button> <a-button type="link" size="small" @click="router.push(`/${tenantCode}/contests/${record.id}`)">查看</a-button>
<a-button v-permission="'contest:update'" type="link" size="small" @click="handleAddJudge(record.id)">评委</a-button> <a-button v-permission="'contest:update'" type="link" size="small" @click="handleAddJudge(record.id)">评委</a-button>
<a-button v-permission="'contest:update'" type="link" size="small" @click="handleEdit(record.id)">编辑</a-button>
<a-button v-permission="'contest:publish'" type="link" size="small" style="color: #f59e0b" @click="handlePublishClick(record)">取消发布</a-button> <a-button v-permission="'contest:publish'" type="link" size="small" style="color: #f59e0b" @click="handlePublishClick(record)">取消发布</a-button>
</template> </template>
</template> </template>

View File

@ -348,7 +348,6 @@ const handleSubmit = async () => {
) )
} }
message.success("添加评委成功")
emit("success") emit("success")
} catch (error: any) { } catch (error: any) {
message.error(error?.response?.data?.message || "添加评委失败") message.error(error?.response?.data?.message || "添加评委失败")

View File

@ -60,12 +60,18 @@
<a-button v-if="!isLoggedIn" type="primary" size="large" block class="action-btn" @click="goLogin"> <a-button v-if="!isLoggedIn" type="primary" size="large" block class="action-btn" @click="goLogin">
登录后查看作品 登录后查看作品
</a-button> </a-button>
<a-button v-else-if="!hasRegistered && isRegisterOpen" type="primary" size="large" block class="action-btn" @click="showRegisterModal = true">
立即报名
</a-button>
<a-button v-else-if="!hasRegistered" size="large" block disabled class="action-btn-disabled"> <a-button v-else-if="!hasRegistered" size="large" block disabled class="action-btn-disabled">
需先报名才能提交作品 报名已截止
</a-button> </a-button>
<a-button v-else-if="!hasSubmittedWork" type="primary" size="large" block class="action-btn" @click="openSubmitWork"> <a-button v-else-if="!hasSubmittedWork" type="primary" size="large" block class="action-btn" @click="openSubmitWork">
<upload-outlined /> 提交作品 <upload-outlined /> 提交作品
</a-button> </a-button>
<a-button v-else-if="canResubmit" type="primary" size="large" block class="action-btn" @click="openSubmitWork">
<upload-outlined /> 重新提交
</a-button>
<a-button v-else size="large" block class="action-btn-done"> <a-button v-else size="large" block class="action-btn-done">
<check-circle-outlined /> 作品已提交 <check-circle-outlined /> 作品已提交
</a-button> </a-button>
@ -246,14 +252,27 @@ const formatDate = (d: string) => dayjs(d).format('YYYY-MM-DD')
const isLoggedIn = computed(() => !!localStorage.getItem('public_token')) const isLoggedIn = computed(() => !!localStorage.getItem('public_token'))
//
const isRegisterOpen = computed(() => {
if (!activity.value) return false
const now = dayjs()
return !now.isBefore(activity.value.registerStartTime) && now.isBefore(activity.value.registerEndTime)
})
// submitRule === 'resubmit'
const canResubmit = computed(() => {
return hasSubmittedWork.value && activity.value?.submitRule === 'resubmit'
})
// //
const currentStage = computed(() => { const currentStage = computed(() => {
if (!activity.value) return 'pending' if (!activity.value) return 'pending'
const now = dayjs() const now = dayjs()
const a = activity.value const a = activity.value
if (now.isBefore(a.registerStartTime)) return 'pending' if (now.isBefore(a.registerStartTime)) return 'pending'
//
if (a.submitStartTime && !now.isBefore(a.submitStartTime) && a.submitEndTime && now.isBefore(a.submitEndTime)) return 'submit'
if (now.isBefore(a.registerEndTime)) return 'register' if (now.isBefore(a.registerEndTime)) return 'register'
if (a.submitStartTime && now.isBefore(a.submitEndTime)) return 'submit'
if (a.reviewStartTime && now.isBefore(a.reviewEndTime)) return 'review' if (a.reviewStartTime && now.isBefore(a.reviewEndTime)) return 'review'
if (a.status === 'finished' || a.resultState === 'published') return 'finished' if (a.status === 'finished' || a.resultState === 'published') return 'finished'
return 'review' return 'review'