diff --git a/pom.xml b/pom.xml
index 3cc388d..e49dd2e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -215,6 +215,11 @@
tinypinyin
2.0.3
+
+ com.hankcs
+ hanlp
+ portable-1.8.4
+
com.imyeyu.font.icon
timi-icon-font
diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoCustomerChartController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoCustomerChartController.java
index e738d60..7f54169 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoCustomerChartController.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoCustomerChartController.java
@@ -5,6 +5,8 @@ import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.service.GaoCustomerChartService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseWordReq;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseWordView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
import com.imyeyu.java.bean.timi.TimiException;
@@ -51,4 +53,15 @@ public class GaoCustomerChartController {
req.setStoreId(storeScopeService.resolveStoreId(req.getStoreId()));
return service.eventRecordCalendar(req);
}
+
+ /// 查询客户病症词频
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.STAT_READ)
+ @PostMapping("/customer/disease/word")
+ public List customerDiseaseWord(@RequestBody GaoCustomerDiseaseWordReq req) {
+ TimiException.required(req, "not found req");
+ req.setStoreId(storeScopeService.resolveStoreId(req.getStoreId()));
+ return service.customerDiseaseWord(req);
+ }
}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreChartController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreChartController.java
index ab17bd6..f8f32d9 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreChartController.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreChartController.java
@@ -4,6 +4,7 @@ import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.service.GaoStoreChartService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerAgeStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
@@ -54,6 +55,15 @@ public class GaoStoreChartController {
return service.customerGender(storeScopeService.resolveStoreId(storeId));
}
+ /// 查询客户年龄分布
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.STAT_READ)
+ @PostMapping("/customer/age")
+ public List customerAge(@RequestParam String storeId) {
+ return service.customerAge(storeScopeService.resolveStoreId(storeId));
+ }
+
/// 查询登记事件每日统计,可同时支撑登记趋势和事件结构图表
@AOPLog
@RequestRateLimit
diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerMapper.java
index 118e40c..e0d1e35 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerMapper.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerMapper.java
@@ -1,7 +1,9 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerAgeStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseTextView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPage;
import com.imyeyu.spring.mapper.BaseMapper;
@@ -38,6 +40,18 @@ public interface GaoCustomerMapper extends BaseMapper {
/// @return 性别统计列表
List stateGender(@Param("storeId") String storeId);
+ /// 统计客户年龄分布
+ ///
+ /// @param storeId 门店 ID
+ /// @return 客户年龄列表
+ List stateAge(@Param("storeId") String storeId);
+
+ /// 查询客户病症原始文本
+ ///
+ /// @param storeId 门店 ID
+ /// @return 客户备注和病症
+ List selectDiseaseTextByStoreId(@Param("storeId") String storeId);
+
/// 查询客户总数
///
/// @param page 查询条件
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerChartService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerChartService.java
index 252a50f..84dbb5b 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerChartService.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerChartService.java
@@ -1,6 +1,8 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseWordReq;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseWordView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
@@ -17,4 +19,7 @@ public interface GaoCustomerChartService {
/// 查询客户登记日历记录
List eventRecordCalendar(GaoCustomerChartReq req);
+
+ /// 查询客户病症词频
+ List customerDiseaseWord(GaoCustomerDiseaseWordReq req);
}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerService.java
index 85a77dc..aec9311 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerService.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerService.java
@@ -1,6 +1,7 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerAgeStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPage;
@@ -87,4 +88,10 @@ public interface GaoCustomerService extends BaseService {
/// @param storeId 门店 ID
/// @return 性别统计列表
List stateGender(String storeId);
+
+ /// 统计客户年龄分布
+ ///
+ /// @param storeId 门店 ID
+ /// @return 客户年龄列表
+ List stateAge(String storeId);
}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoStoreChartService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoStoreChartService.java
index cc2e6e1..1cc6f73 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoStoreChartService.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoStoreChartService.java
@@ -1,6 +1,7 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerAgeStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
@@ -21,6 +22,9 @@ public interface GaoStoreChartService {
/// 查询客户性别统计
List customerGender(String storeId);
+ /// 查询客户年龄分布
+ List customerAge(String storeId);
+
/// 查询登记事件每日统计
List eventRecordDailyStat(GaoStoreChartReq req);
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerChartServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerChartServiceImplement.java
index f1341d0..6f30c4f 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerChartServiceImplement.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerChartServiceImplement.java
@@ -1,15 +1,23 @@
package com.imyeyu.api.modules.gao.service.implement;
+import com.hankcs.hanlp.seg.common.Term;
+import com.hankcs.hanlp.tokenizer.StandardTokenizer;
+import com.imyeyu.api.modules.gao.mapper.GaoCustomerMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerChartService;
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseTextView;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseWordReq;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseWordView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
import com.imyeyu.java.bean.timi.TimiException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
/// 客户报表服务实现
///
@@ -20,6 +28,7 @@ import java.util.List;
public class GaoCustomerChartServiceImplement implements GaoCustomerChartService {
private final GaoEventRecordService eventRecordService;
+ private final GaoCustomerMapper customerMapper;
@Override
public List eventRecordDailyStat(GaoCustomerChartReq req) {
@@ -33,6 +42,42 @@ public class GaoCustomerChartServiceImplement implements GaoCustomerChartService
return eventRecordService.calendarByCustomerRange(req);
}
+ @Override
+ public List customerDiseaseWord(GaoCustomerDiseaseWordReq req) {
+ TimiException.required(req, "not found req");
+ TimiException.required(req.getStoreId(), "not found req.storeId");
+
+ int topN = normalizeTopN(req.getTopN());
+ Map wordCountMap = new HashMap<>();
+ for (GaoCustomerDiseaseTextView customer : customerMapper.selectDiseaseTextByStoreId(req.getStoreId())) {
+ countNounWord(customer.getDisease(), wordCountMap);
+ countNounWord(customer.getRemark(), wordCountMap);
+ }
+ return wordCountMap.entrySet().stream()
+ .sorted(Map.Entry.comparingByValue().reversed().thenComparing(Map.Entry.comparingByKey()))
+ .limit(topN)
+ .map(item -> new GaoCustomerDiseaseWordView(item.getKey(), item.getValue()))
+ .toList();
+ }
+
+ private void countNounWord(String text, Map wordCountMap) {
+ if (text == null || text.isBlank()) {
+ return;
+ }
+ for (Term term : StandardTokenizer.segment(text)) {
+ if (term.nature != null && term.nature.toString().startsWith("n")) {
+ wordCountMap.merge(term.word, 1L, Long::sum);
+ }
+ }
+ }
+
+ private int normalizeTopN(Integer topN) {
+ if (topN == null) {
+ return 60;
+ }
+ return Math.clamp(topN, 1, 100);
+ }
+
private void checkRangeReq(GaoCustomerChartReq req) {
TimiException.required(req, "not found req");
TimiException.required(req.getStoreId(), "not found req.storeId");
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerServiceImplement.java
index d070e4d..e60a960 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerServiceImplement.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerServiceImplement.java
@@ -14,6 +14,7 @@ import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
import com.imyeyu.api.modules.gao.service.GaoPointAccountService;
import com.imyeyu.api.modules.gao.util.CustomerXSSFBuilder;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerAgeStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPage;
@@ -431,6 +432,12 @@ public class GaoCustomerServiceImplement extends AbstractEntityService stateAge(String storeId) {
+ TimiException.required(storeId, "not found storeId");
+ return mapper.stateAge(storeId);
+ }
+
private void changeInvitedCount(String customerId, long delta) {
if (TimiJava.isEmpty(customerId) || delta == 0) {
return;
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreChartServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreChartServiceImplement.java
index 4406f60..666a3d2 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreChartServiceImplement.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreChartServiceImplement.java
@@ -3,6 +3,7 @@ package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
import com.imyeyu.api.modules.gao.service.GaoStoreChartService;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerAgeStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerTrendStateReq;
@@ -43,6 +44,12 @@ public class GaoStoreChartServiceImplement implements GaoStoreChartService {
return customerService.stateGender(storeId);
}
+ @Override
+ public List customerAge(String storeId) {
+ TimiException.required(storeId, "not found storeId");
+ return customerService.stateAge(storeId);
+ }
+
@Override
public List eventRecordDailyStat(GaoStoreChartReq req) {
checkRangeReq(req);
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerAgeStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerAgeStatView.java
new file mode 100644
index 0000000..3d42c52
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerAgeStatView.java
@@ -0,0 +1,18 @@
+package com.imyeyu.api.modules.gao.vo;
+
+import lombok.Data;
+
+///
+/// 客户年龄统计视图
+///
+/// @author Codex
+/// @since 2026-09-11
+@Data
+public class GaoCustomerAgeStatView {
+
+ /// 客户年龄
+ private Long age;
+
+ /// 该年龄的客户数量
+ private Long count;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerDiseaseTextView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerDiseaseTextView.java
new file mode 100644
index 0000000..798fdf7
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerDiseaseTextView.java
@@ -0,0 +1,19 @@
+package com.imyeyu.api.modules.gao.vo;
+
+import lombok.Data;
+
+/// 客户病症原始文本
+///
+/// 只用于后端分词,不对外返回。
+///
+/// @author Codex
+/// @since 2026-09-11
+@Data
+public class GaoCustomerDiseaseTextView {
+
+ /// 备注
+ private String remark;
+
+ /// 病症
+ private String disease;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerDiseaseWordReq.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerDiseaseWordReq.java
new file mode 100644
index 0000000..965ccf0
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerDiseaseWordReq.java
@@ -0,0 +1,17 @@
+package com.imyeyu.api.modules.gao.vo;
+
+import lombok.Data;
+
+/// 客户病症词频查询请求
+///
+/// @author Codex
+/// @since 2026-09-11
+@Data
+public class GaoCustomerDiseaseWordReq {
+
+ /// 门店 ID
+ private String storeId;
+
+ /// 返回词语数量
+ private Integer topN;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerDiseaseWordView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerDiseaseWordView.java
new file mode 100644
index 0000000..7459aca
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerDiseaseWordView.java
@@ -0,0 +1,19 @@
+package com.imyeyu.api.modules.gao.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+/// 客户病症词频
+///
+/// @author Codex
+/// @since 2026-09-11
+@Data
+@AllArgsConstructor
+public class GaoCustomerDiseaseWordView {
+
+ /// 词语
+ private String word;
+
+ /// 词语出现次数
+ private Long value;
+}
diff --git a/src/main/resources/mapper/timi-server/gao/GaoCustomerMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoCustomerMapper.xml
index 1b95a6d..67b701d 100644
--- a/src/main/resources/mapper/timi-server/gao/GaoCustomerMapper.xml
+++ b/src/main/resources/mapper/timi-server/gao/GaoCustomerMapper.xml
@@ -112,4 +112,31 @@
AND `deleted_at` IS NULL
GROUP BY IFNULL(CAST(`gender` AS CHAR), 'UNKNOWN')
+
+
+
+