ソースを参照

轮胎优惠券大屏功能

liyuan 3 週間 前
コミット
0cc37a86b6
16 ファイル変更885 行追加0 行削除
  1. 42 0
      blade-service-api/blade-sales-part-api/src/main/java/org/springblade/salesPart/coupon/excel/CouponUsageRankExcel.java
  2. 26 0
      blade-service-api/blade-sales-part-api/src/main/java/org/springblade/salesPart/coupon/screen/CouponDailyCountVO.java
  3. 32 0
      blade-service-api/blade-sales-part-api/src/main/java/org/springblade/salesPart/coupon/screen/CouponDailyOrderAmountVO.java
  4. 27 0
      blade-service-api/blade-sales-part-api/src/main/java/org/springblade/salesPart/coupon/screen/CouponOrderAmountAggVO.java
  5. 27 0
      blade-service-api/blade-sales-part-api/src/main/java/org/springblade/salesPart/coupon/screen/CouponScreenKpiVO.java
  6. 27 0
      blade-service-api/blade-sales-part-api/src/main/java/org/springblade/salesPart/coupon/screen/CouponScreenOverviewVO.java
  7. 27 0
      blade-service-api/blade-sales-part-api/src/main/java/org/springblade/salesPart/coupon/screen/CouponScreenQuery.java
  8. 30 0
      blade-service-api/blade-sales-part-api/src/main/java/org/springblade/salesPart/coupon/screen/CouponTrendDailyVO.java
  9. 26 0
      blade-service-api/blade-sales-part-api/src/main/java/org/springblade/salesPart/coupon/screen/CouponUserCouponCountAggVO.java
  10. 14 0
      blade-service-api/blade-sales-part-api/src/main/java/org/springblade/salesPart/coupon/screen/CustomerSelectVo.java
  11. 33 0
      blade-service-api/blade-sales-part-api/src/main/java/org/springblade/salesPart/coupon/screen/UsageRankItemVO.java
  12. 80 0
      blade-service/blade-sales-part/src/main/java/org/springblade/salesPart/coupon/controller/CouponScreenController.java
  13. 98 0
      blade-service/blade-sales-part/src/main/java/org/springblade/salesPart/coupon/mapper/CouponScreenMapper.java
  14. 164 0
      blade-service/blade-sales-part/src/main/java/org/springblade/salesPart/coupon/mapper/CouponScreenMapper.xml
  15. 48 0
      blade-service/blade-sales-part/src/main/java/org/springblade/salesPart/coupon/service/ICouponScreenService.java
  16. 184 0
      blade-service/blade-sales-part/src/main/java/org/springblade/salesPart/coupon/service/impl/CouponScreenServiceImpl.java

+ 42 - 0
blade-service-api/blade-sales-part-api/src/main/java/org/springblade/salesPart/coupon/excel/CouponUsageRankExcel.java

@@ -0,0 +1,42 @@
+package org.springblade.salesPart.coupon.excel;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.alibaba.excel.annotation.write.style.ColumnWidth;
+import com.alibaba.excel.annotation.write.style.ContentRowHeight;
+import com.alibaba.excel.annotation.write.style.HeadRowHeight;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 优惠券大屏-使用排行导出
+ *
+ * @author Rain
+ */
+@Data
+@ColumnWidth(22)
+@HeadRowHeight(20)
+@ContentRowHeight(18)
+public class CouponUsageRankExcel implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	@ExcelProperty("客户名称")
+	private String name;
+
+	@ExcelProperty("销售公司")
+	private String salesCompanyName;
+
+	@ExcelProperty("订单数")
+	private Long usedCount;
+
+	@ExcelProperty("使用优惠券张数")
+	private Long usedCouponCount;
+
+	@ExcelProperty("已付红包金额")
+	private BigDecimal paidRedPacketAmount;
+
+	@ExcelProperty("待付占用红包金额")
+	private BigDecimal unpaidRedPacketAmount;
+}

+ 26 - 0
blade-service-api/blade-sales-part-api/src/main/java/org/springblade/salesPart/coupon/screen/CouponDailyCountVO.java

@@ -0,0 +1,26 @@
+package org.springblade.salesPart.coupon.screen;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 按日数量统计
+ *
+ * @author Rain
+ */
+@Data
+public class CouponDailyCountVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 日期 yyyy-MM-dd
+     */
+    private String dt;
+
+    /**
+     * 数量
+     */
+    private Long cnt;
+}

+ 32 - 0
blade-service-api/blade-sales-part-api/src/main/java/org/springblade/salesPart/coupon/screen/CouponDailyOrderAmountVO.java

@@ -0,0 +1,32 @@
+package org.springblade.salesPart.coupon.screen;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 按日订单红包金额
+ *
+ * @author Rain
+ */
+@Data
+public class CouponDailyOrderAmountVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 日期 yyyy-MM-dd
+     */
+    private String dt;
+
+    /**
+     * 当日已支付红包金额
+     */
+    private BigDecimal paid;
+
+    /**
+     * 当日未支付占用红包金额
+     */
+    private BigDecimal unpaid;
+}

+ 27 - 0
blade-service-api/blade-sales-part-api/src/main/java/org/springblade/salesPart/coupon/screen/CouponOrderAmountAggVO.java

@@ -0,0 +1,27 @@
+package org.springblade.salesPart.coupon.screen;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 订单红包汇总
+ *
+ * @author Rain
+ */
+@Data
+public class CouponOrderAmountAggVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 已支付红包金额
+     */
+    private BigDecimal paid;
+
+    /**
+     * 未支付占用红包金额
+     */
+    private BigDecimal unpaid;
+}

+ 27 - 0
blade-service-api/blade-sales-part-api/src/main/java/org/springblade/salesPart/coupon/screen/CouponScreenKpiVO.java

@@ -0,0 +1,27 @@
+package org.springblade.salesPart.coupon.screen;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+@Data
+@ApiModel("优惠券大屏 KPI")
+public class CouponScreenKpiVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("领取张数")
+    private Long totalReceived;
+
+    @ApiModelProperty("已使用张数(券占用)")
+    private Long totalUsed;
+
+    @ApiModelProperty("已支付红包金额")
+    private BigDecimal paidRedPacketAmount;
+
+    @ApiModelProperty("待支付占用(未付已占券)")
+    private BigDecimal unpaidRedPacketAmount;
+}

+ 27 - 0
blade-service-api/blade-sales-part-api/src/main/java/org/springblade/salesPart/coupon/screen/CouponScreenOverviewVO.java

@@ -0,0 +1,27 @@
+package org.springblade.salesPart.coupon.screen;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+@ApiModel("优惠券大屏-总览")
+public class CouponScreenOverviewVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("门店名称(可选)")
+    private String storeName;
+
+    @ApiModelProperty("KPI")
+    private CouponScreenKpiVO kpis;
+
+    @ApiModelProperty("按日趋势(自然月每日;全部时由趋势接口单独约定粒度)")
+    private List<CouponTrendDailyVO> trend;
+
+    @ApiModelProperty("店内使用排行")
+    private List<UsageRankItemVO> usageRank;
+}

+ 27 - 0
blade-service-api/blade-sales-part-api/src/main/java/org/springblade/salesPart/coupon/screen/CouponScreenQuery.java

@@ -0,0 +1,27 @@
+package org.springblade.salesPart.coupon.screen;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 门店优惠券大屏查询
+ * @author Rain
+ */
+@Data
+@ApiModel("优惠券大屏查询")
+public class CouponScreenQuery implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("租户ID,不传表示查询全部租户")
+    private String tenantId;
+
+    /**
+     * 自然月 yyyy-MM;为空或 null 表示统计「全部」累计
+     */
+    @ApiModelProperty("统计月份 yyyy-MM,不传表示全部")
+    private String yearMonth;
+}

+ 30 - 0
blade-service-api/blade-sales-part-api/src/main/java/org/springblade/salesPart/coupon/screen/CouponTrendDailyVO.java

@@ -0,0 +1,30 @@
+package org.springblade.salesPart.coupon.screen;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+@Data
+@ApiModel("优惠券大屏-按日趋势")
+public class CouponTrendDailyVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("日期 yyyy-MM-dd")
+    private String date;
+
+    @ApiModelProperty("当日已支付红包汇总")
+    private BigDecimal paidRedPacketAmount;
+
+    @ApiModelProperty("当日待支付占用红包汇总")
+    private BigDecimal unpaidRedPacketAmount;
+
+    @ApiModelProperty("当日领取张数")
+    private Long receivedCount;
+
+    @ApiModelProperty("当日使用张数")
+    private Long usedCount;
+}

+ 26 - 0
blade-service-api/blade-sales-part-api/src/main/java/org/springblade/salesPart/coupon/screen/CouponUserCouponCountAggVO.java

@@ -0,0 +1,26 @@
+package org.springblade.salesPart.coupon.screen;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 用户券领取/使用汇总
+ *
+ * @author Rain
+ */
+@Data
+public class CouponUserCouponCountAggVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 领取总数
+     */
+    private Long totalReceived;
+
+    /**
+     * 使用总数
+     */
+    private Long totalUsed;
+}

+ 14 - 0
blade-service-api/blade-sales-part-api/src/main/java/org/springblade/salesPart/coupon/screen/CustomerSelectVo.java

@@ -0,0 +1,14 @@
+package org.springblade.salesPart.coupon.screen;
+
+import lombok.Data;
+
+@Data
+public class CustomerSelectVo {
+
+
+	private String tenantId;
+
+
+	private String tenantName;
+
+}

+ 33 - 0
blade-service-api/blade-sales-part-api/src/main/java/org/springblade/salesPart/coupon/screen/UsageRankItemVO.java

@@ -0,0 +1,33 @@
+package org.springblade.salesPart.coupon.screen;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+@Data
+@ApiModel("店内使用排行项")
+public class UsageRankItemVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("展示名:客户名或业务员名")
+    private String name;
+
+    @ApiModelProperty("销售公司名称")
+    private String salesCompanyName;
+
+    @ApiModelProperty("用券张数(或订单笔数,按你方口径)")
+    private Long usedCount;
+
+    @ApiModelProperty("使用优惠券张数")
+    private Long usedCouponCount;
+
+    @ApiModelProperty("已付红包金额")
+    private BigDecimal paidRedPacketAmount;
+
+    @ApiModelProperty("待付占用红包金额")
+    private BigDecimal unpaidRedPacketAmount;
+}

+ 80 - 0
blade-service/blade-sales-part/src/main/java/org/springblade/salesPart/coupon/controller/CouponScreenController.java

@@ -0,0 +1,80 @@
+package org.springblade.salesPart.coupon.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.AllArgsConstructor;
+import org.springblade.core.tool.api.R;
+import org.springblade.salesPart.coupon.screen.CouponScreenQuery;
+import org.springblade.salesPart.coupon.screen.CouponScreenOverviewVO;
+import org.springblade.salesPart.coupon.screen.CustomerSelectVo;
+import org.springblade.salesPart.coupon.service.ICouponScreenService;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * 门店优惠券大屏接口
+ *
+ * @author Rain
+ */
+@RestController
+@AllArgsConstructor
+@RequestMapping("/tire/coupon/screen")
+@Api(value = "优惠券大屏", tags = "优惠券大屏")
+public class CouponScreenController {
+
+	private final ICouponScreenService couponScreenService;
+
+	/**
+	 * 大屏总览:KPI + 趋势 + 使用排行
+	 *
+	 * @param query 查询参数
+	 * @return 总览结果
+	 */
+	@PostMapping("/overview")
+	@ApiOperation("大屏总览:KPI + 趋势 + 店内排行")
+	public R<CouponScreenOverviewVO> overview(@RequestBody CouponScreenQuery query) {
+		return R.data(couponScreenService.overview(query));
+	}
+
+	/**
+	 * 单独获取趋势数据
+	 *
+	 * @param query 查询参数
+	 * @return 趋势结果
+	 */
+	@PostMapping("/acquire-use-trend")
+	@ApiOperation("趋势(可与 overview 同结构,便于单独刷新)")
+	public R<Map<String, Object>> acquireUseTrend(@RequestBody CouponScreenQuery query) {
+		return R.data(couponScreenService.acquireUseTrend(query));
+	}
+
+	/**
+	 * 获取可选客户(租户)列表
+	 *
+	 * @return 列表数据
+	 */
+	@PostMapping("/getCustomerList")
+	public R<List<CustomerSelectVo>> getCustomerList() {
+		return R.data(couponScreenService.getCustomerList());
+	}
+
+	/**
+	 * 导出使用排行(与大屏使用排行口径一致)
+	 *
+	 * @param query    查询参数
+	 * @param response 响应流
+	 */
+	@PostMapping("/export-usage-rank")
+	@ApiOperation("导出使用排行(与大屏排行一致)")
+	public void exportUsageRank(@RequestBody CouponScreenQuery query, HttpServletResponse response) {
+		couponScreenService.exportUsageRank(query, response);
+	}
+
+
+}

+ 98 - 0
blade-service/blade-sales-part/src/main/java/org/springblade/salesPart/coupon/mapper/CouponScreenMapper.java

@@ -0,0 +1,98 @@
+package org.springblade.salesPart.coupon.mapper;
+
+import org.apache.ibatis.annotations.Param;
+import org.springblade.core.tenant.annotation.TenantIgnore;
+import org.springblade.salesPart.coupon.screen.*;
+
+import java.util.List;
+
+/**
+ * 优惠券大屏统计(表名、字段请按实际库表微调)
+ *
+ * @author Rain
+ */
+@TenantIgnore
+public interface CouponScreenMapper {
+
+	/**
+	 * 订单侧:已付/待付红包汇总
+	 *
+	 * @param tenantId  租户ID(可空,空时查全部租户)
+	 * @param yearMonth 统计月份 yyyy-MM(可空)
+	 * @return 红包金额汇总
+	 */
+	CouponOrderAmountAggVO sumOrderRedPacketByPayStatus(@Param("tenantId") String tenantId,
+														@Param("yearMonth") String yearMonth);
+
+	/**
+	 * 用户券:领取/使用张数汇总
+	 *
+	 * @param tenantId  租户ID(可空,空时查全部租户)
+	 * @param yearMonth 统计月份 yyyy-MM(可空)
+	 * @return 用户券汇总
+	 */
+	CouponUserCouponCountAggVO sumUserCouponCounts(@Param("tenantId") String tenantId,
+												   @Param("yearMonth") String yearMonth);
+
+	/**
+	 * 按日:订单红包已付/待付(用于趋势图)
+	 *
+	 * @param tenantId  租户ID(可空,空时查全部租户)
+	 * @param yearMonth 统计月份 yyyy-MM(可空)
+	 * @param trendFrom 趋势起始日期 yyyy-MM-dd
+	 * @param trendTo   趋势结束日期 yyyy-MM-dd
+	 * @return 按日红包金额
+	 */
+	List<CouponDailyOrderAmountVO> dailyOrderRedPacket(@Param("tenantId") String tenantId,
+													   @Param("yearMonth") String yearMonth,
+													   @Param("trendFrom") String trendFrom,
+													   @Param("trendTo") String trendTo);
+
+	/**
+	 * 按日:领取张数(按 acquire_time)
+	 *
+	 * @param tenantId  租户ID(可空,空时查全部租户)
+	 * @param yearMonth 统计月份 yyyy-MM(可空)
+	 * @param trendFrom 趋势起始日期 yyyy-MM-dd
+	 * @param trendTo   趋势结束日期 yyyy-MM-dd
+	 * @return 按日领取数量
+	 */
+	List<CouponDailyCountVO> dailyReceivedCount(@Param("tenantId") String tenantId,
+												@Param("yearMonth") String yearMonth,
+												@Param("trendFrom") String trendFrom,
+												@Param("trendTo") String trendTo);
+
+	/**
+	 * 按日:使用张数(按 used_time,仅 status=已使用)
+	 *
+	 * @param tenantId  租户ID(可空,空时查全部租户)
+	 * @param yearMonth 统计月份 yyyy-MM(可空)
+	 * @param trendFrom 趋势起始日期 yyyy-MM-dd
+	 * @param trendTo   趋势结束日期 yyyy-MM-dd
+	 * @return 按日使用数量
+	 */
+	List<CouponDailyCountVO> dailyUsedCount(@Param("tenantId") String tenantId,
+											@Param("yearMonth") String yearMonth,
+											@Param("trendFrom") String trendFrom,
+											@Param("trendTo") String trendTo);
+
+	/**
+	 * 使用排行:按客户汇总用券笔数、券张数与红包金额
+	 *
+	 * @param tenantId  租户ID(可空,空时查全部租户)
+	 * @param yearMonth 统计月份 yyyy-MM(可空)
+	 * @param limit     返回条数
+	 * @return 使用排行列表
+	 */
+	List<UsageRankItemVO> usageRankByCustomer(@Param("tenantId") String tenantId,
+											  @Param("yearMonth") String yearMonth,
+											  @Param("limit") int limit);
+
+
+	/**
+	 * 获取客户列表
+	 *
+	 * @return 客户列表
+	 */
+	List<CustomerSelectVo> getCustomerList();
+}

+ 164 - 0
blade-service/blade-sales-part/src/main/java/org/springblade/salesPart/coupon/mapper/CouponScreenMapper.xml

@@ -0,0 +1,164 @@
+<?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">
+<!--suppress ALL -->
+<mapper namespace="org.springblade.salesPart.coupon.mapper.CouponScreenMapper">
+
+    <!--
+      说明(请按实际库表修改):
+      1. pjpf_order:若不存在 corps_id,请改为与门店关联条件(如 customer_id 子查询、storage 关联等)。
+      2. actual_payment_status:已支付=1;若与现网不一致,请改 CASE 条件。
+      3. tire_user_coupon:若无 is_deleted 字段,删除对应条件。
+      4. corps 名称:selectCorpsName 的表名/字段改为你们 blade-sales-part 中实际表。
+    -->
+
+    <select id="sumOrderRedPacketByPayStatus" resultType="org.springblade.salesPart.coupon.screen.CouponOrderAmountAggVO">
+        SELECT
+        COALESCE(SUM(CASE WHEN o.actual_payment_status IN (2, 3) THEN COALESCE(o.red_packet_amount, 0) ELSE 0 END), 0) AS
+        paid,
+        COALESCE(SUM(CASE WHEN (o.actual_payment_status IS NULL OR o.actual_payment_status NOT IN (2, 3))
+        AND COALESCE(o.red_packet_amount, 0) > 0 THEN COALESCE(o.red_packet_amount, 0) ELSE 0 END), 0) AS unpaid
+        FROM pjpf_order o
+        WHERE o.is_deleted = 0
+        AND o.bs_type = 'XS'
+        <if test="tenantId != null and tenantId != ''">
+            AND o.tenant_id = #{tenantId}
+        </if>
+        <if test="yearMonth != null and yearMonth != ''">
+            AND DATE_FORMAT(o.busines_date, '%Y-%m') = #{yearMonth}
+        </if>
+    </select>
+
+    <select id="sumUserCouponCounts" resultType="org.springblade.salesPart.coupon.screen.CouponUserCouponCountAggVO">
+        SELECT
+        COUNT(1) AS totalReceived,
+        COALESCE(SUM(CASE WHEN uc.status = 1 THEN 1 ELSE 0 END), 0) AS totalUsed
+        FROM tire_user_coupon uc
+        WHERE 1 = 1
+        <if test="tenantId != null and tenantId != ''">
+            AND uc.tenant_id = #{tenantId}
+        </if>
+        <if test="yearMonth != null and yearMonth != ''">
+            AND DATE_FORMAT(uc.acquire_time, '%Y-%m') = #{yearMonth}
+        </if>
+    </select>
+
+    <select id="dailyOrderRedPacket" resultType="org.springblade.salesPart.coupon.screen.CouponDailyOrderAmountVO">
+        SELECT
+        DATE_FORMAT(o.busines_date, '%Y-%m-%d') AS dt,
+        COALESCE(SUM(CASE WHEN o.actual_payment_status IN (2, 3) THEN COALESCE(o.red_packet_amount, 0) ELSE 0 END), 0) AS
+        paid,
+        COALESCE(SUM(CASE WHEN (o.actual_payment_status IS NULL OR o.actual_payment_status NOT IN (2, 3))
+        AND COALESCE(o.red_packet_amount, 0) > 0 THEN COALESCE(o.red_packet_amount, 0) ELSE 0 END), 0) AS unpaid
+        FROM pjpf_order o
+        WHERE o.is_deleted = 0
+        AND o.bs_type = 'XS'
+        <if test="tenantId != null and tenantId != ''">
+            AND o.tenant_id = #{tenantId}
+        </if>
+        <choose>
+            <when test="yearMonth != null and yearMonth != ''">
+                AND DATE_FORMAT(o.busines_date, '%Y-%m') = #{yearMonth}
+            </when>
+            <otherwise>
+                AND DATE(o.busines_date) &gt;= #{trendFrom}
+                AND DATE(o.busines_date) &lt;= #{trendTo}
+            </otherwise>
+        </choose>
+        GROUP BY DATE_FORMAT(o.busines_date, '%Y-%m-%d')
+        ORDER BY dt
+    </select>
+
+    <select id="dailyReceivedCount" resultType="org.springblade.salesPart.coupon.screen.CouponDailyCountVO">
+        SELECT DATE_FORMAT(uc.acquire_time, '%Y-%m-%d') AS dt, COUNT(1) AS cnt
+        FROM tire_user_coupon uc
+        WHERE 1 = 1
+        <if test="tenantId != null and tenantId != ''">
+            AND uc.tenant_id = #{tenantId}
+        </if>
+        <choose>
+            <when test="yearMonth != null and yearMonth != ''">
+                AND DATE_FORMAT(uc.acquire_time, '%Y-%m') = #{yearMonth}
+            </when>
+            <otherwise>
+                AND DATE(uc.acquire_time) &gt;= #{trendFrom}
+                AND DATE(uc.acquire_time) &lt;= #{trendTo}
+            </otherwise>
+        </choose>
+        GROUP BY DATE_FORMAT(uc.acquire_time, '%Y-%m-%d')
+        ORDER BY dt
+    </select>
+
+    <select id="dailyUsedCount" resultType="org.springblade.salesPart.coupon.screen.CouponDailyCountVO">
+        SELECT DATE_FORMAT(uc.used_time, '%Y-%m-%d') AS dt, COUNT(1) AS cnt
+        FROM tire_user_coupon uc
+        WHERE 1 = 1
+        <if test="tenantId != null and tenantId != ''">
+            AND uc.tenant_id = #{tenantId}
+        </if>
+        AND uc.status = 1
+        AND uc.used_time IS NOT NULL
+        <choose>
+            <when test="yearMonth != null and yearMonth != ''">
+                AND DATE_FORMAT(uc.used_time, '%Y-%m') = #{yearMonth}
+            </when>
+            <otherwise>
+                AND DATE(uc.used_time) &gt;= #{trendFrom}
+                AND DATE(uc.used_time) &lt;= #{trendTo}
+            </otherwise>
+        </choose>
+        GROUP BY DATE_FORMAT(uc.used_time, '%Y-%m-%d')
+        ORDER BY dt
+    </select>
+
+    <select id="usageRankByCustomer" resultType="org.springblade.salesPart.coupon.screen.UsageRankItemVO">
+        SELECT t.name,
+               t.salesCompanyName,
+               t.usedCount,
+               t.usedCouponCount,
+               t.paidRedPacketAmount,
+               t.unpaidRedPacketAmount
+        FROM (
+                 SELECT
+                     o.customer_name AS name,
+                     MAX(o.sales_company_name) AS salesCompanyName,
+                     COUNT(DISTINCT o.id) AS usedCount,
+                     COALESCE(SUM(COALESCE(uc_order.usedCouponCount, 0)), 0) AS usedCouponCount,
+                     COALESCE(SUM(CASE WHEN o.actual_payment_status IN (2, 3) THEN COALESCE(o.red_packet_amount, 0) ELSE 0 END), 0) AS paidRedPacketAmount,
+                     COALESCE(SUM(CASE WHEN (o.actual_payment_status IS NULL OR o.actual_payment_status NOT IN (2, 3))
+                         AND COALESCE(o.red_packet_amount, 0) > 0 THEN COALESCE(o.red_packet_amount, 0) ELSE 0 END), 0) AS unpaidRedPacketAmount
+                 FROM pjpf_order o
+                          LEFT JOIN (
+                             SELECT uc.order_id, COUNT(1) AS usedCouponCount
+                             FROM tire_user_coupon uc
+                             WHERE uc.status = 1
+                               AND uc.used_time IS NOT NULL
+                             <if test="tenantId != null and tenantId != ''">
+                                 AND uc.tenant_id = #{tenantId}
+                             </if>
+                             GROUP BY uc.order_id
+                         ) uc_order ON uc_order.order_id = o.id
+                 WHERE o.is_deleted = 0
+                   AND o.bs_type = 'XS'
+                   AND COALESCE(o.red_packet_amount, 0) > 0
+                 <if test="tenantId != null and tenantId != ''">
+                     AND o.tenant_id = #{tenantId}
+                 </if>
+                 <if test="yearMonth != null and yearMonth != ''">
+                     AND DATE_FORMAT(o.busines_date, '%Y-%m') = #{yearMonth}
+                 </if>
+                 GROUP BY o.customer_id, o.customer_name
+             ) t
+        ORDER BY (t.paidRedPacketAmount + t.unpaidRedPacketAmount) DESC
+        LIMIT #{limit}
+    </select>
+
+    <select id="getCustomerList" resultType="org.springblade.salesPart.coupon.screen.CustomerSelectVo">
+        SELECT
+            bt.tenant_id as tenantId,
+            bt.tenant_name as tenantName
+        FROM
+            blade_param_service bps
+                INNER JOIN blade_tenant bt ON bps.tenant_id = bt.tenant_id
+        WHERE bps.param_key = 'whether.financing' and bps.param_value = 1
+    </select>
+</mapper>

+ 48 - 0
blade-service/blade-sales-part/src/main/java/org/springblade/salesPart/coupon/service/ICouponScreenService.java

@@ -0,0 +1,48 @@
+package org.springblade.salesPart.coupon.service;
+
+import org.springblade.salesPart.coupon.screen.CouponScreenQuery;
+import org.springblade.salesPart.coupon.screen.CouponScreenOverviewVO;
+import org.springblade.salesPart.coupon.screen.CustomerSelectVo;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 优惠券大屏服务
+ *
+ * @author Rain
+ */
+public interface ICouponScreenService {
+
+    /**
+     * 大屏总览(KPI + 趋势 + 排行)
+     *
+     * @param query 查询参数
+     * @return 总览结果
+     */
+    CouponScreenOverviewVO overview(CouponScreenQuery query);
+
+	/**
+	 * 趋势数据(便于前端单独刷新)
+	 *
+	 * @param query 查询参数
+	 * @return 趋势结果
+	 */
+	Map<String, Object> acquireUseTrend(CouponScreenQuery query);
+
+	/**
+	 * 获取客户列表
+	 *
+	 * @return 客户列表
+	 */
+	List<CustomerSelectVo> getCustomerList();
+
+	/**
+	 * 导出使用排行(与 buildUsageRank 一致)
+	 *
+	 * @param query 查询参数
+	 * @param response 响应流
+	 */
+	void exportUsageRank(CouponScreenQuery query, HttpServletResponse response);
+}

+ 184 - 0
blade-service/blade-sales-part/src/main/java/org/springblade/salesPart/coupon/service/impl/CouponScreenServiceImpl.java

@@ -0,0 +1,184 @@
+package org.springblade.salesPart.coupon.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import org.springblade.core.tenant.annotation.TenantIgnore;
+import org.springblade.core.excel.util.ExcelUtil;
+import org.springblade.core.tool.utils.BeanUtil;
+import org.springblade.salesPart.coupon.excel.CouponUsageRankExcel;
+import org.springblade.salesPart.coupon.mapper.CouponScreenMapper;
+import org.springblade.salesPart.coupon.screen.*;
+import org.springblade.salesPart.coupon.service.ICouponScreenService;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import javax.servlet.http.HttpServletResponse;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.YearMonth;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+/**
+ * 优惠券大屏统计实现
+ *
+ * @author Rain
+ */
+@Service
+@RequiredArgsConstructor
+public class CouponScreenServiceImpl implements ICouponScreenService {
+
+	private final CouponScreenMapper couponScreenMapper;
+
+	private static final DateTimeFormatter DAY = DateTimeFormatter.ISO_LOCAL_DATE;
+
+	@Override
+	@TenantIgnore
+	public CouponScreenOverviewVO overview(CouponScreenQuery query) {
+		normalizeQuery(query);
+		CouponScreenOverviewVO vo = new CouponScreenOverviewVO();
+		vo.setStoreName(null);
+		vo.setKpis(buildKpi(query));
+		vo.setTrend(buildTrend(query));
+		vo.setUsageRank(buildUsageRank(query));
+		return vo;
+	}
+
+	@Override
+	@TenantIgnore
+	public Map<String, Object> acquireUseTrend(CouponScreenQuery query) {
+		normalizeQuery(query);
+		Map<String, Object> m = new HashMap<>(2);
+		m.put("trend", buildTrend(query));
+		return m;
+	}
+
+	@Override
+	public List<CustomerSelectVo> getCustomerList() {
+		return couponScreenMapper.getCustomerList();
+	}
+
+	@Override
+	@TenantIgnore
+	public void exportUsageRank(CouponScreenQuery query, HttpServletResponse response) {
+		normalizeQuery(query);
+		List<UsageRankItemVO> rankList = buildUsageRank(query);
+		List<CouponUsageRankExcel> excelList = BeanUtil.copy(rankList, CouponUsageRankExcel.class);
+		ExcelUtil.export(response, "优惠券使用排行", "使用排行", excelList, CouponUsageRankExcel.class);
+	}
+
+	private void normalizeQuery(CouponScreenQuery query) {
+		if (query == null) {
+			throw new IllegalArgumentException("query不能为空");
+		}
+	}
+
+	private CouponScreenKpiVO buildKpi(CouponScreenQuery query) {
+		CouponScreenKpiVO k = new CouponScreenKpiVO();
+		CouponOrderAmountAggVO orderAmt = couponScreenMapper.sumOrderRedPacketByPayStatus(
+			emptyToNull(query.getTenantId()), emptyToNull(query.getYearMonth()));
+		CouponUserCouponCountAggVO uc = couponScreenMapper.sumUserCouponCounts(
+			emptyToNull(query.getTenantId()), emptyToNull(query.getYearMonth()));
+
+		k.setPaidRedPacketAmount(orderAmt == null ? BigDecimal.ZERO : nullSafe(orderAmt.getPaid()));
+		k.setUnpaidRedPacketAmount(orderAmt == null ? BigDecimal.ZERO : nullSafe(orderAmt.getUnpaid()));
+		k.setTotalReceived(uc == null ? 0L : nullSafe(uc.getTotalReceived()));
+		k.setTotalUsed(uc == null ? 0L : nullSafe(uc.getTotalUsed()));
+		return k;
+	}
+
+	private List<UsageRankItemVO> buildUsageRank(CouponScreenQuery query) {
+		List<UsageRankItemVO> list = couponScreenMapper.usageRankByCustomer(
+			emptyToNull(query.getTenantId()), emptyToNull(query.getYearMonth()), 50);
+		return list != null ? list : Collections.emptyList();
+	}
+
+	private List<CouponTrendDailyVO> buildTrend(CouponScreenQuery query) {
+		String ym = emptyToNull(query.getYearMonth());
+		LocalDate from;
+		LocalDate to;
+		if (ym != null) {
+			YearMonth m = YearMonth.parse(ym);
+			from = m.atDay(1);
+			to = m.atEndOfMonth();
+		} else {
+			to = LocalDate.now();
+			from = to.minusDays(89);
+		}
+		String trendFrom = from.format(DAY);
+		String trendTo = to.format(DAY);
+
+		List<CouponDailyOrderAmountVO> orderDaily = couponScreenMapper.dailyOrderRedPacket(
+			emptyToNull(query.getTenantId()), ym, trendFrom, trendTo);
+		List<CouponDailyCountVO> recvDaily = couponScreenMapper.dailyReceivedCount(
+			emptyToNull(query.getTenantId()), ym, trendFrom, trendTo);
+		List<CouponDailyCountVO> usedDaily = couponScreenMapper.dailyUsedCount(
+			emptyToNull(query.getTenantId()), ym, trendFrom, trendTo);
+
+		Map<String, BigDecimal> paidByDay = new HashMap<>();
+		Map<String, BigDecimal> unpaidByDay = new HashMap<>();
+		Map<String, Long> recvByDay = new HashMap<>();
+		Map<String, Long> usedByDay = new HashMap<>();
+		if (orderDaily != null) {
+			for (CouponDailyOrderAmountVO row : orderDaily) {
+				String key = normalizeDtKey(row.getDt());
+				paidByDay.put(key, nullSafe(row.getPaid()));
+				unpaidByDay.put(key, nullSafe(row.getUnpaid()));
+			}
+		}
+		if (recvDaily != null) {
+			for (CouponDailyCountVO row : recvDaily) {
+				recvByDay.put(normalizeDtKey(row.getDt()), nullSafe(row.getCnt()));
+			}
+		}
+		if (usedDaily != null) {
+			for (CouponDailyCountVO row : usedDaily) {
+				usedByDay.put(normalizeDtKey(row.getDt()), nullSafe(row.getCnt()));
+			}
+		}
+
+		List<CouponTrendDailyVO> out = new ArrayList<>();
+		for (LocalDate d = from; !d.isAfter(to); d = d.plusDays(1)) {
+			String key = d.format(DAY);
+			CouponTrendDailyVO row = new CouponTrendDailyVO();
+			row.setDate(key);
+			row.setPaidRedPacketAmount(paidByDay.getOrDefault(key, BigDecimal.ZERO));
+			row.setUnpaidRedPacketAmount(unpaidByDay.getOrDefault(key, BigDecimal.ZERO));
+			row.setReceivedCount(recvByDay.getOrDefault(key, 0L));
+			row.setUsedCount(usedByDay.getOrDefault(key, 0L));
+			out.add(row);
+		}
+		return out;
+	}
+
+	private static String emptyToNull(String s) {
+		return StringUtils.hasText(s) ? s : null;
+	}
+
+	/**
+	 * 统一 yyyy-MM-dd,避免 JDBC 返回 Date/Timestamp 导致 key 对不上
+	 */
+	private static String normalizeDtKey(Object dt) {
+		if (dt == null) {
+			return "";
+		}
+		if (dt instanceof java.sql.Date) {
+			return ((java.sql.Date) dt).toLocalDate().format(DAY);
+		}
+		if (dt instanceof Date) {
+			return new java.sql.Timestamp(((Date) dt).getTime()).toLocalDateTime().toLocalDate().format(DAY);
+		}
+		if (dt instanceof LocalDate) {
+			return ((LocalDate) dt).format(DAY);
+		}
+		String s = dt.toString();
+		return s.length() >= 10 ? s.substring(0, 10) : s;
+	}
+
+	private static BigDecimal nullSafe(BigDecimal value) {
+		return value == null ? BigDecimal.ZERO : value;
+	}
+
+	private static Long nullSafe(Long value) {
+		return value == null ? 0L : value;
+	}
+}