Kaynağa Gözat

优惠券发放类型

skyline 1 ay önce
ebeveyn
işleme
76cde8307b

+ 16 - 2
haha-admin-web/src/views/marketing/coupon/index.vue

@@ -27,6 +27,8 @@ const {
   handleAdd,
   handleDelete,
   handleToggleStatus,
+  handleDistribute,
+  handleViewRecords,
   handleSizeChange,
   handleCurrentChange
 } = useCoupon();
@@ -57,8 +59,19 @@ const {
         >
           <el-option label="满减券" :value="1" />
           <el-option label="折扣券" :value="2" />
-          <el-option label="现金券" :value="3" />
-          <el-option label="兑换券" :value="4" />
+          <el-option label="立减券" :value="3" />
+          <el-option label="商品券" :value="4" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="发放类型:" prop="receiveType">
+        <el-select
+          v-model="form.receiveType"
+          placeholder="请选择"
+          clearable
+          class="w-[180px]!"
+        >
+          <el-option label="主动领取" value="Collect" />
+          <el-option label="系统发放" value="Release" />
         </el-select>
       </el-form-item>
       <el-form-item label="状态:" prop="status">
@@ -134,6 +147,7 @@ const {
               {{ row.status === 1 ? '停用' : '启用' }}
             </el-button>
             <el-button
+              v-if="row.receiveType === 'Release'"
               class="reset-margin"
               link
               type="success"

+ 185 - 23
haha-admin-web/src/views/marketing/coupon/utils/hook.tsx

@@ -10,6 +10,7 @@ import {
   distributeCoupon
 } from "@/api/coupon";
 import { distributeCoupons, getCouponDistributeRecords } from "@/api/marketing";
+import { getUserList } from "@/api/user";
 import ShopSelector from "../../components/ShopSelector.vue";
 import ProductSelector from "../../components/ProductSelector.vue";
 import DeviceSelector from "../../components/DeviceSelector.vue";
@@ -26,7 +27,8 @@ import {
   ElInputNumber,
   ElTag,
   ElRadioGroup,
-  ElRadioButton
+  ElRadioButton,
+  ElAlert
 } from "element-plus";
 import { 
   initPagination, 
@@ -40,12 +42,14 @@ interface SearchFormProps {
   name: string;
   type: number | undefined;
   status: number | undefined;
+  receiveType: string | undefined;
 }
 
 interface CouponFormData {
   id?: number;
   name: string;
   activityId: string;
+  receiveType: string; // Collect-主动领取,Release-系统发放
   type: number | undefined;
   value: number | null;
   totalCount: number | null;
@@ -63,7 +67,8 @@ export function useCoupon() {
   const form = reactive<SearchFormProps>({
     name: "",
     type: undefined,
-    status: undefined
+    status: undefined,
+    receiveType: undefined
   });
   const loading = ref(true);
   const dataList = ref([]);
@@ -72,8 +77,8 @@ export function useCoupon() {
   const typeMap: Record<number, { text: string; type: string }> = {
     1: { text: "满减券", type: "primary" },
     2: { text: "折扣券", type: "success" },
-    3: { text: "现金券", type: "warning" },
-    4: { text: "兑换券", type: "info" }
+    3: { text: "立减券", type: "warning" },
+    4: { text: "商品券", type: "info" }
   };
 
   const statusMap: Record<number, { text: string; type: string }> = {
@@ -101,13 +106,24 @@ export function useCoupon() {
     },
     {
       label: "类型",
-      prop: "type",
+      prop: "couponType",
       minWidth: 100,
       cellRenderer: ({ row }) => {
-        const item = typeMap[row.type] || { text: "未知", type: "info" };
+        const item = typeMap[row.couponType] || { text: "未知", type: "info" };
         return <ElTag type={item.type}>{item.text}</ElTag>;
       }
     },
+    {
+      label: "发放类型",
+      prop: "receiveType",
+      minWidth: 110,
+      cellRenderer: ({ row }) => {
+        if (row.receiveType === 'Collect') {
+          return <ElTag type="success">主动领取</ElTag>;
+        }
+        return <ElTag type="primary">系统发放</ElTag>;
+      }
+    },
     {
       label: "优惠值",
       prop: "value",
@@ -171,6 +187,7 @@ export function useCoupon() {
       if (form.name) searchParams.name = form.name;
       if (form.type !== undefined) searchParams.type = form.type;
       if (form.status !== undefined) searchParams.status = form.status;
+      if (form.receiveType) searchParams.receiveType = form.receiveType;
       
       const { data } = await getCouponList(searchParams);
       if (data) {
@@ -206,35 +223,140 @@ export function useCoupon() {
   }
 
   function handleDistribute(row) {
+    const distributeForm = reactive({
+      targetType: "all", // all-全部用户, specific-指定用户
+      userIds: []
+    });
+
+    // 用户列表状态
+    const userList = ref([]);
+    const userLoading = ref(false);
+    const userSearchKeyword = ref("");
+
+    // 加载用户列表
+    const loadUserList = async (keyword = "") => {
+      userLoading.value = true;
+      try {
+        const { data } = await getUserList({
+          page: 1,
+          pageSize: 100,
+          phone: keyword || undefined,
+          nickname: keyword || undefined
+        });
+        if (data) {
+          userList.value = data.list || [];
+        }
+      } catch (error) {
+        console.error("加载用户列表失败:", error);
+        message("加载用户列表失败", { type: "error" });
+      } finally {
+        userLoading.value = false;
+      }
+    };
+
+    // 用户搜索
+    const handleUserSearch = (query: string) => {
+      userSearchKeyword.value = query;
+      if (query) {
+        loadUserList(query);
+      } else {
+        loadUserList();
+      }
+    };
+
     addDialog({
-      title: "定向发放优惠券",
-      width: "600px",
+      title: `发放优惠券 - ${row.couponName || '未知优惠券'}`,
+      width: "700px",
       draggable: true,
       fullscreen: deviceDetection(),
+      onOpened: () => {
+        // 弹窗打开时加载用户列表
+        loadUserList();
+      },
       contentRenderer: () => (
         <ElForm label-width="100px" size="default">
-          <ElFormItem label="发放类型">
-            <ElRadioGroup>
-              <ElRadioButton value="1">主动领取</ElRadioButton>
-              <ElRadioButton value="2">系统发放</ElRadioButton>
-              <ElRadioButton value="3">定向发放</ElRadioButton>
-            </ElRadioGroup>
+          <ElFormItem label="优惠券">
+            <ElInput value={row.couponName} disabled />
+          </ElFormItem>
+          <ElFormItem label="发放总量">
+            <ElInput value={`${row.receivedCount || 0} / ${row.totalCount || 0}`} disabled />
           </ElFormItem>
-          <ElFormItem label="目标用户">
-            <ElSelect placeholder="请选择目标用户" class="w-full" multiple>
+          <ElFormItem label="目标用户" required>
+            <ElSelect 
+              v-model={distributeForm.targetType} 
+              placeholder="请选择目标用户" 
+              class="w-full"
+              onChange={() => {
+                // 切换时清空用户选择
+                if (distributeForm.targetType === 'all') {
+                  distributeForm.userIds = [];
+                }
+              }}
+            >
               <ElOption label="全部用户" value="all" />
-              <ElOption label="新用户" value="new" />
-              <ElOption label="VIP用户" value="vip" />
+              <ElOption label="指定用户" value="specific" />
             </ElSelect>
           </ElFormItem>
-          <ElFormItem label="发放数量">
-            <ElInputNumber placeholder="请输入发放数量" class="w-full" min={1} max={10000} />
+          
+          {distributeForm.targetType === 'specific' && (
+            <ElFormItem label="选择用户" required>
+              <ElSelect 
+                v-model={distributeForm.userIds} 
+                class="w-full"
+                multiple
+                filterable
+                remote
+                reserve-keyword
+                remote-show-suffix={true}
+                loading={userLoading.value}
+                onRemoteMethod={handleUserSearch}
+                placeholder="请输入用户名或手机号搜索"
+              >
+                {userList.value.map((user: any) => (
+                  <ElOption 
+                    key={user.id} 
+                    label={`${user.nickname || user.phone || '未知用户'} (${user.phone || '无手机号'})`} 
+                    value={user.id} 
+                  />
+                ))}
+              </ElSelect>
+              <div style="font-size: 12px; color: #9ca3af; margin-top: 4px;">
+                已选择 {distributeForm.userIds.length} 个用户
+              </div>
+            </ElFormItem>
+          )}
+          
+          <ElFormItem label="发放数量" required>
+            <ElInputNumber 
+              placeholder="请输入发放数量" 
+              class="w-full" 
+              min={1} 
+              max={row.totalCount - (row.receivedCount || 0)} 
+            />
           </ElFormItem>
+          
+          <ElAlert
+            type="info"
+            closable={false}
+            showIcon={true}
+            style={{marginTop: '8px'}}
+          >
+            {distributeForm.targetType === 'all' 
+              ? '将向平台所有用户发放该优惠券' 
+              : `将向已选择的 ${distributeForm.userIds.length} 个用户发放该优惠券`}
+          </ElAlert>
         </ElForm>
       ),
       beforeSure: done => {
+        // 验证
+        if (distributeForm.targetType === 'specific' && distributeForm.userIds.length === 0) {
+          message("请选择至少一个用户", { type: "warning" });
+          return;
+        }
+        
         message("发放成功", { type: "success" });
         done();
+        onSearch();
       }
     });
   }
@@ -445,7 +567,23 @@ export function useCoupon() {
                   <ElOption label="不关联活动" value="" />
                 </ElSelect>
               </ElFormItem>
+              <ElFormItem label="发放类型" required>
+                <ElRadioGroup v-model={formData.receiveType}>
+                  <ElRadioButton value="Release">系统发放</ElRadioButton>
+                  <ElRadioButton value="Collect">主动领取</ElRadioButton>
+                </ElRadioGroup>
+              </ElFormItem>
             </div>
+            <ElAlert
+              type="info"
+              closable={false}
+              showIcon={true}
+              style={{marginTop: '12px'}}
+            >
+              {formData.receiveType === 'Collect' 
+                ? '主动领取:将显示在用户端小程序领券中心,用户可自行领取' 
+                : '系统发放:创建后可在列表中点击发放,选择指定用户或全部用户'}
+            </ElAlert>
           </ElForm>
         </div>
 
@@ -582,6 +720,7 @@ export function useCoupon() {
     const formData = reactive<CouponFormData>({
       name: "",
       activityId: "",
+      receiveType: "Release", // 默认为系统发放
       type: undefined,
       value: null,
       totalCount: null,
@@ -604,14 +743,37 @@ export function useCoupon() {
       beforeSure: async (done) => {
         if (!validateForm(formData)) return;
         try {
-          const { code } = await createCoupon(toRaw(formData));
+          // 将前端表单数据转换为后端 DTO 格式
+          const submitData = {
+            couponName: formData.name,
+            couponType: formData.type,
+            receiveType: formData.receiveType,
+            discountValue: formData.value,
+            totalCount: formData.totalCount,
+            validType: 1, // 固定时间
+            validStartTime: formData.validPeriod?.[0],
+            validEndTime: formData.validPeriod?.[1],
+            applyScope: formData.applyScope,
+            productScope: formData.productScope,
+            activityId: formData.activityId ? Number(formData.activityId) : null,
+            shopIds: formData.shopIds,
+            productIds: formData.productIds,
+            couponDesc: formData.description,
+            receiveLimit: 1
+          };
+          
+          const { code, message: errorMsg } = await createCoupon(submitData);
           if (code === 200) {
             message("新增成功", { type: "success" });
             done();
             onSearch();
+          } else {
+            message(errorMsg || "新增失败", { type: "error" });
           }
-        } catch (error) {
-          message("新增失败", { type: "error" });
+        } catch (error: any) {
+          // 展示后端返回的具体错误信息
+          const errorMsg = error?.response?.data?.message || error?.message || "新增失败,请重试";
+          message(errorMsg, { type: "error" });
         }
       }
     });

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

@@ -55,6 +55,8 @@ public class CouponTemplate implements Serializable {
 
     private Integer status;
 
+    private String receiveType; // 发放类型:Collect-主动领取,Release-系统发放
+
     private Long creatorId;
 
     private String creatorName;

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

@@ -41,6 +41,8 @@ public class CouponCreateDTO extends PageQueryDTO {
 
     private Long activityId;
 
+    private String receiveType; // 发放类型:Collect-主动领取,Release-系统发放
+
     private List<Long> shopIds;
 
     private List<Long> productIds;

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

@@ -15,6 +15,8 @@ public class CouponQueryDTO extends PageQueryDTO {
 
     private Integer status;
 
+    private String receiveType; // 发放类型:Collect-主动领取,Release-系统发放
+
     private Long activityId;
 
     private LocalDateTime validStartTimeBegin;

+ 1 - 1
haha-mapper/src/main/java/com/haha/mapper/CouponTemplateMapper.java

@@ -15,7 +15,7 @@ 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")
+    @Select("SELECT * FROM t_coupon_template WHERE status = 1 AND deleted = 0 AND remain_count > 0 AND receive_type = 'Collect' 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")

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

@@ -48,6 +48,7 @@ public class CouponTemplateServiceImpl extends ServiceImpl<CouponTemplateMapper,
         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(StringUtils.hasText(queryDTO.getReceiveType()), CouponTemplate::getReceiveType, queryDTO.getReceiveType());
         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());
@@ -93,6 +94,7 @@ public class CouponTemplateServiceImpl extends ServiceImpl<CouponTemplateMapper,
         template.setApplyScope(dto.getApplyScope());
         template.setProductScope(dto.getProductScope());
         template.setActivityId(dto.getActivityId());
+        template.setReceiveType(dto.getReceiveType() != null ? dto.getReceiveType() : "Release");
         template.setStatus(1);
         template.setCreatorId(creatorId);
         template.setCreatorName(creatorName);