feat: 成果发布与报名数据优化、作品列表空结果修复、创建活动主办单位默认

- 成果发布:所属活动下拉与 GET /contests 筛选(id/resultState/creatorTenantId)

- 报名列表/详情:回显 contest、creatorTenant、user;主办机构列绑定 creatorTenant

- 作品列表:contestIds 为空时跳过评委 IN 查询避免 500

- 登录与用户信息返回 tenantName;创建活动默认主办单位

- 活动监管多页筛选表单补充 model

Made-with: Cursor
This commit is contained in:
zhonghua 2026-04-10 10:32:37 +08:00
parent 8561f3d320
commit 1fff56d700
11 changed files with 208 additions and 62 deletions

View File

@ -13,6 +13,9 @@ public class QueryContestDto {
@Schema(description = "每页条数", defaultValue = "10")
private Long pageSize = 10L;
@Schema(description = "活动ID精确筛选")
private Long id;
@Schema(description = "活动名称")
private String contestName;
@ -31,6 +34,9 @@ public class QueryContestDto {
@Schema(description = "活动阶段")
private String stage;
@Schema(description = "成果发布状态筛选published/unpublished")
private String resultState;
@Schema(description = "创建者租户ID")
private Long creatorTenantId;

View File

@ -18,7 +18,9 @@ import com.competition.modules.biz.contest.mapper.ContestMapper;
import com.competition.modules.biz.contest.mapper.ContestRegistrationMapper;
import com.competition.modules.biz.contest.mapper.ContestRegistrationTeacherMapper;
import com.competition.modules.biz.contest.service.IContestRegistrationService;
import com.competition.modules.sys.entity.SysTenant;
import com.competition.modules.sys.entity.SysUser;
import com.competition.modules.sys.mapper.SysTenantMapper;
import com.competition.modules.sys.mapper.SysUserMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -39,6 +41,7 @@ public class ContestRegistrationServiceImpl extends ServiceImpl<ContestRegistrat
private final ContestRegistrationTeacherMapper contestRegistrationTeacherMapper;
private final ContestMapper contestMapper;
private final SysUserMapper sysUserMapper;
private final SysTenantMapper sysTenantMapper;
@Override
public Map<String, Object> createRegistration(CreateRegistrationDto dto, Long tenantId, Long creatorId) {
@ -119,9 +122,66 @@ public class ContestRegistrationServiceImpl extends ServiceImpl<ContestRegistrat
Page<BizContestRegistration> page = new Page<>(dto.getPage(), dto.getPageSize());
Page<BizContestRegistration> result = contestRegistrationMapper.selectPage(page, wrapper);
List<Map<String, Object>> voList = result.getRecords().stream()
.map(this::registrationToMap)
.collect(Collectors.toList());
Set<Long> contestIds = result.getRecords().stream()
.map(BizContestRegistration::getContestId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map<Long, BizContest> contestMap = new HashMap<>();
if (!contestIds.isEmpty()) {
contestMap = contestMapper.selectBatchIds(contestIds).stream()
.collect(Collectors.toMap(BizContest::getId, c -> c));
}
Set<Long> organizerTenantIds = new HashSet<>();
for (BizContest c : contestMap.values()) {
List<Integer> ct = c.getContestTenants();
if (ct != null && !ct.isEmpty()) {
organizerTenantIds.add(ct.get(0).longValue());
}
}
Map<Long, SysTenant> organizerTenantMap = new HashMap<>();
if (!organizerTenantIds.isEmpty()) {
for (SysTenant t : sysTenantMapper.selectBatchIds(organizerTenantIds)) {
if (t != null && t.getId() != null) {
organizerTenantMap.put(t.getId(), t);
}
}
}
List<Map<String, Object>> voList = new ArrayList<>();
for (BizContestRegistration reg : result.getRecords()) {
Map<String, Object> map = registrationToMap(reg);
BizContest contest = contestMap.get(reg.getContestId());
if (contest != null) {
Map<String, Object> contestVo = new LinkedHashMap<>();
contestVo.put("id", contest.getId());
contestVo.put("contestName", contest.getContestName());
map.put("contest", contestVo);
List<Integer> ct = contest.getContestTenants();
if (ct != null && !ct.isEmpty()) {
SysTenant org = organizerTenantMap.get(ct.get(0).longValue());
if (org != null) {
Map<String, Object> tenantVo = new LinkedHashMap<>();
tenantVo.put("id", org.getId());
tenantVo.put("name", org.getName());
tenantVo.put("code", org.getCode());
map.put("creatorTenant", tenantVo);
}
}
}
if (reg.getUserId() != null) {
SysUser user = sysUserMapper.selectById(reg.getUserId());
if (user != null) {
Map<String, Object> userVo = new LinkedHashMap<>();
userVo.put("id", user.getId());
userVo.put("username", user.getUsername());
userVo.put("nickname", user.getNickname());
userVo.put("phone", user.getPhone());
map.put("user", userVo);
}
}
voList.add(map);
}
return PageResult.from(result, voList);
}
@ -191,6 +251,28 @@ public class ContestRegistrationServiceImpl extends ServiceImpl<ContestRegistrat
Map<String, Object> result = registrationToMap(registration);
// 活动与主办机构前端列表/详情需要 contestcreatorTenant
if (registration.getContestId() != null) {
BizContest contest = contestMapper.selectById(registration.getContestId());
if (contest != null) {
Map<String, Object> contestVo = new LinkedHashMap<>();
contestVo.put("id", contest.getId());
contestVo.put("contestName", contest.getContestName());
result.put("contest", contestVo);
List<Integer> ct = contest.getContestTenants();
if (ct != null && !ct.isEmpty()) {
SysTenant org = sysTenantMapper.selectById(ct.get(0).longValue());
if (org != null) {
Map<String, Object> tenantVo = new LinkedHashMap<>();
tenantVo.put("id", org.getId());
tenantVo.put("name", org.getName());
tenantVo.put("code", org.getCode());
result.put("creatorTenant", tenantVo);
}
}
}
}
// 查询用户详情
if (registration.getUserId() != null) {
SysUser user = sysUserMapper.selectById(registration.getUserId());
@ -203,6 +285,12 @@ public class ContestRegistrationServiceImpl extends ServiceImpl<ContestRegistrat
userInfo.put("email", user.getEmail());
userInfo.put("avatar", user.getAvatar());
result.put("userInfo", userInfo);
Map<String, Object> userVo = new LinkedHashMap<>();
userVo.put("id", user.getId());
userVo.put("username", user.getUsername());
userVo.put("nickname", user.getNickname());
userVo.put("phone", user.getPhone());
result.put("user", userVo);
}
}

View File

@ -114,6 +114,9 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
LambdaQueryWrapper<BizContest> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(BizContest::getValidState, 1);
if (dto.getId() != null) {
wrapper.eq(BizContest::getId, dto.getId());
}
if (StringUtils.hasText(dto.getContestName())) {
wrapper.like(BizContest::getContestName, dto.getContestName());
}
@ -129,6 +132,12 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
if (StringUtils.hasText(dto.getContestType())) {
wrapper.eq(BizContest::getContestType, dto.getContestType());
}
if (StringUtils.hasText(dto.getResultState())) {
wrapper.eq(BizContest::getResultState, dto.getResultState());
}
if (dto.getCreatorTenantId() != null) {
wrapper.apply("JSON_CONTAINS(contest_tenants, CAST({0} AS JSON))", dto.getCreatorTenantId());
}
// 阶段筛选与前端活动列表活动阶段一致unpublished/finished/registering/submitting/reviewing
if (StringUtils.hasText(dto.getStage())) {

View File

@ -403,11 +403,14 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
.forEach(r -> ruleMap.put(r.getId(), r));
}
// 批量预加载所有相关活动的评委权重
LambdaQueryWrapper<BizContestJudge> allJudgesWrapper = new LambdaQueryWrapper<>();
allJudgesWrapper.in(BizContestJudge::getContestId, contestIds);
allJudgesWrapper.eq(BizContestJudge::getValidState, 1);
List<BizContestJudge> allJudges = contestJudgeMapper.selectList(allJudgesWrapper);
// 批量预加载所有相关活动的评委权重contestIds 为空时不能拼 IN ()否则 SQL 报错 500
List<BizContestJudge> allJudges = Collections.emptyList();
if (!contestIds.isEmpty()) {
LambdaQueryWrapper<BizContestJudge> allJudgesWrapper = new LambdaQueryWrapper<>();
allJudgesWrapper.in(BizContestJudge::getContestId, contestIds);
allJudgesWrapper.eq(BizContestJudge::getValidState, 1);
allJudges = contestJudgeMapper.selectList(allJudgesWrapper);
}
Map<Long, List<BizContestJudge>> judgesByContestId = allJudges.stream()
.collect(Collectors.groupingBy(BizContestJudge::getContestId));

View File

@ -118,6 +118,8 @@ export interface UpdateContestForm extends Partial<CreateContestForm> {
}
export interface QueryContestParams extends PaginationParams {
/** 活动 ID精确筛选如成果发布「所属活动」 */
id?: number;
contestName?: string;
contestState?: "unpublished" | "published";
status?: "ongoing" | "finished";
@ -131,6 +133,8 @@ export interface QueryContestParams extends PaginationParams {
| "finished";
creatorTenantId?: number;
role?: "student" | "teacher" | "judge";
/** 成果发布状态筛选 */
resultState?: "published" | "unpublished";
}
export interface ContestStats {
@ -224,6 +228,8 @@ export interface ContestRegistration {
createTime?: string;
modifyTime?: string;
contest?: Contest;
/** 活动主办机构(与活动 contest_tenants 首个机构一致) */
creatorTenant?: { id: number; name: string; code?: string };
user?: {
id: number;
username: string;

View File

@ -26,7 +26,7 @@
<!-- 筛选栏 -->
<div class="filter-bar">
<a-form layout="inline" @finish="handleSuperSearch">
<a-form layout="inline" :model="superSearch" @finish="handleSuperSearch">
<a-form-item label="所属活动">
<a-select
v-model:value="superSearch.contestId"
@ -124,7 +124,7 @@
{{ record.accountNo || record.user?.username || '-' }}
</template>
<template v-else-if="column.key === 'orgName'">
{{ record.user?.tenant?.name || '-' }}
{{ record.creatorTenant?.name || '-' }}
</template>
<template v-else-if="column.key === 'registrationState'">
<a-tag :color="stateColorMap[record.registrationState] || 'default'">
@ -234,7 +234,7 @@
<!-- 筛选 -->
<div class="filter-bar">
<a-form layout="inline" @finish="handleSearch">
<a-form layout="inline" :model="searchParams" @finish="handleSearch">
<a-form-item label="活动名称">
<a-input v-model:value="searchParams.contestName" placeholder="请输入活动名称" allow-clear style="width: 200px" />
</a-form-item>

View File

@ -26,13 +26,16 @@
<!-- 筛选栏 -->
<div class="filter-bar">
<a-form layout="inline" @finish="handleSuperSearch">
<a-form-item label="活动名称">
<a-input
v-model:value="superSearch.contestName"
placeholder="请输入活动名称"
<a-form layout="inline" :model="superSearch" @finish="handleSuperSearch">
<a-form-item label="所属活动">
<a-select
v-model:value="superSearch.contestId"
placeholder="全部活动"
allow-clear
style="width: 180px"
show-search
:filter-option="filterContestOption"
style="width: 200px"
:options="contestOptions"
/>
</a-form-item>
<a-form-item label="发布状态">
@ -147,7 +150,7 @@
<!-- 筛选 -->
<div class="filter-bar">
<a-form layout="inline" @finish="handleSearch">
<a-form layout="inline" :model="searchParams" @finish="handleSearch">
<a-form-item label="活动名称">
<a-input v-model:value="searchParams.contestName" placeholder="请输入活动名称" allow-clear style="width: 200px" />
</a-form-item>
@ -242,11 +245,24 @@ const superPagination = reactive({
showTotal: (t: number) => `${t}`,
})
const superSearch = reactive({
contestName: undefined as string | undefined,
contestId: undefined as number | undefined,
resultState: undefined as string | undefined,
creatorTenantId: undefined as number | undefined,
})
//
const contestOptions = ref<{ label: string; value: number }[]>([])
const fetchContestOptions = async () => {
try {
const res = await contestsApi.getList({ page: 1, pageSize: 500, contestState: 'published' })
contestOptions.value = res.list.map((c) => ({ label: c.contestName, value: c.id }))
} catch {
/* 静默 */
}
}
const filterContestOption = (input: string, option: any) =>
option.label?.toLowerCase().includes(input.toLowerCase())
//
const publishedCount = ref(0)
const unpublishedCount = ref(0)
@ -285,24 +301,14 @@ const fetchSuperList = async () => {
const params: QueryContestParams = {
page: superPagination.current,
pageSize: superPagination.pageSize,
contestName: superSearch.contestName,
contestState: 'published', //
contestState: 'published',
id: superSearch.contestId,
creatorTenantId: superSearch.creatorTenantId,
resultState: superSearch.resultState as 'published' | 'unpublished' | undefined,
}
const res = await contestsApi.getList(params)
//
let list = res.list
if (superSearch.resultState) {
list = list.filter((c) =>
superSearch.resultState === 'published'
? c.resultState === 'published'
: c.resultState !== 'published'
)
}
superDataSource.value = list
superPagination.total = superSearch.resultState ? list.length : res.total
superDataSource.value = res.list
superPagination.total = res.total
} catch {
message.error('获取成果列表失败')
} finally {
@ -323,7 +329,7 @@ const handleSuperSearch = () => {
}
const handleSuperReset = () => {
superSearch.contestName = undefined
superSearch.contestId = undefined
superSearch.resultState = undefined
superSearch.creatorTenantId = undefined
activeFilter.value = ''
@ -431,6 +437,7 @@ const handleViewDetail = (record: Contest) => { router.push(`/${tenantCode}/cont
onMounted(() => {
if (isSuperAdmin.value) {
fetchTenants()
fetchContestOptions()
fetchSuperStats()
fetchSuperList()
} else {

View File

@ -26,7 +26,7 @@
<!-- 筛选栏 -->
<div class="filter-bar">
<a-form layout="inline" @finish="handleSuperSearch">
<a-form layout="inline" :model="superSearch" @finish="handleSuperSearch">
<a-form-item label="所属活动">
<a-select
v-model:value="superSearch.contestId"
@ -212,7 +212,7 @@
<!-- 筛选 -->
<div class="filter-bar">
<a-form layout="inline" @finish="handleSearch">
<a-form layout="inline" :model="searchParams" @finish="handleSearch">
<a-form-item label="活动名称">
<a-input v-model:value="searchParams.contestName" placeholder="请输入活动名称" allow-clear style="width: 200px" />
</a-form-item>

View File

@ -26,7 +26,7 @@
<!-- 筛选栏 -->
<div class="filter-bar">
<a-form layout="inline" @finish="handleSuperSearch">
<a-form layout="inline" :model="superSearch" @finish="handleSuperSearch">
<a-form-item label="所属活动">
<a-select
v-model:value="superSearch.contestId"
@ -147,7 +147,7 @@
<!-- 筛选 -->
<div class="filter-bar">
<a-form layout="inline" @finish="handleSearch">
<a-form layout="inline" :model="searchParams" @finish="handleSearch">
<a-form-item label="活动名称">
<a-input v-model:value="searchParams.contestName" placeholder="请输入活动名称" allow-clear style="width: 200px" />
</a-form-item>

27
pnpm-lock.yaml generated
View File

@ -40,6 +40,9 @@ importers:
dayjs:
specifier: ^1.11.10
version: 1.11.19
dompurify:
specifier: ^3.3.3
version: 3.3.3
echarts:
specifier: ^6.0.0
version: 6.0.0
@ -71,6 +74,9 @@ importers:
'@types/crypto-js':
specifier: ^4.2.2
version: 4.2.2
'@types/dompurify':
specifier: ^3.2.0
version: 3.2.0
'@types/multer':
specifier: ^2.0.0
version: 2.0.0
@ -593,6 +599,10 @@ packages:
'@types/crypto-js@4.2.2':
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
'@types/dompurify@3.2.0':
resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==}
deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@ -632,6 +642,9 @@ packages:
'@types/serve-static@1.15.10':
resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
'@typescript-eslint/eslint-plugin@7.18.0':
resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==}
engines: {node: ^18.18.0 || >=20.0.0}
@ -1153,6 +1166,9 @@ packages:
dom7@3.0.0:
resolution: {integrity: sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==}
dompurify@3.3.3:
resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@ -2685,6 +2701,10 @@ snapshots:
'@types/crypto-js@4.2.2': {}
'@types/dompurify@3.2.0':
dependencies:
dompurify: 3.3.3
'@types/estree@1.0.8': {}
'@types/event-emitter@0.3.5': {}
@ -2734,6 +2754,9 @@ snapshots:
'@types/node': 20.19.25
'@types/send': 0.17.6
'@types/trusted-types@2.0.7':
optional: true
'@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
@ -3366,6 +3389,10 @@ snapshots:
dependencies:
ssr-window: 3.0.0
dompurify@3.3.3:
optionalDependencies:
'@types/trusted-types': 2.0.7
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2