fix: 修复联系人默认唯一性问题 & 添加单位统计API

- 问题1: 设置默认联系人时自动取消其他默认联系人
- 问题3: 新增 /organization-stats API 按单位统计运动员、项目、金额

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
2026-01-08 16:08:31 +08:00
parent 742272026b
commit c40ca5b35b
13 changed files with 302 additions and 12 deletions

View File

@@ -14,8 +14,6 @@ import org.springblade.modules.martial.pojo.entity.MartialContact;
import org.springblade.modules.martial.service.IMartialContactService;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
@Slf4j
@RestController
@AllArgsConstructor
@@ -43,16 +41,10 @@ public class MartialContactController extends BladeController {
@Operation(summary = "保存", description = "新增或修改联系人")
public R<Boolean> submit(@RequestBody MartialContact contact) {
Long userId = AuthUtil.getUserId();
log.info("Contact submit - id: {}, name: {}, userId: {}", contact.getId(), contact.getName(), userId);
log.info("Contact submit - id: {}, name: {}, userId: {}, isDefault: {}",
contact.getId(), contact.getName(), userId, contact.getIsDefault());
if (contact.getId() == null) {
contact.setCreateUser(userId);
contact.setCreateTime(new Date());
}
contact.setUpdateUser(userId);
contact.setUpdateTime(new Date());
return R.data(contactService.saveOrUpdate(contact));
return R.data(contactService.saveContact(contact, userId));
}
@PostMapping("/remove")

View File

@@ -13,21 +13,26 @@ import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.modules.martial.pojo.entity.MartialAthlete;
import org.springblade.modules.martial.pojo.entity.MartialCompetition;
import org.springblade.modules.martial.pojo.entity.MartialProject;
import org.springblade.modules.martial.pojo.entity.MartialRegistrationOrder;
import org.springblade.modules.martial.pojo.entity.MartialTeam;
import org.springblade.modules.martial.pojo.entity.MartialTeamMember;
import org.springblade.modules.martial.pojo.dto.RegistrationSubmitDTO;
import org.springblade.modules.martial.pojo.vo.MartialRegistrationOrderVO;
import org.springblade.modules.martial.pojo.vo.OrganizationStatsVO;
import org.springblade.modules.martial.service.IMartialAthleteService;
import org.springblade.modules.martial.service.IMartialCompetitionService;
import org.springblade.modules.martial.service.IMartialProjectService;
import org.springblade.modules.martial.service.IMartialRegistrationOrderService;
import org.springblade.modules.martial.service.IMartialTeamService;
import org.springblade.modules.martial.mapper.MartialTeamMemberMapper;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@RestController
@@ -40,6 +45,7 @@ public class MartialRegistrationOrderController extends BladeController {
private final IMartialAthleteService athleteService;
private final IMartialTeamService teamService;
private final IMartialCompetitionService competitionService;
private final IMartialProjectService projectService;
private final MartialTeamMemberMapper teamMemberMapper;
@GetMapping("/detail")
@@ -60,6 +66,192 @@ public class MartialRegistrationOrderController extends BladeController {
return R.data(pages);
}
@GetMapping("/organization-stats")
@Operation(summary = "单位统计", description = "按单位统计运动员、项目、金额")
public R<List<OrganizationStatsVO>> getOrganizationStats(@RequestParam Long competitionId) {
log.info("获取单位统计: competitionId={}", competitionId);
// 1. Get all athletes for this competition
LambdaQueryWrapper<MartialAthlete> athleteWrapper = new LambdaQueryWrapper<>();
athleteWrapper.eq(MartialAthlete::getCompetitionId, competitionId)
.eq(MartialAthlete::getIsDeleted, 0);
List<MartialAthlete> athletes = athleteService.list(athleteWrapper);
if (athletes.isEmpty()) {
return R.data(new ArrayList<>());
}
// 2. Get all projects for this competition
Set<Long> projectIds = athletes.stream()
.map(MartialAthlete::getProjectId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
final Map<Long, MartialProject> projectMap = new HashMap<>();
if (!projectIds.isEmpty()) {
List<MartialProject> projects = projectService.listByIds(projectIds);
projectMap.putAll(projects.stream().collect(Collectors.toMap(MartialProject::getId, p -> p)));
}
// 3. Get team members for team projects
Set<Long> teamIds = athletes.stream()
.filter(a -> {
MartialProject project = projectMap.get(a.getProjectId());
return project != null && project.getType() != null && project.getType() == 2;
})
.map(a -> {
// Try to get team ID from team table by team name
LambdaQueryWrapper<MartialTeam> teamWrapper = new LambdaQueryWrapper<>();
teamWrapper.eq(MartialTeam::getTeamName, a.getTeamName())
.eq(MartialTeam::getIsDeleted, 0)
.last("LIMIT 1");
MartialTeam team = teamService.getOne(teamWrapper, false);
return team != null ? team.getId() : null;
})
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// Get team members
Map<Long, List<MartialTeamMember>> teamMembersMap = new HashMap<>();
if (!teamIds.isEmpty()) {
LambdaQueryWrapper<MartialTeamMember> memberWrapper = new LambdaQueryWrapper<>();
memberWrapper.in(MartialTeamMember::getTeamId, teamIds)
.eq(MartialTeamMember::getIsDeleted, 0);
List<MartialTeamMember> members = teamMemberMapper.selectList(memberWrapper);
teamMembersMap = members.stream().collect(Collectors.groupingBy(MartialTeamMember::getTeamId));
}
// 4. Group by organization and calculate stats
Map<String, OrganizationStatsVO> orgStatsMap = new LinkedHashMap<>();
for (MartialAthlete athlete : athletes) {
String org = athlete.getOrganization();
if (org == null || org.isEmpty()) {
org = "未知单位";
}
OrganizationStatsVO stats = orgStatsMap.computeIfAbsent(org, k -> {
OrganizationStatsVO vo = new OrganizationStatsVO();
vo.setOrganization(k);
vo.setAthleteCount(0);
vo.setProjectCount(0);
vo.setSingleProjectCount(0);
vo.setTeamProjectCount(0);
vo.setMaleCount(0);
vo.setFemaleCount(0);
vo.setTotalAmount(BigDecimal.ZERO);
vo.setProjectAmounts(new ArrayList<>());
return vo;
});
MartialProject project = projectMap.get(athlete.getProjectId());
if (project == null) continue;
// Check if project already counted for this org
boolean projectExists = stats.getProjectAmounts().stream()
.anyMatch(pa -> pa.getProjectId().equals(athlete.getProjectId()));
if (!projectExists) {
// Add project amount item
OrganizationStatsVO.ProjectAmountItem item = new OrganizationStatsVO.ProjectAmountItem();
item.setProjectId(project.getId());
item.setProjectName(project.getProjectName());
item.setProjectType(project.getType());
item.setCount(1);
item.setPrice(project.getPrice() != null ? project.getPrice() : BigDecimal.ZERO);
item.setAmount(item.getPrice());
stats.getProjectAmounts().add(item);
stats.setProjectCount(stats.getProjectCount() + 1);
if (project.getType() != null && project.getType() == 2) {
stats.setTeamProjectCount(stats.getTeamProjectCount() + 1);
} else {
stats.setSingleProjectCount(stats.getSingleProjectCount() + 1);
}
} else {
// Update count for existing project
stats.getProjectAmounts().stream()
.filter(pa -> pa.getProjectId().equals(athlete.getProjectId()))
.findFirst()
.ifPresent(pa -> {
pa.setCount(pa.getCount() + 1);
pa.setAmount(pa.getPrice().multiply(BigDecimal.valueOf(pa.getCount())));
});
}
}
// 5. Calculate unique athletes and gender counts per organization
for (Map.Entry<String, OrganizationStatsVO> entry : orgStatsMap.entrySet()) {
String org = entry.getKey();
OrganizationStatsVO stats = entry.getValue();
// Get all athletes for this org
Set<String> uniqueIdCards = new HashSet<>();
int maleCount = 0;
int femaleCount = 0;
for (MartialAthlete athlete : athletes) {
String athleteOrg = athlete.getOrganization();
if (athleteOrg == null || athleteOrg.isEmpty()) athleteOrg = "未知单位";
if (!athleteOrg.equals(org)) continue;
MartialProject project = projectMap.get(athlete.getProjectId());
if (project == null) continue;
// For individual projects, count the athlete
if (project.getType() == null || project.getType() == 1) {
String idCard = athlete.getIdCard();
if (idCard != null && !idCard.isEmpty() && !uniqueIdCards.contains(idCard)) {
uniqueIdCards.add(idCard);
if (athlete.getGender() != null && athlete.getGender() == 1) {
maleCount++;
} else if (athlete.getGender() != null && athlete.getGender() == 2) {
femaleCount++;
}
}
} else {
// For team projects, count team members
String teamName = athlete.getTeamName();
if (teamName != null) {
LambdaQueryWrapper<MartialTeam> teamWrapper = new LambdaQueryWrapper<>();
teamWrapper.eq(MartialTeam::getTeamName, teamName)
.eq(MartialTeam::getIsDeleted, 0)
.last("LIMIT 1");
MartialTeam team = teamService.getOne(teamWrapper, false);
if (team != null && teamMembersMap.containsKey(team.getId())) {
for (MartialTeamMember member : teamMembersMap.get(team.getId())) {
MartialAthlete memberAthlete = athleteService.getById(member.getAthleteId());
if (memberAthlete != null) {
String idCard = memberAthlete.getIdCard();
if (idCard != null && !idCard.isEmpty() && !uniqueIdCards.contains(idCard)) {
uniqueIdCards.add(idCard);
if (memberAthlete.getGender() != null && memberAthlete.getGender() == 1) {
maleCount++;
} else if (memberAthlete.getGender() != null && memberAthlete.getGender() == 2) {
femaleCount++;
}
}
}
}
}
}
}
}
stats.setAthleteCount(uniqueIdCards.size());
stats.setMaleCount(maleCount);
stats.setFemaleCount(femaleCount);
// Calculate total amount
BigDecimal totalAmount = stats.getProjectAmounts().stream()
.map(OrganizationStatsVO.ProjectAmountItem::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
stats.setTotalAmount(totalAmount);
}
return R.data(new ArrayList<>(orgStatsMap.values()));
}
@PostMapping("/submit")
@Operation(summary = "提交报名", description = "提交报名订单并关联选手或集体")
@Transactional(rollbackFor = Exception.class)

View File

@@ -0,0 +1,69 @@
package org.springblade.modules.martial.pojo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
* Organization Statistics VO
*/
@Data
@Schema(description = "单位统计视图对象")
public class OrganizationStatsVO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "单位名称")
private String organization;
@Schema(description = "运动员人数(去重)")
private Integer athleteCount;
@Schema(description = "项目数量")
private Integer projectCount;
@Schema(description = "单人项目数")
private Integer singleProjectCount;
@Schema(description = "集体项目数")
private Integer teamProjectCount;
@Schema(description = "男运动员数")
private Integer maleCount;
@Schema(description = "女运动员数")
private Integer femaleCount;
@Schema(description = "总金额")
private BigDecimal totalAmount;
@Schema(description = "项目金额明细")
private List<ProjectAmountItem> projectAmounts;
@Data
@Schema(description = "项目金额明细")
public static class ProjectAmountItem implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "项目ID")
private Long projectId;
@Schema(description = "项目名称")
private String projectName;
@Schema(description = "项目类型(1=单人,2=集体)")
private Integer projectType;
@Schema(description = "报名人数/集体数")
private Integer count;
@Schema(description = "单价")
private BigDecimal price;
@Schema(description = "小计金额")
private BigDecimal amount;
}
}

View File

@@ -19,4 +19,9 @@ public interface IMartialContactService extends IService<MartialContact> {
*/
MartialContact getContactDetail(Long id);
/**
* Save contact with default uniqueness handling
*/
boolean saveContact(MartialContact contact, Long userId);
}

View File

@@ -1,6 +1,7 @@
package org.springblade.modules.martial.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -10,6 +11,9 @@ import org.springblade.modules.martial.mapper.MartialContactMapper;
import org.springblade.modules.martial.pojo.entity.MartialContact;
import org.springblade.modules.martial.service.IMartialContactService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
/**
* Contact Service Implementation
@@ -35,4 +39,32 @@ public class MartialContactServiceImpl extends ServiceImpl<MartialContactMapper,
return this.getById(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean saveContact(MartialContact contact, Long userId) {
// If setting as default, clear other defaults first
if (Boolean.TRUE.equals(contact.getIsDefault())) {
LambdaUpdateWrapper<MartialContact> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(MartialContact::getCreateUser, userId)
.eq(MartialContact::getIsDeleted, 0)
.set(MartialContact::getIsDefault, false);
// Exclude current contact if it's an update
if (contact.getId() != null) {
updateWrapper.ne(MartialContact::getId, contact.getId());
}
this.update(updateWrapper);
log.info("Cleared default status for user {}'s other contacts", userId);
}
// Set audit fields
if (contact.getId() == null) {
contact.setCreateUser(userId);
contact.setCreateTime(new Date());
}
contact.setUpdateUser(userId);
contact.setUpdateTime(new Date());
return this.saveOrUpdate(contact);
}
}