diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventController.java index 33f410a..0e86a79 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventController.java +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventController.java @@ -3,6 +3,7 @@ package com.imyeyu.api.modules.gao.controller; import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; import com.imyeyu.api.modules.gao.entity.GaoEvent; +import com.imyeyu.api.modules.gao.entity.GaoEventOptionGroup; import com.imyeyu.api.modules.gao.service.GaoEventService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.java.TimiJava; @@ -49,6 +50,7 @@ public class GaoEventController { for (GaoEvent event : result.getList()) { event.setRoleCodeList(roleMap.get(event.getId())); } + service.attachOptionGroupList(result.getList()); return result; } @@ -69,6 +71,7 @@ public class GaoEventController { TimiException.required(event, "not found event"); storeScopeService.checkStoreId(event.getStoreId()); event.setRoleCodeList(service.listRoleCode(event.getId())); + service.attachOptionGroupList(List.of(event)); return event; } @@ -95,6 +98,17 @@ public class GaoEventController { service.update(event); } + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.EVENT_UPDATE) + @PostMapping("/options/update") + public void updateOptions(@RequestParam String id, @RequestBody List optionGroupList) { + GaoEvent event = service.get(id); + TimiException.required(event, "not found event"); + storeScopeService.checkStoreId(event.getStoreId()); + service.updateOptionGroupList(id, optionGroupList); + } + @AOPLog @RequestRateLimit @RequireGaoPermission(GaoPermissionCode.EVENT_UPDATE) diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventRecordController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventRecordController.java index 46f0fa8..4361af7 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventRecordController.java +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventRecordController.java @@ -16,6 +16,7 @@ import com.imyeyu.api.modules.gao.vo.GaoCustomerEventRankView; import com.imyeyu.api.modules.gao.vo.GaoEventRecordPage; import com.imyeyu.api.modules.gao.vo.GaoEventRecordPageResult; import com.imyeyu.api.modules.gao.vo.GaoEventRecordRankPage; +import com.imyeyu.api.modules.gao.vo.GaoEventOptionStatView; import com.imyeyu.api.modules.user.service.UserService; import com.imyeyu.java.TimiJava; import com.imyeyu.java.bean.timi.TimiException; @@ -78,6 +79,7 @@ public class GaoEventRecordController { record.setCustomer(customerMap.get(record.getCustomerId())); record.getCustomer().setAttachmentList(attachMap.get(record.getCustomerId())); } + service.attachSelectedOptionGroupList(result.getList()); } return result; } @@ -110,11 +112,13 @@ public class GaoEventRecordController { record.setCustomer(customerService.get(record.getCustomerId())); record.getCustomer().setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_CUSTOMER, record.getCustomerId())); record.setEvent(eventService.get(record.getEventId())); + eventService.attachOptionGroupList(List.of(record.getEvent())); record.setOperator(userService.get(record.getOperatorUserId())); if (record.getVerifierUserId() != null) { record.setVerifier(userService.get(record.getVerifierUserId())); } record.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_EVENT_RECORD, record.getId())); + service.attachSelectedOptionGroupList(List.of(record)); return record; } @@ -234,6 +238,16 @@ public class GaoEventRecordController { return result; } + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/option/stat") + public List optionStat(@RequestBody GaoEventRecordPage page) { + TimiException.required(page, "not found page"); + page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); + return service.optionStatByRange(page); + } + private void resolveCreateRecordStore(GaoEventRecord record) { TimiException.required(record, "not found event record"); GaoEvent event = eventService.get(record.getEventId()); diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java index 28ff496..1eb6102 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java @@ -138,4 +138,8 @@ public class GaoEvent extends UUIDEntity { /// 可登记角色代码列表 @Transient private List roleCodeList; + + /// 登记事件选项组列表 + @Transient + private List optionGroupList; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventOption.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventOption.java new file mode 100644 index 0000000..4eb6963 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventOption.java @@ -0,0 +1,23 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// 登记事件选项 +/// +/// @author Codex +/// @since 2026-08-30 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoEventOption extends UUIDEntity { + + /// 选项组 ID + private String groupId; + + /// 选项名称 + private String label; + + /// 排序值 + private Integer sort; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventOptionGroup.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventOptionGroup.java new file mode 100644 index 0000000..e99b5fc --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventOptionGroup.java @@ -0,0 +1,46 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.spring.annotation.table.Transient; +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/// 登记事件选项组 +/// +/// @author Codex +/// @since 2026-08-30 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoEventOptionGroup extends UUIDEntity { + + /// 选项选择模式 + public enum SelectionMode { + + /// 只能选择一个选项 + SINGLE, + + /// 可以选择多个选项 + MULTIPLE + } + + /// 登记事件 ID + private String eventId; + + /// 选项组名称 + private String name; + + /// 选择模式 + private SelectionMode selectionMode; + + /// true 为登记时必须选择至少一个选项 + private Boolean required; + + /// 排序值 + private Integer sort; + + /// 选项列表 + @Transient + private List optionList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecord.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecord.java index 964dffc..410ce81 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecord.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecord.java @@ -105,4 +105,8 @@ public class GaoEventRecord extends UUIDEntity { /// 客户 ID 列表,批量登记时使用 @Transient private List customerIdList; + + /// 登记记录已选择的选项组列表 + @Transient + private List selectedOptionGroupList; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecordOption.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecordOption.java new file mode 100644 index 0000000..5be51cf --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecordOption.java @@ -0,0 +1,35 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// 登记事件记录选项关系 +/// +/// @author Codex +/// @since 2026-08-30 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoEventRecordOption extends UUIDEntity { + + /// 登记事件记录 ID + private String eventRecordId; + + /// 选项组 ID + private String groupId; + + /// 选项 ID + private String optionId; + + /// 选项组排序快照 + private Integer groupSort; + + /// 选项排序快照 + private Integer optionSort; + + /// 选项组名称快照 + private String groupNameSnapshot; + + /// 选项名称快照 + private String optionLabelSnapshot; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecordOptionGroup.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecordOptionGroup.java new file mode 100644 index 0000000..2102cd9 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecordOptionGroup.java @@ -0,0 +1,22 @@ +package com.imyeyu.api.modules.gao.entity; + +import lombok.Data; + +import java.util.List; + +/// 登记事件记录选项组视图 +/// +/// @author Codex +/// @since 2026-08-30 +@Data +public class GaoEventRecordOptionGroup { + + /// 选项组 ID + private String groupId; + + /// 选项组名称 + private String groupName; + + /// 已选择的选项列表 + private List selectedOptionList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventOptionGroupMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventOptionGroupMapper.java new file mode 100644 index 0000000..2c34d03 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventOptionGroupMapper.java @@ -0,0 +1,17 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoEventOptionGroup; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/// 登记事件选项组 Mapper +/// +/// @author Codex +/// @since 2026-08-30 +public interface GaoEventOptionGroupMapper extends BaseMapper { + + List selectAllByEventIdList(@Param("eventIdList") Collection eventIdList); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventOptionMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventOptionMapper.java new file mode 100644 index 0000000..ab51d07 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventOptionMapper.java @@ -0,0 +1,17 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoEventOption; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/// 登记事件选项 Mapper +/// +/// @author Codex +/// @since 2026-08-30 +public interface GaoEventOptionMapper extends BaseMapper { + + List selectAllByGroupIdList(@Param("groupIdList") Collection groupIdList); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java index 6527854..b829bbd 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java @@ -8,6 +8,7 @@ import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView; import com.imyeyu.api.modules.gao.vo.GaoEventRecordNumberValueDailyStatView; import com.imyeyu.api.modules.gao.vo.GaoEventRecordPage; import com.imyeyu.api.modules.gao.vo.GaoEventRecordRankPage; +import com.imyeyu.api.modules.gao.vo.GaoEventOptionStatView; import com.imyeyu.api.modules.gao.vo.GaoStoreChartReq; import com.imyeyu.spring.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; @@ -79,6 +80,12 @@ public interface GaoEventRecordMapper extends BaseMapper /// @return 登记数值合计 Long sumNumberValueByRange(GaoEventRecordPage page); + /// 按选项统计登记记录数量 + /// + /// @param page 登记记录范围查询请求 + /// @return 选项统计列表 + List selectOptionStatByRange(GaoEventRecordPage page); + /// 统计客户登记排行榜总数 /// /// @param page 排行榜分页请求 diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordOptionMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordOptionMapper.java new file mode 100644 index 0000000..452178f --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordOptionMapper.java @@ -0,0 +1,17 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoEventRecordOption; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/// 登记事件记录选项 Mapper +/// +/// @author Codex +/// @since 2026-08-30 +public interface GaoEventRecordOptionMapper extends BaseMapper { + + List selectAllByEventRecordIdList(@Param("eventRecordIdList") Collection eventRecordIdList); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java index 3cb8326..7db0e8a 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java @@ -2,6 +2,7 @@ package com.imyeyu.api.modules.gao.service; import com.imyeyu.api.modules.gao.entity.GaoCustomer; import com.imyeyu.api.modules.gao.entity.GaoEventRecord; +import com.imyeyu.api.modules.gao.vo.GaoEventOptionStatView; import com.imyeyu.api.modules.gao.vo.GaoCustomerEventRankView; import com.imyeyu.api.modules.gao.vo.GaoEventRecordPage; import com.imyeyu.api.modules.gao.vo.GaoEventRecordRankPage; @@ -24,6 +25,11 @@ public interface GaoEventRecordService extends BaseService listByCustomerId(String customerId); + /// 为登记记录附加已选择的选项组 + /// + /// @param recordList 登记记录列表 + void attachSelectedOptionGroupList(List recordList); + /// 核销登记事件记录 /// /// @param id 登记记录 ID @@ -49,6 +55,12 @@ public interface GaoEventRecordService extends BaseService optionStatByRange(GaoEventRecordPage req); + /// 按时间范围查询客户登记排行榜 /// /// @param page 范围查询请求,不传时间为全部时间 diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventService.java index c008fb9..8e4feb2 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventService.java @@ -1,6 +1,7 @@ package com.imyeyu.api.modules.gao.service; import com.imyeyu.api.modules.gao.entity.GaoEvent; +import com.imyeyu.api.modules.gao.entity.GaoEventOptionGroup; import com.imyeyu.spring.service.BaseService; import java.util.Collection; @@ -24,6 +25,17 @@ public interface GaoEventService extends BaseService { /// @return 登记事件列表 List listValid(String storeId); + /// 为登记事件附加选项组和选项 + /// + /// @param eventList 登记事件列表 + void attachOptionGroupList(Collection eventList); + + /// 更新登记事件的选项组配置 + /// + /// @param eventId 登记事件 ID + /// @param optionGroupList 选项组配置列表 + void updateOptionGroupList(String eventId, List optionGroupList); + /// 删除门店下的全部登记事件及角色关联 /// /// @param storeId 门店 ID diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java index 56c1e65..478f8d2 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java @@ -7,8 +7,13 @@ import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.service.AttachmentService; import com.imyeyu.api.modules.gao.entity.GaoCustomer; import com.imyeyu.api.modules.gao.entity.GaoEvent; +import com.imyeyu.api.modules.gao.entity.GaoEventOption; +import com.imyeyu.api.modules.gao.entity.GaoEventOptionGroup; import com.imyeyu.api.modules.gao.entity.GaoEventRecord; +import com.imyeyu.api.modules.gao.entity.GaoEventRecordOption; +import com.imyeyu.api.modules.gao.entity.GaoEventRecordOptionGroup; import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper; +import com.imyeyu.api.modules.gao.mapper.GaoEventRecordOptionMapper; import com.imyeyu.api.modules.gao.service.GaoCustomerService; import com.imyeyu.api.modules.gao.service.GaoEventRecordService; import com.imyeyu.api.modules.gao.service.GaoEventService; @@ -16,6 +21,7 @@ import com.imyeyu.api.modules.gao.service.GaoPointTriggerService; import com.imyeyu.api.modules.gao.service.GaoStoreBusinessDayService; import com.imyeyu.api.modules.gao.util.EventRecordXSSFBuilder; import com.imyeyu.api.modules.gao.vo.GaoCustomerEventRankView; +import com.imyeyu.api.modules.gao.vo.GaoEventOptionStatView; import com.imyeyu.api.modules.gao.vo.GaoEventRecordPage; import com.imyeyu.api.modules.gao.vo.GaoEventRecordRankPage; import com.imyeyu.api.modules.system.entity.Logger; @@ -44,7 +50,9 @@ import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAdjusters; import java.util.ArrayList; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; /// 登记事件记录服务实现 @@ -69,6 +77,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService recordList) { + List recordIdList = new ArrayList<>(); + for (GaoEventRecord record : TimiJava.safeIterable(recordList)) { + if (record != null && TimiJava.isNotEmpty(record.getId())) { + recordIdList.add(record.getId()); + } + } + if (recordIdList.isEmpty()) { + return; + } + + Map> groupMap = new LinkedHashMap<>(); + for (GaoEventRecordOption relation : recordOptionMapper.selectAllByEventRecordIdList(recordIdList)) { + Map recordGroupMap = groupMap.computeIfAbsent(relation.getEventRecordId(), key -> new LinkedHashMap<>()); + GaoEventRecordOptionGroup group = recordGroupMap.computeIfAbsent(relation.getGroupId(), key -> { + GaoEventRecordOptionGroup result = new GaoEventRecordOptionGroup(); + result.setGroupId(relation.getGroupId()); + result.setGroupName(relation.getGroupNameSnapshot()); + result.setSelectedOptionList(new ArrayList<>()); + return result; + }); + GaoEventOption option = new GaoEventOption(); + option.setId(relation.getOptionId()); + option.setLabel(relation.getOptionLabelSnapshot()); + option.setSort(relation.getOptionSort()); + group.getSelectedOptionList().add(option); + } + for (GaoEventRecord record : TimiJava.safeIterable(recordList)) { + if (record != null) { + record.setSelectedOptionGroupList(new ArrayList<>(groupMap.getOrDefault(record.getId(), Map.of()).values())); + } + } + } + @Transactional(TimiServerDBConfig.ROLLBACKER) @Override public void verify(String id) { @@ -127,6 +171,8 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService canonicalOptionList = resolveRecordOptionList(event, record.getSelectedOptionGroupList()); verifyRecordValue(event, record.getNumberValue()); verifyRecordAttachment(event, record.getAttachmentList()); // 检查权限 @@ -166,6 +212,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService canonicalOptionList = null; + if (record.getSelectedOptionGroupList() != null) { + eventService.attachOptionGroupList(List.of(event)); + canonicalOptionList = resolveRecordOptionList(event, record.getSelectedOptionGroupList()); + } verifyRecordAttachment(event, record.getAttachmentList()); checkEventPermission(event); dbRecord.setRemark(record.getRemark()); super.update(dbRecord); + if (canonicalOptionList != null) { + GaoEventRecordOption optionExample = new GaoEventRecordOption(); + optionExample.setEventRecordId(record.getId()); + recordOptionMapper.deleteAllByExample(optionExample); + saveRecordOptionList(record.getId(), canonicalOptionList); + } attachmentService.updateByBizId(Attachment.BizType.GAO_EVENT_RECORD, record.getId(), record.getAttachmentList()); logger.setLevel(Logger.Level.INFO); logger.setResult(record.getId()); @@ -269,6 +327,9 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService optionStatByRange(GaoEventRecordPage page) { + TimiException.required(page.getStoreId(), "not found page.storeId"); + return mapper.selectOptionStatByRange(page); + } + @Override public byte[] export(GaoEventRecordPage page) throws IOException { TimiException.required(page.getStoreId(), "not found page.storeId"); @@ -432,6 +499,71 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService resolveRecordOptionList(GaoEvent event, List selectedGroupList) { + List groupList = event.getOptionGroupList(); + Map groupMap = new LinkedHashMap<>(); + for (GaoEventOptionGroup group : TimiJava.safeIterable(groupList)) { + groupMap.put(group.getId(), group); + } + Map selectedMap = new LinkedHashMap<>(); + for (GaoEventRecordOptionGroup selectedGroup : TimiJava.safeIterable(selectedGroupList)) { + TimiException.required(selectedGroup, "登记记录选项组不能为空"); + String groupId = selectedGroup.getGroupId(); + GaoEventOptionGroup group = groupMap.get(groupId); + TimiException.required(group, "登记记录选项组不存在或不属于当前事件"); + TimiException.requiredTrue(selectedMap.put(groupId, selectedGroup) == null, "登记记录选项组不能重复"); + } + + List result = new ArrayList<>(); + for (GaoEventOptionGroup group : TimiJava.safeIterable(groupList)) { + GaoEventRecordOptionGroup selectedGroup = selectedMap.get(group.getId()); + List selectedOptionList = selectedGroup == null ? List.of() : TimiJava.defaultIfNull(selectedGroup.getSelectedOptionList(), List.of()); + if (Boolean.TRUE.equals(group.getRequired())) { + TimiException.requiredTrue(!selectedOptionList.isEmpty(), "请至少选择%s中的一个选项".formatted(group.getName())); + } + if (group.getSelectionMode() == GaoEventOptionGroup.SelectionMode.SINGLE) { + TimiException.requiredTrue(selectedOptionList.size() < 2, "%s只能选择一个选项".formatted(group.getName())); + } + Map optionMap = new LinkedHashMap<>(); + for (GaoEventOption option : TimiJava.safeIterable(group.getOptionList())) { + optionMap.put(option.getId(), option); + } + Set selectedOptionIdSet = new HashSet<>(); + int optionSort = 0; + int groupSort = groupList.indexOf(group); + for (GaoEventOption selectedOption : selectedOptionList) { + TimiException.required(selectedOption, "登记记录选项不能为空"); + GaoEventOption option = optionMap.get(selectedOption.getId()); + TimiException.required(option, "登记记录选项不存在或不属于当前选项组"); + TimiException.requiredTrue(selectedOptionIdSet.add(option.getId()), "登记记录选项不能重复"); + GaoEventRecordOption recordOption = new GaoEventRecordOption(); + recordOption.setGroupId(group.getId()); + recordOption.setOptionId(option.getId()); + recordOption.setGroupSort(groupSort); + recordOption.setOptionSort(option.getSort() == null ? optionSort : option.getSort()); + recordOption.setGroupNameSnapshot(group.getName()); + recordOption.setOptionLabelSnapshot(option.getLabel()); + result.add(recordOption); + optionSort++; + } + } + return result; + } + + private void saveRecordOptionList(String eventRecordId, List optionList) { + for (GaoEventRecordOption option : TimiJava.safeIterable(optionList)) { + GaoEventRecordOption recordOption = new GaoEventRecordOption(); + recordOption.setEventRecordId(eventRecordId); + recordOption.setGroupId(option.getGroupId()); + recordOption.setOptionId(option.getOptionId()); + recordOption.setGroupSort(option.getGroupSort()); + recordOption.setOptionSort(option.getOptionSort()); + recordOption.setGroupNameSnapshot(option.getGroupNameSnapshot()); + recordOption.setOptionLabelSnapshot(option.getOptionLabelSnapshot()); + recordOptionMapper.insert(recordOption); + } + } + private void verifyRecordAttachment(GaoEvent event, List attachmentList) { if (!Boolean.TRUE.equals(event.getRequirePhoto())) { return; diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventServiceImplement.java index ac4a5b9..4bc546a 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventServiceImplement.java @@ -3,8 +3,12 @@ package com.imyeyu.api.modules.gao.service.implement; import com.fasterxml.jackson.databind.ObjectMapper; import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.modules.gao.entity.GaoEvent; +import com.imyeyu.api.modules.gao.entity.GaoEventOption; +import com.imyeyu.api.modules.gao.entity.GaoEventOptionGroup; import com.imyeyu.api.modules.gao.entity.GaoEventRoleRelation; import com.imyeyu.api.modules.gao.mapper.GaoEventMapper; +import com.imyeyu.api.modules.gao.mapper.GaoEventOptionGroupMapper; +import com.imyeyu.api.modules.gao.mapper.GaoEventOptionMapper; import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper; import com.imyeyu.api.modules.gao.mapper.GaoEventRoleRelationMapper; import com.imyeyu.api.modules.gao.service.GaoEventService; @@ -23,6 +27,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; @@ -46,6 +51,8 @@ public class GaoEventServiceImplement extends AbstractEntityService optionGroupList) { + Logger logger = new Logger(Logger.Module.GAO, "GAO_EVENT_OPTION_UPDATE"); + try { + logger.setContent(jackson.writeValueAsString(optionGroupList)); + GaoEvent event = get(eventId); + TimiException.required(event, "not found event"); + verifyOptionGroupList(optionGroupList); + saveOptionGroupList(eventId, optionGroupList); + logger.setLevel(Logger.Level.INFO); + logger.setResult(eventId); + } catch (TimiException e) { + logger.setLevel(Logger.Level.WARN); + logger.setException(e.getMessage()); + throw e; + } catch (Exception e) { + logger.setLevel(Logger.Level.ERROR); + logger.setException(TimiJava.serializeThrowable(e)); + log.error("update event option error", e); + throw new TimiException(TimiCode.ERROR, "update event option error", e); + } finally { + loggerService.create(logger); + } + } + @Transactional(TimiServerDBConfig.ROLLBACKER) @Override public void delete(String id) { @@ -203,7 +243,36 @@ public class GaoEventServiceImplement extends AbstractEntityService listValid(String storeId) { TimiException.required(storeId, "not found storeId"); - return mapper.selectValidList(storeId, Time.now()); + List eventList = mapper.selectValidList(storeId, Time.now()); + attachOptionGroupList(eventList); + return eventList; + } + + @Override + public void attachOptionGroupList(Collection eventList) { + List validEventList = new ArrayList<>(); + for (GaoEvent event : TimiJava.safeIterable(eventList)) { + if (event != null && TimiJava.isNotEmpty(event.getId())) { + validEventList.add(event); + } + } + if (validEventList.isEmpty()) { + return; + } + + List eventIdList = validEventList.stream().map(GaoEvent::getId).toList(); + List groupList = optionGroupMapper.selectAllByEventIdList(eventIdList); + List groupIdList = groupList.stream().map(GaoEventOptionGroup::getId).filter(TimiJava::isNotEmpty).toList(); + List optionList = optionMapper.selectAllByGroupIdList(groupIdList); + Map> optionMap = optionList.stream().collect(Collectors.groupingBy(GaoEventOption::getGroupId)); + Map> groupMap = new HashMap<>(); + for (GaoEventOptionGroup group : groupList) { + group.setOptionList(optionMap.getOrDefault(group.getId(), List.of())); + groupMap.computeIfAbsent(group.getEventId(), key -> new ArrayList<>()).add(group); + } + for (GaoEvent event : validEventList) { + event.setOptionGroupList(groupMap.getOrDefault(event.getId(), List.of())); + } } @Override @@ -242,6 +311,106 @@ public class GaoEventServiceImplement extends AbstractEntityService groupList) { + Set groupIdSet = new HashSet<>(); + Set groupNameSet = new HashSet<>(); + for (GaoEventOptionGroup group : TimiJava.safeIterable(groupList)) { + TimiException.required(group, "登记事件选项组不能为空"); + String groupName = group.getName() == null ? "" : group.getName().trim(); + TimiException.requiredTrue(!groupName.isEmpty(), "登记事件选项组名称不能为空"); + TimiException.requiredTrue(groupNameSet.add(groupName), "登记事件选项组名称不能重复"); + if (TimiJava.isNotEmpty(group.getId())) { + TimiException.requiredTrue(groupIdSet.add(group.getId()), "登记事件选项组 ID 不能重复"); + } + TimiException.required(group.getSelectionMode(), "登记事件选项组必须配置选择模式"); + List optionList = group.getOptionList(); + TimiException.requiredTrue(optionList != null && !optionList.isEmpty(), "登记事件选项组至少配置一个选项"); + Set optionIdSet = new HashSet<>(); + Set optionNameSet = new HashSet<>(); + for (GaoEventOption option : TimiJava.safeIterable(optionList)) { + TimiException.required(option, "登记事件选项不能为空"); + String label = option.getLabel() == null ? "" : option.getLabel().trim(); + TimiException.requiredTrue(!label.isEmpty(), "登记事件选项名称不能为空"); + TimiException.requiredTrue(optionNameSet.add(label), "同一选项组内选项名称不能重复"); + if (TimiJava.isNotEmpty(option.getId())) { + TimiException.requiredTrue(optionIdSet.add(option.getId()), "登记事件选项 ID 不能重复"); + } + } + } + } + + private void saveOptionGroupList(String eventId, List groupList) { + List dbGroupList = optionGroupMapper.selectAllByEventIdList(List.of(eventId)); + Map dbGroupMap = dbGroupList.stream().collect(Collectors.toMap(GaoEventOptionGroup::getId, Function.identity())); + Set retainedGroupIdSet = new HashSet<>(); + int groupSort = 0; + for (GaoEventOptionGroup group : TimiJava.safeIterable(groupList)) { + String groupId = group.getId(); + GaoEventOptionGroup dbGroup; + if (TimiJava.isNotEmpty(groupId)) { + dbGroup = dbGroupMap.get(groupId); + TimiException.required(dbGroup, "登记事件选项组不属于当前事件"); + dbGroup.setName(group.getName().trim()); + dbGroup.setSelectionMode(group.getSelectionMode()); + dbGroup.setRequired(TimiJava.defaultIfNull(group.getRequired(), false)); + dbGroup.setSort(groupSort); + optionGroupMapper.update(dbGroup); + } else { + dbGroup = group; + dbGroup.setEventId(eventId); + dbGroup.setName(group.getName().trim()); + dbGroup.setRequired(TimiJava.defaultIfNull(group.getRequired(), false)); + dbGroup.setSort(groupSort); + optionGroupMapper.insert(dbGroup); + } + groupId = dbGroup.getId(); + retainedGroupIdSet.add(groupId); + saveOptionList(groupId, group.getOptionList()); + groupSort++; + } + for (GaoEventOptionGroup dbGroup : dbGroupList) { + if (retainedGroupIdSet.contains(dbGroup.getId())) { + continue; + } + GaoEventOption optionExample = new GaoEventOption(); + optionExample.setGroupId(dbGroup.getId()); + optionMapper.deleteAllByExample(optionExample); + optionGroupMapper.delete(dbGroup.getId()); + } + } + + private void saveOptionList(String groupId, List optionList) { + List dbOptionList = optionMapper.selectAllByGroupIdList(List.of(groupId)); + Map dbOptionMap = dbOptionList.stream().collect(Collectors.toMap(GaoEventOption::getId, Function.identity())); + Set retainedOptionIdSet = new HashSet<>(); + int optionSort = 0; + for (GaoEventOption option : TimiJava.safeIterable(optionList)) { + String optionId = option.getId(); + GaoEventOption dbOption; + if (TimiJava.isNotEmpty(optionId)) { + dbOption = dbOptionMap.get(optionId); + TimiException.required(dbOption, "登记事件选项不属于当前选项组"); + dbOption.setLabel(option.getLabel().trim()); + dbOption.setSort(optionSort); + optionMapper.update(dbOption); + } else { + dbOption = option; + dbOption.setGroupId(groupId); + dbOption.setLabel(option.getLabel().trim()); + dbOption.setSort(optionSort); + optionMapper.insert(dbOption); + } + retainedOptionIdSet.add(dbOption.getId()); + optionSort++; + } + for (GaoEventOption dbOption : dbOptionList) { + if (!retainedOptionIdSet.contains(dbOption.getId())) { + optionMapper.delete(dbOption.getId()); + } + } } private void verifyValueDefinitionChange(GaoEvent event, GaoEvent.ValueType oldValueType, GaoEvent.ValueUnit oldValueUnit) { diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventOptionStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventOptionStatView.java new file mode 100644 index 0000000..10c1427 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventOptionStatView.java @@ -0,0 +1,26 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// 登记事件选项统计视图 +/// +/// @author Codex +/// @since 2026-08-30 +@Data +public class GaoEventOptionStatView { + + /// 选项组 ID + private String groupId; + + /// 选项组名称 + private String groupName; + + /// 选项 ID + private String optionId; + + /// 选项名称 + private String optionLabel; + + /// 登记记录数量 + private Long recordCount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventRecordPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventRecordPage.java index 96aa1b7..8ba90b1 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventRecordPage.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventRecordPage.java @@ -25,6 +25,9 @@ public class GaoEventRecordPage extends BasePage { /// 登记事件 ID 列表,为空查询全部登记事件 private List eventIdList; + /// 必须同时包含的选项 ID 列表 + private List optionIdList; + /// 核销状态,为空查询全部状态 private GaoEventRecord.VerificationStatus verificationStatus; diff --git a/src/main/resources/db/migration/timiserver/V46__create_gao_event_option_tables.sql b/src/main/resources/db/migration/timiserver/V46__create_gao_event_option_tables.sql new file mode 100644 index 0000000..32b677e --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V46__create_gao_event_option_tables.sql @@ -0,0 +1,43 @@ +CREATE TABLE IF NOT EXISTS `gao_event_option_group` ( + `id` VARCHAR(36) NOT NULL, + `event_id` VARCHAR(36) NOT NULL COMMENT '登记事件 ID', + `name` VARCHAR(64) NOT NULL COMMENT '选项组名称', + `selection_mode` VARCHAR(16) NOT NULL COMMENT '选择模式,SINGLE 单选,MULTIPLE 多选', + `required` BIT(1) NOT NULL DEFAULT b'0' COMMENT 'true 为登记时必须选择至少一个选项', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + KEY `idx_gao_event_option_group_event` (`event_id`, `deleted_at`, `sort`, `created_at`) +) COMMENT='登记事件选项组'; + +CREATE TABLE IF NOT EXISTS `gao_event_option` ( + `id` VARCHAR(36) NOT NULL, + `group_id` VARCHAR(36) NOT NULL COMMENT '选项组 ID', + `label` VARCHAR(128) NOT NULL COMMENT '选项名称', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + KEY `idx_gao_event_option_group` (`group_id`, `deleted_at`, `sort`, `created_at`) +) COMMENT='登记事件选项'; + +CREATE TABLE IF NOT EXISTS `gao_event_record_option` ( + `id` VARCHAR(36) NOT NULL, + `event_record_id` VARCHAR(36) NOT NULL COMMENT '登记事件记录 ID', + `group_id` VARCHAR(36) NOT NULL COMMENT '选项组 ID', + `option_id` VARCHAR(36) NOT NULL COMMENT '选项 ID', + `group_sort` INT NOT NULL DEFAULT 0 COMMENT '选项组排序快照', + `option_sort` INT NOT NULL DEFAULT 0 COMMENT '选项排序快照', + `group_name_snapshot` VARCHAR(64) NOT NULL COMMENT '选项组名称快照', + `option_label_snapshot` VARCHAR(128) NOT NULL COMMENT '选项名称快照', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_event_record_option` (`event_record_id`, `option_id`, `deleted_at`), + KEY `idx_gao_event_record_option_option` (`option_id`, `deleted_at`, `event_record_id`), + KEY `idx_gao_event_record_option_group` (`group_id`, `deleted_at`, `event_record_id`) +) COMMENT='登记事件记录选项'; diff --git a/src/main/resources/mapper/timi-server/gao/GaoEventOptionGroupMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoEventOptionGroupMapper.xml new file mode 100644 index 0000000..3181221 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoEventOptionGroupMapper.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoEventOptionMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoEventOptionMapper.xml new file mode 100644 index 0000000..cb8f9ed --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoEventOptionMapper.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml index 1a1b425..f8b7fd9 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml @@ -118,6 +118,17 @@ #{eventId} + + + AND EXISTS ( + SELECT 1 + FROM `gao_event_record_option` fro + WHERE fro.`event_record_id` = r.`id` + AND fro.`option_id` = #{optionId} + AND fro.`deleted_at` IS NULL + ) + + AND r.`verification_status` = #{verificationStatus} @@ -154,6 +165,27 @@ AND r.`number_value` IS NOT NULL + r.`deleted_at` IS NULL diff --git a/src/main/resources/mapper/timi-server/gao/GaoEventRecordOptionMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoEventRecordOptionMapper.xml new file mode 100644 index 0000000..dd94780 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoEventRecordOptionMapper.xml @@ -0,0 +1,22 @@ + + + + +