Browse Source

feat: 更新费控

husenlin 3 tuần trước cách đây
mục cha
commit
1c71ca1ca4

+ 4 - 7
backend/app/plugin/module_payment/apikey/schema.py

@@ -1,7 +1,7 @@
 from datetime import datetime
 from fastapi import Query
 from typing import Generic, TypeVar, Optional
-from pydantic import BaseModel, Field
+from pydantic import BaseModel, Field, ConfigDict
 from app.api.v1.module_system.auth.schema import AuthSchema
 
 
@@ -26,8 +26,7 @@ class TenantApiKeyResponse(BaseModel):
     created_time: datetime
     return_url: Optional[str]
 
-    class Config:
-        from_attributes = True
+    model_config = ConfigDict(from_attributes=True)
 
 
 class TenantApiKeyListResponse(BaseModel):
@@ -41,8 +40,7 @@ class TenantApiKeyListResponse(BaseModel):
     description: Optional[str]
     return_url: Optional[str]
 
-    class Config:
-        from_attributes = True
+    model_config = ConfigDict(from_attributes=True)
 
 
 class TenantApiKeyUpdate(BaseModel):
@@ -72,8 +70,7 @@ class TenantApiLogResponse(BaseModel):
     response_time: float
     created_time: datetime
 
-    class Config:
-        from_attributes = True
+    model_config = ConfigDict(from_attributes=True)
 
 
 class ApiKeyQueryParam:

+ 1 - 1
backend/app/plugin/module_payment/department/schema.py

@@ -97,7 +97,7 @@ class DepartmentTreeOutSchema(BaseModel):
     
     model_config = ConfigDict(from_attributes=True)
     
-    id: int = Field(description="主键ID")
+    # id: int = Field(description="主键ID")
     department_id: str = Field(description="部门ID")
     department_name: str = Field(description="部门名称")
     department_code: Optional[str] = Field(default=None, description="部门编码")

+ 11 - 18
backend/app/plugin/module_payment/department/service.py

@@ -50,7 +50,7 @@ class DepartmentService:
         if data.department_code:
             department_create_model.department_code = data.department_code
         if data.parent_department_id:
-            department_create_model.parent_department_id = data.parent_department_id or "-1"
+            department_create_model.parent_department_id = data.parent_department_id
                   
         client = AlipayClient.get_client()
 
@@ -75,7 +75,7 @@ class DepartmentService:
             "department_id": result.department_id or "",
             "department_name": data.department_name,
             "department_code": data.department_code,
-            "parent_department_id": data.parent_department_id or "-1",
+            "parent_department_id": data.parent_department_id,
             "enterprise_id": data.enterprise_id,
             "sort_order": data.sort_order,
             "leader_employee_id": data.leader_employee_id,
@@ -187,40 +187,33 @@ class DepartmentService:
     ) -> List[DepartmentTreeOutSchema]:
         """获取部门树形结构"""
         crud = DepartmentCRUD(auth)
-        
         # 查询指定企业下的所有部门
         departments = await crud.list({"enterprise_id": enterprise_id})
-        
         # 转换为字典,方便查找
-        department_dict = {dept.department_id: dept for dept in departments}
-        
+        department_dict = {dept.department_id: DepartmentTreeOutSchema.model_validate(dept) for dept in departments}
         # 构建树形结构
-        tree = []
+        tree: List[DepartmentTreeOutSchema]  = []
         
-        for dept in departments:
+        for dept in department_dict.values():
             # 找到父部门
             parent_id = dept.parent_department_id
             if not parent_id or parent_id == "-1" or parent_id not in department_dict:
-                # 没有父部门,作为根节点
                 tree.append(dept)
             else:
                 # 添加到父部门的子节点
-                if not hasattr(department_dict[parent_id], 'children'):
-                    department_dict[parent_id].children = []
                 department_dict[parent_id].children.append(dept)
         
         # 排序子节点
-        def sort_children(node):
-            if hasattr(node, 'children') and node.children:
-                node.children.sort(key=lambda x: (x.sort_order or 0, x.department_name))
-                for child in node.children:
+        def sort_children(n: DepartmentTreeOutSchema):
+            if n.children:
+                n.children.sort(key=lambda x: (x.sort_order or 0, x.department_name))
+                for child in n.children:
                     sort_children(child)
         
         for node in tree:
             sort_children(node)
-        
-        # 转换为响应模型
-        return [DepartmentTreeOutSchema.model_validate(node) for node in tree]
+
+        return tree
 
     @classmethod
     async def get_all_departments(

+ 5 - 5
backend/app/scripts/init_app.py

@@ -137,10 +137,10 @@ def register_routers(app: FastAPI) -> None:
     from app.api.v1.module_monitor import monitor_router
     from app.api.v1.module_system import system_router
 
-    app.include_router(common_router, dependencies=[Depends(RateLimiter(times=5, seconds=10))])
-    app.include_router(application_router, dependencies=[Depends(RateLimiter(times=5, seconds=10))])
-    app.include_router(system_router, dependencies=[Depends(RateLimiter(times=5, seconds=10))])
-    app.include_router(monitor_router, dependencies=[Depends(RateLimiter(times=5, seconds=10))])
+    app.include_router(common_router, dependencies=[Depends(RateLimiter(times=10000, seconds=10))])
+    app.include_router(application_router, dependencies=[Depends(RateLimiter(times=10000, seconds=10))])
+    app.include_router(system_router, dependencies=[Depends(RateLimiter(times=10000, seconds=10))])
+    app.include_router(monitor_router, dependencies=[Depends(RateLimiter(times=10000, seconds=10))])
 
     # 先将动态路由注册到应用,使用速率限制器
     from app.core.discover import get_dynamic_router
@@ -148,7 +148,7 @@ def register_routers(app: FastAPI) -> None:
     # 获取动态路由实例
     app.include_router(
         router=get_dynamic_router(),
-        dependencies=[Depends(RateLimiter(times=5, seconds=10))],
+        dependencies=[Depends(RateLimiter(times=10000, seconds=10))],
     )
 
 

+ 21 - 1
backend/tests/test_apikey_sign.py

@@ -3,6 +3,26 @@ from app.plugin.module_payment.apikey.service import TenantApiKeyService
 
 class TestApiKeySign(unittest.TestCase):
 
+    def test_bank(self):
+        data = {
+            "account_book_id": "2088480770941200",
+            "amount": "1.00",
+            "order_title": "Apikey转账33",
+            "third_biz_no": "1234242026042700333",
+            "payee_info": {
+                "identity_type": "bank",
+                "name": "钱红武",
+                "identity": "6214680152863039",
+                # "bankcard_ext_info": {
+                #     "account_type": "2"
+                # }
+            }
+        }
+        sign = TenantApiKeyService.generate_signature(
+            "8a6eac1c235fd5fcdd51376e08da46348e6f1160e30b83283e5f8d5698588b2c60d85dd3d98167e6ece08e8e08182dd456627de9c2fc511593489f195ffb708d",
+            request_data=data)
+        print(sign)
+
     def test_qq(self):
         data = {
             "third_biz_no": "123424202604270088"
@@ -19,7 +39,7 @@ class TestApiKeySign(unittest.TestCase):
             "order_title": "Apikey转账22",
             "third_biz_no": "1234242026042700222",
             "payee_info": {
-                "identity_type": "ALIPAY_ACCOUNT",
+                "identity_type": "bank",
                 "name": "钱红武",
                 "identity": "15399795365"
             }

+ 11 - 22
frontend/src/api/module_payment/department.ts

@@ -153,28 +153,17 @@ export default {
       method: "get",
       params,
     }).then(response => {
-      // 转换数据结构以匹配前端选择器
-      if (response.data.code === 200 && response.data.data) {
-        const convertTreeToOptions = (tree: any[], level = 0): any[] => {
-          const options: any[] = [];
-          const indent = '  '.repeat(level);
-          
-          tree.forEach(node => {
-            options.push({
-              value: node.id,
-              label: `${indent}${node.label}`,
-              disabled: false
-            });
-            
-            if (node.children && node.children.length > 0) {
-              options.push(...convertTreeToOptions(node.children, level + 1));
-            }
-          });
-          
-          return options;
-        };
-        
-        response.data.data = convertTreeToOptions(response.data.data);
+      // 转换数据结构以匹配前端选择框
+      if (response.data.code === 0 && response.data.data) {
+        const convertTreeStructure = (tree: any[]): any[] => {
+          return tree.map(node => ({
+            value: node.department_id,
+            label: node.department_name,
+            disabled: false,
+            children: node.children && node.children.length > 0 ? convertTreeStructure(node.children) : []
+          }));
+        };    
+        response.data.data = convertTreeStructure(response.data.data);
       }
       return response;
     });

+ 24 - 43
frontend/src/views/module_payment/employee/components/DepartmentForm.vue

@@ -24,19 +24,15 @@
       </el-form-item>
 
       <el-form-item label="上级部门" prop="parent_id">
-        <el-select
+        <el-tree-select
           v-model="formData.parent_id"
+          :data="departmentOptions"
+          check-strictly
+          :render-after-expand="false"
           placeholder="请选择上级部门"
           clearable
-        >
-          <el-option
-            v-for="option in departmentOptions"
-            :key="option.value"
-            :label="option.label"
-            :value="option.value"
-            :disabled="option.disabled"
-          />
-        </el-select>
+          style="width: 100%"
+        />
       </el-form-item>
     </el-form>
   </div>
@@ -45,7 +41,7 @@
 <script setup lang="ts">
 import { ref, reactive, onMounted, watch } from 'vue';
 import { ElMessage, ElLoading } from 'element-plus';
-import DepartmentAPI, { DepartmentInfo } from '@/api/module_payment/department';
+import DepartmentAPI from '@/api/module_payment/department';
 
 // 部门表单数据类型
 interface DepartmentForm {
@@ -57,7 +53,7 @@ interface DepartmentForm {
 const props = defineProps<{
   type: 'create' | 'update';
   departmentId?: string;
-  enterpriseId?: string;
+  enterpriseId: string;
 }>();
 
 const emit = defineEmits<{
@@ -78,13 +74,13 @@ const rules = reactive({
   code: [{ required: true, message: '请输入部门编码', trigger: 'blur' }]
 });
 
+// 部门选项
 const departmentOptions = ref<any[]>([]);
-
 // 加载部门选项
 const loadDepartmentOptions = async () => {
   try {
     const res = await DepartmentAPI.getDepartmentOptions({ enterprise_id: props.enterpriseId });
-    if (res.data.code === 200) {
+    if (res.data.code === 0) {
       departmentOptions.value = res.data.data || [];
     }
   } catch (error) {
@@ -95,22 +91,10 @@ const loadDepartmentOptions = async () => {
 // 生成部门编码
 const generateDepartmentCode = async () => {
   try {
-    const res = await DepartmentAPI.listDepartment({ enterprise_id: props.enterpriseId });
-    if (res.data.code === 200) {
-      const departments = res.data.data?.items || [];
-      const codes = departments.map((dept: any) => dept.code).filter(Boolean);
-      
-      let maxCode = 0;
-      codes.forEach((code: string) => {
-        const num = parseInt(code.replace(/[^0-9]/g, ''), 10);
-        if (!isNaN(num) && num > maxCode) {
-          maxCode = num;
-        }
-      });
-      
-      const newCode = `D${String(maxCode + 1).padStart(5, '0')}`;
-      formData.code = newCode;
-    }
+    // 生成 D + 6位随机数
+    const randomNum = Math.floor(Math.random() * 900000) + 100000;
+    const newCode = `D${randomNum}`;
+    formData.code = newCode;
   } catch (error) {
     console.error('生成部门编码失败:', error);
   }
@@ -158,7 +142,7 @@ const submitForm = async (isContinue = false) => {
           });
         }
         
-        if (res.data.code === 200) {
+        if (res.data.code === 0) {
           ElMessage.success(props.type === 'create' ? '创建部门成功' : '更新部门成功');
           
           if (isContinue && props.type === 'create') {
@@ -201,25 +185,22 @@ defineExpose({
 });
 
 onMounted(async () => {
-  await Promise.all([
-    loadDepartmentOptions(),
-    loadEmployeeOptions()
-  ]);
+  await Promise.all([loadDepartmentOptions(),]);
   
   if (props.type === 'create') {
-    await generateDepartmentCode();
+    resetForm();
   } else {
     await loadDepartmentDetail();
   }
 });
 
-watch(() => props.type, async (newType) => {
-  if (newType === 'create') {
-    resetForm();
-  } else if (newType === 'update' && props.departmentId) {
-    await loadDepartmentDetail();
-  }
-});
+// watch(() => props.type, async (newType) => {
+//   if (newType === 'create') {
+//     resetForm();
+//   } else if (newType === 'update' && props.departmentId) {
+//     await loadDepartmentDetail();
+//   }
+// });
 </script>
 
 <style scoped lang="scss">

+ 28 - 55
frontend/src/views/module_payment/employee/components/EmployeeForm.vue

@@ -10,36 +10,32 @@
       class="employee-form__inner"
     >
       <el-form-item label="员工姓名" prop="employee_name" required>
-        <el-input v-model="formData.employee_name" placeholder="请输入" :maxlength="128" />
+        <el-input v-model="formData.employee_name" placeholder="请输入员工姓名" :maxlength="128" />
       </el-form-item>
 
       <el-form-item label="员工工号" prop="employee_no">
-        <el-input v-model="formData.employee_no" placeholder="请输入" :maxlength="64" />
+        <el-input v-model="formData.employee_no" placeholder="请输入员工工号" :maxlength="64" />
       </el-form-item>
 
       <el-form-item label="手机号" prop="employee_mobile">
-        <el-input v-model="formData.employee_mobile" placeholder="请输入" :maxlength="32" />
+        <el-input v-model="formData.employee_mobile" placeholder="请输入手机号" :maxlength="32" />
       </el-form-item>
 
       <el-form-item label="邮箱" prop="employee_email">
-        <el-input v-model="formData.employee_email" placeholder="请输入" :maxlength="128" />
+        <el-input v-model="formData.employee_email" placeholder="请输入邮箱" :maxlength="128" />
         <div class="employee-form__hint">手机号/邮箱任意必填其中之一</div>
       </el-form-item>
 
       <el-form-item label="所属部门" prop="department_ids">
-        <el-select
+        <el-tree-select
           v-model="formData.department_ids"
-          placeholder="请选择"
+          :data="departmentOptions"
+          check-strictly
+          :render-after-expand="false"
+          placeholder="请选择所属部门"
+          clearable
           style="width: 100%"
-          multiple
-        >
-          <el-option
-            v-for="item in departmentOptions"
-            :key="item.value"
-            :label="item.label"
-            :value="item.value"
-          />
-        </el-select>
+        />
       </el-form-item>
 
       <el-form-item label="入职日期" prop="profiles.join_date">
@@ -51,7 +47,7 @@
         />
       </el-form-item>
 
-      <el-form-item label="入职城市" prop="profiles.city">
+      <!-- <el-form-item label="入职城市" prop="profiles.city">
         <el-select
           v-model="formData.profiles.city"
           placeholder="选择城市"
@@ -64,7 +60,7 @@
             :value="item.value"
           />
         </el-select>
-      </el-form-item>
+      </el-form-item> -->
 
       <!-- <el-form-item label="角色" prop="profiles.role" required>
         <el-select
@@ -94,6 +90,7 @@
 import { computed, reactive, ref, watch } from "vue";
 import EmployeeAPI, { EmployeeForm, EmployeeDetail } from "@/api/module_payment/employee";
 import { useEnterpriseStore } from "@/store/modules/enterprise.store";
+import DepartmentAPI from "@/api/module_payment/department";
 
 
 const enterpriseStore = useEnterpriseStore();
@@ -102,7 +99,7 @@ const enterpriseStore = useEnterpriseStore();
 interface Props {
   type: "create" | "update";
   employeeId?: string;
-  enterpriseId?: string;
+  enterpriseId: string;
 }
 
 const props = defineProps<Props>();
@@ -144,29 +141,18 @@ const formData = reactive<EmployeeForm>(
 );
 
 // 模拟部门选项
-const departmentOptions = [
-  { label: "技术部", value: "dept_001" },
-  { label: "市场部", value: "dept_002" },
-  { label: "财务部", value: "dept_003" },
-  { label: "人力资源部", value: "dept_004" },
-];
-
-// 模拟城市选项
-const cityOptions = [
-  { label: "北京", value: "beijing" },
-  { label: "上海", value: "shanghai" },
-  { label: "广州", value: "guangzhou" },
-  { label: "深圳", value: "shenzhen" },
-  { label: "杭州", value: "hangzhou" },
-];
-
-// 模拟角色选项
-const roleOptions = [
-  { label: "普通员工", value: "employee" },
-  { label: "部门经理", value: "manager" },
-  { label: "财务", value: "finance" },
-  { label: "管理员", value: "admin" },
-];
+const departmentOptions = ref<any[]>([]);
+// 加载部门选项
+const loadDepartmentOptions = async () => {
+  try {
+    const res = await DepartmentAPI.getDepartmentOptions({ enterprise_id: props.enterpriseId });
+    if (res.data.code === 0) {
+      departmentOptions.value = res.data.data || [];
+    }
+  } catch (error) {
+    console.error('加载部门选项失败:', error);
+  }
+};
 
 watch(
   () => props.employeeId,
@@ -204,6 +190,7 @@ watch(
   (newVal) => {
     if (newVal && props.type === "create") {
       formData.enterprise_id = newVal;
+      loadDepartmentOptions();
     }
   },
   { immediate: true }
@@ -222,20 +209,6 @@ const validateContact = (_rule: unknown, value: string, callback: (error?: Error
 };
 
 const rules = reactive({
-  // enterprise_id: [
-  //   {
-  //     required: true,
-  //     message: "请输入企业ID",
-  //     trigger: "blur",
-  //     validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
-  //       if (props.type === "create" && !formData.enterprise_id) {
-  //         callback(new Error("企业ID不能为空"));
-  //       } else {
-  //         callback();
-  //       }
-  //     },
-  //   },
-  // ],
   employee_name: [{ required: true, message: "请输入员工姓名", trigger: "blur" }],
   employee_no: [{ message: "请输入员工工号", trigger: "blur" }],
   employee_mobile: [{ validator: validateContact, trigger: "blur" }],

+ 23 - 15
frontend/src/views/module_payment/employee/index.vue

@@ -345,16 +345,16 @@ const deptSearchConfig = reactive<ISearchConfig>({
       type: "input",
       attrs: { placeholder: "请输入部门编码", clearable: true },
     },
-    {
-      prop: "status",
-      label: "状态",
-      type: "select",
-      options: Object.entries(DEPARTMENT_STATUS_LABEL).map(([value, label]) => ({
-        label,
-        value,
-      })),
-      attrs: { placeholder: "请选择状态", clearable: true },
-    },
+    // {
+    //   prop: "status",
+    //   label: "状态",
+    //   type: "select",
+    //   options: Object.entries(DEPARTMENT_STATUS_LABEL).map(([value, label]) => ({
+    //     label,
+    //     value,
+    //   })),
+    //   attrs: { placeholder: "请选择状态", clearable: true },
+    // },
   ],
 });
 
@@ -369,7 +369,7 @@ const deptContentCols = reactive<
   { prop: "selection", label: "选择框", show: false },
   // { prop: "index", label: "序号", show: true },
   { prop: "name", label: "部门名称", show: true },
-  { prop: "code", label: "部门编码", show: true },
+  // { prop: "code", label: "部门编码", show: true },
   // { prop: "parent_name", label: "上级部门", show: true },
   // { prop: "leader_employee_name", label: "部门负责人", show: true },
   // { prop: "sort_order", label: "排序值", show: true },
@@ -404,7 +404,11 @@ const contentConfig = reactive<IContentConfig<EmployeePageQuery>>({
   hideColumnFilter: false,
   toolbar: [],
   defaultToolbar: ["refresh", "filter", "import", "export"],
-  pagination: true,
+  pagination: {
+    pageSize: 10,
+    pageSizes: [10, 20, 30, 50],
+  },
+  request: { page_no: "page", page_size: "page_size" },
   indexAction: async (params) => {
     const query: EmployeePageQuery = {
       page_no: params.page_no,
@@ -416,7 +420,7 @@ const contentConfig = reactive<IContentConfig<EmployeePageQuery>>({
     const res = await EmployeeAPI.listEmployee(query);
     return {
       list: res.data.data?.items || [],
-      total: res.data.data?.total || 0,
+      total: Number(res.data.data?.total) || 0,
     };
   },
 });
@@ -429,7 +433,11 @@ const deptContentConfig = reactive<IContentConfig<DepartmentPageQuery>>({
   hideColumnFilter: false,
   toolbar: [],
   defaultToolbar: ["refresh", "filter", "import", "export"],
-  pagination: true,
+  pagination: {
+    pageSize: 10,
+    pageSizes: [10, 20, 30, 50],
+  },
+  request: { page_no: "page", page_size: "page_size" },
   initialParams: computed(() => ({
     enterprise_id: enterpriseIdFromUrl.value,
   })) as Record<string, unknown>,
@@ -458,7 +466,7 @@ const deptContentConfig = reactive<IContentConfig<DepartmentPageQuery>>({
     })) || [];
     return {
       list: items,
-      total: res.data.data?.total || 0,
+      total: Number(res.data.data?.total) || 0,
     };
   },
 });