Просмотр исходного кода

feat(coupon): 优惠券停用/启用功能 - 新增status字段+迁移268+toggle-status接口+管理端按钮

- Coupon实体新增status字段(ACTIVE/DISABLED)
- DatabaseInitializer迁移268为coupon表加status列, schema.sql同步
- CouponService所有用户侧查询/领取/使用/兑换/自动发放过滤停用券
- PointsExchangeService/FamilyPlatformPointsService兑换入口加停用校验
- AdminCouponController新增POST /api/admin/coupon/toggle-status
- CouponManagement.vue修复假状态列+新增停用/启用按钮
- coupon.js新增toggleCouponStatus封装
Xiaogang Liao 2 месяцев назад
Родитель
Сommit
943c331b67

+ 20 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCouponController.java

@@ -90,6 +90,26 @@ public class AdminCouponController {
         return Result.success("删除成功");
         return Result.success("删除成功");
     }
     }
 
 
+    @PostMapping("/toggle-status")
+    public Result<String> toggleStatus(@RequestBody Map<String, Object> params,
+                                       @RequestAttribute("role") String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("无权限");
+        }
+        Long id = ParamUtils.getLong(params.get("id"));
+        String status = params.get("status") == null ? null : params.get("status").toString();
+        if (id == null || !("ACTIVE".equals(status) || "DISABLED".equals(status))) {
+            return Result.error("参数错误");
+        }
+        Coupon coupon = couponMapper.selectById(id);
+        if (coupon == null) {
+            return Result.error("优惠券不存在");
+        }
+        coupon.setStatus(status);
+        couponMapper.updateById(coupon);
+        return Result.success("ACTIVE".equals(status) ? "启用成功" : "停用成功");
+    }
+
     @PostMapping("/grant-log")
     @PostMapping("/grant-log")
     public Result<List<CouponGrantLog>> grantLog(@RequestBody Map<String, Object> params,
     public Result<List<CouponGrantLog>> grantLog(@RequestBody Map<String, Object> params,
                                                  @RequestAttribute("role") String role) {
                                                  @RequestAttribute("role") String role) {

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/Coupon.java

@@ -57,5 +57,8 @@ public class Coupon implements Serializable {
 
 
     private Integer usedCount;
     private Integer usedCount;
 
 
+    /** 状态: ACTIVE=启用 / DISABLED=停用,NULL 视为 ACTIVE */
+    private String status;
+
     private Date createdAt;
     private Date createdAt;
 }
 }

+ 26 - 5
cfc-backend/src/main/java/com/etotem/cfc/service/CouponService.java

@@ -49,6 +49,7 @@ public class CouponService {
         return couponMapper.selectList(
         return couponMapper.selectList(
                 new LambdaQueryWrapper<Coupon>()
                 new LambdaQueryWrapper<Coupon>()
                         .in(Coupon::getId, couponIds)
                         .in(Coupon::getId, couponIds)
+                        .and(w -> w.isNull(Coupon::getStatus).or().eq(Coupon::getStatus, "ACTIVE"))
                         .le(Coupon::getValidFrom, now)
                         .le(Coupon::getValidFrom, now)
                         .ge(Coupon::getValidUntil, now)
                         .ge(Coupon::getValidUntil, now)
                         .orderByAsc(Coupon::getValue));
                         .orderByAsc(Coupon::getValue));
@@ -60,6 +61,9 @@ public class CouponService {
         if (coupon == null) {
         if (coupon == null) {
             return "优惠券不存在";
             return "优惠券不存在";
         }
         }
+        if (!"ACTIVE".equals(coupon.getStatus())) {
+            return "优惠券已停用";
+        }
         if (coupon.getTotalCount() != null && coupon.getUsedCount() != null
         if (coupon.getTotalCount() != null && coupon.getUsedCount() != null
                 && coupon.getUsedCount() >= coupon.getTotalCount()) {
                 && coupon.getUsedCount() >= coupon.getTotalCount()) {
             return "优惠券已领完";
             return "优惠券已领完";
@@ -98,6 +102,9 @@ public class CouponService {
         if (coupon == null) {
         if (coupon == null) {
             return null;
             return null;
         }
         }
+        if (!"ACTIVE".equals(coupon.getStatus())) {
+            return null;
+        }
         Date now = new Date();
         Date now = new Date();
         if (coupon.getValidFrom() != null && now.before(coupon.getValidFrom())) {
         if (coupon.getValidFrom() != null && now.before(coupon.getValidFrom())) {
             return null;
             return null;
@@ -167,6 +174,9 @@ public class CouponService {
         if (coupon == null) {
         if (coupon == null) {
             return false;
             return false;
         }
         }
+        if (!"ACTIVE".equals(coupon.getStatus())) {
+            return false;
+        }
         if (period != null) {
         if (period != null) {
             Long exists = couponGrantLogMapper.selectCount(
             Long exists = couponGrantLogMapper.selectCount(
                     new LambdaQueryWrapper<CouponGrantLog>()
                     new LambdaQueryWrapper<CouponGrantLog>()
@@ -200,7 +210,8 @@ public class CouponService {
         List<Coupon> coupons = couponMapper.selectList(
         List<Coupon> coupons = couponMapper.selectList(
                 new LambdaQueryWrapper<Coupon>()
                 new LambdaQueryWrapper<Coupon>()
                         .eq(Coupon::getGrantType, "JOIN")
                         .eq(Coupon::getGrantType, "JOIN")
-                        .eq(Coupon::getGrantLevelCode, levelCode));
+                        .eq(Coupon::getGrantLevelCode, levelCode)
+                        .and(w -> w.isNull(Coupon::getStatus).or().eq(Coupon::getStatus, "ACTIVE")));
         if (coupons.isEmpty()) {
         if (coupons.isEmpty()) {
             logger.warn("未找到 JOIN 赠券模板: userId={}, levelCode={}", userId, levelCode);
             logger.warn("未找到 JOIN 赠券模板: userId={}, levelCode={}", userId, levelCode);
             return;
             return;
@@ -216,7 +227,8 @@ public class CouponService {
     public void grantPopulationCoupons(Long userId, Long memberId) {
     public void grantPopulationCoupons(Long userId, Long memberId) {
         List<Coupon> coupons = couponMapper.selectList(
         List<Coupon> coupons = couponMapper.selectList(
                 new LambdaQueryWrapper<Coupon>()
                 new LambdaQueryWrapper<Coupon>()
-                        .eq(Coupon::getGrantType, "POPULATION"));
+                        .eq(Coupon::getGrantType, "POPULATION")
+                        .and(w -> w.isNull(Coupon::getStatus).or().eq(Coupon::getStatus, "ACTIVE")));
         for (Coupon c : coupons) {
         for (Coupon c : coupons) {
             grant(userId, c.getId(), "POPULATION", null, "member:" + memberId);
             grant(userId, c.getId(), "POPULATION", null, "member:" + memberId);
         }
         }
@@ -225,7 +237,9 @@ public class CouponService {
     /** PERIODIC: 返回所有周期性补发券模板 */
     /** PERIODIC: 返回所有周期性补发券模板 */
     public List<Coupon> listPeriodicTemplates() {
     public List<Coupon> listPeriodicTemplates() {
         return couponMapper.selectList(
         return couponMapper.selectList(
-                new LambdaQueryWrapper<Coupon>().eq(Coupon::getGrantType, "PERIODIC"));
+                new LambdaQueryWrapper<Coupon>()
+                        .eq(Coupon::getGrantType, "PERIODIC")
+                        .and(w -> w.isNull(Coupon::getStatus).or().eq(Coupon::getStatus, "ACTIVE")));
     }
     }
 
 
     /** 积分兑换中心:所有可兑换券(points_price>0 且在有效期内) */
     /** 积分兑换中心:所有可兑换券(points_price>0 且在有效期内) */
@@ -234,6 +248,7 @@ public class CouponService {
         return couponMapper.selectList(
         return couponMapper.selectList(
                 new LambdaQueryWrapper<Coupon>()
                 new LambdaQueryWrapper<Coupon>()
                         .gt(Coupon::getPointsPrice, 0)
                         .gt(Coupon::getPointsPrice, 0)
+                        .and(w -> w.isNull(Coupon::getStatus).or().eq(Coupon::getStatus, "ACTIVE"))
                         .le(Coupon::getValidFrom, now)
                         .le(Coupon::getValidFrom, now)
                         .ge(Coupon::getValidUntil, now)
                         .ge(Coupon::getValidUntil, now)
                         .orderByAsc(Coupon::getPointsPrice));
                         .orderByAsc(Coupon::getPointsPrice));
@@ -246,6 +261,7 @@ public class CouponService {
                 new LambdaQueryWrapper<Coupon>()
                 new LambdaQueryWrapper<Coupon>()
                         .eq(Coupon::getProductId, productId)
                         .eq(Coupon::getProductId, productId)
                         .gt(Coupon::getPointsPrice, 0)
                         .gt(Coupon::getPointsPrice, 0)
+                        .and(w -> w.isNull(Coupon::getStatus).or().eq(Coupon::getStatus, "ACTIVE"))
                         .le(Coupon::getValidFrom, now)
                         .le(Coupon::getValidFrom, now)
                         .ge(Coupon::getValidUntil, now)
                         .ge(Coupon::getValidUntil, now)
                         .orderByAsc(Coupon::getPointsPrice));
                         .orderByAsc(Coupon::getPointsPrice));
@@ -264,6 +280,7 @@ public class CouponService {
                 .stream().map(UserCoupon::getCouponId).collect(java.util.stream.Collectors.toList());
                 .stream().map(UserCoupon::getCouponId).collect(java.util.stream.Collectors.toList());
         LambdaQueryWrapper<Coupon> wrapper = new LambdaQueryWrapper<Coupon>()
         LambdaQueryWrapper<Coupon> wrapper = new LambdaQueryWrapper<Coupon>()
                 .gt(Coupon::getPointsPrice, 0)
                 .gt(Coupon::getPointsPrice, 0)
+                .and(w -> w.isNull(Coupon::getStatus).or().eq(Coupon::getStatus, "ACTIVE"))
                 .le(Coupon::getValidFrom, now)
                 .le(Coupon::getValidFrom, now)
                 .ge(Coupon::getValidUntil, now)
                 .ge(Coupon::getValidUntil, now)
                 .notIn(!ownedIds.isEmpty(), Coupon::getId, ownedIds)
                 .notIn(!ownedIds.isEmpty(), Coupon::getId, ownedIds)
@@ -277,7 +294,9 @@ public class CouponService {
     /** JOIN 权益展示:所有加入赠券模板 */
     /** JOIN 权益展示:所有加入赠券模板 */
     public List<Coupon> listJoinRules() {
     public List<Coupon> listJoinRules() {
         return couponMapper.selectList(
         return couponMapper.selectList(
-                new LambdaQueryWrapper<Coupon>().eq(Coupon::getGrantType, "JOIN"));
+                new LambdaQueryWrapper<Coupon>()
+                        .eq(Coupon::getGrantType, "JOIN")
+                        .and(w -> w.isNull(Coupon::getStatus).or().eq(Coupon::getStatus, "ACTIVE")));
     }
     }
 
 
     /** 我的优惠券:返回 user_coupon + coupon 联合数据,含 status/receivedAt/usedAt */
     /** 我的优惠券:返回 user_coupon + coupon 联合数据,含 status/receivedAt/usedAt */
@@ -296,7 +315,9 @@ public class CouponService {
             if (coupon == null) continue;
             if (coupon == null) continue;
             // 过期判断
             // 过期判断
             String status = uc.getStatus();
             String status = uc.getStatus();
-            if ("AVAILABLE".equals(status) && coupon.getValidUntil() != null && now.after(coupon.getValidUntil())) {
+            if ("AVAILABLE".equals(status) && !"ACTIVE".equals(coupon.getStatus())) {
+                status = "DISABLED";
+            } else if ("AVAILABLE".equals(status) && coupon.getValidUntil() != null && now.after(coupon.getValidUntil())) {
                 status = "EXPIRED";
                 status = "EXPIRED";
             }
             }
             Map<String, Object> item = new HashMap<>();
             Map<String, Object> item = new HashMap<>();

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyPlatformPointsService.java

@@ -214,6 +214,9 @@ public class FamilyPlatformPointsService {
         if (coupon == null) {
         if (coupon == null) {
             throw new RuntimeException("优惠券不存在");
             throw new RuntimeException("优惠券不存在");
         }
         }
+        if (!"ACTIVE".equals(coupon.getStatus())) {
+            throw new RuntimeException("优惠券已停用");
+        }
         Integer pointsPrice = coupon.getPointsPrice();
         Integer pointsPrice = coupon.getPointsPrice();
         if (pointsPrice == null || pointsPrice <= 0) {
         if (pointsPrice == null || pointsPrice <= 0) {
             throw new RuntimeException("该优惠券不支持CF值兑换");
             throw new RuntimeException("该优惠券不支持CF值兑换");

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/PointsExchangeService.java

@@ -181,6 +181,9 @@ public class PointsExchangeService {
         if (coupon == null) {
         if (coupon == null) {
             throw new RuntimeException("优惠券不存在");
             throw new RuntimeException("优惠券不存在");
         }
         }
+        if (!"ACTIVE".equals(coupon.getStatus())) {
+            throw new RuntimeException("优惠券已停用");
+        }
         if (coupon.getPointsPrice() == null || coupon.getPointsPrice() <= 0) {
         if (coupon.getPointsPrice() == null || coupon.getPointsPrice() <= 0) {
             throw new RuntimeException("该优惠券不支持积分兑换");
             throw new RuntimeException("该优惠券不支持积分兑换");
         }
         }

+ 4 - 0
cfc-web/src/api/coupon.js

@@ -23,3 +23,7 @@ export function batchIssueCoupons(data) {
 export function getCouponGrantLog(data) {
 export function getCouponGrantLog(data) {
   return request({ url: '/api/admin/coupon/grant-log', method: 'post', data })
   return request({ url: '/api/admin/coupon/grant-log', method: 'post', data })
 }
 }
+
+export function toggleCouponStatus(id, status) {
+  return request({ url: '/api/admin/coupon/toggle-status', method: 'post', data: { id, status } })
+}

+ 22 - 3
cfc-web/src/views/admin/CouponManagement.vue

@@ -63,8 +63,8 @@
         <el-table-column prop="usedCount" label="已用" width="70" />
         <el-table-column prop="usedCount" label="已用" width="70" />
         <el-table-column label="状态" width="80">
         <el-table-column label="状态" width="80">
           <template slot-scope="{ row }">
           <template slot-scope="{ row }">
-            <el-tag :type="row.status === 'active' ? 'success' : 'info'" size="mini">
-              {{ row.status === 'active' ? '启用' : '停用' }}
+            <el-tag :type="row.status !== 'DISABLED' ? 'success' : 'info'" size="mini">
+              {{ row.status !== 'DISABLED' ? '启用' : '停用' }}
             </el-tag>
             </el-tag>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
@@ -76,6 +76,8 @@
                 更多<i class="el-icon-arrow-down el-icon--right"></i>
                 更多<i class="el-icon-arrow-down el-icon--right"></i>
               </el-button>
               </el-button>
               <el-dropdown-menu slot="dropdown">
               <el-dropdown-menu slot="dropdown">
+                <el-dropdown-item command="toggle-on" icon="el-icon-check" v-if="row.status === 'DISABLED'">启用</el-dropdown-item>
+                <el-dropdown-item command="toggle-off" icon="el-icon-remove-outline" v-if="row.status !== 'DISABLED'">停用</el-dropdown-item>
                 <el-dropdown-item command="delete" icon="el-icon-delete">删除</el-dropdown-item>
                 <el-dropdown-item command="delete" icon="el-icon-delete">删除</el-dropdown-item>
               </el-dropdown-menu>
               </el-dropdown-menu>
             </el-dropdown>
             </el-dropdown>
@@ -205,7 +207,7 @@
 
 
 <script>
 <script>
 import SortMixin from '@/mixins/SortMixin'
 import SortMixin from '@/mixins/SortMixin'
-import { getCouponList, createCoupon, updateCoupon, deleteCoupon, batchIssueCoupons } from '@/api/coupon'
+import { getCouponList, createCoupon, updateCoupon, deleteCoupon, batchIssueCoupons, toggleCouponStatus } from '@/api/coupon'
 import ProductPicker from '@/components/ProductPicker'
 import ProductPicker from '@/components/ProductPicker'
 
 
 export default {
 export default {
@@ -428,11 +430,28 @@ export default {
     },
     },
     handleActionCmd(row, cmd) {
     handleActionCmd(row, cmd) {
       switch (cmd) {
       switch (cmd) {
+        case 'toggle-on':
+          this.handleToggleStatus(row, 'ACTIVE')
+          break
+        case 'toggle-off':
+          this.handleToggleStatus(row, 'DISABLED')
+          break
         case 'delete':
         case 'delete':
           this.handleDelete(row)
           this.handleDelete(row)
           break
           break
       }
       }
     },
     },
+    async handleToggleStatus(row, status) {
+      const action = status === 'ACTIVE' ? '启用' : '停用'
+      try {
+        await this.$confirm(`确定${action}该优惠券吗?${status === 'DISABLED' ? '停用后用户将无法领取和使用。' : ''}`, '提示', { type: 'warning' })
+        await toggleCouponStatus(row.id, status)
+        this.$message.success(`${action}成功`)
+        this.loadData()
+      } catch (e) {
+        if (e !== 'cancel') this.$message.error(`${action}失败`)
+      }
+    },
     handleContinue() {
     handleContinue() {
       this.successDialogVisible = false
       this.successDialogVisible = false
       this.isEdit = false
       this.isEdit = false