From e2b3605106c81ec21a45dfcd6f163ed174d295c0 Mon Sep 17 00:00:00 2001
From: shinnosuke <tantao@1hoper.com>
Date: 星期一, 03 八月 2026 18:03:31 +0800
Subject: [PATCH] fix: resolve admin merchant management defects

---
 talent-card-admin/src/views/MerchantCreateView.vue                                               |  170 +++++++++++++++++++++++++---
 talent-card-admin/src/views/OrderView.vue                                                        |   12 +
 talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantCreateRequest.java |    6 +
 talent-card-admin/src/views/RefundRecordView.vue                                                 |    8 +
 talent-card-admin/src/views/ProductView.vue                                                      |    6 
 talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java  |   38 +++++
 talent-card-admin/src/views/LoginView.vue                                                        |   22 +++
 talent-card-admin/src/style.css                                                                  |   38 +++++
 talent-card-admin/src/layouts/AdminLayout.vue                                                    |    5 
 talent-card-admin/src/views/MerchantSettlementView.vue                                           |   13 +
 10 files changed, 279 insertions(+), 39 deletions(-)

diff --git a/talent-card-admin/src/layouts/AdminLayout.vue b/talent-card-admin/src/layouts/AdminLayout.vue
index dbe8216..3cf9767 100644
--- a/talent-card-admin/src/layouts/AdminLayout.vue
+++ b/talent-card-admin/src/layouts/AdminLayout.vue
@@ -12,6 +12,7 @@
   SwitchButton,
   Tickets,
   User,
+  Wallet,
 } from '@element-plus/icons-vue'
 import { logoutAdmin } from '../api/auth'
 import { useAppStore } from '../stores/app'
@@ -78,6 +79,9 @@
 const merchantMenus: MenuItem[] = [
   { path: '/merchants', label: '商家查询' },
   { path: '/products', label: '团单审核' },
+]
+
+const financeMenus: MenuItem[] = [
   { path: '/merchant-settlement', label: '商户结算审核' },
 ]
 
@@ -121,6 +125,7 @@
   { index: 'merchant-management', label: '商家管理', icon: OfficeBuilding, items: merchantMenus },
   { index: 'order-management', label: '订单管理', icon: Tickets, items: orderMenus },
   { index: 'reservation-management', label: '预约管理', icon: Calendar, items: reservationMenus },
+  { index: 'finance-management', label: '财务管理', icon: Wallet, items: financeMenus },
   { index: 'operation-management', label: '运营管理', icon: Operation, items: operationMenus },
   { index: 'system-management', label: '系统管理', icon: Setting, items: systemMenus },
 ]
diff --git a/talent-card-admin/src/style.css b/talent-card-admin/src/style.css
index 78d3a02..b169a76 100644
--- a/talent-card-admin/src/style.css
+++ b/talent-card-admin/src/style.css
@@ -542,6 +542,15 @@
   align-items: start;
 }
 
+.merchant-create-content.service-merchant-content {
+  grid-template-columns: minmax(470px, 1.15fr) minmax(320px, 0.88fr);
+  justify-content: start;
+}
+
+.service-merchant-content .merchant-hours-block {
+  margin-top: 18px;
+}
+
 .merchant-form-row,
 .merchant-cover-row,
 .merchant-hours-block {
@@ -621,7 +630,7 @@
   align-items: center;
 }
 
-.merchant-hour-row .el-input {
+.merchant-hour-row .el-date-editor {
   width: 210px;
 }
 
@@ -1154,6 +1163,19 @@
   margin: 46px 34px 0 0;
 }
 
+.merchant-coupons-filter .el-input__wrapper,
+.merchant-coupons-filter .el-select__wrapper,
+.merchant-coupons-filter .el-range-editor.el-input__wrapper,
+.merchant-coupon-edit-form .el-input__wrapper,
+.merchant-coupon-edit-form .el-select__wrapper,
+.merchant-coupon-edit-form .el-range-editor.el-input__wrapper {
+  background: #ffffff;
+}
+
+.merchant-coupons-filter .coupon-created-time-picker {
+  width: 280px;
+}
+
 .merchant-product-form-panel {
   margin-bottom: 18px;
   padding: 20px 18px 26px;
@@ -1389,6 +1411,10 @@
   margin-right: 8px;
 }
 
+.product-right-row .right-limit-field .el-input-number {
+  width: 100%;
+}
+
 .product-right-row .right-unit {
   flex: 0 0 22px;
 }
@@ -1399,7 +1425,7 @@
 }
 
 .product-right-row .right-reset-select {
-  flex: 0 0 64px;
+  flex: 0 0 96px;
   margin-right: 12px;
 }
 
@@ -1429,6 +1455,10 @@
 }
 
 .merchant-product-form-panel .product-right-sub .el-input {
+  width: 100px;
+}
+
+.merchant-product-form-panel .product-right-sub .right-exceed-discount-input {
   width: 100px;
 }
 
@@ -1505,12 +1535,12 @@
 .merchant-coupon-edit-form .el-date-editor.el-input__wrapper {
   min-height: 28px;
   border-radius: 0;
-  background: #f7f8fb;
+  background: #ffffff;
   box-shadow: none;
 }
 
 .merchant-coupon-edit-form .el-date-editor.el-input__wrapper {
-  width: 200px;
+  width: 280px;
 }
 
 .merchant-coupon-edit-form .el-date-editor .el-range-input,
diff --git a/talent-card-admin/src/views/LoginView.vue b/talent-card-admin/src/views/LoginView.vue
index 470267d..1a0b6d9 100644
--- a/talent-card-admin/src/views/LoginView.vue
+++ b/talent-card-admin/src/views/LoginView.vue
@@ -5,11 +5,26 @@
 import { loginAdmin } from '../api/auth'
 import { useAppStore } from '../stores/app'
 
+const rememberedCredentialsKey = 'talent-card-admin-remembered-credentials'
+
+function rememberedCredentials() {
+  try {
+    return JSON.parse(localStorage.getItem(rememberedCredentialsKey) || '{}') as { account?: string; password?: string }
+  } catch {
+    return {}
+  }
+}
+
 const router = useRouter()
 const route = useRoute()
 const appStore = useAppStore()
 const submitting = ref(false)
-const form = reactive({ account: '', password: '', remember: true })
+const storedCredentials = rememberedCredentials()
+const form = reactive({
+  account: storedCredentials.account || '',
+  password: storedCredentials.password || '',
+  remember: Boolean(storedCredentials.account && storedCredentials.password),
+})
 const errors = reactive({ account: '', password: '' })
 
 function clearError(field: 'account' | 'password') {
@@ -24,6 +39,11 @@
   submitting.value = true
   try {
     const result = await loginAdmin(account, form.password)
+    if (form.remember) {
+      localStorage.setItem(rememberedCredentialsKey, JSON.stringify({ account, password: form.password }))
+    } else {
+      localStorage.removeItem(rememberedCredentialsKey)
+    }
     appStore.login(
       result.accessToken,
       { name: result.operator.name, role: result.operator.roleCode === 'platform_admin' ? '平台管理' : result.operator.roleCode },
diff --git a/talent-card-admin/src/views/MerchantCreateView.vue b/talent-card-admin/src/views/MerchantCreateView.vue
index 5d09ae1..7b66bf3 100644
--- a/talent-card-admin/src/views/MerchantCreateView.vue
+++ b/talent-card-admin/src/views/MerchantCreateView.vue
@@ -27,7 +27,7 @@
 
 interface BusinessHour {
   id: number
-  value: string
+  value: string[]
 }
 
 interface MerchantRightConfig {
@@ -58,10 +58,16 @@
 const couponMode = ref<'list' | 'edit'>('list')
 const isEditMode = computed(() => route.query.mode === 'edit')
 const isFoodMerchant = computed(() => form.merchantType === '餐饮服务')
+const isServiceMerchant = computed(() => isFoodMerchant.value || form.merchantType === '景区服务')
 
 function queryString(key: string, fallback = '') {
   const value = route.query[key]
   return typeof value === 'string' && value ? value : fallback
+}
+
+function defaultMerchantRecommend() {
+  const merchantType = queryString('merchantType')
+  return merchantType === '餐饮服务' || merchantType === '景区服务' ? 1 : 0
 }
 
 const tabs = [
@@ -86,8 +92,8 @@
   usageInstructions: '',
   rightsText: '',
   serviceLabel: '',
-  recommend: 0,
-  sortNo: 0,
+  recommend: defaultMerchantRecommend(),
+  sortNo: undefined,
   status: 1,
 })
 
@@ -159,6 +165,8 @@
   availableTime: '',
   otherRule: '',
 })
+const productValidPeriodRange = ref<string[]>([])
+const productAvailableTimeRange = ref<string[]>([])
 
 const productRights = reactive([
   {
@@ -170,6 +178,7 @@
     exceedDiscount: '',
     resetCycle: '每天',
     subsidy: true,
+    exceedSubsidy: true,
   },
   {
     level: '杰出人才卡',
@@ -180,6 +189,7 @@
     exceedDiscount: '',
     resetCycle: '每天',
     subsidy: true,
+    exceedSubsidy: true,
   },
   {
     level: '菁英人才卡',
@@ -190,6 +200,7 @@
     exceedDiscount: '',
     resetCycle: '每天',
     subsidy: true,
+    exceedSubsidy: true,
   },
 ])
 
@@ -204,6 +215,7 @@
       exceedDiscount: '',
       resetCycle: '每天',
       subsidy: true,
+      exceedSubsidy: true,
     },
     {
       level: '杰出人才卡',
@@ -214,6 +226,7 @@
       exceedDiscount: '',
       resetCycle: '每天',
       subsidy: true,
+      exceedSubsidy: true,
     },
     {
       level: '菁英人才卡',
@@ -224,6 +237,7 @@
       exceedDiscount: '',
       resetCycle: '每天',
       subsidy: true,
+      exceedSubsidy: true,
     },
   ]
 }
@@ -262,7 +276,7 @@
 const couponProducts = ref<Product[]>([])
 const selectedCouponProductIds = ref<Array<number | string>>([])
 
-const businessHours = reactive<BusinessHour[]>([{ id: 1, value: '' }])
+const businessHours = reactive<BusinessHour[]>([{ id: 1, value: [] }])
 
 function defaultMerchantRights(): MerchantRightConfig[] {
   return [
@@ -335,12 +349,19 @@
 const mapMarkerIcon = L.divIcon({ className: 'merchant-map-marker', html: '<span></span>', iconSize: [26, 26], iconAnchor: [13, 26] })
 
 function addBusinessHour() {
-  businessHours.push({ id: Date.now(), value: '' })
+  businessHours.push({ id: Date.now(), value: [] })
 }
 
 function removeBusinessHour(index: number) {
   if (businessHours.length <= 1) return
   businessHours.splice(index, 1)
+}
+
+function hasValidBusinessHours() {
+  return businessHours.length > 0 && businessHours.every((item) => {
+    const [startTime, endTime] = item.value
+    return Boolean(startTime && endTime && startTime < endTime)
+  })
 }
 
 function selectedCoordinates() {
@@ -549,6 +570,14 @@
   }
 }
 
+function rangeValues(value: string, pattern: RegExp) {
+  return (value.match(pattern) || []).slice(0, 2)
+}
+
+function rangeText(value: string[]) {
+  return value.length === 2 && value[0] && value[1] ? `${value[0]} 至 ${value[1]}` : ''
+}
+
 function resetProductRights(config?: Product['rightsConfig']) {
   const next = parseJsonValue(config, defaultProductRights())
   productRights.splice(0, productRights.length, ...defaultProductRights().map((item) => {
@@ -581,6 +610,8 @@
   productNoticeForm.useMethod = notice.useMethod || ''
   productNoticeForm.availableTime = notice.availableTime || ''
   productNoticeForm.otherRule = notice.otherRule || ''
+  productValidPeriodRange.value = rangeValues(productNoticeForm.validPeriod, /\d{4}-\d{2}-\d{2}/g)
+  productAvailableTimeRange.value = rangeValues(productNoticeForm.availableTime, /\d{2}:\d{2}/g)
   resetProductRights(row?.rightsConfig)
 }
 
@@ -640,6 +671,20 @@
   productMode.value = 'list'
 }
 
+function validExceedDiscount(value: unknown) {
+  const text = String(value ?? '').trim()
+  if (!/^\d+(?:\.\d)?$/.test(text)) return false
+  const amount = Number(text)
+  return amount > 0 && amount <= 10
+}
+
+function validFreeLimitCount(value: unknown) {
+  const text = String(value ?? '').trim()
+  if (!/^\d+$/.test(text)) return false
+  const count = Number(text)
+  return count >= 1 && count <= 999
+}
+
 async function saveProduct() {
   if (!productForm.productName?.trim()) {
     ElMessage.warning('请输入商品名称')
@@ -661,6 +706,14 @@
     ElMessage.warning('请输入商品详情')
     return
   }
+  if (productRights.some((item) => item.free && !validFreeLimitCount(item.limitCount))) {
+    ElMessage.warning('免费次数请输入 1 至 999 的整数,999 表示不限次数')
+    return
+  }
+  if (productRights.some((item) => item.free && !validExceedDiscount(item.exceedDiscount))) {
+    ElMessage.warning('超过次数折扣请输入大于 0 且不超过 10 的一位小数')
+    return
+  }
   productSaving.value = true
   try {
     const payload = {
@@ -670,7 +723,11 @@
       originalPrice: productForm.originalPrice === undefined ? undefined : Number(productForm.originalPrice),
       stock: productForm.stock === undefined ? undefined : Number(productForm.stock),
       sortNo: productForm.sortNo === undefined ? 0 : Number(productForm.sortNo),
-      purchaseNotice: { ...productNoticeForm },
+      purchaseNotice: {
+        ...productNoticeForm,
+        validPeriod: rangeText(productValidPeriodRange.value),
+        availableTime: rangeText(productAvailableTimeRange.value),
+      },
       productImages: [...productImages.value],
       rightsConfig: productRights.map((item) => ({ ...item })),
     }
@@ -893,6 +950,14 @@
     ElMessage.warning('请输入使用门槛')
     return
   }
+  if (!couponEditForm.validTime || couponEditForm.validTime.length !== 2 || !couponEditForm.validTime[0] || !couponEditForm.validTime[1]) {
+    ElMessage.warning('请选择优惠券有效期')
+    return
+  }
+  if (couponEditForm.productScope === 'selected' && selectedCouponProductIds.value.length === 0) {
+    ElMessage.warning('请至少选择一个可用商品')
+    return
+  }
   couponSaving.value = true
   try {
     const payload = couponPayload()
@@ -918,7 +983,27 @@
     ElMessage.warning('请输入商家名称')
     return
   }
-	if (!isEditMode.value && isFoodMerchant.value && Number(form.canLoginMerchantApp) === 1) {
+	if (!form.contactMobile?.trim()) {
+		ElMessage.warning('请输入联系电话')
+		return
+	}
+	if (!form.address?.trim()) {
+		ElMessage.warning('请输入商家地址')
+		return
+	}
+	if (!form.coverUrl?.trim()) {
+		ElMessage.warning('请上传商家封面图')
+		return
+	}
+	if (!hasValidBusinessHours()) {
+		ElMessage.warning('请选择有效的营业时间')
+		return
+	}
+	if (form.sortNo === undefined || form.sortNo === null || String(form.sortNo).trim() === '') {
+		ElMessage.warning('请输入排序')
+		return
+	}
+	if (!isEditMode.value && isServiceMerchant.value && Number(form.canLoginMerchantApp) === 1) {
 		if (!/^[A-Za-z0-9]{1,32}$/.test(accountForm.loginAccount)) {
 			ElMessage.warning('登录账号仅支持英文和数字,长度不超过32位')
 			return
@@ -984,6 +1069,12 @@
 
 watch(() => form.coverUrl, resetMerchantCoverPreview)
 watch(() => productForm.coverUrl, resetProductCoverPreview)
+watch(productValidPeriodRange, (value) => {
+  productNoticeForm.validPeriod = rangeText(value)
+})
+watch(productAvailableTimeRange, (value) => {
+  productNoticeForm.availableTime = rangeText(value)
+})
 
 onMounted(() => {
 	if (isEditMode.value && queryString('merchantId')) {
@@ -1027,7 +1118,7 @@
       <section class="merchant-create-panel">
         <h3>商家信息</h3>
 
-        <div class="merchant-create-content">
+        <div class="merchant-create-content" :class="{ 'service-merchant-content': isServiceMerchant }">
           <div class="merchant-create-left">
             <div class="merchant-form-row three-columns">
               <label>商家名称</label>
@@ -1055,13 +1146,13 @@
             </div>
           </div>
 
-          <div class="merchant-create-middle">
+          <div v-if="!isServiceMerchant" class="merchant-create-middle">
             <div class="merchant-form-row">
               <label>联系电话</label>
               <el-input v-model="form.contactMobile" />
             </div>
 
-			<template v-if="!isFoodMerchant">
+			<template>
 				<div class="merchant-form-row recommend-row">
 					<label>开启团购</label>
 					<el-switch v-model="form.groupBuyingEnabled" :active-value="1" :inactive-value="0" />
@@ -1081,7 +1172,7 @@
           </div>
 
           <div class="merchant-create-right">
-            <div class="merchant-form-row">
+            <div v-if="isEditMode" class="merchant-form-row">
               <label>创建时间</label>
               <el-input v-model="form.createdAt" readonly />
             </div>
@@ -1090,7 +1181,16 @@
               <label>营业时间</label>
               <div class="merchant-hours-list">
                 <div v-for="(item, index) in businessHours" :key="item.id" class="merchant-hour-row">
-                  <el-input v-model="item.value" />
+                  <el-time-picker
+                    v-model="item.value"
+                    is-range
+                    arrow-control
+                    format="HH:mm"
+                    value-format="HH:mm"
+                    range-separator="至"
+                    start-placeholder="开始时间"
+                    end-placeholder="结束时间"
+                  />
                   <button class="merchant-hour-action" type="button" @click="removeBusinessHour(index)">
                     <el-icon><Minus /></el-icon>
                   </button>
@@ -1128,7 +1228,7 @@
       <template #footer><el-button @click="locationDialogVisible = false">取消</el-button><el-button type="primary" @click="saveLocation">确认</el-button></template>
     </el-dialog>
 
-		<section class="merchant-create-panel">
+		<section v-if="!isServiceMerchant" class="merchant-create-panel">
 			<h3>小程序详情配置</h3>
 			<div class="merchant-form-row">
 				<label>使用说明</label>
@@ -1143,7 +1243,7 @@
       <section class="merchant-account-panel">
         <h3>账户设置</h3>
         <el-form class="merchant-account-form" :model="accountForm" label-width="72px" label-position="left">
-          <el-form-item v-if="isFoodMerchant" label="登录商户端" label-width="100px">
+          <el-form-item v-if="isServiceMerchant" label="登录商户端" label-width="100px">
             <el-switch v-model="form.canLoginMerchantApp" :active-value="1" :inactive-value="0" />
           </el-form-item>
           <el-form-item label="登录账号" :error="accountTaken ? '登录账号已存在,请更换' : ''">
@@ -1438,7 +1538,14 @@
           <div class="product-notice-grid">
             <div class="product-notice-field">
               <label>有效期</label>
-              <el-input v-model="productNoticeForm.validPeriod" placeholder="请输入" />
+              <el-date-picker
+                v-model="productValidPeriodRange"
+                type="daterange"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                range-separator="至"
+                value-format="YYYY-MM-DD"
+              />
             </div>
             <div class="product-notice-field">
               <label>使用方式</label>
@@ -1446,7 +1553,15 @@
             </div>
             <div class="product-notice-field">
               <label>可用时间</label>
-              <el-input v-model="productNoticeForm.availableTime" placeholder="请输入" />
+              <el-time-picker
+                v-model="productAvailableTimeRange"
+                is-range
+                start-placeholder="开始时间"
+                end-placeholder="结束时间"
+                range-separator="至"
+                value-format="HH:mm"
+                format="HH:mm"
+              />
             </div>
             <div class="product-notice-field">
               <label>其他规则</label>
@@ -1467,7 +1582,14 @@
                 <template v-if="item.free">
                   <div class="right-limit-field">
                     <span class="product-right-tip">设置成999不限次数</span>
-                    <el-input v-model="item.limitCount" placeholder="请输入" />
+                    <el-input-number
+                      v-model="item.limitCount"
+                      :min="1"
+                      :max="999"
+                      :precision="0"
+                      :controls="false"
+                      placeholder="请输入"
+                    />
                   </div>
                   <span class="right-unit">次</span>
                   <span class="right-reset-label">重置周期</span>
@@ -1488,10 +1610,19 @@
               </div>
               <div v-if="item.free" class="product-right-sub">
                 <span>超过次数</span>
-                <el-input v-model="item.exceedDiscount" placeholder="请输入" />
+                <el-input-number
+                  v-model="item.exceedDiscount"
+                  class="right-exceed-discount-input"
+                  :min="0.1"
+                  :max="10"
+                  :step="0.1"
+                  :precision="1"
+                  :controls="false"
+                  placeholder="请输入"
+                />
                 <span>折</span>
                 <span>是否政府补贴</span>
-                <el-switch v-model="item.subsidy" />
+                <el-switch v-model="item.exceedSubsidy" />
               </div>
             </div>
           </div>
@@ -1523,6 +1654,7 @@
             <label>创建时间</label>
             <el-date-picker
               v-model="couponFilters.createdTime"
+              class="coupon-created-time-picker"
               type="daterange"
               start-placeholder="开始日期"
               end-placeholder="结束日期"
diff --git a/talent-card-admin/src/views/MerchantSettlementView.vue b/talent-card-admin/src/views/MerchantSettlementView.vue
index eea6009..c261f6d 100644
--- a/talent-card-admin/src/views/MerchantSettlementView.vue
+++ b/talent-card-admin/src/views/MerchantSettlementView.vue
@@ -1,11 +1,18 @@
 <script setup lang="ts">
-import { onMounted, ref } from 'vue'
+import { computed, onMounted, reactive, ref } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { getMerchantSettlement, reviewMerchantSettlement } from '../api/admin'
 type Kind = 'onboardings' | 'bank-cards' | 'withdrawals'
 const kind = ref<Kind>('onboardings'), loading = ref(false), rows = ref<any[]>([])
 const labels: Record<string,string> = { pending: '待审核', approved: '已通过', rejected: '已驳回', processing: '取现处理中', paid: '已到账', failed: '失败' }
+const filters = reactive({ merchantName: '', applicationNo: '', status: '' })
+const filteredRows = computed(() => rows.value.filter((row) => {
+  const merchantMatched = !filters.merchantName || String(row.merchantName || '').includes(filters.merchantName.trim())
+  const applicationMatched = !filters.applicationNo || String(row.applicationNo || '').includes(filters.applicationNo.trim())
+  return merchantMatched && applicationMatched && (!filters.status || row.status === filters.status)
+}))
 const load = async () => { loading.value = true; try { rows.value = (await getMerchantSettlement(kind.value)).records || [] } finally { loading.value = false } }
+const resetFilters = () => { filters.merchantName = ''; filters.applicationNo = ''; filters.status = '' }
 const review = async (row: any, status: string) => {
   let reason = '', providerValue = ''
   if (status === 'approved') { const isOnboarding = kind.value === 'onboardings'; const result = await ElMessageBox.prompt(isOnboarding ? '请输入汇付进件成功后返回的商户 huifu_id' : '请输入汇付绑卡成功后返回的取现卡 token_no', isOnboarding ? '配置汇付商户号' : '配置汇付取现卡序列号', { inputPattern: /^[A-Za-z0-9]{1,32}$/, inputErrorMessage: '请输入有效的汇付标识' }); providerValue = result.value }
@@ -15,5 +22,5 @@
 }
 onMounted(load)
 </script>
-<template><div class="page-stack"><el-card><div class="toolbar"><el-radio-group v-model="kind" @change="load"><el-radio-button label="onboardings">商户进件</el-radio-button><el-radio-button label="bank-cards">银行卡审核</el-radio-button><el-radio-button label="withdrawals">提现审核</el-radio-button></el-radio-group><el-button @click="load">刷新</el-button></div><el-table v-loading="loading" :data="rows" border><el-table-column prop="merchantName" label="商户"/><el-table-column prop="storeName" label="门店"/><el-table-column prop="applicationNo" label="申请单号"/><el-table-column prop="bankName" label="银行"/><el-table-column label="金额"><template #default="{row}">{{row.amountCents == null ? '-' : '¥' + (Number(row.amountCents)/100).toFixed(2)}}</template></el-table-column><el-table-column label="状态"><template #default="{row}">{{labels[row.status] || row.status}}</template></el-table-column><el-table-column label="操作" width="180"><template #default="{row}"><template v-if="row.status==='pending'"><el-button size="small" type="primary" @click="review(row,kind==='withdrawals'?'paid':'approved')">{{kind==='withdrawals'?'发起取现':'通过'}}</el-button><el-button size="small" type="danger" @click="review(row,kind==='withdrawals'?'failed':'rejected')">驳回</el-button></template></template></el-table-column></el-table></el-card></div></template>
-<style scoped>.toolbar{display:flex;justify-content:space-between;margin-bottom:18px}</style>
+<template><div class="page-stack"><el-card><div class="toolbar"><el-radio-group v-model="kind" @change="load"><el-radio-button label="onboardings">商户进件</el-radio-button><el-radio-button label="bank-cards">银行卡审核</el-radio-button><el-radio-button label="withdrawals">提现审核</el-radio-button></el-radio-group><el-button @click="load">刷新</el-button></div><el-form class="settlement-filters" inline><el-form-item label="商户"><el-input v-model="filters.merchantName" clearable placeholder="请输入商户名称" /></el-form-item><el-form-item label="申请单号"><el-input v-model="filters.applicationNo" clearable placeholder="请输入申请单号" /></el-form-item><el-form-item label="状态"><el-select v-model="filters.status" clearable placeholder="全部"><el-option label="待审核" value="pending" /><el-option label="已通过" value="approved" /><el-option label="已驳回" value="rejected" /><el-option label="处理中" value="processing" /><el-option label="已到账" value="paid" /><el-option label="失败" value="failed" /></el-select></el-form-item><el-form-item><el-button @click="resetFilters">重置</el-button></el-form-item></el-form><el-table v-loading="loading" :data="filteredRows" border><el-table-column prop="merchantName" label="商户"/><el-table-column prop="storeName" label="门店"/><el-table-column prop="applicationNo" label="申请单号"/><el-table-column prop="bankName" label="银行"/><el-table-column label="金额"><template #default="{row}">{{row.amountCents == null ? '-' : '¥' + (Number(row.amountCents)/100).toFixed(2)}}</template></el-table-column><el-table-column label="状态"><template #default="{row}">{{labels[row.status] || row.status}}</template></el-table-column><el-table-column label="操作" width="180"><template #default="{row}"><template v-if="row.status==='pending'"><el-button size="small" type="primary" @click="review(row,kind==='withdrawals'?'paid':'approved')">{{kind==='withdrawals'?'发起取现':'通过'}}</el-button><el-button size="small" type="danger" @click="review(row,kind==='withdrawals'?'failed':'rejected')">驳回</el-button></template></template></el-table-column></el-table></el-card></div></template>
+<style scoped>.toolbar{display:flex;justify-content:space-between;margin-bottom:18px}.settlement-filters{padding:14px 0 4px}</style>
diff --git a/talent-card-admin/src/views/OrderView.vue b/talent-card-admin/src/views/OrderView.vue
index b5feab5..72cf1d7 100644
--- a/talent-card-admin/src/views/OrderView.vue
+++ b/talent-card-admin/src/views/OrderView.vue
@@ -41,6 +41,10 @@
   return Number.isInteger(yuan) ? String(yuan) : yuan.toFixed(2)
 }
 
+function formatDateTime(value?: string) {
+  return value ? value.replace('T', ' ') : ''
+}
+
 function orderStatusText(row: Order) {
   if (row.orderStatus === 0) return '待支付'
   if (row.orderStatus === 2) return '已退款'
@@ -114,7 +118,7 @@
     { title: '手机号', value: row => row.talentMobile }, { title: '人才等级', value: row => row.talentLevel },
     { title: '商家', value: row => row.merchantName }, { title: '商品/权益', value: row => row.productName || row.benefitName },
     { title: '订单金额(元)', value: row => moneyText(row.orderAmount) }, { title: '补贴金额(元)', value: row => moneyText(row.subsidyAmount) },
-    { title: '下单时间', value: row => row.createdAt }, { title: '支付时间', value: row => row.payTime },
+    { title: '下单时间', value: row => formatDateTime(row.createdAt) }, { title: '支付时间', value: row => formatDateTime(row.payTime) },
     { title: '订单状态', value: row => orderStatusText(row) },
   ], result.records)
   ElMessage.success(`已导出 ${result.records.length} 条订单`)
@@ -263,8 +267,8 @@
         <el-table-column label="政府补贴" width="82" align="center">
           <template #default="{ row }">{{ moneyText(row.subsidyAmount) }}</template>
         </el-table-column>
-        <el-table-column prop="createdAt" label="下单时间" width="116" align="center" />
-        <el-table-column prop="payTime" label="支付时间" width="116" align="center" />
+        <el-table-column label="下单时间" width="116" align="center"><template #default="{ row }">{{ formatDateTime(row.createdAt) }}</template></el-table-column>
+        <el-table-column label="支付时间" width="116" align="center"><template #default="{ row }">{{ formatDateTime(row.payTime) }}</template></el-table-column>
         <el-table-column label="状态" width="86" align="center">
           <template #default="{ row }">{{ orderStatusText(row) }}</template>
         </el-table-column>
@@ -302,7 +306,7 @@
           <el-descriptions-item :label="amountLabel">{{ moneyText(activeOrder.orderAmount) }}</el-descriptions-item>
           <el-descriptions-item label="实际支付">{{ moneyText(activeOrder.payAmount) }}</el-descriptions-item>
           <el-descriptions-item label="政府补贴">{{ moneyText(activeOrder.subsidyAmount) }}</el-descriptions-item>
-          <el-descriptions-item label="下单时间">{{ activeOrder.createdAt || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="下单时间">{{ formatDateTime(activeOrder.createdAt) || '-' }}</el-descriptions-item>
           <el-descriptions-item label="状态">{{ orderStatusText(activeOrder) }}</el-descriptions-item>
         </el-descriptions>
       </div>
diff --git a/talent-card-admin/src/views/ProductView.vue b/talent-card-admin/src/views/ProductView.vue
index 5593bc2..0114852 100644
--- a/talent-card-admin/src/views/ProductView.vue
+++ b/talent-card-admin/src/views/ProductView.vue
@@ -55,6 +55,10 @@
   return Number(amount).toFixed(2).replace(/\.00$/, '')
 }
 
+function formatDateTime(value?: string) {
+  return value ? value.replace('T', ' ') : ''
+}
+
 async function loadData() {
   loading.value = true
   try {
@@ -189,7 +193,7 @@
           <template #default="{ row }">{{ row.salesCount || '' }}</template>
         </el-table-column>
         <el-table-column label="提交修改时间" width="145" align="center">
-          <template #default="{ row }">{{ row.submitTime || row.createdAt || '' }}</template>
+          <template #default="{ row }">{{ formatDateTime(row.submitTime || row.createdAt) }}</template>
         </el-table-column>
         <el-table-column label="状态" width="120" align="center">
           <template #default="{ row }">{{ auditStatusText(row.auditStatus) }}</template>
diff --git a/talent-card-admin/src/views/RefundRecordView.vue b/talent-card-admin/src/views/RefundRecordView.vue
index deb5301..d4dbc75 100644
--- a/talent-card-admin/src/views/RefundRecordView.vue
+++ b/talent-card-admin/src/views/RefundRecordView.vue
@@ -34,6 +34,10 @@
   return Number.isInteger(yuan) ? String(yuan) : yuan.toFixed(2)
 }
 
+function formatDateTime(value?: string) {
+  return value ? value.replace('T', ' ') : ''
+}
+
 async function loadData() {
   loading.value = true
   try {
@@ -85,7 +89,7 @@
     { title: '持卡人', value: row => row.talentName }, { title: '手机号', value: row => row.talentMobile },
     { title: '人才等级', value: row => row.talentLevel }, { title: '商家', value: row => row.merchantName },
     { title: '已付金额(元)', value: row => moneyText(row.paidAmount) }, { title: '退款金额(元)', value: row => moneyText(row.refundAmount) },
-    { title: '退款时间', value: row => row.refundTime }, { title: '操作人', value: row => row.operatorName },
+    { title: '退款时间', value: row => formatDateTime(row.refundTime) }, { title: '操作人', value: row => row.operatorName },
   ], result.records)
   ElMessage.success(`已导出 ${result.records.length} 条退款记录`)
 }
@@ -184,7 +188,7 @@
         <el-table-column label="剩余可退金额" width="82" align="center">
           <template #default="{ row }">{{ moneyText(row.refundableAmount) }}</template>
         </el-table-column>
-        <el-table-column prop="refundTime" label="退款时间" width="116" align="center" />
+        <el-table-column label="退款时间" width="116" align="center"><template #default="{ row }">{{ formatDateTime(row.refundTime) }}</template></el-table-column>
         <el-table-column prop="operatorName" label="操作人" width="104" align="center" />
         <el-table-column label="操作" width="104" fixed="right" align="center">
           <template #default="{ row }">
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java
index 374f605..287127a 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java
@@ -1,6 +1,8 @@
 package com.talentcard.server.business.admin;
 
 import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.format.DateTimeParseException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.LinkedHashMap;
@@ -312,7 +314,7 @@
 				insert into merchant(merchant_name, merchant_type, contact_name, contact_mobile, address,
 					                     logo_url, cover_url, business_status, longitude, latitude, can_login_merchant_app, group_buying_enabled,
 					                     usage_instructions, rights_text, merchant_rights_config, service_label, is_recommend, status, sort_no)
-				values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0)
+				values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)
 				""",
 				request.merchantName(),
 				request.merchantType(),
@@ -330,9 +332,10 @@
 				request.rightsText(),
 				toJson(request.merchantRightsConfig()),
 				request.serviceLabel(),
-				request.recommend() == null ? 0 : request.recommend());
+				request.recommend() == null ? defaultRecommend(request.merchantType()) : request.recommend(),
+				request.sortNo() == null ? 0 : request.sortNo());
 		Long id = jdbcTemplate.queryForObject("select last_insert_id()", Long.class);
-		if (isFoodMerchantWithAppAccess(request)) {
+		if (hasMerchantAppAccess(request)) {
 			String account = request.loginAccount().trim();
 			if (!account.matches("^[A-Za-z0-9]{1,32}$")) {
 				throw new BusinessException(ErrorCode.BAD_REQUEST, "商户登录账号只能包含英文字母和数字");
@@ -352,8 +355,13 @@
 		return one("select * from merchant where id = ?", id);
 	}
 
-	private boolean isFoodMerchantWithAppAccess(MerchantCreateRequest request) {
-		return "餐饮服务".equals(request.merchantType()) && (request.canLoginMerchantApp() == null || request.canLoginMerchantApp() == 1);
+	private boolean hasMerchantAppAccess(MerchantCreateRequest request) {
+		return ("餐饮服务".equals(request.merchantType()) || "景区服务".equals(request.merchantType()))
+				&& (request.canLoginMerchantApp() == null || request.canLoginMerchantApp() == 1);
+	}
+
+	private byte defaultRecommend(String merchantType) {
+		return (byte) (("餐饮服务".equals(merchantType) || "景区服务".equals(merchantType)) ? 1 : 0);
 	}
 
 	@Transactional
@@ -696,6 +704,7 @@
 	@Transactional
 	public Map<String, Object> createCoupon(CouponCreateRequest request) {
 		ensureMerchantExists(request.merchantId());
+		validateCouponConfiguration(request.validStart(), request.validEnd(), request.productScope(), request.productIds());
 		jdbcTemplate.update("""
 				insert into merchant_coupon(merchant_id, coupon_name, condition_type, condition_amount,
 				                            coupon_amount, talent_levels, valid_start, valid_end,
@@ -721,6 +730,7 @@
 	@Transactional
 	public Map<String, Object> updateCoupon(Long id, CouponUpdateRequest request) {
 		ensureMerchantExists(request.merchantId());
+		validateCouponConfiguration(request.validStart(), request.validEnd(), request.productScope(), request.productIds());
 		int updated = jdbcTemplate.update("""
 				update merchant_coupon
 				set merchant_id = ?, coupon_name = ?, condition_type = ?, condition_amount = ?,
@@ -859,6 +869,24 @@
 		return "ALL".equalsIgnoreCase(value) ? "ALL" : "SELECTED";
 	}
 
+	private void validateCouponConfiguration(String validStart, String validEnd, String productScope, List<Long> productIds) {
+		if (validStart == null || validStart.isBlank() || validEnd == null || validEnd.isBlank()) {
+			throw new BusinessException(ErrorCode.BAD_REQUEST, "优惠券有效期不能为空");
+		}
+		try {
+			LocalDate start = LocalDate.parse(validStart);
+			LocalDate end = LocalDate.parse(validEnd);
+			if (start.isAfter(end)) {
+				throw new BusinessException(ErrorCode.BAD_REQUEST, "优惠券结束日期不能早于开始日期");
+			}
+		} catch (DateTimeParseException exception) {
+			throw new BusinessException(ErrorCode.BAD_REQUEST, "优惠券有效期格式错误");
+		}
+		if (!"ALL".equalsIgnoreCase(productScope) && (productIds == null || productIds.isEmpty())) {
+			throw new BusinessException(ErrorCode.BAD_REQUEST, "请至少选择一个可用商品");
+		}
+	}
+
 	private int defaultNewsStatus(Integer status) {
 		return status == null ? 1 : status;
 	}
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantCreateRequest.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantCreateRequest.java
index 22ef5ea..b3d2a61 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantCreateRequest.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantCreateRequest.java
@@ -1,6 +1,7 @@
 package com.talentcard.server.business.admin;
 
 import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
 import jakarta.validation.constraints.Size;
 
 import java.util.List;
@@ -14,15 +15,20 @@
 		String merchantType,
 		@Size(max = 64)
 		String contactName,
+		@NotBlank(message = "请输入联系电话")
 		@Size(max = 20)
 		String contactMobile,
+		@NotBlank(message = "请输入商家地址")
 		@Size(max = 255)
 		String address,
 		String logoUrl,
+		@NotBlank(message = "请上传商家封面图")
 		String coverUrl,
 		@Size(max = 32)
 		String businessStatus,
 		Byte recommend,
+		@NotNull(message = "请输入排序")
+		Integer sortNo,
 		Double longitude,
 		Double latitude,
 		Byte groupBuyingEnabled,

--
Gitblit v1.7.1