Compare commits

...

2 Commits

Author SHA1 Message Date
En
c1113c937c feat: 赛事→活动术语统一,AI创作嵌套路由重构,前端依赖升级
后端:
- 全局将"赛事"统一为"活动"(Swagger注解、DTO、Entity、Controller、Service)
- 评审模块DTO/Entity/Service字段调整与优化
- 新增V9迁移脚本,修改V2/V4/V6迁移脚本注释
- PublicRegisterActivityDto字段对齐

前端:
- AI绘本创作路由重构为嵌套路由(11个子路由)
- 新增依赖:@stomp/stompjs、ali-oss、crypto-js
- 环境配置(.env)更新,vite配置调整
- API接口术语统一,PublicLayout与aicreate store优化
- 新增nginx部署文档

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 22:58:07 +08:00
En
5bb159358f fix: Webhook同步修复——实现findUserIdByPhone,补充PROCESSING状态coverUrl同步
1. 实现 findUserIdByPhone() 方法(原为 TODO 占位),注入 SysUserMapper 按手机号查询用户
2. updateProcessing() 方法补充 coverUrl 字段同步,AI创作过程中推送的封面图不再被丢弃
3. insertNewWork 中增加 WARN 日志,记录手机号未找到用户或手机号为空的情况

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 22:57:03 +08:00
63 changed files with 1089 additions and 269 deletions

View File

@ -12,7 +12,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "赛事附件")
@Tag(name = "活动附件")
@RestController
@RequestMapping("/contests/attachments")
@RequiredArgsConstructor
@ -30,7 +30,7 @@ public class ContestAttachmentController {
@GetMapping("/contest/{contestId}")
@RequirePermission("contest:read")
@Operation(summary = "查询赛事下的附件列表")
@Operation(summary = "查询活动下的附件列表")
public Result<List<BizContestAttachment>> findByContest(@PathVariable Long contestId) {
List<BizContestAttachment> list = attachmentService.list(
new LambdaQueryWrapper<BizContestAttachment>()

View File

@ -17,7 +17,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.Map;
@Tag(name = "赛事管理")
@Tag(name = "活动管理")
@RestController
@RequestMapping("/contests")
@RequiredArgsConstructor
@ -28,14 +28,14 @@ public class ContestController {
@PostMapping
@RequirePermission("contest:create")
@Operation(summary = "创建赛事")
@Operation(summary = "创建活动")
public Result<BizContest> create(@Valid @RequestBody CreateContestDto dto) {
return Result.success(contestService.createContest(dto, SecurityUtil.getCurrentUserId()));
}
@GetMapping("/stats")
@RequirePermission("contest:read")
@Operation(summary = "获取赛事统计")
@Operation(summary = "获取活动统计")
public Result<Map<String, Object>> getStats() {
Long tenantId = SecurityUtil.getCurrentTenantId();
boolean isSuperTenant = tenantService.isSuperTenant(tenantId);
@ -44,14 +44,14 @@ public class ContestController {
@GetMapping("/dashboard")
@RequirePermission("contest:read")
@Operation(summary = "获取赛事看板")
@Operation(summary = "获取活动看板")
public Result<Map<String, Object>> getDashboard() {
return Result.success(contestService.getDashboard(SecurityUtil.getCurrentTenantId()));
}
@GetMapping
@RequirePermission("contest:read")
@Operation(summary = "查询赛事列表")
@Operation(summary = "查询活动列表")
public Result<PageResult<Map<String, Object>>> findAll(QueryContestDto dto) {
Long tenantId = SecurityUtil.getCurrentTenantId();
boolean isSuperTenant = tenantService.isSuperTenant(tenantId);
@ -60,7 +60,7 @@ public class ContestController {
@GetMapping("/my-contests")
@RequirePermission({"contest:read", "contest:activity:read"})
@Operation(summary = "获取我的赛事")
@Operation(summary = "获取我的活动")
public Result<PageResult<Map<String, Object>>> getMyContests(QueryContestDto dto) {
Long userId = SecurityUtil.getCurrentUserId();
Long tenantId = SecurityUtil.getCurrentTenantId();
@ -69,21 +69,21 @@ public class ContestController {
@GetMapping("/{id}")
@RequirePermission("contest:read")
@Operation(summary = "查询赛事详情")
@Operation(summary = "查询活动详情")
public Result<Map<String, Object>> findDetail(@PathVariable Long id) {
return Result.success(contestService.findDetail(id));
}
@PatchMapping("/{id}")
@RequirePermission("contest:update")
@Operation(summary = "更新赛事")
@Operation(summary = "更新活动")
public Result<BizContest> update(@PathVariable Long id, @RequestBody CreateContestDto dto) {
return Result.success(contestService.updateContest(id, dto));
}
@PatchMapping("/{id}/publish")
@RequirePermission("contest:publish")
@Operation(summary = "发布/撤回赛事")
@Operation(summary = "发布/撤回活动")
public Result<Void> publish(@PathVariable Long id, @RequestBody Map<String, String> body) {
contestService.publishContest(id, body.get("contestState"));
return Result.success();
@ -91,7 +91,7 @@ public class ContestController {
@PatchMapping("/{id}/finish")
@RequirePermission("contest:update")
@Operation(summary = "结束赛事")
@Operation(summary = "结束活动")
public Result<Void> finish(@PathVariable Long id) {
contestService.finishContest(id);
return Result.success();
@ -99,7 +99,7 @@ public class ContestController {
@PatchMapping("/{id}/reopen")
@RequirePermission("contest:update")
@Operation(summary = "重新开放赛事")
@Operation(summary = "重新开放活动")
public Result<Void> reopen(@PathVariable Long id) {
contestService.reopenContest(id);
return Result.success();
@ -107,7 +107,7 @@ public class ContestController {
@DeleteMapping("/{id}")
@RequirePermission("contest:delete")
@Operation(summary = "删除赛事")
@Operation(summary = "删除活动")
public Result<Void> remove(@PathVariable Long id) {
contestService.removeContest(id);
return Result.success();

View File

@ -21,7 +21,7 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Tag(name = "赛事公告")
@Tag(name = "活动公告")
@RestController
@RequestMapping("/contests/notices")
@RequiredArgsConstructor
@ -78,7 +78,7 @@ public class ContestNoticeController {
@GetMapping("/contest/{contestId}")
@RequirePermission("notice:read")
@Operation(summary = "查询赛事下的公告列表")
@Operation(summary = "查询活动下的公告列表")
public Result<List<BizContestNotice>> findByContest(@PathVariable Long contestId) {
Long tenantId = SecurityUtil.getCurrentTenantId();
List<BizContestNotice> list = noticeService.list(

View File

@ -34,7 +34,7 @@ public class ContestTeamController {
@GetMapping("/contest/{contestId}")
@RequirePermission("team:read")
@Operation(summary = "查询赛事下的团队列表")
@Operation(summary = "查询活动下的团队列表")
public Result<List<Map<String, Object>>> findByContest(@PathVariable Long contestId) {
return Result.success(teamService.findByContest(contestId, SecurityUtil.getCurrentTenantId()));
}

View File

@ -7,15 +7,15 @@ import lombok.Data;
import java.util.List;
@Data
@Schema(description = "创建赛事DTO")
@Schema(description = "创建活动DTO")
public class CreateContestDto {
@NotBlank(message = "赛事名称不能为空")
@Schema(description = "赛事名称")
@NotBlank(message = "活动名称不能为空")
@Schema(description = "活动名称")
private String contestName;
@NotBlank(message = "赛事类型不能为空")
@Schema(description = "赛事类型")
@NotBlank(message = "活动类型不能为空")
@Schema(description = "活动类型")
private String contestType;
@Schema(description = "可见性")
@ -41,10 +41,10 @@ public class CreateContestDto {
@Schema(description = "地址")
private String address;
@Schema(description = "赛事内容")
@Schema(description = "活动内容")
private String content;
@Schema(description = "赛事关联租户ID列表")
@Schema(description = "活动关联租户ID列表")
private List<Integer> contestTenants;
@Schema(description = "封面图URL")

View File

@ -9,8 +9,8 @@ import lombok.Data;
@Schema(description = "创建公告DTO")
public class CreateNoticeDto {
@NotNull(message = "赛事ID不能为空")
@Schema(description = "赛事ID")
@NotNull(message = "活动ID不能为空")
@Schema(description = "活动ID")
private Long contestId;
@NotBlank(message = "公告标题不能为空")

View File

@ -9,8 +9,8 @@ import lombok.Data;
@Schema(description = "创建报名DTO")
public class CreateRegistrationDto {
@NotNull(message = "赛事ID不能为空")
@Schema(description = "赛事ID")
@NotNull(message = "活动ID不能为空")
@Schema(description = "活动ID")
private Long contestId;
@NotBlank(message = "报名类型不能为空")

View File

@ -11,8 +11,8 @@ import java.util.List;
@Schema(description = "创建团队DTO")
public class CreateTeamDto {
@NotNull(message = "赛事ID不能为空")
@Schema(description = "赛事ID")
@NotNull(message = "活动ID不能为空")
@Schema(description = "活动ID")
private Long contestId;
@NotBlank(message = "团队名称不能为空")

View File

@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "查询赛事DTO")
@Schema(description = "查询活动DTO")
public class QueryContestDto {
@Schema(description = "页码", defaultValue = "1")
@ -13,22 +13,22 @@ public class QueryContestDto {
@Schema(description = "每页条数", defaultValue = "10")
private Long pageSize = 10L;
@Schema(description = "赛事名称")
@Schema(description = "活动名称")
private String contestName;
@Schema(description = "赛事状态")
@Schema(description = "活动状态")
private String contestState;
@Schema(description = "状态")
private String status;
@Schema(description = "赛事类型")
@Schema(description = "活动类型")
private String contestType;
@Schema(description = "可见性")
private String visibility;
@Schema(description = "赛事阶段")
@Schema(description = "活动阶段")
private String stage;
@Schema(description = "创建者租户ID")

View File

@ -13,7 +13,7 @@ public class QueryRegistrationDto {
@Schema(description = "每页条数", defaultValue = "10")
private Long pageSize = 10L;
@Schema(description = "赛事ID")
@Schema(description = "活动ID")
private Long contestId;
@Schema(description = "报名状态")

View File

@ -13,7 +13,7 @@ public class QueryWorkDto {
@Schema(description = "每页条数", defaultValue = "10")
private Long pageSize = 10L;
@Schema(description = "赛事ID")
@Schema(description = "活动ID")
private Long contestId;
@Schema(description = "报名ID")

View File

@ -13,27 +13,27 @@ import java.time.LocalDateTime;
import java.util.List;
/**
* 赛事实体35+ 字段7 JSON
* 活动实体35+ 字段7 JSON
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName(value = "t_biz_contest", autoResultMap = true)
@Schema(description = "赛事实体")
@Schema(description = "活动实体")
public class BizContest extends BaseEntity {
@Schema(description = "赛事名称")
@Schema(description = "活动名称")
@TableField("contest_name")
private String contestName;
@Schema(description = "赛事类型individual/team")
@Schema(description = "活动类型individual/team")
@TableField("contest_type")
private String contestType;
@Schema(description = "赛事发布状态", allowableValues = {"published", "unpublished"})
@Schema(description = "活动发布状态", allowableValues = {"published", "unpublished"})
@TableField("contest_state")
private String contestState;
@Schema(description = "赛事进度状态ongoing/finished")
@Schema(description = "活动进度状态ongoing/finished")
private String status;
@Schema(description = "开始时间")
@ -47,7 +47,7 @@ public class BizContest extends BaseEntity {
@Schema(description = "线下地址")
private String address;
@Schema(description = "赛事详情(富文本)")
@Schema(description = "活动详情(富文本)")
private String content;
@Schema(description = "可见范围", allowableValues = {"public", "designated", "internal", "private"})

View File

@ -10,10 +10,10 @@ import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_biz_contest_attachment")
@Schema(description = "赛事附件实体")
@Schema(description = "活动附件实体")
public class BizContestAttachment extends BaseEntity {
@Schema(description = "赛事ID")
@Schema(description = "活动ID")
@TableField("contest_id")
private Long contestId;

View File

@ -10,15 +10,15 @@ import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 赛事公告实体
* 活动公告实体
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_biz_contest_notice")
@Schema(description = "赛事公告实体")
@Schema(description = "活动公告实体")
public class BizContestNotice extends BaseEntity {
@Schema(description = "赛事ID")
@Schema(description = "活动ID")
@TableField("contest_id")
private Long contestId;

View File

@ -12,10 +12,10 @@ import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_biz_contest_registration")
@Schema(description = "赛事报名实体")
@Schema(description = "活动报名实体")
public class BizContestRegistration extends BaseEntity {
@Schema(description = "赛事ID")
@Schema(description = "活动ID")
@TableField("contest_id")
private Long contestId;

View File

@ -9,7 +9,7 @@ import java.time.LocalDateTime;
@Data
@TableName("t_biz_contest_registration_teacher")
@Schema(description = "赛事报名老师关联实体")
@Schema(description = "活动报名老师关联实体")
public class BizContestRegistrationTeacher implements Serializable {
@Schema(description = "主键ID")

View File

@ -10,14 +10,14 @@ import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_biz_contest_team")
@Schema(description = "赛事团队实体")
@Schema(description = "活动团队实体")
public class BizContestTeam extends BaseEntity {
@Schema(description = "租户ID")
@TableField("tenant_id")
private Long tenantId;
@Schema(description = "赛事ID")
@Schema(description = "活动ID")
@TableField("contest_id")
private Long contestId;

View File

@ -9,7 +9,7 @@ import java.time.LocalDateTime;
@Data
@TableName("t_biz_contest_team_member")
@Schema(description = "赛事团队成员实体")
@Schema(description = "活动团队成员实体")
public class BizContestTeamMember implements Serializable {
@Schema(description = "主键ID")

View File

@ -15,14 +15,14 @@ import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName(value = "t_biz_contest_work", autoResultMap = true)
@Schema(description = "赛事作品实体")
@Schema(description = "活动作品实体")
public class BizContestWork extends BaseEntity {
@Schema(description = "租户ID")
@TableField("tenant_id")
private Long tenantId;
@Schema(description = "赛事ID")
@Schema(description = "活动ID")
@TableField("contest_id")
private Long contestId;
@ -86,7 +86,7 @@ public class BizContestWork extends BaseEntity {
@TableField("user_work_id")
private Long userWorkId;
// ====== 果字段 ======
// ====== 果字段 ======
@Schema(description = "最终得分")
@TableField("final_score")
private BigDecimal finalScore;

View File

@ -9,7 +9,7 @@ import java.time.LocalDateTime;
@Data
@TableName("t_biz_contest_work_attachment")
@Schema(description = "赛事作品附件实体")
@Schema(description = "活动作品附件实体")
public class BizContestWorkAttachment implements Serializable {
@Schema(description = "主键ID")
@ -20,7 +20,7 @@ public class BizContestWorkAttachment implements Serializable {
@TableField("tenant_id")
private Long tenantId;
@Schema(description = "赛事ID")
@Schema(description = "活动ID")
@TableField("contest_id")
private Long contestId;

View File

@ -12,7 +12,7 @@ import java.util.Map;
public interface IContestWorkService extends IService<BizContestWork> {
/**
* 为指定赛事生成下一个作品编号 {@link #submitWork} 所用规则一致W{contestId}-{序号}
* 为指定活动生成下一个作品编号 {@link #submitWork} 所用规则一致W{contestId}-{序号}
*/
String nextContestWorkNo(Long contestId);

View File

@ -42,15 +42,15 @@ public class ContestRegistrationServiceImpl extends ServiceImpl<ContestRegistrat
@Override
public Map<String, Object> createRegistration(CreateRegistrationDto dto, Long tenantId, Long creatorId) {
log.info("开始创建报名,赛事ID{}用户ID{}", dto.getContestId(), dto.getUserId());
log.info("开始创建报名,活动ID{}用户ID{}", dto.getContestId(), dto.getUserId());
// 验证赛事存在且已发布
// 验证活动存在且已发布
BizContest contest = contestMapper.selectById(dto.getContestId());
if (contest == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "赛事不存在");
throw BusinessException.of(ErrorCode.NOT_FOUND, "活动不存在");
}
if (!PublishStatus.PUBLISHED.getValue().equals(contest.getContestState())) {
throw BusinessException.of(ErrorCode.BAD_REQUEST, "赛事未发布,无法报名");
throw BusinessException.of(ErrorCode.BAD_REQUEST, "活动未发布,无法报名");
}
// 获取用户信息
@ -85,7 +85,7 @@ public class ContestRegistrationServiceImpl extends ServiceImpl<ContestRegistrat
@Override
public PageResult<Map<String, Object>> findAll(QueryRegistrationDto dto, Long tenantId, boolean isSuperTenant) {
log.info("查询报名列表,赛事ID{},页码:{}", dto.getContestId(), dto.getPage());
log.info("查询报名列表,活动ID{},页码:{}", dto.getContestId(), dto.getPage());
LambdaQueryWrapper<BizContestRegistration> wrapper = new LambdaQueryWrapper<>();
@ -128,7 +128,7 @@ public class ContestRegistrationServiceImpl extends ServiceImpl<ContestRegistrat
@Override
public Map<String, Object> getStats(Long contestId, Long tenantId, boolean isSuperAdmin) {
log.info("获取报名统计,赛事ID{}租户ID{},超管:{}", contestId, tenantId, isSuperAdmin);
log.info("获取报名统计,活动ID{}租户ID{},超管:{}", contestId, tenantId, isSuperAdmin);
// 非超管需要按租户过滤
boolean needTenantFilter = !isSuperAdmin && tenantId != null;
@ -217,7 +217,7 @@ public class ContestRegistrationServiceImpl extends ServiceImpl<ContestRegistrat
@Override
public Map<String, Object> getMyRegistration(Long contestId, Long userId, Long tenantId) {
log.info("查询我的报名,赛事ID{}用户ID{}", contestId, userId);
log.info("查询我的报名,活动ID{}用户ID{}", contestId, userId);
LambdaQueryWrapper<BizContestRegistration> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(BizContestRegistration::getContestId, contestId);

View File

@ -73,7 +73,7 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
@Override
public BizContest createContest(CreateContestDto dto, Long creatorId) {
log.info("开始创建赛事,名称:{}", dto.getContestName());
log.info("开始创建活动,名称:{}", dto.getContestName());
try {
BizContest entity = new BizContest();
@ -97,17 +97,17 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
}
save(entity);
log.info("赛事创建成功ID{}, 名称:{}", entity.getId(), entity.getContestName());
log.info("活动创建成功ID{}, 名称:{}", entity.getId(), entity.getContestName());
return entity;
} catch (Exception e) {
log.error("创建赛事失败,名称:{}", dto.getContestName(), e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "创建赛事失败:" + e.getMessage());
log.error("创建活动失败,名称:{}", dto.getContestName(), e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "创建活动失败:" + e.getMessage());
}
}
@Override
public PageResult<Map<String, Object>> findAll(QueryContestDto dto, Long tenantId, boolean isSuperTenant) {
log.info("查询赛事列表,页码:{},每页:{}", dto.getPage(), dto.getPageSize());
log.info("查询活动列表,页码:{},每页:{}", dto.getPage(), dto.getPageSize());
LambdaQueryWrapper<BizContest> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(BizContest::getValidState, 1);
@ -220,7 +220,7 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
@Override
public PageResult<Map<String, Object>> getMyContests(QueryContestDto dto, Long userId, Long tenantId) {
log.info("查询我的赛事用户ID{}租户ID{}", userId, tenantId);
log.info("查询我的活动用户ID{}租户ID{}", userId, tenantId);
LambdaQueryWrapper<BizContest> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(BizContest::getValidState, 1);
@ -249,11 +249,11 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
@Override
public Map<String, Object> findDetail(Long id) {
log.info("查询赛事详情ID{}", id);
log.info("查询活动详情ID{}", id);
BizContest contest = getById(id);
if (contest == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "赛事不存在");
throw BusinessException.of(ErrorCode.NOT_FOUND, "活动不存在");
}
Map<String, Object> result = entityToMap(contest);
@ -287,72 +287,72 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
@Override
public BizContest updateContest(Long id, CreateContestDto dto) {
log.info("更新赛事ID{}", id);
log.info("更新活动ID{}", id);
BizContest entity = getById(id);
if (entity == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "赛事不存在");
throw BusinessException.of(ErrorCode.NOT_FOUND, "活动不存在");
}
mapDtoToEntity(dto, entity);
updateById(entity);
log.info("赛事更新成功ID{}", id);
log.info("活动更新成功ID{}", id);
return entity;
}
@Override
public void publishContest(Long id, String contestState) {
log.info("发布/撤回赛事ID{},状态:{}", id, contestState);
log.info("发布/撤回活动ID{},状态:{}", id, contestState);
BizContest entity = getById(id);
if (entity == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "赛事不存在");
throw BusinessException.of(ErrorCode.NOT_FOUND, "活动不存在");
}
entity.setContestState(contestState);
updateById(entity);
log.info("赛事状态更新成功ID{},新状态:{}", id, contestState);
log.info("活动状态更新成功ID{},新状态:{}", id, contestState);
}
@Override
public void finishContest(Long id) {
log.info("结束赛事ID{}", id);
log.info("结束活动ID{}", id);
BizContest entity = getById(id);
if (entity == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "赛事不存在");
throw BusinessException.of(ErrorCode.NOT_FOUND, "活动不存在");
}
entity.setStatus("finished");
updateById(entity);
log.info("赛事已结束ID{}", id);
log.info("活动已结束ID{}", id);
}
@Override
public void reopenContest(Long id) {
log.info("重新开启赛事ID{}", id);
log.info("重新开启活动ID{}", id);
BizContest entity = getById(id);
if (entity == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "赛事不存在");
throw BusinessException.of(ErrorCode.NOT_FOUND, "活动不存在");
}
entity.setStatus("ongoing");
updateById(entity);
log.info("赛事已重新开启ID{}", id);
log.info("活动已重新开启ID{}", id);
}
@Override
public void removeContest(Long id) {
log.info("删除赛事ID{}", id);
log.info("删除活动ID{}", id);
removeById(id);
log.info("赛事删除成功ID{}", id);
log.info("活动删除成功ID{}", id);
}
@Override
public Map<String, Object> getStats(Long tenantId, boolean isSuperTenant) {
log.info("获取赛事统计租户ID{}", tenantId);
log.info("获取活动统计租户ID{}", tenantId);
LambdaQueryWrapper<BizContest> baseWrapper = new LambdaQueryWrapper<>();
baseWrapper.eq(BizContest::getValidState, 1);

View File

@ -32,7 +32,7 @@ public class ContestTeamServiceImpl extends ServiceImpl<ContestTeamMapper, BizCo
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> createTeam(CreateTeamDto dto, Long tenantId, Long creatorId) {
log.info("开始创建团队,赛事ID{},团队名称:{}", dto.getContestId(), dto.getTeamName());
log.info("开始创建团队,活动ID{},团队名称:{}", dto.getContestId(), dto.getTeamName());
BizContestTeam team = new BizContestTeam();
team.setTenantId(tenantId);
@ -70,7 +70,7 @@ public class ContestTeamServiceImpl extends ServiceImpl<ContestTeamMapper, BizCo
@Override
public List<Map<String, Object>> findByContest(Long contestId, Long tenantId) {
log.info("查询赛事团队列表,赛事ID{}", contestId);
log.info("查询活动团队列表,活动ID{}", contestId);
LambdaQueryWrapper<BizContestTeam> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(BizContestTeam::getContestId, contestId);

View File

@ -76,7 +76,7 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
Long contestId = registration.getContestId();
// 查询赛事提交规则
// 查询活动提交规则
BizContest contest = contestMapper.selectById(contestId);
String submitRule = contest != null ? contest.getSubmitRule() : "once";
@ -157,7 +157,7 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
@Override
public PageResult<Map<String, Object>> findAll(QueryWorkDto dto, Long tenantId, boolean isSuperTenant) {
log.info("查询作品列表,赛事ID{},页码:{}", dto.getContestId(), dto.getPage());
log.info("查询作品列表,活动ID{},页码:{}", dto.getContestId(), dto.getPage());
LambdaQueryWrapper<BizContestWork> wrapper = new LambdaQueryWrapper<>();
@ -310,7 +310,7 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
Page<BizContestWork> page = new Page<>(dto.getPage(), dto.getPageSize());
Page<BizContestWork> result = contestWorkMapper.selectPage(page, wrapper);
// 批量查询报名/赛事信息
// 批量查询报名/活动信息
Set<Long> registrationIds = result.getRecords().stream()
.map(BizContestWork::getRegistrationId)
.filter(Objects::nonNull)
@ -499,7 +499,7 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
@Override
public Map<String, Object> getStats(Long contestId, Long tenantId, boolean isSuperTenant) {
log.info("获取作品统计,赛事ID{}", contestId);
log.info("获取作品统计,活动ID{}", contestId);
// 租户过滤
boolean needTenantFilter = !isSuperTenant && tenantId != null;
@ -611,7 +611,7 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
@Override
public PageResult<Map<String, Object>> getGuidedWorks(Long contestId, String workNo, String playerName,
String accountNo, Long page, Long pageSize, Long userId) {
log.info("查询指导作品,赛事ID{}教师用户ID{}", contestId, userId);
log.info("查询指导作品,活动ID{}教师用户ID{}", contestId, userId);
// 简化实现查询当前教师指导的报名ID列表再查对应作品
// 完整实现需要关联 t_biz_contest_registration_teacher

View File

@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.Map;
@Tag(name = "赛事评委")
@Tag(name = "活动评委")
@RestController
@RequestMapping("/contests/judges")
@RequiredArgsConstructor
@ -23,7 +23,7 @@ public class ContestJudgeController {
@PostMapping
@RequirePermission("contest:update")
@Operation(summary = "添加赛事评委")
@Operation(summary = "添加活动评委")
public Result<BizContestJudge> createJudge(@RequestBody Map<String, Object> body) {
Long contestId = Long.valueOf(body.get("contestId").toString());
Long judgeId = Long.valueOf(body.get("judgeId").toString());
@ -35,7 +35,7 @@ public class ContestJudgeController {
@GetMapping("/contest/{contestId}")
@RequirePermission("contest:read")
@Operation(summary = "查询赛事评委列表",
@Operation(summary = "查询活动评委列表",
description = "返回 assigned显式关联与 implicitPool平台隐式池。添加评委抽屉仅用 assigned 回显;作品分配可选池为 assigned implicitPool前端合并")
public Result<ContestJudgesForContestVo> findByContest(@PathVariable Long contestId) {
return Result.success(contestJudgeService.findByContest(contestId));

View File

@ -82,7 +82,7 @@ public class ContestReviewController {
@GetMapping("/judge/contests")
@RequirePermission("review:score")
@Operation(summary = "获取评委参与的赛事列表")
@Operation(summary = "获取评委参与的活动列表")
public Result<List<Map<String, Object>>> getJudgeContests() {
Long judgeId = SecurityUtil.getCurrentUserId();
return Result.success(contestReviewService.getJudgeContests(judgeId));
@ -90,7 +90,7 @@ public class ContestReviewController {
@GetMapping("/judge/contests/{contestId}/works")
@RequirePermission("review:score")
@Operation(summary = "获取评委赛事作品列表")
@Operation(summary = "获取评委活动作品列表")
public Result<PageResult<Map<String, Object>>> getJudgeContestWorks(
@PathVariable Long contestId,
@RequestParam(defaultValue = "1") Long page,
@ -132,7 +132,7 @@ public class ContestReviewController {
@GetMapping("/judge/contests/{contestId}/detail")
@RequirePermission("review:read")
@Operation(summary = "获取评委视角的赛事详情(含评审规则)")
@Operation(summary = "获取评委视角的活动详情(含评审规则)")
public Result<Map<String, Object>> getJudgeContestDetail(@PathVariable Long contestId) {
Long judgeId = SecurityUtil.getCurrentUserId();
return Result.success(contestReviewService.getJudgeContestDetail(judgeId, contestId));

View File

@ -38,7 +38,7 @@ public class PresetCommentController {
}
@GetMapping("/judge/contests")
@Operation(summary = "获取评委参与的赛事列表")
@Operation(summary = "获取评委参与的活动列表")
public Result<List<Map<String, Object>>> getJudgeContests() {
Long judgeId = SecurityUtil.getCurrentUserId();
return Result.success(presetCommentService.getJudgeContests(judgeId));
@ -76,7 +76,7 @@ public class PresetCommentController {
}
@PostMapping("/sync")
@Operation(summary = "同步评语到其他赛事")
@Operation(summary = "同步评语到其他活动")
public Result<Map<String, Object>> syncComments(@Valid @RequestBody SyncPresetCommentsDto dto) {
Long judgeId = SecurityUtil.getCurrentUserId();
return Result.success(presetCommentService.syncComments(dto.getSourceContestId(), dto.getTargetContestIds(), judgeId));

View File

@ -7,15 +7,15 @@ import java.util.List;
import java.util.Map;
/**
* 赛事下的评委数据显式关联与平台隐式池分离避免扁平列表语义混淆
* 活动下的评委数据显式关联与平台隐式池分离避免扁平列表语义混淆
*/
@Data
@Schema(description = "赛事评委查询结果")
@Schema(description = "活动评委查询结果")
public class ContestJudgesForContestVo {
@Schema(description = "机构为该赛事显式添加的评委t_biz_contest_judge每条必有 id、judgeId添加评委抽屉回显与提交差集仅基于此列表")
@Schema(description = "机构为该活动显式添加的评委t_biz_contest_judge每条必有 id、judgeId添加评委抽屉回显与提交差集仅基于此列表")
private List<Map<String, Object>> assigned;
@Schema(description = "平台评委租户下对该赛事默认可用、未写入关联表的用户id 为 nullisPlatform 为 true可与 assigned 合并作为作品分配可选池")
@Schema(description = "平台评委租户下对该活动默认可用、未写入关联表的用户id 为 nullisPlatform 为 true可与 assigned 合并作为作品分配可选池")
private List<Map<String, Object>> implicitPool;
}

View File

@ -11,8 +11,8 @@ import java.math.BigDecimal;
@Schema(description = "创建预设评语DTO")
public class CreatePresetCommentDto {
@NotNull(message = "赛事ID不能为空")
@Schema(description = "赛事ID")
@NotNull(message = "活动ID不能为空")
@Schema(description = "活动ID")
private Long contestId;
@NotBlank(message = "评语内容不能为空")

View File

@ -11,11 +11,11 @@ import java.util.List;
@Schema(description = "同步预设评语DTO")
public class SyncPresetCommentsDto {
@NotNull(message = "赛事ID不能为空")
@Schema(description = "赛事ID")
@NotNull(message = "活动ID不能为空")
@Schema(description = "活动ID")
private Long sourceContestId;
@NotEmpty(message = "目标赛事列表不能为空")
@Schema(description = "目标赛事ID列表")
@NotEmpty(message = "目标活动列表不能为空")
@Schema(description = "目标活动ID列表")
private List<Long> targetContestIds;
}

View File

@ -12,10 +12,10 @@ import java.math.BigDecimal;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_biz_contest_judge")
@Schema(description = "赛事评委实体")
@Schema(description = "活动评委实体")
public class BizContestJudge extends BaseEntity {
@Schema(description = "赛事ID")
@Schema(description = "活动ID")
@TableField("contest_id")
private Long contestId;

View File

@ -11,7 +11,7 @@ import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName(value = "t_biz_contest_review_rule", autoResultMap = true)
@Schema(description = "赛事评审规则实体")
@Schema(description = "活动评审规则实体")
public class BizContestReviewRule extends BaseEntity {
@Schema(description = "租户ID")

View File

@ -12,14 +12,14 @@ import java.time.LocalDateTime;
@Data
@TableName("t_biz_contest_work_judge_assignment")
@Schema(description = "赛事作品评委分配实体")
@Schema(description = "活动作品评委分配实体")
public class BizContestWorkJudgeAssignment implements Serializable {
@Schema(description = "主键ID")
@TableId(type = IdType.AUTO)
private Long id;
@Schema(description = "赛事ID")
@Schema(description = "活动ID")
@TableField("contest_id")
private Long contestId;

View File

@ -14,14 +14,14 @@ import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName(value = "t_biz_contest_work_score", autoResultMap = true)
@Schema(description = "赛事作品评分实体")
@Schema(description = "活动作品评分实体")
public class BizContestWorkScore extends BaseEntity {
@Schema(description = "租户ID")
@TableField("tenant_id")
private Long tenantId;
@Schema(description = "赛事ID")
@Schema(description = "活动ID")
@TableField("contest_id")
private Long contestId;

View File

@ -15,7 +15,7 @@ import java.math.BigDecimal;
@Schema(description = "预设评语实体")
public class BizPresetComment extends BaseEntity {
@Schema(description = "赛事ID")
@Schema(description = "活动ID")
@TableField("contest_id")
private Long contestId;

View File

@ -12,7 +12,7 @@ public interface IContestJudgeService extends IService<BizContestJudge> {
BizContestJudge createJudge(Long contestId, Long judgeId, String specialty, BigDecimal weight, String description);
/**
* 查询某赛事评委{@link ContestJudgesForContestVo#getAssigned()} 为显式关联
* 查询某活动评委{@link ContestJudgesForContestVo#getAssigned()} 为显式关联
* {@link ContestJudgesForContestVo#getImplicitPool()} 为平台默认可用未落库项
*/
ContestJudgesForContestVo findByContest(Long contestId);

View File

@ -36,7 +36,7 @@ public class ContestJudgeServiceImpl extends ServiceImpl<ContestJudgeMapper, Biz
@Override
public BizContestJudge createJudge(Long contestId, Long judgeId, String specialty, BigDecimal weight, String description) {
log.info("添加评委,赛事ID{}评委用户ID{}", contestId, judgeId);
log.info("添加评委,活动ID{}评委用户ID{}", contestId, judgeId);
// 校验是否重复
LambdaQueryWrapper<BizContestJudge> dupWrapper = new LambdaQueryWrapper<>();
@ -44,7 +44,7 @@ public class ContestJudgeServiceImpl extends ServiceImpl<ContestJudgeMapper, Biz
dupWrapper.eq(BizContestJudge::getJudgeId, judgeId);
dupWrapper.eq(BizContestJudge::getValidState, 1);
if (count(dupWrapper) > 0) {
throw BusinessException.of(ErrorCode.BAD_REQUEST, "该评委已添加到此赛事");
throw BusinessException.of(ErrorCode.BAD_REQUEST, "该评委已添加到此活动");
}
BizContestJudge entity = new BizContestJudge();
@ -61,7 +61,7 @@ public class ContestJudgeServiceImpl extends ServiceImpl<ContestJudgeMapper, Biz
@Override
public ContestJudgesForContestVo findByContest(Long contestId) {
log.info("查询赛事评委列表,赛事ID{}", contestId);
log.info("查询活动评委列表,活动ID{}", contestId);
// 获取平台评委租户 ID
Long judgeTenantId = getJudgeTenantId();
@ -77,7 +77,7 @@ public class ContestJudgeServiceImpl extends ServiceImpl<ContestJudgeMapper, Biz
// 收集所有需要查询的用户 ID已分配评委
Set<Long> assignedJudgeIds = judges.stream().map(BizContestJudge::getJudgeId).collect(Collectors.toSet());
// 2. 查询平台评委自动对所有赛事可用
// 2. 查询平台评委自动对所有活动可用
List<SysUser> platformJudges = new ArrayList<>();
if (judgeTenantId != null) {
LambdaQueryWrapper<SysUser> platformWrapper = new LambdaQueryWrapper<>();
@ -105,7 +105,7 @@ public class ContestJudgeServiceImpl extends ServiceImpl<ContestJudgeMapper, Biz
}
}
// 批量查询每个评委在该赛事下的已分配作品数
// 批量查询每个评委在该活动下的已分配作品数
Map<Long, Long> assignedCountMap = new HashMap<>();
for (Long judgeId : allUserIds) {
LambdaQueryWrapper<BizContestWorkJudgeAssignment> assignWrapper = new LambdaQueryWrapper<>();

View File

@ -48,11 +48,11 @@ public class ContestResultServiceImpl implements IContestResultService {
@Override
public Map<String, Object> calculateAllFinalScores(Long contestId) {
log.info("批量计算终分,赛事ID{}", contestId);
log.info("批量计算终分,活动ID{}", contestId);
BizContest contest = contestMapper.selectById(contestId);
if (contest == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "赛事不存在");
throw BusinessException.of(ErrorCode.NOT_FOUND, "活动不存在");
}
// 获取计算规则
@ -107,7 +107,7 @@ public class ContestResultServiceImpl implements IContestResultService {
}
}
log.info("批量计算终分完成,赛事ID{},计算数量:{}", contestId, calculatedCount);
log.info("批量计算终分完成,活动ID{},计算数量:{}", contestId, calculatedCount);
Map<String, Object> result = new LinkedHashMap<>();
result.put("calculatedCount", calculatedCount);
@ -117,7 +117,7 @@ public class ContestResultServiceImpl implements IContestResultService {
@Override
public Map<String, Object> calculateRankings(Long contestId) {
log.info("计算排名,赛事ID{}", contestId);
log.info("计算排名,活动ID{}", contestId);
LambdaQueryWrapper<BizContestWork> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(BizContestWork::getContestId, contestId);
@ -145,7 +145,7 @@ public class ContestResultServiceImpl implements IContestResultService {
workMapper.updateById(work);
}
log.info("排名计算完成,赛事ID{},排名数量:{}", contestId, works.size());
log.info("排名计算完成,活动ID{},排名数量:{}", contestId, works.size());
Map<String, Object> result = new LinkedHashMap<>();
result.put("rankedCount", works.size());
@ -171,7 +171,7 @@ public class ContestResultServiceImpl implements IContestResultService {
@Override
public Map<String, Object> batchSetAwards(Long contestId, BatchSetAwardsDto dto) {
log.info("批量设置奖项,赛事ID{},数量:{}", contestId, dto.getAwards().size());
log.info("批量设置奖项,活动ID{},数量:{}", contestId, dto.getAwards().size());
int updated = 0;
for (BatchSetAwardsDto.AwardItem item : dto.getAwards()) {
@ -193,7 +193,7 @@ public class ContestResultServiceImpl implements IContestResultService {
@Override
public Map<String, Object> autoSetAwards(Long contestId, AutoSetAwardsDto dto) {
log.info("自动设置奖项,赛事ID{}", contestId);
log.info("自动设置奖项,活动ID{}", contestId);
// 1. 获取已排名作品按排名排序
LambdaQueryWrapper<BizContestWork> wrapper = new LambdaQueryWrapper<>();
@ -252,11 +252,11 @@ public class ContestResultServiceImpl implements IContestResultService {
@Override
public void publishResults(Long contestId) {
log.info("发布赛果,赛事ID{}", contestId);
log.info("发布成果,活动ID{}", contestId);
BizContest contest = contestMapper.selectById(contestId);
if (contest == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "赛事不存在");
throw BusinessException.of(ErrorCode.NOT_FOUND, "活动不存在");
}
// 验证有排名作品
@ -276,32 +276,32 @@ public class ContestResultServiceImpl implements IContestResultService {
contest.setStatus("finished");
contestMapper.updateById(contest);
log.info("赛果发布成功,赛事ID{}", contestId);
log.info("成果发布成功,活动ID{}", contestId);
}
@Override
public void unpublishResults(Long contestId) {
log.info("撤回赛果,赛事ID{}", contestId);
log.info("撤回成果,活动ID{}", contestId);
BizContest contest = contestMapper.selectById(contestId);
if (contest == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "赛事不存在");
throw BusinessException.of(ErrorCode.NOT_FOUND, "活动不存在");
}
if (!PublishStatus.PUBLISHED.getValue().equals(contest.getResultState())) {
throw BusinessException.of(ErrorCode.BAD_REQUEST, "果未发布,无法撤回");
throw BusinessException.of(ErrorCode.BAD_REQUEST, "果未发布,无法撤回");
}
contest.setResultState(PublishStatus.UNPUBLISHED.getValue());
contest.setResultPublishTime(null);
contestMapper.updateById(contest);
log.info("赛果撤回成功,赛事ID{}", contestId);
log.info("成果撤回成功,活动ID{}", contestId);
}
@Override
public PageResult<Map<String, Object>> getResults(Long contestId, Long page, Long pageSize, String workNo, String accountNo) {
log.info("查询赛果列表,赛事ID{},页码:{}", contestId, page);
log.info("查询成果列表,活动ID{},页码:{}", contestId, page);
LambdaQueryWrapper<BizContestWork> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(BizContestWork::getContestId, contestId);
@ -446,7 +446,7 @@ public class ContestResultServiceImpl implements IContestResultService {
@Override
public Map<String, Object> getResultsSummary(Long contestId) {
log.info("查询赛果概览,赛事ID{}", contestId);
log.info("查询成果概览,活动ID{}", contestId);
LambdaQueryWrapper<BizContestWork> baseWrapper = new LambdaQueryWrapper<>();
baseWrapper.eq(BizContestWork::getContestId, contestId);
@ -507,7 +507,7 @@ public class ContestResultServiceImpl implements IContestResultService {
BizContest contest = contestMapper.selectById(contestId);
if (contest == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "赛事不存在");
throw BusinessException.of(ErrorCode.NOT_FOUND, "活动不存在");
}
Map<String, Object> contestMap = new LinkedHashMap<>();

View File

@ -62,7 +62,7 @@ public class ContestReviewServiceImpl implements IContestReviewService {
List<Long> desiredOrder = new ArrayList<>(new LinkedHashSet<>(judgeIds));
Set<Long> desired = new HashSet<>(desiredOrder);
log.info("分配作品(同步),赛事ID{}作品ID{},期望评委数:{}", contestId, workId, desired.size());
log.info("分配作品(同步),活动ID{}作品ID{},期望评委数:{}", contestId, workId, desired.size());
// 1. 取消已分配但不在本次列表中的评委仅删除未评分的分配行
LambdaQueryWrapper<BizContestWorkJudgeAssignment> existWrapper = new LambdaQueryWrapper<>();
@ -124,7 +124,7 @@ public class ContestReviewServiceImpl implements IContestReviewService {
@Override
public Map<String, Object> batchAssignWorks(Long contestId, List<Long> workIds, List<Long> judgeIds, Long creatorId) {
log.info("批量分配作品,赛事ID{},作品数:{},评委数:{}", contestId, workIds.size(), judgeIds.size());
log.info("批量分配作品,活动ID{},作品数:{},评委数:{}", contestId, workIds.size(), judgeIds.size());
int totalCreated = 0;
int totalSkipped = 0;
@ -144,7 +144,7 @@ public class ContestReviewServiceImpl implements IContestReviewService {
@Override
public Map<String, Object> autoAssignWorks(Long contestId, Long creatorId) {
log.info("自动分配作品,赛事ID{}", contestId);
log.info("自动分配作品,活动ID{}", contestId);
// 1. 获取所有未分配的作品
LambdaQueryWrapper<BizContestWork> workWrapper = new LambdaQueryWrapper<>();
@ -159,14 +159,14 @@ public class ContestReviewServiceImpl implements IContestReviewService {
throw BusinessException.of(ErrorCode.BAD_REQUEST, "没有需要分配的作品");
}
// 2. 获取赛事所有评委
// 2. 获取活动所有评委
LambdaQueryWrapper<BizContestJudge> judgeWrapper = new LambdaQueryWrapper<>();
judgeWrapper.eq(BizContestJudge::getContestId, contestId);
judgeWrapper.eq(BizContestJudge::getValidState, 1);
List<BizContestJudge> judges = judgeMapper.selectList(judgeWrapper);
if (judges.isEmpty()) {
throw BusinessException.of(ErrorCode.BAD_REQUEST, "赛事尚未配置评委");
throw BusinessException.of(ErrorCode.BAD_REQUEST, "活动尚未配置评委");
}
// 3. 轮询分配每个作品分配给3个评委
@ -303,7 +303,7 @@ public class ContestReviewServiceImpl implements IContestReviewService {
@Override
public List<Map<String, Object>> getAssignedWorks(Long judgeId, Long contestId) {
log.info("查询评委已分配作品评委ID{}赛事ID{}", judgeId, contestId);
log.info("查询评委已分配作品评委ID{}活动ID{}", judgeId, contestId);
LambdaQueryWrapper<BizContestWorkJudgeAssignment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(BizContestWorkJudgeAssignment::getJudgeId, judgeId);
@ -343,7 +343,7 @@ public class ContestReviewServiceImpl implements IContestReviewService {
@Override
public List<Map<String, Object>> getJudgeContests(Long judgeId) {
log.info("查询评委关联赛事评委ID{}", judgeId);
log.info("查询评委关联活动评委ID{}", judgeId);
Set<Long> contestIds = new LinkedHashSet<>();
@ -399,7 +399,7 @@ public class ContestReviewServiceImpl implements IContestReviewService {
@Override
public PageResult<Map<String, Object>> getJudgeContestWorks(Long judgeId, Long contestId, Long page, Long pageSize,
String workNo, String accountNo, String reviewStatus) {
log.info("查询评委赛事作品评委ID{},赛事ID{},页码:{}", judgeId, contestId, page);
log.info("查询评委活动作品评委ID{},活动ID{},页码:{}", judgeId, contestId, page);
// 分页查询分配记录
LambdaQueryWrapper<BizContestWorkJudgeAssignment> wrapper = new LambdaQueryWrapper<>();
@ -511,7 +511,7 @@ public class ContestReviewServiceImpl implements IContestReviewService {
@Override
public Map<String, Object> getReviewProgress(Long contestId) {
log.info("查询评审进度,赛事ID{}", contestId);
log.info("查询评审进度,活动ID{}", contestId);
// 总作品数
LambdaQueryWrapper<BizContestWork> workWrapper = new LambdaQueryWrapper<>();
@ -580,7 +580,7 @@ public class ContestReviewServiceImpl implements IContestReviewService {
@Override
public Map<String, Object> getWorkStatusStats(Long contestId) {
log.info("查询作品状态统计,赛事ID{}", contestId);
log.info("查询作品状态统计,活动ID{}", contestId);
LambdaQueryWrapper<BizContestWork> baseWrapper = new LambdaQueryWrapper<>();
baseWrapper.eq(BizContestWork::getContestId, contestId);
@ -669,7 +669,7 @@ public class ContestReviewServiceImpl implements IContestReviewService {
public Map<String, Object> calculateFinalScore(Long workId) {
log.info("计算作品终分作品ID{}", workId);
// 1. 获取作品赛事评审规则
// 1. 获取作品活动评审规则
BizContestWork work = workMapper.selectById(workId);
if (work == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "作品不存在");
@ -677,7 +677,7 @@ public class ContestReviewServiceImpl implements IContestReviewService {
BizContest contest = contestMapper.selectById(work.getContestId());
if (contest == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "赛事不存在");
throw BusinessException.of(ErrorCode.NOT_FOUND, "活动不存在");
}
String calculationRule = "average"; // 默认
@ -768,11 +768,11 @@ public class ContestReviewServiceImpl implements IContestReviewService {
return result;
}
// ====== 评委赛事详情 ======
// ====== 评委活动详情 ======
@Override
public Map<String, Object> getJudgeContestDetail(Long judgeId, Long contestId) {
log.info("获取评委赛事详情评委ID{},赛事ID{}", judgeId, contestId);
log.info("获取评委活动详情评委ID{},活动ID{}", judgeId, contestId);
// 显式评委 已有作品分配记录
LambdaQueryWrapper<BizContestJudge> judgeWrapper = new LambdaQueryWrapper<>();
@ -785,13 +785,13 @@ public class ContestReviewServiceImpl implements IContestReviewService {
assignCheck.eq(BizContestWorkJudgeAssignment::getContestId, contestId);
assignCheck.eq(BizContestWorkJudgeAssignment::getJudgeId, judgeId);
if (assignmentMapper.selectCount(assignCheck) == 0) {
throw BusinessException.of(ErrorCode.FORBIDDEN, "您不是该赛事的评委");
throw BusinessException.of(ErrorCode.FORBIDDEN, "您不是该活动的评委");
}
}
BizContest contest = contestMapper.selectById(contestId);
if (contest == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "赛事不存在");
throw BusinessException.of(ErrorCode.NOT_FOUND, "活动不存在");
}
Map<String, Object> result = new LinkedHashMap<>();

View File

@ -31,7 +31,7 @@ public class PresetCommentServiceImpl extends ServiceImpl<PresetCommentMapper, B
@Override
public BizPresetComment createComment(CreatePresetCommentDto dto, Long judgeId) {
log.info("创建预设评语评委ID{}赛事ID{}", judgeId, dto.getContestId());
log.info("创建预设评语评委ID{}活动ID{}", judgeId, dto.getContestId());
BizPresetComment entity = new BizPresetComment();
entity.setContestId(dto.getContestId());
@ -48,7 +48,7 @@ public class PresetCommentServiceImpl extends ServiceImpl<PresetCommentMapper, B
@Override
public List<Map<String, Object>> findAll(Long contestId, Long judgeId) {
log.info("查询预设评语列表,赛事ID{}评委ID{}", contestId, judgeId);
log.info("查询预设评语列表,活动ID{}评委ID{}", contestId, judgeId);
LambdaQueryWrapper<BizPresetComment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(BizPresetComment::getValidState, 1);
@ -155,7 +155,7 @@ public class PresetCommentServiceImpl extends ServiceImpl<PresetCommentMapper, B
@Override
public List<Map<String, Object>> getJudgeContests(Long judgeId) {
log.info("查询评委关联赛事预设评语视角评委ID{}", judgeId);
log.info("查询评委关联活动预设评语视角评委ID{}", judgeId);
LambdaQueryWrapper<BizContestJudge> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(BizContestJudge::getJudgeId, judgeId);
@ -166,7 +166,7 @@ public class PresetCommentServiceImpl extends ServiceImpl<PresetCommentMapper, B
return judgeRecords.stream().map(j -> {
Map<String, Object> map = new LinkedHashMap<>();
map.put("id", j.getContestId());
// 查询赛事详情获取名称和状态
// 查询活动详情获取名称和状态
BizContest contest = contestMapper.selectById(j.getContestId());
if (contest != null) {
map.put("contestName", contest.getContestName());
@ -179,9 +179,9 @@ public class PresetCommentServiceImpl extends ServiceImpl<PresetCommentMapper, B
@Override
public Map<String, Object> syncComments(Long sourceContestId, List<Long> targetContestIds, Long judgeId) {
log.info("同步预设评语,源赛事ID{},目标赛事数:{}评委ID{}", sourceContestId, targetContestIds.size(), judgeId);
log.info("同步预设评语,源活动ID{},目标活动数:{}评委ID{}", sourceContestId, targetContestIds.size(), judgeId);
// 查询源赛事的评语
// 查询源活动的评语
LambdaQueryWrapper<BizPresetComment> sourceWrapper = new LambdaQueryWrapper<>();
sourceWrapper.eq(BizPresetComment::getContestId, sourceContestId);
sourceWrapper.eq(BizPresetComment::getJudgeId, judgeId);
@ -189,7 +189,7 @@ public class PresetCommentServiceImpl extends ServiceImpl<PresetCommentMapper, B
List<BizPresetComment> sourceComments = presetCommentMapper.selectList(sourceWrapper);
if (sourceComments.isEmpty()) {
throw BusinessException.of(ErrorCode.BAD_REQUEST, "赛事没有预设评语");
throw BusinessException.of(ErrorCode.BAD_REQUEST, "活动没有预设评语");
}
int created = 0;

View File

@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.competition.common.enums.Visibility;
import com.competition.modules.leai.util.LeaiUtil;
import com.competition.modules.sys.entity.SysUser;
import com.competition.modules.sys.mapper.SysUserMapper;
import com.competition.modules.ugc.entity.UgcWork;
import com.competition.modules.ugc.entity.UgcWorkPage;
import com.competition.modules.ugc.mapper.UgcWorkMapper;
@ -28,6 +30,7 @@ public class LeaiSyncService implements ILeaiSyncService {
private final UgcWorkMapper ugcWorkMapper;
private final UgcWorkPageMapper ugcWorkPageMapper;
private final LeaiApiClient leaiApiClient;
private final SysUserMapper sysUserMapper;
/**
* V4.0 核心同步逻辑
@ -128,7 +131,11 @@ public class LeaiSyncService implements ILeaiSyncService {
Long userId = findUserIdByPhone(phone);
if (userId != null) {
work.setUserId(userId);
} else {
log.warn("通过手机号未找到对应用户,作品将无法关联用户: remoteWorkId={}, phone={}", remoteWorkId, phone);
}
} else if (phone == null) {
log.warn("Webhook回调中手机号为空无法关联用户: remoteWorkId={}", remoteWorkId);
}
ugcWorkMapper.insert(work);
@ -165,6 +172,12 @@ public class LeaiSyncService implements ILeaiSyncService {
if (remoteData.containsKey("progressMessage")) {
wrapper.set(UgcWork::getProgressMessage, LeaiUtil.toString(remoteData.get("progressMessage"), null));
}
// 同步封面图AI创作过程中可能推送 coverUrl
Object coverUrl = remoteData.get("coverUrl");
if (coverUrl == null) coverUrl = remoteData.get("cover_url");
if (coverUrl != null) {
wrapper.set(UgcWork::getCoverUrl, coverUrl.toString());
}
wrapper.set(UgcWork::getModifyTime, LocalDateTime.now());
ugcWorkMapper.update(null, wrapper);
@ -269,11 +282,12 @@ public class LeaiSyncService implements ILeaiSyncService {
/**
* 通过手机号查找用户ID
* 多租户场景需要确定租户后查找
*/
private Long findUserIdByPhone(String phone) {
// TODO: 如果需要按租户隔离需要传入 orgId即租户 code/tenant_code查找租户再查找用户
// 当前简化处理直接通过手机号查用户
return null; // 暂时不自动关联用户后续通过 phone + orgId租户 code查询
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysUser::getPhone, phone)
.last("LIMIT 1");
SysUser user = sysUserMapper.selectOne(wrapper);
return user != null ? user.getId() : null;
}
}

View File

@ -13,6 +13,6 @@ public class PublicRegisterActivityDto {
@Schema(description = "子女IDparticipantType=child时必填")
private Long childId;
@Schema(description = "团队ID团队赛事时填写)")
@Schema(description = "团队ID团队活动时填写)")
private Long teamId;
}

View File

@ -15,6 +15,6 @@ WHERE tenant_id IS NULL;
ALTER TABLE t_biz_contest_notice
ADD INDEX idx_tenant_id (tenant_id);
-- 4. 添加联合索引优化按租户和赛事查询
-- 4. 添加联合索引优化按租户和活动查询
ALTER TABLE t_biz_contest_notice
ADD INDEX idx_tenant_contest (tenant_id, contest_id);

View File

@ -4,12 +4,12 @@
-- 使用 DROP + CREATE 确保表结构完整,因为评审模块尚未有生产数据
-- ============================================================
-- 1. 赛事评委表(对应实体 BizContestJudge继承 BaseEntity
-- 1. 活动评委表(对应实体 BizContestJudge继承 BaseEntity
-- ============================================================
DROP TABLE IF EXISTS t_biz_contest_judge;
CREATE TABLE t_biz_contest_judge (
id BIGINT NOT NULL AUTO_INCREMENT,
contest_id BIGINT NOT NULL COMMENT '赛事ID',
contest_id BIGINT NOT NULL COMMENT '活动ID',
judge_id BIGINT NOT NULL COMMENT '评委用户ID',
specialty VARCHAR(100) DEFAULT NULL COMMENT '专业领域',
weight DECIMAL(3,2) DEFAULT 1.00 COMMENT '评委权重 0-1',
@ -26,7 +26,7 @@ CREATE TABLE t_biz_contest_judge (
PRIMARY KEY (id),
INDEX idx_contest_id (contest_id),
INDEX idx_judge_id (judge_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='赛事评委表';
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动评委表';
-- ============================================================
-- 2. 评审规则表(对应实体 BizContestReviewRule继承 BaseEntity
@ -60,7 +60,7 @@ CREATE TABLE t_biz_contest_review_rule (
DROP TABLE IF EXISTS t_biz_contest_work_judge_assignment;
CREATE TABLE t_biz_contest_work_judge_assignment (
id BIGINT NOT NULL AUTO_INCREMENT,
contest_id BIGINT NOT NULL COMMENT '赛事ID',
contest_id BIGINT NOT NULL COMMENT '活动ID',
work_id BIGINT NOT NULL COMMENT '作品ID',
judge_id BIGINT NOT NULL COMMENT '评委用户ID',
assignment_time DATETIME DEFAULT NULL COMMENT '分配时间',
@ -82,7 +82,7 @@ DROP TABLE IF EXISTS t_biz_contest_work_score;
CREATE TABLE t_biz_contest_work_score (
id BIGINT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT DEFAULT NULL COMMENT '租户ID',
contest_id BIGINT NOT NULL COMMENT '赛事ID',
contest_id BIGINT NOT NULL COMMENT '活动ID',
work_id BIGINT NOT NULL COMMENT '作品ID',
assignment_id BIGINT DEFAULT NULL COMMENT '分配记录ID',
judge_id BIGINT NOT NULL COMMENT '评委用户ID',
@ -113,7 +113,7 @@ CREATE TABLE t_biz_contest_work_score (
DROP TABLE IF EXISTS t_biz_preset_comment;
CREATE TABLE t_biz_preset_comment (
id BIGINT NOT NULL AUTO_INCREMENT,
contest_id BIGINT DEFAULT NULL COMMENT '赛事IDnull 表示全局)',
contest_id BIGINT DEFAULT NULL COMMENT '活动IDnull 表示全局)',
judge_id BIGINT DEFAULT NULL COMMENT '评委IDnull 表示通用)',
content VARCHAR(1000) NOT NULL COMMENT '评语文本',
category VARCHAR(50) DEFAULT NULL COMMENT '评语分类',

View File

@ -8,15 +8,15 @@
-- 一、表级别注释
-- ============================================================
ALTER TABLE t_biz_contest COMMENT='赛事';
ALTER TABLE t_biz_contest_attachment COMMENT='赛事附件表';
ALTER TABLE t_biz_contest_notice COMMENT='赛事公告表';
ALTER TABLE t_biz_contest_registration COMMENT='赛事报名表';
ALTER TABLE t_biz_contest_registration_teacher COMMENT='赛事报名老师关联表';
ALTER TABLE t_biz_contest_team COMMENT='赛事团队表';
ALTER TABLE t_biz_contest_team_member COMMENT='赛事团队成员表';
ALTER TABLE t_biz_contest_work COMMENT='赛事作品表';
ALTER TABLE t_biz_contest_work_attachment COMMENT='赛事作品附件表';
ALTER TABLE t_biz_contest COMMENT='活动';
ALTER TABLE t_biz_contest_attachment COMMENT='活动附件表';
ALTER TABLE t_biz_contest_notice COMMENT='活动公告表';
ALTER TABLE t_biz_contest_registration COMMENT='活动报名表';
ALTER TABLE t_biz_contest_registration_teacher COMMENT='活动报名老师关联表';
ALTER TABLE t_biz_contest_team COMMENT='活动团队表';
ALTER TABLE t_biz_contest_team_member COMMENT='活动团队成员表';
ALTER TABLE t_biz_contest_work COMMENT='活动作品表';
ALTER TABLE t_biz_contest_work_attachment COMMENT='活动作品附件表';
ALTER TABLE t_biz_homework COMMENT='作业表';
ALTER TABLE t_biz_homework_review_rule COMMENT='作业评审规则表';
ALTER TABLE t_biz_homework_score COMMENT='作业评分表';

View File

@ -0,0 +1,24 @@
-- V9: 将数据库表/列 COMMENT 中的旧品牌用词统一改为"活动"
-- 表级 COMMENT 更新
ALTER TABLE t_biz_contest COMMENT='活动表';
ALTER TABLE t_biz_contest_attachment COMMENT='活动附件表';
ALTER TABLE t_biz_contest_notice COMMENT='活动公告表';
ALTER TABLE t_biz_contest_registration COMMENT='活动报名表';
ALTER TABLE t_biz_contest_registration_teacher COMMENT='活动报名老师关联表';
ALTER TABLE t_biz_contest_team COMMENT='活动团队表';
ALTER TABLE t_biz_contest_team_member COMMENT='活动团队成员表';
ALTER TABLE t_biz_contest_work COMMENT='活动作品表';
ALTER TABLE t_biz_contest_work_attachment COMMENT='活动作品附件表';
ALTER TABLE t_biz_contest_judge COMMENT='活动评委表';
ALTER TABLE t_biz_contest_review_rule COMMENT='活动评审规则表';
ALTER TABLE t_biz_contest_work_score COMMENT='活动作品评分表';
ALTER TABLE t_biz_contest_work_judge_assignment COMMENT='活动作品评委分配表';
ALTER TABLE t_biz_preset_comment COMMENT='活动预设评语表';
-- 列级 COMMENT 更新V4 中创建的列)
ALTER TABLE t_biz_contest_judge MODIFY COLUMN contest_id BIGINT NOT NULL COMMENT '活动ID';
-- 注意t_biz_contest_review_rule 表没有 contest_id 列(评审规则是基于 tenant_id 的全局规则),跳过
ALTER TABLE t_biz_contest_work_score MODIFY COLUMN contest_id BIGINT NOT NULL COMMENT '活动ID';
ALTER TABLE t_biz_contest_work_judge_assignment MODIFY COLUMN contest_id BIGINT NOT NULL COMMENT '活动ID';
ALTER TABLE t_biz_preset_comment MODIFY COLUMN contest_id BIGINT DEFAULT NULL COMMENT '活动IDnull 表示全局)';

View File

@ -1,6 +1,4 @@
# 开发环境
VITE_API_BASE_URL=/api
# AI 绘本子项目本地 dev servervite proxy /ai-web → 该地址)
VITE_AI_CLIENT_DEV_URL=http://localhost:3001
# 兼容旧名:与 VITE_AI_CLIENT_DEV_URL 二选一即可,优先前者
VITE_AI_POST_MESSAGE_URL=http://localhost:3001
# 乐读派 AI 创作后端地址(直连,不走 Vite 代理)
VITE_LEAI_API_URL=http://192.168.1.120:8080

View File

@ -1,6 +1,4 @@
# 生产环境
VITE_API_BASE_URL=/api
# 如果后端部署在不同域名,可以改成完整地址:
# VITE_API_BASE_URL=https://api.your-domain.com
VITE_AI_POST_MESSAGE_URL=http://localhost:3001
# 乐读派 AI 创作后端地址(直连)
VITE_LEAI_API_URL=http://192.168.1.120:8080

View File

@ -1,3 +1,5 @@
# 测试环境
VITE_BASE_URL=/web-test/
VITE_API_BASE_URL=/api-test
# 乐读派 AI 创作后端地址(直连)
VITE_LEAI_API_URL=http://192.168.1.120:8080

View File

@ -0,0 +1,79 @@
# Nginx 部署配置指南 — AI 绘本创作模块整合
## 概述
AI 绘本创作模块已整合进主前端。该模块通过 `VITE_LEAI_API_URL` 环境变量**直连乐读派后端**(跨域),不走 Nginx 代理。只需确保乐读派后端允许跨域即可。
## 关键配置
| 项目 | 配置方式 | 说明 |
|------|---------|------|
| 乐读派 API 地址 | `.env.*``VITE_LEAI_API_URL` | 编译时注入,浏览器直连 |
| 乐读派 WebSocket | 自动从 `VITE_LEAI_API_URL` 推导 `ws://` | CreatingView 页面使用 |
| 主后端 API | Nginx 代理 `/api``8580` | 与之前相同 |
## 环境变量配置
在每个环境对应的 `.env.*` 文件中配置乐读派后端地址:
```bash
# .env.development / .env.production / .env.test
VITE_LEAI_API_URL=http://192.168.1.120:8080
```
## 乐读派后端 CORS 配置
由于前端直连乐读派后端(跨域),乐读派后端需要配置 CORS
```yaml
# 乐读派后端 application.yml 需要添加:
allowed-origins:
- http://your-domain.com # 生产域名
- http://localhost:3000 # 本地开发
```
## Nginx 配置示例
```nginx
server {
listen 80;
server_name your-domain.com;
# ─── 主后端 API与之前相同 ───
location /api/ {
proxy_pass http://127.0.0.1:8580/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# ─── 前端静态资源 ───
location /web/ {
alias /path/to/frontend/dist/;
try_files $uri $uri/ /web/index.html;
}
# ─── SPA 回退 ───
location / {
return 301 /web/;
}
}
```
## 开发环境
```bash
# 只需启动一个前端(不再需要 aicreate-client
cd frontend
npm run dev
```
Vite 代理只需配置主后端 `/api → 8580`,乐读派 API 由浏览器直接访问 `VITE_LEAI_API_URL`
## 注意事项
1. **CORS 必须配置**:乐读派后端必须允许前端域名的跨域请求
2. **WebSocket 跨域**Creating 页面的进度推送使用 WebSocket乐读派后端也需要允许 WS 跨域
3. **HTTPS 环境**:如果前端使用 HTTPS`VITE_LEAI_API_URL` 建议也使用 `https://`,否则浏览器会阻止混合内容
4. **修改地址后需重新构建**`VITE_LEAI_API_URL` 是编译时变量,修改后需要 `npm run build`

File diff suppressed because it is too large Load Diff

View File

@ -14,11 +14,14 @@
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@stomp/stompjs": "^7.3.0",
"@vee-validate/zod": "^4.12.4",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"ali-oss": "^6.23.0",
"ant-design-vue": "^4.1.1",
"axios": "^1.6.7",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.10",
"echarts": "^6.0.0",
"pinia": "^2.1.7",
@ -31,6 +34,7 @@
},
"devDependencies": {
"@playwright/test": "^1.59.1",
"@types/crypto-js": "^4.2.2",
"@types/multer": "^2.0.0",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/eslint-config-typescript": "^13.0.0",

View File

@ -1,7 +1,7 @@
/**
* AI 3D API
* competition-management-system-stripped-modules/
*
*
*/
import request from "@/utils/request";
import type { PaginationParams } from "@/types/api";

View File

@ -1302,7 +1302,7 @@ export const reviewsApi = {
return response;
},
// 获取评委视角的赛事详情(含评审规则)
// 获取评委视角的活动详情(含评审规则)
getJudgeContestDetail: async (contestId: number): Promise<any> => {
const response = await request.get<any, any>(
`/contests/reviews/judge/contests/${contestId}/detail`,
@ -1556,7 +1556,7 @@ export const resultsApi = {
// 评委管理
export const judgesApi = {
// 获取赛事评委assigned + implicitPool
// 获取活动评委assigned + implicitPool
getList: async (
contestId: number,
): Promise<ContestJudgesForContestResponse> => {

View File

@ -1,7 +1,7 @@
/**
* API
* competition-management-system-stripped-modules/
*
*
*/
import request from "@/utils/request";
import type { PaginationParams, PaginationResponse } from "@/types/api";

View File

@ -1,7 +1,7 @@
/**
* API
* competition-management-system-stripped-modules/
*
*
*/
import request from "@/utils/request";
import type { PaginationParams, PaginationResponse } from "@/types/api";

View File

@ -63,10 +63,11 @@
<!-- 主内容 -->
<main class="public-main">
<!-- 创作页始终挂载 v-show 控制显隐不移动 DOMiframe 状态保留 -->
<PublicCreate v-if="createMounted" v-show="isCreateRoute" />
<!-- 其他页面正常路由渲染 -->
<router-view v-if="!isCreateRoute" />
<router-view v-slot="{ Component }">
<keep-alive :include="['AiCreateShell']">
<component :is="Component" />
</keep-alive>
</router-view>
</main>
<!-- 移动端底部导航 -->
@ -116,13 +117,14 @@
</template>
<script setup lang="ts">
import { computed, ref, watch } from "vue"
import { computed } from "vue"
import { useRouter, useRoute } from "vue-router"
import { HomeOutlined, UserOutlined, PlusCircleOutlined, AppstoreOutlined, TrophyOutlined } from "@ant-design/icons-vue"
import PublicCreate from "@/views/public/create/Index.vue"
import { useAicreateStore } from "@/stores/aicreate"
const router = useRouter()
const route = useRoute()
const aicreateStore = useAicreateStore()
const isLoggedIn = computed(() => !!localStorage.getItem("public_token"))
const user = computed(() => {
@ -131,26 +133,6 @@ const user = computed(() => {
})
const userAvatar = computed(() => user.value?.avatar || undefined)
// /create/generating/:id
const isCreateRoute = computed(() => route.name === 'PublicCreate')
// 访
const createMounted = ref(false)
//
watch(isCreateRoute, (val) => {
if (val && isLoggedIn.value) {
createMounted.value = true
}
}, { immediate: true })
//
watch(isLoggedIn, (val) => {
if (!val) {
createMounted.value = false
}
})
const currentTab = computed(() => {
const path = route.path
if (path.includes("/mine")) return "mine"
@ -164,7 +146,13 @@ const goHome = () => router.push("/p/gallery")
const goActivity = () => router.push("/p/activities")
const goCreate = () => {
if (!isLoggedIn.value) { router.push("/p/login"); return }
router.push("/p/create")
const saved = aicreateStore.lastCreateRoute
//
if (saved && saved !== '/p/create') {
router.push(saved)
} else {
router.push("/p/create")
}
}
const goWorks = () => {
if (!isLoggedIn.value) { router.push("/p/login"); return }

View File

@ -78,18 +78,70 @@ const baseRoutes: RouteRecordRaw[] = [
component: () => import("@/views/public/mine/Children.vue"),
meta: { title: "子女账号" },
},
// ========== 创作与作品库 ==========
// ========== AI 绘本创作(嵌套路由) ==========
{
path: "create",
name: "PublicCreate",
name: "PublicCreateShell",
component: () => import("@/views/public/create/Index.vue"),
meta: { title: "绘本创作" },
},
{
path: "create/generating/:id",
name: "PublicCreating",
component: () => import("@/views/public/create/Generating.vue"),
meta: { title: "生成中" },
children: [
{
path: "",
name: "PublicCreateWelcome",
component: () => import("@/views/public/create/views/WelcomeView.vue"),
},
{
path: "upload",
name: "PublicCreateUpload",
component: () => import("@/views/public/create/views/UploadView.vue"),
},
{
path: "characters",
name: "PublicCreateCharacters",
component: () => import("@/views/public/create/views/CharactersView.vue"),
},
{
path: "style",
name: "PublicCreateStyle",
component: () => import("@/views/public/create/views/StyleSelectView.vue"),
},
{
path: "story",
name: "PublicCreateStory",
component: () => import("@/views/public/create/views/StoryInputView.vue"),
},
{
path: "creating",
name: "PublicCreateCreating",
component: () => import("@/views/public/create/views/CreatingView.vue"),
},
{
path: "preview/:workId?",
name: "PublicCreatePreview",
component: () => import("@/views/public/create/views/PreviewView.vue"),
},
{
path: "edit-info/:workId",
name: "PublicCreateEditInfo",
component: () => import("@/views/public/create/views/EditInfoView.vue"),
},
{
path: "save-success/:workId",
name: "PublicCreateSaveSuccess",
component: () => import("@/views/public/create/views/SaveSuccessView.vue"),
},
{
path: "dubbing/:workId",
name: "PublicCreateDubbing",
component: () => import("@/views/public/create/views/DubbingView.vue"),
},
{
path: "read/:workId",
name: "PublicCreateRead",
component: () => import("@/views/public/create/views/BookReaderView.vue"),
meta: { noAuth: true },
},
],
},
{
path: "works",

View File

@ -25,6 +25,9 @@ export const useAicreateStore = defineStore('aicreate', () => {
const workDetail = ref<any>(null)
const authRedirectUrl = ref('')
// ─── Tab 切换状态保存 ───
const lastCreateRoute = ref('')
// ─── 方法 ───
function setPhone(val: string) {
phone.value = val
@ -51,6 +54,14 @@ export const useAicreateStore = defineStore('aicreate', () => {
sessionStorage.removeItem('le_sessionToken')
}
function setLastCreateRoute(path: string) {
lastCreateRoute.value = path
}
function clearLastCreateRoute() {
lastCreateRoute.value = ''
}
function reset() {
imageUrl.value = ''
extractId.value = ''
@ -60,6 +71,7 @@ export const useAicreateStore = defineStore('aicreate', () => {
storyData.value = null
workId.value = ''
workDetail.value = null
lastCreateRoute.value = ''
localStorage.removeItem('le_workId')
}
@ -104,5 +116,7 @@ export const useAicreateStore = defineStore('aicreate', () => {
imageUrl, extractId, characters, selectedCharacter,
selectedStyle, storyData, workId, workDetail,
reset, saveRecoveryState, restoreRecoveryState,
// Tab 切换状态
lastCreateRoute, setLastCreateRoute, clearLastCreateRoute,
}
})

View File

@ -1,3 +1,7 @@
<script lang="ts">
export default { name: 'AiCreateShell' }
</script>
<template>
<div class="ai-create-shell">
<!-- 初始化加载 -->
@ -12,22 +16,33 @@
<!-- 子路由渲染 -->
<router-view v-else v-slot="{ Component }">
<transition name="ai-slide" mode="out-in">
<component :is="Component" />
<keep-alive>
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import { message } from 'ant-design-vue'
import { leaiApi } from '@/api/public'
import { useAicreateStore } from '@/stores/aicreate'
const store = useAicreateStore()
const route = useRoute()
const loading = ref(true)
const loadError = ref('')
// store
watch(() => route.path, (path) => {
if (path.startsWith('/p/create')) {
store.setLastCreateRoute(path)
}
}, { immediate: true })
/** 获取乐读派 Token 并存入 store */
const initToken = async () => {
loading.value = true
@ -60,11 +75,15 @@ onMounted(() => {
<style lang="scss">
// 使 scoped aicreate.scss .ai-create-shell
// PublicLayout public-main
// tabbar
.public-main:has(> .ai-create-shell) {
padding: 0 !important;
padding-bottom: 0 !important;
padding-left: 0 !important;
padding-right: 0 !important;
padding-top: 0 !important;
// padding-bottom tabbar 40px / 80px
max-width: 430px;
overflow: hidden;
background: var(--ai-bg, #FFFDF7);
}
.ai-create-shell {

View File

@ -16,14 +16,6 @@ const getBase = (mode: string) => {
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
// vite.config 顶层无法从 .env 读取 VITE_*,需在回调内 loadEnv
const env = loadEnv(mode, process.cwd(), "");
// AI 绘本子项目 Vite 开发服务地址(主站 /ai-web 代理到此地址,需与 lesingle-aicreate-client 端口一致)
const aiProxyTarget =
env.VITE_AI_CLIENT_DEV_URL ||
env.VITE_AI_POST_MESSAGE_URL ||
"http://localhost:3001";
return {
base: getBase(mode),
plugins: [vue()],
@ -36,15 +28,10 @@ export default defineConfig(({ mode }) => {
host: "0.0.0.0",
port: 3000,
proxy: {
// 主后端
"/api": {
target: "http://localhost:8580",
changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, ''),
},
"/ai-web": {
target: aiProxyTarget,
changeOrigin: true,
// 子项目 base 为 /ai-web/,不重写路径,直接转发到子 dev server
},
},
},

View File

@ -183,7 +183,7 @@ export function checkQuota() {
return api.post('/query/validate', {
orgId: store.orgId,
phone: store.phone,
type: 'A'
apiType: 'A3'
})
}