Bladeren bron

feat(web): 优惠券配置表单扩展+发券记录页+移除商品会员价输入

Xiaogang Liao 1 maand geleden
bovenliggende
commit
a09a72ab6f

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

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

+ 6 - 0
cfc-web/src/router/index.js

@@ -544,6 +544,12 @@ const routes = [
         component: () => import('@/views/admin/CouponManagement'),
         meta: { title: '优惠券管理', perm: 'marketing:coupon' }
       },
+      {
+        path: 'coupon-grant-log',
+        name: 'CouponGrantLog',
+        component: () => import('@/views/admin/CouponGrantLog.vue'),
+        meta: { title: '发券记录', perm: 'marketing:coupon' }
+      },
       {
         path: 'promotion',
         name: 'PromotionManagement',

+ 1 - 0
cfc-web/src/views/Layout.vue

@@ -198,6 +198,7 @@ export default {
             { path: '/pending-refund', label: '待退款管理', icon: 'el-icon-warning', perm: 'commerce:orders' },
             { path: '/product-profit-rate', label: '产品利润率', icon: 'el-icon-data-line', perm: 'commerce:profit' },
             { path: '/coupon', label: '优惠券管理', icon: 'el-icon-ticket', perm: 'marketing:coupon' },
+            { path: '/coupon-grant-log', label: '发券记录', icon: 'el-icon-document', perm: 'marketing:coupon' },
             { path: '/promotion', label: '推广管理', icon: 'el-icon-s-marketing', perm: 'marketing:promotion' },
             { path: '/family-earnings', label: '家庭收益', icon: 'el-icon-s-money', perm: 'marketing:promotion' },
             { path: '/family-earnings-withdraw', label: '收益提现审核', icon: 'el-icon-document-checked', perm: 'audit:withdraw' },

+ 112 - 0
cfc-web/src/views/admin/CouponGrantLog.vue

@@ -0,0 +1,112 @@
+<template>
+  <div class="coupon-grant-log admin-page">
+    <el-card>
+      <div slot="header" class="admin-page-header">
+        <span class="admin-page-title">发券记录</span>
+        <div class="admin-page-actions">
+          <el-input v-model="userId" placeholder="用户ID" clearable style="width: 140px" @keyup.enter.native="handleSearch" @clear="handleSearch" />
+          <el-input v-model="couponId" placeholder="优惠券ID" clearable style="width: 140px" @keyup.enter.native="handleSearch" @clear="handleSearch" />
+          <el-button type="primary" size="small" icon="el-icon-search" @click="handleSearch">查询</el-button>
+        </div>
+      </div>
+
+      <div class="table-scroll-wrap-sm">
+        <el-table :max-height="tableHeight" :data="list" v-loading="loading" border stripe>
+          <el-table-column prop="id" label="ID" width="80" />
+          <el-table-column prop="userId" label="用户ID" width="100" />
+          <el-table-column prop="couponId" label="优惠券ID" width="110" />
+          <el-table-column label="发放类型" width="120">
+            <template slot-scope="{ row }">
+              <el-tag size="mini" :type="grantTypeTagType(row.grantType)">{{ grantTypeLabel(row.grantType) }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="period" label="周期标识" width="120">
+            <template slot-scope="{ row }">{{ row.period || '-' }}</template>
+          </el-table-column>
+          <el-table-column prop="quantity" label="数量" width="80" />
+          <el-table-column prop="source" label="触发来源" min-width="160" show-overflow-tooltip>
+            <template slot-scope="{ row }">{{ row.source || '-' }}</template>
+          </el-table-column>
+          <el-table-column prop="createdAt" label="发放时间" width="180" />
+        </el-table>
+      </div>
+
+      <el-pagination
+        @current-change="onPageChange"
+        :current-page="page"
+        :page-size="size"
+        :total="total"
+        layout="total, prev, pager, next"
+        class="pagination-wrap"
+      />
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { getCouponGrantLog } from '@/api/coupon'
+
+export default {
+  name: 'CouponGrantLog',
+  computed: {
+    tableHeight() {
+      return window.innerHeight - 300
+    }
+  },
+  data() {
+    return {
+      list: [],
+      loading: false,
+      page: 1,
+      size: 20,
+      total: 0,
+      userId: '',
+      couponId: ''
+    }
+  },
+  created() {
+    this.loadData()
+  },
+  methods: {
+    grantTypeLabel(val) {
+      const map = { JOIN: '开通赠券', PERIODIC: '周期补发', POPULATION: '家庭人口券', EXCHANGE: '积分兑换' }
+      return map[val] || val
+    },
+    grantTypeTagType(val) {
+      const map = { JOIN: 'success', PERIODIC: 'primary', POPULATION: 'warning', EXCHANGE: 'danger' }
+      return map[val] || 'info'
+    },
+    async loadData() {
+      this.loading = true
+      try {
+        const res = await getCouponGrantLog({
+          page: this.page,
+          size: this.size,
+          userId: this.userId || undefined,
+          couponId: this.couponId || undefined
+        })
+        if (res.data) {
+          this.list = res.data
+          this.total = this.list.length
+        }
+      } catch (e) {
+        console.error(e)
+      } finally {
+        this.loading = false
+      }
+    },
+    handleSearch() {
+      this.page = 1
+      this.loadData()
+    },
+    onPageChange(p) {
+      if (p) this.page = p
+      this.loadData()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.coupon-grant-log { padding: 20px; }
+</style>

+ 100 - 11
cfc-web/src/views/admin/CouponManagement.vue

@@ -14,20 +14,32 @@
         <el-table :max-height="tableHeight" :data="list" v-loading="loading" border stripe>
         <el-table-column prop="id" label="ID" width="70" />
         <el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
-        <el-table-column label="类型" width="90">
+        <el-table-column label="类型" width="110">
           <template slot-scope="{ row }">
-            <el-tag size="mini">{{ row.type }}</el-tag>
+            <el-tag size="mini">{{ typeLabel(row.type) }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="面值(元)" width="90">
-          <template slot-scope="{ row }">{{ (row.value / 100).toFixed(2) }}</template>
+        <el-table-column label="面值/折扣" width="100">
+          <template slot-scope="{ row }">
+            <span v-if="row.type === 'DISCOUNT'">{{ (row.discountRate / 100).toFixed(1) }}折</span>
+            <span v-else>{{ (row.value / 100).toFixed(2) }}元</span>
+          </template>
         </el-table-column>
         <el-table-column label="使用门槛(元)" width="110">
           <template slot-scope="{ row }">{{ row.minSpend ? (row.minSpend / 100).toFixed(2) : '无' }}</template>
         </el-table-column>
-        <el-table-column prop="applicableTo" label="适用对象" width="110">
+        <el-table-column label="适用对象" width="110">
           <template slot-scope="{ row }">{{ applicableLabel(row.applicableTo) }}</template>
         </el-table-column>
+        <el-table-column label="积分价" width="80">
+          <template slot-scope="{ row }">{{ row.pointsPrice || 0 }}</template>
+        </el-table-column>
+        <el-table-column label="发放规则" width="130">
+          <template slot-scope="{ row }">
+            <span v-if="row.grantType">{{ grantTypeLabel(row.grantType) }}</span>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
         <el-table-column label="有效期" width="180">
           <template slot-scope="{ row }">{{ row.validFrom }} ~ {{ row.validUntil }}</template>
         </el-table-column>
@@ -74,13 +86,19 @@
         </el-form-item>
         <el-form-item label="类型" required>
           <el-select v-model="form.type" placeholder="请选择类型" style="width: 100%">
-            <el-option label="FIXED" value="FIXED" />
+            <el-option label="FIXED(满减)" value="FIXED" />
+            <el-option label="CASH(无门槛)" value="CASH" />
+            <el-option label="DISCOUNT(折扣)" value="DISCOUNT" />
           </el-select>
         </el-form-item>
-        <el-form-item label="面值(元)" required>
+        <el-form-item v-if="form.type !== 'DISCOUNT'" label="面值(元)" required>
           <el-input-number v-model="form.valueYuan" :min="0.01" :step="0.5" :precision="2" style="width: 200px" />
         </el-form-item>
-        <el-form-item label="使用门槛(元)">
+        <el-form-item v-if="form.type === 'DISCOUNT'" label="折扣率(千分比)" required>
+          <el-input-number v-model="form.discountRate" :min="1" :max="10000" :step="100" style="width: 200px" />
+          <div class="form-item-tip">9000 = 9折,10000 = 无折扣</div>
+        </el-form-item>
+        <el-form-item v-if="form.type === 'FIXED'" label="使用门槛(元)">
           <el-input-number v-model="form.minSpendYuan" :min="0" :step="10" :precision="2" style="width: 200px" />
         </el-form-item>
         <el-form-item label="适用对象">
@@ -91,6 +109,38 @@
             <el-option label="测评服务" value="ASSESSMENT" />
           </el-select>
         </el-form-item>
+        <el-form-item label="绑定商品ID">
+          <el-input-number v-model="form.productId" :min="0" :step="1" style="width: 200px" />
+          <div class="form-item-tip">0 或留空 = 全场通用</div>
+        </el-form-item>
+        <el-form-item label="积分兑换价">
+          <el-input-number v-model="form.pointsPrice" :min="0" :step="100" style="width: 200px" />
+          <div class="form-item-tip">0 = 不可积分兑换</div>
+        </el-form-item>
+        <el-form-item label="自动发放规则">
+          <el-select v-model="form.grantType" placeholder="不自动发放" style="width: 100%">
+            <el-option label="不自动发放" value="" />
+            <el-option label="JOIN(开通会员赠)" value="JOIN" />
+            <el-option label="PERIODIC(周期补发)" value="PERIODIC" />
+            <el-option label="POPULATION(家庭人口券)" value="POPULATION" />
+          </el-select>
+        </el-form-item>
+        <el-form-item v-if="form.grantType === 'JOIN' || form.grantType === 'PERIODIC'" label="适用会员等级">
+          <el-select v-model="form.grantLevelCode" placeholder="请选择会员等级" style="width: 100%">
+            <el-option label="FREE" value="FREE" />
+            <el-option label="FAMILY" value="FAMILY" />
+            <el-option label="PREMIUM" value="PREMIUM" />
+          </el-select>
+        </el-form-item>
+        <el-form-item v-if="form.grantType === 'PERIODIC'" label="补发周期">
+          <el-select v-model="form.grantPeriod" placeholder="请选择补发周期" style="width: 100%">
+            <el-option label="MONTHLY(每月)" value="MONTHLY" />
+            <el-option label="QUARTERLY(每季度)" value="QUARTERLY" />
+          </el-select>
+        </el-form-item>
+        <el-form-item v-if="form.grantType && form.grantType !== ''" label="每次发放数量">
+          <el-input-number v-model="form.grantQuantity" :min="1" :step="1" style="width: 200px" />
+        </el-form-item>
         <el-form-item label="有效期起" required>
           <el-date-picker v-model="form.validFrom" type="date" placeholder="选择开始日期" value-format="yyyy-MM-dd" style="width: 100%" />
         </el-form-item>
@@ -178,6 +228,13 @@ export default {
         type: 'FIXED',
         valueYuan: null,
         minSpendYuan: null,
+        discountRate: null,
+        productId: null,
+        pointsPrice: 0,
+        grantType: '',
+        grantLevelCode: 'FAMILY',
+        grantPeriod: 'MONTHLY',
+        grantQuantity: 1,
         applicableTo: 'ALL',
         validFrom: '',
         validUntil: '',
@@ -188,6 +245,14 @@ export default {
       const map = { ALL: '全部商品', MEMBERSHIP: '会员服务', PRODUCT: '特定商品', ASSESSMENT: '测评服务' }
       return map[val] || val
     },
+    typeLabel(val) {
+      const map = { FIXED: '满减券', CASH: '无门槛券', DISCOUNT: '折扣券' }
+      return map[val] || val
+    },
+    grantTypeLabel(val) {
+      const map = { JOIN: '开通赠券', PERIODIC: '周期补发', POPULATION: '家庭人口券' }
+      return map[val] || val
+    },
     async loadData() {
       this.loading = true
       try {
@@ -226,8 +291,15 @@ export default {
         id: row.id,
         name: row.name,
         type: row.type,
-        valueYuan: row.value / 100,
+        valueYuan: row.type === 'DISCOUNT' ? null : row.value / 100,
         minSpendYuan: row.minSpend ? row.minSpend / 100 : null,
+        discountRate: row.discountRate || null,
+        productId: row.productId || null,
+        pointsPrice: row.pointsPrice || 0,
+        grantType: row.grantType || '',
+        grantLevelCode: row.grantLevelCode || 'FAMILY',
+        grantPeriod: row.grantPeriod || 'MONTHLY',
+        grantQuantity: row.grantQuantity || 1,
         applicableTo: row.applicableTo,
         validFrom: row.validFrom,
         validUntil: row.validUntil,
@@ -246,7 +318,17 @@ export default {
       }
     },
     async handleSubmit() {
-      if (!this.form.name || !this.form.valueYuan || !this.form.totalCount || !this.form.validFrom || !this.form.validUntil) {
+      const isDiscount = this.form.type === 'DISCOUNT'
+      if (isDiscount) {
+        if (!this.form.discountRate) {
+          this.$message.warning('折扣券请填写折扣率')
+          return
+        }
+      } else if (!this.form.valueYuan) {
+        this.$message.warning('请填写面值')
+        return
+      }
+      if (!this.form.name || !this.form.totalCount || !this.form.validFrom || !this.form.validUntil) {
         this.$message.warning('请填写完整信息')
         return
       }
@@ -255,8 +337,15 @@ export default {
         const payload = {
           name: this.form.name,
           type: this.form.type,
-          value: Math.round(this.form.valueYuan * 100),
+          value: isDiscount ? 0 : Math.round(this.form.valueYuan * 100),
           minSpend: this.form.minSpendYuan ? Math.round(this.form.minSpendYuan * 100) : 0,
+          discountRate: isDiscount ? this.form.discountRate : null,
+          productId: this.form.productId || null,
+          pointsPrice: this.form.pointsPrice || 0,
+          grantType: this.form.grantType || null,
+          grantLevelCode: this.form.grantType ? this.form.grantLevelCode : null,
+          grantPeriod: this.form.grantType === 'PERIODIC' ? this.form.grantPeriod : null,
+          grantQuantity: this.form.grantType ? this.form.grantQuantity : null,
           applicableTo: this.form.applicableTo,
           validFrom: this.form.validFrom,
           validUntil: this.form.validUntil,

+ 0 - 8
cfc-web/src/views/admin/ProductEdit.vue

@@ -122,11 +122,6 @@
               <el-input-number v-model="form.price" :min="0" :precision="2" placeholder="0.00" style="width: 100%;" controls />
             </el-form-item>
           </el-col>
-          <el-col :span="6">
-            <el-form-item label="会员价(元)">
-              <el-input-number v-model="form.memberPrice" :min="0" :precision="2" placeholder="0.00" style="width: 100%;" controls />
-            </el-form-item>
-          </el-col>
           <el-col :span="6">
             <el-form-item label="库存" prop="stock">
               <el-input-number v-model="form.stock" :min="0" :precision="0" placeholder="0" style="width: 100%;" controls />
@@ -251,7 +246,6 @@ export default {
         images: '',
         productType: 'physical',
         price: null,
-        memberPrice: null,
         stock: null,
         minQuantity: 1,
         vendorName: '',
@@ -396,7 +390,6 @@ export default {
             images: p.images || '',
             productType: p.productType || 'physical',
             price: (p.price != null) ? p.price / 100 : null,
-            memberPrice: (p.memberPrice != null) ? p.memberPrice / 100 : null,
             stock: (p.stock != null) ? p.stock : null,
             minQuantity: p.minQuantity || null,
             vendorName: p.vendorName || '',
@@ -571,7 +564,6 @@ async loadGiftRule(productId) {
           const productData = {
             ...this.form,
             price: (this.form.price != null) ? Math.round(this.form.price * 100) : 0,
-            memberPrice: (this.form.memberPrice != null) ? Math.round(this.form.memberPrice * 100) : null,
             images: JSON.stringify(this.imageList)
           }
           // Extract dimensionWeights before sending to product API

+ 0 - 1
cfc-web/src/views/admin/ProductManage.vue

@@ -127,7 +127,6 @@
         <el-descriptions-item label="商品名称">{{ detail.name }}</el-descriptions-item>
         <el-descriptions-item label="商品类型">{{ typeLabel(detail.productType) }}</el-descriptions-item>
         <el-descriptions-item label="售价">{{ formatPriceWithSymbol(detail.price || 0) }}</el-descriptions-item>
-        <el-descriptions-item label="会员价">{{ detail.memberPrice ? formatPriceWithSymbol(detail.memberPrice) : '-' }}</el-descriptions-item>
         <el-descriptions-item label="库存">{{ detail.stock }}</el-descriptions-item>
         <el-descriptions-item label="销量">{{ detail.salesCount || 0 }}</el-descriptions-item>
         <el-descriptions-item label="供应商">{{ detail.vendorName || '-' }}</el-descriptions-item>