Procházet zdrojové kódy

小高薪 自定义表头

cqp před 2 měsíci
rodič
revize
2f6fd35606

+ 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);
+        }
+    }
+}

+ 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');
+    }
+}

+ 24 - 0
app/Model/Item.php

@@ -2,6 +2,8 @@
 
 namespace App\Model;
 
+use App\Traits\HasCustomFields;
+
 class Item extends DataScopeBaseModel
 {
     protected $table = "item"; //指定表
@@ -28,4 +30,26 @@ class Item extends DataScopeBaseModel
         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';
+
+}

+ 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, ''];
+    }
+}

+ 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];
+    }
 }

+ 8 - 0
app/Service/ItemService.php

@@ -69,6 +69,12 @@ class ItemService extends Service
 
             $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();
@@ -200,6 +206,7 @@ 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();
@@ -221,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');
 

+ 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;
+    }
+}

+ 7 - 0
routes/api.php

@@ -44,6 +44,13 @@ 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')->middleware('OssFileDeal');
     $route->any('employeeEdit', 'Api\EmployeeController@employeeEdit')->middleware('OssFileDeal');