husenlin il y a 3 semaines
Parent
commit
2d964c7ef7

+ 1 - 1
backend/app/plugin/module_payment/expense/institution/controller.py

@@ -49,7 +49,7 @@ async def create_institution_controller(
     auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:institution:create"]))],
 ) -> JSONResponse:
     """创建费控制度"""
-    institution_create_model = AlipayEbppInvoiceInstitutionCreateModel(**data)
+    institution_create_model = AlipayEbppInvoiceInstitutionCreateModel.from_alipay_dict(data)
     result = await InstitutionService.create_institution_service(auth=auth, data=institution_create_model)
     log.info(f"创建费控制度成功: {institution_create_model.institution_name}, institution_id={result.institution_id}")
     return SuccessResponse(data=result, msg="创建费控制度成功")

+ 22 - 12
backend/app/plugin/module_payment/openapi/service.py

@@ -4,7 +4,6 @@ from app.core.logger import log
 from app.utils.snowflake import get_snowflake_id_str, get_snowflake_id
 from typing import Optional
 import time
-import json
 
 from .crud import OpenConfCRUD, OpenTransferCRUD
 from .schema import OpenConfOutSchema, OpenConfUpdateSchema, OpenTransferSchema, OpenTransferOutSchema, \
@@ -16,14 +15,23 @@ import asyncio
 from ..apikey.service import TenantApiKeyService
 
 
-async def fetch_manual_retry(session, url, notify_id, timestamp, content, max_retries=2):
+async def fetch_manual_retry(
+    session,
+    url: str,
+    notify_id: str,
+    timestamp: int,
+    content: str,
+    max_retries: int=2
+):
     for attempt in range(max_retries):
         try:
             log.debug("第 {} 次尝试: {}", attempt + 1, url)
+
             form_data = aiohttp.FormData()
             form_data.add_field('notify_id', notify_id)
             form_data.add_field('timestamp', timestamp)
             form_data.add_field('content', content)
+
             async with session.post(url=url, data=form_data) as response:
                 if response.status == 200:
                     return await response.text()
@@ -66,17 +74,17 @@ class OpenTransferService:
         """
         try:
             transfer_crud = TransferCRUD(auth)
-            transfer = await transfer_crud.get_by_order_no(order_no)
+            transfer_data = await transfer_crud.get_by_order_no(order_no)
 
-            if not transfer or not transfer.out_biz_no:
-                log.info("回调通知: 订单不存在或缺少 out_biz_no, order_no={}", order_no)
+            if not transfer_data or not transfer_data.out_biz_no:
+                log.info("开放回调通知: 订单不存在或缺少 out_biz_no, order_no={}", order_no)
                 return False
 
             open_transfer_crud = OpenTransferCRUD(auth)
-            open_data = await open_transfer_crud.get(out_biz_no=transfer.out_biz_no)
+            open_data = await open_transfer_crud.get(out_biz_no=transfer_data.out_biz_no)
 
             if not open_data:
-                log.info("回调通知: 开放转账记录不存在, out_biz_no={}", transfer.out_biz_no)
+                log.info("开放回调通知: 开放转账记录不存在, out_biz_no={}", transfer_data.out_biz_no)
                 return False
 
             auth.tenant_id = open_data.tenant_id
@@ -92,15 +100,15 @@ class OpenTransferService:
             else:
                 conf = await OpenConfService.get_conf_service(auth)
                 if not conf:
-                    log.info("回调通知: 开放转账配置不存在, tenant_id={}", auth.tenant_id)
+                    log.info("开放回调通知: 开放转账配置不存在, tenant_id={}", auth.tenant_id)
                     return False
                 return_url = conf.return_url
 
             if not return_url:
-                log.info("回调通知: 回调地址不存在")
+                log.info("开放回调通知: 回调地址不存在")
                 return False
 
-            result = TransferOutSchema.model_validate(transfer)
+            result = TransferOutSchema.model_validate(transfer_data)
             result.third_biz_no = open_data.third_biz_no
             
             notify_id = f"n{get_snowflake_id()}"
@@ -109,14 +117,15 @@ class OpenTransferService:
 
             timeout = aiohttp.ClientTimeout(total=30)
             async with aiohttp.ClientSession(timeout=timeout) as session:
-                log.info("回调通知: order_no={}, url={}, notify_id={}", order_no, return_url, notify_id)
+                log.info("开放回调通知: 回调请求 third_biz_no={} order_no={}, url={}, notify_id={}",
+                            open_data.third_biz_no, order_no, return_url, notify_id)
                 await fetch_manual_retry(
                     session, return_url, notify_id, timestamp, content
                 )
             return True
 
         except Exception as e:
-            log.error("回调通知异常: order_no={}, error={}", order_no, e, exc_info=True)
+            log.error("开放回调通知异常: order_no={}, error={}", order_no, e, exc_info=True)
             return False
 
 
@@ -163,6 +172,7 @@ class OpenTransferService:
             "out_biz_no": result.out_biz_no,
             "api_key": data.api_key,
         }
+
         await crud.create(create_data)
 
         return OpenTransferOutSchema(

+ 173 - 72
frontend/src/views/module_payment/institution/index.vue

@@ -1,19 +1,48 @@
 <template>
   <div v-loading="pageLoading" class="app-container" :element-loading-text="loadingText">
-    <PageSearch
-      ref="searchRef"
-      :search-config="searchConfig"
-      @query-click="handleQueryClick"
-      @reset-click="handleResetClick"
-    />
+    <!-- 顶部分类标签栏 -->
+    <div class="category-tabs">
+      <div class="category-tabs__nav">
+        <el-button
+          v-for="tab in categoryTabs"
+          :key="tab.key"
+          :type="activeCategory === tab.key ? 'primary' : 'default'"
+          :icon="tab.icon"
+          @click="handleCategoryChange(tab.key)"
+        >
+          {{ tab.label }}
+        </el-button>
+      </div>
+    </div>
+
+    <!-- 搜索栏和场景选择 -->
+    <div class="search-bar">
+      <div class="search-bar__scene">
+        <span class="search-bar__label">场景:</span>
+        <el-radio-group v-model="sceneValue" class="scene-radio-group">
+          <el-radio
+            v-for="scene in currentScenes"
+            :key="scene.value"
+            :label="scene.value"
+          >
+            {{ scene.label }}
+          </el-radio>
+        </el-radio-group>
+      </div>
+      <div class="search-bar__right">
+        <el-input
+          v-model="searchName"
+          placeholder="制度名称搜索"
+          class="search-bar__input"
+          @keyup.enter="handleSearch"
+        />
+        <el-button type="primary" @click="handleSearch">搜索</el-button>
+        <el-button type="success" @click="handleOpenDialog('create')">新增制度</el-button>
+      </div>
+    </div>
 
     <PageContent ref="contentRef" :content-config="contentConfig">
       <template #toolbar="{ toolbarRight, onToolbar, removeIds, cols }">
-        <CrudToolbarLeft
-          :remove-ids="removeIds"
-          :perm-create="['module_payment:institution:create']"
-          @add="handleOpenDialog('create')"
-        />
         <div class="data-table__toolbar--right">
           <CrudToolbarRight :buttons="toolbarRight" :cols="cols" :on-toolbar="onToolbar" />
         </div>
@@ -38,14 +67,6 @@
               min-width="55"
               align="center"
             />
-            <el-table-column
-              v-if="contentCols.find((col) => col.prop === 'institution_id')?.show"
-              key="institution_id"
-              label="制度ID"
-              prop="institution_id"
-              min-width="150"
-              show-overflow-tooltip
-            />
             <el-table-column
               v-if="contentCols.find((col) => col.prop === 'name')?.show"
               key="name"
@@ -54,6 +75,14 @@
               min-width="150"
               show-overflow-tooltip
             />
+            <el-table-column
+              v-if="contentCols.find((col) => col.prop === 'valid_period')?.show"
+              key="valid_period"
+              label="制度有效期"
+              prop="valid_period"
+              min-width="150"
+              show-overflow-tooltip
+            />
             <el-table-column
               v-if="contentCols.find((col) => col.prop === 'expense_type')?.show"
               key="expense_type"
@@ -68,7 +97,7 @@
             <el-table-column
               v-if="contentCols.find((col) => col.prop === 'status')?.show"
               key="status"
-              label="状态"
+              label="制度状态"
               prop="status"
               min-width="100"
             >
@@ -79,10 +108,10 @@
               </template>
             </el-table-column>
             <el-table-column
-              v-if="contentCols.find((col) => col.prop === 'created_time')?.show"
-              key="created_time"
-              label="创建时间"
-              prop="created_time"
+              v-if="contentCols.find((col) => col.prop === 'updated_time')?.show"
+              key="updated_time"
+              label="修改时间"
+              prop="updated_time"
               min-width="160"
               sortable
             />
@@ -94,45 +123,28 @@
               min-width="240"
             >
               <template #default="scope">
-                <el-button
-                  v-hasPerm="['module_payment:institution:detail']"
-                  type="info"
-                  size="small"
-                  link
-                  icon="View"
-                  @click="handleOpenDialog('detail', scope.row.institution_id)"
-                >
-                  详情
-                </el-button>
                 <el-button
                   v-hasPerm="['module_payment:institution:update']"
-                  type="primary"
+                  type="text"
                   size="small"
-                  link
-                  icon="edit"
                   @click="handleOpenDialog('update', scope.row.institution_id)"
                 >
                   编辑
                 </el-button>
                 <el-button
-                  v-hasPerm="['module_payment:institution:scope']"
-                  type="warning"
+                  v-hasPerm="['module_payment:institution:manual_pay']"
+                  type="text"
                   size="small"
-                  link
-                  icon="User"
-                  @click="handleOpenScopeDialog(scope.row.institution_id)"
+                  @click="handleManualPay(scope.row.institution_id)"
                 >
-                  适用范围
+                  手动发钱
                 </el-button>
                 <el-button
-                  v-hasPerm="['module_payment:institution:delete']"
-                  type="danger"
+                  type="text"
                   size="small"
-                  link
-                  icon="delete"
-                  @click="handleDelete(scope.row.institution_id)"
+                  @click="handleMore(scope.row)"
                 >
-                  删除
+                  更多
                 </el-button>
               </template>
             </el-table-column>
@@ -190,15 +202,13 @@ import InstitutionAPI, {
   STATUS_LABEL,
   EXPENSE_TYPE_LABEL,
 } from "@/api/module_payment/institution";
-import CrudToolbarLeft from "@/components/CURD/CrudToolbarLeft.vue";
 import CrudToolbarRight from "@/components/CURD/CrudToolbarRight.vue";
-import PageSearch from "@/components/CURD/PageSearch.vue";
 import PageContent from "@/components/CURD/PageContent.vue";
 import EnhancedDialog from "@/components/CURD/EnhancedDialog.vue";
 import InstitutionForm from "./components/InstitutionForm.vue";
 import InstitutionDetail from "./components/InstitutionDetail.vue";
 import ScopeDialog from "./components/ScopeDialog.vue";
-import type { ISearchConfig, IContentConfig } from "@/components/CURD/types";
+import type { IContentConfig } from "@/components/CURD/types";
 import { useCrudList } from "@/components/CURD/useCrudList";
 import { useLoadingAction } from "@/composables/useLoadingAction";
 import { useRoute } from "vue-router";
@@ -207,6 +217,39 @@ import { ref, reactive, computed } from "vue";
 
 const route = useRoute();
 
+// 分类标签与对应场景映射
+const categoryTabs = [
+  { key: "meal", label: "餐饮", icon: "UtensilsCrossed", scenes: [{ label: "差旅餐饮", value: "business_meal" }, { label: "员工餐补", value: "staff_meal" }, { label: "团建聚餐", value: "team_dinner" }] },
+  { key: "hotel", label: "酒店", icon: "Building2", scenes: [{ label: "商务出差", value: "business_trip" }, { label: "会议住宿", value: "meeting_hotel" }, { label: "培训住宿", value: "training_hotel" }] },
+  { key: "flight", label: "机票", icon: "Plane", scenes: [{ label: "国内出差", value: "domestic_flight" }, { label: "国际出差", value: "international_flight" }, { label: "紧急出差", value: "urgent_flight" }] },
+  { key: "train", label: "火车票", icon: "Train", scenes: [{ label: "省内出差", value: "provincial_train" }, { label: "跨省出差", value: "interprovincial_train" }, { label: "通勤", value: "commute_train" }] },
+  { key: "bus", label: "公交", icon: "Bus", scenes: [{ label: "市内通勤", value: "city_bus" }, { label: "郊区出行", value: "suburb_bus" }] },
+  { key: "subway", label: "地铁", icon: "Metro", scenes: [{ label: "日常通勤", value: "daily_subway" }, { label: "加班补贴", value: "overtime_subway" }] },
+  { key: "car", label: "用车", icon: "Car", scenes: [{ label: "公务用车", value: "official_car" }, { label: "网约车", value: "ride_hailing" }, { label: "自驾补贴", value: "self_drive" }] },
+  { key: "service", label: "服务", icon: "Headphones", scenes: [{ label: "咨询服务", value: "consult_service" }, { label: "外包服务", value: "outsourcing" }] },
+  { key: "shopping", label: "商城", icon: "ShoppingCart", scenes: [{ label: "办公用品", value: "office_supplies" }, { label: "劳保用品", value: "labor_protection" }, { label: "员工福利", value: "employee_welfare" }] },
+  { key: "express", label: "快递", icon: "Truck", scenes: [{ label: "日常快递", value: "daily_express" }, { label: "大件物流", value: "bulk_logistics" }] },
+  { key: "gas", label: "加油", icon: "Fuel", scenes: [{ label: "公务车加油", value: "official_gas" }, { label: "私家车补贴", value: "private_gas" }] },
+  { key: "medical", label: "医疗", icon: "Stethoscope", scenes: [{ label: "体检", value: "medical_checkup" }, { label: "门诊报销", value: "outpatient" }, { label: "住院报销", value: "hospitalization" }] },
+  { key: "default", label: "默认", icon: "FileText", scenes: [{ label: "通用", value: "common" }, { label: "其他", value: "other" }] },
+  { key: "advertising", label: "电商广告充值", icon: "Monitor", scenes: [{ label: "平台推广", value: "platform_promo" }, { label: "品牌广告", value: "brand_ad" }, { label: "促销活动", value: "promotion" }] },
+];
+
+// 当前激活的分类
+const activeCategory = ref("default");
+
+// 当前分类对应的场景列表
+const currentScenes = computed(() => {
+  const category = categoryTabs.find(tab => tab.key === activeCategory.value);
+  return category?.scenes || [{ label: "通用", value: "common" }];
+});
+
+// 场景值
+const sceneValue = ref("");
+
+// 搜索名称
+const searchName = ref("");
+
 const { searchRef, contentRef, handleQueryClick, handleResetClick, refreshList } =
   useCrudList();
 const formRef = ref();
@@ -215,22 +258,6 @@ const { pageLoading, loadingText, execute: loadingExecute } = useLoadingAction()
 
 const enterpriseIdFromUrl = computed(() => route.query.enterprise_id as string | undefined);
 
-const searchConfig = reactive<ISearchConfig>({
-  permPrefix: "module_payment:institution",
-  colon: true,
-  isExpandable: true,
-  showNumber: 2,
-  form: { labelWidth: "auto" },
-  formItems: [
-    {
-      prop: "enterprise_id",
-      label: "企业ID",
-      type: "input",
-      attrs: { placeholder: "请输入企业ID", clearable: true },
-    },
-  ],
-});
-
 const contentCols = reactive<
   Array<{
     prop?: string;
@@ -240,11 +267,11 @@ const contentCols = reactive<
 >([
   { prop: "selection", label: "选择框", show: false },
   { prop: "index", label: "序号", show: true },
-  { prop: "institution_id", label: "制度ID", show: true },
   { prop: "name", label: "制度名称", show: true },
+  { prop: "valid_period", label: "制度有效期", show: true },
   { prop: "expense_type", label: "费用类型", show: true },
-  { prop: "status", label: "状态", show: true },
-  { prop: "created_time", label: "创建时间", show: true },
+  { prop: "status", label: "制度状态", show: true },
+  { prop: "updated_time", label: "修改时间", show: true },
   { prop: "operation", label: "操作", show: true },
 ]);
 
@@ -332,4 +359,78 @@ async function handleDelete(institutionId?: string) {
     },
   });
 }
-</script>
+
+function handleCategoryChange(categoryKey: string) {
+  activeCategory.value = categoryKey;
+  // 切换分类后,重置场景为当前分类的第一个场景
+  const category = categoryTabs.find(tab => tab.key === categoryKey);
+  sceneValue.value = category?.scenes?.[0]?.value || "";
+  refreshList();
+}
+
+function handleSearch() {
+  refreshList();
+}
+
+function handleManualPay(institutionId?: string) {
+  if (!institutionId) {
+    ElMessage.warning("制度ID不存在");
+    return;
+  }
+  ElMessage.info(`手动发钱功能:制度ID ${institutionId}`);
+}
+
+function handleMore(row: any) {
+  ElMessage.info(`更多操作:制度名称 ${row.name}`);
+}
+</script>
+
+<style scoped>
+.category-tabs {
+  margin-bottom: 16px;
+  padding: 16px;
+  background: #fff;
+  border-radius: 8px;
+}
+
+.category-tabs__nav {
+  display: flex;
+  gap: 8px;
+  flex-wrap: wrap;
+}
+
+.search-bar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16px;
+  padding: 16px;
+  background: #fff;
+  border-radius: 8px;
+}
+
+.search-bar__scene {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.search-bar__label {
+  font-weight: 500;
+}
+
+.search-bar__right {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.search-bar__input {
+  width: 200px;
+}
+
+.scene-radio-group {
+  display: flex;
+  gap: 16px;
+}
+</style>