diff --git a/pom.xml b/pom.xml
index 79c07ea..9ed94da 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,7 +11,7 @@
com.imyeyu.timiserverapi
TimiServerAPI
- 1.0.27
+ 1.0.28
jar
TimiServerAPI
imyeyu.com API
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 @@
+
+
+
+
+