Explorar o código

feat: 新增停车券发放记录功能

停车券生成时落库记录(t_parking_coupon_record),跳转使用时更新状态。
admin-web 和 admin-h5 同步新增查询页面。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline hai 2 días
pai
achega
aae3d02e43

+ 14 - 0
admin-h5/src/api/parkingCouponRecord.js

@@ -0,0 +1,14 @@
+import { get } from '../utils'
+
+/**
+ * 获取停车券发放记录分页列表
+ * @param {Object} params - 查询参数 { pageNum, pageSize, stationId, status }
+ * @returns {Promise}
+ */
+export const getParkingCouponRecordList = (params = {}) => {
+  return get('/parking-coupon-record/page', {
+    pageNum: params.pageNum || 1,
+    pageSize: params.pageSize || 10,
+    ...params
+  })
+}

+ 6 - 0
admin-h5/src/pages.json

@@ -83,6 +83,12 @@
         "navigationBarTitleText": "数据统计"
       }
     },
+    {
+      "path": "pages/parking-coupon-record/list",
+      "style": {
+        "navigationBarTitleText": "停车券发放记录"
+      }
+    },
     {
       "path": "pages/setting/index",
       "style": {

+ 308 - 0
admin-h5/src/pages/parking-coupon-record/list.vue

@@ -0,0 +1,308 @@
+<template>
+  <view class="record-container">
+    <!-- 顶部导航栏 -->
+    <view class="header-nav">
+      <view class="nav-left" @click="goBack">
+        <AppIcon name="chevron-left" size="20" color="#FFFFFF" />
+      </view>
+      <text class="nav-title">停车券发放记录</text>
+      <view class="nav-right"></view>
+    </view>
+
+    <!-- 状态筛选 -->
+    <view class="filter-bar">
+      <view
+        v-for="(option, index) in statusOptions"
+        :key="index"
+        class="filter-item"
+        :class="{ active: activeStatus === option.value }"
+        @click="handleStatusChange(option.value)">
+        <text>{{ option.label }}</text>
+      </view>
+    </view>
+
+    <!-- 记录列表 -->
+    <view class="record-list" v-if="list.length > 0">
+      <view
+        class="record-item"
+        v-for="(item, index) in list"
+        :key="index">
+        <view class="item-header">
+          <text class="item-code">{{ item.code || '-' }}</text>
+          <text class="status-tag" :class="item.status === 1 ? 'used' : 'unused'">
+            {{ item.status === 1 ? '已使用' : '未使用' }}
+          </text>
+        </view>
+        <view class="item-content">
+          <view class="info-row">
+            <text class="info-label">用户ID</text>
+            <text class="info-value">{{ item.userId || '-' }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">站点ID</text>
+            <text class="info-value">{{ item.stationId || '-' }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">关联订单</text>
+            <text class="info-value order">{{ item.orderId || '-' }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">过期时间</text>
+            <text class="info-value">{{ item.expireTime ? formatTime(item.expireTime) : '-' }}</text>
+          </view>
+        </view>
+        <view class="item-footer">
+          <text class="footer-time">发放: {{ formatTime(item.createTime) }}</text>
+          <text class="footer-time" v-if="item.status === 1">使用: {{ formatTime(item.usedTime) }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 加载更多 -->
+    <view class="load-more" v-if="list.length > 0">
+      <text class="load-more-text" v-if="loadMoreStatus === 'more'">上拉加载更多</text>
+      <text class="load-more-text" v-else-if="loadMoreStatus === 'loading'">正在加载...</text>
+      <text class="load-more-text" v-else>没有更多数据了</text>
+    </view>
+
+    <!-- 空状态 -->
+    <view class="empty-state" v-if="list.length === 0 && !loading">
+      <view class="empty-icon-wrapper"><AppIcon name="home" size="48" color="#BFBFBF" /></view>
+      <text class="empty-text">暂无停车券发放记录</text>
+    </view>
+
+    <!-- 加载状态 -->
+    <view class="loading-state" v-if="loading && list.length === 0">
+      <view class="loading-spinner"></view>
+      <text class="loading-text">加载中...</text>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { onReachBottom } from '@dcloudio/uni-app'
+import { getParkingCouponRecordList } from '../../api/parkingCouponRecord.js'
+import { formatTime } from '../../utils/index.js'
+
+const list = ref([])
+const loading = ref(true)
+const page = ref(1)
+const pageSize = ref(10)
+const hasMore = ref(true)
+const loadMoreStatus = ref('more')
+const activeStatus = ref('')
+
+const statusOptions = [
+  { label: '全部', value: '' },
+  { label: '未使用', value: 0 },
+  { label: '已使用', value: 1 }
+]
+
+const goBack = () => {
+  uni.navigateBack()
+}
+
+const loadData = async (isLoadMore = false) => {
+  if (!isLoadMore) {
+    page.value = 1
+    list.value = []
+    hasMore.value = true
+    loadMoreStatus.value = 'more'
+  } else {
+    loadMoreStatus.value = 'loading'
+  }
+
+  loading.value = true
+  try {
+    const params = {
+      pageNum: page.value,
+      pageSize: pageSize.value
+    }
+    if (activeStatus.value !== '') params.status = activeStatus.value
+
+    const res = await getParkingCouponRecordList(params)
+    if (res && res.code === 200) {
+      const data = res.data
+      const records = data.list || data.records || data
+      const total = data.total || 0
+
+      const totalPages = Math.ceil(total / pageSize.value)
+      hasMore.value = page.value < totalPages
+      loadMoreStatus.value = hasMore.value ? 'more' : 'noMore'
+
+      if (isLoadMore) {
+        list.value = [...list.value, ...records]
+        page.value++
+      } else {
+        list.value = records
+        page.value = 2
+      }
+    }
+  } catch (error) {
+    console.error('加载停车券记录失败:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+const loadMore = () => {
+  if (hasMore.value && !loading.value && loadMoreStatus.value !== 'loading') {
+    loadData(true)
+  }
+}
+
+onReachBottom(() => {
+  loadMore()
+})
+
+const handleStatusChange = (status) => {
+  activeStatus.value = status
+  loadData()
+}
+
+onMounted(() => {
+  loadData()
+})
+</script>
+
+<style scoped>
+.record-container {
+  min-height: 100vh;
+  background-color: #F5F7FA;
+  padding-bottom: 100rpx;
+}
+
+.header-nav {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding-top: calc(24rpx + var(--status-bar-height));
+  padding-right: 30rpx;
+  padding-bottom: 24rpx;
+  padding-left: 30rpx;
+  background: #C6171E;
+}
+.nav-left, .nav-right { width: 120rpx; }
+.nav-title { font-size: 34rpx; color: #FFFFFF; font-weight: 600; }
+
+.filter-bar {
+  display: flex;
+  padding: 16rpx 20rpx;
+  background-color: #FFFFFF;
+  gap: 16rpx;
+}
+.filter-item {
+  padding: 10rpx 24rpx;
+  background-color: #F5F5F5;
+  border-radius: 20rpx;
+  font-size: 24rpx;
+  color: #666666;
+}
+.filter-item.active {
+  background: #C6171E;
+  color: #FFFFFF;
+}
+
+.record-list {
+  padding: 20rpx;
+}
+.record-item {
+  background-color: #FFFFFF;
+  border-radius: 20rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
+}
+.item-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-bottom: 16rpx;
+  border-bottom: 1rpx solid #F0F0F0;
+  margin-bottom: 16rpx;
+}
+.item-code {
+  font-size: 24rpx;
+  font-weight: 500;
+  color: #1A1A1A;
+  word-break: break-all;
+  flex: 1;
+  margin-right: 16rpx;
+}
+.status-tag {
+  font-size: 22rpx;
+  padding: 6rpx 16rpx;
+  border-radius: 20rpx;
+  font-weight: 500;
+  flex-shrink: 0;
+}
+.status-tag.unused {
+  color: #909399;
+  background-color: #F4F4F5;
+}
+.status-tag.used {
+  color: #67C23A;
+  background-color: #F0F9EB;
+}
+
+.info-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8rpx 0;
+}
+.info-label {
+  font-size: 26rpx;
+  color: #999999;
+}
+.info-value {
+  font-size: 26rpx;
+  color: #1A1A1A;
+  max-width: 360rpx;
+  text-align: right;
+}
+.info-value.order {
+  font-size: 22rpx;
+}
+
+.item-footer {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-top: 16rpx;
+  border-top: 1rpx solid #F0F0F0;
+  margin-top: 16rpx;
+}
+.footer-time {
+  font-size: 22rpx;
+  color: #999999;
+}
+
+.load-more {
+  display: flex;
+  justify-content: center;
+  padding: 30rpx 0;
+}
+.load-more-text { font-size: 26rpx; color: #999999; }
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 120rpx 0;
+}
+.empty-text { font-size: 28rpx; color: #999999; }
+
+.loading-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 120rpx 0;
+}
+.loading-spinner {
+  font-size: 60rpx;
+  animation: spin 1s linear infinite;
+}
+.loading-text { font-size: 28rpx; color: #999999; margin-top: 20rpx; }
+</style>

+ 16 - 1
admin-web/src/router/route.ts

@@ -435,7 +435,7 @@ export const adminRoutes: Array<RouteRecordRaw> = [
                     isAffix: false,
                     isIframe: false,
                     icon: 'ele-Promotion',
-                    perm: "banner.list,promotion.list",
+                    perm: "banner.list,promotion.list,parkingCouponRecord.list",
                 },
                 children: [
                     {
@@ -468,6 +468,21 @@ export const adminRoutes: Array<RouteRecordRaw> = [
                             icon: 'ele-Present',
                         },
                     },
+                    {
+                        path: '/marketing/parkingCouponRecord',
+                        name: 'adminParkingCouponRecord',
+                        component: () => import('/@/views/admin/parking-coupon-record/index.vue'),
+                        meta: {
+                            title: '停车券发放记录',
+                            isLink: '',
+                            isHide: false,
+                            isKeepAlive: true,
+                            isAffix: false,
+                            isIframe: false,
+                            perm: "parkingCouponRecord.list",
+                            icon: 'ele-Tickets',
+                        },
+                    },
                 ]
             },
 

+ 166 - 0
admin-web/src/views/admin/parking-coupon-record/index.vue

@@ -0,0 +1,166 @@
+<style scoped lang="scss">
+.system-container {
+
+  :deep(.el-card__body) {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    flex: 1;
+    overflow: auto;
+
+    .el-table {
+      flex: 1;
+    }
+  }
+}
+
+.page-pager {
+  background-color: var(--el-color-white);
+  height: 24px;
+}
+</style>
+<template>
+  <div class="system-container layout-padding">
+    <el-card shadow="hover" class="layout-padding-auto">
+      <el-form
+          :model="state.formQuery"
+          ref="queryRef"
+          size="default" label-width="0px" class="mt5 mb5">
+        <ext-select
+            v-model="state.formQuery.stationId"
+            placeholder="站点"
+            url="washStation/list"
+            url-method="post"
+            label-key="stationName"
+            value-key="stationId"
+            data-key="list"
+            clearable
+            class="wd200 ml10"/>
+
+        <el-select
+            v-model="state.formQuery.status"
+            placeholder="状态"
+            clearable
+            class="wd150 ml10">
+          <el-option label="未使用" :value="0"/>
+          <el-option label="已使用" :value="1"/>
+        </el-select>
+
+        <el-button class="ml10" plain size="default" type="success" @click="loadData(true)">
+          <SvgIcon name="ele-Search"/>
+          查询
+        </el-button>
+      </el-form>
+
+      <el-table
+          border
+          stripe="stripe"
+          :height="state.tableData.height"
+          :data="state.tableData.data"
+          v-loading="state.tableData.loading">
+        <template #empty>
+          <el-empty></el-empty>
+        </template>
+        <el-table-column
+            v-for="field in state.columns"
+            :key="field.prop"
+            :label="field.label"
+            :column-key="field.prop"
+            :width="field.width"
+            :min-width="field.minWidth"
+            :fixed="field.fixed"
+            :show-overflow-tooltip="!field.fixed&&field.width>150">
+
+          <template #default="{row}">
+            <template v-if="field.prop === 'status'">
+              <el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
+                {{ row.status === 1 ? '已使用' : '未使用' }}
+              </el-tag>
+            </template>
+            <template v-else-if="['createTime','usedTime','expireTime'].includes(field.prop)">
+              {{ row[field.prop] ? u.fmt.fmtDateTime(row[field.prop]) : '-' }}
+            </template>
+            <template v-else>
+              <div>{{ row[field.prop] || '-' }}</div>
+            </template>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <ext-page class="page-pager" v-model:value="state.pageQuery" @change="loadData(false)"/>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts" name="adminParkingCouponRecord">
+import {reactive, onMounted, ref, nextTick} from 'vue';
+import {$get} from "/@/utils/request";
+import u from '/@/utils/u'
+
+import ExtPage from '/@/components/form/ExtPage.vue'
+import ExtSelect from "/@/components/form/ExtSelect.vue";
+
+const queryRef = ref();
+
+const state = reactive({
+  formQuery: {
+    stationId: '',
+    status: '' as any,
+  },
+  pageQuery: {
+    pageNum: 1,
+    pageSize: 10,
+    total: 0
+  },
+  tableData: {
+    height: 500,
+    data: [] as Array<any>,
+    loading: false
+  },
+  columns: [
+    {label: '券码', width: 280, prop: 'code', fixed: 'left', resizable: true},
+    {label: '用户ID', width: 100, prop: 'userId', resizable: true},
+    {label: '站点ID', width: 120, prop: 'stationId', resizable: true},
+    {label: '关联订单', width: 200, prop: 'orderId', resizable: true},
+    {label: '状态', width: 100, prop: 'status', resizable: true},
+    {label: '发放时间', width: 180, prop: 'createTime', resizable: true},
+    {label: '使用时间', width: 180, prop: 'usedTime', resizable: true},
+    {label: '过期时间', width: 180, prop: 'expireTime', resizable: true},
+  ],
+})
+
+onMounted(() => {
+  loadData();
+
+  nextTick(() => {
+    let bodyHeight = document.body.clientHeight;
+    let queryHeight = queryRef.value.$el.clientHeight;
+    state.tableData.height = bodyHeight - queryHeight - 320
+  })
+});
+
+const loadData = (refresh: boolean = false) => {
+  if (refresh) {
+    state.pageQuery.pageNum = 1;
+  }
+  state.tableData.loading = true;
+  const params: any = {
+    pageNum: state.pageQuery.pageNum,
+    pageSize: state.pageQuery.pageSize,
+  };
+  if (state.formQuery.stationId) params.stationId = state.formQuery.stationId;
+  if (state.formQuery.status !== '' && state.formQuery.status !== null && state.formQuery.status !== undefined) {
+    params.status = state.formQuery.status;
+  }
+
+  $get(`/parking-coupon-record/page`, params).then((res: any) => {
+    let {list, total} = res;
+    state.tableData.data = list || [];
+    state.pageQuery.total = total || 0;
+    state.tableData.loading = false;
+  }).catch(e => {
+    console.error(e)
+    state.tableData.loading = false;
+  })
+};
+</script>

+ 33 - 0
car-wash-admin/src/main/java/com/kym/admin/controller/ParkingCouponRecordController.java

@@ -0,0 +1,33 @@
+package com.kym.admin.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import com.kym.common.R;
+import com.kym.service.ParkingCouponRecordService;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 停车券发放记录 前端控制器
+ *
+ * @author skyline
+ * @since 2026-06-12
+ */
+@RestController
+@RequestMapping("/parking-coupon-record")
+public class ParkingCouponRecordController {
+
+    private final ParkingCouponRecordService parkingCouponRecordService;
+
+    public ParkingCouponRecordController(ParkingCouponRecordService parkingCouponRecordService) {
+        this.parkingCouponRecordService = parkingCouponRecordService;
+    }
+
+    @SaCheckPermission("parkingCouponRecord.list")
+    @GetMapping("/page")
+    public R<?> page(@RequestParam(defaultValue = "1") Integer pageNum,
+                     @RequestParam(defaultValue = "10") Integer pageSize,
+                     @RequestParam(required = false) Long userId,
+                     @RequestParam(required = false) String stationId,
+                     @RequestParam(required = false) Integer status) {
+        return R.success(parkingCouponRecordService.page(pageNum, pageSize, userId, stationId, status));
+    }
+}

+ 71 - 0
car-wash-entity/src/main/java/com/kym/entity/ParkingCouponRecord.java

@@ -0,0 +1,71 @@
+package com.kym.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 停车券发放记录表
+ * </p>
+ *
+ * @author skyline
+ * @since 2026-06-12
+ */
+@Getter
+@Setter
+@TableName("t_parking_coupon_record")
+@Accessors(chain = true)
+public class ParkingCouponRecord extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    public static final int STATUS_未使用 = 0;
+    public static final int STATUS_已使用 = 1;
+
+    /**
+     * UUID唯一标识
+     */
+    private String code;
+
+    /**
+     * 用户ID
+     */
+    private Long userId;
+
+    /**
+     * 站点ID
+     */
+    private String stationId;
+
+    /**
+     * 触发发放的订单号
+     */
+    private String orderId;
+
+    /**
+     * 停车券目标URL
+     */
+    private String targetUrl;
+
+    /**
+     * 状态:0-未使用,1-已使用
+     */
+    private Integer status;
+
+    /**
+     * 使用时间
+     */
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime usedTime;
+
+    /**
+     * 过期时间
+     */
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime expireTime;
+}

+ 24 - 0
car-wash-entity/src/main/resources/sql/v13_parking_coupon_record.sql

@@ -0,0 +1,24 @@
+-- ====================================================
+-- 停车券发放记录表
+-- 发布日期:2026-06-12
+-- ====================================================
+
+CREATE TABLE `t_parking_coupon_record` (
+    `id` bigint NOT NULL AUTO_INCREMENT,
+    `company_id` bigint DEFAULT NULL,
+    `code` varchar(64) NOT NULL COMMENT 'UUID唯一标识',
+    `user_id` bigint NOT NULL COMMENT '用户ID',
+    `station_id` varchar(64) DEFAULT NULL COMMENT '站点ID',
+    `order_id` varchar(64) DEFAULT NULL COMMENT '触发发放的订单号',
+    `target_url` varchar(2048) DEFAULT NULL COMMENT '停车券目标URL',
+    `status` tinyint DEFAULT 0 COMMENT '0-未使用,1-已使用',
+    `used_time` datetime DEFAULT NULL COMMENT '使用时间',
+    `expire_time` datetime DEFAULT NULL COMMENT '过期时间',
+    `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
+    `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_code` (`code`),
+    KEY `idx_user_id` (`user_id`),
+    KEY `idx_station_id` (`station_id`),
+    KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='停车券发放记录表';

+ 16 - 0
car-wash-mapper/src/main/java/com/kym/mapper/ParkingCouponRecordMapper.java

@@ -0,0 +1,16 @@
+package com.kym.mapper;
+
+import com.kym.entity.ParkingCouponRecord;
+import com.kym.mapper.mybatisplus.MyBaseMapper;
+
+/**
+ * <p>
+ * 停车券发放记录表 Mapper 接口
+ * </p>
+ *
+ * @author skyline
+ * @since 2026-06-12
+ */
+public interface ParkingCouponRecordMapper extends MyBaseMapper<ParkingCouponRecord> {
+
+}

+ 13 - 1
car-wash-miniapp/src/main/java/com/kym/miniapp/controller/ParkingCouponController.java

@@ -2,9 +2,11 @@ package com.kym.miniapp.controller;
 
 import cn.dev33.satoken.annotation.SaIgnore;
 import com.kym.common.utils.HttpUtil;
+import com.kym.entity.ParkingCouponRecord;
 import com.kym.entity.User;
 import com.kym.entity.WashOrder;
 import com.kym.service.MpMsgTemplateService;
+import com.kym.service.ParkingCouponRecordService;
 import com.kym.service.UserService;
 import com.kym.service.WashOrderService;
 import com.kym.service.cache.KymCache;
@@ -15,6 +17,7 @@ import org.springframework.web.bind.annotation.*;
 
 import java.io.IOException;
 import java.net.URL;
+import java.time.LocalDateTime;
 import java.util.UUID;
 
 /**
@@ -32,16 +35,19 @@ public class ParkingCouponController {
     private final WashOrderService washOrderService;
     private final MpMsgTemplateService mpMsgTemplateService;
     private final UserService userService;
+    private final ParkingCouponRecordService parkingCouponRecordService;
 
     @Value("${kym.domain}")
     private String DOMAIN;
 
     public ParkingCouponController(WashOrderService washOrderService,
                                    MpMsgTemplateService mpMsgTemplateService,
-                                   UserService userService) {
+                                   UserService userService,
+                                   ParkingCouponRecordService parkingCouponRecordService) {
         this.washOrderService = washOrderService;
         this.mpMsgTemplateService = mpMsgTemplateService;
         this.userService = userService;
+        this.parkingCouponRecordService = parkingCouponRecordService;
     }
 
     /**
@@ -67,6 +73,12 @@ public class ParkingCouponController {
             return;
         }
 
+        parkingCouponRecordService.lambdaUpdate()
+                .eq(ParkingCouponRecord::getCode, code)
+                .set(ParkingCouponRecord::getStatus, ParkingCouponRecord.STATUS_已使用)
+                .set(ParkingCouponRecord::getUsedTime, LocalDateTime.now())
+                .update();
+
         if (url.length() < MAX_REDIRECT_URL_LENGTH) {
             log.info("短 URL 直接重定向, code: {}, urlLength: {}", code, url.length());
             response.sendRedirect(url);

+ 18 - 0
car-wash-service/src/main/java/com/kym/service/ParkingCouponRecordService.java

@@ -0,0 +1,18 @@
+package com.kym.service;
+
+import com.kym.entity.ParkingCouponRecord;
+import com.kym.entity.common.PageBean;
+import com.kym.service.mybatisplus.MyBaseService;
+
+/**
+ * <p>
+ * 停车券发放记录表 服务类
+ * </p>
+ *
+ * @author skyline
+ * @since 2026-06-12
+ */
+public interface ParkingCouponRecordService extends MyBaseService<ParkingCouponRecord> {
+
+    PageBean<ParkingCouponRecord> page(Integer pageNum, Integer pageSize, Long userId, String stationId, Integer status);
+}

+ 12 - 1
car-wash-service/src/main/java/com/kym/service/impl/OrderSettlementServiceImpl.java

@@ -37,11 +37,13 @@ public class OrderSettlementServiceImpl implements OrderSettlementService {
     private final PayLogService payLogService;
     private final UserService userService;
     private final MpMsgTemplateService mpMsgTemplateService;
+    private final ParkingCouponRecordService parkingCouponRecordService;
 
     public OrderSettlementServiceImpl(WashOrderService washOrderService, WalletDetailService walletDetailService,
                                       AccountService accountService, SplitRecordService splitRecordService,
                                       PayLogService payLogService, UserService userService,
-                                      MpMsgTemplateService mpMsgTemplateService) {
+                                      MpMsgTemplateService mpMsgTemplateService,
+                                      ParkingCouponRecordService parkingCouponRecordService) {
         this.washOrderService = washOrderService;
         this.walletDetailService = walletDetailService;
         this.accountService = accountService;
@@ -49,6 +51,7 @@ public class OrderSettlementServiceImpl implements OrderSettlementService {
         this.payLogService = payLogService;
         this.userService = userService;
         this.mpMsgTemplateService = mpMsgTemplateService;
+        this.parkingCouponRecordService = parkingCouponRecordService;
     }
 
     @Override
@@ -136,6 +139,14 @@ public class OrderSettlementServiceImpl implements OrderSettlementService {
             var parkingCouponUrl = KymCache.INSTANCE.getParkingQrCodeUrlByStationId(washOrder.getStationId());
             var code = UUID.randomUUID().toString();
             KymCache.INSTANCE.setParkingCouponCode(code, parkingCouponUrl, 3600 * 2L);
+            parkingCouponRecordService.save(new ParkingCouponRecord()
+                    .setCode(code)
+                    .setUserId(washOrder.getUserId())
+                    .setStationId(washOrder.getStationId())
+                    .setOrderId(washOrder.getOrderId())
+                    .setTargetUrl(parkingCouponUrl)
+                    .setStatus(ParkingCouponRecord.STATUS_未使用)
+                    .setExpireTime(LocalDateTime.now().plusHours(2)));
             var url = DOMAIN + "/api/parking-coupon?code=" + code;
             mpMsgTemplateService.sendParkingCouponMsg(washOrder, url);
         } else {

+ 34 - 0
car-wash-service/src/main/java/com/kym/service/impl/ParkingCouponRecordServiceImpl.java

@@ -0,0 +1,34 @@
+package com.kym.service.impl;
+
+import com.github.pagehelper.PageHelper;
+import com.kym.common.utils.CommUtil;
+import com.kym.entity.ParkingCouponRecord;
+import com.kym.entity.common.PageBean;
+import com.kym.mapper.ParkingCouponRecordMapper;
+import com.kym.service.ParkingCouponRecordService;
+import com.kym.service.mybatisplus.MyBaseServiceImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 停车券发放记录表 服务实现类
+ * </p>
+ *
+ * @author skyline
+ * @since 2026-06-12
+ */
+@Service
+public class ParkingCouponRecordServiceImpl extends MyBaseServiceImpl<ParkingCouponRecordMapper, ParkingCouponRecord> implements ParkingCouponRecordService {
+
+    @Override
+    public PageBean<ParkingCouponRecord> page(Integer pageNum, Integer pageSize, Long userId, String stationId, Integer status) {
+        PageHelper.startPage(pageNum, pageSize);
+        var list = lambdaQuery()
+                .eq(userId != null, ParkingCouponRecord::getUserId, userId)
+                .eq(CommUtil.isNotEmptyAndNull(stationId), ParkingCouponRecord::getStationId, stationId)
+                .eq(status != null, ParkingCouponRecord::getStatus, status)
+                .orderByDesc(ParkingCouponRecord::getCreateTime)
+                .list();
+        return new PageBean<>(list);
+    }
+}

+ 22 - 1
car-wash-service/src/main/java/com/kym/service/impl/WashOrderServiceImpl.java

@@ -10,6 +10,7 @@ import com.kym.common.exception.BusinessException;
 import com.kym.common.utils.CommUtil;
 import com.kym.common.utils.OrderUtils;
 import com.kym.entity.Account;
+import com.kym.entity.ParkingCouponRecord;
 import com.kym.entity.User;
 import com.kym.entity.WashOrder;
 import com.kym.entity.WashStation;
@@ -24,6 +25,7 @@ import com.kym.entity.vo.WashOrderVo;
 import com.kym.mapper.WashOrderMapper;
 import com.kym.service.AccountService;
 import com.kym.service.OrderSettlementService;
+import com.kym.service.ParkingCouponRecordService;
 import com.kym.service.UserService;
 import com.kym.service.WashOrderService;
 import com.kym.service.WashStationService;
@@ -62,18 +64,21 @@ public class WashOrderServiceImpl extends MyBaseServiceImpl<WashOrderMapper, Was
     private final WashStationService washStationService;
     private final UserService userService;
     private final OrderSettlementService orderSettlementService;
+    private final ParkingCouponRecordService parkingCouponRecordService;
 
     @Value("${kym.domain}")
     private String DOMAIN;
 
     public WashOrderServiceImpl(AwoaraService awoaraService, AccountService accountService,
                                 @Lazy WashStationService washStationService, UserService userService,
-                                @Lazy OrderSettlementService orderSettlementService) {
+                                @Lazy OrderSettlementService orderSettlementService,
+                                ParkingCouponRecordService parkingCouponRecordService) {
         this.awoaraService = awoaraService;
         this.accountService = accountService;
         this.washStationService = washStationService;
         this.userService = userService;
         this.orderSettlementService = orderSettlementService;
+        this.parkingCouponRecordService = parkingCouponRecordService;
     }
 
     /**
@@ -520,6 +525,14 @@ public class WashOrderServiceImpl extends MyBaseServiceImpl<WashOrderMapper, Was
             var parkingCouponUrl = KymCache.INSTANCE.getParkingQrCodeUrlByStationId(orderList.get(0).getStationId());
             var code = UUID.randomUUID().toString();
             KymCache.INSTANCE.setParkingCouponCode(code, parkingCouponUrl, 3600 * 2L);
+            parkingCouponRecordService.save(new ParkingCouponRecord()
+                    .setCode(code)
+                    .setUserId(user.getId())
+                    .setStationId(orderList.get(0).getStationId())
+                    .setOrderId(orderList.get(0).getOrderId())
+                    .setTargetUrl(parkingCouponUrl)
+                    .setStatus(ParkingCouponRecord.STATUS_未使用)
+                    .setExpireTime(LocalDateTime.now().plusHours(2)));
             return DOMAIN + "/api/parking-coupon?code=" + code;
         }
 
@@ -557,6 +570,14 @@ public class WashOrderServiceImpl extends MyBaseServiceImpl<WashOrderMapper, Was
                     var parkingCouponUrl = KymCache.INSTANCE.getParkingQrCodeUrlByStationId(orderList.get(0).getStationId());
                     var code = UUID.randomUUID().toString();
                     KymCache.INSTANCE.setParkingCouponCode(code, parkingCouponUrl, 3600 * 2L);
+                    parkingCouponRecordService.save(new ParkingCouponRecord()
+                            .setCode(code)
+                            .setUserId(user.getId())
+                            .setStationId(orderList.get(0).getStationId())
+                            .setOrderId(orderList.get(0).getOrderId())
+                            .setTargetUrl(parkingCouponUrl)
+                            .setStatus(ParkingCouponRecord.STATUS_未使用)
+                            .setExpireTime(LocalDateTime.now().plusHours(2)));
                     return DOMAIN + "/api/parking-coupon?code=" + code;
                 }
             }