zhangchen hai 5 horas
pai
achega
578c06fdc3
Modificáronse 16 ficheiros con 1029 adicións e 2 borrados
  1. 35 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/detail/AnalyticsAiChatDetailVo.java
  2. 29 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/detail/AnalyticsAiResponseDetailVo.java
  3. 38 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/detail/AnalyticsContentPublishDetailVo.java
  4. 30 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/detail/AnalyticsConversionDetailVo.java
  5. 33 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/detail/AnalyticsDauDetailVo.java
  6. 33 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/detail/AnalyticsMerchantUvDetailVo.java
  7. 32 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/detail/AnalyticsNewUserDetailVo.java
  8. 32 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/detail/AnalyticsOnlineUserDetailVo.java
  9. 55 0
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsDashboardDetailMapper.java
  10. 208 0
      alien-entity/src/main/resources/mapper/AnalyticsDashboardDetailMapper.xml
  11. 2 2
      alien-second/src/main/java/shop/alien/second/controller/SecondGoodsController.java
  12. 1 0
      alien-second/src/main/java/shop/alien/second/service/impl/SecondGoodsServiceImpl.java
  13. 112 0
      alien-store/src/main/java/shop/alien/store/controller/analytics/AnalyticsDashboardDetailController.java
  14. 26 0
      alien-store/src/main/java/shop/alien/store/service/analytics/AnalyticsDashboardDetailService.java
  15. 343 0
      alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsDashboardDetailServiceImpl.java
  16. 20 0
      alien-store/src/main/java/shop/alien/store/util/analytics/AnalyticsDateUtil.java

+ 35 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/detail/AnalyticsAiChatDetailVo.java

@@ -0,0 +1,35 @@
+package shop.alien.entity.analytics.vo.dashboard.detail;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@ApiModel("看板-对话次数明细")
+public class AnalyticsAiChatDetailVo {
+
+    @ApiModelProperty("对话ID")
+    private String chatId;
+
+    @ApiModelProperty("展示对话ID")
+    private String displayChatId;
+
+    @ApiModelProperty("用户ID")
+    private Long userId;
+
+    @ApiModelProperty("展示用户ID")
+    private String displayUserId;
+
+    @ApiModelProperty("开始时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date startTime;
+
+    @ApiModelProperty("消息数")
+    private Integer messageCount;
+
+    @ApiModelProperty("AI响应时长(ms)")
+    private Long aiResponseDurationMs;
+}

+ 29 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/detail/AnalyticsAiResponseDetailVo.java

@@ -0,0 +1,29 @@
+package shop.alien.entity.analytics.vo.dashboard.detail;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@ApiModel("看板-AI响应时间明细(按小时)")
+public class AnalyticsAiResponseDetailVo {
+
+    @ApiModelProperty("统计时间(小时桶)")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date statTime;
+
+    @ApiModelProperty("请求数")
+    private Integer requestCount;
+
+    @ApiModelProperty("平均响应(ms)")
+    private Long avgResponseMs;
+
+    @ApiModelProperty("P95响应(ms),无足够样本时为空")
+    private Long p95ResponseMs;
+
+    @ApiModelProperty("超时数")
+    private Integer timeoutCount;
+}

+ 38 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/detail/AnalyticsContentPublishDetailVo.java

@@ -0,0 +1,38 @@
+package shop.alien.entity.analytics.vo.dashboard.detail;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@ApiModel("看板-内容发布明细")
+public class AnalyticsContentPublishDetailVo {
+
+    @ApiModelProperty("内容ID")
+    private Long contentId;
+
+    @ApiModelProperty("展示内容ID")
+    private String displayContentId;
+
+    @ApiModelProperty("标题")
+    private String title;
+
+    @ApiModelProperty("分类名称")
+    private String category;
+
+    @ApiModelProperty("作者ID")
+    private Long authorId;
+
+    @ApiModelProperty("展示作者ID")
+    private String displayAuthorId;
+
+    @ApiModelProperty("发布时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date publishTime;
+
+    @ApiModelProperty("互动数")
+    private Integer interactionCount;
+}

+ 30 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/detail/AnalyticsConversionDetailVo.java

@@ -0,0 +1,30 @@
+package shop.alien.entity.analytics.vo.dashboard.detail;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@ApiModel("看板-转化率明细(按日)")
+public class AnalyticsConversionDetailVo {
+
+    @ApiModelProperty("日期")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date statDate;
+
+    @ApiModelProperty("DAU")
+    private Integer dau;
+
+    @ApiModelProperty("支付用户数")
+    private Integer payUserCount;
+
+    @ApiModelProperty("转化率(%)")
+    private BigDecimal conversionRate;
+
+    @ApiModelProperty("客单价(元)")
+    private BigDecimal avgOrderAmount;
+}

+ 33 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/detail/AnalyticsDauDetailVo.java

@@ -0,0 +1,33 @@
+package shop.alien.entity.analytics.vo.dashboard.detail;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@ApiModel("看板-DAU明细")
+public class AnalyticsDauDetailVo {
+
+    @ApiModelProperty("用户ID")
+    private Long userId;
+
+    @ApiModelProperty("展示用户ID")
+    private String displayUserId;
+
+    @ApiModelProperty("首次启动时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date firstLaunchTime;
+
+    @ApiModelProperty("最后活跃时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date lastActiveTime;
+
+    @ApiModelProperty("城市")
+    private String city;
+
+    @ApiModelProperty("设备")
+    private String device;
+}

+ 33 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/detail/AnalyticsMerchantUvDetailVo.java

@@ -0,0 +1,33 @@
+package shop.alien.entity.analytics.vo.dashboard.detail;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+@ApiModel("看板-商家访问UV明细")
+public class AnalyticsMerchantUvDetailVo {
+
+    @ApiModelProperty("商家ID")
+    private Long merchantId;
+
+    @ApiModelProperty("展示商家ID")
+    private String displayMerchantId;
+
+    @ApiModelProperty("商家名称")
+    private String merchantName;
+
+    @ApiModelProperty("分类")
+    private String category;
+
+    @ApiModelProperty("访问UV")
+    private Integer visitUv;
+
+    @ApiModelProperty("访问PV")
+    private Integer visitPv;
+
+    @ApiModelProperty("核销转化率(%)")
+    private BigDecimal verifyConversionRate;
+}

+ 32 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/detail/AnalyticsNewUserDetailVo.java

@@ -0,0 +1,32 @@
+package shop.alien.entity.analytics.vo.dashboard.detail;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@ApiModel("看板-新增用户明细")
+public class AnalyticsNewUserDetailVo {
+
+    @ApiModelProperty("用户ID")
+    private Long userId;
+
+    @ApiModelProperty("展示用户ID")
+    private String displayUserId;
+
+    @ApiModelProperty("脱敏手机号")
+    private String maskedPhone;
+
+    @ApiModelProperty("注册时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date registerTime;
+
+    @ApiModelProperty("渠道")
+    private String channel;
+
+    @ApiModelProperty("城市")
+    private String city;
+}

+ 32 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/detail/AnalyticsOnlineUserDetailVo.java

@@ -0,0 +1,32 @@
+package shop.alien.entity.analytics.vo.dashboard.detail;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@ApiModel("看板-当前在线用户明细")
+public class AnalyticsOnlineUserDetailVo {
+
+    @ApiModelProperty("用户ID")
+    private Long userId;
+
+    @ApiModelProperty("展示用户ID")
+    private String displayUserId;
+
+    @ApiModelProperty("城市")
+    private String city;
+
+    @ApiModelProperty("设备")
+    private String device;
+
+    @ApiModelProperty("最后心跳/活跃时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date lastHeartbeatTime;
+
+    @ApiModelProperty("在线时长(分)")
+    private Integer onlineDurationMin;
+}

+ 55 - 0
alien-entity/src/main/java/shop/alien/mapper/AnalyticsDashboardDetailMapper.java

@@ -0,0 +1,55 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import shop.alien.entity.analytics.AnalyticsAiChatStat;
+import shop.alien.entity.analytics.AnalyticsDailySummary;
+import shop.alien.entity.analytics.AnalyticsUserStat;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 数据看板 KPI 明细分页(独立于 {@link AnalyticsDashboardMapper})
+ */
+@Mapper
+public interface AnalyticsDashboardDetailMapper {
+
+    IPage<AnalyticsUserStat> pageDauByStatDate(Page<?> page, @Param("statDate") Date statDate);
+
+    IPage<AnalyticsUserStat> pageDauInDateRange(Page<?> page,
+                                                @Param("startDate") Date startDate,
+                                                @Param("endDate") Date endDate);
+
+    IPage<AnalyticsUserStat> pageNewUserInRange(Page<?> page,
+                                                @Param("startTime") Date startTime,
+                                                @Param("endTime") Date endTime);
+
+    IPage<AnalyticsAiChatStat> pageAiChatInRange(Page<?> page,
+                                                 @Param("startTime") Date startTime,
+                                                 @Param("endTime") Date endTime);
+
+    IPage<Map<String, Object>> pageContentPublishInRange(Page<?> page,
+                                                         @Param("startTime") Date startTime,
+                                                         @Param("endTime") Date endTime);
+
+    IPage<Map<String, Object>> pageMerchantUvAggregated(Page<?> page,
+                                                       @Param("startDate") Date startDate,
+                                                       @Param("endDate") Date endDate);
+
+    IPage<Map<String, Object>> pageAiResponseHourly(Page<?> page,
+                                                    @Param("startTime") Date startTime,
+                                                    @Param("endTime") Date endTime);
+
+    List<Long> listAiResponseDurationsForHour(@Param("hourStart") Date hourStart,
+                                              @Param("hourEnd") Date hourEnd);
+
+    IPage<AnalyticsUserStat> pageOnlineUsers(Page<?> page, @Param("since") Date since);
+
+    IPage<AnalyticsDailySummary> pageConversionDaily(Page<?> page,
+                                                     @Param("startDate") Date startDate,
+                                                     @Param("endDate") Date endDate);
+}

+ 208 - 0
alien-entity/src/main/resources/mapper/AnalyticsDashboardDetailMapper.xml

@@ -0,0 +1,208 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="shop.alien.mapper.AnalyticsDashboardDetailMapper">
+
+    <sql id="userStatUnionByStatDate">
+        SELECT user_id, first_launch_time, last_active_time, city, device_type, user_phone, channel,
+               register_time, is_new_user, online_duration_min, stat_date, last_active_time AS sort_time
+        FROM analytics_user_stat_history
+        WHERE stat_date = #{statDate}
+        UNION ALL
+        SELECT user_id, first_launch_time, last_active_time, city, device_type, user_phone, channel,
+               register_time, is_new_user, online_duration_min, CURDATE() AS stat_date, last_active_time AS sort_time
+        FROM analytics_user_stat_today
+        WHERE #{statDate} = CURDATE()
+    </sql>
+
+    <sql id="userStatUnionInDateRange">
+        SELECT user_id, first_launch_time, last_active_time, city, device_type, user_phone, channel,
+               register_time, is_new_user, online_duration_min, stat_date, last_active_time AS sort_time
+        FROM analytics_user_stat_history
+        WHERE stat_date &gt;= #{startDate}
+          AND stat_date &lt;= #{endDate}
+        UNION ALL
+        SELECT user_id, first_launch_time, last_active_time, city, device_type, user_phone, channel,
+               register_time, is_new_user, online_duration_min, CURDATE() AS stat_date, last_active_time AS sort_time
+        FROM analytics_user_stat_today
+        WHERE #{endDate} &gt;= CURDATE()
+          AND #{startDate} &lt;= CURDATE()
+    </sql>
+
+    <select id="pageDauByStatDate" resultType="shop.alien.entity.analytics.AnalyticsUserStat">
+        SELECT t.user_id AS userId,
+               t.first_launch_time AS firstLaunchTime,
+               t.last_active_time AS lastActiveTime,
+               t.city,
+               t.device_type AS deviceType
+        FROM (
+            <include refid="userStatUnionByStatDate"/>
+        ) t
+        ORDER BY t.sort_time DESC
+    </select>
+
+    <select id="pageDauInDateRange" resultType="shop.alien.entity.analytics.AnalyticsUserStat">
+        SELECT t.user_id AS userId,
+               t.first_launch_time AS firstLaunchTime,
+               t.last_active_time AS lastActiveTime,
+               t.city,
+               t.device_type AS deviceType
+        FROM (
+            <include refid="userStatUnionInDateRange"/>
+        ) t
+        INNER JOIN (
+            SELECT user_id, MAX(sort_time) AS max_sort
+            FROM (
+                <include refid="userStatUnionInDateRange"/>
+            ) u
+            GROUP BY user_id
+        ) latest ON t.user_id = latest.user_id AND t.sort_time = latest.max_sort
+        ORDER BY t.sort_time DESC
+    </select>
+
+    <select id="pageNewUserInRange" resultType="shop.alien.entity.analytics.AnalyticsUserStat">
+        SELECT t.user_id AS userId,
+               t.user_phone AS userPhone,
+               t.register_time AS registerTime,
+               t.channel,
+               t.city
+        FROM (
+            SELECT user_id, user_phone, register_time, channel, city, register_time AS sort_time
+            FROM analytics_user_stat_history
+            WHERE register_time &gt;= #{startTime}
+              AND register_time &lt; #{endTime}
+              AND is_new_user = 1
+            UNION ALL
+            SELECT user_id, user_phone, register_time, channel, city, register_time AS sort_time
+            FROM analytics_user_stat_today
+            WHERE register_time &gt;= #{startTime}
+              AND register_time &lt; #{endTime}
+              AND is_new_user = 1
+        ) t
+        INNER JOIN (
+            SELECT user_id, MAX(sort_time) AS max_sort
+            FROM (
+                SELECT user_id, register_time AS sort_time
+                FROM analytics_user_stat_history
+                WHERE register_time &gt;= #{startTime}
+                  AND register_time &lt; #{endTime}
+                  AND is_new_user = 1
+                UNION ALL
+                SELECT user_id, register_time AS sort_time
+                FROM analytics_user_stat_today
+                WHERE register_time &gt;= #{startTime}
+                  AND register_time &lt; #{endTime}
+                  AND is_new_user = 1
+            ) u
+            GROUP BY user_id
+        ) latest ON t.user_id = latest.user_id AND t.sort_time = latest.max_sort
+        ORDER BY t.register_time DESC
+    </select>
+
+    <select id="pageAiChatInRange" resultType="shop.alien.entity.analytics.AnalyticsAiChatStat">
+        SELECT chat_id AS chatId,
+               user_id AS userId,
+               start_time AS startTime,
+               message_count AS messageCount,
+               ai_response_duration_ms AS aiResponseDurationMs
+        FROM (
+            SELECT chat_id, user_id, start_time, message_count, ai_response_duration_ms
+            FROM analytics_ai_chat_stat_history
+            WHERE start_time &gt;= #{startTime}
+              AND start_time &lt; #{endTime}
+            UNION ALL
+            SELECT chat_id, user_id, start_time, message_count, ai_response_duration_ms
+            FROM analytics_ai_chat_stat_today
+            WHERE start_time &gt;= #{startTime}
+              AND start_time &lt; #{endTime}
+        ) t
+        ORDER BY t.start_time DESC
+    </select>
+
+    <select id="pageContentPublishInRange" resultType="java.util.HashMap">
+        SELECT content_id AS contentId,
+               content_title AS contentTitle,
+               business_category AS businessCategory,
+               author_type AS authorType,
+               author_id AS authorId,
+               publish_time AS publishTime,
+               interaction_count AS interactionCount
+        FROM (
+            SELECT content_id, content_title, business_category, author_type, author_id, publish_time, interaction_count
+            FROM analytics_content_stat_history
+            WHERE publish_time &gt;= #{startTime}
+              AND publish_time &lt; #{endTime}
+            UNION ALL
+            SELECT content_id, content_title, business_category, author_type, author_id, publish_time, interaction_count
+            FROM analytics_content_stat_today
+            WHERE publish_time &gt;= #{startTime}
+              AND publish_time &lt; #{endTime}
+        ) t
+        ORDER BY t.publish_time DESC
+    </select>
+
+    <select id="pageMerchantUvAggregated" resultType="java.util.HashMap">
+        SELECT merchant_id AS merchantId,
+               MAX(shop_type) AS shopType,
+               SUM(visit_uv) AS visitUv,
+               SUM(visit_pv) AS visitPv,
+               SUM(verify_count) AS verifyCount
+        FROM (
+            SELECT merchant_id, shop_type, visit_uv, visit_pv, verify_count
+            FROM analytics_merchant_stat_history
+            WHERE stat_date &gt;= #{startDate}
+              AND stat_date &lt;= #{endDate}
+            UNION ALL
+            SELECT merchant_id, shop_type, visit_uv, visit_pv, verify_count
+            FROM analytics_merchant_stat_today
+            WHERE #{endDate} &gt;= CURDATE()
+              AND #{startDate} &lt;= CURDATE()
+        ) t
+        GROUP BY merchant_id
+        HAVING SUM(visit_uv) &gt; 0 OR SUM(visit_pv) &gt; 0
+        ORDER BY SUM(visit_uv) DESC, SUM(visit_pv) DESC
+    </select>
+
+    <select id="pageAiResponseHourly" resultType="java.util.HashMap">
+        SELECT DATE_FORMAT(created_time, '%Y-%m-%d %H:00:00') AS statTime,
+               COUNT(*) AS requestCount,
+               ROUND(AVG(response_duration_ms)) AS avgResponseMs,
+               SUM(CASE WHEN is_timeout = 1 THEN 1 ELSE 0 END) AS timeoutCount
+        FROM analytics_ai_request
+        WHERE created_time &gt;= #{startTime}
+          AND created_time &lt; #{endTime}
+        GROUP BY DATE_FORMAT(created_time, '%Y-%m-%d %H:00:00')
+        ORDER BY statTime DESC
+    </select>
+
+    <select id="listAiResponseDurationsForHour" resultType="java.lang.Long">
+        SELECT response_duration_ms
+        FROM analytics_ai_request
+        WHERE created_time &gt;= #{hourStart}
+          AND created_time &lt; #{hourEnd}
+          AND response_duration_ms IS NOT NULL
+        ORDER BY response_duration_ms ASC
+    </select>
+
+    <select id="pageOnlineUsers" resultType="shop.alien.entity.analytics.AnalyticsUserStat">
+        SELECT user_id AS userId,
+               city,
+               device_type AS deviceType,
+               last_active_time AS lastActiveTime,
+               online_duration_min AS onlineDurationMin
+        FROM analytics_user_stat_today
+        WHERE last_active_time &gt;= #{since}
+        ORDER BY last_active_time DESC
+    </select>
+
+    <select id="pageConversionDaily" resultType="shop.alien.entity.analytics.AnalyticsDailySummary">
+        SELECT stat_date AS statDate,
+               dau,
+               pay_user_count AS payUserCount,
+               conversion_rate AS conversionRate,
+               avg_order_amount AS avgOrderAmount
+        FROM analytics_daily_summary
+        WHERE stat_date &gt;= #{startDate}
+          AND stat_date &lt;= #{endDate}
+        ORDER BY stat_date DESC
+    </select>
+</mapper>

+ 2 - 2
alien-second/src/main/java/shop/alien/second/controller/SecondGoodsController.java

@@ -101,7 +101,7 @@ public class SecondGoodsController {
     @PostMapping("/save")
     @ApiOperation("发布二手商品")
     @NoRepeatSubmit(expireTime = 5, message = "请勿重复提交发布商品请求")
-    public R<Void> addSecondGoods(@ApiParam("二手商品信息") @RequestBody SecondGoodsVo secondGoods) throws Exception {
+    public R<Integer> addSecondGoods(@ApiParam("二手商品信息") @RequestBody SecondGoodsVo secondGoods) throws Exception {
         log.info("SecondGoodsController.addSecondGoods?secondGoods={}", secondGoods.toString());
         JSONObject data = JwtUtil.getCurrentUserInfo();
         if (null != data) {
@@ -122,7 +122,7 @@ public class SecondGoodsController {
             if (!secondGoodsService.createBasicInfo(secondGoods,0)) {
                 return R.fail("添加二手商品失败");
             }
-            return R.success("添加二手商品成功");
+            return R.data(secondGoods.getId(), "添加二手商品成功");
         }
     }
 

+ 1 - 0
alien-second/src/main/java/shop/alien/second/service/impl/SecondGoodsServiceImpl.java

@@ -694,6 +694,7 @@ public class SecondGoodsServiceImpl extends ServiceImpl<SecondGoodsMapper, Secon
             if (savedGoodsId == null) {
                 return false; // 如果获取不到ID,视为操作失败
             }
+            goodsDTO.setId(savedGoodsId);
 
             // 保存商品图片信息
             if (!saveStoreImages(savedGoodsId, goodsDTO )) {

+ 112 - 0
alien-store/src/main/java/shop/alien/store/controller/analytics/AnalyticsDashboardDetailController.java

@@ -0,0 +1,112 @@
+package shop.alien.store.controller.analytics;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiSort;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.analytics.AnalyticsStatPeriod;
+import shop.alien.entity.analytics.vo.dashboard.detail.*;
+import shop.alien.entity.result.R;
+import shop.alien.store.service.analytics.AnalyticsDashboardDetailService;
+
+/**
+ * 数据看板 KPI 明细分页(独立于 {@link AnalyticsDashboardController})
+ */
+@Slf4j
+@Api(tags = {"平台埋点-数据看板明细"})
+@ApiSort(27)
+@CrossOrigin
+@RestController
+@RequestMapping("/analytics/dashboard-detail")
+@RequiredArgsConstructor
+public class AnalyticsDashboardDetailController {
+
+    private final AnalyticsDashboardDetailService dashboardDetailService;
+
+    @ApiOperation("DAU 明细分页")
+    @ApiOperationSupport(order = 1)
+    @GetMapping("/dau/page")
+    public R<IPage<AnalyticsDauDetailVo>> pageDau(
+            @ApiParam(value = "统计周期", allowableValues = "TODAY,YESTERDAY,LAST_7D,LAST_30D")
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period,
+            @RequestParam(defaultValue = "1") long page,
+            @RequestParam(defaultValue = "10") long size) {
+        return R.data(dashboardDetailService.pageDau(period, page, size));
+    }
+
+    @ApiOperation("新增用户明细分页")
+    @ApiOperationSupport(order = 2)
+    @GetMapping("/new-user/page")
+    public R<IPage<AnalyticsNewUserDetailVo>> pageNewUser(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period,
+            @RequestParam(defaultValue = "1") long page,
+            @RequestParam(defaultValue = "10") long size) {
+        return R.data(dashboardDetailService.pageNewUser(period, page, size));
+    }
+
+    @ApiOperation("对话次数明细分页")
+    @ApiOperationSupport(order = 3)
+    @GetMapping("/ai-chat/page")
+    public R<IPage<AnalyticsAiChatDetailVo>> pageAiChat(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period,
+            @RequestParam(defaultValue = "1") long page,
+            @RequestParam(defaultValue = "10") long size) {
+        return R.data(dashboardDetailService.pageAiChat(period, page, size));
+    }
+
+    @ApiOperation("内容发布明细分页")
+    @ApiOperationSupport(order = 4)
+    @GetMapping("/content-publish/page")
+    public R<IPage<AnalyticsContentPublishDetailVo>> pageContentPublish(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period,
+            @RequestParam(defaultValue = "1") long page,
+            @RequestParam(defaultValue = "10") long size) {
+        return R.data(dashboardDetailService.pageContentPublish(period, page, size));
+    }
+
+    @ApiOperation("商家访问 UV 明细分页")
+    @ApiOperationSupport(order = 5)
+    @GetMapping("/merchant-uv/page")
+    public R<IPage<AnalyticsMerchantUvDetailVo>> pageMerchantUv(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period,
+            @RequestParam(defaultValue = "1") long page,
+            @RequestParam(defaultValue = "10") long size) {
+        return R.data(dashboardDetailService.pageMerchantUv(period, page, size));
+    }
+
+    @ApiOperation("AI 响应时间明细分页(按小时)")
+    @ApiOperationSupport(order = 6)
+    @GetMapping("/ai-response/page")
+    public R<IPage<AnalyticsAiResponseDetailVo>> pageAiResponse(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period,
+            @RequestParam(defaultValue = "1") long page,
+            @RequestParam(defaultValue = "10") long size) {
+        return R.data(dashboardDetailService.pageAiResponse(period, page, size));
+    }
+
+    @ApiOperation("当前在线用户明细分页(仅 TODAY 有效,实时窗口)")
+    @ApiOperationSupport(order = 7)
+    @GetMapping("/online/page")
+    public R<IPage<AnalyticsOnlineUserDetailVo>> pageOnline(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period,
+            @RequestParam(defaultValue = "1") long page,
+            @RequestParam(defaultValue = "10") long size,
+            @RequestParam(defaultValue = "5") int onlineWithinMin) {
+        return R.data(dashboardDetailService.pageOnline(period, page, size, onlineWithinMin));
+    }
+
+    @ApiOperation("转化率明细分页(按日)")
+    @ApiOperationSupport(order = 8)
+    @GetMapping("/conversion/page")
+    public R<IPage<AnalyticsConversionDetailVo>> pageConversion(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period,
+            @RequestParam(defaultValue = "1") long page,
+            @RequestParam(defaultValue = "10") long size) {
+        return R.data(dashboardDetailService.pageConversion(period, page, size));
+    }
+}

+ 26 - 0
alien-store/src/main/java/shop/alien/store/service/analytics/AnalyticsDashboardDetailService.java

@@ -0,0 +1,26 @@
+package shop.alien.store.service.analytics;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import shop.alien.entity.analytics.vo.dashboard.detail.*;
+
+/**
+ * 数据看板 KPI 明细分页(独立于 {@link AnalyticsDashboardService})
+ */
+public interface AnalyticsDashboardDetailService {
+
+    IPage<AnalyticsDauDetailVo> pageDau(String period, long page, long size);
+
+    IPage<AnalyticsNewUserDetailVo> pageNewUser(String period, long page, long size);
+
+    IPage<AnalyticsAiChatDetailVo> pageAiChat(String period, long page, long size);
+
+    IPage<AnalyticsContentPublishDetailVo> pageContentPublish(String period, long page, long size);
+
+    IPage<AnalyticsMerchantUvDetailVo> pageMerchantUv(String period, long page, long size);
+
+    IPage<AnalyticsAiResponseDetailVo> pageAiResponse(String period, long page, long size);
+
+    IPage<AnalyticsOnlineUserDetailVo> pageOnline(String period, long page, long size, int onlineWithinMin);
+
+    IPage<AnalyticsConversionDetailVo> pageConversion(String period, long page, long size);
+}

+ 343 - 0
alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsDashboardDetailServiceImpl.java

@@ -0,0 +1,343 @@
+package shop.alien.store.service.analytics.impl;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import shop.alien.entity.analytics.*;
+import shop.alien.entity.analytics.vo.dashboard.detail.*;
+import shop.alien.entity.store.StoreInfo;
+import shop.alien.mapper.AnalyticsDashboardDetailMapper;
+import shop.alien.mapper.StoreInfoMapper;
+import shop.alien.store.service.analytics.AnalyticsDashboardDetailService;
+import shop.alien.store.util.analytics.AnalyticsDateUtil;
+import shop.alien.store.util.analytics.AnalyticsPeriodContext;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Service
+@RequiredArgsConstructor
+public class AnalyticsDashboardDetailServiceImpl implements AnalyticsDashboardDetailService {
+
+    private static final int DEFAULT_ONLINE_WITHIN_MIN = 5;
+
+    private final AnalyticsDashboardDetailMapper detailMapper;
+    private final StoreInfoMapper storeInfoMapper;
+
+    @Override
+    public IPage<AnalyticsDauDetailVo> pageDau(String period, long page, long size) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        IPage<AnalyticsUserStat> raw;
+        if (isSingleDayPeriod(ctx)) {
+            raw = detailMapper.pageDauByStatDate(new Page<>(page, size), ctx.getStartDate());
+        } else {
+            raw = detailMapper.pageDauInDateRange(new Page<>(page, size), ctx.getStartDate(), ctx.getEndDate());
+        }
+        return raw.convert(this::toDauVo);
+    }
+
+    @Override
+    public IPage<AnalyticsNewUserDetailVo> pageNewUser(String period, long page, long size) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        IPage<AnalyticsUserStat> raw = detailMapper.pageNewUserInRange(
+                new Page<>(page, size), ctx.getStartTime(), ctx.getEndTimeExclusive());
+        return raw.convert(this::toNewUserVo);
+    }
+
+    @Override
+    public IPage<AnalyticsAiChatDetailVo> pageAiChat(String period, long page, long size) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        IPage<AnalyticsAiChatStat> raw = detailMapper.pageAiChatInRange(
+                new Page<>(page, size), ctx.getStartTime(), ctx.getEndTimeExclusive());
+        return raw.convert(this::toAiChatVo);
+    }
+
+    @Override
+    public IPage<AnalyticsContentPublishDetailVo> pageContentPublish(String period, long page, long size) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        IPage<Map<String, Object>> raw = detailMapper.pageContentPublishInRange(
+                new Page<>(page, size), ctx.getStartTime(), ctx.getEndTimeExclusive());
+        return raw.convert(this::toContentVo);
+    }
+
+    @Override
+    public IPage<AnalyticsMerchantUvDetailVo> pageMerchantUv(String period, long page, long size) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        IPage<Map<String, Object>> raw = detailMapper.pageMerchantUvAggregated(
+                new Page<>(page, size), ctx.getStartDate(), ctx.getEndDate());
+        return raw.convert(this::toMerchantVo);
+    }
+
+    @Override
+    public IPage<AnalyticsAiResponseDetailVo> pageAiResponse(String period, long page, long size) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        IPage<Map<String, Object>> raw = detailMapper.pageAiResponseHourly(
+                new Page<>(page, size), ctx.getStartTime(), ctx.getEndTimeExclusive());
+        IPage<AnalyticsAiResponseDetailVo> converted = raw.convert(this::toAiResponseVo);
+        for (AnalyticsAiResponseDetailVo row : converted.getRecords()) {
+            fillP95(row);
+        }
+        return converted;
+    }
+
+    @Override
+    public IPage<AnalyticsOnlineUserDetailVo> pageOnline(String period, long page, long size, int onlineWithinMin) {
+        if (!AnalyticsStatPeriod.TODAY.equals(AnalyticsPeriodContext.resolve(period).getPeriod())) {
+            return new Page<>(page, size, 0);
+        }
+        int withinMin = onlineWithinMin > 0 ? onlineWithinMin : DEFAULT_ONLINE_WITHIN_MIN;
+        Calendar threshold = Calendar.getInstance();
+        threshold.add(Calendar.MINUTE, -withinMin);
+        IPage<AnalyticsUserStat> raw = detailMapper.pageOnlineUsers(new Page<>(page, size), threshold.getTime());
+        return raw.convert(this::toOnlineVo);
+    }
+
+    @Override
+    public IPage<AnalyticsConversionDetailVo> pageConversion(String period, long page, long size) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        IPage<AnalyticsDailySummary> raw = detailMapper.pageConversionDaily(
+                new Page<>(page, size), ctx.getStartDate(), ctx.getEndDate());
+        return raw.convert(this::toConversionVo);
+    }
+
+    private boolean isSingleDayPeriod(AnalyticsPeriodContext ctx) {
+        return AnalyticsStatPeriod.TODAY.equals(ctx.getPeriod())
+                || AnalyticsStatPeriod.YESTERDAY.equals(ctx.getPeriod());
+    }
+
+    private AnalyticsDauDetailVo toDauVo(AnalyticsUserStat stat) {
+        AnalyticsDauDetailVo vo = new AnalyticsDauDetailVo();
+        vo.setUserId(stat.getUserId());
+        vo.setDisplayUserId(formatUserId(stat.getUserId()));
+        vo.setFirstLaunchTime(stat.getFirstLaunchTime());
+        vo.setLastActiveTime(stat.getLastActiveTime());
+        vo.setCity(stat.getCity());
+        vo.setDevice(stat.getDeviceType());
+        return vo;
+    }
+
+    private AnalyticsNewUserDetailVo toNewUserVo(AnalyticsUserStat stat) {
+        AnalyticsNewUserDetailVo vo = new AnalyticsNewUserDetailVo();
+        vo.setUserId(stat.getUserId());
+        vo.setDisplayUserId(formatUserId(stat.getUserId()));
+        vo.setMaskedPhone(maskPhone(stat.getUserPhone()));
+        vo.setRegisterTime(stat.getRegisterTime());
+        vo.setChannel(stat.getChannel());
+        vo.setCity(stat.getCity());
+        return vo;
+    }
+
+    private AnalyticsAiChatDetailVo toAiChatVo(AnalyticsAiChatStat stat) {
+        AnalyticsAiChatDetailVo vo = new AnalyticsAiChatDetailVo();
+        vo.setChatId(stat.getChatId());
+        vo.setDisplayChatId(formatChatId(stat.getChatId()));
+        vo.setUserId(stat.getUserId());
+        vo.setDisplayUserId(formatUserId(stat.getUserId()));
+        vo.setStartTime(stat.getStartTime());
+        vo.setMessageCount(stat.getMessageCount());
+        vo.setAiResponseDurationMs(stat.getAiResponseDurationMs());
+        return vo;
+    }
+
+    private AnalyticsContentPublishDetailVo toContentVo(Map<String, Object> row) {
+        AnalyticsContentPublishDetailVo vo = new AnalyticsContentPublishDetailVo();
+        Long contentId = longValue(row.get("contentId"));
+        vo.setContentId(contentId);
+        vo.setDisplayContentId(formatContentId(contentId));
+        vo.setTitle(stringValue(row.get("contentTitle")));
+        vo.setCategory(resolveBusinessCategoryName(intValue(row.get("businessCategory"))));
+        Long authorId = longValue(row.get("authorId"));
+        vo.setAuthorId(authorId);
+        vo.setDisplayAuthorId(formatUserId(authorId));
+        Object publishTime = row.get("publishTime");
+        if (publishTime instanceof Date) {
+            vo.setPublishTime((Date) publishTime);
+        }
+        vo.setInteractionCount(intValue(row.get("interactionCount")));
+        return vo;
+    }
+
+    private AnalyticsMerchantUvDetailVo toMerchantVo(Map<String, Object> row) {
+        AnalyticsMerchantUvDetailVo vo = new AnalyticsMerchantUvDetailVo();
+        Long merchantId = longValue(row.get("merchantId"));
+        vo.setMerchantId(merchantId);
+        vo.setDisplayMerchantId(formatMerchantId(merchantId));
+        vo.setVisitUv(intValue(row.get("visitUv")));
+        vo.setVisitPv(intValue(row.get("visitPv")));
+        int verifyCount = intValue(row.get("verifyCount"));
+        if (vo.getVisitUv() != null && vo.getVisitUv() > 0 && verifyCount >= 0) {
+            vo.setVerifyConversionRate(BigDecimal.valueOf(verifyCount * 100.0 / vo.getVisitUv())
+                    .setScale(1, RoundingMode.HALF_UP));
+        }
+        if (merchantId != null) {
+            StoreInfo store = storeInfoMapper.selectById(merchantId);
+            if (store != null) {
+                vo.setMerchantName(store.getStoreName());
+                vo.setCategory(firstNonBlank(store.getBusinessCategoryName(), store.getBusinessTypeName(),
+                        resolveShopTypeName(intValue(row.get("shopType")))));
+            } else {
+                vo.setCategory(resolveShopTypeName(intValue(row.get("shopType"))));
+            }
+        }
+        return vo;
+    }
+
+    private AnalyticsAiResponseDetailVo toAiResponseVo(Map<String, Object> row) {
+        AnalyticsAiResponseDetailVo vo = new AnalyticsAiResponseDetailVo();
+        Object statTime = row.get("statTime");
+        if (statTime instanceof Date) {
+            vo.setStatTime((Date) statTime);
+        } else if (statTime != null) {
+            vo.setStatTime(AnalyticsDateUtil.parseDateTime(String.valueOf(statTime)));
+        }
+        vo.setRequestCount(intValue(row.get("requestCount")));
+        vo.setAvgResponseMs(longValue(row.get("avgResponseMs")));
+        vo.setTimeoutCount(intValue(row.get("timeoutCount")));
+        return vo;
+    }
+
+    private void fillP95(AnalyticsAiResponseDetailVo row) {
+        if (row.getStatTime() == null) {
+            return;
+        }
+        Date hourStart = row.getStatTime();
+        Date hourEnd = AnalyticsDateUtil.addHours(hourStart, 1);
+        List<Long> durations = detailMapper.listAiResponseDurationsForHour(hourStart, hourEnd);
+        if (durations == null || durations.isEmpty()) {
+            return;
+        }
+        int idx = (int) Math.ceil(durations.size() * 0.95) - 1;
+        if (idx < 0) {
+            idx = 0;
+        }
+        if (idx >= durations.size()) {
+            idx = durations.size() - 1;
+        }
+        row.setP95ResponseMs(durations.get(idx));
+    }
+
+    private AnalyticsOnlineUserDetailVo toOnlineVo(AnalyticsUserStat stat) {
+        AnalyticsOnlineUserDetailVo vo = new AnalyticsOnlineUserDetailVo();
+        vo.setUserId(stat.getUserId());
+        vo.setDisplayUserId(formatUserId(stat.getUserId()));
+        vo.setCity(stat.getCity());
+        vo.setDevice(stat.getDeviceType());
+        vo.setLastHeartbeatTime(stat.getLastActiveTime());
+        vo.setOnlineDurationMin(stat.getOnlineDurationMin());
+        return vo;
+    }
+
+    private AnalyticsConversionDetailVo toConversionVo(AnalyticsDailySummary summary) {
+        AnalyticsConversionDetailVo vo = new AnalyticsConversionDetailVo();
+        vo.setStatDate(summary.getStatDate());
+        vo.setDau(summary.getDau());
+        vo.setPayUserCount(summary.getPayUserCount());
+        vo.setConversionRate(summary.getConversionRate());
+        vo.setAvgOrderAmount(summary.getAvgOrderAmount());
+        return vo;
+    }
+
+    private String formatUserId(Long userId) {
+        return userId != null ? "U" + userId : null;
+    }
+
+    private String formatMerchantId(Long merchantId) {
+        return merchantId != null ? "M" + merchantId : null;
+    }
+
+    private String formatContentId(Long contentId) {
+        return contentId != null ? "C" + contentId : null;
+    }
+
+    private String formatChatId(String chatId) {
+        if (!StringUtils.hasText(chatId)) {
+            return null;
+        }
+        return chatId.startsWith("C") ? chatId : chatId;
+    }
+
+    private String maskPhone(String phone) {
+        if (!StringUtils.hasText(phone) || phone.length() < 7) {
+            return phone;
+        }
+        return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
+    }
+
+    private String resolveBusinessCategoryName(Integer businessCategory) {
+        if (businessCategory == null) {
+            return null;
+        }
+        switch (businessCategory) {
+            case AnalyticsBusinessCategory.FOOD: return "美食";
+            case AnalyticsBusinessCategory.LEISURE: return "休闲娱乐";
+            case AnalyticsBusinessCategory.LIFE_SERVICE: return "生活服务";
+            case AnalyticsBusinessCategory.TRAVEL: return "旅游";
+            case AnalyticsBusinessCategory.HOTEL: return "酒店";
+            case AnalyticsBusinessCategory.SHOPPING: return "购物";
+            case AnalyticsBusinessCategory.OTHER: return "其他";
+            default: return null;
+        }
+    }
+
+    private String resolveShopTypeName(Integer shopType) {
+        if (shopType == null) {
+            return null;
+        }
+        switch (shopType) {
+            case 1: return "美食";
+            case 2: return "休闲娱乐";
+            case 3: return "生活服务";
+            default: return null;
+        }
+    }
+
+    private String firstNonBlank(String... values) {
+        if (values == null) {
+            return null;
+        }
+        for (String v : values) {
+            if (StringUtils.hasText(v)) {
+                return v;
+            }
+        }
+        return null;
+    }
+
+    private String stringValue(Object o) {
+        return o != null ? String.valueOf(o) : null;
+    }
+
+    private Integer intValue(Object o) {
+        if (o == null) {
+            return null;
+        }
+        if (o instanceof Number) {
+            return ((Number) o).intValue();
+        }
+        try {
+            return Integer.parseInt(String.valueOf(o));
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
+    private Long longValue(Object o) {
+        if (o == null) {
+            return null;
+        }
+        if (o instanceof Number) {
+            return ((Number) o).longValue();
+        }
+        try {
+            return Long.parseLong(String.valueOf(o));
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+}

+ 20 - 0
alien-store/src/main/java/shop/alien/store/util/analytics/AnalyticsDateUtil.java

@@ -1,5 +1,7 @@
 package shop.alien.store.util.analytics;
 
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
 import java.util.Calendar;
 import java.util.Date;
 
@@ -39,6 +41,24 @@ public final class AnalyticsDateUtil {
         return cal.getTime();
     }
 
+    public static Date addHours(Date date, int hours) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+        cal.add(Calendar.HOUR_OF_DAY, hours);
+        return cal.getTime();
+    }
+
+    public static Date parseDateTime(String text) {
+        if (text == null || text.trim().isEmpty()) {
+            return null;
+        }
+        try {
+            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(text.trim());
+        } catch (ParseException e) {
+            return null;
+        }
+    }
+
     public static java.math.BigDecimal calcRate(Number numerator, Number denominator) {
         if (numerator == null || denominator == null) {
             return null;