Bladeren bron

Merge remote-tracking branch 'origin/master'

gogs 2 maanden geleden
bovenliggende
commit
52eeecbb55
41 gewijzigde bestanden met toevoegingen van 1522 en 90 verwijderingen
  1. 63 0
      app/Http/Controllers/Api/CustomFieldSettingController.php
  2. 13 0
      app/Http/Controllers/Api/EmployeeController.php
  3. 83 0
      app/Jobs/ProcessOssTask.php
  4. 23 0
      app/Model/CustomFieldDefinitions.php
  5. 21 0
      app/Model/CustomFieldValue.php
  6. 1 1
      app/Model/Device.php
  7. 4 3
      app/Model/Employee.php
  8. 14 0
      app/Model/EmployeeFile.php
  9. 1 1
      app/Model/ExpenseClaimsDetails.php
  10. 8 1
      app/Model/Fee.php
  11. 35 4
      app/Model/Item.php
  12. 17 0
      app/Model/SysModules.php
  13. 15 0
      app/Model/SysOssTasks.php
  14. 36 0
      app/Service/ArchiveService.php
  15. 6 3
      app/Service/AuxiliaryAccountService.php
  16. 7 4
      app/Service/BIService.php
  17. 233 0
      app/Service/CustomFieldSettingService.php
  18. 7 2
      app/Service/DeviceDepreciationService.php
  19. 2 0
      app/Service/DeviceService.php
  20. 187 12
      app/Service/DeviceWorkService.php
  21. 82 16
      app/Service/EmployeeService.php
  22. 20 2
      app/Service/ExpenseClaimsService.php
  23. 2 2
      app/Service/ExportFileService.php
  24. 2 0
      app/Service/FeeService.php
  25. 29 0
      app/Service/FileUploadService.php
  26. 118 9
      app/Service/ImportService.php
  27. 35 2
      app/Service/ItemService.php
  28. 7 2
      app/Service/PLeaveOverService.php
  29. 17 5
      app/Service/PersonSalaryService.php
  30. 181 7
      app/Service/PersonWorkService.php
  31. 8 3
      app/Service/RuleSetService.php
  32. 9 0
      app/Service/SysMenuService.php
  33. 66 0
      app/Traits/HasCustomFields.php
  34. 9 0
      config/excel/device.php
  35. 24 2
      config/excel/employee.php
  36. 11 0
      config/excel/fee.php
  37. 22 0
      config/excel/feeOrder.php
  38. 55 0
      config/excel/item.php
  39. 3 3
      config/excel/monthDwOrder.php
  40. 36 3
      config/excel/monthPsOrder.php
  41. 10 3
      routes/api.php

+ 63 - 0
app/Http/Controllers/Api/CustomFieldSettingController.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace App\Http\Controllers\Api;
+
+use App\Service\CustomFieldSettingService;
+use Illuminate\Http\Request;
+
+//客户自定义表头
+class CustomFieldSettingController extends BaseController
+{
+    public function add(Request $request)
+    {
+        $service = new CustomFieldSettingService();
+        $user = $request->userData;
+        list($status,$data) = $service->add($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
+    public function edit(Request $request)
+    {
+        $service = new CustomFieldSettingService();
+        $user = $request->userData;
+        list($status,$data) = $service->edit($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+
+    }
+
+    public function del(Request $request)
+    {
+        $service = new CustomFieldSettingService();
+        $user = $request->userData;
+        list($status,$data) = $service->del($request->all(), $user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
+    public function list(Request $request)
+    {
+        $service = new CustomFieldSettingService();
+        $user = $request->userData;
+        list($status,$data) = $service->list($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+}

+ 13 - 0
app/Http/Controllers/Api/EmployeeController.php

@@ -72,6 +72,19 @@ class EmployeeController extends BaseController
         }
     }
 
+    public function getEmployeeImg(Request $request)
+    {
+        $service = new EmployeeService();
+        $user = $request->userData->toArray();
+        list($status,$data) = $service->getEmployeeImg($request->all(), $user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
     public function employeeDetail(Request $request)
     {
         $service = new EmployeeService();

+ 83 - 0
app/Jobs/ProcessOssTask.php

@@ -0,0 +1,83 @@
+<?php
+
+namespace App\Jobs;
+
+use App\Model\SysOssTasks;
+use App\Service\FileUploadService;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+use Symfony\Component\Console\Output\ConsoleOutput;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ProcessOssTask implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    public $timeout = 300;
+
+    public function __construct()
+    {
+
+    }
+
+    public function handle()
+    {
+        $currentBatchIds = [];
+
+        try {
+            $service = new FileUploadService();
+            SysOssTasks::where('status', 0)
+                ->chunk(50, function ($tasks) use (&$currentBatchIds, $service) {
+                    // 每次进入新的一批,先记录这一批的所有 ID
+                    $currentBatchIds = $tasks->pluck('id')->toArray();
+
+                    foreach ($tasks as $task) {
+                        $status_db = 2;
+                        if($task['type'] == 1){
+                            //删除
+                            list($status, $msg) = $service->createOssUploadOldSingle($task->url);
+                            if($status) $status_db = 1;
+
+                        }elseif ($task['type'] == 2){
+                            //新增
+                            list($status, $msg) = $service->createOssUploadSingle($task->url);
+                            if($status) $status_db = 1;
+                        }else{
+                            $msg = "单条失败:类型不存在";
+                        }
+
+                        $task->update([
+                            'status' => $status_db,
+                            'remark' => $msg,
+                            'upd_time' => time()
+                        ]);
+                    }
+
+                    // 处理完这一批后,清空当前批次 ID 记录
+                    $currentBatchIds = [];
+                });
+        } catch (\Throwable $e) {
+            $errorMessage = "当前批次执行中断:" . $e->getMessage();
+            if (!empty($currentBatchIds)) {
+                SysOssTasks::whereIn('id', $currentBatchIds)
+                    ->where('status', 0)
+                    ->update([
+                        'status' => 2,
+                        'remark' => $errorMessage,
+                        'upd_time' => time(),
+                    ]);
+            }
+
+            $this->delete();
+        }
+    }
+
+    protected function echoMessage(OutputInterface $output)
+    {
+        //输出消息
+        $output->writeln(json_encode($this->data));
+    }
+}

+ 23 - 0
app/Model/CustomFieldDefinitions.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace App\Model;
+
+use Illuminate\Database\Eloquent\Model;
+
+// 自定义项设置表
+class CustomFieldDefinitions extends DataScopeBaseModel
+{
+    protected $guarded = [];
+    protected $table = "custom_field_definitions"; //指定表
+    const CREATED_AT = 'crt_time';
+    const UPDATED_AT = 'upd_time';
+    protected $dateFormat = 'U';
+
+    public static $field = ['menu_id','id', 'top_depart_id', 'field_label', 'field_type','crt_time', 'crt_id'];
+    const TYPE_ONE = 1;
+    const TYPE_TWO = 2;
+    const State_Type = [
+        self::TYPE_ONE => '文本',
+        self::TYPE_TWO => '附件',
+    ];
+}

+ 21 - 0
app/Model/CustomFieldValue.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace App\Model;
+
+use Illuminate\Database\Eloquent\Model;
+
+// 自定义项存储表
+class CustomFieldValue extends DataScopeBaseModel
+{
+    protected $guarded = [];
+    protected $table = "custom_field_values"; //指定表
+    const CREATED_AT = 'crt_time';
+    const UPDATED_AT = 'upd_time';
+    protected $dateFormat = 'U';
+
+    public function definition()
+    {
+        // 即使你不需要它的数据,whereHas 也要靠这个方法生成 SQL 的 JOIN 或 EXISTS
+        return $this->belongsTo(CustomFieldDefinitions::class, 'definition_id');
+    }
+}

+ 1 - 1
app/Model/Device.php

@@ -15,7 +15,7 @@ class Device extends DataScopeBaseModel
         2 => '否',
     ];
 
-    public static $field = ['title','id','code','crt_id','crt_time','size','mark','is_use','type','power', 'in_time', 'original_value', 'initial_value'];
+    public static $field = ['position','title','id','code','crt_id','crt_time','size','mark','is_use','type','power', 'in_time', 'original_value', 'initial_value'];
 
     const type_one = 1;
     const type_two = 2;

+ 4 - 3
app/Model/Employee.php

@@ -11,7 +11,7 @@ class Employee extends DataScopeBaseModel
     const CREATED_AT = 'crt_time';
     const UPDATED_AT = 'upd_time';
     protected $dateFormat = 'U';
-    public static $field = ['number','mobile','title','id','is_admin', 'account', 'crt_time', 'state', 'education', 'major', 'id_card','p_title','sex', 'entrust_type', 'man_type'];
+    public static $field = ['number','mobile','title','id','is_admin', 'account', 'crt_time', 'state', 'education', 'major', 'id_card','p_title','sex', 'entrust_type', 'man_type', 'position'];
     const SPECIAL_ADMIN = 1;
     const USE = 1;
     const NOT_USE = 2;
@@ -58,11 +58,12 @@ class Employee extends DataScopeBaseModel
         self::TYPE_THREE => '休假',
     ];
 
+    //是否技术研究人员
     const MAN_TYPE_ONE = 1;
     const MAN_TYPE_TWO = 2;
     const Man_Type = [
-        self::MAN_TYPE_ONE => '研究人员',
-        self::MAN_TYPE_TWO => '技术人员',
+        self::MAN_TYPE_ONE => '',
+        self::MAN_TYPE_TWO => '',
     ];
 
     const WT_TYPE_ZERO = 0;

+ 14 - 0
app/Model/EmployeeFile.php

@@ -0,0 +1,14 @@
+<?php
+
+namespace App\Model;
+
+use Illuminate\Database\Eloquent\Model;
+
+class EmployeeFile extends Model
+{
+    protected $table = "employee_file"; //指定表
+    const CREATED_AT = null;
+    const UPDATED_AT = null;
+    protected $dateFormat = 'U';
+
+}

+ 1 - 1
app/Model/ExpenseClaimsDetails.php

@@ -4,7 +4,7 @@ namespace App\Model;
 
 class ExpenseClaimsDetails extends DataScopeBaseModel
 {
-    //设备日工时单
+    //项目费用
     protected $table = "expense_claims_details"; //指定表
     const CREATED_AT = 'crt_time';
     const UPDATED_AT = 'upd_time';

+ 8 - 1
app/Model/Fee.php

@@ -13,5 +13,12 @@ class Fee extends DataScopeBaseModel
     protected $dateFormat = 'U';
     const IS_UES = 1;//启用
 
-    public static $field = ['title','id','code','is_use','parent_id'];
+    public static $field = ['title','id','code','is_use','parent_id','is_other'];
+
+    const IS_OTHER_ZERO = 0;
+    const IS_OTHER_ONE = 1;
+    const IS_OTHER = [
+        self::IS_OTHER_ZERO => '否',
+        self::IS_OTHER_ONE => '是',
+    ];
 }

+ 35 - 4
app/Model/Item.php

@@ -2,6 +2,8 @@
 
 namespace App\Model;
 
+use App\Traits\HasCustomFields;
+
 class Item extends DataScopeBaseModel
 {
     protected $table = "item"; //指定表
@@ -10,15 +12,44 @@ class Item extends DataScopeBaseModel
     protected $dateFormat = 'U';
     const employee_column = "crt_id";
 
-    public static $field = ['title','id','code','start_time','end_time','mark','crt_id','crt_time','state'];
+    public static $field = ['title','id','code','start_time','end_time','mark','crt_id','crt_time','state','budget','charge_id','item_attribute','field'];
 
-    const TYPE_ONE = 1; // 在职
-    const TYPE_TWO = 2; // 离职
-    const TYPE_THREE = 3; // 休假
+    const TYPE_ONE = 1;
+    const TYPE_TWO = 2;
+    const TYPE_THREE = 3;
     const State_Type = [
         self::TYPE_ONE => '待开始',
         self::TYPE_TWO => '进行中',
         self::TYPE_THREE => '已完成',
     ];
 
+
+    const Attr_TYPE_ONE = 1;
+    const Attr_TYPE_TWO = 2;
+    const Item_Attribute = [
+        self::TYPE_ONE => '自主开发',
+        self::TYPE_TWO => '合作开发',
+    ];
+
+    // 引入自定义字段预加载关系
+    use HasCustomFields;
+
+    // 作用域 里面关联了客户自定义的表头数据
+    public function scopeWithCustomData($query, $menuId, $user)
+    {
+        if (empty($menuId)) {
+            return $query;
+        }
+
+        $top_depart_id = $user['top_depart_id'];
+
+        return $query->with(['customFieldValues' => function ($subQuery) use ($menuId, $top_depart_id) {
+            //只查询设置的id 和 自定义字段的值 还有业务单据/档案的id(这个必须要 预加载里没有主外键关联关系就查询失败)
+            $subQuery->select(['definition_id', 'field_value', 'model_id'])
+                ->where('top_depart_id', $top_depart_id)
+                ->whereHas('definition', function ($q) use ($menuId) {
+                    $q->where('menu_id', $menuId);
+                });
+        }]);
+    }
 }

+ 17 - 0
app/Model/SysModules.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\Model;
+
+use Illuminate\Database\Eloquent\Model;
+
+// 菜单是否能开启表头自定义项
+class SysModules extends DataScopeBaseModel
+{
+    protected $guarded = [];
+    protected $table = "sys_modules"; //指定表
+    const CREATED_AT = 'crt_time';
+    const UPDATED_AT = 'upd_time';
+    protected $dateFormat = 'U';
+
+    public static $field = ['menu_id','id'];
+}

+ 15 - 0
app/Model/SysOssTasks.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace App\Model;
+
+use Illuminate\Database\Eloquent\Model;
+
+class SysOssTasks extends Model
+{
+    protected $guarded = [];
+    protected $table = "sys_oss_tasks"; //指定表
+    const CREATED_AT = 'crt_time';
+    const UPDATED_AT = 'upd_time';
+    protected $dateFormat = 'U';
+
+}

+ 36 - 0
app/Service/ArchiveService.php

@@ -123,4 +123,40 @@ class ArchiveService extends Service
 
         return [true, ''];
     }
+
+    public static function fillIsArchive($months, $user)
+    {
+        // 1. 统一转为数组并去重(避免重复计算)
+        $rawMonths = is_array($months) ? array_unique($months) : [$months];
+
+        // 2. 建立 原始输入 -> 归一化时间戳 的中间映射
+        $lookup = [];
+        foreach ($rawMonths as $rawTime) {
+            $timestamp = is_numeric($rawTime) ? (int)$rawTime : strtotime($rawTime);
+            // 归一化为当月1号用于数据库对比
+            $lookup[$rawTime] = strtotime(date('Y-m-01', $timestamp));
+        }
+
+        // 3. 获取所有需要查询的归一化月份(去重后)
+        $normalizedToQuery = array_unique(array_values($lookup));
+
+        // 4. 批量查询数据库
+        $dbArchived = Archive::where('del_time', 0)
+            ->where('top_depart_id', $user['top_depart_id'])
+            ->whereIn('month', $normalizedToQuery)
+            ->pluck('month')
+            ->toArray();
+
+        // 转换成 Set 结构(以时间戳为键),提高后续查询性能
+        $dbArchivedSet = array_flip($dbArchived);
+
+        // 5. 构建最终 Map:原始时间戳 => 布尔值
+        $archiveMap = [];
+        foreach ($lookup as $rawTime => $normalizedTs) {
+            // 只要归一化后的时间戳在数据库里,原始时间戳对应的状态就是 true
+            $archiveMap[$rawTime] = isset($dbArchivedSet[$normalizedTs]);
+        }
+
+        return $archiveMap;
+    }
 }

+ 6 - 3
app/Service/AuxiliaryAccountService.php

@@ -20,7 +20,7 @@ class AuxiliaryAccountService extends Service
     public function auxiliaryAccountList($data,$user){
         $model = $this->setCommon($data, $user);
         $list = $this->limit($model,'',$data);
-        $list = $this->fillData($list);
+        $list = $this->fillData($list, $user);
 
         return [true, $list];
     }
@@ -43,15 +43,16 @@ class AuxiliaryAccountService extends Service
         return $model;
     }
 
-    public function fillData($data){
+    public function fillData($data, $user){
         if(empty($data['data'])) return $data;
 
         $emp = (new EmployeeService())->getEmployeeMap(array_unique(array_column($data['data'],'crt_id')));
-
+        $map = ArchiveService::fillIsArchive(array_unique(array_column($data['data'],'month')), $user);
         foreach ($data['data'] as $key => $value){
             $data['data'][$key]['crt_time'] = $value['crt_time'] ? date('Y-m-d H:i:s',$value['crt_time']) : '';
             $data['data'][$key]['crt_name'] = $emp[$value['crt_id']] ?? '';
             $data['data'][$key]['month'] =  $value['month'] ? date('Y-m',$value['month']) : '';
+            $data['data'][$key]['is_archive'] = $map[$value['month']] ?? false;
         }
 
         return $data;
@@ -420,6 +421,8 @@ class AuxiliaryAccountService extends Service
         $customer = $customer->toArray();
         $customer['crt_name'] = Employee::where('id',$customer['crt_id'])->value('title');
         $customer['crt_time'] =$this->utcTime($customer['crt_time']);
+        $map = ArchiveService::fillIsArchive($customer['month'], $user);
+        $customer['is_archive'] = $map[$customer['month']] ?? false;
         $customer['month'] =$this->utcTime($customer['month']);
 
         $details = $this->getDetail($data['id']);

+ 7 - 4
app/Service/BIService.php

@@ -3,6 +3,7 @@
 namespace App\Service;
 
 use App\Model\DailyPwOrder;
+use App\Model\DailyPwOrderDetails;
 use App\Model\ExpenseClaims;
 use App\Model\ExpenseClaimsDetails;
 use App\Model\Fee;
@@ -123,13 +124,15 @@ class BIService extends Service
         $monthly_ps_order_ids = array_keys($monthly_ps_order);
 
         $month_employee_salary = MonthlyPsOrderDetails::whereIn('main_id', $monthly_ps_order_ids)
-            ->select("salary", "main_id")
+            ->select("base_salary","performance_salary","other","bonus", "main_id")
             ->get()
             ->toArray();
         $total = 0;
         $salary_map = [];
         foreach ($month_employee_salary as $value) {
-            $currentAmount = (string)$value['salary'];
+            $currentAmount = bcadd($value['base_salary'], $value['performance_salary'],3);
+            $currentAmount = bcadd($currentAmount, $value['other'],3);
+            $currentAmount = bcadd($currentAmount, $value['bonus'],3);
 
             if(isset($monthly_ps_order[$value['main_id']])){
                 $month = $monthly_ps_order[$value['main_id']];
@@ -321,14 +324,14 @@ class BIService extends Service
         $item_map = Item::whereIn('id',array_unique(array_column($daily_pw,'item_id')))
             ->pluck('title','id')
             ->toArray();
-        $p = PLeaveOverOrderDetails::whereIn('main_id', array_column($daily_pw,'id'))
+        $p = DailyPwOrderDetails::whereIn('main_id', array_column($daily_pw,'id'))
             ->select("item_id", "total_work_min")
             ->get()
             ->toArray();
         $total = 0;
         $map = [];
         foreach ($p as $value) {
-            $total_min = (string)$value['total_min'];
+            $total_min = (string)$value['total_work_min'];
             if(isset($map[$value['item_id']])){
                 $map[$value['item_id']] = bcadd($map[$value['item_id']], $total_min,2);
             }else{

+ 233 - 0
app/Service/CustomFieldSettingService.php

@@ -0,0 +1,233 @@
+<?php
+
+namespace App\Service;
+
+use App\Model\CustomFieldDefinitions;
+use App\Model\SysMenu;
+use App\Model\SysModules;
+use App\Model\SysOssTasks;
+use Illuminate\Support\Facades\DB;
+
+class CustomFieldSettingService extends Service
+{
+    public function add($data,$user){
+        list($status,$msg) = $this->fieldRule($data,$user);
+        if(!$status) return [$status,$msg];
+
+        try {
+            DB::beginTransaction();
+
+            CustomFieldDefinitions::insert($data['data']);
+
+            DB::commit();
+        }catch (\Exception $exception){
+            DB::rollBack();
+            return [false,$exception->getMessage()];
+        }
+
+        return [true,''];
+    }
+
+    public function edit($data,$user){
+        list($status,$msg) = $this->fieldRule($data,$user, false);
+        if(!$status) return [$status,$msg];
+
+        try {
+            DB::beginTransaction();
+
+            $update = $msg['data'][0];
+            $model = new CustomFieldDefinitions();
+            $model->where('id', $data['id'])->update($update);
+
+            DB::commit();
+        }catch (\Exception $exception){
+            DB::rollBack();
+            return [false,$exception->getMessage()];
+        }
+
+        return [true,''];
+    }
+
+    public function del($data, $user){
+        if(empty($data['id']) || ! is_array($data['id'])) return [false,'ID不能为空或格式错误'];
+
+        try {
+            CustomFieldDefinitions::whereIn('id', $data['id'])->update([
+                'del_time' => time()
+            ]);
+
+        }catch (\Exception $exception){
+            DB::rollBack();
+            return [false,$exception->getMessage()];
+        }
+
+        return [true,''];
+    }
+
+    public function fieldCommon($data,$user, $field = []){
+        if(empty($field)) $field = CustomFieldDefinitions::$field;
+
+        $model = CustomFieldDefinitions::TopClear($user,$data);
+        $model = $model->where('del_time',0)
+            ->select($field)
+            ->orderby('id', 'desc');
+
+        if(! empty($data['field_label'])) $model->where('field_label', 'LIKE', '%'.$data['field_label'].'%');
+
+        return $model;
+    }
+
+    public function list($data, $user){
+        $model = $this->fieldCommon($data, $user);
+        $list = $this->limit($model,'',$data);
+        $list = $this->fillList($list);
+
+        return [true, $list];
+    }
+
+    public function fillList($data){
+        if(empty($data['data'])) return $data;
+
+        $emp = (new EmployeeService())->getEmployeeMap(array_unique(array_column($data['data'],'crt_id')));
+        $menu_map = (new SysMenuService())->getMap(array_unique(array_column($data['data'],'menu_id')));
+        foreach ($data['data'] as $key => $value){
+            $data['data'][$key]['crt_time'] = $value['crt_time'] ? date('Y-m-d H:i:s',$value['crt_time']) : '';
+            $data['data'][$key]['crt_name'] = $emp[$value['crt_id']] ?? '';
+            $data['data'][$key]['menu_title'] = $menu_map[$value['menu_id']] ?? '';
+            $data['data'][$key]['field_type_title'] = CustomFieldDefinitions::State_Type[$value['field_type']] ?? '';
+        }
+
+        return $data;
+    }
+
+    public function fieldRule(&$data,$user, $is_add = true){
+        if(empty($data['menu_id'])) return [false, '菜单不能为空'];
+        $data['top_depart_id'] = $user['top_depart_id'];
+
+        $title = SysMenu::where('id', $data['menu_id'])->value('title');
+        if(empty($title)) return [false, '菜单不存在或已被删除'];
+
+        $bool = SysModules::where('del_time',0)
+            ->where('top_depart_id', $data['top_depart_id'])
+            ->where('menu_id', $data['menu_id'])
+            ->exists();
+        if(! $bool) return [false, $title . '不允许增加自定义表头'];
+
+        if(empty($data['data'])) return [false,'自定义数据不能为空'];
+
+        $title = array_column($data['data'],'field_label');
+        $title = array_map(function($val) {
+            return $val !== null ? $val : 0;
+        }, $title);
+        $title_count = array_count_values($title);
+        foreach ($title as $value){
+            if(empty($value)) return [false,'自定义名称不能为空!'];
+            if($title_count[$value] > 1) return [false,'自定义名称不能重复'];
+        }
+
+        foreach ($data['data'] as $key => $value){
+            if(empty($value['field_label'])) return [false, '自定义项名称不能为空'];
+            if(empty($value['field_type'])) return [false, '自定义项类型不能为空'];
+            if(! isset(CustomFieldDefinitions::State_Type[$value['field_type']])) return [false, '自定义项类型错误'];
+
+            $top_depart_id = $user['top_depart_id'];
+            $data['data'][$key]['menu_id'] = $data['menu_id'];
+            $data['data'][$key]['top_depart_id'] = $top_depart_id;
+            $data['data'][$key]['upd_time'] = time();
+
+            if($is_add){
+                $data['data'][$key]['crt_time'] = time();
+                $fieldKey = 'f_' . substr(md5(microtime() . $value['field_label']), 0, 8);
+                $data['data'][$key]['field_key'] = $fieldKey;
+                $bool = CustomFieldDefinitions::where("field_label", $value['field_label'])
+                    ->where('top_depart_id', $top_depart_id)
+                    ->where('menu_id', $data['menu_id'])
+                    ->where('del_time',0)
+                    ->exists();
+            }else{
+                if($this->isEmpty($data,'id')) return [false,'id不能为空!'];
+                $bool = CustomFieldDefinitions::where("field_label", $value['field_label'])
+                    ->where('top_depart_id', $top_depart_id)
+                    ->where('menu_id', $data['menu_id'])
+                    ->where('id','<>',$data['id'])
+                    ->where('del_time',0)
+                    ->exists();
+                if(isset($value['field_key'])) unset($data['data'][$key]['field_key']);
+            }
+            if($bool) return [false,'自定义设置已存在'];
+        }
+
+        return [true, ''];
+    }
+
+    public static function syncCustomFieldData($id, $data, $user)
+    {
+        $top_depart_id = $user['top_depart_id'];
+        if (empty($data['menu_id'])) return [false, '菜单ID不能为空'];
+        $menuId = $data['menu_id'];
+
+        $definitions = CustomFieldDefinitions::where('menu_id', $menuId)
+            ->where('top_depart_id', $top_depart_id)
+            ->get();
+
+        $pendingTasks = [];
+
+        $time = time();
+        foreach ($definitions as $df) {
+            $key = $df->field_key;
+            // 如果请求数据中没有这个自定义字段的 key,跳过(支持局部更新)
+            if (!array_key_exists($key, $data)) continue;
+
+            $newVal = (string)($data[$key] ?? '');
+
+            // --- 附件类型逻辑处理 ---
+            if ($df->field_type === 'file') {
+                // 查询数据库中已有的旧路径
+                $oldVal = DB::table('custom_field_values')
+                        ->where('model_id', $id)
+                        ->where('definition_id', $df->id)
+                        ->value('field_value') ?? '';
+
+                if ($newVal !== $oldVal) {
+                    //产生一个【删除任务】
+                    if (!empty($oldVal)) {
+                        $pendingTasks[] = [
+                            'type'          => 1, // 删除旧文件
+                            'url'           => $oldVal,
+                            'top_depart_id' => $top_depart_id,
+                            'crt_time'      => $time,
+                        ];
+                    }
+
+                    //产生一个【新增同步任务】
+                    if (!empty($newVal)) {
+                        $pendingTasks[] = [
+                            'type'          => 2, // 新增同步到 OSS
+                            'url'           => $newVal,
+                            'top_depart_id' => $top_depart_id,
+                            'crt_time'      => $time,
+                        ];
+                    }
+                }
+            }
+
+            // --- 统一更新业务值表 (文本或附件路径) ---
+            DB::table('custom_field_values')->updateOrInsert(
+                [
+                    'model_id'      => $id,
+                    'definition_id' => $df->id,
+                    'top_depart_id' => $top_depart_id,
+                ],
+                [
+                    'field_value' => $newVal, // 如果是删除,这里存入空字符串
+                    'upd_time'    => time()
+                ]
+            );
+        }
+
+        //需要执行的oss的任务 调用这个方法的地方在 事务结束后触发下队列 todo
+        if(! empty($pendingTasks)) SysOssTasks::insert($pendingTasks);
+
+        return [true, ''];
+    }
+}

+ 7 - 2
app/Service/DeviceDepreciationService.php

@@ -155,6 +155,8 @@ class DeviceDepreciationService extends Service
         $customer = $customer->toArray();
         $customer['crt_name'] = Employee::where('id',$customer['crt_id'])->value('title');
         $customer['crt_time'] = $customer['crt_time'] ? date("Y-m-d H:i:s",$customer['crt_time']): '';
+        $map = ArchiveService::fillIsArchive($customer['month'], $user);
+        $customer['is_archive'] = $map[$customer['month']] ?? false;
         $customer['month'] = $customer['month'] ? date("Y-m",$customer['month']): '';
 
         $details = $this->getDetail($data['id']);
@@ -190,7 +192,7 @@ class DeviceDepreciationService extends Service
     public function monthlyDdOrderList($data,$user){
         $model = $this->monthlyDdOrderCommon($data, $user);
         $list = $this->limit($model,'',$data);
-        $list = $this->fillData($list);
+        $list = $this->fillData($list, $user);
 
         return [true, $list];
     }
@@ -233,14 +235,17 @@ class DeviceDepreciationService extends Service
         return [true, ''];
     }
 
-    public function fillData($data){
+    public function fillData($data, $user){
         if(empty($data['data'])) return $data;
 
         $emp = (new EmployeeService())->getEmployeeMap(array_unique(array_column($data['data'],'crt_id')));
+        $map = ArchiveService::fillIsArchive(array_unique(array_column($data['data'],'month')), $user);
+
         foreach ($data['data'] as $key => $value){
             $data['data'][$key]['crt_time'] = $value['crt_time'] ? date('Y-m-d H:i:s',$value['crt_time']) : '';
             $data['data'][$key]['month'] = $value['month'] ? date('Y-m',$value['month']) : '';
             $data['data'][$key]['crt_name'] = $emp[$value['crt_id']] ?? '';
+            $data['data'][$key]['is_archive'] = $map[$value['month']] ?? false;
         }
 
         return $data;

+ 2 - 0
app/Service/DeviceService.php

@@ -25,6 +25,7 @@ class DeviceService extends Service
             $model->type = $data['type'] ?? 0;
             $model->in_time = $data['in_time'] ?? 0;
             $model->power = $data['power'] ?? "";
+            $model->position = $data['position'] ?? "";
             $model->original_value = $data['original_value'] ?? 0;
             $model->initial_value = $data['initial_value'] ?? 0;
             $model->save();
@@ -54,6 +55,7 @@ class DeviceService extends Service
             $model->type = $data['type'] ?? 0;
             $model->in_time = $data['in_time'] ?? 0;
             $model->power = $data['power'] ?? "";
+            $model->position = $data['position'] ?? "";
             $model->original_value = $data['original_value'] ?? 0;
             $model->initial_value = $data['initial_value'] ?? 0;
             $model->top_depart_id = $data['top_depart_id'] ?? 0;

+ 187 - 12
app/Service/DeviceWorkService.php

@@ -165,6 +165,8 @@ class DeviceWorkService extends Service
         $customer = $customer->toArray();
         $customer['crt_name'] = Employee::where('id',$customer['crt_id'])->value('title');
         $customer['crt_time'] = $customer['crt_time'] ? date("Y-m-d H:i:s",$customer['crt_time']): '';
+        $map = ArchiveService::fillIsArchive($customer['month'], $user);
+        $customer['is_archive'] = $map[$customer['month']] ?? false;
         $customer['month'] = $customer['month'] ? date("Y-m",$customer['month']): '';
 
         $details = $this->getDetail($data['id']);
@@ -200,7 +202,7 @@ class DeviceWorkService extends Service
     public function monthlyDwOrderList($data,$user){
         $model = $this->monthlyDwOrderCommon($data, $user);
         $list = $this->limit($model,'',$data);
-        $list = $this->fillData($list);
+        $list = $this->fillData($list, $user);
 
         return [true, $list];
     }
@@ -232,9 +234,9 @@ class DeviceWorkService extends Service
 
         // 字段中文映射,用于报错
         $fieldNames = [
-            'total_days'    => '出勤总天数',
-            'rd_total_days' => '研发出勤天数',
-            'total_hours'   => '出勤总工时',
+            'total_days'    => '使用总天数',
+            'rd_total_days' => '研发使用天数',
+            'total_hours'   => '使用总工时',
             'rd_total_hours'=> '研发总工时'
         ];
 
@@ -261,18 +263,18 @@ class DeviceWorkService extends Service
             if ($sysData) {
                 // A. 内部逻辑:研发不能大于总额
                 if ($value['rd_total_days'] > $value['total_days']) {
-                    return [false, "第{$line}行:设备{$deviceDisplayName}的研发出勤天数不能大于出勤总天数"];
+                    return [false, "第{$line}行:设备{$deviceDisplayName}的研发使用天数不能大于使用总天数"];
                 }
                 if ($value['rd_total_hours'] > $value['total_hours']) {
-                    return [false, "第{$line}行:设备{$deviceDisplayName}的研发总工时不能大于出勤总工时"];
+                    return [false, "第{$line}行:设备{$deviceDisplayName}的研发总工时不能大于使用总工时"];
                 }
 
                 // B. 外部逻辑:不能超过系统根据日历算出的上限
                 if ($value['total_days'] != $sysData['attendance_days']) {
-                    return [false, "第{$line}行:设备{$deviceDisplayName}的出勤总天数({$value['total_days']})不等于当月标准天数({$sysData['attendance_days']})"];
+                    return [false, "第{$line}行:设备{$deviceDisplayName}的使用总天数({$value['total_days']})不等于系统核算天数({$sysData['attendance_days']})"];
                 }
                 if ($value['total_hours'] != $sysData['final_work_hour']) {
-                    return [false, "第{$line}行:设备{$deviceDisplayName}的出勤总工时({$value['total_hours']})不等于当月标准工时({$sysData['final_work_hour']})"];
+                    return [false, "第{$line}行:设备{$deviceDisplayName}的使用总工时({$value['total_hours']})不等于系统核算工时({$sysData['final_work_hour']})"];
                 }
             }
 
@@ -299,14 +301,17 @@ class DeviceWorkService extends Service
         return [true, ''];
     }
 
-    public function fillData($data){
+    public function fillData($data, $user){
         if(empty($data['data'])) return $data;
 
         $emp = (new EmployeeService())->getEmployeeMap(array_unique(array_column($data['data'],'crt_id')));
+        $map = ArchiveService::fillIsArchive(array_unique(array_column($data['data'],'month')), $user);
+
         foreach ($data['data'] as $key => $value){
             $data['data'][$key]['crt_time'] = $value['crt_time'] ? date('Y-m-d H:i:s',$value['crt_time']) : '';
             $data['data'][$key]['month'] = $value['month'] ? date('Y-m',$value['month']) : '';
             $data['data'][$key]['crt_name'] = $emp[$value['crt_id']] ?? '';
+            $data['data'][$key]['is_archive'] = $map[$value['month']] ?? false;
         }
 
         return $data;
@@ -535,6 +540,8 @@ class DeviceWorkService extends Service
         $item = Item::where('id', $customer['item_id'])->first();
         $customer['item_title'] = $item->title;
         $customer['item_code'] = $item->code;
+        $map = ArchiveService::fillIsArchive($customer['order_time'], $user);
+        $customer['is_archive'] = $map[$customer['order_time']] ?? false;
         $customer['order_time'] = $customer['order_time'] ? date("Y-m-d",$customer['order_time']): '';
 
         $details = $this->getDetailDaily($data['id']);
@@ -578,7 +585,7 @@ class DeviceWorkService extends Service
     public function dailyDwOrderList($data,$user){
         $model = $this->dailyDwOrderCommon($data, $user);
         $list = $this->limit($model,'',$data);
-        $list = $this->fillDataDaily($list);
+        $list = $this->fillDataDaily($list, $user);
 
         return [true, $list];
     }
@@ -688,11 +695,12 @@ class DeviceWorkService extends Service
         return [true, ''];
     }
 
-    public function fillDataDaily($data){
+    public function fillDataDaily($data, $user){
         if(empty($data['data'])) return $data;
 
         $emp = (new EmployeeService())->getEmployeeMap(array_unique(array_column($data['data'],'crt_id')));
         $item = (new ItemService())->getItemMap(array_unique(array_column($data['data'],'item_id')));
+        $map = ArchiveService::fillIsArchive(array_unique(array_column($data['data'],'month')), $user);
         foreach ($data['data'] as $key => $value){
             $data['data'][$key]['crt_time'] = $value['crt_time'] ? date('Y-m-d H:i:s',$value['crt_time']) : '';
             $data['data'][$key]['order_time'] = $value['order_time'] ? date('Y-m-d',$value['order_time']) : '';
@@ -700,6 +708,7 @@ class DeviceWorkService extends Service
             $item_tmp = $item[$value['item_id']] ?? [];
             $data['data'][$key]['item_title'] = $item_tmp['title'] ?? '';
             $data['data'][$key]['item_code'] = $item_tmp['code'] ?? '';
+            $data['data'][$key]['is_archive'] = $map[$value['order_time']] ?? false;
         }
 
         return $data;
@@ -1037,7 +1046,7 @@ class DeviceWorkService extends Service
         ]];
     }
 
-    private function calculateDailyDeviceAllocation($monthStart, $topDepartId, $user)
+    private function calculateDailyDeviceAllocation1($monthStart, $topDepartId, $user)
     {
         // 加载月度设备明细
         $monthlyOrder = DB::table('monthly_dw_order_details as d')
@@ -1189,6 +1198,172 @@ class DeviceWorkService extends Service
         return ['status' => true, 'data' => $previewList];
     }
 
+    private function calculateDailyDeviceAllocation($monthStart, $topDepartId, $user)
+    {
+        // 加载月度设备明细
+        $monthlyOrder = DB::table('monthly_dw_order_details as d')
+            ->join('monthly_dw_order as m', 'm.id', '=', 'd.main_id')
+            ->where('m.month', $monthStart)
+            ->where('m.top_depart_id', $topDepartId)
+            ->where('m.del_time', 0)
+            ->where('d.del_time', 0)
+            ->select('d.*')
+            ->get();
+
+        if ($monthlyOrder->isEmpty()) return ['status' => false, 'msg' => '未找到设备月度工时明细'];
+
+        $usedDeviceIds = $monthlyOrder->pluck('device_id')->unique()->toArray();
+
+        $deviceMap = DB::table('device')
+            ->whereIn('id', $usedDeviceIds)
+            ->pluck('title', 'id')
+            ->toArray();
+
+        // 加载分配规则
+        $ruleSet = DB::table('rule_set_details as rd')
+            ->join('rule_set as r', 'r.id', '=', 'rd.main_id')
+            ->where('r.month', $monthStart)
+            ->where('rd.type', 2) // 设备类型
+            ->where('rd.top_depart_id', $topDepartId)
+            ->where('r.del_time', 0)
+            ->where('rd.del_time', 0)
+            ->select('rd.*')
+            ->get();
+
+        $usedItemIds = $ruleSet->pluck('item_id')->unique()->toArray();
+        $itemMap = DB::table('item')
+            ->whereIn('id', $usedItemIds)
+            ->pluck('title', 'id')
+            ->toArray();
+
+        $ruleSetGrouped = $ruleSet->groupBy('data_id');
+
+        // 标准班次 & 日历
+        $standardWorkRanges = DB::table('work_range_details')
+            ->where('top_depart_id', $topDepartId)
+            ->where('del_time', 0)
+            ->get();
+        $dayMaxAvail = (int)$standardWorkRanges->sum('total_work_min');
+
+        $workDays = DB::table('calendar_details')
+            ->where('top_depart_id', $topDepartId)
+            ->where('month', $monthStart)
+            ->where('is_work', 1)
+            ->where('del_time', 0)
+            ->orderBy('time', 'asc')->get();
+
+        if ($workDays->isEmpty()) return ['status' => false, 'msg' => '未配置工作日历'];
+
+        // --- 2. 阶段一:计算每天每台设备分配的项目分钟数 (保持原逻辑) ---
+        $finalAlloc = [];
+        foreach ($monthlyOrder as $mDetail) {
+            $deviceId = $mDetail->device_id;
+            $deviceRules = $ruleSetGrouped->get($deviceId);
+            if (!$deviceRules) continue;
+
+            $remainingMin = (int)round((float)$mDetail->rd_total_hours * 60);
+            if ($remainingMin <= 0) continue;
+
+            foreach ($workDays as $dayInfo) {
+                if ($remainingMin <= 0) break;
+
+                $canAllocToday = min($remainingMin, $dayMaxAvail);
+                $allocatedInDay = 0;
+                $ruleCount = count($deviceRules);
+
+                foreach ($deviceRules as $index => $rule) {
+                    $rate = (float)$rule->rate / 100;
+                    if ($index === $ruleCount - 1) {
+                        $projectMin = $canAllocToday - $allocatedInDay;
+                    } else {
+                        $projectMin = (int)round($canAllocToday * $rate);
+                    }
+
+                    if ($projectMin > 0) {
+                        $finalAlloc[$dayInfo->time][$rule->item_id][$deviceId] = $projectMin;
+                        $allocatedInDay += $projectMin;
+                    }
+                }
+                $remainingMin -= $canAllocToday;
+            }
+        }
+
+        // --- 3. 阶段二:生成预览行并对齐 30 分钟 ---
+        $previewList = [];
+        $dailyDevicePools = [];
+        $tempMainIdCounter = 1;
+        $step = 30; // 步长
+
+        foreach ($finalAlloc as $dayTs => $projects) {
+            foreach ($projects as $itemId => $devices) {
+
+                $currentTempMainId = $tempMainIdCounter++;
+                $itemTitle = $itemMap[$itemId] ?? '未知项目';
+
+                foreach ($devices as $deviceId => $toAllocMin) {
+                    if (!isset($dailyDevicePools[$dayTs][$deviceId])) {
+                        $pool = [];
+                        foreach ($standardWorkRanges as $swr) {
+                            $pool[] = [
+                                's' => (int)($swr->start_time_hour * 60 + $swr->start_time_min),
+                                'e' => (int)($swr->end_time_hour * 60 + $swr->end_time_min)
+                            ];
+                        }
+                        $dailyDevicePools[$dayTs][$deviceId] = $pool;
+                    }
+
+                    $tempRem = (int)$toAllocMin;
+                    foreach ($dailyDevicePools[$dayTs][$deviceId] as &$p) {
+                        if ($tempRem <= 0) break;
+
+                        // 【改动】起始点对齐:非30分钟整点则向上对齐
+                        if ($p['s'] % $step != 0) {
+                            $p['s'] = ceil($p['s'] / $step) * $step;
+                        }
+
+                        $pMax = (int)($p['e'] - $p['s']);
+                        if ($pMax <= 0) continue;
+
+                        // 【改动】取量对齐逻辑
+                        if ($tempRem >= $step) {
+                            // 尽量按步长分配
+                            $take = min(floor($tempRem / $step) * $step, floor($pMax / $step) * $step);
+                            if ($take <= 0) continue;
+                        } else {
+                            // 最后一丁点零头
+                            $take = min($tempRem, $pMax);
+                        }
+
+                        $start = (int)$p['s'];
+                        $end = $start + $take;
+
+                        $previewList[] = [
+                            'temp_main_id'    => $currentTempMainId,
+                            'order_time'      => date('Y-m-d', $dayTs),
+                            'order_timestamp' => $dayTs,
+                            'item_id'         => $itemId,
+                            'item_title'      => $itemTitle,
+                            'device_id'       => $deviceId,
+                            'device_title'    => $deviceMap[$deviceId] ?? '未知设备',
+                            'start_time'      => sprintf('%02d:%02d', floor($start / 60), $start % 60),
+                            'end_time'        => sprintf('%02d:%02d', floor($end / 60), $end % 60),
+                            'start_hour'      => (int)floor($start / 60),
+                            'start_min'       => (int)($start % 60),
+                            'end_hour'        => (int)floor($end / 60),
+                            'end_min'         => (int)($end % 60),
+                            'total_work_min'  => $take,
+                        ];
+
+                        $tempRem -= $take;
+                        $p['s'] = $end;
+                    }
+                }
+            }
+        }
+
+        return ['status' => true, 'data' => $previewList];
+    }
+
     public function dailyDwOrderSave($data, $user)
     {
         $list = $data['list'] ?? [];

+ 82 - 16
app/Service/EmployeeService.php

@@ -6,7 +6,7 @@ use App\Model\CalendarDetails;
 use App\Model\Depart;
 use App\Model\Employee;
 use App\Model\EmployeeDepartPermission;
-use App\Model\EmployeeDetails;
+use App\Model\EmployeeFile;
 use App\Model\EmployeeRole;
 use App\Model\EmployeeWorkRange;
 use App\Model\Role;
@@ -28,6 +28,7 @@ class EmployeeService extends Service
             $model = new Employee();
             $model = $model->where('id',$user['id'])->first();
             $model->password = Hash::make($data['new_password']);
+            $model->p_version = $model->p_version + 1;
             $model->save();
 
             DB::commit();
@@ -68,6 +69,7 @@ class EmployeeService extends Service
             $model->education = $data['education'] ?? "";
             $model->id_card = $data['id_card'] ?? "";
             $model->major = $data['major'] ?? "";
+            $model->position = $data['position'] ?? "";
             $model->p_title = $data['p_title'] ?? "";
             $model->state = $data['state'] ?? 0;
             $model->is_admin = $data['is_admin'] ?? 0;
@@ -79,6 +81,7 @@ class EmployeeService extends Service
             }
             $model->save();
 
+            $time = time();
             EmployeeDepartPermission::where('employee_id',$data['id'])->delete();
             if(isset($data['depart'])){
                 $insert = [];
@@ -93,7 +96,7 @@ class EmployeeService extends Service
             }
 
             EmployeeRole::where('employee_id',$data['id'])->update([
-                'del_time' => time()
+                'del_time' => $time
             ]);
             if(isset($data['role'])){
                 $insert = [];
@@ -101,8 +104,7 @@ class EmployeeService extends Service
                     $insert[] = [
                         'employee_id' => $model->id,
                         'role_id' => $value,
-                        'crt_time' => time(),
-                        'upd_time' => time(),
+                        'crt_time' => $time,
                     ];
                 }
                 EmployeeRole::insert($insert);
@@ -121,19 +123,40 @@ class EmployeeService extends Service
                         'total_work_min' => $value['total_work_min'],
                         'top_depart_id' => $value['top_depart_id'],
                         'crt_time' => time(),
-                        'upd_time' => time(),
                     ];
                 }
                 EmployeeWorkRange::insert($insert);
             }
 
+            $old = EmployeeFile::where('del_time',0)
+                ->where('employee_id',$model->id)
+                ->pluck('file')
+                ->toArray();
+
+            EmployeeFile::where('del_time',0)
+                ->where('employee_id',$model->id)
+                ->update(['del_time' => $time]);
+
+            $new = [];
+            $insert = [];
+            if(! empty($data['img_url'])){
+                $insert[] = [
+                    'employee_id' => $model->id,
+                    'file' => $data['img_url'],
+                    'crt_time' => $time,
+                    'top_depart_id' => $user['top_depart_id'],
+                ];
+                EmployeeFile::insert($insert);
+                $new[] = $data['img_url'];
+            }
+
             DB::commit();
         }catch (\Exception $exception){
             DB::rollBack();
             return [false, $exception->getMessage()];
         }
 
-        return [true,''];
+        return [true, ['file' => ['new' => $new, 'old' => $old]]];
     }
 
     public function employeeAdd($data,$user){
@@ -150,6 +173,7 @@ class EmployeeService extends Service
             $model->education = $data['education'] ?? "";
             $model->id_card = $data['id_card'] ?? "";
             $model->major = $data['major'] ?? "";
+            $model->position = $data['position'] ?? "";
             $model->p_title = $data['p_title'] ?? "";
             $model->state = $data['state'] ?? 0;
             $model->crt_id = $user['id'];
@@ -161,6 +185,7 @@ class EmployeeService extends Service
             $model->top_depart_id = $data['top_depart_id'];
             $model->save();
 
+            $time = time();
             if(isset($data['depart'])){
                 $insert = [];
                 foreach ($data['depart'] as $value){
@@ -179,8 +204,7 @@ class EmployeeService extends Service
                     $insert[] = [
                         'employee_id' => $model->id,
                         'role_id' => $value,
-                        'crt_time' => time(),
-                        'upd_time' => time(),
+                        'crt_time' => $time,
                     ];
                 }
                 EmployeeRole::insert($insert);
@@ -197,20 +221,32 @@ class EmployeeService extends Service
                         'end_time_min' => $value['end_time_min'],
                         'total_work_min' => $value['total_work_min'],
                         'top_depart_id' => $value['top_depart_id'],
-                        'crt_time' => time(),
-                        'upd_time' => time(),
+                        'crt_time' => $time,
                     ];
                 }
                 EmployeeWorkRange::insert($insert);
             }
 
+            $new = [];
+            $insert = [];
+            if(! empty($data['img_url'])){
+                $insert[] = [
+                    'employee_id' => $model->id,
+                    'file' => $data['img_url'],
+                    'top_depart_id' => $value['top_depart_id'],
+                    'crt_time' => $time,
+                ];
+                EmployeeFile::insert($insert);
+                $new[] = $data['img_url'];
+            }
+
             DB::commit();
         }catch (Exception $e){
             DB::rollBack();
             return [false, $e->getMessage()];
         }
 
-        return [true,''];
+        return [true, ['file' => ['new' => $new]]];
     }
 
     public function employeeDel($data){
@@ -229,23 +265,52 @@ class EmployeeService extends Service
             EmployeeDepartPermission::whereIn('employee_id',$data['id'])->delete();
             EmployeeWorkRange::whereIn('employee_id',$data['id'])->delete();
 
+            $old = EmployeeFile::where('del_time',0)
+                ->whereIn('employee_id',$data['id'])
+                ->pluck('file')
+                ->toArray();
+
             DB::commit();
         }catch (\Exception $exception){
             DB::rollBack();
             return [false,$exception->getMessage()];
         }
 
-        return [true, ''];
+        return [true,  ['file' => ['old' => $old]]];
     }
 
     public function employeeDetail($data, $user){
         if(empty($data['id'])) return [false,'人员id不能为空'];
         list($status, $return) = $this->employeeList(['id' => [$data['id']]], $user);
         $user = $return['data'][0] ?? [];
+        list($img, $img2) = $this->showUserImg($data['id']);
+        $user['img_url'] = $img;
+        $user['img_url_show'] = $img2;
 
         return [true, $user];
     }
 
+    public function getEmployeeImg($data, $user){
+        list($img, $img_str) = $this->showUserImg($user['id']);
+        return [true, ['img_url' => $img_str]];
+    }
+
+    private function showUserImg($id){
+        $file = EmployeeFile::where('del_time',0)
+            ->where('employee_id',$id)
+            ->pluck('file')
+            ->toArray();
+        $file = $file[0] ?? "";
+        $img_str = "";
+        $timeStamp = 86400;
+        if(! empty($file)){
+            $fileUploadService = new FileUploadService();
+            $img_str = $fileUploadService->getFileShow($file, $timeStamp);
+        }
+
+        return [$file, $img_str];
+    }
+
     public function employeeCommon($data,$user, $field = []){
         if(empty($field)) $field = Employee::$field;
 
@@ -569,6 +634,7 @@ class EmployeeService extends Service
         if(! empty($data['sex']) && ! isset(Employee::SEX_TYPE[$data['sex']])) return [false, '性别不存在'];
         if(empty($data['mobile'])) return [false,'联系电话不能为空'];
 //        if(! $this->isValidPhone($data['mobile'])) return [false, '手机号码格式错误'];
+        if(empty($data['major'])) return [false, '专业领域不能为空'];
         if(! empty($data['education']) && ! isset(Employee::Education[$data['education']])) return [false, '学历不存在'];
         if(empty($data['id_card'])) return [false, '身份证号不能为空'];
         if(empty($data['depart'])) return [false,'部门不能为空'];
@@ -580,10 +646,10 @@ class EmployeeService extends Service
             if(mb_strlen($data['password']) < 6) return [false, '密码长度不得小于6位长度'];
             if(empty($data['role'])) return [false, '角色不能为空'];
         }
-        if(empty($data['man_type'])) return [false,'人员类别不能为空'];
-        if(! isset(Employee::Man_Type[$data['man_type']])) return [false,'人员类别不存在'];
-//        if(empty($data['entrust_type'])) return [false,'委托方式不能为空'];
-//        if(! isset(Employee::WT_Type[$data['entrust_type']])) return [false,'委托方式不存在'];
+        if(empty($data['man_type'])) return [false,'是否为技术研究人员不能为空'];
+        if(! isset(Employee::Man_Type[$data['man_type']])) return [false,'是否为技术研究人员不存在'];
+        if(! isset($data['entrust_type'])) return [false,'委托方式不能为空'];
+        if(! isset(Employee::WT_Type[$data['entrust_type']])) return [false,'委托方式不存在'];
         $data['top_depart_id'] = $user['top_depart_id'];
 
         if(! empty($data['work_range'])){

+ 20 - 2
app/Service/ExpenseClaimsService.php

@@ -16,7 +16,7 @@ class ExpenseClaimsService extends Service
     {
         $model = $this->expenseClaimsSetCommon($data, $user);
         $list = $this->limit($model, '', $data);
-        $list = $this->fillData($list);
+        $list = $this->fillData($list, $user);
 
         return [true, $list];
     }
@@ -40,16 +40,18 @@ class ExpenseClaimsService extends Service
         return $model;
     }
 
-    public function fillData($data)
+    public function fillData($data, $user)
     {
         if (empty($data['data'])) return $data;
 
         $emp = (new EmployeeService())->getEmployeeMap(array_unique(array_column($data['data'], 'crt_id')));
+        $map = ArchiveService::fillIsArchive(array_unique(array_column($data['data'],'month')), $user);
 
         foreach ($data['data'] as $key => $value) {
             $data['data'][$key]['crt_time'] = $value['crt_time'] ? date('Y-m-d H:i:s', $value['crt_time']) : '';
             $data['data'][$key]['crt_name'] = $emp[$value['crt_id']] ?? '';
             $data['data'][$key]['month'] = $value['month'] ? date('Y-m', $value['month']) : '';
+            $data['data'][$key]['is_archive'] = $map[$value['month']] ?? false;
         }
 
         return $data;
@@ -99,6 +101,7 @@ class ExpenseClaimsService extends Service
                     'employee_id' => $value['employee_id'] ?? "",
                     'fee_id' => $value['fee_id'] ?? 0,
                     'amount' => $value['amount'] ?? 0,
+                    'entrust_amount' => $value['entrust_amount'] ?? 0,
                     'remark' => $value['remark'] ?? "",
                     'claim_date' => strtotime($value['claim_date']),
                     'entrust_type' => $value['entrust_type'] ?? 0,
@@ -155,6 +158,17 @@ class ExpenseClaimsService extends Service
             // 判断:claim_date 必须早于 month
             // 如果 claim_date 是 2026-02-28,而 month 是 2026-03-01,则校验通过
             if ($claimTime < $monthStart || $claimTime > $monthEnd) return [false, "第" . ($index + 1) . "行项费用产生日期必须在当前月份内(" . date("Y-m", $data['month']) . ")"];
+            if(! isset(ExpenseClaimsDetails::State_Type[$item['entrust_type']])) return [false, "第" . ($index + 1) . "委托类型不存在"];
+            $title = ExpenseClaimsDetails::State_Type[$item['entrust_type']];
+
+            if(! isset($item['entrust1_amount'])) return [false, "第" . ($index + 1) . "行{$title}金额不能存在"];
+            $res = $this->checkNumber($item['entrust1_amount'], 2, 'non-negative');
+            if (!$res['valid']) return [false, "第" . ($index + 1) . "行{$title}金额". $res['error']];
+            if(! isset($item['entrust2_amount'])) return [false, "第" . ($index + 1) . "行{$title}金额不能存在"];
+            $res = $this->checkNumber($item['entrust2_amount'], 2, 'non-negative');
+            if (!$res['valid']) return [false, "第" . ($index + 1) . "行{$title}金额". $res['error']];
+            $tmp = bcadd($item['entrust1_amount'], $item['entrust2_amount'],2);
+            if(floatval($tmp) > $item['amount']) return [false, "第" . ($index + 1) . "行委托金额总和不能超过费用金额"];
         }
 
         $query = ExpenseClaims::where('top_depart_id', $data['top_depart_id'])
@@ -254,6 +268,8 @@ class ExpenseClaimsService extends Service
         $customer = $customer->toArray();
         $customer['crt_name'] = Employee::where('id', $customer['crt_id'])->value('title');
         $customer['crt_time'] = $customer['crt_time'] ? date("Y-m-d H:i:s", $customer['crt_time']) : '';
+        $map = ArchiveService::fillIsArchive($customer['month'], $user);
+        $customer['is_archive'] = $map[$customer['month']] ?? false;
         $customer['month'] = $this->UtcTime($customer['month']);
         $details = $this->getDetail($data, $user);
         $customer["details"] = $details;
@@ -370,6 +386,8 @@ class ExpenseClaimsService extends Service
                 'fee_code' => $tmpFee ? $tmpFee->code : '',
                 'fee_title' => $tmpFee ? $tmpFee->title : '',
                 'amount' => $item->amount,
+                'entrust1_amount' => $item->entrust1_amount,
+                'entrust2_amount' => $item->entrust2_amount,
                 'claim_date' => $item->claim_date ? date('Y-m-d', $item->claim_date) : '',
                 'voucher_no' => $item->voucher_no,
                 'remark' => $item->remark,

+ 2 - 2
app/Service/ExportFileService.php

@@ -128,7 +128,7 @@ class ExportFileService extends Service
             //订单数据
             $list = $service->fillDepartList($list, $user, true);
             //返回数据
-            $this->fillData($list['data'], $column, $return);
+            $this->fillData($list, $column, $return);
         });
 
         return [true, $this->saveExportData($return,$header)];
@@ -469,7 +469,7 @@ class ExportFileService extends Service
         list($status, $result) = $service->employeeDayHourStatistic($data, $user);
         if (!$status) return $result;
 
-        $sourceData = collect($result['data']);
+        $sourceData = collect($result);
 
         // 2. 按月份分组,准备多 Sheet 数据
         // 结构:[ '2024-04' => [ 'days' => 30, 'data' => [...] ], ... ]

+ 2 - 0
app/Service/FeeService.php

@@ -30,6 +30,7 @@ class FeeService extends Service
                 $model->parent_id = $value['parent_id'];
                 $model->title = $value['title'];
                 $model->code = $value['code'];
+                $model->is_other = $value['is_other'] ?? Fee::IS_OTHER_ZERO;
                 $model->top_depart_id = $value['top_depart_id'];
                 $model->save();
             }
@@ -107,6 +108,7 @@ class FeeService extends Service
                 $tmp = $map[$value['parent_id']] ?? "";
                 $list['data'][$key]['parent_code'] = $tmp['code'] ?? "";
                 $list['data'][$key]['parent_title'] = $tmp['title'] ?? "";
+                $list['data'][$key]['is_other_title'] = Fee::IS_OTHER[$value['is_other']] ?? "";
             }
         }
 

+ 29 - 0
app/Service/FileUploadService.php

@@ -166,4 +166,33 @@ class FileUploadService extends Service
 
         Storage::disk('public')->delete($dir);
     }
+
+    public function createOssUploadSingle($file){
+        $filename = str_replace(FileUploadService::string.FileUploadService::string2,'', $file);
+        $timestamp = substr($filename, 0, 8); // 截取前八位数字
+        $date = \DateTime::createFromFormat('Ymd', $timestamp);
+        $date = $date->format('Y-m-d');
+        $dir = FileUploadService::tmp_dir . '/' . $date . '/' . $filename;
+        if(Storage::disk('public')->exists($dir)){
+            $realPath = storage_path() . "/app/public/" . $dir;
+            $savePath = self::string3 . $date . '/' . $filename;
+            list($status,$msg) = (new OssService())->uploadFile($realPath,$savePath);
+            if($status) Storage::disk('public')->delete($dir);
+
+            return [$status, $msg];
+        }else{
+            return [false, '本地文件不存在'];
+        }
+    }
+
+    public function createOssUploadOldSingle($file){
+        $filename = str_replace(FileUploadService::string.FileUploadService::string2,'',$file);
+        $timestamp = substr($filename, 0, 8); // 截取前八位数字
+        $date = \DateTime::createFromFormat('Ymd', $timestamp);
+        $date = $date->format('Y-m-d');
+        $delPath = self::string3 . $date . '/' . $filename;
+
+        list($status,$msg) = (new OssService())->deleteFile($delPath);
+        return [$status, $msg];
+    }
 }

+ 118 - 9
app/Service/ImportService.php

@@ -10,6 +10,7 @@ use App\Model\Depart;
 use App\Model\Device;
 use App\Model\Employee;
 use App\Model\EmployeeDepartPermission;
+use App\Model\ExpenseClaimsDetails;
 use App\Model\Fee;
 use App\Model\Item;
 use App\Model\ItemDetails;
@@ -298,6 +299,7 @@ class ImportService extends Service
         $eductionIdx = array_search('education', $keys);
         $stateIdx = array_search('state', $keys);
         $manIdx = array_search('man_type', $keys);
+        $entrustIdx = array_search('entrust_type', $keys);
         $depIdx = array_search('depart_code', $keys);
 
         $code_map = $this->getEmployeeList($array, $user, $codeIdx);
@@ -312,6 +314,7 @@ class ImportService extends Service
         $e_map = array_flip(Employee::Education);
         $state_map = array_flip(Employee::State_Type);
         $man_map = array_flip(Employee::Man_Type);
+        $wt_map = array_flip(Employee::WT_Type);
 
         // 获取所有标记为 is_main 的列索引,用于一致性对比
         $mainColIndices = [];
@@ -380,6 +383,14 @@ class ImportService extends Service
                 $array[$rowIndex][$manIdx] = $man_map[$man_text];
             }
 
+            //委托类型
+            $wt_text = trim($rowValue[$entrustIdx] ?? '');
+            if (!isset($wt_map[$wt_text])) {
+                $errors[] = "第{$displayLine}行:委托方式[{$wt_text}]无效";
+            } else {
+                $array[$rowIndex][$entrustIdx] = $wt_map[$wt_text];
+            }
+
             // 4. 解析部门 (分行模式)
             if ($depIdx !== false && !empty($rowValue[$depIdx])) {
                 $mNum = trim($rowValue[$depIdx]);
@@ -828,6 +839,9 @@ class ImportService extends Service
             $main_tmp = [];
             foreach ($value as $k => $val) {
                 if (!empty($table_config[$k]['is_main'])) {
+
+                    if ($table_config[$k]['key'] == 'employee_title') continue;
+
                     $main_tmp[$table_config[$k]['key']] = $val;
                 }
             }
@@ -925,10 +939,19 @@ class ImportService extends Service
         $keys = array_column($table_config, 'key');
         $codeIdx = array_search('code', $keys);
         $stateIdx = array_search('state', $keys);
+        $attrIdx = array_search('item_attribute', $keys);
+        $budgetIdx = array_search('budget', $keys);
         $typeIdx = array_search('type', $keys);     // 明细类型列
         $code2Idx = array_search('code_2', $keys);  // 资产/工号列
         $dateIdx = array_search('start_time', $keys);
         $date2Idx = array_search('end_time', $keys);
+        $empIdx = array_search('charge_id', $keys);
+
+        $allEmpNumbers = array_filter(array_unique(array_column($array, $empIdx)));
+        $dbEmps = Employee::where('del_time', 0)
+            ->whereIn('number', $allEmpNumbers)
+            ->where('top_depart_id', $user['top_depart_id'])
+            ->pluck('id', 'number')->toArray();
 
         $code_map = $this->getItemList($array, $user, $codeIdx);
         // 这里的 getDataList 需要适配平铺后的 code_2 列
@@ -940,6 +963,7 @@ class ImportService extends Service
         $mainDataConsistency = []; // 一致性校验
 
         $state_type_map = array_flip(Item::State_Type);
+        $attr_map = array_flip(Item::Item_Attribute);
         $detail_type_map = array_flip(ItemDetails::$type_name);
 
         // 识别主表字段列索引
@@ -951,6 +975,7 @@ class ImportService extends Service
         foreach ($array as $rowIndex => $rowValue) {
             $displayLine = $rowIndex + 1;
             $valCode = trim($rowValue[$codeIdx] ?? '');
+            $valEmp = trim($rowValue[$empIdx] ?? '');
             if ($valCode === '') continue;
 
             // --- A. 主表一致性校验 ---
@@ -977,6 +1002,28 @@ class ImportService extends Service
                 $array[$rowIndex][$stateIdx] = $state_type_map[$state_text];
             }
 
+            $attr_text = $rowValue[$attrIdx] ?? '';
+            if (!isset($attr_map[$attr_text])) {
+                $errors[] = "第{$displayLine}行:项目属性[{$attr_text}]无效";
+            } else {
+                $array[$rowIndex][$attrIdx] = $attr_map[$attr_text];
+            }
+
+            $budget_text = $rowValue[$budgetIdx] ?? 0;
+            if(! empty($budget_text)){
+                $res = $this->checkNumber($budget_text, 2, 'non-negative');
+                if (!$res['valid']) $errors[] = "第{$displayLine}行预算:" . $res['error'];
+            }
+
+            // D. 人员校验
+            if(! empty($valEmp)){
+                if (!isset($dbEmps[$valEmp])) {
+                    $errors[] = "第{$displayLine}行:人员工号[{$valEmp}]不存在";
+                } else {
+                    $array[$rowIndex][$empIdx] = $dbEmps[$valEmp];
+                }
+            }
+
             // 日期转换
             foreach ([$dateIdx, $date2Idx] as $dIdx) {
                 if ($dIdx !== false && !empty($rowValue[$dIdx])) {
@@ -1155,6 +1202,7 @@ class ImportService extends Service
         $keys = array_column($table_config, 'key');
         $codeIdx = array_search('code', $keys);
         $parentIdx = array_search('parent_id', $keys);
+        $isOtherIdx = array_search('is_other', $keys);
 
         // 1. 获取基础数据
         list($dbFeeMap, $excelCodesMap) = $this->getFeeList($array, $user, $codeIdx);
@@ -1163,6 +1211,8 @@ class ImportService extends Service
         $update = [];
         $parent_code_map = [];
 
+        $is_other_map = array_flip(Fee::IS_OTHER);
+
         // 2. 建立 Excel 内部父子关系映射(用于环路追溯)
         $currentExcelMap = [];
         foreach ($array as $row) {
@@ -1185,6 +1235,14 @@ class ImportService extends Service
                 $update[$rowIndex] = $dbFeeMap[$valCode]['id'];
             }
 
+            // 其他费用
+            $is_other_text = $value[$isOtherIdx] ?? '';
+            if (!isset($is_other_map[$is_other_text])) {
+                $errors[] = "第{$displayLine}行:是否其他费用[{$is_other_text}]无效";
+            } else {
+                $array[$rowIndex][$isOtherIdx] = $is_other_map[$is_other_text];
+            }
+
             if ($valParentCode !== '') {
                 // --- A. 存在性校验 ---
                 // 这里会检查 006 是否在数据库,或者是否在本次 Excel 的其他行中
@@ -1926,9 +1984,12 @@ class ImportService extends Service
         $monthIdx = array_search('month', $keys);
         $empIdx = array_search('employee_id', $keys);
 
-        $numIdx = array_search('salary', $keys);
+        $numIdx = array_search('base_salary', $keys);
         $num2Idx = array_search('social_insurance', $keys);
         $num3Idx = array_search('public_housing_fund', $keys);
+        $num4Idx = array_search('performance_salary', $keys);
+        $num5Idx = array_search('bonus', $keys);
+        $num6Idx = array_search('other', $keys);
 
         // 1. 预加载基础数据
         $allEmpNumbers = array_filter(array_unique(array_column($array, $empIdx)));
@@ -2040,9 +2101,12 @@ class ImportService extends Service
 
             // E. 数字格式校验
             $numChecks = [
-                $numIdx => '工资总额',
+                $numIdx => '基本工资',
                 $num2Idx => '社保',
-                $num3Idx => '公积金'
+                $num3Idx => '公积金',
+                $num4Idx => '绩效工资',
+                $num5Idx => '奖金',
+                $num6Idx => '其他',
             ];
             foreach ($numChecks as $idx => $label) {
                 $res = $this->checkNumber($row[$idx] ?? 0, 2, 'non-negative');
@@ -3659,12 +3723,12 @@ class ImportService extends Service
                     if ($fieldKey == 'fee_id') $fieldVal = $maps['fees'][$fieldVal] ?? 0;
 
                     // 转换枚举值(委托方式、是否定位到人)
-                    if ($fieldKey == 'entrust_type') {
-                        $fieldVal = array_search($fieldVal, \App\Model\ExpenseClaimsDetails::State_Type) ?: 0;
-                    }
-                    if ($fieldKey == 'expense_type') {
-                        $fieldVal = array_search($fieldVal, \App\Model\ExpenseClaimsDetails::State_Type_2) ?: 0;
-                    }
+//                    if ($fieldKey == 'entrust_type') {
+//                        $fieldVal = array_search($fieldVal, \App\Model\ExpenseClaimsDetails::State_Type) ?: 0;
+//                    }
+//                    if ($fieldKey == 'expense_type') {
+//                        $fieldVal = array_search($fieldVal, \App\Model\ExpenseClaimsDetails::State_Type_2) ?: 0;
+//                    }
 
                     // 注意:claim_date 已经在 check 方法里转成了时间戳,这里直接赋值即可
                     $detailTmp[$fieldKey] = $fieldVal;
@@ -3741,6 +3805,14 @@ class ImportService extends Service
         $itemIdx = array_search('item_id', $keys);
         $feeIdx = array_search('fee_id', $keys);
         $claimDateIdx = array_search('claim_date', $keys);
+        $amountIdx = array_search('amount', $keys);
+        $eAmountIdx = array_search('entrust1_amount', $keys);
+        $e2AmountIdx = array_search('entrust2_amount', $keys);
+        $eTypeIdx = array_search('entrust_type', $keys);
+        $exTypeIdx = array_search('expense_type', $keys);
+
+        $state_map = array_flip(ExpenseClaimsDetails::State_Type);
+        $ex_map = array_flip(ExpenseClaimsDetails::State_Type_2);
 
         $topDepartId = $user['top_depart_id'];
         $errors = [];
@@ -3843,6 +3915,43 @@ class ImportService extends Service
                     $errors[] = "第{$line}行:月份[" . date('Y-m', $valMonthTs) . "]已存在单据[{$existingMonthsMap[$valMonthTs]}],请填写该单号进行编辑";
                 }
             }
+
+            // 数字格式校验
+            $amount = $row[$amountIdx];
+            $res = $this->checkNumber($amount, 2, 'non-negative');
+            if (!$res['valid']) $errors[] = "第{$line}行费用金额:" . $res['error'];
+
+            // 委托方式
+            $e_text = $row[$eTypeIdx] ?? '';
+            if (!isset($state_map[$e_text])) {
+                $errors[] = "第{$line}行:委托方式[{$e_text}]无效";
+            } else {
+                $v_tmp = $state_map[$e_text];
+                $array[$rowIndex][$eTypeIdx] = $v_tmp;
+            }
+
+            $e1_text = $row[$eAmountIdx];
+            $res = $this->checkNumber($e1_text, 2, 'non-negative');
+            if (!$res['valid']) {
+                $errors[] = "第{$line}行:境内委托金额" . $res['error'];
+            }
+            $e2_text = $row[$e2AmountIdx];
+            $res = $this->checkNumber($e2_text, 2, 'non-negative');
+            if (!$res['valid']) {
+                $errors[] = "第{$line}行:境外委托金额" . $res['error'];
+            }
+
+            $tmp = bcadd($e1_text, $e2_text,2);
+            if(floatval($tmp) > $amount) return [false, "第{$line}行委托金额总和不能超过费用金额"];
+
+            //是否定位到人
+            $ex_text = $row[$exTypeIdx] ?? '';
+            if (!isset($ex_map[$ex_text])) {
+                $errors[] = "第{$line}行:是否定位到人[{$ex_text}]无效";
+            } else {
+                $v_tmp = $ex_map[$ex_text];
+                $array[$rowIndex][$exTypeIdx] = $v_tmp;
+            }
         }
 
         $maps = ['emps' => $dbEmps, 'items' => $dbItems, 'fees' => $dbFees];

+ 35 - 2
app/Service/ItemService.php

@@ -24,6 +24,10 @@ class ItemService extends Service
             $model->start_time = $data['start_time'] ?? 0;
             $model->end_time = $data['end_time'] ?? 0;
             $model->state = $data['state'] ?? 0;
+            $model->budget = $data['budget'] ?? 0;
+            $model->charge_id = $data['charge_id'] ?? 0;
+            $model->field = $data['field'] ?? 0;
+            $model->item_attribute = $data['item_attribute'] ?? 0;
             $model->save();
 
             $time = time();
@@ -55,12 +59,22 @@ class ItemService extends Service
             $model->start_time = $data['start_time'] ?? 0;
             $model->end_time = $data['end_time'] ?? 0;
             $model->state = $data['state'] ?? 0;
+            $model->budget = $data['budget'] ?? 0;
+            $model->charge_id = $data['charge_id'] ?? 0;
+            $model->field = $data['field'] ?? 0;
+            $model->item_attribute = $data['item_attribute'] ?? 0;
             $model->crt_id = $user['id'];
             $model->top_depart_id = $data['top_depart_id'];
             $model->save();
 
             $this->saveDetail($model->id, time(), $data);
 
+            list($status, $msg) = CustomFieldSettingService::syncCustomFieldData($model->id,$data,$user);
+            if (!$status) {
+                DB::rollBack();
+                return [false, $msg];
+            }
+
             DB::commit();
         }catch (\Exception $exception){
             DB::rollBack();
@@ -192,12 +206,14 @@ class ItemService extends Service
         if($this->isEmpty($data,'id')) return [false,'请选择数据!'];
         $customer = Item::where('del_time',0)
             ->where('id',$data['id'])
+            ->withCustomData($data['menu_id'] ?? 0, $user) //预加载表头自定义项数据
             ->first();
         if(empty($customer)) return [false,'项目不存在或已被删除'];
         $customer = $customer->toArray();
         $customer['start_time']  = ! empty($customer['start_time']) ? date("Y-m-d", $customer['start_time']) : "";
         $customer['end_time']  = ! empty($customer['end_time']) ? date("Y-m-d", $customer['end_time']) : "";
         $customer['crt_name'] = Employee::where('id',$customer['crt_id'])->value('title');
+        $customer['charge_name'] = Employee::where('id',$customer['charge_id'])->value('title');
         $customer['crt_time'] = $customer['crt_time'] ? date("Y-m-d H:i:s",$customer['crt_time']): '';
         $customer['state_title'] = Employee::State_Type[$customer['state']] ?? '';
 
@@ -212,6 +228,7 @@ class ItemService extends Service
 
         $model = Item::TopClear($user,$data);
         $model = $model->where('del_time',0)
+            ->withCustomData($data['menu_id'] ?? 0, $user) //预加载表头自定义项数据
             ->select($field)
             ->orderby('id', 'desc');
 
@@ -266,6 +283,12 @@ class ItemService extends Service
         list($status, $msg) = $this->checkArrayRepeat($data['device_list'],'data_id','设备');
         if(! $status) return [false, $msg];
 
+        if(isset($data['budget'])){
+            $res = $this->checkNumber($data['budget'],2,'non-negative');
+            if(! $res['valid']) return [false,'预算:' . $res['error']];
+        }
+        if(! empty($data['item_attribute']) && ! isset(Item::Item_Attribute[$data['item_attribute']])) return [false, '项目属性不存在'];
+
         if($is_add){
             $bool = Item::where('code',$data['code'])
                 ->where('top_depart_id', $data['top_depart_id'])
@@ -287,12 +310,13 @@ class ItemService extends Service
     public function fillData($data){
         if(empty($data['data'])) return $data;
 
-        $emp = (new EmployeeService())->getEmployeeMap(array_unique(array_column($data['data'],'crt_id')));
+        $emp = (new EmployeeService())->getEmployeeMap(array_unique(array_merge_recursive(array_column($data['data'],'charge_id'), array_column($data['data'],'crt_id'))));
         foreach ($data['data'] as $key => $value){
             $data['data'][$key]['crt_time'] = $value['crt_time'] ? date('Y-m-d H:i:s',$value['crt_time']) : '';
             $data['data'][$key]['start_time'] = $value['start_time'] ? date('Y-m-d',$value['start_time']) : '';
             $data['data'][$key]['end_time'] = $value['end_time'] ? date('Y-m-d',$value['end_time']) : '';
             $data['data'][$key]['crt_name'] = $emp[$value['crt_id']] ?? '';
+            $data['data'][$key]['charge_name'] = $emp[$value['charge_id']] ?? '';
             $data['data'][$key]['state_title'] = Item::State_Type[$value['state']] ?? "";
         }
 
@@ -314,6 +338,9 @@ class ItemService extends Service
 
         // 2. 获取主表状态映射
         $stateMap = \App\Model\Item::State_Type;
+        $attrMap = \App\Model\Item::Item_Attribute;
+
+        $empMap = Employee::whereIn('id', array_unique(array_column($dataArray,'charge_id')))->get()->keyBy('id');
 
         foreach ($dataArray as $main) {
             $itemId = $main['id'];
@@ -321,7 +348,13 @@ class ItemService extends Service
 
             // 3. 提取并格式化主表信息
             $mainInfo = $main;
-            $mainInfo['state_title'] = $stateMap[$main['state'] ?? ''] ?? '未知';
+            $mainInfo['state_title'] = $stateMap[$main['state'] ?? ''] ?? '';
+            $mainInfo['item_attribute_title'] = $attrMap[$main['item_attribute'] ?? ''] ?? '';
+
+            $tmpEmp = $empMap[$main['charge_id']] ?? null;
+            $mainInfo['charge_number'] = $tmpEmp ? $tmpEmp->number : '';
+            $mainInfo['charge_name'] = $tmpEmp ? $tmpEmp->title : '';
+
             // 日期格式化
             $mainInfo['start_time'] = !empty($main['start_time']) ? date('Y-m-d', $main['start_time']) : '';
             $mainInfo['end_time']   = !empty($main['end_time']) ? date('Y-m-d', $main['end_time']) : '';

+ 7 - 2
app/Service/PLeaveOverService.php

@@ -165,6 +165,8 @@ class PLeaveOverService extends Service
         $customer['type_title'] = PLeaveOverOrder::Type[$customer['type']] ?? "";
         $customer['crt_name'] = Employee::where('id',$customer['crt_id'])->value('title');
         $customer['crt_time'] = $customer['crt_time'] ? date("Y-m-d H:i:s",$customer['crt_time']): '';
+        $map = ArchiveService::fillIsArchive($customer['order_time'], $user);
+        $customer['is_archive'] = $map[$customer['order_time']] ?? false;
         $customer['order_time'] = $customer['order_time'] ? date("Y-m-d",$customer['order_time']): '';
 
         $details = $this->getDetail($data['id']);
@@ -201,7 +203,7 @@ class PLeaveOverService extends Service
     public function pLeaveOverList($data,$user){
         $model = $this->pLeaveOverCommon($data, $user);
         $list = $this->limit($model,'',$data);
-        $list = $this->fillData($list);
+        $list = $this->fillData($list, $user);
 
         return [true, $list];
     }
@@ -387,15 +389,18 @@ class PLeaveOverService extends Service
         return [true, ''];
     }
 
-    public function fillData($data){
+    public function fillData($data, $user){
         if(empty($data['data'])) return $data;
 
         $emp = (new EmployeeService())->getEmployeeMap(array_unique(array_column($data['data'],'crt_id')));
+        $map = ArchiveService::fillIsArchive(array_unique(array_column($data['data'],'month')), $user);
+
         foreach ($data['data'] as $key => $value){
             $data['data'][$key]['crt_time'] = $value['crt_time'] ? date('Y-m-d H:i:s',$value['crt_time']) : '';
             $data['data'][$key]['order_time'] = $value['order_time'] ? date('Y-m-d',$value['order_time']) : '';
             $data['data'][$key]['crt_name'] = $emp[$value['crt_id']] ?? '';
             $data['data'][$key]['type_title'] = PLeaveOverOrder::Type[$value['type']] ?? "";
+            $data['data'][$key]['is_archive'] = $map[$value['order_time']] ?? false;
         }
 
         return $data;

+ 17 - 5
app/Service/PersonSalaryService.php

@@ -71,7 +71,10 @@ class PersonSalaryService extends Service
                 $unit[] = [
                     'main_id' => $id,
                     'employee_id' => $value['employee_id'],
-                    'salary' => $value['salary'],
+                    'base_salary' => $value['base_salary'],
+                    'performance_salary' => $value['performance_salary'],
+                    'bonus' => $value['bonus'],
+                    'other' => $value['other'],
                     'social_insurance' => $value['social_insurance'],
                     'public_housing_fund' => $value['public_housing_fund'],
                     'crt_time' => $time,
@@ -85,7 +88,7 @@ class PersonSalaryService extends Service
     private function getDetail($id){
         $data = MonthlyPsOrderDetails::where('del_time',0)
             ->where('main_id', $id)
-            ->select('employee_id', 'salary', 'social_insurance', 'public_housing_fund')
+            ->select('employee_id', 'base_salary', 'social_insurance', 'public_housing_fund', 'performance_salary', 'bonus', 'other')
             ->get()->toArray();
 
         $id = array_column($data,'employee_id');
@@ -208,12 +211,18 @@ class PersonSalaryService extends Service
         if(empty($data['details'])) return [false, '人员月度工时单明细不能为空'];
         foreach ($data['details'] as $key => $value){
             if(empty($value['employee_id'])) return [false, '人员不能为空'];
-            $res = $this->checkNumber($value['salary'],2,'non-negative');
-            if(! $res['valid']) return [false,'工资:' . $res['error']];
+            $res = $this->checkNumber($value['base_salary'],2,'non-negative');
+            if(! $res['valid']) return [false,'基本工资:' . $res['error']];
+            $res = $this->checkNumber($value['performance_salary'],2,'non-negative');
+            if(! $res['valid']) return [false,'绩效工资:' . $res['error']];
             $res = $this->checkNumber($value['social_insurance'],2,'non-negative');
             if(! $res['valid']) return [false,'社保:' . $res['error']];
             $res = $this->checkNumber($value['public_housing_fund'],2,'non-negative');
             if(! $res['valid']) return [false,'公积金:' . $res['error']];
+            $res = $this->checkNumber($value['bonus'],2,'non-negative');
+            if(! $res['valid']) return [false,'奖金工资:' . $res['error']];
+            $res = $this->checkNumber($value['other'],2,'non-negative');
+            if(! $res['valid']) return [false,'其他:' . $res['error']];
             $data['details'][$key]['top_depart_id'] = $data['top_depart_id'];
         }
         list($status, $msg) = $this->checkArrayRepeat($data['details'],'employee_id','人员');
@@ -305,9 +314,12 @@ class PersonSalaryService extends Service
             $res[$item->main_id][] = [
                 'employee_number' => $tmpEmp ? $tmpEmp->number : '',
                 'employee_title' => $tmpEmp ? $tmpEmp->title : '',
-                'salary'     => $item->salary,
+                'base_salary'     => $item->base_salary,
                 'social_insurance'  => $item->social_insurance,
                 'public_housing_fund'    => $item->public_housing_fund,
+                'performance_salary'     => $item->performance_salary,
+                'bonus'     => $item->bonus,
+                'other'     => $item->other,
             ];
         }
         return $res; // 返回 [main_id => [detail_row, detail_row]]

+ 181 - 7
app/Service/PersonWorkService.php

@@ -221,15 +221,25 @@ class PersonWorkService extends Service
         list($status, $systemStats) = (new EmployeeService())->getEmployeesMonthStats($empIds, $data['month'], $user);
         if (!$status) return [false, $systemStats]; // 如果日历未设置,直接拦截
 
+        // 字段中文映射,用于报错
+        $fieldNames = [
+            'total_days'    => '出勤总天数',
+            'rd_total_days' => '研发出勤总天数',
+            'total_hours'   => '出勤总工时',
+            'rd_total_hours'=> '研发总工时'
+        ];
+
         foreach ($data['details'] as $key => $value) {
+            $lineTitle = "第" . ($key + 1) . "行:";
             if (empty($value['employee_id'])) return [false, '人员不能为空'];
             $empId = $value['employee_id'];
 
             // 基础数字格式检查
-            foreach (['total_days', 'rd_total_days', 'total_hours', 'rd_total_hours'] as $field) {
+            foreach ($fieldNames as $field => $title) {
+                if(! isset($value[$field])) return [false, $lineTitle . $title . "不存在"];
                 $precision = 2;
                 $res = $this->checkNumber($value[$field], $precision, 'non-negative');
-                if (!$res['valid']) return [false, $value['employee_title'] . "的" . $field . ":" . $res['error']];
+                if (!$res['valid']) return [false, $lineTitle . $value['employee_title'] . "的" . $title . ":" . $res['error']];
             }
 
             // --- 业务逻辑校验:出勤天数与工时合法性 ---
@@ -237,22 +247,22 @@ class PersonWorkService extends Service
             if ($sysData) {
                 // 1. 研发天数不能大于出勤总天数
                 if ($value['rd_total_days'] > $value['total_days']) {
-                    return [false, "第" . ($key + 1) . "行:研发出勤天数不能大于出勤总天数"];
+                    return [false, $lineTitle . "研发出勤天数不能大于出勤总天数"];
                 }
 
                 // 2. 研发工时不能大于出勤总工时
                 if ($value['rd_total_hours'] > $value['total_hours']) {
-                    return [false, "第" . ($key + 1) . "行:研发总工时不能大于出勤总工时"];
+                    return [false, $lineTitle . "研发总工时不能大于出勤总工时"];
                 }
 
                 // 4. 校验出勤总天数是否超过了系统计算的上限
                 if ($value['total_days'] != $sysData['attendance_days']) {
-                    return [false, "人员[{$empId}]填写的出勤总天数({$value['total_days']})不等于系统核算的天数({$sysData['attendance_days']})"];
+                    return [false, $lineTitle . "人员[{$empId}]填写的出勤总天数({$value['total_days']})不等于系统核算的天数({$sysData['attendance_days']})"];
                 }
 
                 //校验出勤总工时是否超过了系统计算的上限
                 if ($value['total_hours'] != $sysData['final_work_hour']) {
-                    return [false, "人员[{$empId}]填写的出勤总工时({$value['total_hours']})不等于系统核算的工时({$sysData['final_work_hour']})"];
+                    return [false, $lineTitle . "人员[{$empId}]填写的出勤总工时({$value['total_hours']})不等于系统核算的工时({$sysData['final_work_hour']})"];
                 }
             }
 
@@ -1089,7 +1099,7 @@ class PersonWorkService extends Service
      * @param array $user 用户信息
      * @return array
      */
-    private function calculateDailyAllocation($monthStart, $topDepartId, $user)
+    private function calculateDailyAllocation1($monthStart, $topDepartId, $user)
     {
         $monthEnd = strtotime('+1 month', $monthStart) - 1;
         $now = time();
@@ -1249,6 +1259,170 @@ class PersonWorkService extends Service
         return ['status' => true, 'data' => $previewList];
     }
 
+    private function calculateDailyAllocation($monthStart, $topDepartId, $user)
+    {
+        $monthEnd = strtotime('+1 month', $monthStart) - 1;
+        $now = time();
+
+        // --- 1. 基础数据加载 (保持原逻辑) ---
+        $monthlyOrder = DB::table('monthly_pw_order_details as d')
+            ->join('monthly_pw_order as m', 'm.id', '=', 'd.main_id')
+            ->leftJoin('employee as e', 'e.id', '=', 'd.employee_id')
+            ->where('m.month', $monthStart)
+            ->where('m.top_depart_id', $topDepartId)
+            ->where('m.del_time', 0)
+            ->where('d.del_time', 0)
+            ->select('d.*', 'e.title as employee_title')
+            ->get();
+
+        if ($monthlyOrder->isEmpty()) return ['status' => false, 'msg' => '未找到该月份的月度工时明细'];
+
+        $empNameMap = $monthlyOrder->pluck('employee_title', 'employee_id')->toArray();
+        $empIds = array_keys($empNameMap);
+
+        $itemIds = DB::table('rule_set_details as rd')
+            ->join('rule_set as r', 'r.id', '=', 'rd.main_id')
+            ->where('r.month', $monthStart)
+            ->where('r.top_depart_id', $topDepartId)
+            ->pluck('rd.item_id')->unique()->toArray();
+
+        $itemMap = DB::table('item')->whereIn('id', $itemIds)->pluck('title', 'id')->toArray();
+
+        $ruleSet = DB::table('rule_set_details as rd')
+            ->join('rule_set as r', 'r.id', '=', 'rd.main_id')
+            ->where('r.month', $monthStart)
+            ->where('rd.type', 1)
+            ->where('r.del_time', 0)
+            ->where('rd.del_time', 0)
+            ->select('rd.*')
+            ->get()
+            ->groupBy('data_id');
+
+        $empWorkRanges = DB::table('employee_work_range')
+            ->whereIn('employee_id', $empIds)
+            ->where('top_depart_id', $topDepartId)
+            ->get()->groupBy('employee_id');
+        $standardWorkRanges = DB::table('work_range_details')->where('top_depart_id', $topDepartId)
+            ->where('del_time', 0)->get();
+        $allDays = DB::table('calendar_details')->where('top_depart_id', $topDepartId)
+            ->where('month', $monthStart)
+            ->where('del_time', 0)
+            ->orderBy('time', 'asc')
+            ->get();
+        $leaveOverData = DB::table('p_leave_over_order_details as d')->join('p_leave_over_order as m', 'd.main_id', '=', 'm.id')
+            ->whereBetween('m.order_time', [$monthStart, $monthEnd])
+            ->where('d.top_depart_id', $topDepartId)
+            ->where('m.del_time', 0)
+            ->select('d.*', 'm.order_time', 'm.type as main_type')
+            ->get()->groupBy(['employee_id', 'order_time']);
+
+        // --- 2. 阶段一:计算每个人每天在每个项目上应分配的整数分钟数 (保持原逻辑) ---
+        $finalAlloc = [];
+        foreach ($monthlyOrder as $mDetail) {
+            $empId = $mDetail->employee_id;
+            $empRules = $ruleSet->get($empId);
+            if (!$empRules) continue;
+
+            $empRemainingMin = (int)round((float)$mDetail->rd_total_hours * 60);
+            if ($empRemainingMin <= 0) continue;
+
+            foreach ($allDays as $dayInfo) {
+                if ($empRemainingMin <= 0) break;
+                $dayTs = $dayInfo->time;
+                $tempPool = $this->buildAvailablePool($empId, $dayTs, $allDays, $empWorkRanges, $standardWorkRanges, $leaveOverData);
+                $dayAvailableMin = 0;
+                foreach ($tempPool as $p) { $dayAvailableMin += (int)($p['e'] - $p['s']); }
+                if ($dayAvailableMin <= 0) continue;
+
+                $canAllocToday = min($empRemainingMin, $dayAvailableMin);
+                $allocatedInDay = 0;
+                $ruleCount = count($empRules);
+
+                foreach ($empRules as $index => $rule) {
+                    $rate = (float)$rule->rate / 100;
+                    if ($index === $ruleCount - 1) {
+                        $projectMin = $canAllocToday - $allocatedInDay;
+                    } else {
+                        $projectMin = (int)round($canAllocToday * $rate);
+                    }
+                    if ($projectMin > 0) {
+                        $finalAlloc[$dayTs][$rule->item_id][$empId] = $projectMin;
+                        $allocatedInDay += $projectMin;
+                    }
+                }
+                $empRemainingMin -= $canAllocToday;
+            }
+        }
+
+        // --- 3. 阶段二:打散到具体时间点并对齐 30 分钟步长 ---
+        $previewList = [];
+        $dailyEmpTimePools = [];
+        $tempMainIdCounter = 1;
+        $step = 30; // 设定步长为 30 分钟
+
+        foreach ($finalAlloc as $dayTs => $projects) {
+            foreach ($projects as $itemId => $employees) {
+                $currentTempMainId = $tempMainIdCounter++;
+                $itemTitle = $itemMap[$itemId] ?? '未知项目';
+
+                foreach ($employees as $empId => $toAllocMin) {
+                    if (!isset($dailyEmpTimePools[$dayTs][$empId])) {
+                        $dailyEmpTimePools[$dayTs][$empId] = $this->buildAvailablePool($empId, $dayTs, $allDays, $empWorkRanges, $standardWorkRanges, $leaveOverData);
+                    }
+
+                    $tempRem = (int)$toAllocMin;
+                    foreach ($dailyEmpTimePools[$dayTs][$empId] as &$p) {
+                        if ($tempRem <= 0) break;
+
+                        // 1. 起始点对齐:如果当前开始时间不是 30 的倍数,向上取整
+                        // 例如:08:15 (495min) -> 08:30 (510min)
+                        if ($p['s'] % $step != 0) {
+                            $p['s'] = ceil($p['s'] / $step) * $step;
+                        }
+
+                        $pMax = (int)($p['e'] - $p['s']);
+                        if ($pMax <= 0) continue;
+
+                        // 2. 确定本次扣除的分钟数
+                        // 如果剩余量大于步长,取步长的倍数;如果小于步长,则一次性取完
+                        if ($tempRem >= $step) {
+                            $take = min(floor($tempRem / $step) * $step, floor($pMax / $step) * $step);
+                            // 如果计算出的 take 为 0(说明当前池子不够 30min),则跳过该池子
+                            if ($take <= 0) continue;
+                        } else {
+                            $take = min($tempRem, $pMax);
+                        }
+
+                        $realStart = (int)$p['s'];
+                        $realEnd = $realStart + $take;
+
+                        $previewList[] = [
+                            'temp_main_id'    => $currentTempMainId,
+                            'order_time'      => date('Y-m-d', $dayTs),
+                            'order_timestamp' => $dayTs,
+                            'item_id'         => $itemId,
+                            'item_title'      => $itemTitle,
+                            'employee_id'     => $empId,
+                            'employee_title'  => $empNameMap[$empId] ?? '未知人员',
+                            'start_time'      => sprintf('%02d:%02d', floor($realStart / 60), $realStart % 60),
+                            'end_time'        => sprintf('%02d:%02d', floor($realEnd / 60), $realEnd % 60),
+                            'start_hour'      => (int)floor($realStart / 60),
+                            'start_min'       => (int)($realStart % 60),
+                            'end_hour'        => (int)floor($realEnd / 60),
+                            'end_min'         => (int)($realEnd % 60),
+                            'total_work_min'  => $take,
+                        ];
+
+                        $tempRem -= $take;
+                        $p['s'] = $realEnd;
+                    }
+                }
+            }
+        }
+
+        return ['status' => true, 'data' => $previewList];
+    }
+
     public function dailyPwOrderSave($data, $user)
     {
         $list = $data['list'] ?? [];

+ 8 - 3
app/Service/RuleSetService.php

@@ -208,9 +208,11 @@ class RuleSetService extends Service
             ->first();
         if(empty($customer)) return [false,'规则配置单不存在或已被删除'];
         $customer = $customer->toArray();
-        $customer['month']  = ! empty($customer['month']) ? date("Y-m", $customer['month']) : "";
         $customer['crt_name'] = Employee::where('id',$customer['crt_id'])->value('title');
         $customer['crt_time'] = $customer['crt_time'] ? date("Y-m-d H:i:s",$customer['crt_time']): '';
+        $map = ArchiveService::fillIsArchive($customer['month'], $user);
+        $customer['is_archive'] = $map[$customer['month']] ?? false;
+        $customer['month']  = ! empty($customer['month']) ? date("Y-m", $customer['month']) : "";
 
         $details = $this->getDetail($data['id']);
         $customer = array_merge($customer, $details);
@@ -245,7 +247,7 @@ class RuleSetService extends Service
     public function ruleSetList($data,$user){
         $model = $this->ruleSetCommon($data, $user);
         $list = $this->limit($model,'',$data);
-        $list = $this->fillData($list);
+        $list = $this->fillData($list, $user);
 
         return [true, $list];
     }
@@ -340,14 +342,17 @@ class RuleSetService extends Service
         return [true, ''];
     }
 
-    public function fillData($data){
+    public function fillData($data, $user){
         if(empty($data['data'])) return $data;
 
         $emp = (new EmployeeService())->getEmployeeMap(array_unique(array_column($data['data'],'crt_id')));
+        $map = ArchiveService::fillIsArchive(array_unique(array_column($data['data'],'month')), $user);
+
         foreach ($data['data'] as $key => $value){
             $data['data'][$key]['crt_time'] = $value['crt_time'] ? date('Y-m-d H:i:s',$value['crt_time']) : '';
             $data['data'][$key]['month'] = $value['month'] ? date('Y-m',$value['month']) : '';
             $data['data'][$key]['crt_name'] = $emp[$value['crt_id']] ?? '';
+            $data['data'][$key]['is_archive'] = $map[$value['month']] ?? false;
         }
 
         return $data;

+ 9 - 0
app/Service/SysMenuService.php

@@ -420,4 +420,13 @@ class SysMenuService extends Service
             ->select('menu_id','type')
             ->get()->toArray();
     }
+
+    public function getMap($menu_id){
+        $menu_id = array_filter((array)$menu_id);
+        if (empty($menu_id)) return [];
+
+        return SysMenu::whereIn('id', $menu_id)
+            ->pluck('title', 'id')
+            ->toArray();
+    }
 }

+ 66 - 0
app/Traits/HasCustomFields.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace App\Traits;
+
+use App\Model\CustomFieldValue;
+
+trait HasCustomFields
+{
+    /**
+     * 基础关联:获取该模型实例关联的所有自定义值
+     */
+    public function customFieldValues()
+    {
+        return $this->hasMany(CustomFieldValue::class, 'model_id')->where('del_time', 0);
+    }
+
+    /**
+     * 获取指定菜单下的自定义字段数据
+     */
+    public function getCustomFieldsByMenu($menuId)
+    {
+        // 预加载已经在 itemCommon 中完成,这里 get() 会直接使用缓存数据
+        return $this->customFieldValues
+            ->filter(function ($item) use ($menuId) {
+                // 过滤属于该菜单的定义(因为 with 预加载了 definition)
+                return $item->definition && $item->definition->menu_id == $menuId;
+            })
+            ->map(function ($item) {
+                $definition = $item->definition;
+                $value = $item->field_value;
+
+                $url = null;
+                // 1. 处理文件 URL 逻辑
+                if ($definition->field_type === 'file' && $value) {
+                    // 调用之前写的 getFileShow,并传入当前模型的 top_depart_id
+//                    $url = (new FileUploadService())->getFileShow($value, $this->top_depart_id);
+                    $url = "";
+                }
+
+                return [
+                    'definition_id' => $definition->id,
+                    'key'           => $definition->field_key,
+                    'label'         => $definition->field_label,
+                    'type'          => $definition->field_type,
+                    'value'         => $value,
+                    'url'           => $url, // 这一步是前端展示的关键
+                ];
+            })
+            ->values(); // 重置集合索引
+    }
+
+    /**
+     * 辅助方法:快速获取某个特定字段的值
+     */
+    public function getCustomFieldValueByKey($menuId, $fieldKey)
+    {
+        $item = $this->customFieldValues
+            ->first(function ($val) use ($menuId, $fieldKey) {
+                return $val->definition
+                    && $val->definition->menu_id == $menuId
+                    && $val->definition->field_key == $fieldKey;
+            });
+
+        return $item ? $item->field_value : null;
+    }
+}

+ 9 - 0
config/excel/device.php

@@ -53,6 +53,15 @@ return [
             'unique' => false,
             'default' => "",
         ],
+        [
+            'key' =>'position',
+            'export' =>'position',
+            'value' => '位置',
+            'required' => false,
+            'is_main' => true,
+            'unique' => false,
+            'default' => "",
+        ],
         [
             'key' =>'power',
             'export' =>'power',

+ 24 - 2
config/excel/employee.php

@@ -61,11 +61,22 @@ return [
             'key' =>'major',
             'export' =>'major',
             'value' => '专业领域',
-            'required' => false,
+            'required' => true,
             'is_main' => true,
             'default' => "",
             'unique' => false,
             'enums' => [],
+            'comments' => '必填'
+        ],
+        [
+            'key' =>'position',
+            'export' =>'position',
+            'value' => '岗位',
+            'required' => false,
+            'is_main' => true,
+            'unique' => false,
+            'enums' => [],
+            'default' => "",
             'comments' => ''
         ],
         [
@@ -93,7 +104,7 @@ return [
         [
             'key' =>'man_type',
             'export' =>'man_type_title',
-            'value' => '人员类别',
+            'value' => '是否为技术研究人员',
             'required' => true,
             'is_main' => true,
             'unique' => false,
@@ -101,6 +112,17 @@ return [
             'default' => 0,
             'comments' => "必填"
         ],
+        [
+            'key' =>'entrust_type',
+            'export' =>'entrust_type_title',
+            'value' => '委托方式',
+            'required' => false,
+            'is_main' => true,
+            'unique' => false,
+            'enums' => array_values(\App\Model\Employee::WT_Type),
+            'default' => \App\Model\Employee::WT_TYPE_ZERO,
+            'comments' => "必填"
+        ],
         [
             'key' =>'state',
             'export' =>'state_title',

+ 11 - 0
config/excel/fee.php

@@ -24,6 +24,17 @@ return [
             'enums' => [],
             'comments' => '必填'
         ],
+        [
+            'key' =>'is_other',
+            'export' =>'is_other_title',
+            'value' => '是否其他费用',
+            'required' => true,
+            'is_main' => true,
+            'default' => \App\Model\Fee::IS_OTHER_ZERO,
+            'unique' => false,
+            'enums' => array_values(\App\Model\Fee::IS_OTHER),
+            'comments' => '必填'
+        ],
         [
             'key' =>'parent_id',
             'export' =>'parent_code',

+ 22 - 0
config/excel/feeOrder.php

@@ -145,6 +145,28 @@ return [
             'default' => \App\Model\ExpenseClaimsDetails::TYPE_ZERO,
             'comments' => '选填'
         ],
+        [
+            'key' =>'entrust1_amount',
+            'export' =>'entrust1_amount',
+            'value' => '境内委托金额',
+            'required' => false,
+            'is_main' => false,
+            'unique' => false,
+            'enums' => [],
+            'default' => 0,
+            'comments' => ''
+        ],
+        [
+            'key' =>'entrust2_amount',
+            'export' =>'entrust3_amount',
+            'value' => '境外委托金额',
+            'required' => false,
+            'is_main' => false,
+            'unique' => false,
+            'enums' => [],
+            'default' => 0,
+            'comments' => ''
+        ],
         [
             'key' =>'expense_type',
             'export' =>'expense_type_title',

+ 55 - 0
config/excel/item.php

@@ -24,6 +24,61 @@ return [
             'enums' => [],
             'comments' => '必填'
         ],
+        [
+            'key' =>'budget',
+            'export' =>'budget',
+            'value' => '预算',
+            'required' => false,
+            'is_main' => true,
+            'default' => 0,
+            'unique' => false,
+            'enums' => [],
+            'comments' => ''
+        ],
+        [
+            'key' =>'field',
+            'export' =>'field',
+            'value' => '领域',
+            'required' => false,
+            'is_main' => true,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => ''
+        ],
+        [
+            'key' =>'item_attribute',
+            'export' =>'item_attribute_title',
+            'value' => '项目属性',
+            'required' => true,
+            'is_main' => true,
+            'default' => \App\Model\Item::Attr_TYPE_ONE,
+            'unique' => false,
+            'enums' => array_values(\App\Model\Item::Item_Attribute),
+            'comments' => '必填'
+        ],
+        [
+            'key' =>'charge_id',
+            'export' =>'charge_number',
+            'value' => '负责人工号',
+            'required' => false,
+            'is_main' => true,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '选填(填写负责人工号)'
+        ],
+        [
+            'key' =>'employee_title',
+            'export' =>'charge_name',
+            'value' => '负责人名称',
+            'required' => false,
+            'is_main' => true,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '选填(便于操作人查看编码对应的名称)'
+        ],
         [
             'key' =>'start_time',
             'export' =>'start_time',

+ 3 - 3
config/excel/monthDwOrder.php

@@ -49,7 +49,7 @@ return [
         [
             'key' =>'total_days',
             'export' =>'total_days',
-            'value' => '出勤总天数',
+            'value' => '使用总天数',
             'required' => true,
             'is_main' => false,
             'default' => 0,
@@ -60,7 +60,7 @@ return [
         [
             'key' =>'rd_total_days',
             'export' =>'rd_total_days',
-            'value' => '研发出勤总天数',
+            'value' => '研发使用总天数',
             'required' => true,
             'is_main' => false,
             'default' => 0,
@@ -71,7 +71,7 @@ return [
         [
             'key' =>'total_hours',
             'export' =>'total_hours',
-            'value' => '出勤总工时',
+            'value' => '使用总工时',
             'required' => true,
             'is_main' => false,
             'default' => 0,

+ 36 - 3
config/excel/monthPsOrder.php

@@ -47,9 +47,9 @@ return [
             'comments' => '选填(便于操作人查看编码对应的名称)'
         ],
         [
-            'key' =>'salary',
-            'export' =>'salary',
-            'value' => '工资总额',
+            'key' =>'base_salary',
+            'export' =>'base_salary',
+            'value' => '基本工资',
             'required' => true,
             'is_main' => false,
             'default' => 0,
@@ -57,6 +57,17 @@ return [
             'enums' => [],
             'comments' => '必填'
         ],
+        [
+            'key' =>'performance_salary',
+            'export' =>'performance_salary',
+            'value' => '绩效工资',
+            'required' => false,
+            'is_main' => false,
+            'default' => 0,
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填'
+        ],
         [
             'key' =>'social_insurance',
             'export' =>'social_insurance',
@@ -79,5 +90,27 @@ return [
             'enums' => [],
             'comments' => '必填'
         ],
+        [
+            'key' =>'bonus',
+            'export' =>'bonus',
+            'value' => '奖金',
+            'required' => false,
+            'is_main' => false,
+            'default' => 0,
+            'unique' => false,
+            'enums' => [],
+            'comments' => ''
+        ],
+        [
+            'key' =>'other',
+            'export' =>'other',
+            'value' => '其他',
+            'required' => false,
+            'is_main' => false,
+            'default' => 0,
+            'unique' => false,
+            'enums' => [],
+            'comments' => ''
+        ],
     ]
 ];

+ 10 - 3
routes/api.php

@@ -45,11 +45,18 @@ Route::group(['middleware'=> ['checkLogin']],function ($route){
     $route->any('employeeManageAdd', 'Api\EmployeeController@employeeManageAdd')->name('only.manageAdd');
     $route->any('employeeManageEdit', 'Api\EmployeeController@employeeManageEdit')->name('only.admin');
 
+    //用户表头自定义设置相关
+    $route->any('customFieldList', 'Api\CustomFieldSettingController@list');
+    $route->any('customFieldEdit', 'Api\CustomFieldSettingController@edit');
+    $route->any('customFieldAdd', 'Api\CustomFieldSettingController@add');
+    $route->any('customFieldDel', 'Api\CustomFieldSettingController@del');
+    $route->any('customFieldDetail', 'Api\CustomFieldSettingController@detail');
+
     //人员
-    $route->any('employeeAdd', 'Api\EmployeeController@employeeAdd')->name('employee.add');
-    $route->any('employeeEdit', 'Api\EmployeeController@employeeEdit')->name('employee.edit');
+    $route->any('employeeAdd', 'Api\EmployeeController@employeeAdd')->middleware('OssFileDeal');
+    $route->any('employeeEdit', 'Api\EmployeeController@employeeEdit')->middleware('OssFileDeal');
     $route->any('employeeEditOther', 'Api\EmployeeController@employeeEditOther')->name('employee.editPassword');
-    $route->any('employeeDel', 'Api\EmployeeController@employeeDel')->name('employee.delete');
+    $route->any('employeeDel', 'Api\EmployeeController@employeeDel')->middleware('OssFileDeal');
     $route->any('employeeDetail', 'Api\EmployeeController@employeeDetail')->name('employee.detail');
     $route->any('employeeList', 'Api\EmployeeController@employeeList')->name('employee.list');