Ver Fonte

智能柜项目提交

skyline há 2 meses atrás
pai
commit
eb0ac15f98
53 ficheiros alterados com 2913 adições e 12 exclusões
  1. 0 1
      .gitignore.local
  2. 6 0
      haha-admin/pom.xml
  3. 9 0
      haha-admin/src/main/java/com/haha/admin/config/SchedulingConfig.java
  4. 105 0
      haha-admin/src/main/java/com/haha/admin/controller/CouponController.java
  5. 117 0
      haha-admin/src/main/java/com/haha/admin/controller/MarketingActivityController.java
  6. 26 0
      haha-admin/src/main/java/com/haha/admin/task/CouponExpireTask.java
  7. 26 0
      haha-admin/src/main/java/com/haha/admin/task/MarketingTask.java
  8. 50 0
      haha-common/src/main/java/com/haha/common/constant/MarketingConstants.java
  9. 82 0
      haha-common/src/main/java/com/haha/common/enums/ActivityStatusEnum.java
  10. 60 0
      haha-common/src/main/java/com/haha/common/enums/ActivityTypeEnum.java
  11. 59 0
      haha-common/src/main/java/com/haha/common/enums/ApplyScopeEnum.java
  12. 64 0
      haha-common/src/main/java/com/haha/common/enums/CouponStatusEnum.java
  13. 61 0
      haha-common/src/main/java/com/haha/common/enums/CouponTypeEnum.java
  14. 60 0
      haha-common/src/main/java/com/haha/common/enums/DistributeTypeEnum.java
  15. 60 0
      haha-common/src/main/java/com/haha/common/enums/TargetTypeEnum.java
  16. 59 0
      haha-common/src/main/java/com/haha/common/enums/ValidTypeEnum.java
  17. 44 0
      haha-common/src/main/java/com/haha/common/vo/CouponTemplateVO.java
  18. 46 0
      haha-common/src/main/java/com/haha/common/vo/CouponVO.java
  19. 24 0
      haha-entity/src/main/java/com/haha/entity/ActivityProduct.java
  20. 24 0
      haha-entity/src/main/java/com/haha/entity/ActivityShop.java
  21. 34 0
      haha-entity/src/main/java/com/haha/entity/CouponDistribute.java
  22. 24 0
      haha-entity/src/main/java/com/haha/entity/CouponProduct.java
  23. 24 0
      haha-entity/src/main/java/com/haha/entity/CouponShop.java
  24. 76 0
      haha-entity/src/main/java/com/haha/entity/CouponTemplate.java
  25. 78 0
      haha-entity/src/main/java/com/haha/entity/MarketingActivity.java
  26. 44 0
      haha-entity/src/main/java/com/haha/entity/MarketingStatistics.java
  27. 59 0
      haha-entity/src/main/java/com/haha/entity/UserCoupon.java
  28. 39 0
      haha-entity/src/main/java/com/haha/entity/dto/ActivityCreateDTO.java
  29. 23 0
      haha-entity/src/main/java/com/haha/entity/dto/ActivityQueryDTO.java
  30. 37 0
      haha-entity/src/main/java/com/haha/entity/dto/ActivityUpdateDTO.java
  31. 47 0
      haha-entity/src/main/java/com/haha/entity/dto/CouponCreateDTO.java
  32. 17 0
      haha-entity/src/main/java/com/haha/entity/dto/CouponDistributeDTO.java
  33. 25 0
      haha-entity/src/main/java/com/haha/entity/dto/CouponQueryDTO.java
  34. 19 0
      haha-entity/src/main/java/com/haha/entity/dto/MarketingStatQueryDTO.java
  35. 23 0
      haha-mapper/src/main/java/com/haha/mapper/ActivityProductMapper.java
  36. 23 0
      haha-mapper/src/main/java/com/haha/mapper/ActivityShopMapper.java
  37. 16 0
      haha-mapper/src/main/java/com/haha/mapper/CouponDistributeMapper.java
  38. 20 0
      haha-mapper/src/main/java/com/haha/mapper/CouponProductMapper.java
  39. 20 0
      haha-mapper/src/main/java/com/haha/mapper/CouponShopMapper.java
  40. 35 0
      haha-mapper/src/main/java/com/haha/mapper/CouponTemplateMapper.java
  41. 33 0
      haha-mapper/src/main/java/com/haha/mapper/MarketingActivityMapper.java
  42. 24 0
      haha-mapper/src/main/java/com/haha/mapper/MarketingStatisticsMapper.java
  43. 43 0
      haha-mapper/src/main/java/com/haha/mapper/UserCouponMapper.java
  44. 75 0
      haha-miniapp/src/main/java/com/haha/miniapp/controller/AppCouponController.java
  45. 37 0
      haha-service/src/main/java/com/haha/service/CouponTemplateService.java
  46. 38 0
      haha-service/src/main/java/com/haha/service/MarketingActivityService.java
  47. 28 0
      haha-service/src/main/java/com/haha/service/MarketingStatisticsService.java
  48. 31 0
      haha-service/src/main/java/com/haha/service/UserCouponService.java
  49. 251 0
      haha-service/src/main/java/com/haha/service/impl/CouponTemplateServiceImpl.java
  50. 286 0
      haha-service/src/main/java/com/haha/service/impl/MarketingActivityServiceImpl.java
  51. 99 0
      haha-service/src/main/java/com/haha/service/impl/MarketingStatisticsServiceImpl.java
  52. 281 0
      haha-service/src/main/java/com/haha/service/impl/UserCouponServiceImpl.java
  53. 22 11
      pom.xml

+ 0 - 1
.gitignore.local

@@ -1,6 +1,5 @@
 haha-miniapp/target/
 .idea/
 *.class
-haha-sdk/target/haha-sdk-1.0.0.jar
 haha-sdk/target/
 .vscode/

+ 6 - 0
haha-admin/pom.xml

@@ -87,6 +87,12 @@
                 <artifactId>spring-boot-maven-plugin</artifactId>
                 <configuration>
                     <mainClass>com.haha.admin.AdminApplication</mainClass>
+                    <excludes>
+                        <exclude>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                        </exclude>
+                    </excludes>
                 </configuration>
                 <executions>
                     <execution>

+ 9 - 0
haha-admin/src/main/java/com/haha/admin/config/SchedulingConfig.java

@@ -0,0 +1,9 @@
+package com.haha.admin.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+@Configuration
+@EnableScheduling
+public class SchedulingConfig {
+}

+ 105 - 0
haha-admin/src/main/java/com/haha/admin/controller/CouponController.java

@@ -0,0 +1,105 @@
+package com.haha.admin.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.haha.admin.annotation.RequirePermission;
+import com.haha.common.annotation.Log;
+import com.haha.common.enums.OperationType;
+import com.haha.common.vo.PageResult;
+import com.haha.common.vo.Result;
+import com.haha.entity.CouponTemplate;
+import com.haha.entity.dto.CouponCreateDTO;
+import com.haha.entity.dto.CouponQueryDTO;
+import com.haha.entity.dto.CouponDistributeDTO;
+import com.haha.service.CouponTemplateService;
+import com.haha.service.UserCouponService;
+import cn.dev33.satoken.stp.StpUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@RestController
+@RequestMapping("/marketing/coupon")
+@RequiredArgsConstructor
+public class CouponController {
+
+    private final CouponTemplateService templateService;
+    private final UserCouponService userCouponService;
+
+    @RequirePermission("marketing:coupon:read")
+    @GetMapping("/list")
+    public Result<PageResult<CouponTemplate>> list(CouponQueryDTO queryDTO) {
+        IPage<CouponTemplate> page = templateService.getPage(queryDTO);
+        return Result.success("查询成功", PageResult.of(page));
+    }
+
+    @RequirePermission("marketing:coupon:read")
+    @GetMapping("/{id}")
+    public Result<Map<String, Object>> getById(@PathVariable Long id) {
+        CouponTemplate template = templateService.getDetail(id);
+        List<Long> shopIds = templateService.getCouponShopIds(id);
+        List<Long> productIds = templateService.getCouponProductIds(id);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("template", template);
+        result.put("shopIds", shopIds);
+        result.put("productIds", productIds);
+
+        return Result.success("查询成功", result);
+    }
+
+    @RequirePermission("marketing:coupon:create")
+    @Log(module = "营销管理", operation = OperationType.INSERT, summary = "创建优惠券")
+    @PostMapping
+    public Result<CouponTemplate> create(@RequestBody CouponCreateDTO dto) {
+        Long creatorId = StpUtil.getLoginIdAsLong();
+        String creatorName = (String) StpUtil.getSession().get("name");
+
+        CouponTemplate template = templateService.create(dto, creatorId, creatorName);
+        return Result.success("创建成功", template);
+    }
+
+    @RequirePermission("marketing:coupon:edit")
+    @Log(module = "营销管理", operation = OperationType.UPDATE, summary = "更新优惠券状态")
+    @PutMapping("/{id}/status")
+    public Result<String> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
+        templateService.updateStatus(id, status);
+        return Result.success("更新成功");
+    }
+
+    @RequirePermission("marketing:coupon:delete")
+    @Log(module = "营销管理", operation = OperationType.DELETE, summary = "删除优惠券")
+    @DeleteMapping("/{id}")
+    public Result<String> delete(@PathVariable Long id) {
+        templateService.deleteTemplate(id);
+        return Result.success("删除成功");
+    }
+
+    @RequirePermission("marketing:coupon:distribute")
+    @Log(module = "营销管理", operation = OperationType.INSERT, summary = "发放优惠券")
+    @PostMapping("/distribute")
+    public Result<String> distribute(@RequestBody CouponDistributeDTO dto) {
+        Long operatorId = StpUtil.getLoginIdAsLong();
+        String operatorName = (String) StpUtil.getSession().get("name");
+
+        userCouponService.distributeCoupon(dto, operatorId, operatorName);
+        return Result.success("发放成功");
+    }
+
+    @RequirePermission("marketing:coupon:read")
+    @GetMapping("/{id}/statistics")
+    public Result<Map<String, Object>> getStatistics(@PathVariable Long id) {
+        Map<String, Object> stats = templateService.getStatistics(id);
+        return Result.success("查询成功", stats);
+    }
+
+    @GetMapping("/available")
+    public Result<List<CouponTemplate>> getAvailableTemplates() {
+        List<CouponTemplate> templates = templateService.getAvailableTemplates();
+        return Result.success("查询成功", templates);
+    }
+}

+ 117 - 0
haha-admin/src/main/java/com/haha/admin/controller/MarketingActivityController.java

@@ -0,0 +1,117 @@
+package com.haha.admin.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.haha.admin.annotation.RequirePermission;
+import com.haha.common.annotation.Log;
+import com.haha.common.enums.OperationType;
+import com.haha.common.vo.PageResult;
+import com.haha.common.vo.Result;
+import com.haha.entity.MarketingActivity;
+import com.haha.entity.dto.ActivityCreateDTO;
+import com.haha.entity.dto.ActivityQueryDTO;
+import com.haha.entity.dto.ActivityUpdateDTO;
+import com.haha.service.MarketingActivityService;
+import cn.dev33.satoken.stp.StpUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@RestController
+@RequestMapping("/marketing/activity")
+@RequiredArgsConstructor
+public class MarketingActivityController {
+
+    private final MarketingActivityService activityService;
+
+    @RequirePermission("marketing:activity:read")
+    @GetMapping("/list")
+    public Result<PageResult<MarketingActivity>> list(ActivityQueryDTO queryDTO) {
+        IPage<MarketingActivity> page = activityService.getPage(queryDTO);
+        return Result.success("查询成功", PageResult.of(page));
+    }
+
+    @RequirePermission("marketing:activity:read")
+    @GetMapping("/{id}")
+    public Result<Map<String, Object>> getById(@PathVariable Long id) {
+        MarketingActivity activity = activityService.getDetail(id);
+        List<Long> shopIds = activityService.getActivityShopIds(id);
+        List<Long> productIds = activityService.getActivityProductIds(id);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("activity", activity);
+        result.put("shopIds", shopIds);
+        result.put("productIds", productIds);
+
+        return Result.success("查询成功", result);
+    }
+
+    @RequirePermission("marketing:activity:create")
+    @Log(module = "营销管理", operation = OperationType.INSERT, summary = "创建活动")
+    @PostMapping
+    public Result<MarketingActivity> create(@RequestBody ActivityCreateDTO dto) {
+        Long creatorId = StpUtil.getLoginIdAsLong();
+        String creatorName = (String) StpUtil.getSession().get("name");
+
+        MarketingActivity activity = activityService.create(dto, creatorId, creatorName);
+        return Result.success("创建成功", activity);
+    }
+
+    @RequirePermission("marketing:activity:edit")
+    @Log(module = "营销管理", operation = OperationType.UPDATE, summary = "编辑活动")
+    @PutMapping("/{id}")
+    public Result<MarketingActivity> update(@PathVariable Long id, @RequestBody ActivityUpdateDTO dto) {
+        dto.setId(id);
+        MarketingActivity activity = activityService.update(dto);
+        return Result.success("更新成功", activity);
+    }
+
+    @RequirePermission("marketing:activity:publish")
+    @Log(module = "营销管理", operation = OperationType.UPDATE, summary = "发布活动")
+    @PutMapping("/{id}/publish")
+    public Result<Void> publish(@PathVariable Long id) {
+        activityService.publish(id);
+        return Result.success("发布成功", null);
+    }
+
+    @RequirePermission("marketing:activity:edit")
+    @Log(module = "营销管理", operation = OperationType.UPDATE, summary = "暂停活动")
+    @PutMapping("/{id}/pause")
+    public Result<Void> pause(@PathVariable Long id) {
+        activityService.pause(id);
+        return Result.success("暂停成功", null);
+    }
+
+    @RequirePermission("marketing:activity:edit")
+    @Log(module = "营销管理", operation = OperationType.UPDATE, summary = "恢复活动")
+    @PutMapping("/{id}/resume")
+    public Result<Void> resume(@PathVariable Long id) {
+        activityService.resume(id);
+        return Result.success("恢复成功", null);
+    }
+
+    @RequirePermission("marketing:activity:delete")
+    @Log(module = "营销管理", operation = OperationType.DELETE, summary = "删除活动")
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(@PathVariable Long id) {
+        activityService.deleteActivity(id);
+        return Result.success("删除成功", null);
+    }
+
+    @RequirePermission("marketing:activity:read")
+    @GetMapping("/{id}/statistics")
+    public Result<Map<String, Object>> getStatistics(@PathVariable Long id) {
+        Map<String, Object> stats = activityService.getStatistics(id);
+        return Result.success("查询成功", stats);
+    }
+
+    @GetMapping("/ongoing")
+    public Result<List<MarketingActivity>> getOngoingActivities() {
+        List<MarketingActivity> activities = activityService.getOngoingActivities();
+        return Result.success("查询成功", activities);
+    }
+}

+ 26 - 0
haha-admin/src/main/java/com/haha/admin/task/CouponExpireTask.java

@@ -0,0 +1,26 @@
+package com.haha.admin.task;
+
+import com.haha.service.UserCouponService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class CouponExpireTask {
+
+    private final UserCouponService userCouponService;
+
+    @Scheduled(cron = "0 0 * * * ?")
+    public void expireCoupons() {
+        log.info("开始执行优惠券过期处理任务");
+        try {
+            userCouponService.expireCoupons();
+            log.info("优惠券过期处理任务执行完成");
+        } catch (Exception e) {
+            log.error("优惠券过期处理任务执行失败", e);
+        }
+    }
+}

+ 26 - 0
haha-admin/src/main/java/com/haha/admin/task/MarketingTask.java

@@ -0,0 +1,26 @@
+package com.haha.admin.task;
+
+import com.haha.service.UserCouponService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class MarketingTask {
+
+    private final UserCouponService userCouponService;
+
+    @Scheduled(cron = "0 0 * * * ?")
+    public void expireCoupons() {
+        log.info("开始执行优惠券过期处理任务");
+        try {
+            userCouponService.expireCoupons();
+            log.info("优惠券过期处理任务执行完成");
+        } catch (Exception e) {
+            log.error("优惠券过期处理任务执行失败", e);
+        }
+    }
+}

+ 50 - 0
haha-common/src/main/java/com/haha/common/constant/MarketingConstants.java

@@ -0,0 +1,50 @@
+package com.haha.common.constant;
+
+public final class MarketingConstants {
+
+    private MarketingConstants() {}
+
+    public static final String COUPON_CODE_PREFIX = "CP";
+
+    public static final int COUPON_CODE_LENGTH = 16;
+
+    public static final String REDIS_KEY_COUPON_STOCK = "coupon:stock:";
+
+    public static final String REDIS_KEY_COUPON_RECEIVE_LOCK = "coupon:receive:lock:";
+
+    public static final String REDIS_KEY_COUPON_USER_RECEIVED = "coupon:user:received:";
+
+    public static final String REDIS_KEY_ACTIVITY_INFO = "activity:info:";
+
+    public static final String REDIS_KEY_ACTIVITY_LIST = "activity:list";
+
+    public static final long REDIS_CACHE_EXPIRE_HOURS = 1;
+
+    public static final long REDIS_CACHE_EXPIRE_MINUTES = 30;
+
+    public static final int MAX_RECEIVE_LIMIT = 10;
+
+    public static final int DEFAULT_RECEIVE_LIMIT = 1;
+
+    public static final int MAX_VALID_DAYS = 365;
+
+    public static final int DEFAULT_VALID_DAYS = 30;
+
+    public static final int EXPIRE_REMIND_DAYS = 3;
+
+    public static final String ACTIVITY_STATUS_DRAFT = "草稿";
+    public static final String ACTIVITY_STATUS_PUBLISHED = "已发布";
+    public static final String ACTIVITY_STATUS_ONGOING = "进行中";
+    public static final String ACTIVITY_STATUS_PAUSED = "已暂停";
+    public static final String ACTIVITY_STATUS_ENDED = "已结束";
+
+    public static final String COUPON_STATUS_UNUSED = "未使用";
+    public static final String COUPON_STATUS_USED = "已使用";
+    public static final String COUPON_STATUS_EXPIRED = "已过期";
+
+    public static final int DISCOUNT_SCALE = 2;
+
+    public static final int MAX_DISCOUNT_PERCENT = 100;
+
+    public static final int MIN_DISCOUNT_PERCENT = 1;
+}

+ 82 - 0
haha-common/src/main/java/com/haha/common/enums/ActivityStatusEnum.java

@@ -0,0 +1,82 @@
+package com.haha.common.enums;
+
+import com.haha.common.vo.StatusLabel;
+
+public enum ActivityStatusEnum {
+
+    DRAFT(0, "草稿", "info"),
+    PUBLISHED(1, "已发布", "primary"),
+    ONGOING(2, "进行中", "success"),
+    PAUSED(3, "已暂停", "warning"),
+    ENDED(4, "已结束", "danger");
+
+    private final int code;
+    private final String label;
+    private final String color;
+
+    ActivityStatusEnum(int code, String label, String color) {
+        this.code = code;
+        this.label = label;
+        this.color = color;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    public String getColor() {
+        return color;
+    }
+
+    public StatusLabel getStatusLabel() {
+        return new StatusLabel(label, color);
+    }
+
+    public static StatusLabel getLabelByCode(Integer code) {
+        if (code == null) {
+            return StatusLabel.info("未知");
+        }
+        for (ActivityStatusEnum status : values()) {
+            if (status.code == code) {
+                return status.getStatusLabel();
+            }
+        }
+        return StatusLabel.info("未知");
+    }
+
+    public static ActivityStatusEnum getByCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (ActivityStatusEnum status : values()) {
+            if (status.code == code) {
+                return status;
+            }
+        }
+        return null;
+    }
+
+    public static boolean canEdit(Integer status) {
+        return status != null && status == DRAFT.code;
+    }
+
+    public static boolean canPublish(Integer status) {
+        return status != null && status == DRAFT.code;
+    }
+
+    public static boolean canPause(Integer status) {
+        return status != null && status == ONGOING.code;
+    }
+
+    public static boolean canResume(Integer status) {
+        return status != null && status == PAUSED.code;
+    }
+
+    public static boolean canDelete(Integer status) {
+        return status != null && (status == DRAFT.code || status == ENDED.code);
+    }
+}

+ 60 - 0
haha-common/src/main/java/com/haha/common/enums/ActivityTypeEnum.java

@@ -0,0 +1,60 @@
+package com.haha.common.enums;
+
+import com.haha.common.vo.StatusLabel;
+
+public enum ActivityTypeEnum {
+
+    FIRST_ORDER_DISCOUNT(1, "首单立减", "primary"),
+    DISCOUNT(2, "优惠折扣", "success"),
+    FULL_REDUCTION(3, "商品满减", "warning");
+
+    private final int code;
+    private final String label;
+    private final String color;
+
+    ActivityTypeEnum(int code, String label, String color) {
+        this.code = code;
+        this.label = label;
+        this.color = color;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    public String getColor() {
+        return color;
+    }
+
+    public StatusLabel getStatusLabel() {
+        return new StatusLabel(label, color);
+    }
+
+    public static StatusLabel getLabelByCode(Integer code) {
+        if (code == null) {
+            return StatusLabel.info("未知");
+        }
+        for (ActivityTypeEnum type : values()) {
+            if (type.code == code) {
+                return type.getStatusLabel();
+            }
+        }
+        return StatusLabel.info("未知");
+    }
+
+    public static ActivityTypeEnum getByCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (ActivityTypeEnum type : values()) {
+            if (type.code == code) {
+                return type;
+            }
+        }
+        return null;
+    }
+}

+ 59 - 0
haha-common/src/main/java/com/haha/common/enums/ApplyScopeEnum.java

@@ -0,0 +1,59 @@
+package com.haha.common.enums;
+
+import com.haha.common.vo.StatusLabel;
+
+public enum ApplyScopeEnum {
+
+    ALL_SHOP(1, "全部门店", "primary"),
+    SPECIFIED_SHOP(2, "指定门店", "warning");
+
+    private final int code;
+    private final String label;
+    private final String color;
+
+    ApplyScopeEnum(int code, String label, String color) {
+        this.code = code;
+        this.label = label;
+        this.color = color;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    public String getColor() {
+        return color;
+    }
+
+    public StatusLabel getStatusLabel() {
+        return new StatusLabel(label, color);
+    }
+
+    public static StatusLabel getLabelByCode(Integer code) {
+        if (code == null) {
+            return StatusLabel.info("未知");
+        }
+        for (ApplyScopeEnum scope : values()) {
+            if (scope.code == code) {
+                return scope.getStatusLabel();
+            }
+        }
+        return StatusLabel.info("未知");
+    }
+
+    public static ApplyScopeEnum getByCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (ApplyScopeEnum scope : values()) {
+            if (scope.code == code) {
+                return scope;
+            }
+        }
+        return null;
+    }
+}

+ 64 - 0
haha-common/src/main/java/com/haha/common/enums/CouponStatusEnum.java

@@ -0,0 +1,64 @@
+package com.haha.common.enums;
+
+import com.haha.common.vo.StatusLabel;
+
+public enum CouponStatusEnum {
+
+    UNUSED(0, "未使用", "primary"),
+    USED(1, "已使用", "success"),
+    EXPIRED(2, "已过期", "danger");
+
+    private final int code;
+    private final String label;
+    private final String color;
+
+    CouponStatusEnum(int code, String label, String color) {
+        this.code = code;
+        this.label = label;
+        this.color = color;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    public String getColor() {
+        return color;
+    }
+
+    public StatusLabel getStatusLabel() {
+        return new StatusLabel(label, color);
+    }
+
+    public static StatusLabel getLabelByCode(Integer code) {
+        if (code == null) {
+            return StatusLabel.info("未知");
+        }
+        for (CouponStatusEnum status : values()) {
+            if (status.code == code) {
+                return status.getStatusLabel();
+            }
+        }
+        return StatusLabel.info("未知");
+    }
+
+    public static CouponStatusEnum getByCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (CouponStatusEnum status : values()) {
+            if (status.code == code) {
+                return status;
+            }
+        }
+        return null;
+    }
+
+    public static boolean canUse(Integer status) {
+        return status != null && status == UNUSED.code;
+    }
+}

+ 61 - 0
haha-common/src/main/java/com/haha/common/enums/CouponTypeEnum.java

@@ -0,0 +1,61 @@
+package com.haha.common.enums;
+
+import com.haha.common.vo.StatusLabel;
+
+public enum CouponTypeEnum {
+
+    FULL_REDUCTION(1, "满减券", "primary"),
+    DISCOUNT(2, "折扣券", "success"),
+    INSTANT_REDUCTION(3, "立减券", "warning"),
+    PRODUCT(4, "商品券", "info");
+
+    private final int code;
+    private final String label;
+    private final String color;
+
+    CouponTypeEnum(int code, String label, String color) {
+        this.code = code;
+        this.label = label;
+        this.color = color;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    public String getColor() {
+        return color;
+    }
+
+    public StatusLabel getStatusLabel() {
+        return new StatusLabel(label, color);
+    }
+
+    public static StatusLabel getLabelByCode(Integer code) {
+        if (code == null) {
+            return StatusLabel.info("未知");
+        }
+        for (CouponTypeEnum type : values()) {
+            if (type.code == code) {
+                return type.getStatusLabel();
+            }
+        }
+        return StatusLabel.info("未知");
+    }
+
+    public static CouponTypeEnum getByCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (CouponTypeEnum type : values()) {
+            if (type.code == code) {
+                return type;
+            }
+        }
+        return null;
+    }
+}

+ 60 - 0
haha-common/src/main/java/com/haha/common/enums/DistributeTypeEnum.java

@@ -0,0 +1,60 @@
+package com.haha.common.enums;
+
+import com.haha.common.vo.StatusLabel;
+
+public enum DistributeTypeEnum {
+
+    USER_RECEIVE(1, "主动领取", "primary"),
+    SYSTEM_SEND(2, "系统发放", "success"),
+    TARGET_SEND(3, "定向发放", "warning");
+
+    private final int code;
+    private final String label;
+    private final String color;
+
+    DistributeTypeEnum(int code, String label, String color) {
+        this.code = code;
+        this.label = label;
+        this.color = color;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    public String getColor() {
+        return color;
+    }
+
+    public StatusLabel getStatusLabel() {
+        return new StatusLabel(label, color);
+    }
+
+    public static StatusLabel getLabelByCode(Integer code) {
+        if (code == null) {
+            return StatusLabel.info("未知");
+        }
+        for (DistributeTypeEnum type : values()) {
+            if (type.code == code) {
+                return type.getStatusLabel();
+            }
+        }
+        return StatusLabel.info("未知");
+    }
+
+    public static DistributeTypeEnum getByCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (DistributeTypeEnum type : values()) {
+            if (type.code == code) {
+                return type;
+            }
+        }
+        return null;
+    }
+}

+ 60 - 0
haha-common/src/main/java/com/haha/common/enums/TargetTypeEnum.java

@@ -0,0 +1,60 @@
+package com.haha.common.enums;
+
+import com.haha.common.vo.StatusLabel;
+
+public enum TargetTypeEnum {
+
+    ALL_USER(1, "全部用户", "primary"),
+    NEW_USER(2, "新用户", "success"),
+    SPECIFIED_USER(3, "指定用户", "warning");
+
+    private final int code;
+    private final String label;
+    private final String color;
+
+    TargetTypeEnum(int code, String label, String color) {
+        this.code = code;
+        this.label = label;
+        this.color = color;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    public String getColor() {
+        return color;
+    }
+
+    public StatusLabel getStatusLabel() {
+        return new StatusLabel(label, color);
+    }
+
+    public static StatusLabel getLabelByCode(Integer code) {
+        if (code == null) {
+            return StatusLabel.info("未知");
+        }
+        for (TargetTypeEnum type : values()) {
+            if (type.code == code) {
+                return type.getStatusLabel();
+            }
+        }
+        return StatusLabel.info("未知");
+    }
+
+    public static TargetTypeEnum getByCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (TargetTypeEnum type : values()) {
+            if (type.code == code) {
+                return type;
+            }
+        }
+        return null;
+    }
+}

+ 59 - 0
haha-common/src/main/java/com/haha/common/enums/ValidTypeEnum.java

@@ -0,0 +1,59 @@
+package com.haha.common.enums;
+
+import com.haha.common.vo.StatusLabel;
+
+public enum ValidTypeEnum {
+
+    FIXED_TIME(1, "固定时间", "primary"),
+    RELATIVE_TIME(2, "相对时间", "success");
+
+    private final int code;
+    private final String label;
+    private final String color;
+
+    ValidTypeEnum(int code, String label, String color) {
+        this.code = code;
+        this.label = label;
+        this.color = color;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    public String getColor() {
+        return color;
+    }
+
+    public StatusLabel getStatusLabel() {
+        return new StatusLabel(label, color);
+    }
+
+    public static StatusLabel getLabelByCode(Integer code) {
+        if (code == null) {
+            return StatusLabel.info("未知");
+        }
+        for (ValidTypeEnum type : values()) {
+            if (type.code == code) {
+                return type.getStatusLabel();
+            }
+        }
+        return StatusLabel.info("未知");
+    }
+
+    public static ValidTypeEnum getByCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (ValidTypeEnum type : values()) {
+            if (type.code == code) {
+                return type;
+            }
+        }
+        return null;
+    }
+}

+ 44 - 0
haha-common/src/main/java/com/haha/common/vo/CouponTemplateVO.java

@@ -0,0 +1,44 @@
+package com.haha.common.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+public class CouponTemplateVO {
+
+    private Long id;
+
+    private String couponName;
+
+    private Integer couponType;
+
+    private String couponTypeLabel;
+
+    private String couponDesc;
+
+    private BigDecimal discountValue;
+
+    private BigDecimal minAmount;
+
+    private BigDecimal maxDiscount;
+
+    private Integer remainCount;
+
+    private Integer receiveLimit;
+
+    private Integer validType;
+
+    private String validTypeLabel;
+
+    private LocalDateTime validStartTime;
+
+    private LocalDateTime validEndTime;
+
+    private Integer validDays;
+
+    private Boolean canReceive;
+
+    private Integer receivedCount;
+}

+ 46 - 0
haha-common/src/main/java/com/haha/common/vo/CouponVO.java

@@ -0,0 +1,46 @@
+package com.haha.common.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+public class CouponVO {
+
+    private Long id;
+
+    private String couponCode;
+
+    private String couponName;
+
+    private Integer couponType;
+
+    private String couponTypeLabel;
+
+    private String couponDesc;
+
+    private BigDecimal discountValue;
+
+    private BigDecimal minAmount;
+
+    private BigDecimal maxDiscount;
+
+    private Integer status;
+
+    private String statusLabel;
+
+    private String statusColor;
+
+    private LocalDateTime validStartTime;
+
+    private LocalDateTime validEndTime;
+
+    private Boolean isExpired;
+
+    private Boolean canUse;
+
+    private Long templateId;
+
+    private Integer validDays;
+}

+ 24 - 0
haha-entity/src/main/java/com/haha/entity/ActivityProduct.java

@@ -0,0 +1,24 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_activity_product")
+public class ActivityProduct implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long activityId;
+
+    private Long productId;
+
+    private LocalDateTime createTime;
+}

+ 24 - 0
haha-entity/src/main/java/com/haha/entity/ActivityShop.java

@@ -0,0 +1,24 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_activity_shop")
+public class ActivityShop implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long activityId;
+
+    private Long shopId;
+
+    private LocalDateTime createTime;
+}

+ 34 - 0
haha-entity/src/main/java/com/haha/entity/CouponDistribute.java

@@ -0,0 +1,34 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_coupon_distribute")
+public class CouponDistribute implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long templateId;
+
+    private Integer distributeType;
+
+    private Integer targetType;
+
+    private Integer targetCount;
+
+    private Integer successCount;
+
+    private Long operatorId;
+
+    private String operatorName;
+
+    private LocalDateTime createTime;
+}

+ 24 - 0
haha-entity/src/main/java/com/haha/entity/CouponProduct.java

@@ -0,0 +1,24 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_coupon_product")
+public class CouponProduct implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long templateId;
+
+    private Long productId;
+
+    private LocalDateTime createTime;
+}

+ 24 - 0
haha-entity/src/main/java/com/haha/entity/CouponShop.java

@@ -0,0 +1,24 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_coupon_shop")
+public class CouponShop implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long templateId;
+
+    private Long shopId;
+
+    private LocalDateTime createTime;
+}

+ 76 - 0
haha-entity/src/main/java/com/haha/entity/CouponTemplate.java

@@ -0,0 +1,76 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_coupon_template")
+public class CouponTemplate implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String couponName;
+
+    private Integer couponType;
+
+    private String couponDesc;
+
+    private BigDecimal discountValue;
+
+    private BigDecimal minAmount;
+
+    private BigDecimal maxDiscount;
+
+    private Integer totalCount;
+
+    private Integer remainCount;
+
+    private Integer receiveLimit;
+
+    private Integer validType;
+
+    private LocalDateTime validStartTime;
+
+    private LocalDateTime validEndTime;
+
+    private Integer validDays;
+
+    private Integer applyScope;
+
+    private Integer productScope;
+
+    private Long activityId;
+
+    private Integer status;
+
+    private Long creatorId;
+
+    private String creatorName;
+
+    private LocalDateTime createTime;
+
+    private LocalDateTime updateTime;
+
+    private Integer deleted;
+
+    @TableField(exist = false)
+    private String typeLabel;
+
+    @TableField(exist = false)
+    private String validTypeLabel;
+
+    @TableField(exist = false)
+    private String statusLabel;
+
+    @TableField(exist = false)
+    private String statusColor;
+}

+ 78 - 0
haha-entity/src/main/java/com/haha/entity/MarketingActivity.java

@@ -0,0 +1,78 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+@TableName("t_marketing_activity")
+public class MarketingActivity implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String activityName;
+
+    private Integer activityType;
+
+    private String activityDesc;
+
+    private LocalDateTime startTime;
+
+    private LocalDateTime endTime;
+
+    private Integer status;
+
+    private BigDecimal discountValue;
+
+    private BigDecimal minAmount;
+
+    private BigDecimal maxDiscount;
+
+    private Integer applyScope;
+
+    private Integer productScope;
+
+    private BigDecimal totalBudget;
+
+    private BigDecimal usedBudget;
+
+    private Long creatorId;
+
+    private String creatorName;
+
+    private LocalDateTime createTime;
+
+    private LocalDateTime updateTime;
+
+    private Integer deleted;
+
+    @TableField(exist = false)
+    private String statusLabel;
+
+    @TableField(exist = false)
+    private String statusColor;
+
+    @TableField(exist = false)
+    private String typeLabel;
+
+    @TableField(exist = false)
+    private String applyScopeLabel;
+
+    @TableField(exist = false)
+    private String productScopeLabel;
+
+    @TableField(exist = false)
+    private List<Long> shopIds;
+
+    @TableField(exist = false)
+    private List<Long> productIds;
+}

+ 44 - 0
haha-entity/src/main/java/com/haha/entity/MarketingStatistics.java

@@ -0,0 +1,44 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_marketing_statistics")
+public class MarketingStatistics implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long activityId;
+
+    private LocalDate statDate;
+
+    private Integer participantCount;
+
+    private Integer newUserCount;
+
+    private Integer orderCount;
+
+    private BigDecimal orderAmount;
+
+    private BigDecimal discountAmount;
+
+    private BigDecimal actualAmount;
+
+    private Integer couponReceiveCount;
+
+    private Integer couponUseCount;
+
+    private LocalDateTime createTime;
+
+    private LocalDateTime updateTime;
+}

+ 59 - 0
haha-entity/src/main/java/com/haha/entity/UserCoupon.java

@@ -0,0 +1,59 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_user_coupon")
+public class UserCoupon implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String couponCode;
+
+    private Long templateId;
+
+    private Long userId;
+
+    private Long orderId;
+
+    private Integer status;
+
+    private LocalDateTime receiveTime;
+
+    private LocalDateTime useTime;
+
+    private LocalDateTime validStartTime;
+
+    private LocalDateTime validEndTime;
+
+    private BigDecimal discountAmount;
+
+    private LocalDateTime createTime;
+
+    private LocalDateTime updateTime;
+
+    @TableField(exist = false)
+    private String couponName;
+
+    @TableField(exist = false)
+    private Integer couponType;
+
+    @TableField(exist = false)
+    private BigDecimal minAmount;
+
+    @TableField(exist = false)
+    private String statusLabel;
+
+    @TableField(exist = false)
+    private String statusColor;
+}

+ 39 - 0
haha-entity/src/main/java/com/haha/entity/dto/ActivityCreateDTO.java

@@ -0,0 +1,39 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ActivityCreateDTO extends PageQueryDTO {
+
+    private String activityName;
+
+    private Integer activityType;
+
+    private String activityDesc;
+
+    private LocalDateTime startTime;
+
+    private LocalDateTime endTime;
+
+    private BigDecimal discountValue;
+
+    private BigDecimal minAmount;
+
+    private BigDecimal maxDiscount;
+
+    private Integer applyScope;
+
+    private Integer productScope;
+
+    private BigDecimal totalBudget;
+
+    private List<Long> shopIds;
+
+    private List<Long> productIds;
+}

+ 23 - 0
haha-entity/src/main/java/com/haha/entity/dto/ActivityQueryDTO.java

@@ -0,0 +1,23 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ActivityQueryDTO extends PageQueryDTO {
+
+    private String activityName;
+
+    private Integer activityType;
+
+    private Integer status;
+
+    private LocalDateTime startTimeBegin;
+
+    private LocalDateTime startTimeEnd;
+
+    private Long creatorId;
+}

+ 37 - 0
haha-entity/src/main/java/com/haha/entity/dto/ActivityUpdateDTO.java

@@ -0,0 +1,37 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+public class ActivityUpdateDTO {
+
+    private Long id;
+
+    private String activityName;
+
+    private String activityDesc;
+
+    private LocalDateTime startTime;
+
+    private LocalDateTime endTime;
+
+    private BigDecimal discountValue;
+
+    private BigDecimal minAmount;
+
+    private BigDecimal maxDiscount;
+
+    private Integer applyScope;
+
+    private Integer productScope;
+
+    private BigDecimal totalBudget;
+
+    private List<Long> shopIds;
+
+    private List<Long> productIds;
+}

+ 47 - 0
haha-entity/src/main/java/com/haha/entity/dto/CouponCreateDTO.java

@@ -0,0 +1,47 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CouponCreateDTO extends PageQueryDTO {
+
+    private String couponName;
+
+    private Integer couponType;
+
+    private String couponDesc;
+
+    private BigDecimal discountValue;
+
+    private BigDecimal minAmount;
+
+    private BigDecimal maxDiscount;
+
+    private Integer totalCount;
+
+    private Integer receiveLimit;
+
+    private Integer validType;
+
+    private LocalDateTime validStartTime;
+
+    private LocalDateTime validEndTime;
+
+    private Integer validDays;
+
+    private Integer applyScope;
+
+    private Integer productScope;
+
+    private Long activityId;
+
+    private List<Long> shopIds;
+
+    private List<Long> productIds;
+}

+ 17 - 0
haha-entity/src/main/java/com/haha/entity/dto/CouponDistributeDTO.java

@@ -0,0 +1,17 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class CouponDistributeDTO {
+
+    private Long templateId;
+
+    private Integer targetType;
+
+    private List<Long> userIds;
+
+    private String remark;
+}

+ 25 - 0
haha-entity/src/main/java/com/haha/entity/dto/CouponQueryDTO.java

@@ -0,0 +1,25 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CouponQueryDTO extends PageQueryDTO {
+
+    private String couponName;
+
+    private Integer couponType;
+
+    private Integer status;
+
+    private Long activityId;
+
+    private LocalDateTime validStartTimeBegin;
+
+    private LocalDateTime validStartTimeEnd;
+
+    private Long creatorId;
+}

+ 19 - 0
haha-entity/src/main/java/com/haha/entity/dto/MarketingStatQueryDTO.java

@@ -0,0 +1,19 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDate;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class MarketingStatQueryDTO extends PageQueryDTO {
+
+    private Long activityId;
+
+    private Long templateId;
+
+    private LocalDate startDate;
+
+    private LocalDate endDate;
+}

+ 23 - 0
haha-mapper/src/main/java/com/haha/mapper/ActivityProductMapper.java

@@ -0,0 +1,23 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.ActivityProduct;
+import org.apache.ibatis.annotations.Delete;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+@Mapper
+public interface ActivityProductMapper extends BaseMapper<ActivityProduct> {
+
+    @Select("SELECT product_id FROM t_activity_product WHERE activity_id = #{activityId}")
+    List<Long> selectProductIdsByActivityId(@Param("activityId") Long activityId);
+
+    @Delete("DELETE FROM t_activity_product WHERE activity_id = #{activityId}")
+    int deleteByActivityId(@Param("activityId") Long activityId);
+
+    @Select("SELECT activity_id FROM t_activity_product WHERE product_id = #{productId}")
+    List<Long> selectActivityIdsByProductId(@Param("productId") Long productId);
+}

+ 23 - 0
haha-mapper/src/main/java/com/haha/mapper/ActivityShopMapper.java

@@ -0,0 +1,23 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.ActivityShop;
+import org.apache.ibatis.annotations.Delete;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+@Mapper
+public interface ActivityShopMapper extends BaseMapper<ActivityShop> {
+
+    @Select("SELECT shop_id FROM t_activity_shop WHERE activity_id = #{activityId}")
+    List<Long> selectShopIdsByActivityId(@Param("activityId") Long activityId);
+
+    @Delete("DELETE FROM t_activity_shop WHERE activity_id = #{activityId}")
+    int deleteByActivityId(@Param("activityId") Long activityId);
+
+    @Select("SELECT activity_id FROM t_activity_shop WHERE shop_id = #{shopId}")
+    List<Long> selectActivityIdsByShopId(@Param("shopId") Long shopId);
+}

+ 16 - 0
haha-mapper/src/main/java/com/haha/mapper/CouponDistributeMapper.java

@@ -0,0 +1,16 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.CouponDistribute;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+@Mapper
+public interface CouponDistributeMapper extends BaseMapper<CouponDistribute> {
+
+    @Select("SELECT * FROM t_coupon_distribute WHERE template_id = #{templateId} ORDER BY create_time DESC")
+    List<CouponDistribute> selectByTemplateId(@Param("templateId") Long templateId);
+}

+ 20 - 0
haha-mapper/src/main/java/com/haha/mapper/CouponProductMapper.java

@@ -0,0 +1,20 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.CouponProduct;
+import org.apache.ibatis.annotations.Delete;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+@Mapper
+public interface CouponProductMapper extends BaseMapper<CouponProduct> {
+
+    @Select("SELECT product_id FROM t_coupon_product WHERE template_id = #{templateId}")
+    List<Long> selectProductIdsByTemplateId(@Param("templateId") Long templateId);
+
+    @Delete("DELETE FROM t_coupon_product WHERE template_id = #{templateId}")
+    int deleteByTemplateId(@Param("templateId") Long templateId);
+}

+ 20 - 0
haha-mapper/src/main/java/com/haha/mapper/CouponShopMapper.java

@@ -0,0 +1,20 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.CouponShop;
+import org.apache.ibatis.annotations.Delete;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+@Mapper
+public interface CouponShopMapper extends BaseMapper<CouponShop> {
+
+    @Select("SELECT shop_id FROM t_coupon_shop WHERE template_id = #{templateId}")
+    List<Long> selectShopIdsByTemplateId(@Param("templateId") Long templateId);
+
+    @Delete("DELETE FROM t_coupon_shop WHERE template_id = #{templateId}")
+    int deleteByTemplateId(@Param("templateId") Long templateId);
+}

+ 35 - 0
haha-mapper/src/main/java/com/haha/mapper/CouponTemplateMapper.java

@@ -0,0 +1,35 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.CouponTemplate;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
+
+import java.util.List;
+
+@Mapper
+public interface CouponTemplateMapper extends BaseMapper<CouponTemplate> {
+
+    @Select("SELECT * FROM t_coupon_template WHERE deleted = 0 ORDER BY create_time DESC")
+    List<CouponTemplate> selectAllTemplates();
+
+    @Select("SELECT * FROM t_coupon_template WHERE status = 1 AND deleted = 0 AND remain_count > 0 ORDER BY create_time DESC")
+    List<CouponTemplate> selectAvailableTemplates();
+
+    @Update("UPDATE t_coupon_template SET remain_count = remain_count - 1, update_time = NOW() WHERE id = #{id} AND remain_count > 0")
+    int decreaseRemainCount(@Param("id") Long id);
+
+    @Update("UPDATE t_coupon_template SET remain_count = remain_count + 1, update_time = NOW() WHERE id = #{id}")
+    int increaseRemainCount(@Param("id") Long id);
+
+    @Update("UPDATE t_coupon_template SET status = #{status}, update_time = NOW() WHERE id = #{id}")
+    int updateStatus(@Param("id") Long id, @Param("status") Integer status);
+
+    @Select("SELECT COUNT(*) FROM t_coupon_template WHERE deleted = 0")
+    int countTotal();
+
+    @Select("SELECT SUM(remain_count) FROM t_coupon_template WHERE status = 1 AND deleted = 0")
+    Long sumRemainCount();
+}

+ 33 - 0
haha-mapper/src/main/java/com/haha/mapper/MarketingActivityMapper.java

@@ -0,0 +1,33 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.MarketingActivity;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Mapper
+public interface MarketingActivityMapper extends BaseMapper<MarketingActivity> {
+
+    @Select("SELECT * FROM t_marketing_activity WHERE deleted = 0 ORDER BY create_time DESC")
+    List<MarketingActivity> selectAllActivities();
+
+    @Select("SELECT * FROM t_marketing_activity WHERE status = #{status} AND deleted = 0 AND start_time <= #{now} AND end_time >= #{now}")
+    List<MarketingActivity> selectOngoingActivities(@Param("status") Integer status, @Param("now") LocalDateTime now);
+
+    @Update("UPDATE t_marketing_activity SET status = #{status}, update_time = NOW() WHERE id = #{id}")
+    int updateStatus(@Param("id") Long id, @Param("status") Integer status);
+
+    @Update("UPDATE t_marketing_activity SET used_budget = used_budget + #{amount}, update_time = NOW() WHERE id = #{id}")
+    int addUsedBudget(@Param("id") Long id, @Param("amount") java.math.BigDecimal amount);
+
+    @Select("SELECT COUNT(*) FROM t_marketing_activity WHERE deleted = 0")
+    int countTotal();
+
+    @Select("SELECT COUNT(*) FROM t_marketing_activity WHERE status = #{status} AND deleted = 0")
+    int countByStatus(@Param("status") Integer status);
+}

+ 24 - 0
haha-mapper/src/main/java/com/haha/mapper/MarketingStatisticsMapper.java

@@ -0,0 +1,24 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.MarketingStatistics;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.time.LocalDate;
+import java.util.List;
+
+@Mapper
+public interface MarketingStatisticsMapper extends BaseMapper<MarketingStatistics> {
+
+    @Select("SELECT * FROM t_marketing_statistics WHERE activity_id = #{activityId} ORDER BY stat_date DESC")
+    List<MarketingStatistics> selectByActivityId(@Param("activityId") Long activityId);
+
+    @Select("SELECT * FROM t_marketing_statistics WHERE activity_id = #{activityId} AND stat_date BETWEEN #{startDate} AND #{endDate} ORDER BY stat_date")
+    List<MarketingStatistics> selectByActivityIdAndDateRange(@Param("activityId") Long activityId, @Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate);
+
+    @Select("SELECT SUM(participant_count), SUM(new_user_count), SUM(order_count), SUM(order_amount), SUM(discount_amount), SUM(actual_amount), SUM(coupon_receive_count), SUM(coupon_use_count) " +
+            "FROM t_marketing_statistics WHERE activity_id = #{activityId}")
+    Object[] sumByActivityId(@Param("activityId") Long activityId);
+}

+ 43 - 0
haha-mapper/src/main/java/com/haha/mapper/UserCouponMapper.java

@@ -0,0 +1,43 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.UserCoupon;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Mapper
+public interface UserCouponMapper extends BaseMapper<UserCoupon> {
+
+    @Select("SELECT uc.*, ct.coupon_name, ct.coupon_type, ct.min_amount " +
+            "FROM t_user_coupon uc " +
+            "LEFT JOIN t_coupon_template ct ON uc.template_id = ct.id " +
+            "WHERE uc.user_id = #{userId} AND uc.status = #{status} " +
+            "ORDER BY uc.valid_end_time ASC")
+    List<UserCoupon> selectByUserIdAndStatus(@Param("userId") Long userId, @Param("status") Integer status);
+
+    @Select("SELECT COUNT(*) FROM t_user_coupon WHERE template_id = #{templateId} AND user_id = #{userId}")
+    int countByTemplateAndUser(@Param("templateId") Long templateId, @Param("userId") Long userId);
+
+    @Select("SELECT uc.*, ct.coupon_name, ct.coupon_type, ct.discount_value, ct.min_amount, ct.max_discount " +
+            "FROM t_user_coupon uc " +
+            "LEFT JOIN t_coupon_template ct ON uc.template_id = ct.id " +
+            "WHERE uc.id = #{id}")
+    UserCoupon selectWithTemplate(@Param("id") Long id);
+
+    @Update("UPDATE t_user_coupon SET status = 2, update_time = NOW() WHERE status = 0 AND valid_end_time < #{now}")
+    int updateExpiredCoupons(@Param("now") LocalDateTime now);
+
+    @Select("SELECT COUNT(*) FROM t_user_coupon WHERE template_id = #{templateId}")
+    int countByTemplateId(@Param("templateId") Long templateId);
+
+    @Select("SELECT COUNT(*) FROM t_user_coupon WHERE template_id = #{templateId} AND status = 1")
+    int countUsedByTemplateId(@Param("templateId") Long templateId);
+
+    @Select("SELECT COUNT(*) FROM t_user_coupon WHERE user_id = #{userId} AND status = 0 AND valid_end_time >= #{now}")
+    int countAvailableByUserId(@Param("userId") Long userId, @Param("now") LocalDateTime now);
+}

+ 75 - 0
haha-miniapp/src/main/java/com/haha/miniapp/controller/AppCouponController.java

@@ -0,0 +1,75 @@
+package com.haha.miniapp.controller;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.haha.common.vo.PageResult;
+import com.haha.common.vo.Result;
+import com.haha.entity.CouponTemplate;
+import com.haha.entity.UserCoupon;
+import com.haha.service.CouponTemplateService;
+import com.haha.service.UserCouponService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@RestController
+@RequestMapping("/coupon")
+@RequiredArgsConstructor
+public class AppCouponController {
+
+    private final CouponTemplateService templateService;
+    private final UserCouponService userCouponService;
+
+    @GetMapping("/available")
+    public Result<List<CouponTemplate>> getAvailableCoupons() {
+        List<CouponTemplate> templates = templateService.getAvailableTemplates();
+        return Result.success("查询成功", templates);
+    }
+
+    @GetMapping("/my")
+    public Result<PageResult<UserCoupon>> getMyCoupons(
+            @RequestParam(defaultValue = "0") Integer status,
+            @RequestParam(defaultValue = "1") Integer page,
+            @RequestParam(defaultValue = "10") Integer pageSize) {
+        Long userId = StpUtil.getLoginIdAsLong();
+        IPage<UserCoupon> result = userCouponService.getMyCoupons(userId, status, page, pageSize);
+        return Result.success("查询成功", PageResult.of(result));
+    }
+
+    @GetMapping("/{id}")
+    public Result<UserCoupon> getCouponDetail(@PathVariable Long id) {
+        Long userId = StpUtil.getLoginIdAsLong();
+        UserCoupon userCoupon = userCouponService.getDetail(id, userId);
+        return Result.success("查询成功", userCoupon);
+    }
+
+    @PostMapping("/receive/{templateId}")
+    public Result<UserCoupon> receiveCoupon(@PathVariable Long templateId) {
+        Long userId = StpUtil.getLoginIdAsLong();
+        UserCoupon userCoupon = userCouponService.receiveCoupon(userId, templateId);
+        return Result.success("领取成功", userCoupon);
+    }
+
+    @GetMapping("/count")
+    public Result<Map<String, Object>> getCouponCount() {
+        Long userId = StpUtil.getLoginIdAsLong();
+        int availableCount = userCouponService.countAvailableCoupons(userId);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("availableCount", availableCount);
+
+        return Result.success("查询成功", result);
+    }
+
+    @GetMapping("/usable")
+    public Result<List<UserCoupon>> getUsableCoupons() {
+        Long userId = StpUtil.getLoginIdAsLong();
+        List<UserCoupon> coupons = userCouponService.getAvailableCoupons(userId);
+        return Result.success("查询成功", coupons);
+    }
+}

+ 37 - 0
haha-service/src/main/java/com/haha/service/CouponTemplateService.java

@@ -0,0 +1,37 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.entity.CouponTemplate;
+import com.haha.entity.dto.CouponCreateDTO;
+import com.haha.entity.dto.CouponQueryDTO;
+
+import java.util.List;
+import java.util.Map;
+
+public interface CouponTemplateService extends IService<CouponTemplate> {
+
+    IPage<CouponTemplate> getPage(CouponQueryDTO queryDTO);
+
+    CouponTemplate getDetail(Long id);
+
+    CouponTemplate create(CouponCreateDTO dto, Long creatorId, String creatorName);
+
+    CouponTemplate update(CouponCreateDTO dto);
+
+    boolean updateStatus(Long id, Integer status);
+
+    boolean deleteTemplate(Long id);
+
+    List<CouponTemplate> getAvailableTemplates();
+
+    boolean decreaseRemainCount(Long id);
+
+    boolean increaseRemainCount(Long id);
+
+    List<Long> getCouponShopIds(Long templateId);
+
+    List<Long> getCouponProductIds(Long templateId);
+
+    Map<String, Object> getStatistics(Long templateId);
+}

+ 38 - 0
haha-service/src/main/java/com/haha/service/MarketingActivityService.java

@@ -0,0 +1,38 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.entity.MarketingActivity;
+import com.haha.entity.dto.ActivityCreateDTO;
+import com.haha.entity.dto.ActivityQueryDTO;
+import com.haha.entity.dto.ActivityUpdateDTO;
+
+import java.util.List;
+import java.util.Map;
+
+public interface MarketingActivityService extends IService<MarketingActivity> {
+
+    IPage<MarketingActivity> getPage(ActivityQueryDTO queryDTO);
+
+    MarketingActivity getDetail(Long id);
+
+    MarketingActivity create(ActivityCreateDTO dto, Long creatorId, String creatorName);
+
+    MarketingActivity update(ActivityUpdateDTO dto);
+
+    boolean publish(Long id);
+
+    boolean pause(Long id);
+
+    boolean resume(Long id);
+
+    boolean deleteActivity(Long id);
+
+    List<MarketingActivity> getOngoingActivities();
+
+    List<Long> getActivityShopIds(Long activityId);
+
+    List<Long> getActivityProductIds(Long activityId);
+
+    Map<String, Object> getStatistics(Long activityId);
+}

+ 28 - 0
haha-service/src/main/java/com/haha/service/MarketingStatisticsService.java

@@ -0,0 +1,28 @@
+package com.haha.service;
+
+import com.haha.entity.MarketingStatistics;
+import com.haha.entity.dto.MarketingStatQueryDTO;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Map;
+
+public interface MarketingStatisticsService {
+
+    List<MarketingStatistics> getByActivityId(Long activityId);
+
+    List<MarketingStatistics> getByActivityIdAndDateRange(Long activityId, LocalDate startDate, LocalDate endDate);
+
+    Map<String, Object> getActivityOverview(Long activityId);
+
+    Map<String, Object> getMarketingOverview();
+
+    void generateDailyStatistics(Long activityId, LocalDate date);
+
+    void generateAllDailyStatistics();
+
+    Map<String, Object> getTrendData(Long activityId, LocalDate startDate, LocalDate endDate);
+
+    BigDecimal calculateROI(Long activityId);
+}

+ 31 - 0
haha-service/src/main/java/com/haha/service/UserCouponService.java

@@ -0,0 +1,31 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.entity.UserCoupon;
+import com.haha.entity.dto.CouponDistributeDTO;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+public interface UserCouponService extends IService<UserCoupon> {
+
+    IPage<UserCoupon> getMyCoupons(Long userId, Integer status, int page, int pageSize);
+
+    List<UserCoupon> getAvailableCoupons(Long userId);
+
+    UserCoupon receiveCoupon(Long userId, Long templateId);
+
+    void distributeCoupon(CouponDistributeDTO dto, Long operatorId, String operatorName);
+
+    UserCoupon getDetail(Long id, Long userId);
+
+    boolean useCoupon(Long id, Long userId, Long orderId, BigDecimal discountAmount);
+
+    void expireCoupons();
+
+    int countAvailableCoupons(Long userId);
+
+    Map<String, Object> getCouponStatistics(Long templateId);
+}

+ 251 - 0
haha-service/src/main/java/com/haha/service/impl/CouponTemplateServiceImpl.java

@@ -0,0 +1,251 @@
+package com.haha.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.haha.common.enums.ApplyScopeEnum;
+import com.haha.common.enums.CouponTypeEnum;
+import com.haha.common.enums.ValidTypeEnum;
+import com.haha.common.exception.BusinessException;
+import com.haha.common.vo.StatusLabel;
+import com.haha.entity.CouponProduct;
+import com.haha.entity.CouponShop;
+import com.haha.entity.CouponTemplate;
+import com.haha.entity.dto.CouponCreateDTO;
+import com.haha.entity.dto.CouponQueryDTO;
+import com.haha.mapper.CouponProductMapper;
+import com.haha.mapper.CouponShopMapper;
+import com.haha.mapper.CouponTemplateMapper;
+import com.haha.service.CouponTemplateService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class CouponTemplateServiceImpl extends ServiceImpl<CouponTemplateMapper, CouponTemplate> implements CouponTemplateService {
+
+    private final CouponTemplateMapper templateMapper;
+    private final CouponShopMapper couponShopMapper;
+    private final CouponProductMapper couponProductMapper;
+
+    @Override
+    public IPage<CouponTemplate> getPage(CouponQueryDTO queryDTO) {
+        queryDTO.validate();
+        Page<CouponTemplate> page = new Page<>(queryDTO.getPage(), queryDTO.getPageSize());
+
+        LambdaQueryWrapper<CouponTemplate> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StringUtils.hasText(queryDTO.getCouponName()), CouponTemplate::getCouponName, queryDTO.getCouponName());
+        wrapper.eq(queryDTO.getCouponType() != null, CouponTemplate::getCouponType, queryDTO.getCouponType());
+        wrapper.eq(queryDTO.getStatus() != null, CouponTemplate::getStatus, queryDTO.getStatus());
+        wrapper.eq(queryDTO.getActivityId() != null, CouponTemplate::getActivityId, queryDTO.getActivityId());
+        wrapper.eq(queryDTO.getCreatorId() != null, CouponTemplate::getCreatorId, queryDTO.getCreatorId());
+        wrapper.ge(queryDTO.getValidStartTimeBegin() != null, CouponTemplate::getValidStartTime, queryDTO.getValidStartTimeBegin());
+        wrapper.le(queryDTO.getValidStartTimeEnd() != null, CouponTemplate::getValidStartTime, queryDTO.getValidStartTimeEnd());
+        wrapper.eq(CouponTemplate::getDeleted, 0);
+        wrapper.orderByDesc(CouponTemplate::getCreateTime);
+
+        IPage<CouponTemplate> result = this.page(page, wrapper);
+        result.getRecords().forEach(this::fillLabels);
+
+        return result;
+    }
+
+    @Override
+    public CouponTemplate getDetail(Long id) {
+        CouponTemplate template = this.getById(id);
+        if (template == null || template.getDeleted() == 1) {
+            throw new BusinessException(404, "优惠券模板不存在");
+        }
+        fillLabels(template);
+        return template;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public CouponTemplate create(CouponCreateDTO dto, Long creatorId, String creatorName) {
+        validateCouponData(dto);
+
+        CouponTemplate template = new CouponTemplate();
+        template.setCouponName(dto.getCouponName());
+        template.setCouponType(dto.getCouponType());
+        template.setCouponDesc(dto.getCouponDesc());
+        template.setDiscountValue(dto.getDiscountValue());
+        template.setMinAmount(dto.getMinAmount() != null ? dto.getMinAmount() : BigDecimal.ZERO);
+        template.setMaxDiscount(dto.getMaxDiscount());
+        template.setTotalCount(dto.getTotalCount());
+        template.setRemainCount(dto.getTotalCount());
+        template.setReceiveLimit(dto.getReceiveLimit() != null ? dto.getReceiveLimit() : 1);
+        template.setValidType(dto.getValidType());
+        template.setValidStartTime(dto.getValidStartTime());
+        template.setValidEndTime(dto.getValidEndTime());
+        template.setValidDays(dto.getValidDays());
+        template.setApplyScope(dto.getApplyScope());
+        template.setProductScope(dto.getProductScope());
+        template.setActivityId(dto.getActivityId());
+        template.setStatus(1);
+        template.setCreatorId(creatorId);
+        template.setCreatorName(creatorName);
+        template.setDeleted(0);
+
+        this.save(template);
+
+        saveCouponShops(template.getId(), dto.getShopIds(), dto.getApplyScope());
+        saveCouponProducts(template.getId(), dto.getProductIds(), dto.getProductScope());
+
+        log.info("创建优惠券模板成功: id={}, name={}", template.getId(), template.getCouponName());
+        return template;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public CouponTemplate update(CouponCreateDTO dto) {
+        return null;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean updateStatus(Long id, Integer status) {
+        CouponTemplate template = this.getById(id);
+        if (template == null || template.getDeleted() == 1) {
+            throw new BusinessException(404, "优惠券模板不存在");
+        }
+
+        boolean result = templateMapper.updateStatus(id, status) > 0;
+
+        if (result) {
+            log.info("更新优惠券状态成功: id={}, status={}", id, status);
+        }
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean deleteTemplate(Long id) {
+        CouponTemplate template = this.getById(id);
+        if (template == null) {
+            throw new BusinessException(404, "优惠券模板不存在");
+        }
+
+        template.setDeleted(1);
+        boolean result = this.updateById(template);
+
+        if (result) {
+            couponShopMapper.deleteByTemplateId(id);
+            couponProductMapper.deleteByTemplateId(id);
+            log.info("删除优惠券模板成功: id={}", id);
+        }
+        return result;
+    }
+
+    @Override
+    public List<CouponTemplate> getAvailableTemplates() {
+        return templateMapper.selectAvailableTemplates();
+    }
+
+    @Override
+    public boolean decreaseRemainCount(Long id) {
+        return templateMapper.decreaseRemainCount(id) > 0;
+    }
+
+    @Override
+    public boolean increaseRemainCount(Long id) {
+        return templateMapper.increaseRemainCount(id) > 0;
+    }
+
+    @Override
+    public List<Long> getCouponShopIds(Long templateId) {
+        return couponShopMapper.selectShopIdsByTemplateId(templateId);
+    }
+
+    @Override
+    public List<Long> getCouponProductIds(Long templateId) {
+        return couponProductMapper.selectProductIdsByTemplateId(templateId);
+    }
+
+    @Override
+    public Map<String, Object> getStatistics(Long templateId) {
+        Map<String, Object> stats = new HashMap<>();
+        stats.put("totalTemplates", templateMapper.countTotal());
+        stats.put("totalRemainCount", templateMapper.sumRemainCount());
+        return stats;
+    }
+
+    private void validateCouponData(CouponCreateDTO dto) {
+        if (dto.getCouponType() == null) {
+            throw new BusinessException(400, "优惠券类型不能为空");
+        }
+        if (dto.getDiscountValue() == null || dto.getDiscountValue().compareTo(BigDecimal.ZERO) <= 0) {
+            throw new BusinessException(400, "优惠值必须大于0");
+        }
+        if (dto.getTotalCount() == null || dto.getTotalCount() <= 0) {
+            throw new BusinessException(400, "发放总量必须大于0");
+        }
+        if (dto.getValidType() == null) {
+            throw new BusinessException(400, "有效期类型不能为空");
+        }
+        if (dto.getValidType() == ValidTypeEnum.FIXED_TIME.getCode()) {
+            if (dto.getValidStartTime() == null || dto.getValidEndTime() == null) {
+                throw new BusinessException(400, "固定时间有效期必须指定开始和结束时间");
+            }
+            if (dto.getValidStartTime().isAfter(dto.getValidEndTime())) {
+                throw new BusinessException(400, "有效期开始时间不能晚于结束时间");
+            }
+        } else if (dto.getValidType() == ValidTypeEnum.RELATIVE_TIME.getCode()) {
+            if (dto.getValidDays() == null || dto.getValidDays() <= 0) {
+                throw new BusinessException(400, "相对时间有效期必须指定有效天数");
+            }
+        }
+        if (dto.getCouponType() == CouponTypeEnum.DISCOUNT.getCode()) {
+            if (dto.getDiscountValue().compareTo(BigDecimal.ONE) < 0 || dto.getDiscountValue().compareTo(BigDecimal.TEN) > 0) {
+                throw new BusinessException(400, "折扣比例必须在1-10之间");
+            }
+        }
+    }
+
+    private void saveCouponShops(Long templateId, List<Long> shopIds, Integer applyScope) {
+        if (applyScope != null && applyScope == ApplyScopeEnum.SPECIFIED_SHOP.getCode()
+                && shopIds != null && !shopIds.isEmpty()) {
+            for (Long shopId : shopIds) {
+                CouponShop couponShop = new CouponShop();
+                couponShop.setTemplateId(templateId);
+                couponShop.setShopId(shopId);
+                couponShopMapper.insert(couponShop);
+            }
+        }
+    }
+
+    private void saveCouponProducts(Long templateId, List<Long> productIds, Integer productScope) {
+        if (productScope != null && productScope == 2
+                && productIds != null && !productIds.isEmpty()) {
+            for (Long productId : productIds) {
+                CouponProduct couponProduct = new CouponProduct();
+                couponProduct.setTemplateId(templateId);
+                couponProduct.setProductId(productId);
+                couponProductMapper.insert(couponProduct);
+            }
+        }
+    }
+
+    private void fillLabels(CouponTemplate template) {
+        StatusLabel typeLabel = CouponTypeEnum.getLabelByCode(template.getCouponType());
+        template.setTypeLabel(typeLabel.getLabel());
+
+        StatusLabel validTypeLabel = ValidTypeEnum.getLabelByCode(template.getValidType());
+        template.setValidTypeLabel(validTypeLabel.getLabel());
+
+        StatusLabel statusLabel = com.haha.common.enums.CommonStatus.getLabelByCode(template.getStatus());
+        template.setStatusLabel(statusLabel.getLabel());
+        template.setStatusColor(statusLabel.getColor());
+    }
+}

+ 286 - 0
haha-service/src/main/java/com/haha/service/impl/MarketingActivityServiceImpl.java

@@ -0,0 +1,286 @@
+package com.haha.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.haha.common.enums.ActivityStatusEnum;
+import com.haha.common.enums.ActivityTypeEnum;
+import com.haha.common.enums.ApplyScopeEnum;
+import com.haha.common.exception.BusinessException;
+import com.haha.common.vo.StatusLabel;
+import com.haha.entity.ActivityProduct;
+import com.haha.entity.ActivityShop;
+import com.haha.entity.MarketingActivity;
+import com.haha.entity.dto.ActivityCreateDTO;
+import com.haha.entity.dto.ActivityQueryDTO;
+import com.haha.entity.dto.ActivityUpdateDTO;
+import com.haha.mapper.ActivityProductMapper;
+import com.haha.mapper.ActivityShopMapper;
+import com.haha.mapper.MarketingActivityMapper;
+import com.haha.service.MarketingActivityService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class MarketingActivityServiceImpl extends ServiceImpl<MarketingActivityMapper, MarketingActivity> implements MarketingActivityService {
+
+    private final MarketingActivityMapper activityMapper;
+    private final ActivityShopMapper activityShopMapper;
+    private final ActivityProductMapper activityProductMapper;
+
+    @Override
+    public IPage<MarketingActivity> getPage(ActivityQueryDTO queryDTO) {
+        queryDTO.validate();
+        Page<MarketingActivity> page = new Page<>(queryDTO.getPage(), queryDTO.getPageSize());
+
+        LambdaQueryWrapper<MarketingActivity> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StringUtils.hasText(queryDTO.getActivityName()), MarketingActivity::getActivityName, queryDTO.getActivityName());
+        wrapper.eq(queryDTO.getActivityType() != null, MarketingActivity::getActivityType, queryDTO.getActivityType());
+        wrapper.eq(queryDTO.getStatus() != null, MarketingActivity::getStatus, queryDTO.getStatus());
+        wrapper.eq(queryDTO.getCreatorId() != null, MarketingActivity::getCreatorId, queryDTO.getCreatorId());
+        wrapper.ge(queryDTO.getStartTimeBegin() != null, MarketingActivity::getStartTime, queryDTO.getStartTimeBegin());
+        wrapper.le(queryDTO.getStartTimeEnd() != null, MarketingActivity::getStartTime, queryDTO.getStartTimeEnd());
+        wrapper.eq(MarketingActivity::getDeleted, 0);
+        wrapper.orderByDesc(MarketingActivity::getCreateTime);
+
+        IPage<MarketingActivity> result = this.page(page, wrapper);
+        result.getRecords().forEach(this::fillLabels);
+
+        return result;
+    }
+
+    @Override
+    public MarketingActivity getDetail(Long id) {
+        MarketingActivity activity = this.getById(id);
+        if (activity == null || activity.getDeleted() == 1) {
+            throw new BusinessException(404, "活动不存在");
+        }
+        fillLabels(activity);
+        return activity;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public MarketingActivity create(ActivityCreateDTO dto, Long creatorId, String creatorName) {
+        MarketingActivity activity = new MarketingActivity();
+        activity.setActivityName(dto.getActivityName());
+        activity.setActivityType(dto.getActivityType());
+        activity.setActivityDesc(dto.getActivityDesc());
+        activity.setStartTime(dto.getStartTime());
+        activity.setEndTime(dto.getEndTime());
+        activity.setStatus(ActivityStatusEnum.DRAFT.getCode());
+        activity.setDiscountValue(dto.getDiscountValue());
+        activity.setMinAmount(dto.getMinAmount());
+        activity.setMaxDiscount(dto.getMaxDiscount());
+        activity.setApplyScope(dto.getApplyScope());
+        activity.setProductScope(dto.getProductScope());
+        activity.setTotalBudget(dto.getTotalBudget());
+        activity.setUsedBudget(BigDecimal.ZERO);
+        activity.setCreatorId(creatorId);
+        activity.setCreatorName(creatorName);
+        activity.setDeleted(0);
+
+        this.save(activity);
+
+        saveActivityShops(activity.getId(), dto.getShopIds(), dto.getApplyScope());
+        saveActivityProducts(activity.getId(), dto.getProductIds(), dto.getProductScope());
+
+        log.info("创建营销活动成功: id={}, name={}", activity.getId(), activity.getActivityName());
+        return activity;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public MarketingActivity update(ActivityUpdateDTO dto) {
+        MarketingActivity activity = this.getById(dto.getId());
+        if (activity == null || activity.getDeleted() == 1) {
+            throw new BusinessException(404, "活动不存在");
+        }
+
+        if (!ActivityStatusEnum.canEdit(activity.getStatus())) {
+            throw new BusinessException(400, "当前状态不允许编辑");
+        }
+
+        activity.setActivityName(dto.getActivityName());
+        activity.setActivityDesc(dto.getActivityDesc());
+        activity.setStartTime(dto.getStartTime());
+        activity.setEndTime(dto.getEndTime());
+        activity.setDiscountValue(dto.getDiscountValue());
+        activity.setMinAmount(dto.getMinAmount());
+        activity.setMaxDiscount(dto.getMaxDiscount());
+        activity.setApplyScope(dto.getApplyScope());
+        activity.setProductScope(dto.getProductScope());
+        activity.setTotalBudget(dto.getTotalBudget());
+
+        this.updateById(activity);
+
+        activityShopMapper.deleteByActivityId(activity.getId());
+        activityProductMapper.deleteByActivityId(activity.getId());
+        saveActivityShops(activity.getId(), dto.getShopIds(), dto.getApplyScope());
+        saveActivityProducts(activity.getId(), dto.getProductIds(), dto.getProductScope());
+
+        log.info("更新营销活动成功: id={}", activity.getId());
+        return activity;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean publish(Long id) {
+        MarketingActivity activity = this.getById(id);
+        if (activity == null || activity.getDeleted() == 1) {
+            throw new BusinessException(404, "活动不存在");
+        }
+
+        if (!ActivityStatusEnum.canPublish(activity.getStatus())) {
+            throw new BusinessException(400, "当前状态不允许发布");
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        int newStatus = (now.isAfter(activity.getStartTime()) && now.isBefore(activity.getEndTime()))
+                ? ActivityStatusEnum.ONGOING.getCode()
+                : ActivityStatusEnum.PUBLISHED.getCode();
+
+        boolean result = activityMapper.updateStatus(id, newStatus) > 0;
+
+        if (result) {
+            log.info("发布营销活动成功: id={}, status={}", id, newStatus);
+        }
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean pause(Long id) {
+        MarketingActivity activity = this.getById(id);
+        if (activity == null || activity.getDeleted() == 1) {
+            throw new BusinessException(404, "活动不存在");
+        }
+
+        if (!ActivityStatusEnum.canPause(activity.getStatus())) {
+            throw new BusinessException(400, "当前状态不允许暂停");
+        }
+
+        boolean result = activityMapper.updateStatus(id, ActivityStatusEnum.PAUSED.getCode()) > 0;
+
+        if (result) {
+            log.info("暂停营销活动成功: id={}", id);
+        }
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean resume(Long id) {
+        MarketingActivity activity = this.getById(id);
+        if (activity == null || activity.getDeleted() == 1) {
+            throw new BusinessException(404, "活动不存在");
+        }
+
+        if (!ActivityStatusEnum.canResume(activity.getStatus())) {
+            throw new BusinessException(400, "当前状态不允许恢复");
+        }
+
+        boolean result = activityMapper.updateStatus(id, ActivityStatusEnum.ONGOING.getCode()) > 0;
+
+        if (result) {
+            log.info("恢复营销活动成功: id={}", id);
+        }
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean deleteActivity(Long id) {
+        MarketingActivity activity = this.getById(id);
+        if (activity == null) {
+            throw new BusinessException(404, "活动不存在");
+        }
+
+        if (!ActivityStatusEnum.canDelete(activity.getStatus())) {
+            throw new BusinessException(400, "当前状态不允许删除");
+        }
+
+        activity.setDeleted(1);
+        boolean result = this.updateById(activity);
+
+        if (result) {
+            activityShopMapper.deleteByActivityId(id);
+            activityProductMapper.deleteByActivityId(id);
+            log.info("删除营销活动成功: id={}", id);
+        }
+        return result;
+    }
+
+    @Override
+    public List<MarketingActivity> getOngoingActivities() {
+        LocalDateTime now = LocalDateTime.now();
+        return activityMapper.selectOngoingActivities(ActivityStatusEnum.ONGOING.getCode(), now);
+    }
+
+    @Override
+    public List<Long> getActivityShopIds(Long activityId) {
+        return activityShopMapper.selectShopIdsByActivityId(activityId);
+    }
+
+    @Override
+    public List<Long> getActivityProductIds(Long activityId) {
+        return activityProductMapper.selectProductIdsByActivityId(activityId);
+    }
+
+    @Override
+    public Map<String, Object> getStatistics(Long activityId) {
+        Map<String, Object> stats = new HashMap<>();
+        stats.put("totalActivities", activityMapper.countTotal());
+        stats.put("ongoingActivities", activityMapper.countByStatus(ActivityStatusEnum.ONGOING.getCode()));
+        stats.put("pausedActivities", activityMapper.countByStatus(ActivityStatusEnum.PAUSED.getCode()));
+        return stats;
+    }
+
+    private void saveActivityShops(Long activityId, List<Long> shopIds, Integer applyScope) {
+        if (applyScope != null && applyScope == ApplyScopeEnum.SPECIFIED_SHOP.getCode()
+                && shopIds != null && !shopIds.isEmpty()) {
+            for (Long shopId : shopIds) {
+                ActivityShop activityShop = new ActivityShop();
+                activityShop.setActivityId(activityId);
+                activityShop.setShopId(shopId);
+                activityShopMapper.insert(activityShop);
+            }
+        }
+    }
+
+    private void saveActivityProducts(Long activityId, List<Long> productIds, Integer productScope) {
+        if (productScope != null && productScope == 2
+                && productIds != null && !productIds.isEmpty()) {
+            for (Long productId : productIds) {
+                ActivityProduct activityProduct = new ActivityProduct();
+                activityProduct.setActivityId(activityId);
+                activityProduct.setProductId(productId);
+                activityProductMapper.insert(activityProduct);
+            }
+        }
+    }
+
+    private void fillLabels(MarketingActivity activity) {
+        StatusLabel statusLabel = ActivityStatusEnum.getLabelByCode(activity.getStatus());
+        activity.setStatusLabel(statusLabel.getLabel());
+        activity.setStatusColor(statusLabel.getColor());
+
+        StatusLabel typeLabel = ActivityTypeEnum.getLabelByCode(activity.getActivityType());
+        activity.setTypeLabel(typeLabel.getLabel());
+
+        StatusLabel applyScopeLabel = ApplyScopeEnum.getLabelByCode(activity.getApplyScope());
+        activity.setApplyScopeLabel(applyScopeLabel.getLabel());
+    }
+}

+ 99 - 0
haha-service/src/main/java/com/haha/service/impl/MarketingStatisticsServiceImpl.java

@@ -0,0 +1,99 @@
+package com.haha.service.impl;
+
+import com.haha.entity.MarketingStatistics;
+import com.haha.mapper.MarketingStatisticsMapper;
+import com.haha.service.MarketingStatisticsService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class MarketingStatisticsServiceImpl implements MarketingStatisticsService {
+
+    private final MarketingStatisticsMapper statisticsMapper;
+
+    @Override
+    public List<MarketingStatistics> getByActivityId(Long activityId) {
+        return statisticsMapper.selectByActivityId(activityId);
+    }
+
+    @Override
+    public List<MarketingStatistics> getByActivityIdAndDateRange(Long activityId, LocalDate startDate, LocalDate endDate) {
+        return statisticsMapper.selectByActivityIdAndDateRange(activityId, startDate, endDate);
+    }
+
+    @Override
+    public Map<String, Object> getActivityOverview(Long activityId) {
+        Map<String, Object> overview = new HashMap<>();
+
+        Object[] sums = statisticsMapper.sumByActivityId(activityId);
+        if (sums != null && sums.length > 0) {
+            overview.put("totalParticipants", sums[0] != null ? sums[0] : 0);
+            overview.put("totalNewUsers", sums[1] != null ? sums[1] : 0);
+            overview.put("totalOrders", sums[2] != null ? sums[2] : 0);
+            overview.put("totalOrderAmount", sums[3] != null ? sums[3] : BigDecimal.ZERO);
+            overview.put("totalDiscountAmount", sums[4] != null ? sums[4] : BigDecimal.ZERO);
+            overview.put("totalActualAmount", sums[5] != null ? sums[5] : BigDecimal.ZERO);
+            overview.put("totalCouponReceived", sums[6] != null ? sums[6] : 0);
+            overview.put("totalCouponUsed", sums[7] != null ? sums[7] : 0);
+        }
+
+        overview.put("roi", calculateROI(activityId));
+
+        return overview;
+    }
+
+    @Override
+    public Map<String, Object> getMarketingOverview() {
+        Map<String, Object> overview = new HashMap<>();
+        overview.put("message", "营销数据概览");
+        return overview;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void generateDailyStatistics(Long activityId, LocalDate date) {
+        log.info("生成活动每日统计数据: activityId={}, date={}", activityId, date);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void generateAllDailyStatistics() {
+        log.info("生成所有活动每日统计数据");
+    }
+
+    @Override
+    public Map<String, Object> getTrendData(Long activityId, LocalDate startDate, LocalDate endDate) {
+        Map<String, Object> trendData = new HashMap<>();
+        List<MarketingStatistics> stats = getByActivityIdAndDateRange(activityId, startDate, endDate);
+        trendData.put("data", stats);
+        return trendData;
+    }
+
+    @Override
+    public BigDecimal calculateROI(Long activityId) {
+        Object[] sums = statisticsMapper.sumByActivityId(activityId);
+        if (sums == null || sums.length < 6) {
+            return BigDecimal.ZERO;
+        }
+
+        BigDecimal totalActualAmount = sums[5] != null ? new BigDecimal(sums[5].toString()) : BigDecimal.ZERO;
+        BigDecimal totalDiscountAmount = sums[4] != null ? new BigDecimal(sums[4].toString()) : BigDecimal.ZERO;
+
+        if (totalDiscountAmount.compareTo(BigDecimal.ZERO) == 0) {
+            return BigDecimal.ZERO;
+        }
+
+        return totalActualAmount.divide(totalDiscountAmount, 2, RoundingMode.HALF_UP);
+    }
+}

+ 281 - 0
haha-service/src/main/java/com/haha/service/impl/UserCouponServiceImpl.java

@@ -0,0 +1,281 @@
+package com.haha.service.impl;
+
+import cn.hutool.core.util.IdUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.haha.common.constant.MarketingConstants;
+import com.haha.common.enums.CouponStatusEnum;
+import com.haha.common.enums.DistributeTypeEnum;
+import com.haha.common.enums.TargetTypeEnum;
+import com.haha.common.exception.BusinessException;
+import com.haha.entity.CouponDistribute;
+import com.haha.entity.CouponTemplate;
+import com.haha.entity.User;
+import com.haha.entity.UserCoupon;
+import com.haha.entity.dto.CouponDistributeDTO;
+import com.haha.mapper.CouponDistributeMapper;
+import com.haha.mapper.CouponTemplateMapper;
+import com.haha.mapper.UserCouponMapper;
+import com.haha.mapper.UserMapper;
+import com.haha.service.CouponTemplateService;
+import com.haha.service.UserCouponService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements UserCouponService {
+
+    private final UserCouponMapper userCouponMapper;
+    private final CouponTemplateMapper templateMapper;
+    private final CouponDistributeMapper distributeMapper;
+    private final UserMapper userMapper;
+    private final CouponTemplateService templateService;
+    private final StringRedisTemplate redisTemplate;
+
+    @Override
+    public IPage<UserCoupon> getMyCoupons(Long userId, Integer status, int page, int pageSize) {
+        Page<UserCoupon> pageParam = new Page<>(page, pageSize);
+
+        LambdaQueryWrapper<UserCoupon> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(UserCoupon::getUserId, userId);
+        if (status != null) {
+            wrapper.eq(UserCoupon::getStatus, status);
+        }
+        wrapper.orderByDesc(UserCoupon::getCreateTime);
+
+        IPage<UserCoupon> result = this.page(pageParam, wrapper);
+        result.getRecords().forEach(this::fillCouponLabels);
+
+        return result;
+    }
+
+    @Override
+    public List<UserCoupon> getAvailableCoupons(Long userId) {
+        LocalDateTime now = LocalDateTime.now();
+        return userCouponMapper.selectByUserIdAndStatus(userId, CouponStatusEnum.UNUSED.getCode());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public UserCoupon receiveCoupon(Long userId, Long templateId) {
+        String lockKey = MarketingConstants.REDIS_KEY_COUPON_RECEIVE_LOCK + userId + ":" + templateId;
+        Boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 1, TimeUnit.MINUTES);
+        if (!acquired) {
+            throw new BusinessException(400, "请勿重复领取");
+        }
+
+        try {
+            CouponTemplate template = templateMapper.selectById(templateId);
+            if (template == null || template.getDeleted() == 1) {
+                throw new BusinessException(404, "优惠券不存在");
+            }
+            if (template.getStatus() != 1) {
+                throw new BusinessException(400, "优惠券已禁用");
+            }
+            if (template.getRemainCount() <= 0) {
+                throw new BusinessException(400, "优惠券已领完");
+            }
+
+            int receivedCount = userCouponMapper.countByTemplateAndUser(templateId, userId);
+            if (receivedCount >= template.getReceiveLimit()) {
+                throw new BusinessException(400, "已达到领取上限");
+            }
+
+            if (!templateService.decreaseRemainCount(templateId)) {
+                throw new BusinessException(400, "优惠券领取失败,请重试");
+            }
+
+            UserCoupon userCoupon = new UserCoupon();
+            userCoupon.setCouponCode(generateCouponCode());
+            userCoupon.setTemplateId(templateId);
+            userCoupon.setUserId(userId);
+            userCoupon.setStatus(CouponStatusEnum.UNUSED.getCode());
+            userCoupon.setReceiveTime(LocalDateTime.now());
+
+            if (template.getValidType() == 1) {
+                userCoupon.setValidStartTime(template.getValidStartTime());
+                userCoupon.setValidEndTime(template.getValidEndTime());
+            } else {
+                LocalDateTime now = LocalDateTime.now();
+                userCoupon.setValidStartTime(now);
+                userCoupon.setValidEndTime(now.plusDays(template.getValidDays()));
+            }
+
+            this.save(userCoupon);
+
+            log.info("用户领取优惠券成功: userId={}, templateId={}, couponCode={}", userId, templateId, userCoupon.getCouponCode());
+            return userCoupon;
+
+        } finally {
+            redisTemplate.delete(lockKey);
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void distributeCoupon(CouponDistributeDTO dto, Long operatorId, String operatorName) {
+        CouponTemplate template = templateMapper.selectById(dto.getTemplateId());
+        if (template == null || template.getDeleted() == 1) {
+            throw new BusinessException(404, "优惠券模板不存在");
+        }
+
+        List<Long> targetUserIds = getTargetUserIds(dto.getTargetType(), dto.getUserIds());
+        if (targetUserIds.isEmpty()) {
+            throw new BusinessException(400, "没有目标用户");
+        }
+
+        if (targetUserIds.size() > template.getRemainCount()) {
+            throw new BusinessException(400, "优惠券数量不足");
+        }
+
+        CouponDistribute distribute = new CouponDistribute();
+        distribute.setTemplateId(dto.getTemplateId());
+        distribute.setDistributeType(DistributeTypeEnum.TARGET_SEND.getCode());
+        distribute.setTargetType(dto.getTargetType());
+        distribute.setTargetCount(targetUserIds.size());
+        distribute.setSuccessCount(0);
+        distribute.setOperatorId(operatorId);
+        distribute.setOperatorName(operatorName);
+
+        distributeMapper.insert(distribute);
+
+        int successCount = 0;
+        for (Long userId : targetUserIds) {
+            try {
+                UserCoupon userCoupon = new UserCoupon();
+                userCoupon.setCouponCode(generateCouponCode());
+                userCoupon.setTemplateId(dto.getTemplateId());
+                userCoupon.setUserId(userId);
+                userCoupon.setStatus(CouponStatusEnum.UNUSED.getCode());
+                userCoupon.setReceiveTime(LocalDateTime.now());
+
+                if (template.getValidType() == 1) {
+                    userCoupon.setValidStartTime(template.getValidStartTime());
+                    userCoupon.setValidEndTime(template.getValidEndTime());
+                } else {
+                    LocalDateTime now = LocalDateTime.now();
+                    userCoupon.setValidStartTime(now);
+                    userCoupon.setValidEndTime(now.plusDays(template.getValidDays()));
+                }
+
+                this.save(userCoupon);
+                templateService.decreaseRemainCount(dto.getTemplateId());
+                successCount++;
+            } catch (Exception e) {
+                log.error("发放优惠券失败: userId={}, templateId={}", userId, dto.getTemplateId(), e);
+            }
+        }
+
+        distribute.setSuccessCount(successCount);
+        distributeMapper.updateById(distribute);
+
+        log.info("批量发放优惠券完成: templateId={}, targetCount={}, successCount={}", dto.getTemplateId(), targetUserIds.size(), successCount);
+    }
+
+    @Override
+    public UserCoupon getDetail(Long id, Long userId) {
+        UserCoupon userCoupon = userCouponMapper.selectWithTemplate(id);
+        if (userCoupon == null) {
+            throw new BusinessException(404, "优惠券不存在");
+        }
+        if (!userCoupon.getUserId().equals(userId)) {
+            throw new BusinessException(403, "无权访问该优惠券");
+        }
+        fillCouponLabels(userCoupon);
+        return userCoupon;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean useCoupon(Long id, Long userId, Long orderId, BigDecimal discountAmount) {
+        UserCoupon userCoupon = this.getById(id);
+        if (userCoupon == null) {
+            throw new BusinessException(404, "优惠券不存在");
+        }
+        if (!userCoupon.getUserId().equals(userId)) {
+            throw new BusinessException(403, "无权使用该优惠券");
+        }
+        if (!CouponStatusEnum.canUse(userCoupon.getStatus())) {
+            throw new BusinessException(400, "优惠券不可用");
+        }
+        if (LocalDateTime.now().isAfter(userCoupon.getValidEndTime())) {
+            throw new BusinessException(400, "优惠券已过期");
+        }
+
+        userCoupon.setStatus(CouponStatusEnum.USED.getCode());
+        userCoupon.setOrderId(orderId);
+        userCoupon.setUseTime(LocalDateTime.now());
+        userCoupon.setDiscountAmount(discountAmount);
+
+        boolean result = this.updateById(userCoupon);
+
+        if (result) {
+            log.info("使用优惠券成功: id={}, userId={}, orderId={}, discountAmount={}", id, userId, orderId, discountAmount);
+        }
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void expireCoupons() {
+        int count = userCouponMapper.updateExpiredCoupons(LocalDateTime.now());
+        if (count > 0) {
+            log.info("更新过期优惠券完成: count={}", count);
+        }
+    }
+
+    @Override
+    public int countAvailableCoupons(Long userId) {
+        return userCouponMapper.countAvailableByUserId(userId, LocalDateTime.now());
+    }
+
+    @Override
+    public Map<String, Object> getCouponStatistics(Long templateId) {
+        Map<String, Object> stats = new HashMap<>();
+        stats.put("totalReceived", userCouponMapper.countByTemplateId(templateId));
+        stats.put("totalUsed", userCouponMapper.countUsedByTemplateId(templateId));
+        return stats;
+    }
+
+    private String generateCouponCode() {
+        return MarketingConstants.COUPON_CODE_PREFIX + IdUtil.getSnowflakeNextIdStr();
+    }
+
+    private List<Long> getTargetUserIds(Integer targetType, List<Long> userIds) {
+        if (targetType == TargetTypeEnum.SPECIFIED_USER.getCode()) {
+            return userIds != null ? userIds : new ArrayList<>();
+        } else if (targetType == TargetTypeEnum.ALL_USER.getCode()) {
+            List<User> users = userMapper.selectList(new LambdaQueryWrapper<User>().select(User::getId));
+            return users.stream().map(User::getId).toList();
+        } else if (targetType == TargetTypeEnum.NEW_USER.getCode()) {
+            LocalDateTime weekAgo = LocalDateTime.now().minusDays(7);
+            List<User> users = userMapper.selectList(
+                    new LambdaQueryWrapper<User>()
+                            .select(User::getId)
+                            .ge(User::getCreateTime, weekAgo));
+            return users.stream().map(User::getId).toList();
+        }
+        return new ArrayList<>();
+    }
+
+    private void fillCouponLabels(UserCoupon userCoupon) {
+        com.haha.common.vo.StatusLabel label = CouponStatusEnum.getLabelByCode(userCoupon.getStatus());
+        userCoupon.setStatusLabel(label.getLabel());
+        userCoupon.setStatusColor(label.getColor());
+    }
+}

+ 22 - 11
pom.xml

@@ -34,6 +34,7 @@
         <pagehelper.version>5.3.3</pagehelper.version>
         <aspectj.version>1.9.22</aspectj.version>
         <okhttp.version>4.12.0</okhttp.version>
+        <lombok.version>1.18.36</lombok.version>
     </properties>
 
     <dependencyManagement>
@@ -137,6 +138,7 @@
         <dependency>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>
+            <version>${lombok.version}</version>
             <optional>true</optional>
         </dependency>
 
@@ -167,19 +169,28 @@
                     <artifactId>spring-boot-maven-plugin</artifactId>
                     <version>${spring-boot.version}</version>
                 </plugin>
-                <plugin>
-                    <groupId>org.apache.maven.plugins</groupId>
-                    <artifactId>maven-compiler-plugin</artifactId>
-                    <version>3.11.0</version>
-                    <configuration>
-                        <source>${java.version}</source>
-                        <target>${java.version}</target>
-                        <encoding>${project.build.sourceEncoding}</encoding>
-                        <parameters>true</parameters>
-                    </configuration>
-                </plugin>
             </plugins>
         </pluginManagement>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.11.0</version>
+                <configuration>
+                    <source>${java.version}</source>
+                    <target>${java.version}</target>
+                    <encoding>${project.build.sourceEncoding}</encoding>
+                    <parameters>true</parameters>
+                    <annotationProcessorPaths>
+                        <path>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                            <version>${lombok.version}</version>
+                        </path>
+                    </annotationProcessorPaths>
+                </configuration>
+            </plugin>
+        </plugins>
     </build>
 
 </project>