feat: C端活动详情返回公告与附件,子女账号简化报名弹窗

Made-with: Cursor
This commit is contained in:
zhonghua 2026-04-08 16:00:59 +08:00
parent 3fa1ef95ac
commit 328533e805
3 changed files with 163 additions and 27 deletions

View File

@ -8,9 +8,13 @@ import com.competition.common.enums.*;
import com.competition.common.exception.BusinessException;
import com.competition.common.result.PageResult;
import com.competition.modules.biz.contest.entity.BizContest;
import com.competition.modules.biz.contest.entity.BizContestAttachment;
import com.competition.modules.biz.contest.entity.BizContestNotice;
import com.competition.modules.biz.contest.entity.BizContestRegistration;
import com.competition.modules.biz.contest.entity.BizContestWork;
import com.competition.modules.biz.contest.mapper.ContestAttachmentMapper;
import com.competition.modules.biz.contest.mapper.ContestMapper;
import com.competition.modules.biz.contest.mapper.ContestNoticeMapper;
import com.competition.modules.biz.contest.mapper.ContestRegistrationMapper;
import com.competition.modules.biz.contest.mapper.ContestWorkMapper;
import com.competition.modules.biz.contest.service.IContestWorkService;
@ -19,6 +23,8 @@ import com.competition.modules.ugc.entity.UgcWork;
import com.competition.modules.ugc.entity.UgcWorkPage;
import com.competition.modules.ugc.mapper.UgcWorkMapper;
import com.competition.modules.ugc.mapper.UgcWorkPageMapper;
import com.competition.modules.sys.entity.SysUser;
import com.competition.modules.sys.mapper.SysUserMapper;
import com.competition.modules.user.entity.UserChild;
import com.competition.modules.user.mapper.UserChildMapper;
import lombok.RequiredArgsConstructor;
@ -42,6 +48,9 @@ public class PublicActivityService {
private final UserChildMapper userChildMapper;
private final UgcWorkMapper ugcWorkMapper;
private final UgcWorkPageMapper ugcWorkPageMapper;
private final ContestNoticeMapper contestNoticeMapper;
private final ContestAttachmentMapper contestAttachmentMapper;
private final SysUserMapper sysUserMapper;
/**
* 活动列表公开
@ -102,9 +111,60 @@ public class PublicActivityService {
result.put("workRequirement", contest.getWorkRequirement());
result.put("resultState", contest.getResultState());
result.put("resultPublishTime", contest.getResultPublishTime());
// 活动公告仅已发布publish_time 非空租户范围与活动授权租户一致
result.put("notices", listPublishedNoticesForPublic(contest));
// 附件与租户端赛事详情一致便于 C 端下载
LambdaQueryWrapper<BizContestAttachment> attWrapper = new LambdaQueryWrapper<>();
attWrapper.eq(BizContestAttachment::getContestId, id);
attWrapper.orderByAsc(BizContestAttachment::getCreateTime);
List<BizContestAttachment> attachmentEntities = contestAttachmentMapper.selectList(attWrapper);
List<Map<String, Object>> attachments = new ArrayList<>();
for (BizContestAttachment a : attachmentEntities) {
Map<String, Object> am = new LinkedHashMap<>();
am.put("id", a.getId());
am.put("fileName", a.getFileName());
am.put("fileUrl", a.getFileUrl());
am.put("fileType", a.getFileType());
am.put("format", a.getFormat());
am.put("size", a.getSize());
attachments.add(am);
}
result.put("attachments", attachments);
return result;
}
/**
* 公众端可见公告已发布且属于该活动授权租户若有无授权租户列表时仅按 contest_id 过滤
*/
private List<Map<String, Object>> listPublishedNoticesForPublic(BizContest contest) {
Long contestId = contest.getId();
LambdaQueryWrapper<BizContestNotice> nw = new LambdaQueryWrapper<>();
nw.eq(BizContestNotice::getContestId, contestId);
nw.isNotNull(BizContestNotice::getPublishTime);
List<Integer> tenantInts = contest.getContestTenants();
if (tenantInts != null && !tenantInts.isEmpty()) {
List<Long> tenantIds = tenantInts.stream().map(Integer::longValue).toList();
nw.in(BizContestNotice::getTenantId, tenantIds);
}
nw.orderByDesc(BizContestNotice::getPublishTime);
List<BizContestNotice> rows = contestNoticeMapper.selectList(nw);
List<Map<String, Object>> list = new ArrayList<>();
for (BizContestNotice n : rows) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("id", n.getId());
m.put("title", n.getTitle());
m.put("content", n.getContent());
m.put("noticeType", n.getNoticeType());
m.put("publishTime", n.getPublishTime());
m.put("createTime", n.getCreateTime());
list.add(m);
}
return list;
}
/**
* 查询当前用户的报名信息包含作品提交状态
*/
@ -294,6 +354,15 @@ public class PublicActivityService {
throw new BusinessException(400, "活动未发布");
}
// 子女账号仅允许以本人报名不可代报其他子女
SysUser currentUser = sysUserMapper.selectById(userId);
if (currentUser != null && UserType.CHILD.getValue().equals(currentUser.getUserType())) {
String pt = dto.getParticipantType() != null ? dto.getParticipantType() : ParticipantType.SELF.getValue();
if (ParticipantType.CHILD.getValue().equals(pt) || dto.getChildId() != null) {
throw new BusinessException(400, "子女账号只能以本人身份报名");
}
}
BizContestRegistration reg = new BizContestRegistration();
reg.setContestId(contestId);
reg.setUserId(userId);

View File

@ -233,6 +233,35 @@ export interface PublicActivity {
workRequirement: string
}
/** 公众端活动详情(含公告、附件等扩展字段) */
export interface PublicActivityNotice {
id: number
title: string
content: string
noticeType?: string
publishTime?: string
createTime?: string
}
export interface PublicActivityAttachment {
id: number
fileName: string
fileUrl: string
fileType?: string
format?: string
size?: string
}
export interface PublicActivityDetail extends PublicActivity {
/** 兼容旧字段;详情正文以后端 content 为准 */
description?: string
notices?: PublicActivityNotice[]
attachments?: PublicActivityAttachment[]
ageMin?: number
ageMax?: number
targetCities?: string[]
}
export const publicActivitiesApi = {
list: (params?: {
page?: number
@ -242,7 +271,8 @@ export const publicActivitiesApi = {
}): Promise<{ list: PublicActivity[]; total: number }> =>
publicApi.get("/public/activities", { params }),
detail: (id: number) => publicApi.get(`/public/activities/${id}`),
detail: (id: number): Promise<PublicActivityDetail> =>
publicApi.get(`/public/activities/${id}`),
register: (
id: number,

View File

@ -119,7 +119,7 @@
<div class="detail-card">
<a-tabs v-model:activeKey="activeTab">
<a-tab-pane key="info" tab="活动详情">
<div class="rich-content" v-html="activity.description"></div>
<div class="rich-content" v-html="activityContentHtml"></div>
<!-- 附件 -->
<div v-if="activity.attachments?.length" class="attachments">
<h4>相关附件</h4>
@ -144,7 +144,7 @@
<div v-for="notice in activity.notices" :key="notice.id" class="notice-item">
<h4>{{ notice.title }}</h4>
<div class="notice-content" v-html="notice.content"></div>
<span class="notice-time">{{ formatDate(notice.createTime) }}</span>
<span class="notice-time">{{ formatNoticeTime(notice) }}</span>
</div>
</div>
</a-tab-pane>
@ -159,23 +159,28 @@
:width="420"
>
<div class="register-modal">
<p class="modal-desc">请选择参与者</p>
<a-radio-group v-model:value="participantForm.participantType" class="participant-options">
<div class="participant-option">
<a-radio value="self">我自己</a-radio>
</div>
<template v-if="children.length">
<div v-for="child in children" :key="child.id" class="participant-option">
<a-radio :value="'child_' + child.id">
子女{{ child.name }}
<span class="child-detail" v-if="child.grade">{{ child.grade }}</span>
</a-radio>
<template v-if="isChildUser">
<p class="modal-desc">将使用当前账号报名确认参加本活动吗</p>
</template>
<template v-else>
<p class="modal-desc">请选择参与者</p>
<a-radio-group v-model:value="participantForm.participantType" class="participant-options">
<div class="participant-option">
<a-radio value="self">我自己</a-radio>
</div>
</template>
</a-radio-group>
<a-button type="link" @click="$router.push('/p/mine/children')" class="add-child-link">
+ 添加新的子女
</a-button>
<template v-if="children.length">
<div v-for="child in children" :key="child.id" class="participant-option">
<a-radio :value="'child_' + child.id">
子女{{ child.name }}
<span class="child-detail" v-if="child.grade">{{ child.grade }}</span>
</a-radio>
</div>
</template>
</a-radio-group>
<a-button type="link" @click="$router.push('/p/mine/children')" class="add-child-link">
+ 添加新的子女
</a-button>
</template>
<a-button
type="primary"
block
@ -212,13 +217,19 @@ import {
PictureOutlined, HourglassOutlined, TrophyOutlined,
TeamOutlined, EnvironmentOutlined, CloseCircleOutlined,
} from '@ant-design/icons-vue'
import { publicActivitiesApi, publicChildrenApi, type UserWork } from '@/api/public'
import {
publicActivitiesApi,
publicChildrenApi,
type PublicActivityDetail,
type PublicActivityNotice,
type UserWork,
} from '@/api/public'
import WorkSelector from './components/WorkSelector.vue'
import dayjs from 'dayjs'
const route = useRoute()
const router = useRouter()
const activity = ref<any>(null)
const activity = ref<PublicActivityDetail | null>(null)
const activeTab = ref('info')
const children = ref<any[]>([])
const showRegisterModal = ref(false)
@ -238,6 +249,27 @@ const participantForm = ref({
const formatDate = (d: string) => dayjs(d).format('YYYY-MM-DD')
const formatNoticeTime = (n: PublicActivityNotice) =>
formatDate(n.publishTime || n.createTime || '')
/** 活动详情富文本:后端字段为 content */
const activityContentHtml = computed(() => {
const a = activity.value
if (!a) return ''
return a.content || a.description || ''
})
/** 子女账号:直接报名,不选参与者、不添加子女 */
const isChildUser = computed(() => {
const raw = localStorage.getItem('public_user')
if (!raw) return false
try {
return JSON.parse(raw).userType === 'child'
} catch {
return false
}
})
const isLoggedIn = computed(() => !!localStorage.getItem('public_token'))
//
@ -345,15 +377,20 @@ const handleRegister = async () => {
router.push({ path: '/p/login', query: { redirect: route.fullPath } })
return
}
if (!activity.value) return
registering.value = true
try {
const val = participantForm.value.participantType
const isChild = val.startsWith('child_')
await publicActivitiesApi.register(activity.value.id, {
participantType: isChild ? 'child' : 'self',
childId: isChild ? parseInt(val.replace('child_', '')) : undefined,
})
if (isChildUser.value) {
await publicActivitiesApi.register(activity.value.id, { participantType: 'self' })
} else {
const val = participantForm.value.participantType
const isChild = val.startsWith('child_')
await publicActivitiesApi.register(activity.value.id, {
participantType: isChild ? 'child' : 'self',
childId: isChild ? parseInt(val.replace('child_', '')) : undefined,
})
}
message.success('报名成功!')
showRegisterModal.value = false
hasRegistered.value = true