From 2b20145159c4c4eefc73fd449093e639812a64df Mon Sep 17 00:00:00 2001
From: shinnosuke <tantao@1hoper.com>
Date: 星期三, 19 八月 2026 14:35:36 +0800
Subject: [PATCH] 又又更新

---
 talent-card-admin/src/views/MedicalReservationView.vue                                               |    2 
 talent-card-admin/src/views/OrderView.vue                                                            |    2 
 talent-card-admin/src/views/MedicalApplicationView.vue                                               |    2 
 talent-card-admin/src/views/AppointmentManagementView.vue                                            |    2 
 talent-card-admin/src/views/BusinessManagementView.vue                                               |   12 +-
 talent-card-admin/src/views/StaffManagementView.vue                                                  |   10 +-
 talent-card-admin/src/router/index.ts                                                                |    9 +
 talent-card-admin/src/views/HotelVerificationView.vue                                                |    2 
 talent-card-admin/src/views/ProductAuditDetailView.vue                                               |    4 
 talent-card-admin/src/main.ts                                                                        |   13 ++
 talent-card-admin/src/views/MovieTicketView.vue                                                      |    6 
 talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementService.java    |    2 
 talent-card-admin/src/views/ChildCardView.vue                                                        |    2 
 talent-card-admin/src/views/MerchantView.vue                                                         |    6 
 talent-card-admin/src/views/ParkingPlateView.vue                                                     |    2 
 talent-card-admin/src/views/RoleFormView.vue                                                         |   44 ++++----
 talent-card-admin/src/views/RefundRecordView.vue                                                     |    2 
 talent-card-admin/src/views/FamilyCardApplicationDetailView.vue                                      |    4 
 talent-card-admin/src/views/ProductView.vue                                                          |    2 
 talent-card-admin/src/views/TalentListView.vue                                                       |    8 
 talent-card-admin/src/views/SpouseEmploymentView.vue                                                 |    2 
 talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthInterceptor.java |   57 +++++++++++
 talent-card-admin/src/views/UpgradeApplicationDetailView.vue                                         |    4 
 talent-card-admin/src/views/MerchantCreateView.vue                                                   |    5 
 talent-card-admin/src/views/ChildCardApplicationDetailView.vue                                       |    4 
 talent-card-admin/src/views/FamilyCardView.vue                                                       |    2 
 talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthService.java     |   19 +++
 talent-card-admin/src/style.css                                                                      |   60 ++++++++++--
 talent-card-admin/src/views/HotelReservationView.vue                                                 |    2 
 talent-card-admin/src/layouts/AdminLayout.vue                                                        |    5 
 talent-card-admin/src/views/FuelServiceView.vue                                                      |    2 
 31 files changed, 218 insertions(+), 80 deletions(-)

diff --git a/talent-card-admin/src/layouts/AdminLayout.vue b/talent-card-admin/src/layouts/AdminLayout.vue
index ee8b33b..d3246ff 100644
--- a/talent-card-admin/src/layouts/AdminLayout.vue
+++ b/talent-card-admin/src/layouts/AdminLayout.vue
@@ -18,6 +18,7 @@
 import { getAdminPendingNotices } from '../api/admin'
 import type { AdminPendingNotice } from '../api/admin'
 import { useAppStore } from '../stores/app'
+import { hasAdminPermission } from '../utils/permissions'
 
 interface MenuItem {
   path?: string
@@ -71,7 +72,7 @@
 ]
 
 const financeMenus: MenuItem[] = [
-  { path: '/merchant-settlement', label: '商户结算审核' },
+  { path: '/merchant-settlement', label: '提现管理' },
 ]
 
 const orderMenus: MenuItem[] = [
@@ -125,7 +126,7 @@
 const canAccess = (path?: string) => {
   if (!path || appStore.user.role === '平台管理') return true
   const required = routePermissions[path]
-  return !required || Boolean(appStore.user.permissionCodes?.includes('*') || appStore.user.permissionCodes?.includes(required))
+  return !required || hasAdminPermission(required, appStore)
 }
 const visibleMenuGroups = computed(() => menuGroups
   .map((group) => ({ ...group, items: group.items.filter((item) => canAccess(item.path)) }))
diff --git a/talent-card-admin/src/main.ts b/talent-card-admin/src/main.ts
index 622e155..8a797fc 100644
--- a/talent-card-admin/src/main.ts
+++ b/talent-card-admin/src/main.ts
@@ -6,5 +6,16 @@
 import './style.css'
 import App from './App.vue'
 import router from './router'
+import { hasAdminPermission } from './utils/permissions'
 
-createApp(App).use(createPinia()).use(router).use(ElementPlus, { locale: zhCn }).mount('#app')
+const app = createApp(App)
+app.use(createPinia()).use(router).use(ElementPlus, { locale: zhCn })
+app.directive('permission', {
+  mounted(el, binding) {
+    if (!hasAdminPermission(String(binding.value))) el.style.display = 'none'
+  },
+  updated(el, binding) {
+    el.style.display = hasAdminPermission(String(binding.value)) ? '' : 'none'
+  },
+})
+app.mount('#app')
diff --git a/talent-card-admin/src/router/index.ts b/talent-card-admin/src/router/index.ts
index d17b165..41e21d6 100644
--- a/talent-card-admin/src/router/index.ts
+++ b/talent-card-admin/src/router/index.ts
@@ -1,6 +1,7 @@
 import { createRouter, createWebHistory } from 'vue-router'
 import { useAppStore } from '../stores/app'
 import AdminLayout from '../layouts/AdminLayout.vue'
+import { hasAdminPermission, requiredRoutePermission } from '../utils/permissions'
 
 const familyCardView = () => import('../views/FamilyCardView.vue')
 const businessManagementView = () => import('../views/BusinessManagementView.vue')
@@ -110,7 +111,7 @@
           path: 'merchant-settlement',
           name: 'merchant-settlement',
           component: () => import('../views/MerchantSettlementView.vue'),
-          meta: { title: '商户结算审核' },
+          meta: { title: '提现管理' },
         },
         {
           path: 'merchant-onboardings/:id',
@@ -348,7 +349,11 @@
 router.beforeEach((to) => {
   const appStore = useAppStore()
   if (!to.meta.public && !appStore.token) {
-    return '/login'
+    return { path: '/login', query: { redirect: to.fullPath } }
+  }
+  if (!to.meta.public) {
+    const required = requiredRoutePermission(to.path, to.query as unknown as Record<string, unknown>)
+    if (required && !hasAdminPermission(required, appStore)) return '/dashboard'
   }
   return true
 })
diff --git a/talent-card-admin/src/style.css b/talent-card-admin/src/style.css
index cd2eee5..8186e05 100644
--- a/talent-card-admin/src/style.css
+++ b/talent-card-admin/src/style.css
@@ -2032,15 +2032,39 @@
 
 .coupon-edit-row {
   display: grid;
-  grid-template-columns: 72px minmax(0, 1fr);
-  gap: 14px;
-  align-items: center;
+  grid-template-columns: 82px minmax(0, 1fr);
+  gap: 6px;
+  align-items: start;
   margin-bottom: 18px;
 }
 
 .coupon-edit-row > label {
-  line-height: 18px;
+  height: 30px;
+  box-sizing: border-box;
+  padding: 0 0 0 12px;
+  line-height: 30px;
+  justify-content: flex-end;
   white-space: nowrap;
+}
+
+.merchant-coupon-edit-form .coupon-edit-row .el-form-item__label {
+  width: 82px !important;
+  max-width: none;
+  height: 30px;
+  box-sizing: border-box;
+  padding: 0 !important;
+  overflow: visible;
+  flex: 0 0 82px;
+  line-height: 30px;
+  white-space: nowrap !important;
+  word-break: keep-all;
+  overflow-wrap: normal;
+}
+
+.coupon-edit-row .el-form-item__content {
+  min-width: 0;
+  min-height: 30px;
+  margin-left: 0 !important;
 }
 
 .merchant-coupon-edit-form .el-input {
@@ -2050,16 +2074,23 @@
 .coupon-amount-control {
   display: flex;
   align-items: center;
-  gap: 8px;
+  gap: 12px;
 }
 
-.coupon-amount-control .el-input-number {
-  width: 200px;
+.coupon-amount-control .el-input {
+  width: 184px;
+}
+
+.merchant-coupon-edit-form .coupon-amount-control .el-input__wrapper {
+  min-height: 30px;
+  padding: 1px 11px;
+  background: #f5f6f8;
+  box-shadow: none;
 }
 
 .coupon-amount-control > span {
   color: #334155;
-  line-height: 28px;
+  line-height: 30px;
   white-space: nowrap;
 }
 
@@ -2085,8 +2116,10 @@
 .coupon-requirement-row .el-radio-group {
   display: flex;
   align-items: center;
+  flex-wrap: nowrap;
   gap: 0;
   height: 28px;
+  white-space: nowrap;
 }
 
 .coupon-requirement-row .el-radio {
@@ -2190,12 +2223,19 @@
 }
 
 .coupon-switch-row {
-  grid-template-columns: 72px 44px;
+  grid-template-columns: 130px 44px;
+  gap: 6px;
   margin-bottom: 18px;
 }
 
 .coupon-switch-row > label {
-  white-space: normal;
+  width: 130px;
+  box-sizing: border-box;
+  padding: 0 0 0 20px;
+  line-height: 30px;
+  justify-content: flex-start;
+  white-space: nowrap;
+  word-break: keep-all;
 }
 
 .coupon-switch-row .el-switch {
diff --git a/talent-card-admin/src/views/AppointmentManagementView.vue b/talent-card-admin/src/views/AppointmentManagementView.vue
index ef7eabd..eaf4af6 100644
--- a/talent-card-admin/src/views/AppointmentManagementView.vue
+++ b/talent-card-admin/src/views/AppointmentManagementView.vue
@@ -44,7 +44,7 @@
         <el-form-item label="卡片等级"><el-select v-model="filters.cardLevel" clearable placeholder="全部"><el-option v-for="level in cardLevels" :key="level" :label="level" :value="level" /></el-select></el-form-item>
         <el-form-item label="状态"><el-select v-model="filters.status" clearable placeholder="全部"><el-option v-for="status in statusOptions" :key="status" :label="status" :value="status" /></el-select></el-form-item>
         <el-form-item label="提交时间"><el-date-picker v-model="filters.submittedTime" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" /></el-form-item>
-        <div class="toolbar-actions"><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="Refresh" @click="resetFilters">重置</el-button><el-button :icon="Download" @click="download">导出</el-button></div>
+        <div class="toolbar-actions"><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="Refresh" @click="resetFilters">重置</el-button><el-button v-permission="'admin.appointment-management.export'" :icon="Download" @click="download">导出</el-button></div>
       </el-form>
       <el-table v-loading="loading" :data="rows" border empty-text="暂无预约申请数据">
         <el-table-column prop="applicationNo" label="申请单号" min-width="130" />
diff --git a/talent-card-admin/src/views/BusinessManagementView.vue b/talent-card-admin/src/views/BusinessManagementView.vue
index 3e07bd7..4087e7e 100644
--- a/talent-card-admin/src/views/BusinessManagementView.vue
+++ b/talent-card-admin/src/views/BusinessManagementView.vue
@@ -202,18 +202,18 @@
         </div>
       </el-form>
       <div class="business-fill-tools">
-        <el-button v-if="!isUpgrade" type="primary" :icon="Plus" @click="openCreate">新建</el-button>
-        <el-button :icon="Download" @click="download">下载</el-button>
+        <el-button v-if="!isUpgrade" v-permission="isRole ? 'admin.roles.create' : 'admin.staff.create'" type="primary" :icon="Plus" @click="openCreate">新建</el-button>
+        <el-button v-permission="isUpgrade ? 'admin.upgrade-applications.export' : isRole ? 'admin.roles.export' : 'admin.staff.export'" :icon="Download" @click="download">下载</el-button>
       </div>
       <el-table v-loading="loading" :data="pagedRows" border class="business-fill-table" :empty-text="`暂无${title}数据`">
         <el-table-column v-for="[key, label] in columns" :key="key" :prop="key" :label="label" min-width="130" show-overflow-tooltip />
         <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 && !(isRole && isPlatformAdminRole(row))" link type="primary" :icon="Edit" @click="openEdit(row)">编辑</el-button>
+            <el-button v-permission="isUpgrade ? 'admin.upgrade-applications.view' : isRole ? 'admin.roles.view' : 'admin.staff.view'" link type="primary" :icon="View" @click="view(row)">查看</el-button>
+            <el-button v-if="!isUpgrade && !(isRole && isPlatformAdminRole(row))" v-permission="isRole ? 'admin.roles.edit' : 'admin.staff.edit'" 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>
+              <el-button v-permission="'admin.roles.disable'" link type="primary" @click="changeRoleStatus(row)">{{ Number(row.status) === 1 ? '停用' : '启用' }}</el-button>
+              <el-button v-permission="'admin.roles.delete'" link type="danger" @click="removeRole(row)">删除</el-button>
             </template>
           </template>
         </el-table-column>
diff --git a/talent-card-admin/src/views/ChildCardApplicationDetailView.vue b/talent-card-admin/src/views/ChildCardApplicationDetailView.vue
index a3e9d7d..e8457f0 100644
--- a/talent-card-admin/src/views/ChildCardApplicationDetailView.vue
+++ b/talent-card-admin/src/views/ChildCardApplicationDetailView.vue
@@ -124,8 +124,8 @@
     </section>
 
     <div v-if="isPending" style="display: flex; gap: 12px; margin-top: 12px;">
-      <el-button type="primary" @click="approveApplication">审核通过</el-button>
-      <el-button type="primary" @click="rejectApplication">审核不通过</el-button>
+      <el-button v-permission="'admin.child-cards.audit'" type="primary" @click="approveApplication">审核通过</el-button>
+      <el-button v-permission="'admin.child-cards.audit'" type="primary" @click="rejectApplication">审核不通过</el-button>
     </div>
   </div>
 </template>
diff --git a/talent-card-admin/src/views/ChildCardView.vue b/talent-card-admin/src/views/ChildCardView.vue
index eba18e7..71f986d 100644
--- a/talent-card-admin/src/views/ChildCardView.vue
+++ b/talent-card-admin/src/views/ChildCardView.vue
@@ -163,7 +163,7 @@
       </el-form>
 
       <div class="family-card-table-tools">
-        <el-button :icon="Download" @click="download">下载</el-button>
+          <el-button v-permission="'admin.child-cards.export'" :icon="Download" @click="download">下载</el-button>
       </div>
 
       <el-table v-loading="loading" :data="rows" class="family-card-table" :empty-text="emptyText">
diff --git a/talent-card-admin/src/views/FamilyCardApplicationDetailView.vue b/talent-card-admin/src/views/FamilyCardApplicationDetailView.vue
index 4861c0d..4facc95 100644
--- a/talent-card-admin/src/views/FamilyCardApplicationDetailView.vue
+++ b/talent-card-admin/src/views/FamilyCardApplicationDetailView.vue
@@ -163,8 +163,8 @@
     </section>
 
     <div v-if="isPending" style="display: flex; gap: 12px; margin-top: 12px;">
-      <el-button type="primary" @click="approveApplication">通过</el-button>
-      <el-button type="primary" @click="rejectApplication">拒绝</el-button>
+      <el-button v-permission="'admin.family-card-applications.audit'" type="primary" @click="approveApplication">通过</el-button>
+      <el-button v-permission="'admin.family-card-applications.audit'" type="primary" @click="rejectApplication">拒绝</el-button>
     </div>
   </div>
 </template>
diff --git a/talent-card-admin/src/views/FamilyCardView.vue b/talent-card-admin/src/views/FamilyCardView.vue
index 2bfc305..7517bae 100644
--- a/talent-card-admin/src/views/FamilyCardView.vue
+++ b/talent-card-admin/src/views/FamilyCardView.vue
@@ -212,7 +212,7 @@
       </el-form>
 
       <div class="family-card-table-tools">
-        <el-button :icon="Download" @click="download">{{ isManagement ? '下载' : '导出' }}</el-button>
+        <el-button v-permission="isManagement ? 'admin.family-cards.export' : 'admin.family-card-applications.export'" :icon="Download" @click="download">{{ isManagement ? '下载' : '导出' }}</el-button>
       </div>
 
       <el-table v-if="isManagement" v-loading="loading" :data="rows" class="family-card-table" :empty-text="emptyText">
diff --git a/talent-card-admin/src/views/FuelServiceView.vue b/talent-card-admin/src/views/FuelServiceView.vue
index 503e35a..ddef4d5 100644
--- a/talent-card-admin/src/views/FuelServiceView.vue
+++ b/talent-card-admin/src/views/FuelServiceView.vue
@@ -132,7 +132,7 @@
       </el-form>
 
       <div class="fuel-service-table-tools">
-        <el-button :icon="Download" @click="download">下载</el-button>
+        <el-button v-permission="'admin.fuel-services.export'" :icon="Download" @click="download">下载</el-button>
       </div>
 
       <el-table
diff --git a/talent-card-admin/src/views/HotelReservationView.vue b/talent-card-admin/src/views/HotelReservationView.vue
index 5a923ad..caf5f04 100644
--- a/talent-card-admin/src/views/HotelReservationView.vue
+++ b/talent-card-admin/src/views/HotelReservationView.vue
@@ -133,7 +133,7 @@
       </el-form>
 
       <div class="hotel-reservation-table-tools">
-        <el-button :icon="Download" @click="download">下载</el-button>
+        <el-button v-permission="'admin.hotel-reservations.export'" :icon="Download" @click="download">下载</el-button>
       </div>
 
       <el-table
diff --git a/talent-card-admin/src/views/HotelVerificationView.vue b/talent-card-admin/src/views/HotelVerificationView.vue
index e4e6902..79cac98 100644
--- a/talent-card-admin/src/views/HotelVerificationView.vue
+++ b/talent-card-admin/src/views/HotelVerificationView.vue
@@ -126,7 +126,7 @@
       </el-form>
 
       <div class="order-table-tools">
-        <el-button :icon="Download" @click="download">下载</el-button>
+        <el-button v-permission="'admin.hotel-free-verifications.export'" :icon="Download" @click="download">下载</el-button>
       </div>
 
       <el-table
diff --git a/talent-card-admin/src/views/MedicalApplicationView.vue b/talent-card-admin/src/views/MedicalApplicationView.vue
index 52ca68d..bde8fbc 100644
--- a/talent-card-admin/src/views/MedicalApplicationView.vue
+++ b/talent-card-admin/src/views/MedicalApplicationView.vue
@@ -123,7 +123,7 @@
       </el-form>
 
       <div class="medical-reservation-table-tools">
-        <el-button :icon="Download" @click="download">下载</el-button>
+        <el-button v-permission="'admin.medical-applications.export'" :icon="Download" @click="download">下载</el-button>
       </div>
 
       <el-table
diff --git a/talent-card-admin/src/views/MedicalReservationView.vue b/talent-card-admin/src/views/MedicalReservationView.vue
index a732331..aaeb43a 100644
--- a/talent-card-admin/src/views/MedicalReservationView.vue
+++ b/talent-card-admin/src/views/MedicalReservationView.vue
@@ -127,7 +127,7 @@
       </el-form>
 
       <div class="medical-reservation-table-tools">
-        <el-button :icon="Download" @click="download">下载</el-button>
+        <el-button v-permission="'admin.medical-reservations.export'" :icon="Download" @click="download">下载</el-button>
       </div>
 
       <el-table
diff --git a/talent-card-admin/src/views/MerchantCreateView.vue b/talent-card-admin/src/views/MerchantCreateView.vue
index 378a15d..35e3bf1 100644
--- a/talent-card-admin/src/views/MerchantCreateView.vue
+++ b/talent-card-admin/src/views/MerchantCreateView.vue
@@ -3092,7 +3092,10 @@
             </el-form-item>
 
             <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>
+              <div class="coupon-amount-control">
+                <el-input v-model="couponEditForm.couponAmount" inputmode="decimal" maxlength="8" placeholder="请输入优惠券金额" />
+                <span>元</span>
+              </div>
             </el-form-item>
 
             <el-form-item class="coupon-edit-row coupon-requirement-row" label="使用要求" prop="fullAmount" :required="couponEditForm.useRequirement === 'full'">
diff --git a/talent-card-admin/src/views/MerchantView.vue b/talent-card-admin/src/views/MerchantView.vue
index 1feb33d..6a410ca 100644
--- a/talent-card-admin/src/views/MerchantView.vue
+++ b/talent-card-admin/src/views/MerchantView.vue
@@ -291,7 +291,7 @@
       </el-form>
 
       <div class="prototype-table-tools">
-        <el-button type="primary" :icon="Plus" @click="openCreate">新建</el-button>
+          <el-button v-permission="'admin.merchants.create'" type="primary" :icon="Plus" @click="openCreate">新建</el-button>
       </div>
 
       <el-table
@@ -332,11 +332,11 @@
         </el-table-column>
         <el-table-column label="操作" width="174" fixed="right" align="center">
           <template #default="{ row }">
-            <el-button link :type="row.status === 0 ? 'success' : 'danger'" @click="toggleStatus(row)">
+            <el-button v-permission="'admin.merchants.disable'" link :type="row.status === 0 ? 'success' : 'danger'" @click="toggleStatus(row)">
               {{ row.status === 0 ? '启用' : '禁用' }}
             </el-button>
             <el-button link type="primary" :icon="Goods" @click="openProducts(row)">团购管理</el-button>
-            <el-button link type="primary" :icon="Edit" @click="openEdit(row)">编辑</el-button>
+            <el-button v-permission="'admin.merchants.edit'" link type="primary" :icon="Edit" @click="openEdit(row)">编辑</el-button>
           </template>
         </el-table-column>
       </el-table>
diff --git a/talent-card-admin/src/views/MovieTicketView.vue b/talent-card-admin/src/views/MovieTicketView.vue
index d60bc69..d77eff0 100644
--- a/talent-card-admin/src/views/MovieTicketView.vue
+++ b/talent-card-admin/src/views/MovieTicketView.vue
@@ -437,7 +437,7 @@
 
         <div class="movie-ticket-package-tools">
           <el-button type="primary" :icon="Plus" @click="openCreate">新增</el-button>
-          <el-button :icon="Download" @click="download">下载</el-button>
+          <el-button v-permission="'admin.movie-tickets.export'" :icon="Download" @click="download">下载</el-button>
         </div>
 
         <el-table v-loading="loading" :data="rows" border class="movie-ticket-package-table" empty-text="暂无票包">
@@ -488,7 +488,7 @@
 
         <div class="movie-ticket-record-tools">
           <el-button type="primary" @click="openSupplement">补录发放</el-button>
-          <el-button :icon="Download" @click="download">下载</el-button>
+          <el-button v-permission="'admin.movie-tickets.export'" :icon="Download" @click="download">下载</el-button>
         </div>
 
         <el-table v-loading="loading" :data="rows" border class="movie-ticket-record-table" empty-text="暂无领取记录">
@@ -528,7 +528,7 @@
           </div>
         </el-form>
         <div class="movie-ticket-package-tools">
-          <el-button type="primary" @click="openImport(selectedPackage || {})">导入码</el-button>
+          <el-button v-permission="'admin.movie-tickets.import'" type="primary" @click="openImport(selectedPackage || {})">导入码</el-button>
           <el-button @click="openGenerate(selectedPackage || {})">批量生成</el-button>
           <span />
         </div>
diff --git a/talent-card-admin/src/views/OrderView.vue b/talent-card-admin/src/views/OrderView.vue
index d2a8b8b..f2467e2 100644
--- a/talent-card-admin/src/views/OrderView.vue
+++ b/talent-card-admin/src/views/OrderView.vue
@@ -203,7 +203,7 @@
       </el-form>
 
       <div class="order-table-tools">
-        <el-button :icon="Download" @click="download">下载</el-button>
+        <el-button v-permission="'admin.orders.export'" :icon="Download" @click="download">下载</el-button>
       </div>
 
       <el-table v-loading="loading" :data="orders" border class="order-list-table" empty-text="暂无订单数据">
diff --git a/talent-card-admin/src/views/ParkingPlateView.vue b/talent-card-admin/src/views/ParkingPlateView.vue
index eeef99d..8c28f22 100644
--- a/talent-card-admin/src/views/ParkingPlateView.vue
+++ b/talent-card-admin/src/views/ParkingPlateView.vue
@@ -34,7 +34,7 @@
         <el-form-item label="提交类型"><el-select v-model="filters.submissionType" clearable placeholder="全部"><el-option v-for="item in submissionTypes" :key="item" :label="item" :value="item" /></el-select></el-form-item>
         <el-form-item label="状态"><el-select v-model="filters.status" clearable placeholder="全部"><el-option v-for="status in statusOptions" :key="status" :label="status" :value="status" /></el-select></el-form-item>
         <el-form-item label="提交时间"><el-date-picker v-model="filters.submittedTime" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" /></el-form-item>
-        <div class="toolbar-actions"><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="Refresh" @click="resetFilters">重置</el-button><el-button :icon="Download" @click="download">导出</el-button></div>
+        <div class="toolbar-actions"><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="Refresh" @click="resetFilters">重置</el-button><el-button v-permission="'admin.parking-plates.export'" :icon="Download" @click="download">导出</el-button></div>
       </el-form>
       <el-table v-loading="loading" :data="rows" border empty-text="暂无车牌提交数据">
         <el-table-column label="提交单号" min-width="130"><template #default="{ row }">{{ text(row.submissionNo || row.applicationNo) }}</template></el-table-column>
diff --git a/talent-card-admin/src/views/ProductAuditDetailView.vue b/talent-card-admin/src/views/ProductAuditDetailView.vue
index 890965a..137ebbf 100644
--- a/talent-card-admin/src/views/ProductAuditDetailView.vue
+++ b/talent-card-admin/src/views/ProductAuditDetailView.vue
@@ -313,8 +313,8 @@
     </section>
 
     <div v-if="isPendingAudit" class="audit-action-row">
-      <el-button type="primary" :loading="auditing" @click="submitAudit(2)">审核通过</el-button>
-      <el-button type="primary" :loading="auditing" @click="submitAudit(3)">审核不通过</el-button>
+      <el-button v-permission="'admin.products.audit'" type="primary" :loading="auditing" @click="submitAudit(2)">审核通过</el-button>
+      <el-button v-permission="'admin.products.audit'" type="primary" :loading="auditing" @click="submitAudit(3)">审核不通过</el-button>
     </div>
     <div v-else class="audit-result-row">
       <span>审核结果:{{ auditResultText }}</span>
diff --git a/talent-card-admin/src/views/ProductView.vue b/talent-card-admin/src/views/ProductView.vue
index 37ab68d..747590a 100644
--- a/talent-card-admin/src/views/ProductView.vue
+++ b/talent-card-admin/src/views/ProductView.vue
@@ -210,7 +210,7 @@
         </el-table-column>
         <el-table-column label="操作" width="180" fixed="right" align="center">
           <template #default="{ row }">
-            <el-button link type="primary" @click="viewDetail(row)">查看</el-button>
+            <el-button v-permission="'admin.products.view'" link type="primary" @click="viewDetail(row)">查看</el-button>
           </template>
         </el-table-column>
       </el-table>
diff --git a/talent-card-admin/src/views/RefundRecordView.vue b/talent-card-admin/src/views/RefundRecordView.vue
index f77665c..492e107 100644
--- a/talent-card-admin/src/views/RefundRecordView.vue
+++ b/talent-card-admin/src/views/RefundRecordView.vue
@@ -174,7 +174,7 @@
       </el-form>
 
       <div class="order-table-tools">
-        <el-button :icon="Download" @click="download">下载</el-button>
+        <el-button v-permission="'admin.refunds.export'" :icon="Download" @click="download">下载</el-button>
       </div>
 
       <el-table v-loading="loading" :data="rows" border class="order-list-table refund-record-table" empty-text="暂无退款记录">
diff --git a/talent-card-admin/src/views/RoleFormView.vue b/talent-card-admin/src/views/RoleFormView.vue
index 098c390..b0c56d2 100644
--- a/talent-card-admin/src/views/RoleFormView.vue
+++ b/talent-card-admin/src/views/RoleFormView.vue
@@ -13,11 +13,11 @@
   {
     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: ['查看', '审核'] },
+      { 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: ['查看', '审核', '下载'] },
     ],
   },
   {
@@ -30,32 +30,32 @@
   {
     name: '订单管理',
     pages: [
-      { code: 'orders', name: '订单列表', actions: ['查看'] },
-      { code: 'refunds', name: '退款记录', actions: ['查看'] },
-      { code: 'hotel-free-verifications', name: '酒店免费核销记录', actions: ['查看'] },
+      { 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: '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: ['查看', '审核'] },
+      { 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: 'merchant-settlement', name: '提现管理', actions: ['查看', '审核'] }] },
   {
     name: '运营管理',
     pages: [
       { code: 'equity-images', name: '权益图片管理', actions: ['查看', '编辑'] },
       { code: 'news', name: '人才资讯管理', actions: ['查看', '新增', '编辑', '删除'] },
       { code: 'policies', name: '人才政策管理', actions: ['查看', '新增', '编辑', '删除'] },
-      { code: 'movie-tickets', name: '电影票管理', actions: ['查看', '新增', '编辑', '审核'] },
+      { code: 'movie-tickets', name: '电影票管理', actions: ['查看', '新增', '编辑', '审核', '批量导入', '下载'] },
     ],
   },
   {
@@ -63,8 +63,8 @@
     pages: [
       { code: 'settings', name: '平台设置', actions: ['查看', '编辑'] },
       { code: 'protocol-settings', name: '协议设置', actions: ['查看', '编辑'] },
-      { code: 'roles', name: '角色管理', actions: ['查看', '新增', '编辑', '删除'] },
-      { code: 'staff', name: '员工管理', actions: ['查看', '新增', '编辑', '启用/停用', '删除', '重置密码'] },
+      { code: 'roles', name: '角色管理', actions: ['查看', '新增', '编辑', '启用/停用', '删除', '下载'] },
+      { code: 'staff', name: '员工管理', actions: ['查看', '新增', '编辑', '启用/停用', '删除', '重置密码', '下载'] },
     ],
   },
 ]
@@ -81,6 +81,8 @@
   '删除': 'delete',
   '重置密码': 'reset',
   '审核': 'audit',
+  '批量导入': 'import',
+  '下载': 'export',
 } 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)
@@ -94,5 +96,5 @@
 async function save() { const valid = await formRef.value?.validate().catch(() => false); if (!valid) return; saving.value = true; try { const payload = { roleName: form.roleName.trim(), roleCode: '', remark: form.remark.trim(), status: form.status, permissionCodes: form.permissionCodes }; if (id.value) await updateAdminRole(id.value, payload); else await createAdminRole(payload); ElMessage.success('保存成功'); router.replace('/roles') } finally { saving.value = false } }
 onMounted(load)
 </script>
-<template><div class="page-stack role-form"><section class="panel"><div class="panel-header"><h3>{{ id ? '编辑角色' : '新增角色' }}</h3></div><el-form ref="formRef" class="base" :model="form" :rules="rules" label-position="top"><el-form-item label="角色名称" prop="roleName"><el-input v-model="form.roleName" placeholder="请输入" maxlength="64" /></el-form-item><el-form-item label="备注"><el-input v-model="form.remark" placeholder="请输入" maxlength="255" /></el-form-item></el-form><div class="permission-title">操作权限</div><div class="matrix"><div class="line header"><span>板块</span><span>页面</span><span>权限</span></div><template v-for="module in modules" :key="module.name"><div v-for="(page, index) in module.pages" :key="page.code" class="line"><span v-if="index === 0" class="module" :style="{ gridRow: `span ${module.pages.length}` }"><el-checkbox :model-value="checked(moduleCodes(module))" :indeterminate="partial(moduleCodes(module))" @click.stop @change="toggleModule(module, Boolean($event))">{{ module.name }}</el-checkbox></span><span><el-checkbox :model-value="checked(pageCodes(page))" :indeterminate="partial(pageCodes(page))" @click.stop @change="togglePage(page, Boolean($event))">{{ page.name }}</el-checkbox></span><span class="actions"><el-checkbox v-for="action in page.actions" :key="action" :model-value="form.permissionCodes.includes(code(page.code, action))" @click.stop @change="toggle([code(page.code, action)], Boolean($event))">{{ action }}</el-checkbox></span></div></template></div><div class="buttons"><el-button type="primary" :loading="saving" @click="save">保存</el-button><el-button @click="router.back()">返回</el-button></div></section></div></template>
-<style scoped>.base{display:grid;grid-template-columns:360px minmax(400px,1fr);gap:28px;padding:16px 20px}.permission-title{padding:14px 20px;border-top:8px solid #f4f6f8;font-size:16px;font-weight:600}.matrix{margin:0 20px;border:1px solid #dfe4ea}.line{display:grid;grid-template-columns:150px 190px 1fr;min-height:44px;border-bottom:1px solid #e7ebf0}.line:last-child{border-bottom:0}.line>span{display:flex;align-items:center;padding:7px 18px;border-right:1px solid #e7ebf0}.line>span:last-child{border-right:0}.header{min-height:36px;background:#fafbfd;font-weight:600}.header span{justify-content:center}.actions{gap:26px;flex-wrap:wrap}.buttons{padding:20px;display:flex;gap:14px}.buttons .el-button{min-width:78px}@media(max-width:850px){.base{grid-template-columns:1fr}.line{grid-template-columns:120px 150px 1fr}.actions{gap:12px}}</style>
+<template><div class="page-stack role-form"><section class="panel"><div class="panel-header"><h3>{{ id ? '编辑角色' : '新增角色' }}</h3></div><el-form ref="formRef" class="base" :model="form" :rules="rules" label-position="top"><el-form-item label="角色名称" prop="roleName"><el-input v-model="form.roleName" placeholder="请输入" maxlength="64" /></el-form-item><el-form-item label="备注"><el-input v-model="form.remark" placeholder="请输入" maxlength="255" /></el-form-item></el-form><div class="permission-title">操作权限</div><div class="matrix"><div class="line header"><span>板块</span><span>页面</span><span>权限</span></div><template v-for="module in modules" :key="module.name"><div class="module-group"><div v-for="(page, index) in module.pages" :key="page.code" class="line"><span v-if="index === 0" class="module" :style="{ gridRow: `span ${module.pages.length}` }"><el-checkbox :model-value="checked(moduleCodes(module))" :indeterminate="partial(moduleCodes(module))" @click.stop @change="toggleModule(module, Boolean($event))">{{ module.name }}</el-checkbox></span><span><el-checkbox :model-value="checked(pageCodes(page))" :indeterminate="partial(pageCodes(page))" @click.stop @change="togglePage(page, Boolean($event))">{{ page.name }}</el-checkbox></span><span class="actions"><el-checkbox v-for="action in page.actions" :key="action" :model-value="form.permissionCodes.includes(code(page.code, action))" @click.stop @change="toggle([code(page.code, action)], Boolean($event))">{{ action }}</el-checkbox></span></div></div></template></div><div class="buttons"><el-button type="primary" :loading="saving" @click="save">保存</el-button><el-button @click="router.back()">返回</el-button></div></section></div></template>
+<style scoped>.base{display:grid;grid-template-columns:360px minmax(400px,1fr);gap:28px;padding:16px 20px}.permission-title{padding:14px 20px;border-top:8px solid #f4f6f8;font-size:16px;font-weight:600}.matrix{display:grid;grid-template-columns:180px 220px minmax(0,1fr);margin:0 20px;border:1px solid #dfe4ea}.module-group{display:contents}.line{display:contents}.line>span{display:flex;align-items:center;min-height:44px;padding:7px 18px;border-right:1px solid #e7ebf0;border-bottom:1px solid #e7ebf0}.line>span:last-child{border-right:0}.module{align-self:stretch}.header{display:contents}.header span{display:flex;align-items:center;justify-content:center;min-height:36px;background:#fafbfd;font-weight:600;border-right:1px solid #e7ebf0;border-bottom:1px solid #e7ebf0}.header span:last-child{border-right:0}.actions{display:flex;align-items:center;gap:26px;flex-wrap:nowrap;white-space:nowrap;overflow:auto}.buttons{padding:20px;display:flex;gap:14px}.buttons .el-button{min-width:78px}@media(max-width:850px){.base{grid-template-columns:1fr}.matrix{grid-template-columns:130px 170px minmax(360px,1fr)}.actions{gap:12px}}</style>
diff --git a/talent-card-admin/src/views/SpouseEmploymentView.vue b/talent-card-admin/src/views/SpouseEmploymentView.vue
index 30c2842..63d52b3 100644
--- a/talent-card-admin/src/views/SpouseEmploymentView.vue
+++ b/talent-card-admin/src/views/SpouseEmploymentView.vue
@@ -119,7 +119,7 @@
       </el-form>
 
       <div class="spouse-employment-table-tools">
-        <el-button :icon="Download" @click="download">下载</el-button>
+        <el-button v-permission="'admin.spouse-employment.export'" :icon="Download" @click="download">下载</el-button>
       </div>
 
       <el-table
diff --git a/talent-card-admin/src/views/StaffManagementView.vue b/talent-card-admin/src/views/StaffManagementView.vue
index 236067d..a03ab2c 100644
--- a/talent-card-admin/src/views/StaffManagementView.vue
+++ b/talent-card-admin/src/views/StaffManagementView.vue
@@ -248,7 +248,7 @@
         ></el-form
       >
       <div class="tools">
-        <el-button type="primary" :icon="Plus" @click="create">新增</el-button>
+        <el-button v-permission="'admin.staff.create'" type="primary" :icon="Plus" @click="create">新增</el-button>
       </div>
       <el-table v-loading="loading" :data="pagedRows" stripe class="table"
         ><el-table-column
@@ -289,16 +289,16 @@
           }}</template></el-table-column
         ><el-table-column label="操作" width="225" fixed="right"
           ><template #default="{ row }"
-            ><el-button v-if="!isDefaultAdmin(row)" link type="primary" @click="edit(row)">编辑</el-button
-            ><el-button
+            ><el-button v-if="!isDefaultAdmin(row)" v-permission="'admin.staff.edit'" link type="primary" @click="edit(row)">编辑</el-button
+            ><el-button v-permission="'admin.staff.disable'"
               v-if="!isDefaultAdmin(row)"
               link
               type="primary"
               @click="changeStatus(row)"
               >{{ row.status === 1 ? "禁用" : "启用" }}</el-button
-            ><el-button link type="primary" @click="password(row)"
+            ><el-button v-permission="'admin.staff.reset'" link type="primary" @click="password(row)"
               >重置密码</el-button
-            ><el-button
+            ><el-button v-permission="'admin.staff.delete'"
               v-if="!isDefaultAdmin(row)"
               link
               type="danger"
diff --git a/talent-card-admin/src/views/TalentListView.vue b/talent-card-admin/src/views/TalentListView.vue
index cfc4897..6cef01e 100644
--- a/talent-card-admin/src/views/TalentListView.vue
+++ b/talent-card-admin/src/views/TalentListView.vue
@@ -236,10 +236,10 @@
 
       <div class="prototype-table-tools">
         <div class="prototype-table-tools-left">
-          <el-button type="primary" :icon="Plus" @click="openCreate">新建</el-button>
-          <el-button :icon="Upload" @click="openImport">批量导入</el-button>
+          <el-button v-permission="'admin.talents.create'" type="primary" :icon="Plus" @click="openCreate">新建</el-button>
+          <el-button v-permission="'admin.talents.import'" :icon="Upload" @click="openImport">批量导入</el-button>
         </div>
-        <el-button :icon="Download" :loading="exporting" @click="download">下载</el-button>
+        <el-button v-permission="'admin.talents.export'" :icon="Download" :loading="exporting" @click="download">下载</el-button>
       </div>
 
       <el-table v-loading="loading" :data="talents" border class="prototype-table" empty-text="暂无持卡人数据">
@@ -267,7 +267,7 @@
         <el-table-column label="操作" width="150" fixed="right" align="center">
           <template #default="{ row }">
             <el-button link type="primary" @click="openDetail(row)">查看</el-button>
-            <el-button link type="primary" @click="changeStatus(row)">{{ row.status === 1 ? '停用' : '启用' }}</el-button>
+          <el-button v-permission="'admin.talents.disable'" link type="primary" @click="changeStatus(row)">{{ row.status === 1 ? '停用' : '启用' }}</el-button>
           </template>
         </el-table-column>
       </el-table>
diff --git a/talent-card-admin/src/views/UpgradeApplicationDetailView.vue b/talent-card-admin/src/views/UpgradeApplicationDetailView.vue
index aedde8c..90148ed 100644
--- a/talent-card-admin/src/views/UpgradeApplicationDetailView.vue
+++ b/talent-card-admin/src/views/UpgradeApplicationDetailView.vue
@@ -109,8 +109,8 @@
     </section>
     <section v-if="detail" class="upgrade-detail-actions">
       <template v-if="pending">
-        <el-button type="primary" :loading="processing" @click="approve">处理</el-button>
-        <el-button type="danger" :loading="processing" @click="reject">审核不通过</el-button>
+        <el-button v-permission="'admin.upgrade-applications.audit'" type="primary" :loading="processing" @click="approve">处理</el-button>
+        <el-button v-permission="'admin.upgrade-applications.audit'" type="danger" :loading="processing" @click="reject">审核不通过</el-button>
       </template>
       <span v-else>审核结果:{{ text(detail.status) }}<template v-if="detail.auditRemark">({{ detail.auditRemark }})</template></span>
     </section>
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 f523d0e..1203c8e 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,7 +24,7 @@
 
     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) { 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> updateRole(Long id, AdminRoleRequest request) { if ("platform_admin".equals(existingRoleCode(id))) throw new BusinessException(ErrorCode.CONFLICT, "超级管理员角色不可编辑"); String oldCode = existingRoleCode(id); String code = request.roleCode() == null || request.roleCode().isBlank() ? oldCode : 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"); jdbc.update("update admin_session s join admin_operator o on o.id=s.operator_id set s.revoked_at=now() where s.revoked_at is null and o.role_code in (?, ?)", oldCode, code); 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); }
 
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthInterceptor.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthInterceptor.java
index edf6827..4a712c7 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthInterceptor.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthInterceptor.java
@@ -5,6 +5,8 @@
 import org.springframework.http.HttpHeaders;
 import org.springframework.stereotype.Component;
 import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.server.ResponseStatusException;
+import org.springframework.http.HttpStatus;
 
 @Component
 public class AdminAuthInterceptor implements HandlerInterceptor {
@@ -22,6 +24,61 @@
         Long operatorId = service.authenticate(request.getHeader(HttpHeaders.AUTHORIZATION));
         request.setAttribute(OPERATOR_ID_ATTRIBUTE, operatorId);
         request.setAttribute(OPERATOR_NAME_ATTRIBUTE, service.operatorName(operatorId));
+        String permission = permission(request.getMethod(), request.getRequestURI());
+        if (!permission.isBlank() && !service.hasPermission(operatorId, permission)) {
+            throw new ResponseStatusException(HttpStatus.FORBIDDEN, "当前账号没有该操作权限");
+        }
         return true;
     }
+
+    private String permission(String method, String uri) {
+        if (uri.startsWith("/api/admin/management/roles")) {
+            return managementPermission(method, uri, "roles");
+        }
+        if (uri.startsWith("/api/admin/management/staff")) {
+            return managementPermission(method, uri, "staff");
+        }
+        String page = page(uri);
+        if (page.isBlank()) return "";
+        String action = "GET".equalsIgnoreCase(method) ? "view" : "edit";
+        if ("POST".equalsIgnoreCase(method)) action = "create";
+        if ("DELETE".equalsIgnoreCase(method)) action = "delete";
+        if ("PATCH".equalsIgnoreCase(method)) {
+            action = uri.endsWith("/audit") || uri.endsWith("/status") ? (uri.endsWith("/audit") ? "audit" : "disable") : "edit";
+        }
+        return "admin." + page + "." + action;
+    }
+
+    private String page(String uri) {
+        if (uri.startsWith("/api/admin/business/merchants")) return "merchants";
+        if (uri.startsWith("/api/admin/business/products")) return "products";
+        if (uri.startsWith("/api/admin/business/news")) return "news";
+        if (uri.startsWith("/api/admin/business/policies")) return "policies";
+        if (uri.startsWith("/api/admin/business/coupons")) return "benefit-coupons";
+        if (uri.startsWith("/api/admin/dependent-card-applies")) return "family-card-applications";
+        if (uri.startsWith("/api/admin/cards/child-applications")) return "child-cards";
+        if (uri.startsWith("/api/admin/service-appointment-applications")) return "appointment-management";
+        if (uri.startsWith("/api/admin/medical-reservations")) return "medical-reservations";
+        if (uri.startsWith("/api/admin/parking-plate-submissions")) return "parking-plates";
+        if (uri.startsWith("/api/admin/car-wash-benefits") || uri.startsWith("/api/admin/car-wash-merchant-jump-configs")) return "car-wash-benefits";
+        if (uri.startsWith("/api/admin/fuel-services")) return "fuel-services";
+        if (uri.startsWith("/api/admin/spouse-employment")) return "spouse-employment";
+        if (uri.startsWith("/api/admin/hotel-reservations")) return "hotel-reservations";
+        if (uri.startsWith("/api/admin/merchant-settlement")) return "merchant-settlement";
+        if (uri.startsWith("/api/admin/ticket-packages")) return "movie-tickets";
+        if (uri.startsWith("/api/admin/display-services")) return "display-services";
+        if (uri.startsWith("/api/admin/equity-images")) return "equity-images";
+        return "";
+    }
+
+    private String managementPermission(String method, String uri, String page) {
+        String action;
+        if ("GET".equalsIgnoreCase(method)) action = "view";
+        else if ("POST".equalsIgnoreCase(method)) action = "create";
+        else if ("DELETE".equalsIgnoreCase(method)) action = "delete";
+        else if ("PUT".equalsIgnoreCase(method)) action = "edit";
+        else if ("PATCH".equalsIgnoreCase(method)) action = uri.endsWith("/status") ? "disable" : "reset";
+        else return "";
+        return "admin." + page + "." + action;
+    }
 }
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthService.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthService.java
index 71a3848..e3fe181 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthService.java
@@ -109,6 +109,25 @@
         return displayName.isBlank() ? "管理员" : displayName;
     }
 
+    @Transactional(readOnly = true)
+    public boolean hasPermission(Long operatorId, String permission) {
+        List<Map<String, Object>> result = rows("""
+                select o.role_code, r.permission_codes
+                from admin_operator o left join admin_role r on r.role_code = o.role_code
+                where o.id = ? and o.deleted = 0
+                limit 1
+                """, operatorId);
+        if (result.isEmpty()) return false;
+        Map<String, Object> operator = result.get(0);
+        if ("platform_admin".equals(String.valueOf(operator.get("roleCode")))) return true;
+        String raw = String.valueOf(operator.getOrDefault("permissionCodes", ""));
+        try {
+            return JSON.parseArray(raw, String.class).contains(permission);
+        } catch (Exception ignored) {
+            return false;
+        }
+    }
+
     private Map<String, Object> issueSession(Map<String, Object> operator) {
         String accessToken = token();
         LocalDateTime now = LocalDateTime.now();

--
Gitblit v1.7.1