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