fix: resolve admin merchant management defects
| | |
| | | SwitchButton, |
| | | Tickets, |
| | | User, |
| | | Wallet, |
| | | } from '@element-plus/icons-vue' |
| | | import { logoutAdmin } from '../api/auth' |
| | | import { useAppStore } from '../stores/app' |
| | |
| | | const merchantMenus: MenuItem[] = [ |
| | | { path: '/merchants', label: '商家查询' }, |
| | | { path: '/products', label: '团单审核' }, |
| | | ] |
| | | |
| | | const financeMenus: MenuItem[] = [ |
| | | { path: '/merchant-settlement', label: '商户结算审核' }, |
| | | ] |
| | | |
| | |
| | | { 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 }, |
| | | ] |
| | |
| | | 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 { |
| | |
| | | align-items: center; |
| | | } |
| | | |
| | | .merchant-hour-row .el-input { |
| | | .merchant-hour-row .el-date-editor { |
| | | width: 210px; |
| | | } |
| | | |
| | |
| | | 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; |
| | |
| | | margin-right: 8px; |
| | | } |
| | | |
| | | .product-right-row .right-limit-field .el-input-number { |
| | | width: 100%; |
| | | } |
| | | |
| | | .product-right-row .right-unit { |
| | | flex: 0 0 22px; |
| | | } |
| | |
| | | } |
| | | |
| | | .product-right-row .right-reset-select { |
| | | flex: 0 0 64px; |
| | | flex: 0 0 96px; |
| | | margin-right: 12px; |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | .merchant-product-form-panel .product-right-sub .el-input { |
| | | width: 100px; |
| | | } |
| | | |
| | | .merchant-product-form-panel .product-right-sub .right-exceed-discount-input { |
| | | width: 100px; |
| | | } |
| | | |
| | |
| | | .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, |
| | |
| | | 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') { |
| | |
| | | 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 }, |
| | |
| | | |
| | | interface BusinessHour { |
| | | id: number |
| | | value: string |
| | | value: string[] |
| | | } |
| | | |
| | | interface MerchantRightConfig { |
| | |
| | | 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 = [ |
| | |
| | | usageInstructions: '', |
| | | rightsText: '', |
| | | serviceLabel: '', |
| | | recommend: 0, |
| | | sortNo: 0, |
| | | recommend: defaultMerchantRecommend(), |
| | | sortNo: undefined, |
| | | status: 1, |
| | | }) |
| | | |
| | |
| | | availableTime: '', |
| | | otherRule: '', |
| | | }) |
| | | const productValidPeriodRange = ref<string[]>([]) |
| | | const productAvailableTimeRange = ref<string[]>([]) |
| | | |
| | | const productRights = reactive([ |
| | | { |
| | |
| | | exceedDiscount: '', |
| | | resetCycle: '每天', |
| | | subsidy: true, |
| | | exceedSubsidy: true, |
| | | }, |
| | | { |
| | | level: '杰出人才卡', |
| | |
| | | exceedDiscount: '', |
| | | resetCycle: '每天', |
| | | subsidy: true, |
| | | exceedSubsidy: true, |
| | | }, |
| | | { |
| | | level: '菁英人才卡', |
| | |
| | | exceedDiscount: '', |
| | | resetCycle: '每天', |
| | | subsidy: true, |
| | | exceedSubsidy: true, |
| | | }, |
| | | ]) |
| | | |
| | |
| | | exceedDiscount: '', |
| | | resetCycle: '每天', |
| | | subsidy: true, |
| | | exceedSubsidy: true, |
| | | }, |
| | | { |
| | | level: '杰出人才卡', |
| | |
| | | exceedDiscount: '', |
| | | resetCycle: '每天', |
| | | subsidy: true, |
| | | exceedSubsidy: true, |
| | | }, |
| | | { |
| | | level: '菁英人才卡', |
| | |
| | | exceedDiscount: '', |
| | | resetCycle: '每天', |
| | | subsidy: true, |
| | | exceedSubsidy: true, |
| | | }, |
| | | ] |
| | | } |
| | |
| | | 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 [ |
| | |
| | | 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() { |
| | |
| | | } |
| | | } |
| | | |
| | | 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) => { |
| | |
| | | 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) |
| | | } |
| | | |
| | |
| | | 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('请输入商品名称') |
| | |
| | | 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 = { |
| | |
| | | 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 })), |
| | | } |
| | |
| | | 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() |
| | |
| | | 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 |
| | |
| | | |
| | | 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')) { |
| | |
| | | <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> |
| | |
| | | </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" /> |
| | |
| | | </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> |
| | |
| | | <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> |
| | |
| | | <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> |
| | |
| | | <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 ? '登录账号已存在,请更换' : ''"> |
| | |
| | | <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> |
| | |
| | | </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> |
| | |
| | | <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> |
| | |
| | | </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> |
| | |
| | | <label>创建时间</label> |
| | | <el-date-picker |
| | | v-model="couponFilters.createdTime" |
| | | class="coupon-created-time-picker" |
| | | type="daterange" |
| | | start-placeholder="开始日期" |
| | | end-placeholder="结束日期" |
| | |
| | | <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 } |
| | |
| | | } |
| | | 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> |
| | |
| | | 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 '已退款' |
| | |
| | | { 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} 条订单`) |
| | |
| | | <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> |
| | |
| | | <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> |
| | |
| | | return Number(amount).toFixed(2).replace(/\.00$/, '') |
| | | } |
| | | |
| | | function formatDateTime(value?: string) { |
| | | return value ? value.replace('T', ' ') : '' |
| | | } |
| | | |
| | | async function loadData() { |
| | | loading.value = true |
| | | try { |
| | |
| | | <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> |
| | |
| | | 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 { |
| | |
| | | { 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} 条退款记录`) |
| | | } |
| | |
| | | <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 }"> |
| | |
| | | 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; |
| | |
| | | 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(), |
| | |
| | | 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, "商户登录账号只能包含英文字母和数字"); |
| | |
| | | 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 |
| | |
| | | @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, |
| | |
| | | @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 = ?, |
| | |
| | | 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; |
| | | } |
| | |
| | | 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; |
| | |
| | | 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, |