From 69138ddcf4936098cb3b67688412badfc8f503a0 Mon Sep 17 00:00:00 2001
From: shinnosuke <tantao@1hoper.com>
Date: 星期二, 18 八月 2026 17:13:53 +0800
Subject: [PATCH] 又又又更新
---
talent-card-admin/src/views/OrderView.vue | 4
talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementController.java | 1
talent-card-server/src/main/java/com/talentcard/server/business/order/OrderService.java | 71 ++-
talent-card-admin/src/views/BusinessManagementView.vue | 10
talent-card-admin/src/views/TalentNewsPublishView.vue | 18
rencaika/static/card-assets/card-outstanding.jpg | 0
rencaika/static/benefits-assets/food.png | 0
talent-card-admin/src/router/index.ts | 2
rencaika_merchant/services/orders.uts | 61 +-
talent-card-admin/src/views/TalentPolicyPublishView.vue | 10
rencaika/static/home-assets/card.png | 0
rencaika/static/home-assets/login-mascot.png | 0
rencaika/static/policy-assets/policy-explanation.jpg | 0
talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessController.java | 5
talent-card-admin/src/views/RefundRecordView.vue | 2
talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantCreateRequest.java | 2
talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantSettlementAdminService.java | 11
talent-card-server/src/main/resources/application.yml | 6
talent-card-server/src/main/resources/db/migration/V113__ticket_package_level_rules.sql | 0
rencaika/static/entertainment-assets/entertainment-hero.webp | 0
rencaika/static/home-assets/top.jpg | 0
talent-card-admin/src/api/admin.ts | 9
talent-card-admin/src/views/TalentNewsEditView.vue | 18
rencaika/static/benefits-assets/premium.png | 0
rencaika/static/card-assets/talent-card-background.jpg | 0
talent-card-admin/src/style.css | 293 ++++++++++---
talent-card-admin/src/layouts/AdminLayout.vue | 107 +++--
talent-card-admin/src/views/TalentDetailView.vue | 2
rencaika_merchant/pages/verification-confirm/verification-confirm.uvue | 13
talent-card-admin/src/views/StaffManagementView.vue | 48 +
talent-card-admin/src/views/TalentCreateView.vue | 22
rencaika/static/card-assets/card-elite.jpg | 0
rencaika/static/product-assets/payment-success.png | 0
rencaika/static/card-assets/card-excellence.jpg | 0
rencaika/static/entertainment-assets/wanda-cinema.webp | 0
talent-card-admin/src/views/ProductAuditDetailView.vue | 6
talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementService.java | 7
talent-card-admin/src/views/MerchantView.vue | 6
rencaika/static/benefits-assets/shopping.png | 0
rencaika/static/stay-assets/hotel-hampton.jpg | 0
talent-card-admin/src/views/RoleFormView.vue | 80 +++
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkService.java | 4
rencaika/manifest.json | 5
rencaika/static/benefits-assets/benefits-hero.jpg | 0
talent-card-server/src/main/resources/db/migration/V114__sync_premium_topic_merchant_mappings.sql | 0
rencaika/static/benefits-assets/travel.png | 0
talent-card-admin/src/views/OrderDetailView.vue | 5
rencaika/static/benefits-assets/all.png | 0
rencaika/static/benefits-assets/culture.png | 0
talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java | 6
talent-card-server/src/main/java/com/talentcard/server/business/portal/AppOrderPortalService.java | 5
talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java | 100 ++++
rencaika/static/policy-assets/policy-hero.jpg | 0
talent-card-server/src/main/resources/db/migration/V112__news_article_top_time_sorting.sql | 0
talent-card-server/src/main/resources/db/migration/V115__benefit_order_card_level_snapshot.sql | 9
rencaika/static/stay-assets/hotel-vienna.jpg | 0
talent-card-admin/src/views/UpgradeApplicationDetailView.vue | 8
rencaika/static/benefits-assets/life.png | 0
/dev/null | 1
talent-card-admin/src/views/MerchantCreateView.vue | 152 +++++--
talent-card-admin/src/views/MerchantOnboardingDetailView.vue | 4
rencaika/static/food-assets/food-2.jpg | 0
talent-card-admin/src/views/TalentPolicyEditView.vue | 2
rencaika/static/food-assets/food-1.jpg | 0
rencaika/static/news-assets/meeting-thumb.jpg | 0
rencaika/static/benefits-assets/stay.png | 0
talent-card-admin/src/views/MerchantSettlementView.vue | 28 +
67 files changed, 786 insertions(+), 357 deletions(-)
diff --git a/rencaika/manifest.json b/rencaika/manifest.json
index 4ea8f20..959a7c4 100644
--- a/rencaika/manifest.json
+++ b/rencaika/manifest.json
@@ -11,7 +11,8 @@
"mp-weixin": {
"appid": "wxcfee477acb54d179",
"setting": {
- "urlCheck": false
+ "urlCheck": false,
+ "sourceMap": false
},
"usingComponents": true
},
@@ -66,4 +67,4 @@
"mode": ""
}
}
-}
\ No newline at end of file
+}
diff --git a/rencaika/static/benefits-assets/all.png b/rencaika/static/benefits-assets/all.png
index 4f8af36..5076619 100644
--- a/rencaika/static/benefits-assets/all.png
+++ b/rencaika/static/benefits-assets/all.png
Binary files differ
diff --git a/rencaika/static/benefits-assets/benefits-hero.jpg b/rencaika/static/benefits-assets/benefits-hero.jpg
index 3c8a191..2d3fc66 100644
--- a/rencaika/static/benefits-assets/benefits-hero.jpg
+++ b/rencaika/static/benefits-assets/benefits-hero.jpg
Binary files differ
diff --git a/rencaika/static/benefits-assets/culture.png b/rencaika/static/benefits-assets/culture.png
index 8758e84..2e0bf3f 100644
--- a/rencaika/static/benefits-assets/culture.png
+++ b/rencaika/static/benefits-assets/culture.png
Binary files differ
diff --git a/rencaika/static/benefits-assets/food.png b/rencaika/static/benefits-assets/food.png
index 68238e0..9af31a6 100644
--- a/rencaika/static/benefits-assets/food.png
+++ b/rencaika/static/benefits-assets/food.png
Binary files differ
diff --git a/rencaika/static/benefits-assets/life.png b/rencaika/static/benefits-assets/life.png
index a0efa51..2a7057b 100644
--- a/rencaika/static/benefits-assets/life.png
+++ b/rencaika/static/benefits-assets/life.png
Binary files differ
diff --git a/rencaika/static/benefits-assets/premium.png b/rencaika/static/benefits-assets/premium.png
index 357711d..6a48d3e 100644
--- a/rencaika/static/benefits-assets/premium.png
+++ b/rencaika/static/benefits-assets/premium.png
Binary files differ
diff --git a/rencaika/static/benefits-assets/shopping.png b/rencaika/static/benefits-assets/shopping.png
index 7e6db88..dad8b5f 100644
--- a/rencaika/static/benefits-assets/shopping.png
+++ b/rencaika/static/benefits-assets/shopping.png
Binary files differ
diff --git a/rencaika/static/benefits-assets/stay.png b/rencaika/static/benefits-assets/stay.png
index 78a47f7..8563d3a 100644
--- a/rencaika/static/benefits-assets/stay.png
+++ b/rencaika/static/benefits-assets/stay.png
Binary files differ
diff --git a/rencaika/static/benefits-assets/travel.png b/rencaika/static/benefits-assets/travel.png
index f55a8db..4fefdb2 100644
--- a/rencaika/static/benefits-assets/travel.png
+++ b/rencaika/static/benefits-assets/travel.png
Binary files differ
diff --git a/rencaika/static/card-assets/card-elite.jpg b/rencaika/static/card-assets/card-elite.jpg
index f37ba03..c77a35d 100644
--- a/rencaika/static/card-assets/card-elite.jpg
+++ b/rencaika/static/card-assets/card-elite.jpg
Binary files differ
diff --git a/rencaika/static/card-assets/card-excellence.jpg b/rencaika/static/card-assets/card-excellence.jpg
index fc85b31..d93a048 100644
--- a/rencaika/static/card-assets/card-excellence.jpg
+++ b/rencaika/static/card-assets/card-excellence.jpg
Binary files differ
diff --git a/rencaika/static/card-assets/card-outstanding.jpg b/rencaika/static/card-assets/card-outstanding.jpg
index b0a49fd..76b5a80 100644
--- a/rencaika/static/card-assets/card-outstanding.jpg
+++ b/rencaika/static/card-assets/card-outstanding.jpg
Binary files differ
diff --git a/rencaika/static/card-assets/talent-card-background.jpg b/rencaika/static/card-assets/talent-card-background.jpg
index 87b32a1..02026b6 100644
--- a/rencaika/static/card-assets/talent-card-background.jpg
+++ b/rencaika/static/card-assets/talent-card-background.jpg
Binary files differ
diff --git a/rencaika/static/entertainment-assets/entertainment-hero.webp b/rencaika/static/entertainment-assets/entertainment-hero.webp
index bccf187..00163de 100644
--- a/rencaika/static/entertainment-assets/entertainment-hero.webp
+++ b/rencaika/static/entertainment-assets/entertainment-hero.webp
Binary files differ
diff --git a/rencaika/static/entertainment-assets/wanda-cinema.webp b/rencaika/static/entertainment-assets/wanda-cinema.webp
index 5eeddcb..d7fc0b9 100644
--- a/rencaika/static/entertainment-assets/wanda-cinema.webp
+++ b/rencaika/static/entertainment-assets/wanda-cinema.webp
Binary files differ
diff --git a/rencaika/static/fonts/stay-calligraphy-subset.ttf b/rencaika/static/fonts/stay-calligraphy-subset.ttf
deleted file mode 100644
index dc4cf1e..0000000
--- a/rencaika/static/fonts/stay-calligraphy-subset.ttf
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/food-assets/food-1.jpg b/rencaika/static/food-assets/food-1.jpg
index 8b58c80..fd5c44b 100644
--- a/rencaika/static/food-assets/food-1.jpg
+++ b/rencaika/static/food-assets/food-1.jpg
Binary files differ
diff --git a/rencaika/static/food-assets/food-2.jpg b/rencaika/static/food-assets/food-2.jpg
index 827e0ac..77ff201 100644
--- a/rencaika/static/food-assets/food-2.jpg
+++ b/rencaika/static/food-assets/food-2.jpg
Binary files differ
diff --git a/rencaika/static/home-assets/card.png b/rencaika/static/home-assets/card.png
index adffef8..8b4da56 100644
--- a/rencaika/static/home-assets/card.png
+++ b/rencaika/static/home-assets/card.png
Binary files differ
diff --git a/rencaika/static/home-assets/login-mascot.png b/rencaika/static/home-assets/login-mascot.png
index ffbf1b1..9989581 100644
--- a/rencaika/static/home-assets/login-mascot.png
+++ b/rencaika/static/home-assets/login-mascot.png
Binary files differ
diff --git a/rencaika/static/home-assets/play.svg b/rencaika/static/home-assets/play.svg
deleted file mode 100644
index d2c36be..0000000
--- a/rencaika/static/home-assets/play.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="19" height="13.600006103515625" viewBox="0 0 19 13.600006103515625"><rect x="0.5" y="0.5" width="18" height="12.59999942779541" rx="4" fill-opacity="0" stroke-opacity="1" stroke="#FFFFFF" fill="none" stroke-width="1"/><path d="M8.9472136,4.22360682L12.9111452,6.2055726C13.6481938,6.5740969,13.6481938,7.6259031,12.9111452,7.9944272L8.9472135,9.9763932C8.28231257,10.3088431,7.5,9.8253474,7.5,9.0819654L7.5,5.1180341C7.5,4.37465209,8.28231257,3.89115629,8.9472136,4.22360682" fill="#FFFFFF" fill-opacity="1"/></svg>
\ No newline at end of file
diff --git a/rencaika/static/home-assets/top.jpg b/rencaika/static/home-assets/top.jpg
index bf8bbeb..bee2ee6 100644
--- a/rencaika/static/home-assets/top.jpg
+++ b/rencaika/static/home-assets/top.jpg
Binary files differ
diff --git a/rencaika/static/news-assets/meeting-thumb.jpg b/rencaika/static/news-assets/meeting-thumb.jpg
index 2612195..47ba123 100644
--- a/rencaika/static/news-assets/meeting-thumb.jpg
+++ b/rencaika/static/news-assets/meeting-thumb.jpg
Binary files differ
diff --git a/rencaika/static/policy-assets/policy-explanation.jpg b/rencaika/static/policy-assets/policy-explanation.jpg
index c4522a1..93d9b78 100644
--- a/rencaika/static/policy-assets/policy-explanation.jpg
+++ b/rencaika/static/policy-assets/policy-explanation.jpg
Binary files differ
diff --git a/rencaika/static/policy-assets/policy-hero.jpg b/rencaika/static/policy-assets/policy-hero.jpg
index df27645..7dce5ed 100644
--- a/rencaika/static/policy-assets/policy-hero.jpg
+++ b/rencaika/static/policy-assets/policy-hero.jpg
Binary files differ
diff --git a/rencaika/static/product-assets/payment-success.png b/rencaika/static/product-assets/payment-success.png
index ea9c086..e3051ec 100644
--- a/rencaika/static/product-assets/payment-success.png
+++ b/rencaika/static/product-assets/payment-success.png
Binary files differ
diff --git a/rencaika/static/stay-assets/hotel-hampton.jpg b/rencaika/static/stay-assets/hotel-hampton.jpg
index 1cffd30..b08c8dc 100644
--- a/rencaika/static/stay-assets/hotel-hampton.jpg
+++ b/rencaika/static/stay-assets/hotel-hampton.jpg
Binary files differ
diff --git a/rencaika/static/stay-assets/hotel-vienna.jpg b/rencaika/static/stay-assets/hotel-vienna.jpg
index 2d0f8de..1df04c6 100644
--- a/rencaika/static/stay-assets/hotel-vienna.jpg
+++ b/rencaika/static/stay-assets/hotel-vienna.jpg
Binary files differ
diff --git a/rencaika_merchant/pages/verification-confirm/verification-confirm.uvue b/rencaika_merchant/pages/verification-confirm/verification-confirm.uvue
index 0f16395..7f357da 100644
--- a/rencaika_merchant/pages/verification-confirm/verification-confirm.uvue
+++ b/rencaika_merchant/pages/verification-confirm/verification-confirm.uvue
@@ -48,6 +48,7 @@
const payAmount = ref(0)
const subsidyAmount = ref(0)
const createdAt = ref('')
+ const loadError = ref('')
const submitting = ref(false)
const formatAmount = (amount: number): string => (amount / 100).toFixed(2)
@@ -72,11 +73,21 @@
const loadOrder = () => {
if (orderNo.value.length == 0) return
- loadMerchantOrderDetail(orderNo.value, (order: MerchantOrderDetail, _traceId: string) => applyOrder(order), () => {})
+ loadMerchantOrderDetail(orderNo.value, (order: MerchantOrderDetail, _traceId: string) => {
+ loadError.value = ''
+ applyOrder(order)
+ }, (error: any) => {
+ loadError.value = error != null && error.message != null ? '' + error.message : '订单详情加载失败,请返回重新扫码'
+ uni.showToast({ title: loadError.value, icon: 'none' })
+ })
}
const confirm = () => {
if (submitting.value || verificationId.value.length == 0) return
+ if (loadError.value.length > 0) {
+ uni.showToast({ title: '订单详情未加载完成,请返回重新扫码', icon: 'none' })
+ return
+ }
uni.showModal({
title: '确认核销',
content: '确认后将完成本次订单核销,是否继续?',
diff --git a/rencaika_merchant/services/orders.uts b/rencaika_merchant/services/orders.uts
index ae27e74..3cb207a 100644
--- a/rencaika_merchant/services/orders.uts
+++ b/rencaika_merchant/services/orders.uts
@@ -88,26 +88,41 @@
}, onError)
}
-const numberValue = (value: any): number => value == null ? 0 : Number(value)
+const fieldValue = (value: any, camelCaseName: string, snakeCaseName: string): any => {
+ if (value == null) return null
+ if (value[camelCaseName] != null) return value[camelCaseName]
+ return value[snakeCaseName] != null ? value[snakeCaseName] : null
+}
+
+const numberValue = (value: any): number => {
+ if (value == null || value === '') return 0
+ const number = Number(value)
+ return isNaN(number) ? 0 : number
+}
+
+const textValue = (value: any, camelCaseName: string, snakeCaseName: string, fallback: string = ''): string => {
+ const field = fieldValue(value, camelCaseName, snakeCaseName)
+ return field != null ? '' + field : fallback
+}
const toOrder = (value: any): MerchantOrder => ({
- id: value != null && value.id != null ? '' + value.id : '',
- orderNo: value != null && value.orderNo != null ? '' + value.orderNo : '',
- category: value != null && value.category != null ? '' + value.category : 'group',
- status: value != null && value.status != null ? '' + value.status : 'pending_verification',
- statusText: value != null && value.statusText != null ? '' + value.statusText : '待核销',
- title: value != null && value.title != null ? '' + value.title : '订单',
- customerName: value != null && value.customerName != null ? '' + value.customerName : '',
- customerMobile: value != null && value.customerMobile != null ? '' + value.customerMobile : '',
- cardLevel: value != null && value.cardLevel != null ? '' + value.cardLevel : '卓越人才卡',
- orderAmount: numberValue(value != null ? value.orderAmount : 0),
- basePayAmount: numberValue(value != null ? value.basePayAmount : 0),
- couponAmount: numberValue(value != null ? value.couponAmount : 0),
- talentDiscountAmount: numberValue(value != null ? value.talentDiscountAmount : 0),
- payAmount: numberValue(value != null ? value.payAmount : 0),
- subsidyAmount: numberValue(value != null ? value.subsidyAmount : 0),
- createdAt: value != null && value.createdAt != null ? '' + value.createdAt : '',
- verifyTime: value != null && value.verifyTime != null ? '' + value.verifyTime : ''
+ id: textValue(value, 'id', 'id'),
+ orderNo: textValue(value, 'orderNo', 'order_no'),
+ category: textValue(value, 'category', 'category', 'group'),
+ status: textValue(value, 'status', 'status', 'pending_verification'),
+ statusText: textValue(value, 'statusText', 'status_text', '待核销'),
+ title: textValue(value, 'title', 'title', '订单'),
+ customerName: textValue(value, 'customerName', 'customer_name'),
+ customerMobile: textValue(value, 'customerMobile', 'customer_mobile'),
+ cardLevel: textValue(value, 'cardLevel', 'card_level', '卓越人才卡'),
+ orderAmount: numberValue(fieldValue(value, 'orderAmount', 'order_amount')),
+ basePayAmount: numberValue(fieldValue(value, 'basePayAmount', 'base_pay_amount')),
+ couponAmount: numberValue(fieldValue(value, 'couponAmount', 'coupon_amount')),
+ talentDiscountAmount: numberValue(fieldValue(value, 'talentDiscountAmount', 'talent_discount_amount')),
+ payAmount: numberValue(fieldValue(value, 'payAmount', 'pay_amount')),
+ subsidyAmount: numberValue(fieldValue(value, 'subsidyAmount', 'subsidy_amount')),
+ createdAt: textValue(value, 'createdAt', 'created_at'),
+ verifyTime: textValue(value, 'verifyTime', 'verify_time')
})
const toOrderDetail = (value: any): MerchantOrderDetail => {
@@ -117,11 +132,11 @@
basePayAmount: order.basePayAmount != null ? order.basePayAmount : order.orderAmount,
couponAmount: order.couponAmount != null ? order.couponAmount : 0,
talentDiscountAmount: order.talentDiscountAmount != null ? order.talentDiscountAmount : 0,
- benefitName: value != null && value.benefitName != null ? '' + value.benefitName : order.title,
- merchantName: value != null && value.merchantName != null ? '' + value.merchantName : '',
- canRevoke: value != null && value.canRevoke == true,
- revokeDeadline: value != null && value.revokeDeadline != null ? '' + value.revokeDeadline : '',
- revokeReason: value != null && value.revokeReason != null ? '' + value.revokeReason : ''
+ benefitName: textValue(value, 'benefitName', 'benefit_name', order.title),
+ merchantName: textValue(value, 'merchantName', 'merchant_name'),
+ canRevoke: fieldValue(value, 'canRevoke', 'can_revoke') == true,
+ revokeDeadline: textValue(value, 'revokeDeadline', 'revoke_deadline'),
+ revokeReason: textValue(value, 'revokeReason', 'revoke_reason')
}
}
diff --git a/talent-card-admin/src/api/admin.ts b/talent-card-admin/src/api/admin.ts
index 0208fdb..075709f 100644
--- a/talent-card-admin/src/api/admin.ts
+++ b/talent-card-admin/src/api/admin.ts
@@ -73,6 +73,14 @@
}
export function getPlatformSettings() { return http.get<Record<string, string>, Record<string, string>>('/api/admin/management/settings') }
+export interface AdminPendingNotice {
+ id: string
+ category: string
+ content: string
+ time: string
+ path: string
+}
+export function getAdminPendingNotices() { return http.get<AdminPendingNotice[], AdminPendingNotice[]>('/api/admin/business/pending-notices') }
export function savePlatformSettings(payload: Record<string, string>) { return http.put<Record<string, string>, Record<string, string>>('/api/admin/management/settings', payload) }
export function getProtocolSetting(key: string) { return http.get<Record<string, any>, Record<string, any>>(`/api/admin/management/protocols/${key}`) }
export function saveProtocolSetting(key: string, contentHtml: string) { return http.put<Record<string, any>, Record<string, any>>(`/api/admin/management/protocols/${key}`, { contentHtml }) }
@@ -82,6 +90,7 @@
export function updateAdminRoleStatus(id: number | string, status: number) { return http.patch<AdminRole, AdminRole>(`/api/admin/management/roles/${id}/status`, { status }) }
export function deleteAdminRole(id: number | string) { return http.delete<any, any>(`/api/admin/management/roles/${id}`) }
export function getAdminStaff() { return http.get<AdminStaff[], AdminStaff[]>('/api/admin/management/staff') }
+export function checkAdminStaffAccount(account: string) { return http.get<{ available: boolean }, { available: boolean }>('/api/admin/management/staff/account-available', { params: { account } }) }
export function createAdminStaff(payload: Omit<AdminStaff, 'id' | 'createdAt'> & { password: string }) { return http.post<AdminStaff, AdminStaff>('/api/admin/management/staff', payload) }
export function updateAdminStaff(id: number | string, payload: Omit<AdminStaff, 'id' | 'createdAt'> & { password?: string }) { return http.put<AdminStaff, AdminStaff>(`/api/admin/management/staff/${id}`, payload) }
export function resetAdminStaffPassword(id: number | string, password: string) { return http.patch<any, any>(`/api/admin/management/staff/${id}/password`, { password }) }
diff --git a/talent-card-admin/src/layouts/AdminLayout.vue b/talent-card-admin/src/layouts/AdminLayout.vue
index f711298..8b0a299 100644
--- a/talent-card-admin/src/layouts/AdminLayout.vue
+++ b/talent-card-admin/src/layouts/AdminLayout.vue
@@ -1,5 +1,5 @@
<script setup lang="ts">
-import { computed, ref } from 'vue'
+import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessageBox } from 'element-plus'
import {
@@ -15,6 +15,8 @@
Wallet,
} from '@element-plus/icons-vue'
import { logoutAdmin } from '../api/auth'
+import { getAdminPendingNotices } from '../api/admin'
+import type { AdminPendingNotice } from '../api/admin'
import { useAppStore } from '../stores/app'
interface MenuItem {
@@ -36,38 +38,23 @@
const appStore = useAppStore()
const avatarText = computed(() => (appStore.user.name || 'A').trim().slice(0, 1).toUpperCase())
-interface HeaderNotice {
- id: number
- content: string
- time: string
- read: boolean
+const notices = ref<AdminPendingNotice[]>([])
+const adminMenuRef = ref<{ open: (index: string) => void; close: (index: string) => void }>()
+
+function formatNoticeTime(value: string) {
+ return value ? value.replace('T', ' ').replace(/\.\d+$/, '') : '-'
}
-const notices = ref<HeaderNotice[]>([
- {
- id: 1,
- content: '上午好,欢迎登录攀枝花市东区人才服务平台。',
- time: '2026/6/25 09:50:54',
- read: false,
- },
- {
- id: 2,
- content: '您有一条新的酒店预约,请及时查看。',
- time: '2026/6/25 09:54:08',
- read: false,
- },
-])
-
-const unreadNoticeCount = computed(() => notices.value.filter((item) => !item.read).length)
-
-function markNoticeRead(notice: HeaderNotice) {
- notice.read = true
+async function loadNotices() {
+ try {
+ notices.value = await getAdminPendingNotices()
+ } catch {
+ notices.value = []
+ }
}
-function markAllNoticesRead() {
- notices.value.forEach((item) => {
- item.read = true
- })
+function openNotice(notice: AdminPendingNotice) {
+ router.push(notice.path)
}
const cardMenus: MenuItem[] = [
@@ -84,7 +71,7 @@
]
const financeMenus: MenuItem[] = [
- { path: '/merchant-settlement', label: '提现管理' },
+ { path: '/merchant-settlement', label: '商户结算审核' },
]
const orderMenus: MenuItem[] = [
@@ -131,11 +118,11 @@
// Keep the navigation in sync with the permissions returned at login. Newer
// routes without a dedicated permission code intentionally remain visible.
const routePermissions: Record<string, string> = {
- '/talents': 'admin.talents.view', '/family-card-applications': 'admin.family-cards.view', '/upgrade-applications': 'admin.talents.view', '/family-cards': 'admin.family-cards.view', '/child-cards': 'admin.family-cards.view',
- '/merchants': 'admin.merchants.view', '/products': 'admin.products.view', '/orders': 'admin.orders.view', '/refund-records': 'admin.refunds.view', '/hotel-free-verifications': 'admin.orders.view',
- '/medical-reservations': 'admin.medical-reservations.view', '/medical-applications': 'admin.medical-reservations.view', '/parking-plates': 'admin.parking-plates.view', '/fuel-services': 'admin.parking-plates.view', '/spouse-employment': 'admin.parking-plates.view', '/hotel-reservations': 'admin.medical-reservations.view',
- '/merchant-settlement': 'admin.merchants.view', '/equity-images': 'admin.equity-images.view', '/talent-news-management': 'admin.news.view', '/talent-policy-management': 'admin.news.view', '/movie-tickets': 'admin.news.view',
- '/system-settings': 'admin.settings.view', '/protocol-settings': 'admin.settings.view', '/roles': 'admin.roles.view', '/staff': 'admin.staff.view',
+ '/talents': 'admin.talents.view', '/family-card-applications': 'admin.family-card-applications.view', '/upgrade-applications': 'admin.upgrade-applications.view', '/family-cards': 'admin.family-cards.view', '/child-cards': 'admin.child-cards.view',
+ '/merchants': 'admin.merchants.view', '/products': 'admin.products.view', '/orders': 'admin.orders.view', '/refund-records': 'admin.refunds.view', '/hotel-free-verifications': 'admin.hotel-free-verifications.view',
+ '/appointment-management': 'admin.appointment-management.view', '/medical-reservations': 'admin.medical-reservations.view', '/medical-applications': 'admin.medical-applications.view', '/parking-plates': 'admin.parking-plates.view', '/car-wash-benefits': 'admin.car-wash-benefits.view', '/fuel-services': 'admin.fuel-services.view', '/spouse-employment': 'admin.spouse-employment.view', '/hotel-reservations': 'admin.hotel-reservations.view',
+ '/merchant-settlement': 'admin.merchant-settlement.view', '/equity-images': 'admin.equity-images.view', '/talent-news-management': 'admin.news.view', '/talent-policy-management': 'admin.policies.view', '/movie-tickets': 'admin.movie-tickets.view',
+ '/system-settings': 'admin.settings.view', '/protocol-settings': 'admin.protocol-settings.view', '/roles': 'admin.roles.view', '/staff': 'admin.staff.view',
}
const canAccess = (path?: string) => {
if (!path || appStore.user.role === '平台管理') return true
@@ -204,6 +191,32 @@
return route.path
})
+function currentMenuGroupIndex() {
+ return menuGroups.find((group) => findMenuLabel(group.items, activeMenu.value))?.index
+}
+
+function closeOtherMenuGroups(openedGroupIndex?: string) {
+ menuGroups.forEach((group) => {
+ if (group.index !== openedGroupIndex) adminMenuRef.value?.close(group.index)
+ })
+}
+
+function handleMenuOpen(_index: string, indexPath: string[]) {
+ // indexPath 的第一个值始终是一级模块;无论点击一级或嵌套菜单,均只保留该模块展开。
+ const openedGroupIndex = indexPath[0]
+ if (menuGroups.some((group) => group.index === openedGroupIndex)) {
+ closeOtherMenuGroups(openedGroupIndex)
+ }
+}
+
+watch(activeMenu, () => {
+ const groupIndex = currentMenuGroupIndex()
+ nextTick(() => {
+ closeOtherMenuGroups(groupIndex)
+ if (groupIndex) adminMenuRef.value?.open(groupIndex)
+ })
+}, { immediate: true })
+
function findMenuLabel(items: MenuItem[], path: string): string | undefined {
for (const item of items) {
if (item.path === path) return item.label
@@ -257,6 +270,8 @@
router.push('/login')
}
}
+
+onMounted(loadNotices)
</script>
<template>
@@ -272,31 +287,27 @@
</div>
<div class="header-right">
- <el-popover placement="bottom-end" :width="330" trigger="click" popper-class="notice-popover">
+ <el-popover placement="bottom-end" :width="330" trigger="click" popper-class="notice-popover" @show="loadNotices">
<template #reference>
- <el-badge :is-dot="unreadNoticeCount > 0" class="notice-badge">
+ <el-badge :is-dot="notices.length > 0" class="notice-badge">
<el-button :icon="Bell" circle aria-label="消息通知" />
</el-badge>
</template>
- <div class="notice-panel">
- <div class="notice-panel-header">
- <strong>通知公告</strong>
- <button type="button" :disabled="unreadNoticeCount === 0" @click="markAllNoticesRead">
- 全部已读
- </button>
- </div>
+ <div class="notice-panel">
+ <div class="notice-panel-header">
+ <strong>待办通知</strong>
+ </div>
<div v-if="notices.length" class="notice-list">
<button
v-for="notice in notices"
:key="notice.id"
type="button"
class="notice-item"
- @click="markNoticeRead(notice)"
+ @click="openNotice(notice)"
>
<span class="notice-content">{{ notice.content }}</span>
- <span v-if="!notice.read" class="notice-unread">未读</span>
- <time>{{ notice.time }}</time>
+ <time>{{ formatNoticeTime(notice.time) }}</time>
</button>
</div>
<div v-else class="notice-empty">暂无通知</div>
@@ -312,10 +323,12 @@
<el-container class="admin-body">
<el-aside width="220px" class="admin-aside">
<el-menu
+ ref="adminMenuRef"
router
:default-active="activeMenu"
:default-openeds="defaultOpeneds"
unique-opened
+ @open="handleMenuOpen"
class="admin-menu"
>
<el-menu-item index="/dashboard">
diff --git a/talent-card-admin/src/router/index.ts b/talent-card-admin/src/router/index.ts
index 79bbe5d..d17b165 100644
--- a/talent-card-admin/src/router/index.ts
+++ b/talent-card-admin/src/router/index.ts
@@ -110,7 +110,7 @@
path: 'merchant-settlement',
name: 'merchant-settlement',
component: () => import('../views/MerchantSettlementView.vue'),
- meta: { title: '提现管理' },
+ meta: { title: '商户结算审核' },
},
{
path: 'merchant-onboardings/:id',
diff --git a/talent-card-admin/src/style.css b/talent-card-admin/src/style.css
index 613c329..aa3024f 100644
--- a/talent-card-admin/src/style.css
+++ b/talent-card-admin/src/style.css
@@ -963,7 +963,7 @@
.merchant-rights-detail-row {
display: grid;
grid-template-columns: 96px minmax(360px, 520px) 270px;
- gap: 14px 24px;
+ gap: 6px 24px;
align-items: start;
padding: 14px 0 0;
}
@@ -976,8 +976,10 @@
.merchant-rights-detail-row label {
grid-column: 1 / 3;
+ margin: 0;
color: #334155;
font-size: 13px;
+ line-height: 20px;
}
.merchant-rights-level-row + .merchant-rights-detail-row {
@@ -1032,6 +1034,11 @@
align-items: start;
color: #111827;
font-size: 13px;
+}
+
+/* 商家权益仅保留后台配置表单,不展示小程序示例预览。 */
+.merchant-rights-panel .merchant-rights-example {
+ display: none !important;
}
.merchant-rights-phone-preview {
@@ -1282,7 +1289,7 @@
.consume-desc-row {
padding-left: 186px;
- padding-top: 14px;
+ padding-top: 10px;
}
.merchant-products-panel,
@@ -1339,8 +1346,14 @@
.merchant-coupons-filter .el-select__wrapper {
min-height: 28px;
border-radius: 0;
- background: #f7f8fb;
- box-shadow: none;
+ background: #ffffff;
+ box-shadow: 0 0 0 1px #d9dee8 inset;
+}
+
+.merchant-products-filter .el-date-editor.el-input__wrapper,
+.merchant-coupons-filter .el-date-editor.el-input__wrapper {
+ background: #ffffff;
+ box-shadow: 0 0 0 1px #d9dee8 inset;
}
.merchant-products-filter .el-date-editor.el-input__wrapper,
@@ -1516,7 +1529,7 @@
}
.merchant-coupons-filter .coupon-created-time-picker {
- width: 320px;
+ width: 320px !important;
}
.merchant-products-filter .product-created-time-picker {
@@ -1544,12 +1557,12 @@
.product-form-grid {
display: flex;
- gap: 26px;
+ gap: 34px;
align-items: start;
}
.product-info-left {
- width: 360px;
+ width: 430px;
flex-shrink: 0;
}
@@ -1573,14 +1586,29 @@
.product-form-row,
.product-static-row {
display: grid;
- grid-template-columns: 54px minmax(0, 1fr);
+ grid-template-columns: 68px minmax(0, 1fr);
align-items: center;
- gap: 10px;
+ gap: 16px;
margin-bottom: 18px;
}
.product-form-row.amount {
- grid-template-columns: 54px 134px 24px;
+ grid-template-columns: 68px minmax(0, 1fr);
+ gap: 16px;
+}
+
+.product-form-row.amount .el-form-item__content {
+ display: flex;
+ flex-wrap: nowrap;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+ margin-left: 0 !important;
+}
+
+.product-form-row.amount .el-form-item__content > span {
+ flex: 0 0 auto;
+ white-space: nowrap;
}
.product-form-row.amount.hotel-amount {
@@ -1608,6 +1636,11 @@
white-space: nowrap;
}
+.merchant-product-edit-form .el-form-item__label {
+ width: 68px !important;
+ white-space: nowrap;
+}
+
.product-static-row span {
color: #475569;
font-weight: 600;
@@ -1615,15 +1648,16 @@
.product-image-row {
display: grid;
- grid-template-columns: 54px 256px 78px;
- gap: 10px;
- align-items: end;
+ grid-template-columns: 68px 256px 78px;
+ column-gap: 16px;
+ align-items: center;
margin-bottom: 20px;
}
.product-image-row.compact {
- grid-template-columns: 54px minmax(172px, 1fr) 78px;
- align-items: start;
+ grid-template-columns: 68px minmax(172px, 1fr) 78px;
+ column-gap: 16px;
+ align-items: center;
}
.product-image-list {
@@ -1634,7 +1668,21 @@
}
.product-image-row > label {
- align-self: start;
+ align-self: center;
+}
+
+.product-image-row > .el-button {
+ align-self: center;
+ justify-self: start;
+}
+
+.product-image-row .el-form-item__content {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 12px;
+ min-width: 0;
+ margin-left: 0 !important;
}
.product-cover-preview,
@@ -1707,13 +1755,13 @@
.product-detail-row {
display: grid;
- grid-template-columns: 54px minmax(0, 1fr);
- gap: 10px;
+ grid-template-columns: 68px minmax(0, 1fr);
+ gap: 16px;
margin-top: 4px;
}
.product-detail-row label {
- align-self: start;
+ align-self: center;
}
.product-detail-row .el-textarea {
@@ -1732,6 +1780,12 @@
gap: 8px;
}
+.product-notice-field .el-date-editor,
+.product-notice-field .el-time-editor,
+.product-notice-field .el-input {
+ width: 100%;
+}
+
.product-rights-list {
display: grid;
gap: 28px;
@@ -1744,57 +1798,79 @@
}
.product-right-main {
- display: flex;
+ /*
+ * 三种权益状态使用同一套列:避免免费、超过次数、打折三行因为
+ * 内容宽度或 margin 不同而把“是否政府补贴”挤到不同的位置。
+ */
+ display: grid;
+ grid-template-columns: 86px 42px 58px 64px 42px 40px 88px 8px 22px 24px 58px 100px 12px 88px 42px;
align-items: center;
min-height: 30px;
}
.product-right-sub {
- display: flex;
+ display: grid;
+ /* 与主行共用列定义,输入框和补贴开关逐列对齐。 */
+ grid-template-columns: 86px 42px 58px 64px 42px 40px 88px 8px 22px 24px 58px 100px 12px 88px 42px;
align-items: center;
min-height: 28px;
- padding-left: 186px;
}
-.product-right-sub > span:first-child {
- width: 64px;
- margin-right: 18px;
+.product-right-main .right-level { grid-column: 1; }
+.product-right-main .right-enabled { grid-column: 2; margin: 0; }
+.product-right-main .right-free-label { grid-column: 4; }
+.product-right-main .right-free-switch { grid-column: 5; margin: 0; }
+.product-right-main.free .right-limit-field { grid-column: 7; margin: 0; }
+.product-right-main.free .right-unit { grid-column: 9; margin: 0; }
+.product-right-main.free .right-reset-label { grid-column: 11; margin: 0; }
+.product-right-main.free .right-reset-select { grid-column: 12; margin: 0; }
+/* 打折行使用一个独立的紧凑控件组,补贴列仍固定在第 14 列。 */
+.product-right-main:not(.free) .right-discount-control {
+ grid-column: 7 / span 6;
+ display: flex;
+ align-items: center;
+ gap: 8px;
}
-.product-right-sub > span:nth-child(3) {
- width: 22px;
- margin-left: 8px;
+.product-right-main:not(.free) .right-discount-label {
+ flex: 0 0 38px;
}
-.product-right-sub > span:nth-child(4) {
- width: 88px;
- margin-left: 10px;
+.product-right-main:not(.free) .right-discount-input {
+ flex: 0 0 100px;
+ width: 100px;
+ margin: 0;
}
+
+.product-right-main:not(.free) .right-unit {
+ flex: 0 0 22px;
+}
+.product-right-main .right-subsidy-label { grid-column: 14; margin: 0 !important; white-space: nowrap; }
+.product-right-main .right-subsidy-switch { grid-column: 15; margin: 0; }
+
+.product-right-sub .right-exceed-label { grid-column: 4 / span 3; }
+.product-right-sub .right-exceed-discount-field { grid-column: 7; }
+.product-right-sub .right-exceed-unit { grid-column: 9; }
+.product-right-sub .right-exceed-subsidy-label { grid-column: 14; white-space: nowrap; }
+.product-right-sub .right-exceed-subsidy-switch { grid-column: 15; }
.product-right-row .right-level {
- flex: 0 0 86px;
color: #111827;
font-size: 12px;
}
.product-right-row .right-enabled {
- flex: 0 0 42px;
- margin-right: 58px;
}
.product-right-row .right-free-label {
- flex: 0 0 64px;
}
.product-right-row .right-free-switch {
- flex: 0 0 42px;
- margin-right: 40px;
}
.product-right-row .right-limit-field {
position: relative;
- flex: 0 0 88px;
- margin-right: 8px;
+ width: 88px;
}
.product-right-row .right-limit-field .el-input-number {
@@ -1802,38 +1878,25 @@
}
.product-right-row .right-unit {
- flex: 0 0 22px;
}
.product-right-row .right-reset-label {
- flex: 0 0 58px;
- margin-left: 24px;
}
.product-right-row .right-reset-select {
- flex: 0 0 96px;
- margin-right: 12px;
+ width: 100px;
}
.product-right-row .right-discount-label {
- flex: 0 0 38px;
}
.product-right-row .right-discount-input {
- flex: 0 0 100px;
- margin-right: 8px;
}
.product-right-row .right-subsidy-label {
- flex: 0 0 88px;
}
.product-right-row .right-subsidy-switch {
- flex: 0 0 42px;
-}
-
-.product-right-main:not(.free) .right-subsidy-label {
- margin-left: 30px;
}
.merchant-product-form-panel .product-right-row .el-input {
@@ -1845,7 +1908,23 @@
}
.merchant-product-form-panel .product-right-sub .right-exceed-discount-input {
- width: 100px;
+ width: 88px;
+}
+
+.product-right-sub .right-exceed-discount-field {
+ position: relative;
+ width: 88px;
+}
+
+.product-right-sub .field-error {
+ position: absolute;
+ top: calc(100% + 4px);
+ left: 0;
+ width: max-content;
+ color: #f56c6c;
+ font-size: 12px;
+ line-height: 16px;
+ white-space: nowrap;
}
.merchant-product-form-panel .product-right-row .right-discount-input {
@@ -1854,6 +1933,12 @@
.merchant-product-form-panel .product-right-row .el-select {
width: 64px;
+}
+
+/* 团单免费次数的重置周期需完整展示“每天/每周/每月/每年”。 */
+.merchant-product-form-panel .product-right-row .right-reset-select {
+ flex-basis: 100px;
+ width: 100px;
}
.product-right-tip {
@@ -1881,8 +1966,19 @@
.merchant-product-form-panel .el-textarea__inner {
min-height: 28px;
border-radius: 0;
- background: #f7f8fb;
+ background: #ffffff;
box-shadow: 0 0 0 1px #d9dee8 inset;
+}
+
+/* Element Plus 会为禁用/只读控件注入灰底;团单编辑页仍需和其他表单控件保持同一白底与边框。 */
+.merchant-product-form-panel .el-input.is-disabled .el-input__wrapper,
+.merchant-product-form-panel .el-input.is-readonly .el-input__wrapper,
+.merchant-product-form-panel .el-input-number.is-disabled .el-input__wrapper,
+.merchant-product-form-panel .el-select.is-disabled .el-select__wrapper,
+.merchant-product-form-panel .el-date-editor.is-disabled .el-input__wrapper,
+.merchant-product-form-panel .el-textarea.is-disabled .el-textarea__inner {
+ background: #ffffff !important;
+ box-shadow: 0 0 0 1px #d9dee8 inset !important;
}
.merchant-product-form-panel .el-button,
@@ -1975,17 +2071,21 @@
}
.coupon-requirement-row .coupon-full-amount {
- width: 108px;
- margin: 0 6px 0 10px;
+ width: 168px;
+ margin: 0 50px 0 10px;
}
-.coupon-requirement-row .coupon-full-unit {
- display: inline-flex;
- flex: 0 0 44px;
- align-items: center;
- margin: 0 50px 0 0;
- line-height: 28px;
+.coupon-requirement-row .coupon-full-amount .el-input-group__append {
+ padding: 0 0 0 8px;
+ color: #334155;
+ background: transparent;
+ border: 0;
+ box-shadow: none;
white-space: nowrap;
+}
+
+.coupon-requirement-row .coupon-full-amount .el-input__wrapper {
+ border-radius: var(--el-border-radius-base);
}
.coupon-requirement-row .coupon-none-radio {
@@ -2487,6 +2587,10 @@
.order-filter-form .el-form-item {
margin-bottom: 0;
+}
+
+.order-filter-form .family-order-filter .el-form-item__label {
+ white-space: nowrap;
}
.order-filter-form .el-input,
@@ -3112,6 +3216,11 @@
margin-bottom: 0;
}
+/* 角色管理筛选项与标题左边缘对齐。 */
+.role-management-filter-form .el-form-item__label {
+ justify-content: flex-start;
+}
+
.business-fill-filter-form .el-input,
.business-fill-filter-form .el-select,
.business-fill-filter-form .el-date-editor {
@@ -3635,6 +3744,7 @@
color: #334155;
font-size: 13px;
line-height: 32px;
+ white-space: nowrap;
}
.news-publish-form .el-input {
@@ -3856,22 +3966,44 @@
padding: 16px 0 10px;
}
-.news-cover-preview {
- width: 120px;
- height: 72px;
- margin-left: 12px;
- vertical-align: middle;
- border: 1px solid #d9dee8;
- border-radius: 4px;
+.article-cover-upload .el-upload {
+ display: block;
}
-.policy-cover-preview {
- width: 120px;
- height: 72px;
- margin-left: 12px;
- vertical-align: middle;
- border: 1px solid #d9dee8;
- border-radius: 4px;
+.article-cover-upload-card,
+.article-cover-preview {
+ width: 76px;
+ height: 76px;
+ box-sizing: border-box;
+ border: 1px dashed #d7dce4;
+}
+
+.article-cover-upload-card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ color: #a8b0bb;
+ background: #ffffff;
+}
+
+.article-cover-upload-card span {
+ height: 30px;
+ font-size: 31px;
+ font-weight: 200;
+ line-height: 26px;
+}
+
+.article-cover-upload-card small {
+ margin-top: 5px;
+ font-size: 12px;
+ line-height: 16px;
+}
+
+.article-cover-preview {
+ display: block;
+ border-style: solid;
+ object-fit: cover;
}
.news-editor-file-input {
@@ -4086,6 +4218,13 @@
width: 100%;
}
+/* 管理后台的日期/时间选择控件统一为 320px,保证范围内容完整且各页面一致。 */
+.page-stack .el-date-editor,
+.prototype-page .el-date-editor,
+.merchant-prototype-page .el-date-editor {
+ width: 320px !important;
+}
+
.prototype-table-tools {
display: flex;
align-items: center;
diff --git a/talent-card-admin/src/views/BusinessManagementView.vue b/talent-card-admin/src/views/BusinessManagementView.vue
index a3e7f77..3e07bd7 100644
--- a/talent-card-admin/src/views/BusinessManagementView.vue
+++ b/talent-card-admin/src/views/BusinessManagementView.vue
@@ -171,7 +171,7 @@
<div class="page-stack prototype-page business-fill-page">
<section class="panel">
<div class="panel-header"><h3>{{ title }}</h3></div>
- <el-form class="business-fill-filter-form" label-width="86px">
+ <el-form class="business-fill-filter-form" :class="{ 'role-management-filter-form': isRole }" label-width="86px">
<el-form-item :label="isUpgrade ? '持卡人' : isRole ? '角色' : '员工'">
<el-input v-model="filters.keyword" :placeholder="isUpgrade ? '持卡人姓名、手机号、卡号' : '请输入关键词'" clearable />
</el-form-item>
@@ -210,7 +210,7 @@
<el-table-column label="操作" width="150" fixed="right" align="center">
<template #default="{ row }">
<el-button link type="primary" :icon="View" @click="view(row)">查看</el-button>
- <el-button v-if="!isUpgrade" link type="primary" :icon="Edit" @click="openEdit(row)">编辑</el-button>
+ <el-button v-if="!isUpgrade && !(isRole && isPlatformAdminRole(row))" link type="primary" :icon="Edit" @click="openEdit(row)">编辑</el-button>
<template v-if="isRole && !isPlatformAdminRole(row)">
<el-button link type="primary" @click="changeRoleStatus(row)">{{ Number(row.status) === 1 ? '停用' : '启用' }}</el-button>
<el-button link type="danger" @click="removeRole(row)">删除</el-button>
@@ -229,9 +229,9 @@
</el-dialog>
<el-dialog v-model="editVisible" :title="`编辑${title}`" width="480px" destroy-on-close :close-on-click-modal="false">
<el-form ref="formRef" :model="form" :rules="rules" label-width="92px">
- <el-form-item :label="isRole ? '角色名称' : '员工姓名'" prop="name"><el-input v-model="form.name" /></el-form-item>
- <el-form-item :label="isRole ? '角色编码' : '登录账号'" prop="code"><el-input v-model="form.code" :placeholder="isRole ? '' : '仅支持英文和数字'" maxlength="32" /></el-form-item>
- <el-form-item v-if="!isRole" label="登录密码" prop="password"><el-input v-model="form.password" type="password" :placeholder="editingId ? '留空则不修改' : '请输入登录密码'" /></el-form-item>
+ <el-form-item :label="isRole ? '角色名称' : '员工姓名'" prop="name" required><el-input v-model="form.name" /></el-form-item>
+ <el-form-item :label="isRole ? '角色编码' : '登录账号'" prop="code" required><el-input v-model="form.code" :placeholder="isRole ? '' : '仅支持英文和数字'" maxlength="32" /></el-form-item>
+ <el-form-item v-if="!isRole" label="登录密码" prop="password" :required="!editingId"><el-input v-model="form.password" type="password" :placeholder="editingId ? '留空则不修改' : '请输入登录密码'" /></el-form-item>
<el-form-item v-if="isRole" label="备注"><el-input v-model="form.remark" /></el-form-item>
<el-form-item label="状态"><el-switch v-model="form.status" :active-value="1" :inactive-value="0" /></el-form-item>
</el-form>
diff --git a/talent-card-admin/src/views/MerchantCreateView.vue b/talent-card-admin/src/views/MerchantCreateView.vue
index 97be179..c343c9c 100644
--- a/talent-card-admin/src/views/MerchantCreateView.vue
+++ b/talent-card-admin/src/views/MerchantCreateView.vue
@@ -75,7 +75,9 @@
const routeTab = typeof route.query.tab === 'string' ? route.query.tab : 'settings'
const activeTab = ref(['settings', 'rights', 'products', 'coupons'].includes(routeTab) ? routeTab : 'settings')
const couponMode = ref<'list' | 'edit'>('list')
-const isEditMode = computed(() => route.query.mode === 'edit')
+const hasValidMerchantId = computed(() => /^\d+$/.test(queryString('merchantId')))
+const isEditMode = computed(() => route.query.mode === 'edit' && hasValidMerchantId.value)
+const hasExistingMerchant = computed(() => isEditMode.value)
const normalizedMerchantType = computed(() => String(form.merchantType || '').trim())
const isFoodMerchant = computed(() => normalizedMerchantType.value === '餐饮服务')
const isApartmentMerchant = computed(() => normalizedMerchantType.value === '公寓租赁')
@@ -89,6 +91,7 @@
const isSpouseEmploymentMerchant = computed(() => normalizedMerchantType.value === '配偶推荐就业')
const isApprovalMerchant = computed(() => normalizedMerchantType.value === '行政审批')
const isServiceMerchant = computed(() => isFoodMerchant.value || normalizedMerchantType.value === '景区服务')
+const requiresFoodMerchantSettings = computed(() => !isEditMode.value && isFoodMerchant.value)
const supportsStoreConsumption = computed(() => [
'酒店服务', '景区服务', '餐饮服务', '口腔治疗保健', '免费体检',
].includes(normalizedMerchantType.value) || (isCarWashMerchant.value && form.miniProgramJumpType === 'normal'))
@@ -99,6 +102,10 @@
'购房优惠', '购车服务', '家电服务', '数码服务', '文化服务', '电信服务', '口腔治疗保健', '加油服务',
]
const usesSimplifiedMerchantSettings = computed(() => simplifiedMerchantSettingsTypes.includes(normalizedMerchantType.value))
+const settingsAndRightsOnlyMerchantTypes = [
+ '加油服务', '购房优惠', '购车服务', '家电服务', '数码服务', '文化服务', '电信服务',
+]
+const usesSettingsAndRightsOnlyTabs = computed(() => settingsAndRightsOnlyMerchantTypes.includes(normalizedMerchantType.value))
const supportsMerchantAppLogin = computed(() => (
isServiceMerchant.value
|| isApartmentMerchant.value
@@ -159,7 +166,22 @@
return merchantType === '餐饮服务' || merchantType === '景区服务' ? 1 : 0
}
+function applyNewMerchantDefaults() {
+ if (isEditMode.value) return
+
+ const merchantType = queryString('merchantType')
+ form.merchantType = merchantType
+ form.recommend = merchantType === '餐饮服务' || merchantType === '景区服务' ? 1 : 0
+ form.sortNo = undefined
+}
+
const tabs = computed(() => {
+ if (usesSettingsAndRightsOnlyTabs.value) {
+ return [
+ { label: '商家设置', value: 'settings' },
+ { label: '商家权益', value: 'rights' },
+ ]
+ }
if (isApartmentMerchant.value) {
return [
{ label: '商家设置', value: 'settings' },
@@ -235,9 +257,12 @@
trigger: 'change',
}],
sortNo: [{
- validator: (_rule, value, callback) => value === undefined || value === null || String(value).trim() === ''
- ? callback(new Error('请输入排序'))
- : callback(),
+ validator: (_rule, value, callback) => {
+ const text = String(value ?? '').trim()
+ if (!text) return callback(new Error('请输入排序'))
+ if (!/^\d+$/.test(text)) return callback(new Error('排序只能输入非负整数'))
+ callback()
+ },
trigger: 'blur',
}],
}))
@@ -376,7 +401,7 @@
validPeriod: '',
useMethod: '',
availableTime: '',
- otherRule: '',
+ otherRules: '',
})
const productValidPeriodRange = ref<string[]>([])
const productAvailableTimeRange = ref<string[]>([])
@@ -416,6 +441,7 @@
exceedSubsidy: true,
},
])
+const productExceedDiscountErrors = reactive<Record<string, string>>({})
function defaultProductRights() {
return [
@@ -961,6 +987,7 @@
'提供包间、免费停车、WiFi等服务',
'菜品以家常菜、特色川菜为主,具体以门店实际为准',
]
+
const locationDialogVisible = ref(false)
const pendingLongitude = ref('')
const pendingLatitude = ref('')
@@ -1536,7 +1563,7 @@
productNoticeForm.validPeriod = notice.validPeriod || ''
productNoticeForm.useMethod = notice.useMethod || ''
productNoticeForm.availableTime = notice.availableTime || ''
- productNoticeForm.otherRule = notice.otherRule || ''
+ productNoticeForm.otherRules = notice.otherRules || notice.otherRule || ''
productValidPeriodRange.value = rangeValues(productNoticeForm.validPeriod, /\d{4}-\d{2}-\d{2}/g)
productAvailableTimeRange.value = rangeValues(productNoticeForm.availableTime, /\d{2}:\d{2}/g)
if (isHealthCheckMerchant.value) {
@@ -1609,6 +1636,16 @@
return amount > 0 && amount <= 10
}
+function validateProductExceedDiscount(item: { level: string, exceedDiscount: unknown }) {
+ const key = item.level
+ if (validExceedDiscount(item.exceedDiscount)) {
+ delete productExceedDiscountErrors[key]
+ return true
+ }
+ productExceedDiscountErrors[key] = '请输入大于 0 且不超过 10 的一位小数'
+ return false
+}
+
function validFreeLimitCount(value: unknown) {
const text = String(value ?? '').trim()
if (!/^\d+$/.test(text)) return false
@@ -1624,8 +1661,7 @@
ElMessage.warning('免费次数请输入 1 至 999 的整数,999 表示不限次数')
return
}
- if (productRights.some((item) => item.free && !validExceedDiscount(item.exceedDiscount))) {
- ElMessage.warning('超过次数折扣请输入大于 0 且不超过 10 的一位小数')
+ if (productRights.some((item) => item.free && !validateProductExceedDiscount(item))) {
return
}
} else {
@@ -2036,6 +2072,12 @@
}
})
+watch(tabs, (availableTabs) => {
+ if (!availableTabs.some((item) => item.value === activeTab.value)) {
+ activeTab.value = 'settings'
+ }
+}, { immediate: true })
+
watch(() => form.coverUrl, resetMerchantCoverPreview)
watch(() => productForm.coverUrl, resetProductCoverPreview)
watch(productValidPeriodRange, (value) => {
@@ -2061,6 +2103,10 @@
watch(() => form.miniProgramJumpUrl, () => {
if (isCarWashMerchant.value) syncCarWashJumpMeta()
})
+
+// The create page is reused when the service type changes, so reset these
+// create-only defaults from the current route instead of retaining old values.
+watch(() => [route.query.mode, route.query.merchantType], applyNewMerchantDefaults, { immediate: true })
onMounted(() => {
if (isEditMode.value && queryString('merchantId')) {
@@ -2116,16 +2162,16 @@
<el-form ref="merchantFormRef" :model="form" :rules="merchantRules" class="merchant-settings-form" label-width="58px" label-position="left">
<div class="merchant-create-content" :class="{ 'service-merchant-content': isServiceMerchant || isApartmentMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant }">
<div class="merchant-create-left" :style="isApartmentMerchant || isFoodMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant ? 'flex: 1' : ''">
- <el-form-item class="merchant-form-row three-columns" label="商家名称" prop="merchantName">
+ <el-form-item class="merchant-form-row three-columns" label="商家名称" prop="merchantName" required>
<el-input v-model="form.merchantName" />
</el-form-item>
- <el-form-item v-if="!isApartmentMerchant" class="merchant-form-row address-row" :label="isCarWashMerchant || isHealthCheckMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant ? '服务点' : '地址'" prop="address">
+ <el-form-item v-if="!isApartmentMerchant" class="merchant-form-row address-row" :label="isCarWashMerchant || isHealthCheckMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant ? '服务点' : '地址'" prop="address" required>
<el-input v-model="form.address" />
<el-button type="primary" @click="markLocation">地图标点</el-button>
</el-form-item>
- <el-form-item class="merchant-cover-row" label="封面图" prop="coverUrl">
+ <el-form-item class="merchant-cover-row" label="封面图" prop="coverUrl" required>
<div class="merchant-cover-preview">
<img
v-if="form.coverUrl && !merchantCoverPreviewFailed"
@@ -2140,7 +2186,7 @@
</div>
<div v-if="!isServiceMerchant && !isApartmentMerchant && !isHotelMerchant && !isCarWashMerchant && !isHealthCheckMerchant && !isParkingMerchant && !isChildcareMerchant && !isFinancialMerchant && !isMovieMerchant && !isSpouseEmploymentMerchant && !isApprovalMerchant" class="merchant-create-middle">
- <el-form-item class="merchant-form-row" label="联系电话" prop="contactMobile">
+ <el-form-item class="merchant-form-row" label="联系电话" prop="contactMobile" required>
<el-input v-model="form.contactMobile" />
</el-form-item>
@@ -2164,16 +2210,16 @@
</div>
<div class="merchant-create-right" :style="isApartmentMerchant || isFoodMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant ? 'flex: 1' : ''">
- <el-form-item v-if="isServiceMerchant || isApartmentMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant" class="merchant-form-row service-contact-row" label="联系电话" prop="contactMobile">
+ <el-form-item v-if="isServiceMerchant || isApartmentMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant" class="merchant-form-row service-contact-row" label="联系电话" prop="contactMobile" required>
<el-input v-model="form.contactMobile" placeholder="请输入联系电话" />
</el-form-item>
- <div v-if="isEditMode" class="merchant-form-row">
+ <div v-if="hasExistingMerchant" class="merchant-form-row">
<label>创建时间</label>
<el-input v-model="form.createdAt" readonly />
</div>
- <el-form-item v-if="!isApprovalMerchant" class="merchant-hours-block" label="营业时间" prop="businessHours">
+ <el-form-item v-if="!isApprovalMerchant" class="merchant-hours-block" label="营业时间" prop="businessHours" required>
<div class="merchant-hours-list">
<div v-for="(item, index) in businessHours" :key="item.id" class="merchant-hour-row">
<el-time-picker
@@ -2206,7 +2252,7 @@
<el-switch v-model="form.recommend" :active-value="1" :inactive-value="0" />
</div>
- <el-form-item class="merchant-form-row" label="排序" prop="sortNo">
+ <el-form-item class="merchant-form-row" label="排序" prop="sortNo" required>
<el-input v-model="form.sortNo" placeholder="请输入" />
</el-form-item>
</div>
@@ -2673,17 +2719,17 @@
</template>
<template v-else>
- <el-form ref="productFormRef" :model="productForm" :rules="productRules" class="merchant-product-edit-form" label-width="54px" label-position="left">
+ <el-form ref="productFormRef" :model="productForm" :rules="productRules" class="merchant-product-edit-form" label-width="68px" label-position="left">
<section class="merchant-product-form-panel">
<h3>商品信息</h3>
<div class="product-form-grid">
<div class="product-info-left">
- <el-form-item class="product-form-row" label="商品名称" prop="productName">
+ <el-form-item class="product-form-row" label="商品名称" prop="productName" required>
<el-input v-model="productForm.productName" placeholder="请输入商品名称" />
</el-form-item>
- <el-form-item class="product-image-row" label="封面图" prop="coverUrl">
+ <el-form-item class="product-image-row" label="封面图" prop="coverUrl" required>
<div class="product-cover-preview">
<img
v-if="productForm.coverUrl && !productCoverPreviewFailed"
@@ -2697,7 +2743,7 @@
<el-button type="primary" :loading="imageUploading" @click="changeCover('productCover')">更换封面</el-button>
</el-form-item>
- <el-form-item v-if="!isHealthCheckMerchant" class="product-image-row compact" label="商品图片" prop="productImages">
+ <el-form-item v-if="!isHealthCheckMerchant" class="product-image-row compact" label="商品图片" prop="productImages" required>
<div class="product-image-list">
<div v-for="(image, index) in productImages" :key="image" class="product-thumb-preview">
<img :src="image" alt="商品图片" />
@@ -2725,7 +2771,7 @@
</div>
<div v-if="!isHealthCheckMerchant" class="product-info-middle">
- <el-form-item class="product-form-row amount" :class="{ 'hotel-amount': isHotelMerchant }" label="商品金额" prop="salePrice">
+ <el-form-item class="product-form-row amount" :class="{ 'hotel-amount': isHotelMerchant }" label="商品金额" prop="salePrice" required>
<template v-if="isHotelMerchant">
<el-input-number
v-model="productForm.salePrice"
@@ -2772,7 +2818,7 @@
</div>
- <el-form-item v-if="!isHealthCheckMerchant" class="product-detail-row" label="商品详情" prop="detailText">
+ <el-form-item v-if="!isHealthCheckMerchant" class="product-detail-row" label="商品详情" prop="detailText" required>
<AdminRichTextEditor :model-value="productForm.detailText || ''" class="merchant-product-rich-editor" placeholder="请输入商品详情" :height="260" @update:model-value="value => { productForm.detailText = value; productFormRef?.validateField('detailText') }" />
</el-form-item>
@@ -2840,7 +2886,7 @@
</div>
<div class="product-notice-field">
<label>其他规则</label>
- <el-input v-model="productNoticeForm.otherRule" placeholder="请输入" />
+ <el-input v-model="productNoticeForm.otherRules" placeholder="请输入" />
</div>
</div>
</section>
@@ -2877,28 +2923,35 @@
</el-select>
</template>
<template v-else>
- <span class="right-discount-label">折扣</span>
- <el-input v-model="item.discount" class="right-discount-input" placeholder="请输入" />
- <span class="right-unit">折</span>
+ <div class="right-discount-control">
+ <span class="right-discount-label">折扣</span>
+ <el-input v-model="item.discount" class="right-discount-input" placeholder="请输入" />
+ <span class="right-unit">折</span>
+ </div>
</template>
<span class="right-subsidy-label">是否政府补贴</span>
<el-switch v-model="item.subsidy" class="right-subsidy-switch" />
</div>
<div v-if="item.free" class="product-right-sub">
- <span>超过次数</span>
- <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.exceedSubsidy" />
+ <span class="right-exceed-label">超过次数</span>
+ <div class="right-exceed-discount-field">
+ <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="请输入"
+ @change="validateProductExceedDiscount(item)"
+ @blur="validateProductExceedDiscount(item)"
+ />
+ <span v-if="productExceedDiscountErrors[item.level]" class="field-error">{{ productExceedDiscountErrors[item.level] }}</span>
+ </div>
+ <span class="right-exceed-unit">折</span>
+ <span class="right-exceed-subsidy-label">是否政府补贴</span>
+ <el-switch v-model="item.exceedSubsidy" class="right-exceed-subsidy-switch" />
</div>
</div>
</div>
@@ -3003,19 +3056,20 @@
<template v-else>
<el-form ref="couponFormRef" :model="couponEditForm" :rules="couponRules" class="merchant-coupon-edit-form" label-width="72px" label-position="left">
- <el-form-item class="coupon-edit-row" label="优惠券名称" prop="couponName">
+ <el-form-item class="coupon-edit-row" label="优惠券名称" prop="couponName" required>
<el-input v-model="couponEditForm.couponName" placeholder="请输入优惠券名称" />
</el-form-item>
- <el-form-item class="coupon-edit-row coupon-amount-row" label="优惠券金额" prop="couponAmount">
+ <el-form-item class="coupon-edit-row coupon-amount-row" label="优惠券金额" prop="couponAmount" required>
<div class="coupon-amount-control"><el-input-number v-model="couponEditForm.couponAmount" :min="0.01" :precision="2" :step="1" controls-position="right" placeholder="请输入优惠券金额" /><span>元</span></div>
</el-form-item>
- <el-form-item class="coupon-edit-row coupon-requirement-row" label="使用要求" prop="fullAmount">
+ <el-form-item class="coupon-edit-row coupon-requirement-row" label="使用要求" prop="fullAmount" :required="couponEditForm.useRequirement === 'full'">
<el-radio-group v-model="couponEditForm.useRequirement">
<el-radio label="full" class="coupon-full-radio">满</el-radio>
- <el-input v-model="couponEditForm.fullAmount" class="coupon-full-amount" />
- <span class="coupon-full-unit">元可用</span>
+ <el-input v-model="couponEditForm.fullAmount" class="coupon-full-amount">
+ <template #append>元可用</template>
+ </el-input>
<el-radio label="none" class="coupon-none-radio">不限</el-radio>
</el-radio-group>
</el-form-item>
@@ -3042,7 +3096,7 @@
</div>
</div>
- <el-form-item class="coupon-edit-row" label="有效期" prop="validTime">
+ <el-form-item class="coupon-edit-row" label="有效期" prop="validTime" required>
<el-date-picker
v-model="couponEditForm.validTime"
class="coupon-valid-time-picker"
@@ -3054,7 +3108,7 @@
/>
</el-form-item>
- <el-form-item class="coupon-edit-row coupon-product-scope-row" label="可用商品" prop="productScope">
+ <el-form-item class="coupon-edit-row coupon-product-scope-row" label="可用商品" prop="productScope" :required="couponEditForm.productScope === 'selected'">
<el-radio-group v-model="couponEditForm.productScope">
<el-radio label="all">不限</el-radio>
<el-radio label="selected">选择</el-radio>
@@ -3113,10 +3167,10 @@
<el-dialog v-model="schoolDialogVisible" :title="editingSchoolIndex >= 0 ? '编辑学校' : '新增学校'" width="820px" destroy-on-close :close-on-click-modal="false">
<el-form ref="schoolFormRef" :model="editingSchool" :rules="schoolRules" label-width="100px" style="padding: 20px 20px 0;">
- <el-form-item label="学校名称" prop="name">
+ <el-form-item label="学校名称" prop="name" required>
<el-input v-model="editingSchool.name" placeholder="请输入" style="width: 320px" />
</el-form-item>
- <el-form-item label="地址" prop="address">
+ <el-form-item label="地址" prop="address" required>
<el-input v-model="editingSchool.address" placeholder="请输入" style="width: 320px" />
</el-form-item>
<el-form-item label="定位点">
diff --git a/talent-card-admin/src/views/MerchantOnboardingDetailView.vue b/talent-card-admin/src/views/MerchantOnboardingDetailView.vue
index b8075ea..b801a4b 100644
--- a/talent-card-admin/src/views/MerchantOnboardingDetailView.vue
+++ b/talent-card-admin/src/views/MerchantOnboardingDetailView.vue
@@ -96,9 +96,9 @@
.detail-panel h3 { margin: 0; color: #1d2129; font-size: 15px; }
.status-value { margin-top: 16px; color: #1d2129; font-size: 13px; font-weight: 600; }
.onboarding-heading { margin-top: 24px !important; }
-.detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 28px 46px; margin-top: 22px; max-width: 860px; }
+.detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 28px 46px; margin-top: 22px; max-width: 1032px; }
.common-grid { margin-bottom: -2px; }
-.detail-field { display: grid; grid-template-columns: 118px minmax(0, 1fr); align-items: center; gap: 12px; min-height: 32px; }
+.detail-field { display: grid; grid-template-columns: 144px minmax(0, 1fr); align-items: center; gap: 12px; min-height: 32px; }
.detail-field > span { color: #303133; font-size: 13px; font-weight: 500; white-space: nowrap; }
.detail-field > strong { min-height: 32px; box-sizing: border-box; padding: 7px 10px; color: #606266; font-size: 13px; font-weight: 400; line-height: 18px; word-break: break-all; background: #f5f7fa; }
.placeholder-field { visibility: hidden; }
diff --git a/talent-card-admin/src/views/MerchantSettlementView.vue b/talent-card-admin/src/views/MerchantSettlementView.vue
index 78217af..9037cf3 100644
--- a/talent-card-admin/src/views/MerchantSettlementView.vue
+++ b/talent-card-admin/src/views/MerchantSettlementView.vue
@@ -1,14 +1,15 @@
<script setup lang="ts">
-import { computed, onMounted, reactive, ref } from 'vue'
-import { useRouter } from 'vue-router'
+import { computed, onMounted, reactive, ref, watch } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh, Search } from '@element-plus/icons-vue'
import { getMerchantSettlement, reviewMerchantSettlement } from '../api/admin'
type Kind = 'onboardings' | 'bank-cards' | 'withdrawals'
-const kind = ref<Kind>('onboardings')
+const kind = ref<Kind>('withdrawals')
const router = useRouter()
+const route = useRoute()
const loading = ref(false)
const rows = ref<Record<string, any>[]>([])
const total = ref(0)
@@ -111,6 +112,13 @@
load()
}
+function applyRouteKind() {
+ const value = String(route.query.kind || '')
+ if (value === 'onboardings' || value === 'bank-cards' || value === 'withdrawals') {
+ kind.value = value
+ }
+}
+
function openReview(row: Record<string, any>) {
if (kind.value === 'onboardings') {
router.push({ name: 'merchant-onboarding-detail', params: { id: row.id } })
@@ -152,7 +160,17 @@
}
}
-onMounted(load)
+watch(() => route.query.kind, () => {
+ applyRouteKind()
+ page.value = 1
+ filters.status = ''
+ load()
+})
+
+onMounted(() => {
+ applyRouteKind()
+ load()
+})
</script>
<template>
@@ -234,7 +252,7 @@
<el-table-column prop="bankName" label="开户银行" min-width="145" show-overflow-tooltip />
<el-table-column prop="branchName" label="开户支行" min-width="180" show-overflow-tooltip />
<el-table-column prop="cityName" label="开户城市" min-width="120" show-overflow-tooltip />
- <el-table-column prop="cardMasked" label="银行卡号" min-width="150" />
+ <el-table-column prop="cardNo" label="银行卡号" min-width="180" />
</template>
<el-table-column label="申请时间" min-width="165">
<template #default="{ row }">{{ formatDate(row.createdAt) }}</template>
diff --git a/talent-card-admin/src/views/MerchantView.vue b/talent-card-admin/src/views/MerchantView.vue
index f5657a0..1feb33d 100644
--- a/talent-card-admin/src/views/MerchantView.vue
+++ b/talent-card-admin/src/views/MerchantView.vue
@@ -167,7 +167,9 @@
typeDialogVisible.value = false
router.push({
path: '/merchants/new',
- query: { merchantType: selectedCreateType.value },
+ // Explicitly mark this as a create route so a previously opened edit page
+ // cannot leave its mode or data in the reused route component.
+ query: { mode: 'create', merchantType: selectedCreateType.value },
})
}
@@ -361,7 +363,7 @@
<el-drawer v-model="drawerVisible" :title="drawerTitle" size="520px" @closed="resetForm">
<el-form ref="formRef" :model="form" :rules="rules" label-width="96px" class="drawer-form">
- <el-form-item label="商家名称" prop="merchantName">
+ <el-form-item label="商家名称" prop="merchantName" required>
<el-input v-model="form.merchantName" placeholder="请输入商家名称" />
</el-form-item>
<el-form-item label="商家类型">
diff --git a/talent-card-admin/src/views/OrderDetailView.vue b/talent-card-admin/src/views/OrderDetailView.vue
index 20e52e7..2a84e81 100644
--- a/talent-card-admin/src/views/OrderDetailView.vue
+++ b/talent-card-admin/src/views/OrderDetailView.vue
@@ -53,10 +53,6 @@
return value.verifyStatus === 1 ? '已核销' : '待核销'
}
-function paymentStatus(value: string) {
- return ({ pending: '待支付', processing: '支付中', paid: '支付成功', failed: '支付失败', closed: '已关闭' } as Record<string, string>)[value] || value || '-'
-}
-
function previewImage(url: string) {
if (!url) return
previewUrl.value = url
@@ -197,7 +193,6 @@
<div><span>支付方式</span><strong>{{ payment.provider === 'HUIFU' ? '微信支付' : payment.provider || '-' }}</strong></div>
<div><span>支付流水号</span><strong>{{ payment.providerTransactionNo || payment.paymentNo || '-' }}</strong></div>
<div><span>支付金额</span><strong class="amount-strong">{{ money(payment.amount) }}</strong></div>
- <div><span>支付状态</span><strong>{{ paymentStatus(payment.status) }}</strong></div>
</div>
</div>
<div v-else class="detail-empty">暂无支付流水</div>
diff --git a/talent-card-admin/src/views/OrderView.vue b/talent-card-admin/src/views/OrderView.vue
index 339674b..d2a8b8b 100644
--- a/talent-card-admin/src/views/OrderView.vue
+++ b/talent-card-admin/src/views/OrderView.vue
@@ -12,7 +12,7 @@
const orders = ref<Order[]>([])
const router = useRouter()
-const cardLevels = ['钻石卡', '卓越人才卡', '精英人才卡', '青年人才卡']
+const cardLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡', '普通游客']
const filters = reactive({
talentKeyword: '',
@@ -189,7 +189,7 @@
<el-option label="已退款" :value="2" />
</el-select>
</el-form-item>
- <el-form-item v-if="isGroupOrder" label="是否家属下单">
+ <el-form-item v-if="isGroupOrder" class="family-order-filter" label="是否家属下单">
<el-select v-model="filters.familyOrder" placeholder="全部" clearable>
<el-option label="全部" :value="undefined" />
<el-option label="是" :value="1" />
diff --git a/talent-card-admin/src/views/ProductAuditDetailView.vue b/talent-card-admin/src/views/ProductAuditDetailView.vue
index 7002be4..a3a8d8a 100644
--- a/talent-card-admin/src/views/ProductAuditDetailView.vue
+++ b/talent-card-admin/src/views/ProductAuditDetailView.vue
@@ -74,10 +74,10 @@
try {
const parsed = typeof value === 'string' ? JSON.parse(value) : value
- notice.validPeriod = parsed.validPeriod || parsed.valid_period || ''
- notice.useMethod = parsed.useMethod || parsed.use_method || ''
+ notice.validPeriod = parsed.validPeriod || parsed.valid_period || parsed.validity || ''
+ notice.useMethod = parsed.useMethod || parsed.use_method || parsed.usage || ''
notice.availableTime = parsed.availableTime || parsed.available_time || ''
- notice.otherRules = parsed.otherRules || parsed.other_rules || ''
+ notice.otherRules = parsed.otherRules || parsed.other_rules || parsed.otherRule || parsed.other_rule || ''
} catch {
notice.otherRules = typeof value === 'string' ? value : ''
}
diff --git a/talent-card-admin/src/views/RefundRecordView.vue b/talent-card-admin/src/views/RefundRecordView.vue
index 4628515..f77665c 100644
--- a/talent-card-admin/src/views/RefundRecordView.vue
+++ b/talent-card-admin/src/views/RefundRecordView.vue
@@ -12,7 +12,7 @@
const loading = ref(false)
const rows = ref<RefundRecord[]>([])
-const cardLevels = ['钻石卡', '卓越人才卡', '精英人才卡', '青年人才卡']
+const cardLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡', '普通游客']
const itemNameLabel = computed(() => (activeTab.value === 'GROUP' ? '团购名称' : '消费项目'))
const filters = reactive({
diff --git a/talent-card-admin/src/views/RoleFormView.vue b/talent-card-admin/src/views/RoleFormView.vue
index dd76e17..098c390 100644
--- a/talent-card-admin/src/views/RoleFormView.vue
+++ b/talent-card-admin/src/views/RoleFormView.vue
@@ -10,26 +10,78 @@
type Module = { name: string; pages: Page[] }
const modules: Module[] = [
{ name: '首页', pages: [{ code: 'dashboard', name: '首页', actions: ['查看'] }] },
- { name: '账户权限管理', pages: [{ code: 'staff', name: '账号管理', actions: ['查看', '新增', '编辑', '启用/禁用', '删除', '重置密码'] }] },
- { name: '角色管理', pages: [{ code: 'roles', name: '角色管理', actions: ['查看', '新增', '编辑', '删除'] }] },
- { name: '持卡管理', pages: [{ code: 'talents', name: '持卡人管理', actions: ['查看', '新增', '编辑', '启用/禁用'] }] },
- { name: '家属卡管理', pages: [{ code: 'family-cards', name: '家属卡管理', actions: ['查看', '审核'] }] },
- { name: '商家管理', pages: [{ code: 'merchants', name: '商家查询', actions: ['查看', '新增', '编辑'] }] },
- { name: '团单审核', pages: [{ code: 'products', name: '团单审核', actions: ['查看', '审核'] }] },
- { name: '订单管理', pages: [{ code: 'orders', name: '订单列表', actions: ['查看', '退款处理'] }] },
- { name: '退款记录', pages: [{ code: 'refunds', name: '退款记录', actions: ['查看'] }] },
- { name: '预约管理', pages: [{ code: 'medical-reservations', name: '体检预约管理', actions: ['查看', '审核'] }] },
- { name: '停车车牌管理', pages: [{ code: 'parking-plates', name: '停车车牌管理', actions: ['查看'] }] },
- { name: '权益图片管理', pages: [{ code: 'equity-images', name: '权益图片管理', actions: ['查看', '编辑'] }] },
- { name: '人才资讯管理', pages: [{ code: 'news', name: '人才资讯管理', actions: ['查看', '新增', '编辑'] }] },
- { name: '系统管理', pages: [{ code: 'settings', name: '平台设置', actions: ['查看', '编辑'] }] },
+ {
+ name: '持卡管理',
+ pages: [
+ { code: 'talents', name: '持卡人管理', actions: ['查看', '新增', '编辑', '启用/停用'] },
+ { code: 'family-card-applications', name: '家属卡申请', actions: ['查看', '审核'] },
+ { code: 'upgrade-applications', name: '升级申请', actions: ['查看', '审核'] },
+ { code: 'family-cards', name: '家属卡管理', actions: ['查看', '启用/停用'] },
+ { code: 'child-cards', name: '子女管理', actions: ['查看', '审核'] },
+ ],
+ },
+ {
+ name: '商家管理',
+ pages: [
+ { code: 'merchants', name: '商家查询', actions: ['查看', '新增', '编辑', '启用/停用'] },
+ { code: 'products', name: '团单审核', actions: ['查看', '审核'] },
+ ],
+ },
+ {
+ name: '订单管理',
+ pages: [
+ { code: 'orders', name: '订单列表', actions: ['查看'] },
+ { code: 'refunds', name: '退款记录', actions: ['查看'] },
+ { code: 'hotel-free-verifications', name: '酒店免费核销记录', actions: ['查看'] },
+ ],
+ },
+ {
+ name: '预约管理',
+ pages: [
+ { code: 'appointment-management', name: '预约服务管理', actions: ['查看', '审核'] },
+ { code: 'medical-reservations', name: '体检预约管理', actions: ['查看', '审核'] },
+ { code: 'medical-applications', name: '就医申请管理', actions: ['查看', '审核'] },
+ { code: 'parking-plates', name: '停车车牌管理', actions: ['查看', '审核'] },
+ { code: 'car-wash-benefits', name: '洗车权益管理', actions: ['查看', '审核'] },
+ { code: 'fuel-services', name: '加油服务管理', actions: ['查看', '审核'] },
+ { code: 'spouse-employment', name: '配偶就业管理', actions: ['查看', '审核'] },
+ { code: 'hotel-reservations', name: '酒店预约管理', actions: ['查看', '审核'] },
+ ],
+ },
+ { name: '财务管理', pages: [{ code: 'merchant-settlement', name: '商户结算审核', actions: ['查看', '审核'] }] },
+ {
+ name: '运营管理',
+ pages: [
+ { code: 'equity-images', name: '权益图片管理', actions: ['查看', '编辑'] },
+ { code: 'news', name: '人才资讯管理', actions: ['查看', '新增', '编辑', '删除'] },
+ { code: 'policies', name: '人才政策管理', actions: ['查看', '新增', '编辑', '删除'] },
+ { code: 'movie-tickets', name: '电影票管理', actions: ['查看', '新增', '编辑', '审核'] },
+ ],
+ },
+ {
+ name: '系统管理',
+ pages: [
+ { code: 'settings', name: '平台设置', actions: ['查看', '编辑'] },
+ { code: 'protocol-settings', name: '协议设置', actions: ['查看', '编辑'] },
+ { code: 'roles', name: '角色管理', actions: ['查看', '新增', '编辑', '删除'] },
+ { code: 'staff', name: '员工管理', actions: ['查看', '新增', '编辑', '启用/停用', '删除', '重置密码'] },
+ ],
+ },
]
const route = useRoute(), router = useRouter(), saving = ref(false)
const id = computed(() => route.params.id ? String(route.params.id) : '')
const form = reactive({ roleName: '', remark: '', status: 1, permissionCodes: [] as string[] })
const formRef = ref<FormInstance>()
const rules: FormRules = { roleName: [{ required: true, message: '请输入角色名称', trigger: 'blur' }] }
-const code = (page: string, action: string) => `admin.${page}.${action === '启用/禁用' ? 'disable' : action === '重置密码' ? 'reset' : action === '退款处理' ? 'refund' : action === '查看' ? 'view' : action === '新增' ? 'create' : action === '编辑' ? 'edit' : action === '删除' ? 'delete' : 'audit'}`
+const code = (page: string, action: string) => `admin.${page}.${({
+ '查看': 'view',
+ '新增': 'create',
+ '编辑': 'edit',
+ '启用/停用': 'disable',
+ '删除': 'delete',
+ '重置密码': 'reset',
+ '审核': 'audit',
+} as Record<string, string>)[action] || 'view'}`
const pageCodes = (page: Page) => page.actions.map((action) => code(page.code, action))
const moduleCodes = (module: Module) => module.pages.flatMap(pageCodes)
const checked = (values: string[]) => values.every((value) => form.permissionCodes.includes(value))
diff --git a/talent-card-admin/src/views/StaffManagementView.vue b/talent-card-admin/src/views/StaffManagementView.vue
index d122626..236067d 100644
--- a/talent-card-admin/src/views/StaffManagementView.vue
+++ b/talent-card-admin/src/views/StaffManagementView.vue
@@ -5,6 +5,7 @@
import { Plus, Refresh, Search } from "@element-plus/icons-vue";
import {
createAdminStaff,
+ checkAdminStaffAccount,
deleteAdminStaff,
getAdminRoles,
getAdminStaff,
@@ -31,6 +32,7 @@
status: 1,
});
const formRef = ref<FormInstance>();
+const accountTaken = ref(false);
const accountPattern = /^[A-Za-z0-9]+$/;
const rules = computed<FormRules>(() => ({
displayName: [{ required: true, message: "请输入人员名称", trigger: "blur" }],
@@ -50,9 +52,11 @@
password: [
{
validator: (_rule, value, callback) => {
- if (editId.value) return callback();
- if (!value) return callback(new Error("请输入登录密码"));
- if (String(value).length < 6) return callback(new Error("新建员工密码至少 6 位"));
+ if (!value) {
+ if (editId.value) return callback();
+ return callback(new Error("请输入登录密码"));
+ }
+ if (String(value).length < 6) return callback(new Error("密码至少 6 位"));
callback();
},
trigger: "blur",
@@ -61,7 +65,10 @@
confirmPassword: [
{
validator: (_rule, value, callback) => {
- if (editId.value) return callback();
+ if (!form.password) {
+ if (editId.value) return callback();
+ return callback(new Error("请输入确认密码"));
+ }
if (!value) return callback(new Error("请输入确认密码"));
if (value !== form.password) return callback(new Error("两次输入的密码不一致"));
callback();
@@ -101,6 +108,15 @@
const keepMobileDigits = () => {
form.mobile = form.mobile.replace(/\D/g, "").slice(0, 11);
};
+const normalizeAccount = () => {
+ form.account = form.account.replace(/[^A-Za-z0-9]/g, "").slice(0, 32);
+ accountTaken.value = false;
+};
+async function checkAccountAvailable() {
+ if (editId.value || !accountPattern.test(form.account.trim())) return;
+ const result = await checkAdminStaffAccount(form.account.trim());
+ accountTaken.value = !result.available;
+}
async function load() {
loading.value = true;
try {
@@ -117,6 +133,7 @@
}
function create() {
editId.value = undefined;
+ accountTaken.value = false;
Object.assign(form, {
displayName: "",
mobile: "",
@@ -132,6 +149,7 @@
}
function edit(row: AdminStaff) {
editId.value = row.id;
+ accountTaken.value = false;
Object.assign(form, {
displayName: row.displayName,
mobile: row.mobile || "",
@@ -148,6 +166,10 @@
async function save() {
const valid = await formRef.value?.validate().catch(() => false);
if (!valid) return;
+ if (!editId.value) {
+ await checkAccountAvailable();
+ if (accountTaken.value) return;
+ }
saving.value = true;
try {
const { confirmPassword, ...formPayload } = form;
@@ -267,7 +289,7 @@
}}</template></el-table-column
><el-table-column label="操作" width="225" fixed="right"
><template #default="{ row }"
- ><el-button link type="primary" @click="edit(row)">编辑</el-button
+ ><el-button v-if="!isDefaultAdmin(row)" link type="primary" @click="edit(row)">编辑</el-button
><el-button
v-if="!isDefaultAdmin(row)"
link
@@ -304,7 +326,7 @@
destroy-on-close
:close-on-click-modal="false"
><el-form ref="formRef" :model="form" :rules="rules" label-width="92px"
- ><el-form-item label="人员名称" prop="displayName"
+ ><el-form-item label="人员名称" prop="displayName" required
><el-input v-model="form.displayName" /></el-form-item
><el-form-item label="联系电话"
><el-input
@@ -312,13 +334,14 @@
inputmode="numeric"
maxlength="11"
@input="keepMobileDigits" /></el-form-item
- ><el-form-item label="登录账号" prop="account"
+ ><el-form-item label="登录账号" prop="account" required :error="accountTaken ? '登录账号已存在,请更换' : ''"
><el-input
v-model="form.account"
:disabled="Boolean(editId)"
maxlength="32"
placeholder="仅支持英文和数字"
- /></el-form-item
+ @input="normalizeAccount"
+ @blur="checkAccountAvailable" /></el-form-item
><el-form-item label="登录密码" prop="password" :required="!editId"
><el-input
v-model="form.password"
@@ -326,19 +349,20 @@
:placeholder="
editId ? '留空则不修改' : '至少 6 位'
" /></el-form-item
- ><el-form-item v-if="!editId" label="确认密码" prop="confirmPassword" required
+ ><el-form-item v-if="!editId || form.password" label="确认密码" prop="confirmPassword" :required="!editId || Boolean(form.password)"
><el-input
v-model="form.confirmPassword"
type="password"
+ show-password
placeholder="请再次输入密码" /></el-form-item
- ><el-form-item label="角色" prop="roleCode"
+ ><el-form-item label="角色" prop="roleCode" required
><el-select v-model="form.roleCode" class="full"
><el-option
v-for="role in roles"
:key="role.id"
:label="role.roleName"
:value="role.roleCode" /></el-select></el-form-item
- ><el-form-item label="职务" prop="positionName"
+ ><el-form-item label="职务" prop="positionName" required
><el-input v-model="form.positionName" /></el-form-item
><el-form-item label="客服微信"
><el-input v-model="form.customerWechat" /></el-form-item
@@ -363,8 +387,6 @@
.filters {
margin: 14px 20px 0;
padding: 16px;
- border: 1px solid #dce3ea;
- border-radius: 4px;
}
.filters :deep(.el-input),
.filters :deep(.el-select) {
diff --git a/talent-card-admin/src/views/TalentCreateView.vue b/talent-card-admin/src/views/TalentCreateView.vue
index c8a4f86..e540e8d 100644
--- a/talent-card-admin/src/views/TalentCreateView.vue
+++ b/talent-card-admin/src/views/TalentCreateView.vue
@@ -128,7 +128,7 @@
<div class="talent-detail-page talent-create-page">
<el-form ref="formRef" :model="form" :rules="rules" label-width="104px">
<section class="detail-block create-mobile-summary">
- <el-form-item label="手机号码:" prop="mobile">
+ <el-form-item label="手机号码:" prop="mobile" required>
<el-input
v-model="form.mobile"
placeholder="请输入"
@@ -143,10 +143,10 @@
<h3>持卡人认证</h3>
<div class="identity-edit-card">
<div class="identity-edit-form">
- <el-form-item label="持卡人姓名:" prop="name">
+ <el-form-item label="持卡人姓名:" prop="name" required>
<el-input v-model="form.name" placeholder="请输入" />
</el-form-item>
- <el-form-item label="身份证号:" prop="idCardNo">
+ <el-form-item label="身份证号:" prop="idCardNo" required>
<el-input
v-model="form.idCardNo"
placeholder="请输入18位身份证号"
@@ -154,7 +154,7 @@
@input="normalizeIdCardNo"
/>
</el-form-item>
- <el-form-item label="联系方式:" prop="mobile">
+ <el-form-item label="联系方式:" prop="mobile" required>
<el-input
v-model="form.mobile"
placeholder="请输入"
@@ -163,13 +163,13 @@
@input="normalizeMobile"
/>
</el-form-item>
- <el-form-item label="联系地址:" prop="contactAddress">
+ <el-form-item label="联系地址:" prop="contactAddress" required>
<el-input v-model="form.contactAddress" placeholder="请输入" />
</el-form-item>
- <el-form-item label="工作地址:" prop="workAddress">
+ <el-form-item label="工作地址:" prop="workAddress" required>
<el-input v-model="form.workAddress" placeholder="请输入" />
</el-form-item>
- <el-form-item label="性别:" prop="gender">
+ <el-form-item label="性别:" prop="gender" required>
<el-select v-model="form.gender" placeholder="选择">
<el-option label="男" :value="1" />
<el-option label="女" :value="2" />
@@ -181,7 +181,7 @@
<section class="detail-block">
<h3>人才卡管理</h3>
- <el-form-item label="人才等级/卡号" prop="talentCardNo">
+ <el-form-item label="人才等级/卡号" prop="talentCardNo" required>
<div class="card-manage-panel">
<div class="talent-card-stack">
<button type="button" class="talent-card-preview create-card-preview" @click="openCardDialog">
@@ -198,7 +198,7 @@
<section class="detail-block">
<h3>专属服务人员</h3>
- <el-form-item label="专属服务人员" prop="serviceStaff" class="service-staff-form-item">
+ <el-form-item label="专属服务人员" prop="serviceStaff" class="service-staff-form-item" required>
<div class="service-staff-panel">
<el-select v-model="serviceStaff" clearable placeholder="请选择专属服务人员" @change="formRef?.validateField('serviceStaff')">
<el-option
@@ -224,7 +224,7 @@
:close-on-click-modal="false"
>
<el-form ref="cardFormRef" :model="cardForm" :rules="cardRules" label-width="100px">
- <el-form-item label="人才等级" prop="talentLevel">
+ <el-form-item label="人才等级" prop="talentLevel" required>
<div class="talent-level-options">
<button
v-for="level in levels"
@@ -245,7 +245,7 @@
</el-radio-group>
<div class="talent-card-number-field">
- <el-form-item label="人才卡号" prop="talentCardNo">
+ <el-form-item label="人才卡号" prop="talentCardNo" required>
<el-input
v-model="cardForm.talentCardNo"
maxlength="64"
diff --git a/talent-card-admin/src/views/TalentDetailView.vue b/talent-card-admin/src/views/TalentDetailView.vue
index 9862536..50af126 100644
--- a/talent-card-admin/src/views/TalentDetailView.vue
+++ b/talent-card-admin/src/views/TalentDetailView.vue
@@ -339,7 +339,7 @@
:close-on-click-modal="false"
>
<el-form ref="levelFormRef" :model="{ talentLevel: pendingTalentLevel }" :rules="levelRules" label-width="88px">
- <el-form-item label="人才等级" prop="talentLevel">
+ <el-form-item label="人才等级" prop="talentLevel" required>
<div class="talent-level-options">
<button
v-for="level in talentLevels"
diff --git a/talent-card-admin/src/views/TalentNewsEditView.vue b/talent-card-admin/src/views/TalentNewsEditView.vue
index 77d9b0a..711b18f 100644
--- a/talent-card-admin/src/views/TalentNewsEditView.vue
+++ b/talent-card-admin/src/views/TalentNewsEditView.vue
@@ -128,16 +128,16 @@
<section class="news-publish-panel">
<h3>资讯内容</h3>
- <el-form ref="formRef" class="news-publish-form" :model="form" :rules="rules" label-width="72px" label-position="left">
- <el-form-item label="资讯标题" prop="title">
+ <el-form ref="formRef" class="news-publish-form" :model="form" :rules="rules" label-width="96px" label-position="left">
+ <el-form-item label="资讯标题" prop="title" required>
<el-input v-model="form.title" placeholder="资讯标题" />
</el-form-item>
- <el-form-item label="资讯标签" prop="summary">
- <el-input v-model="form.summary" placeholder="请输入资讯标签" />
+ <el-form-item label="资讯标签" prop="summary" required>
+ <el-input v-model="form.summary" placeholder="资讯标签" />
</el-form-item>
- <el-form-item class="news-editor-form-item" label="资讯内容" prop="content">
+ <el-form-item class="news-editor-form-item" label="资讯内容" prop="content" required>
<AdminRichTextEditor v-model="form.content" placeholder="请输入资讯内容" @update:model-value="validateContent" />
</el-form-item>
@@ -145,11 +145,11 @@
<el-switch v-model="form.pinned" />
</el-form-item>
- <el-form-item label="封面图" prop="coverUrl">
- <el-upload accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover">
- <el-button>上传图片</el-button>
+ <el-form-item label="封面图" prop="coverUrl" required>
+ <el-upload class="article-cover-upload" accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover">
+ <el-image v-if="form.coverUrl" class="article-cover-preview" :src="form.coverUrl" fit="cover" />
+ <div v-else class="article-cover-upload-card"><span>+</span><small>上传图片</small></div>
</el-upload>
- <el-image v-if="form.coverUrl" class="news-cover-preview" :src="form.coverUrl" fit="cover" />
</el-form-item>
<el-form-item>
diff --git a/talent-card-admin/src/views/TalentNewsPublishView.vue b/talent-card-admin/src/views/TalentNewsPublishView.vue
index 16d31d5..5de2827 100644
--- a/talent-card-admin/src/views/TalentNewsPublishView.vue
+++ b/talent-card-admin/src/views/TalentNewsPublishView.vue
@@ -81,16 +81,16 @@
<section class="news-publish-panel">
<h3>资讯内容</h3>
- <el-form ref="formRef" class="news-publish-form" :model="form" :rules="rules" label-width="72px" label-position="left">
- <el-form-item label="资讯标题" prop="title">
+ <el-form ref="formRef" class="news-publish-form" :model="form" :rules="rules" label-width="96px" label-position="left">
+ <el-form-item label="资讯标题" prop="title" required>
<el-input v-model="form.title" placeholder="资讯标题" />
</el-form-item>
- <el-form-item label="资讯标签" prop="summary">
- <el-input v-model="form.summary" placeholder="请输入资讯标签" />
+ <el-form-item label="资讯标签" prop="summary" required>
+ <el-input v-model="form.summary" placeholder="资讯标签" />
</el-form-item>
- <el-form-item class="news-editor-form-item" label="资讯内容" prop="content">
+ <el-form-item class="news-editor-form-item" label="资讯内容" prop="content" required>
<AdminRichTextEditor v-model="form.content" placeholder="请输入资讯内容" @update:model-value="validateContent" />
</el-form-item>
@@ -98,11 +98,11 @@
<el-switch v-model="form.pinned" />
</el-form-item>
- <el-form-item label="封面图" prop="coverUrl">
- <el-upload accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover">
- <el-button>上传图片</el-button>
+ <el-form-item label="封面图" prop="coverUrl" required>
+ <el-upload class="article-cover-upload" accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover">
+ <el-image v-if="form.coverUrl" class="article-cover-preview" :src="form.coverUrl" fit="cover" />
+ <div v-else class="article-cover-upload-card"><span>+</span><small>上传图片</small></div>
</el-upload>
- <el-image v-if="form.coverUrl" class="news-cover-preview" :src="form.coverUrl" fit="cover" />
</el-form-item>
<el-form-item>
diff --git a/talent-card-admin/src/views/TalentPolicyEditView.vue b/talent-card-admin/src/views/TalentPolicyEditView.vue
index c094798..71db82b 100644
--- a/talent-card-admin/src/views/TalentPolicyEditView.vue
+++ b/talent-card-admin/src/views/TalentPolicyEditView.vue
@@ -41,6 +41,6 @@
</script>
<template>
<div class="policy-edit-page" v-loading="loading"><section class="news-meta-panel"><h3>发布信息</h3><div class="news-meta-grid"><div><span>发布时间</span><strong>{{ createdAt }}</strong></div><div><span>修改时间</span><strong>{{ updatedAt }}</strong></div></div></section><section class="policy-publish-panel"><h3>政策内容</h3><el-form ref="formRef" class="policy-publish-form" :model="form" :rules="rules" label-width="88px">
- <el-form-item label="政策标题" prop="title"><el-input v-model="form.title" /></el-form-item><el-form-item label="政策标签" prop="subtitle"><el-input v-model="form.subtitle" placeholder="请输入政策标签" /></el-form-item><el-form-item class="news-editor-form-item" label="政策内容" prop="content"><AdminRichTextEditor v-model="form.content" @update:model-value="validateContent" /></el-form-item><el-form-item label="封面图" prop="coverUrl"><el-upload accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover"><el-button>上传图片</el-button></el-upload><el-image v-if="form.coverUrl" class="policy-cover-preview" :src="form.coverUrl" fit="cover" /></el-form-item><el-form-item label="排序" prop="sortNo"><el-input v-model.number="form.sortNo" type="number" placeholder="请输入非负整数" /></el-form-item><el-form-item><el-button type="primary" @click="save">保存</el-button></el-form-item>
+ <el-form-item label="政策标题" prop="title"><el-input v-model="form.title" /></el-form-item><el-form-item label="政策标签" prop="subtitle"><el-input v-model="form.subtitle" placeholder="请输入政策标签" /></el-form-item><el-form-item class="news-editor-form-item" label="政策内容" prop="content"><AdminRichTextEditor v-model="form.content" @update:model-value="validateContent" /></el-form-item><el-form-item label="封面图" prop="coverUrl"><el-upload class="article-cover-upload" accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover"><el-image v-if="form.coverUrl" class="article-cover-preview" :src="form.coverUrl" fit="cover" /><div v-else class="article-cover-upload-card"><span>+</span><small>上传图片</small></div></el-upload></el-form-item><el-form-item label="排序" prop="sortNo"><el-input v-model.number="form.sortNo" type="number" placeholder="请输入非负整数" /></el-form-item><el-form-item><el-button type="primary" @click="save">保存</el-button></el-form-item>
</el-form></section></div>
</template>
diff --git a/talent-card-admin/src/views/TalentPolicyPublishView.vue b/talent-card-admin/src/views/TalentPolicyPublishView.vue
index dc6ed4e..1eb13eb 100644
--- a/talent-card-admin/src/views/TalentPolicyPublishView.vue
+++ b/talent-card-admin/src/views/TalentPolicyPublishView.vue
@@ -51,11 +51,11 @@
</script>
<template>
<div class="policy-publish-page"><section class="policy-publish-panel"><h3>政策内容</h3><el-form ref="formRef" class="policy-publish-form" :model="form" :rules="rules" label-width="88px">
- <el-form-item label="政策标题" prop="title"><el-input v-model="form.title" placeholder="请输入政策标题" /></el-form-item>
- <el-form-item label="政策标签" prop="subtitle"><el-input v-model="form.subtitle" placeholder="请输入政策标签" /></el-form-item>
- <el-form-item class="news-editor-form-item" label="政策内容" prop="content"><AdminRichTextEditor v-model="form.content" @update:model-value="validateContent" /></el-form-item>
- <el-form-item label="封面图" prop="coverUrl"><el-upload accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover"><el-button>上传图片</el-button></el-upload><el-image v-if="form.coverUrl" class="policy-cover-preview" :src="form.coverUrl" fit="cover" /></el-form-item>
- <el-form-item label="排序" prop="sortNo"><el-input v-model.number="form.sortNo" type="number" placeholder="请输入非负整数" /></el-form-item>
+ <el-form-item label="政策标题" prop="title" required><el-input v-model="form.title" placeholder="请输入政策标题" /></el-form-item>
+ <el-form-item label="政策标签" prop="subtitle" required><el-input v-model="form.subtitle" placeholder="请输入政策标签" /></el-form-item>
+ <el-form-item class="news-editor-form-item" label="政策内容" prop="content" required><AdminRichTextEditor v-model="form.content" @update:model-value="validateContent" /></el-form-item>
+ <el-form-item label="封面图" prop="coverUrl" required><el-upload class="article-cover-upload" accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover"><el-image v-if="form.coverUrl" class="article-cover-preview" :src="form.coverUrl" fit="cover" /><div v-else class="article-cover-upload-card"><span>+</span><small>上传图片</small></div></el-upload></el-form-item>
+ <el-form-item label="排序" prop="sortNo" required><el-input v-model.number="form.sortNo" type="number" placeholder="请输入非负整数" /></el-form-item>
<el-form-item><el-button type="primary" @click="save">保存</el-button></el-form-item>
</el-form></section></div>
</template>
diff --git a/talent-card-admin/src/views/UpgradeApplicationDetailView.vue b/talent-card-admin/src/views/UpgradeApplicationDetailView.vue
index b5d3f80..aedde8c 100644
--- a/talent-card-admin/src/views/UpgradeApplicationDetailView.vue
+++ b/talent-card-admin/src/views/UpgradeApplicationDetailView.vue
@@ -90,10 +90,10 @@
<div>申请时间:<strong>{{ formatDate(detail.applyTime) }}</strong></div>
<div class="full-line">申请理由:<strong>{{ text(detail.applyReason) }}</strong></div>
</div>
- <div v-for="honor in (detail.honors || [])" :key="honor.id" class="honor-block">
+ <div v-for="honor in (detail.honors || [])" :key="honor.id" class="upgrade-detail-grid honor-block">
<div>新获得的荣誉称号/大奖项:<strong>{{ text(honor.honorName) }}</strong></div>
<div>获得时间:<strong>{{ text(honor.obtainedDate) }}</strong></div>
- <div class="honor-images">
+ <div class="honor-images full-line">
<el-image
v-if="imageUrl(honor)"
:src="resolveImageUrl(imageUrl(honor))"
@@ -125,8 +125,8 @@
.upgrade-detail-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px 36px; padding: 18px; background: #f5f6f8; color: #606266; }
.upgrade-detail-grid strong { color: #303133; font-weight: 500; }
.full-line { grid-column: 1 / -1; }
-.honor-block { margin-top: 14px; padding: 16px; background: #f5f6f8; color: #606266; }
-.honor-images { display: flex; gap: 20px; min-height: 130px; margin-top: 14px; }
+.honor-block { margin-top: 14px; }
+.honor-images { display: flex; gap: 20px; align-items: flex-start; }
.honor-image { width: 150px; height: 110px; background: #e9eaec; cursor: zoom-in; }
.honor-images span { display: flex; align-items: center; justify-content: center; width: 150px; height: 110px; color: #909399; background: #e9eaec; }
.upgrade-detail-actions { display: flex; align-items: center; gap: 12px; padding: 18px 0; }
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessController.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessController.java
index 27deace..f032d4b 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessController.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessController.java
@@ -36,6 +36,11 @@
return ApiResponse.success(adminBusinessService.dashboard());
}
+ @GetMapping("/pending-notices")
+ public ApiResponse<java.util.List<Map<String, Object>>> pendingNotices() {
+ return ApiResponse.success(adminBusinessService.pendingNotices());
+ }
+
@GetMapping("/news")
public ApiResponse<PageResult<Map<String, Object>>> news(
@RequestParam(required = false) String title,
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 f18842c..46f596c 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
@@ -2,9 +2,12 @@
import java.math.BigDecimal;
import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.ZoneId;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -228,21 +231,7 @@
StringBuilder sql = new StringBuilder("""
select m.id, m.merchant_name, m.merchant_type, m.contact_name, m.contact_mobile,
m.logo_url, m.cover_url, m.address, m.is_recommend, m.sort_no, m.status,
- case
- when m.status <> 1 then '休息中'
- when lower(coalesce(m.business_status, 'open')) in ('closed', '休息中') then '休息中'
- when exists (
- select 1
- from merchant_business_hours h
- join merchant_store s on s.id = h.store_id and s.status = 1 and s.deleted = 0
- where h.merchant_id = m.id
- and (
- (h.start_time <= h.end_time and current_time() >= h.start_time and current_time() < h.end_time)
- or (h.start_time > h.end_time and (current_time() >= h.start_time or current_time() < h.end_time))
- )
- ) then '营业中'
- else '休息中'
- end as business_status, m.created_at,
+ m.created_at,
(select count(*) from merchant_product p where p.merchant_id = m.id and p.deleted = 0) as product_count,
(select count(*) from benefit_order o where o.merchant_id = m.id and o.deleted = 0) as order_count,
0 as reservation_count
@@ -290,7 +279,45 @@
args.add(createdEnd);
}
sql.append(" order by m.is_recommend desc, m.sort_no desc, m.id desc");
- return page(sql.toString(), countSql.toString(), page, size, args.toArray());
+ PageResult<Map<String, Object>> result = page(sql.toString(), countSql.toString(), page, size, args.toArray());
+ applyBusinessStatus(result.records());
+ return result;
+ }
+
+ @Transactional(readOnly = true)
+ public List<Map<String, Object>> pendingNotices() {
+ List<Map<String, Object>> notices = new ArrayList<>();
+ addPendingNotice(notices, "product-audit", "团单审核", "您有 %d 条团单待审核", "/products",
+ "select count(*) as pending_count, max(coalesce(submit_time, created_at)) as latest_time from merchant_product where deleted = 0 and audit_status = 1");
+ addPendingNotice(notices, "family-card", "家属卡申请", "您有 %d 条家属卡申请待审核", "/family-card-applications",
+ "select count(*) as pending_count, max(apply_time) as latest_time from family_card_application where deleted = 0 and status = '待审核'");
+ addPendingNotice(notices, "upgrade", "升级申请", "您有 %d 条升级申请待审核", "/upgrade-applications",
+ "select count(*) as pending_count, max(apply_time) as latest_time from upgrade_application where deleted = 0 and status = '待审核'");
+ addPendingNotice(notices, "merchant-onboarding", "商户进件", "您有 %d 条商户进件待审核", "/merchant-settlement?kind=onboardings",
+ "select count(*) as pending_count, max(created_at) as latest_time from merchant_onboarding_application where status = 'pending'");
+ addPendingNotice(notices, "merchant-bank-card", "银行卡申请", "您有 %d 条银行卡申请待审核", "/merchant-settlement?kind=bank-cards",
+ "select count(*) as pending_count, max(created_at) as latest_time from merchant_bank_card where status = 'pending'");
+ addPendingNotice(notices, "merchant-withdrawal", "提现审核", "您有 %d 条提现申请待审核", "/merchant-settlement?kind=withdrawals",
+ "select count(*) as pending_count, max(created_at) as latest_time from merchant_withdrawal where status = 'pending'");
+ addPendingNotice(notices, "medical-reservation", "体检预约", "您有 %d 条体检预约待处理", "/medical-reservations",
+ "select count(*) as pending_count, max(submitted_at) as latest_time from service_application where deleted = 0 and application_type = 'MEDICAL_RESERVATION' and status = '待处理'");
+ addPendingNotice(notices, "medical-application", "就医申请", "您有 %d 条就医申请待处理", "/medical-applications",
+ "select count(*) as pending_count, max(submitted_at) as latest_time from service_application where deleted = 0 and application_type = 'MEDICAL_APPLICATION' and status = '待处理'");
+ notices.sort((left, right) -> String.valueOf(right.get("time")).compareTo(String.valueOf(left.get("time"))));
+ return notices;
+ }
+
+ private void addPendingNotice(List<Map<String, Object>> notices, String id, String category, String template, String path, String sql) {
+ Map<String, Object> summary = DatabaseRows.normalize(jdbcTemplate.queryForMap(sql));
+ long count = summary.get("pendingCount") instanceof Number number ? number.longValue() : 0;
+ if (count == 0) return;
+ Map<String, Object> notice = new LinkedHashMap<>();
+ notice.put("id", id);
+ notice.put("category", category);
+ notice.put("content", template.formatted(count));
+ notice.put("time", summary.get("latestTime"));
+ notice.put("path", path);
+ notices.add(notice);
}
@Transactional(readOnly = true)
@@ -335,7 +362,7 @@
@Transactional
public Map<String, Object> createMerchant(MerchantCreateRequest request) {
validateMerchantAddress(request.merchantType(), request.address());
- validateScenicMerchantCreate(request);
+ validateRequiredMerchantCreateFields(request);
jdbcTemplate.update("""
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,
@@ -396,8 +423,8 @@
return MERCHANT_APP_LOGIN_TYPES.contains(merchantType);
}
- private void validateScenicMerchantCreate(MerchantCreateRequest request) {
- if (!"景区服务".equals(request.merchantType())) {
+ private void validateRequiredMerchantCreateFields(MerchantCreateRequest request) {
+ if (!Set.of("景区服务", "餐饮服务").contains(request.merchantType())) {
return;
}
if (request.businessHours() == null || request.businessHours().isEmpty()
@@ -907,6 +934,41 @@
return new PageResult<>(records, safePage, safeSize, total == null ? 0 : total);
}
+ private void applyBusinessStatus(List<Map<String, Object>> merchants) {
+ if (merchants.isEmpty()) return;
+ List<Long> merchantIds = merchants.stream().map(row -> Long.valueOf(String.valueOf(row.get("id")))).toList();
+ String placeholders = String.join(",", java.util.Collections.nCopies(merchantIds.size(), "?"));
+ List<Map<String, Object>> periods = DatabaseRows.normalize(jdbcTemplate.queryForList("""
+ select h.merchant_id, h.start_time, h.end_time
+ from merchant_business_hours h
+ join merchant_store s on s.id = h.store_id
+ where s.status = 1 and s.deleted = 0 and h.merchant_id in (""" + placeholders + ")", merchantIds.toArray()));
+ LocalTime now = LocalTime.now(ZoneId.of("Asia/Shanghai"));
+ Set<Long> openMerchantIds = new HashSet<>();
+ for (Map<String, Object> period : periods) {
+ if (isWithinBusinessPeriod(now, String.valueOf(period.get("startTime")), String.valueOf(period.get("endTime")))) {
+ openMerchantIds.add(Long.valueOf(String.valueOf(period.get("merchantId"))));
+ }
+ }
+ for (Map<String, Object> merchant : merchants) {
+ Long merchantId = Long.valueOf(String.valueOf(merchant.get("id")));
+ boolean enabled = "1".equals(String.valueOf(merchant.get("status")));
+ merchant.put("businessStatus", enabled && openMerchantIds.contains(merchantId) ? "营业中" : "休息中");
+ }
+ }
+
+ private boolean isWithinBusinessPeriod(LocalTime now, String startValue, String endValue) {
+ try {
+ LocalTime start = LocalTime.parse(startValue);
+ LocalTime end = LocalTime.parse(endValue);
+ return start.isBefore(end)
+ ? !now.isBefore(start) && now.isBefore(end)
+ : !now.isBefore(start) || now.isBefore(end);
+ } catch (DateTimeParseException ignored) {
+ return false;
+ }
+ }
+
private void ensureUpdated(int updated) {
if (updated == 0) {
throw new BusinessException(ErrorCode.NOT_FOUND, "Data not found");
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementController.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementController.java
index d160e5e..ad5c05a 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementController.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementController.java
@@ -24,6 +24,7 @@
@PatchMapping("/roles/{id}/status") public ApiResponse<Map<String,Object>> updateRoleStatus(@PathVariable Long id, @Valid @RequestBody AdminRoleStatusRequest request) { return ApiResponse.success(service.updateRoleStatus(id, request.status())); }
@DeleteMapping("/roles/{id}") public ApiResponse<Map<String,Object>> deleteRole(@PathVariable Long id) { return ApiResponse.success(service.deleteRole(id)); }
@GetMapping("/staff") public ApiResponse<List<Map<String,Object>>> staff() { return ApiResponse.success(service.staff()); }
+ @GetMapping("/staff/account-available") public ApiResponse<Map<String,Object>> staffAccountAvailable(@RequestParam String account) { return ApiResponse.success(service.staffAccountAvailable(account)); }
@PostMapping("/staff") public ApiResponse<Map<String,Object>> createStaff(@Valid @RequestBody AdminStaffRequest request) { return ApiResponse.success(service.createStaff(request)); }
@PutMapping("/staff/{id}") public ApiResponse<Map<String,Object>> updateStaff(@PathVariable Long id, @Valid @RequestBody AdminStaffRequest request) { return ApiResponse.success(service.updateStaff(id, request)); }
@PatchMapping("/staff/{id}/password") public ApiResponse<Map<String,Object>> resetStaffPassword(@PathVariable Long id, @RequestBody Map<String,String> body) { return ApiResponse.success(service.resetStaffPassword(id, body.get("password"))); }
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementService.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementService.java
index a57ec71..f523d0e 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementService.java
@@ -24,13 +24,14 @@
public List<Map<String, Object>> roles() { return rows("select id, role_name, role_code, remark, permission_codes, status, created_at from admin_role order by id"); }
@Transactional public Map<String, Object> createRole(AdminRoleRequest request) { String code = roleCode(request.roleCode()); jdbc.update("insert into admin_role(role_name,role_code,remark,permission_codes,status) values (?,?,?,?,?)", request.roleName().trim(), code, request.remark(), permissions(request.permissionCodes()), request.status()); return one("select id, role_name, role_code, remark, permission_codes, status, created_at from admin_role where id=last_insert_id()"); }
- @Transactional public Map<String, Object> updateRole(Long id, AdminRoleRequest request) { String code = request.roleCode() == null || request.roleCode().isBlank() ? existingRoleCode(id) : request.roleCode().trim(); if (jdbc.update("update admin_role set role_name=?,role_code=?,remark=?,permission_codes=?,status=? where id=?", request.roleName().trim(), code, request.remark(), permissions(request.permissionCodes()), request.status(), id) == 0) throw new BusinessException(ErrorCode.NOT_FOUND, "Role does not exist"); return one("select id, role_name, role_code, remark, permission_codes, status, created_at from admin_role where id=?", id); }
+ @Transactional public Map<String, Object> updateRole(Long id, AdminRoleRequest request) { if ("platform_admin".equals(existingRoleCode(id))) throw new BusinessException(ErrorCode.CONFLICT, "超级管理员角色不可编辑"); String code = request.roleCode() == null || request.roleCode().isBlank() ? existingRoleCode(id) : request.roleCode().trim(); if (jdbc.update("update admin_role set role_name=?,role_code=?,remark=?,permission_codes=?,status=? where id=?", request.roleName().trim(), code, request.remark(), permissions(request.permissionCodes()), request.status(), id) == 0) throw new BusinessException(ErrorCode.NOT_FOUND, "Role does not exist"); return one("select id, role_name, role_code, remark, permission_codes, status, created_at from admin_role where id=?", id); }
@Transactional public Map<String, Object> updateRoleStatus(Long id, Byte status) { String code = existingRoleCode(id); if (status == 0 && "platform_admin".equals(code)) throw new BusinessException(ErrorCode.CONFLICT, "平台管理员角色不可禁用"); jdbc.update("update admin_role set status=? where id=?", status, id); return one("select id, role_name, role_code, remark, permission_codes, status, created_at from admin_role where id=?", id); }
@Transactional public Map<String, Object> deleteRole(Long id) { String code = existingRoleCode(id); if ("platform_admin".equals(code)) throw new BusinessException(ErrorCode.CONFLICT, "平台管理员角色不可删除"); if (Boolean.TRUE.equals(jdbc.queryForObject("select exists(select 1 from admin_operator where role_code=? and deleted=0)", Boolean.class, code))) throw new BusinessException(ErrorCode.CONFLICT, "该角色仍有员工使用,不能删除"); jdbc.update("delete from admin_role where id=?", id); return Map.of("id", id, "deleted", true); }
public List<Map<String, Object>> staff() { return rows("select id,account,display_name,mobile,role_code,position_name,customer_wechat,status,created_at from admin_operator where deleted=0 order by id"); }
- @Transactional public Map<String, Object> createStaff(AdminStaffRequest request) { if (request.password() == null || request.password().isBlank()) throw new BusinessException(ErrorCode.BAD_REQUEST, "A password is required for a new staff account"); jdbc.update("insert into admin_operator(account,password_hash,display_name,mobile,role_code,position_name,customer_wechat,status) values (?,?,?,?,?,?,?,?)", request.account().trim(), MerchantPasswordHasher.encode(request.password()), request.displayName().trim(), blankToNull(request.mobile()), request.roleCode(), request.positionName().trim(), blankToNull(request.customerWechat()), request.status()); return staffOne("last_insert_id()"); }
- @Transactional public Map<String, Object> updateStaff(Long id, AdminStaffRequest request) { if (!matchesExistingAccount(id, request.account())) throw new BusinessException(ErrorCode.CONFLICT, "登录账号不可修改"); if (request.status() == 0 && isDefaultAdmin(id)) throw new BusinessException(ErrorCode.CONFLICT, "超级管理员不可禁用"); String sql = request.password() == null || request.password().isBlank() ? "update admin_operator set account=?,display_name=?,mobile=?,role_code=?,position_name=?,customer_wechat=?,status=? where id=?" : "update admin_operator set account=?,password_hash=?,display_name=?,mobile=?,role_code=?,position_name=?,customer_wechat=?,status=? where id=?"; int updated = request.password() == null || request.password().isBlank() ? jdbc.update(sql, request.account().trim(), request.displayName().trim(), blankToNull(request.mobile()), request.roleCode(), request.positionName().trim(), blankToNull(request.customerWechat()), request.status(), id) : jdbc.update(sql, request.account().trim(), MerchantPasswordHasher.encode(request.password()), request.displayName().trim(), blankToNull(request.mobile()), request.roleCode(), request.positionName().trim(), blankToNull(request.customerWechat()), request.status(), id); if (updated == 0) throw new BusinessException(ErrorCode.NOT_FOUND, "Staff account does not exist"); return staffOne("?", id); }
+ public Map<String, Object> staffAccountAvailable(String account) { String normalized = account == null ? "" : account.trim(); if (!normalized.matches("^[A-Za-z0-9]{1,32}$")) return Map.of("available", false); Long count = jdbc.queryForObject("select count(*) from admin_operator where account=?", Long.class, normalized); return Map.of("available", count == null || count == 0); }
+ @Transactional public Map<String, Object> createStaff(AdminStaffRequest request) { if (request.password() == null || request.password().isBlank()) throw new BusinessException(ErrorCode.BAD_REQUEST, "A password is required for a new staff account"); if (!Boolean.TRUE.equals(staffAccountAvailable(request.account()).get("available"))) throw new BusinessException(ErrorCode.CONFLICT, "登录账号已存在,请更换"); jdbc.update("insert into admin_operator(account,password_hash,display_name,mobile,role_code,position_name,customer_wechat,status) values (?,?,?,?,?,?,?,?)", request.account().trim(), MerchantPasswordHasher.encode(request.password()), request.displayName().trim(), blankToNull(request.mobile()), request.roleCode(), request.positionName().trim(), blankToNull(request.customerWechat()), request.status()); return staffOne("last_insert_id()"); }
+ @Transactional public Map<String, Object> updateStaff(Long id, AdminStaffRequest request) { if (isDefaultAdmin(id)) throw new BusinessException(ErrorCode.CONFLICT, "超级管理员账号不可编辑"); if (!matchesExistingAccount(id, request.account())) throw new BusinessException(ErrorCode.CONFLICT, "登录账号不可修改"); String sql = request.password() == null || request.password().isBlank() ? "update admin_operator set account=?,display_name=?,mobile=?,role_code=?,position_name=?,customer_wechat=?,status=? where id=?" : "update admin_operator set account=?,password_hash=?,display_name=?,mobile=?,role_code=?,position_name=?,customer_wechat=?,status=? where id=?"; int updated = request.password() == null || request.password().isBlank() ? jdbc.update(sql, request.account().trim(), request.displayName().trim(), blankToNull(request.mobile()), request.roleCode(), request.positionName().trim(), blankToNull(request.customerWechat()), request.status(), id) : jdbc.update(sql, request.account().trim(), MerchantPasswordHasher.encode(request.password()), request.displayName().trim(), blankToNull(request.mobile()), request.roleCode(), request.positionName().trim(), blankToNull(request.customerWechat()), request.status(), id); if (updated == 0) throw new BusinessException(ErrorCode.NOT_FOUND, "Staff account does not exist"); return staffOne("?", id); }
@Transactional public Map<String, Object> resetStaffPassword(Long id, String password) { if (password == null || password.length() < 6 || password.length() > 64) throw new BusinessException(ErrorCode.BAD_REQUEST, "Password must be 6-64 characters"); int updated = jdbc.update("update admin_operator set password_hash=?, login_failed_count=0, locked_until=null where id=? and deleted=0", MerchantPasswordHasher.encode(password), id); if (updated == 0) throw new BusinessException(ErrorCode.NOT_FOUND, "Staff account does not exist"); return Map.of("id", id, "reset", true); }
@Transactional public Map<String, Object> deleteStaff(Long id) { int updated = jdbc.update("update admin_operator set deleted=1, status=0 where id=? and deleted=0 and account <> 'admin'", id); if (updated == 0) throw new BusinessException(ErrorCode.CONFLICT, "The account cannot be deleted or does not exist"); return Map.of("id", id, "deleted", true); }
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 a69b8d3..d6f012a 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
@@ -2,6 +2,7 @@
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Size;
import jakarta.validation.Valid;
@@ -28,6 +29,7 @@
String businessStatus,
Byte recommend,
@NotNull(message = "请输入排序")
+ @Min(value = 0, message = "排序只能输入非负整数")
Integer sortNo,
Double longitude,
Double latitude,
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantSettlementAdminService.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantSettlementAdminService.java
index 7ca49ae..09f73bb 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantSettlementAdminService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantSettlementAdminService.java
@@ -31,10 +31,10 @@
List<Map<String, Object>> rows = rows("select a.*, m.merchant_name, m.merchant_type, m.contact_mobile, s.store_name from merchant_onboarding_application a join merchant m on m.id = a.merchant_id join merchant_store s on s.id = a.store_id where a.id = ?", id);
if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "商户进件不存在");
Map<String, Object> result = rows.get(0);
- if ("individual".equalsIgnoreCase(String.valueOf(result.get("subjectType")))) {
- String encrypted = String.valueOf(result.get("subjectIdNoEncrypted"));
- if (!encrypted.isBlank() && !"null".equals(encrypted)) result.put("subjectIdNo", sensitiveDataCrypto.decrypt(encrypted));
- }
+ // 个人主体的历史记录可能使用 individual、personal 或中文类型值;
+ // 只要已保存加密身份证号,后台详情均应解密后完整展示。
+ String encrypted = String.valueOf(result.get("subjectIdNoEncrypted"));
+ if (!encrypted.isBlank() && !"null".equals(encrypted)) result.put("subjectIdNo", sensitiveDataCrypto.decrypt(encrypted));
return result;
}
@Transactional(readOnly = true)
@@ -42,7 +42,8 @@
PageResult<Map<String, Object>> result = page("select c.*, m.merchant_name, m.merchant_type, m.contact_mobile, s.store_name from merchant_bank_card c join merchant m on m.id = c.merchant_id join merchant_store s on s.id = c.store_id", "select count(*) from merchant_bank_card c join merchant m on m.id = c.merchant_id join merchant_store s on s.id = c.store_id", "c", page, size, merchantName, contactMobile, merchantType, status, startDate, endDate);
result.records().forEach(row -> {
String encrypted = String.valueOf(row.remove("cardNoEncrypted"));
- if (!encrypted.isBlank() && !"null".equals(encrypted)) row.put("cardMasked", sensitiveDataCrypto.decrypt(encrypted));
+ if (!encrypted.isBlank() && !"null".equals(encrypted)) row.put("cardNo", sensitiveDataCrypto.decrypt(encrypted));
+ else row.put("cardNo", row.get("cardMasked"));
row.put("cityName", bankCardCityName(row.get("cityCode")));
});
return result;
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java
index 0ad37bd..d83c8d9 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java
@@ -83,13 +83,13 @@
Map<String, Object> merchant = merchant(principal);
String orderNo = BusinessIdGenerator.next("MC");
jdbc.update("""
- insert into benefit_order(order_no, talent_id, card_id, merchant_id, store_id, order_type,
+ insert into benefit_order(order_no, talent_id, card_id, card_level_snapshot, merchant_id, store_id, order_type,
order_amount, subsidy_amount, base_pay_amount, pay_amount, coupon_id, coupon_amount, expires_at, price_version,
merchant_name_snapshot, merchant_address_snapshot, merchant_mobile_snapshot,
merchant_longitude_snapshot, merchant_latitude_snapshot, product_name_snapshot,
order_status, verify_status, merchant_verification_attempt_id, client_idempotency_key)
- values (?, ?, ?, ?, ?, 'STORE_CONSUME', ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?)
- """, orderNo, number(source.get("talentId")), number(source.get("cardId")), principal.merchantId(), principal.storeId(),
+ values (?, ?, ?, ?, ?, ?, 'STORE_CONSUME', ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?)
+ """, orderNo, number(source.get("talentId")), number(source.get("cardId")), string(source.get("cardLevel")), principal.merchantId(), principal.storeId(),
amounts.get("originalAmount"), amounts.get("subsidyAmount"), amounts.get("basePayAmount"), amounts.get("payAmount"), request.couponId(), amounts.get("couponAmount"),
LocalDateTime.now().plusMinutes(15), merchant.get("merchantName"), merchant.get("address"), merchant.get("contactMobile"),
merchant.get("longitude"), merchant.get("latitude"), "到店消费 - " + rule.get("ruleName"),
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkService.java b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkService.java
index 1e03d74..0c3a0a0 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkService.java
@@ -46,7 +46,7 @@
String recordSql = """
select o.id, o.order_no, tu.name as talent_name, tu.mobile as talent_mobile, tu.gender,
- coalesce(nullif(tu.talent_level, ''), tc.card_level) as card_level,
+ coalesce(nullif(o.card_level_snapshot, ''), tc.card_level) as card_level,
b.benefit_name, o.product_name_snapshot, o.order_type, o.order_amount, o.subsidy_amount, o.pay_amount,
o.order_status, o.verify_status, o.created_at, verified.verify_time
from benefit_order o
@@ -71,7 +71,7 @@
public Map<String, Object> order(String orderNo, Long merchantId, Long storeId) {
List<Map<String, Object>> rows = query("""
select o.id, o.order_no, o.talent_id, tu.name as talent_name, tu.mobile as talent_mobile, tu.gender,
- o.card_id, tc.card_no, coalesce(nullif(tu.talent_level, ''), tc.card_level) as card_level,
+ o.card_id, tc.card_no, coalesce(nullif(o.card_level_snapshot, ''), tc.card_level) as card_level,
o.benefit_id, b.benefit_name,
o.merchant_id, m.merchant_name, o.product_id, o.product_name_snapshot, p.rights_config,
o.order_type, o.order_amount, o.base_pay_amount, o.coupon_amount,
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/order/OrderService.java b/talent-card-server/src/main/java/com/talentcard/server/business/order/OrderService.java
index 379711e..285a92f 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/order/OrderService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/order/OrderService.java
@@ -20,6 +20,9 @@
@Service
public class OrderService {
+ private static final String ORDER_CARD_LEVEL_SQL =
+ "coalesce(nullif(o.card_level_snapshot, ''), tc.card_level, tu.talent_level)";
+
private final JdbcTemplate jdbcTemplate;
private final HuifuRefundService refundService;
@@ -35,14 +38,16 @@
long subsidyAmount = MoneyUtils.yuanToCent(request.subsidyAmount());
long payAmount = MoneyUtils.yuanToCent(request.payAmount());
LocalDateTime expiresAt = payAmount == 0 ? null : LocalDateTime.now().plusMinutes(15);
+ String cardLevel = jdbcTemplate.query("select card_level from talent_card where id = ? and deleted = 0", rs -> rs.next() ? rs.getString(1) : null, request.cardId());
jdbcTemplate.update("""
- insert into benefit_order(order_no, talent_id, card_id, benefit_id, merchant_id, store_id,
+ insert into benefit_order(order_no, talent_id, card_id, card_level_snapshot, benefit_id, merchant_id, store_id,
order_type, order_amount, subsidy_amount, base_pay_amount, pay_amount, expires_at, order_status, verify_status)
- values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
+ values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
""",
orderNo,
request.talentId(),
request.cardId(),
+ cardLevel,
request.benefitId(),
request.merchantId(),
request.storeId(),
@@ -110,7 +115,7 @@
public Map<String, Object> detail(String orderNo) {
List<Map<String, Object>> rows = query("""
select o.id, o.order_no, o.talent_id, tu.name as talent_name, tu.mobile as talent_mobile,
- o.card_id, tc.card_no, coalesce(tc.card_level, tu.talent_level) as talent_level, o.benefit_id, b.benefit_name,
+ o.card_id, tc.card_no, %s as talent_level, o.benefit_id, b.benefit_name,
o.product_id, o.product_name_snapshot, o.product_cover_url_snapshot,
p.product_name, p.cover_url as product_cover_url,
o.merchant_id, m.merchant_name, m.merchant_type, m.contact_mobile,
@@ -126,7 +131,7 @@
left join merchant_coupon mc on mc.id = o.coupon_id and mc.deleted = 0
left join merchant m on m.id = o.merchant_id
where o.deleted = 0 and o.order_no = ?
- """, orderNo);
+ """.formatted(ORDER_CARD_LEVEL_SQL), orderNo);
if (rows.isEmpty()) {
throw new BusinessException(ErrorCode.NOT_FOUND, "订单不存在");
}
@@ -137,7 +142,21 @@
where order_id = ?
order by id desc limit 1
""", detail.get("id"));
- detail.put("verifyTime", verificationRows.isEmpty() ? null : verificationRows.get(0).get("verifyTime"));
+ Object verifyTime = verificationRows.isEmpty() ? null : verificationRows.get(0).get("verifyTime");
+ // 到店消费在支付成功后即自动核销,历史数据未必有人工核销记录;此时以成功支付时间作为核销时间。
+ if (verifyTime == null
+ && "STORE_CONSUME".equalsIgnoreCase(String.valueOf(detail.get("orderType")))
+ && detail.get("verifyStatus") instanceof Number verifyStatus
+ && verifyStatus.intValue() == 1) {
+ List<Map<String, Object>> paidTransactions = query("""
+ select paid_at as verify_time
+ from payment_transaction
+ where order_no = ? and status = 'SUCCEEDED' and paid_at is not null
+ order by id desc limit 1
+ """, orderNo);
+ verifyTime = paidTransactions.isEmpty() ? null : paidTransactions.get(0).get("verifyTime");
+ }
+ detail.put("verifyTime", verifyTime);
Object itemId = detail.get("productId") == null ? detail.get("benefitId") : detail.get("productId");
Object itemName = firstNonNull(detail.get("productNameSnapshot"), detail.get("productName"), detail.get("benefitName"));
Object itemCover = firstNonNull(detail.get("productCoverUrlSnapshot"), detail.get("productCoverUrl"), detail.get("merchantCoverUrl"));
@@ -197,11 +216,7 @@
where.append(" and m.merchant_name like ?");
args.add("%" + merchantName.trim() + "%");
}
- if (cardLevel != null && !cardLevel.isBlank()) {
- where.append(" and (tc.card_level = ? or tu.talent_level = ?)");
- args.add(cardLevel);
- args.add(cardLevel);
- }
+ appendCardLevelFilter(where, args, cardLevel);
if (hasSubsidy != null) {
where.append(hasSubsidy == 1 ? " and o.subsidy_amount > 0" : " and o.subsidy_amount = 0");
}
@@ -225,16 +240,18 @@
select r.id, r.refund_no, r.order_no, r.order_type, r.paid_amount, r.refund_amount,
r.refundable_amount, r.refund_status, r.reason, r.operator_name, r.refund_time,
tu.name as talent_name, tu.mobile as talent_mobile,
- coalesce(tc.card_level, tu.talent_level) as talent_level, tc.card_no,
- m.merchant_name, m.merchant_type, coalesce(b.benefit_name, o.product_name_snapshot) as benefit_name,
+ %s as talent_level, tc.card_no,
+ m.merchant_name, m.merchant_type,
+ coalesce(nullif(o.product_name_snapshot, ''), nullif(p.product_name, ''), b.benefit_name) as benefit_name,
o.order_amount, o.subsidy_amount, o.pay_amount, o.created_at as order_time
from order_refund r
left join benefit_order o on o.id = r.order_id
left join talent_user tu on tu.id = r.talent_id
left join talent_card tc on tc.id = r.card_id
left join benefit b on b.id = r.benefit_id
+ left join merchant_product p on p.id = o.product_id and p.deleted = 0
left join merchant m on m.id = r.merchant_id
- """ + where + " order by r.id desc limit ? offset ?", pageArgs.toArray());
+ """.formatted(ORDER_CARD_LEVEL_SQL) + where + " order by r.id desc limit ? offset ?", pageArgs.toArray());
Long total = jdbcTemplate.queryForObject("""
select count(*)
from order_refund r
@@ -281,11 +298,7 @@
where.append(" and m.merchant_name like ?");
args.add("%" + merchantName.trim() + "%");
}
- if (cardLevel != null && !cardLevel.isBlank()) {
- where.append(" and (tc.card_level = ? or tu.talent_level = ?)");
- args.add(cardLevel);
- args.add(cardLevel);
- }
+ appendCardLevelFilter(where, args, cardLevel);
if ("未核销".equals(status)) {
return new PageResult<>(List.of(), safePage, safeSize, 0);
}
@@ -305,7 +318,7 @@
select r.id, r.verify_no, r.verify_type, r.verify_amount, r.subsidy_amount, r.verify_time,
r.remark, o.order_no, o.order_type,
tu.name as talent_name, tu.mobile as talent_mobile,
- coalesce(tc.card_level, tu.talent_level) as talent_level, tc.card_no,
+ %s as talent_level, tc.card_no,
m.merchant_name, m.merchant_type, b.benefit_name,
'已核销' as status
from verify_record r
@@ -314,7 +327,7 @@
left join talent_card tc on tc.id = r.card_id
left join benefit b on b.id = r.benefit_id
left join merchant m on m.id = r.merchant_id
- """ + where + " order by r.id desc limit ? offset ?", pageArgs.toArray());
+ """.formatted(ORDER_CARD_LEVEL_SQL) + where + " order by r.id desc limit ? offset ?", pageArgs.toArray());
Long total = jdbcTemplate.queryForObject("""
select count(*)
from verify_record r
@@ -375,11 +388,7 @@
where.append(" and m.merchant_name like ?");
args.add("%" + merchantName.trim() + "%");
}
- if (cardLevel != null && !cardLevel.isBlank()) {
- where.append(" and (tc.card_level = ? or tu.talent_level = ?)");
- args.add(cardLevel);
- args.add(cardLevel);
- }
+ appendCardLevelFilter(where, args, cardLevel);
if (hasSubsidy != null) {
where.append(hasSubsidy == 1 ? " and o.subsidy_amount > 0" : " and o.subsidy_amount = 0");
}
@@ -404,7 +413,7 @@
pageArgs.add((safePage - 1) * safeSize);
List<Map<String, Object>> records = query("""
select o.id, o.order_no, o.talent_id, tu.name as talent_name, tu.mobile as talent_mobile,
- coalesce(tc.card_level, tu.talent_level) as talent_level,
+ %s as talent_level,
o.card_id, tc.card_no, tc.card_type,
o.merchant_id, m.merchant_name, m.merchant_type,
o.benefit_id, b.benefit_name, o.order_type,
@@ -415,7 +424,7 @@
left join talent_card tc on tc.id = o.card_id
left join benefit b on b.id = o.benefit_id
left join merchant m on m.id = o.merchant_id
- """ + where + " order by o.id desc limit ? offset ?", pageArgs.toArray());
+ """.formatted(ORDER_CARD_LEVEL_SQL) + where + " order by o.id desc limit ? offset ?", pageArgs.toArray());
Long total = jdbcTemplate.queryForObject("""
select count(*)
from benefit_order o
@@ -439,6 +448,14 @@
return rows.get(0);
}
+ private void appendCardLevelFilter(StringBuilder where, List<Object> args, String cardLevel) {
+ if (cardLevel == null || cardLevel.isBlank()) {
+ return;
+ }
+ where.append(" and ").append(ORDER_CARD_LEVEL_SQL).append(" = ?");
+ args.add(cardLevel.trim());
+ }
+
private void appendOrderTypeFilter(StringBuilder where, List<Object> args, String alias, String orderType) {
if (orderType == null || orderType.isBlank()) {
return;
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/portal/AppOrderPortalService.java b/talent-card-server/src/main/java/com/talentcard/server/business/portal/AppOrderPortalService.java
index b8eac50..7f5ff34 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/portal/AppOrderPortalService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/portal/AppOrderPortalService.java
@@ -57,18 +57,19 @@
try {
jdbc.update("""
- insert into benefit_order(order_no, talent_id, card_id, merchant_id, order_type,
+ insert into benefit_order(order_no, talent_id, card_id, card_level_snapshot, merchant_id, order_type,
order_amount, subsidy_amount, base_pay_amount, pay_amount,
coupon_id, coupon_amount, expires_at, price_version,
merchant_name_snapshot, merchant_address_snapshot, merchant_mobile_snapshot,
merchant_longitude_snapshot, merchant_latitude_snapshot,
product_id, product_name_snapshot, product_cover_url_snapshot,
order_status, verify_status)
- values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
orderNo,
talentId,
talentCard.get("id"),
+ talentCard.get("cardLevel"),
request.merchantId(),
normalizeOrderType(product.get("productType")),
originalAmount,
diff --git a/talent-card-server/src/main/resources/application.yml b/talent-card-server/src/main/resources/application.yml
index d2ee9ac..05b62af 100644
--- a/talent-card-server/src/main/resources/application.yml
+++ b/talent-card-server/src/main/resources/application.yml
@@ -57,7 +57,7 @@
system-id: 6666000212191071
product-id: KAZX
merchant-id: 6666000212191071
- mini-program-app-id: wx6ae2cadfea28fce1
+ mini-program-app-id: wxcfee477acb54d179
private-key: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCcsR3R3nUFCv/4l6vczGk6j4UgZhuMm/fTs2MQQqFkIuoISd38TOiIMaLS3KLYL+D+qhUCps56svOK/3M/kdyXfPsBphJyXB5Ar7il8pJ6BDa38StzTrlRm+IXX+4diJvqxUimetjpx0OfnqRkNYQWc/ZVeoEdQbITqLyxogy0R3rkR/wnOTwxHBbkCYsDCxSon4W0SoM5fLs3o947INbPUbUsf0vL5l6NiqlV9YsZ3qAWvvPoPX4HI1s5maMiVKfUwe27P3ARW9n6n3tadBzbB6lrmAEcFoihXpZjNN9UaRKjYfYjTS9JRkDqRyYGhz5cHp9GthEksMvhLP84oEanAgMBAAECggEAKV6d5+Ya8TzpPiIVTxNtCPgRhu9OgL5BHVS0Z/ufjselEJEoGc/YaPTp71AvcnKZrcMMDwopY73RjTZKoFMLcbeesd296xGlLyD12GKGB88K/1+eQDBdv00v0UoTnRqPR9nUh5fRo09n7EG/V3HFsQPm2sSgSDArKwOW5jNJtWLsGIVCW8gg3RQ9YlZ5HW85neBnjBFnrMA9S/hxwZD336M8CiViIa0v9o5CS7qxAqXMzzoOC7Vr3hZ/X0i/5GrSH9DRO7ougSmP4F/zCEc2dViA0rOJ6m3OuS0osh5FulihOBOjI5uLETEt4K/7PuV9xOd4oAz0OBpJ2bhPf3YpGQKBgQDQdEcmnlULa3jTSrbr0vYA3iMpcls/8F8+I/QymMXHAUeVyc8XKuGzblx+/n3x8ZoAvdVg1ZAO5wgLh/P0BEL74iWhfHedjt8xcdk2R5U0RuOnx1TUTKrmLKRZ5gStNFnGlK4yVwpiNqHn/cr+gYYeZSPARFVi00WdXbqVbEEf2wKBgQDAbmo4uHrmHN5GYvt8lVmT9aha1IpKOOLenjd+Y/vKGJcvHzOVCHnOh8OGO/4p7CxOq0sshER713jW2cFGO3xpFEFpIc45pDQ8Ut0XuSoOjKtwPzid8vKX7A1If6FDLMNy7bYR1qqnIJNJnN/f3lsksX+es7QTuRShhA0X0mnEJQKBgBE5OlPHi1FZPO+AYeLUWH0z3YXkLAP5cXUp9Vhk5RqPUYHf3duVMHbp7HyVvIRMKSRciw/oNXH1xuVlA1QrMfmr8t4H8b4z+t8UABWLDpn2LYNEZg7wksJX9/Bxn/tLswtEZ6xDItmAmBoaDza+Lv2975lITCHRQRRClVMZUbw3AoGAVFrSs6SjM2aqRZ60ccuipgrFT3PmX10Kjt46o2w+/zvCYoTB/u3lP5UiMCDybPw7/Ax4+r3Mq6EsNrrkT95ZzlJzzhtkpL4Hq/4DL5gbCJ+/s4fzUMIkeFahCAvy7FieIS51ZuJ8kqDkEBv1w9RcjfxFXpOWOCMlGRyjBLNXbAUCgYEAjcTEuXctlGEjtO1MBaHc4hkkpiPG7gxV/Bq1XXLszv4LTaOfDEpjGGBpVYtb3URYrif/HzX8T1KHELvPziuqP2ghaJQzbztep1TGQ48A5VpOvHdA/w/+k4JJl0PyKBPrFXiHIw1if7rnvOLt27TymwXSaJquSz7Sz2+wXFxzKJk=
platform-public-key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgxOuXt1UEdZNPk6sntqiIlcs7e+0fZ3CVgKMV2Qv4ixwKmha5etL5WyIONmjoGEHePHpanYkXvm0kHkbbDkjpyHDFdEshuP1eGx5tRUTISRvXY/iFuD3xyeWSsvvjW6hS29OvqVCPcnBUC7jcRj15dJS2IpYR0iDGNS+/M0TyOFwRQME07At+qO+MSjKu4aRIjlglukjD7YbE2s35uV066nsBUCZUNpGPuju1pieUW4zyoIW6K70dyXCQy61oRI6JmA4JRCOJV1BE80N3GrLI4Pbq1C3ZBTgOPi5RWYNq1hfu807BDkPLc3dJKYt22Ea1jPJsg6e/B9Xbzh6JaUNAQIDAQAB
notify-url: ${HUIFU_NOTIFY_URL:http://221.182.45.100:11451/api/payments/huifu/notify}
@@ -67,9 +67,9 @@
wechat:
mini-program:
# The app id must be the same mini-program that invokes uni.login and uni.requestPayment.
- app-id: ${WECHAT_MINI_PROGRAM_APP_ID:wx6ae2cadfea28fce1}
+ app-id: ${WECHAT_MINI_PROGRAM_APP_ID:wxcfee477acb54d179}
# Keep this only in the deployment environment. It is required to exchange a wx.login code for an OpenID.
- app-secret: ${WECHAT_MINI_PROGRAM_APP_SECRET:7bf743b066523bbb2d6de80decabc15b}
+ app-secret: ${WECHAT_MINI_PROGRAM_APP_SECRET:716aae42f04d3c5f17904e894f1c1ee9}
management:
endpoints:
diff --git a/talent-card-server/src/main/resources/db/migration/V105__news_article_top_time_sorting.sql b/talent-card-server/src/main/resources/db/migration/V112__news_article_top_time_sorting.sql
similarity index 100%
rename from talent-card-server/src/main/resources/db/migration/V105__news_article_top_time_sorting.sql
rename to talent-card-server/src/main/resources/db/migration/V112__news_article_top_time_sorting.sql
diff --git a/talent-card-server/src/main/resources/db/migration/V108__ticket_package_level_rules.sql b/talent-card-server/src/main/resources/db/migration/V113__ticket_package_level_rules.sql
similarity index 100%
rename from talent-card-server/src/main/resources/db/migration/V108__ticket_package_level_rules.sql
rename to talent-card-server/src/main/resources/db/migration/V113__ticket_package_level_rules.sql
diff --git a/talent-card-server/src/main/resources/db/migration/V109__sync_premium_topic_merchant_mappings.sql b/talent-card-server/src/main/resources/db/migration/V114__sync_premium_topic_merchant_mappings.sql
similarity index 100%
rename from talent-card-server/src/main/resources/db/migration/V109__sync_premium_topic_merchant_mappings.sql
rename to talent-card-server/src/main/resources/db/migration/V114__sync_premium_topic_merchant_mappings.sql
diff --git a/talent-card-server/src/main/resources/db/migration/V115__benefit_order_card_level_snapshot.sql b/talent-card-server/src/main/resources/db/migration/V115__benefit_order_card_level_snapshot.sql
new file mode 100644
index 0000000..db09323
--- /dev/null
+++ b/talent-card-server/src/main/resources/db/migration/V115__benefit_order_card_level_snapshot.sql
@@ -0,0 +1,9 @@
+alter table benefit_order
+ add column card_level_snapshot varchar(64) default null comment '下单时人才卡等级快照' after card_id;
+
+update benefit_order o
+left join talent_card c on c.id = o.card_id
+set o.card_level_snapshot = c.card_level
+where (o.card_level_snapshot is null or o.card_level_snapshot = '')
+ and c.card_level is not null
+ and c.card_level <> '';
--
Gitblit v1.7.1