|
|
@@ -33,6 +33,9 @@ public class AnalyticsService {
|
|
|
this.repo = repo;
|
|
|
}
|
|
|
|
|
|
+ /** 默认来源系统:当前业务系统 */
|
|
|
+ private static final String DEFAULT_SYSTEM = "AI药典";
|
|
|
+
|
|
|
@Async("analyticsExecutor")
|
|
|
public void saveEvent(
|
|
|
final String userKey,
|
|
|
@@ -41,7 +44,8 @@ public class AnalyticsService {
|
|
|
final String pageUrl,
|
|
|
final String referrer,
|
|
|
final String ip,
|
|
|
- final String userAgent) {
|
|
|
+ final String userAgent,
|
|
|
+ final String systemSource) {
|
|
|
try {
|
|
|
// Truncate data to avoid sql errors when user-agent is longer than the column, etc.
|
|
|
UserEvent event = UserEvent.builder()
|
|
|
@@ -52,6 +56,7 @@ public class AnalyticsService {
|
|
|
.referrer(trunc(referrer, 512))
|
|
|
.ip(trunc(ip, 64))
|
|
|
.userAgent(trunc(userAgent, 512))
|
|
|
+ .systemSource(trunc(normalizeSystem(systemSource), 64))
|
|
|
.build();
|
|
|
repo.save(event);
|
|
|
} catch (Exception e) {
|
|
|
@@ -59,6 +64,34 @@ public class AnalyticsService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /** 兼容旧调用方,默认归入 AI药典 系统 */
|
|
|
+ public void saveEvent(
|
|
|
+ final String userKey,
|
|
|
+ final String eventType,
|
|
|
+ @SuppressWarnings("rawtypes") final Map eventData,
|
|
|
+ final String pageUrl,
|
|
|
+ final String referrer,
|
|
|
+ final String ip,
|
|
|
+ final String userAgent) {
|
|
|
+ saveEvent(userKey, eventType, eventData, pageUrl, referrer, ip, userAgent, DEFAULT_SYSTEM);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String normalizeSystem(String systemSource) {
|
|
|
+ return (systemSource == null || systemSource.isBlank()) ? DEFAULT_SYSTEM : systemSource;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询用系统过滤归一化:
|
|
|
+ * null/空 → 默认 AI药典(只看本系统);
|
|
|
+ * "all"(大小写无关)→ null,表示跨系统全量统计;
|
|
|
+ * 其它值 → 原样返回,按该系统过滤(如 "中医")。
|
|
|
+ */
|
|
|
+ private static String normalizeSystemFilter(String systemSource) {
|
|
|
+ if (systemSource == null || systemSource.isBlank()) return DEFAULT_SYSTEM;
|
|
|
+ if ("all".equalsIgnoreCase(systemSource)) return null;
|
|
|
+ return systemSource;
|
|
|
+ }
|
|
|
+
|
|
|
private static String trunc(String s, int max) {
|
|
|
if (s == null) return null;
|
|
|
return s.length() <= max ? s : s.substring(0, max);
|
|
|
@@ -66,14 +99,15 @@ public class AnalyticsService {
|
|
|
|
|
|
// ==================== 埋点事件列表 ====================
|
|
|
|
|
|
- public Map<String, Object> queryEvents(String eventType, String startDate, String endDate,
|
|
|
+ public Map<String, Object> queryEvents(String eventType, String systemSource,
|
|
|
+ String startDate, String endDate,
|
|
|
int page, int pageSize) {
|
|
|
Instant startTime = parseDate(startDate) != null ? parseDate(startDate) : Instant.EPOCH;
|
|
|
Instant endTime = endDate != null && !endDate.isBlank()
|
|
|
? LocalDate.parse(endDate).plusDays(1).atStartOfDay(ZoneOffset.UTC).toInstant()
|
|
|
: FAR_FUTURE;
|
|
|
|
|
|
- Specification<UserEvent> spec = buildTimeSpec(eventType, startTime, endTime);
|
|
|
+ Specification<UserEvent> spec = buildTimeSpec(eventType, normalizeSystemFilter(systemSource), startTime, endTime);
|
|
|
var pageable = PageRequest.of(page - 1, pageSize, Sort.by(Sort.Direction.DESC, "createdAt"));
|
|
|
var result = repo.findAll(spec, pageable);
|
|
|
|
|
|
@@ -81,6 +115,7 @@ public class AnalyticsService {
|
|
|
.map(e -> {
|
|
|
Map<String, Object> m = new LinkedHashMap<>();
|
|
|
m.put("id", e.getId());
|
|
|
+ m.put("system", e.getSystemSource());
|
|
|
m.put("event_type", e.getEventType());
|
|
|
m.put("user_key", e.getUserKey());
|
|
|
m.put("page_url", e.getPageUrl());
|
|
|
@@ -104,6 +139,7 @@ public class AnalyticsService {
|
|
|
var e = opt.get();
|
|
|
Map<String, Object> m = new LinkedHashMap<>();
|
|
|
m.put("id", e.getId());
|
|
|
+ m.put("system", e.getSystemSource());
|
|
|
m.put("user_key", e.getUserKey());
|
|
|
m.put("event_type", e.getEventType());
|
|
|
m.put("event_data", e.getEventData());
|
|
|
@@ -120,13 +156,16 @@ public class AnalyticsService {
|
|
|
/**
|
|
|
* 统计概览
|
|
|
* @param range today | yesterday | week | total | custom
|
|
|
+ * @param systemSource 来源系统:null/空 → 默认 AI药典;"all" → 跨系统全量;其它值 → 按该系统过滤
|
|
|
* @param startDate custom 的起始日期(yyyy-MM-dd),仅 range=custom 时生效
|
|
|
* @param endDate custom 的结束日期(yyyy-MM-dd),仅 range=custom 时生效
|
|
|
*/
|
|
|
- public Map<String, Object> queryOverview(String range, String startDate, String endDate) {
|
|
|
+ public Map<String, Object> queryOverview(String range, String systemSource,
|
|
|
+ String startDate, String endDate) {
|
|
|
if (range == null || range.isBlank()) {
|
|
|
range = "total";
|
|
|
}
|
|
|
+ String systemFilter = normalizeSystemFilter(systemSource);
|
|
|
Instant todayStart = parseDate(LocalDate.now().toString());
|
|
|
Instant todayEnd = LocalDate.now().plusDays(1).atStartOfDay(ZoneOffset.UTC).toInstant();
|
|
|
|
|
|
@@ -167,45 +206,49 @@ public class AnalyticsService {
|
|
|
|
|
|
Map<String, Object> overview = new LinkedHashMap<>();
|
|
|
overview.put("range", label);
|
|
|
- overview.put("total_events", repo.countTotal(startTime, endTime));
|
|
|
- overview.put("active_users", repo.countDistinctUsers(startTime, endTime));
|
|
|
- overview.put("unique_ips", repo.countDistinctIps(startTime, endTime));
|
|
|
- overview.put("page_views", repo.countByType("page_view", startTime, endTime));
|
|
|
- overview.put("searches", repo.countByType("search", startTime, endTime));
|
|
|
- overview.put("ai_qa", repo.countByType("chat", startTime, endTime));
|
|
|
+ overview.put("system", systemFilter != null ? systemFilter : "all");
|
|
|
+ overview.put("total_events", repo.countTotal(startTime, endTime, systemFilter));
|
|
|
+ overview.put("active_users", repo.countDistinctUsers(startTime, endTime, systemFilter));
|
|
|
+ overview.put("unique_ips", repo.countDistinctIps(startTime, endTime, systemFilter));
|
|
|
+ overview.put("page_views", repo.countByType("page_view", startTime, endTime, systemFilter));
|
|
|
+ overview.put("searches", repo.countByType("search", startTime, endTime, systemFilter));
|
|
|
+ overview.put("ai_qa", repo.countByType("chat", startTime, endTime, systemFilter));
|
|
|
return overview;
|
|
|
}
|
|
|
|
|
|
// ==================== 维度分页统计 ====================
|
|
|
|
|
|
- /** 分页维度统计:ip / url / type / user / user_ip */
|
|
|
- public Map<String, Object> queryDimension(String dimension, String startDate, String endDate,
|
|
|
+ /** 分页维度统计:ip / url / type / user / user_ip
|
|
|
+ * systemSource 来源系统:null/空 → 默认 AI药典;"all" → 跨系统全量;其它值 → 按该系统过滤 */
|
|
|
+ public Map<String, Object> queryDimension(String dimension, String systemSource,
|
|
|
+ String startDate, String endDate,
|
|
|
int page, int pageSize) {
|
|
|
Instant startTime = parseDate(startDate) != null ? parseDate(startDate) : Instant.EPOCH;
|
|
|
Instant endTime = endDate != null && !endDate.isBlank()
|
|
|
? LocalDate.parse(endDate).plusDays(1).atStartOfDay(ZoneOffset.UTC).toInstant()
|
|
|
: FAR_FUTURE;
|
|
|
+ String systemFilter = normalizeSystemFilter(systemSource);
|
|
|
|
|
|
Page<Object[]> result;
|
|
|
switch (dimension != null ? dimension : "") {
|
|
|
case "ip":
|
|
|
- result = repo.countByIpPaged(startTime, endTime,
|
|
|
+ result = repo.countByIpPaged(startTime, endTime, systemFilter,
|
|
|
PageRequest.of(page - 1, pageSize));
|
|
|
break;
|
|
|
case "url":
|
|
|
- result = repo.countByPageUrlPaged(startTime, endTime,
|
|
|
+ result = repo.countByPageUrlPaged(startTime, endTime, systemFilter,
|
|
|
PageRequest.of(page - 1, pageSize));
|
|
|
break;
|
|
|
case "type":
|
|
|
- result = repo.countByEventTypePaged(startTime, endTime,
|
|
|
+ result = repo.countByEventTypePaged(startTime, endTime, systemFilter,
|
|
|
PageRequest.of(page - 1, pageSize));
|
|
|
break;
|
|
|
case "user":
|
|
|
- result = repo.countByUserKeyPaged(startTime, endTime,
|
|
|
+ result = repo.countByUserKeyPaged(startTime, endTime, systemFilter,
|
|
|
PageRequest.of(page - 1, pageSize));
|
|
|
break;
|
|
|
case "user_ip":
|
|
|
- result = repo.countByUserIpPaged(startTime, endTime,
|
|
|
+ result = repo.countByUserIpPaged(startTime, endTime, systemFilter,
|
|
|
PageRequest.of(page - 1, pageSize));
|
|
|
break;
|
|
|
default:
|
|
|
@@ -215,6 +258,7 @@ public class AnalyticsService {
|
|
|
List<Map<String, Object>> items = mapResult(result.getContent(), dimension);
|
|
|
Map<String, Object> response = new LinkedHashMap<>();
|
|
|
response.put("dimension", dimension);
|
|
|
+ response.put("system", systemFilter != null ? systemFilter : "all");
|
|
|
response.put("items", items);
|
|
|
response.put("total", result.getTotalElements());
|
|
|
response.put("page", page);
|
|
|
@@ -247,14 +291,34 @@ public class AnalyticsService {
|
|
|
return repo.findDistinctEventTypes();
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 返回当前所有出现过的来源系统,供前端系统切换下拉。
|
|
|
+ * AI药典(本系统)恒定存在且排首位;其余按字母序追加。
|
|
|
+ */
|
|
|
+ public List<String> getSystems() {
|
|
|
+ List<String> distinct = repo.findDistinctSystemSources();
|
|
|
+ List<String> systems = new ArrayList<>();
|
|
|
+ systems.add(DEFAULT_SYSTEM);
|
|
|
+ for (String s : distinct) {
|
|
|
+ if (!DEFAULT_SYSTEM.equals(s)) {
|
|
|
+ systems.add(s);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return systems;
|
|
|
+ }
|
|
|
+
|
|
|
// ==================== Helpers ====================
|
|
|
|
|
|
- private Specification<UserEvent> buildTimeSpec(String eventType, Instant startTime, Instant endTime) {
|
|
|
+ private Specification<UserEvent> buildTimeSpec(String eventType, String systemSource,
|
|
|
+ Instant startTime, Instant endTime) {
|
|
|
return (root, query, cb) -> {
|
|
|
List<Predicate> predicates = new ArrayList<>();
|
|
|
if (eventType != null && !eventType.isBlank()) {
|
|
|
predicates.add(cb.equal(root.get("eventType"), eventType));
|
|
|
}
|
|
|
+ if (systemSource != null && !systemSource.isBlank()) {
|
|
|
+ predicates.add(cb.equal(root.get("systemSource"), systemSource));
|
|
|
+ }
|
|
|
predicates.add(cb.greaterThanOrEqualTo(root.get("createdAt"), startTime));
|
|
|
predicates.add(cb.lessThan(root.get("createdAt"), endTime));
|
|
|
return cb.and(predicates.toArray(new Predicate[0]));
|