ShuanghongS 1 mese fa
parent
commit
05c654c514
4 ha cambiato i file con 790 aggiunte e 137 eliminazioni
  1. 11 9
      service/column.class.php
  2. 0 0
      service/ocean_order.class.php
  3. 467 51
      service/report.class.php
  4. 312 77
      utils/common.class.php

+ 11 - 9
service/column.class.php

@@ -19,9 +19,9 @@ class column {
     public function settingDisplayForVIPReport() {
         $data = array();
         //判断是否重置
-        $level = common::check_input($_POST['level']);
-        $serial_no = common::check_input($_POST['serial_no']);
-        $defualt = common::check_input($_POST['defualt']);
+        $level = common::check_input($_REQUEST['level']);
+        $serial_no = common::check_input($_REQUEST['serial_no']);
+        $defualt = common::check_input($_REQUEST['defualt']);
 
         if($level == "Shipment level"){
             $levelSQlWhere = "level = 'Shipment level'";
@@ -38,14 +38,16 @@ class column {
         }
  
         $kln_report_field_str = " id as ids,
-            code as field, 
-            code as label,
+            code as \"fieldBb\",
+            display_name as field, 
+            display_name as label,
             level as \"fieldLevel\",
             'System' as \"fieldType\",
             data_type as \"dataType\",
             group_name as \"groupName\" ";
         $kln_report_field_config = " field_id as ids,
-            field_db as field,
+            field_db as \"fieldBb\",
+            field_display_name as field,
             field_display_name as label,
             field_level as \"fieldLevel\",
             field_type as \"fieldType\",
@@ -451,10 +453,10 @@ class column {
                 $temp[] = $data;
                 $children['All'] = $temp;
             } 
-            if (array_key_exists($data['group_name'], $children)) {
-                $temp = $children[$data['group_name']];
+            if (array_key_exists($data['groupName'], $children)) {
+                $temp = $children[$data['groupName']];
                 $temp[] = $data;
-                $children[$data['group_name']] = $temp;
+                $children[$data['groupName']] = $temp;
             } 
         }
 

File diff suppressed because it is too large
+ 0 - 0
service/ocean_order.class.php


+ 467 - 51
service/report.class.php

@@ -25,9 +25,71 @@ class report {
      * report 配置
     */
     public function report_config(){
+        // $config = [
+        //     'delivery_frequency' => 'monthly',
+        //     'timezone'           => 'UTC+05',
+        //     'monthly_day'        => [2,3,4],
+        //     'monthly_time'       => '09:00:00',
+        // ];
+        // $config = [
+        //     'delivery_frequency' => 'daily',
+        //     'timezone'           => 'UTC+05',
+        //     'daily_time'       => '09:00:00',
+        // ];
+        // $config = [
+        //     'delivery_frequency' => 'weekly',
+        //     'timezone'           => 'UTC+05',
+        //     'weekly_day'        => [1],
+        //     'weekly_time'       => '09:00:00',
+        // ];
+        // $config = [
+        //     'delivery_frequency' => 'quarterly',
+        //     'timezone'           => 'UTC+05',
+        //     'quarterly_month'        =>'1',
+        //     'quarterly_day'        =>'1',
+        //     'quarterly_time'       => '09:00:00',
+        // ];
+        // $config = [
+        //     'delivery_frequency' => 'yearly',
+        //     'timezone'           => 'UTC+05',
+        //     'yearly_month'        =>[1,2],
+        //     'yearly_day'        =>'1',
+        //     'yearly_time'       => '09:00:00',
+        // ];
+
+        // $next = common::calculateNextRunTime($config);
+        //echo $next->format('Y-m-d H:i:s');
+
         $operate = utils::_get('operate');
         $operate = strtolower($operate);
 
+        if ($operate == "parity_id"){
+            //search  parity id
+            if(_isCustomerLogin()){
+                //$ocean_contact_id = _getContactIDHandNew($_SESSION["ONLINE_USER"], 'public');
+                $ocean_contact_id = _getCompanyContactHandNew($_SESSION["ONLINE_USER"]);
+                $air_contact_id = _getAirContactID('public');
+
+                $all_id = "ALL;".$ocean_contact_id;
+                if (utils::endWith($ocean_contact_id,";")){
+                    $all_id .=$air_contact_id;
+                } else {
+                    $all_id .=";".$air_contact_id;
+                }
+                $arr = explode(',', $all_id);
+                $unique_arr = array_unique($arr);
+            }else{
+                $unique_arr= ['ALL'];
+            }
+            
+            $option = array();
+            foreach($unique_arr as $temp){
+                $option[] =array("label"=>$temp,"value"=>$temp);
+            }
+            common::echo_json_encode(200, $option);
+            exit();
+        }
+
         /**
          * report的配置查询,
         */
@@ -56,6 +118,12 @@ class report {
                 }
             }
 
+            if(!empty($_POST['party_id'])){
+                if($_POST['party_id'] != 'ALL'){
+                    $sqlWhere .= " and '".common::check_input($_POST['party_id'])."' = any(party_ids)";
+                }
+            }
+
             $rc = $_POST ['rc'];
             if ($rc == -1) {
                 $sql = "select count(*) from public.kln_report_template where " .$sqlWhere;
@@ -271,7 +339,7 @@ class report {
             }
             $tp = ceil($rc / $ps);
             if ($rc > 0) {
-                $sql = "select name,description from public.kln_report_template where " .$sqlWhere;
+                $sql = "select serial_no,name,description from public.kln_report_template where " .$sqlWhere;
                 $sql .= " order by id desc limit " . $ps . " offset " . ($cp - 1) * $ps;
                 $rs = common::excuteListSql($sql);
                 $arrTmp = array('searchData' => $rs, 
@@ -294,14 +362,20 @@ class report {
             $tableColumns = array();
             $filtersList = array();
             $sortByOptions = array();
+
+            $CustomFiled = "";
             $reportFiled = common::excuteListSql("select * from public.kln_report_field_config where template_serial_no = '".$serial_no."' 
-                and field_type = 'System' and is_enabled = true order by id ");
+                and is_enabled = true order by id ");
             foreach($reportFiled as $filed){
                 if($filed['is_filter_enabled'] == 't'){
                     $type = $filed['data_type'] == "string" ? "input" : ($filed['data_type'] == "date" ? "date" : "input");
+                    $field_display_name = $filed['field_display_name'];
+                    $field_display_name = strtolower($field_display_name);
+                    $field_display_name = preg_replace('/[^a-z0-9]+/', '_', $field_display_name); // 非字母数字 → _
+                    $field_display_name = trim($field_display_name, '_');
                     $filtersList[] = array(
                         "label"=>$filed['field_display_name_user'],
-                        "field"=>$filed['field_display_name'],
+                        "field"=>$field_display_name,
                         "type"=>$type,
                         "data_type"=>$filed['data_type'],
                         "value"=>[],"options"=>[]);
@@ -310,6 +384,11 @@ class report {
                     $sortByOptions[] = $filed['field_db'];
                 }
 
+                //用户自定义字段
+                if($filed['field_type'] == 'Custom'){
+                    $CustomFiled = " , '".$filed['custom_fixed_value']."' AS \"".$filed['field_display_name_user']."\"";
+                }
+
                 $temp = array();
                 $temp['field'] = $filed['field_display_name'];
                 $temp['title'] = $filed['field_display_name_user'];
@@ -331,10 +410,14 @@ class report {
 
             $filterSQLArr = $this->returnFilterSql($filtersList);
             //return array("vvSearchKLN"=>$vvSearchKLN,"klnOceanSearchKLN"=>$klnOceanSearchKLN,"ocItemSearchKLN"=>$ocItemSearchKLN);
-            $report_sql = str_replace('<{vvSearchKLN}>', $filterSQLArr['vvSearchKLN'], $report_sql);
+            $count_sql = str_replace('<{klnOceanSearchKLN}>', $filterSQLArr['klnOceanSearchKLN'], $count_sql);
+            $count_sql = str_replace('<{ocItemSearchKLN}>', $filterSQLArr['ocItemSearchKLN'], $count_sql);
+            $count_sql = str_replace('<{vvSearchKLN}>', $filterSQLArr['vvSearchKLN'], $count_sql);
+
             $report_sql = str_replace('<{klnOceanSearchKLN}>', $filterSQLArr['klnOceanSearchKLN'], $report_sql);
             $report_sql = str_replace('<{ocItemSearchKLN}>', $filterSQLArr['ocItemSearchKLN'], $report_sql);
-            
+            $report_sql = str_replace('<{CustomFiled}>', $CustomFiled, $report_sql);
+            $report_sql = str_replace('<{vvSearchKLN}>', $filterSQLArr['vvSearchKLN'], $report_sql);
 
             //查询data
             $cp = common::check_input($_POST ['cp']); //current_page
@@ -343,8 +426,9 @@ class report {
                 $ps = 10;
             if (empty($cp))
                 $cp = 1;
-           
-            if (true) {
+            
+            $rc = $_POST ['rc'];
+            if ($rc == -1) {
                 $count_sql = str_replace('<{orderby}>', "", $count_sql);
                 error_log($count_sql);
                 $rc = common::excuteOneSql($count_sql);
@@ -370,7 +454,6 @@ class report {
             common::echo_json_encode(200, $dataReturn);
             exit();
         }
-
         /*
          * export excel
         */
@@ -394,20 +477,15 @@ class report {
         if ($operate == "manage_fileds") {
             $serial_no = common::check_input($_POST ['serial_no']); 
             $reportFiled = common::excuteListSql("select * from public.kln_report_field_config 
-                where template_serial_no = '".$serial_no."' 
-                    and field_type = 'System' order by id ");
-            $showData = array();
-            $hideData = array();    
+                where template_serial_no = '".$serial_no."' order by id ");
+            $data = array(); 
             foreach($reportFiled as $_reportFiled){
                 $_reportFiled['is_filter_enabled'] = $_reportFiled['is_filter_enabled'] == 't' ? true : false;
                 $_reportFiled['is_sort_enabled'] = $_reportFiled['is_sort_enabled'] == 't' ? true : false;
-                if($_reportFiled['is_enabled'] == 't'){
-                    $showData[] = $_reportFiled;
-                }else{
-                    $hideData[] = $_reportFiled;
-                }
+                $_reportFiled['is_enabled'] = $_reportFiled['is_enabled'] == 't' ? true : false;
+                $data[] = $_reportFiled;
             }    
-            common::echo_json_encode(200,array("msg"=>"success","showData" => $showData,"hideData" => $hideData));
+            common::echo_json_encode(200,array("msg"=>"success","data" => $data));
             exit;
         }
         if ($operate == "manage_fileds_save") {
@@ -428,13 +506,14 @@ class report {
                 $_fixed_value = common::check_input($_tempFieldsList['custom_fixed_value']);
                 $_is_filter_enabled = $_tempFieldsList['is_filter_enabled'];
                 $_is_sort_enabled = $_tempFieldsList['is_sort_enabled'];
+                $_is_enabled = $_tempFieldsList['is_enabled'];
                 $sql .=  "INSERT INTO public.kln_report_field_config(
                             template_serial_no, field_id, field_level, field_type, field_db, field_group_name,
                             field_display_name, field_display_name_user, data_type, custom_value_type, 
-                            custom_fixed_value, is_filter_enabled, is_sort_enabled, created_time)
+                            custom_fixed_value, is_filter_enabled, is_sort_enabled,is_enabled, created_time)
                     VALUES ('$serial_no', $_field_id, '$_field_level', '$_field_type', '$_field_db', '$_field_group_name', 
                             '$_field_code', '$_display_name', '$_data_type', '$_value_type', 
-                            '$_fixed_value', '$_is_filter_enabled', '$_is_sort_enabled',now());";
+                            '$_fixed_value', '$_is_filter_enabled', '$_is_sort_enabled','$_is_enabled',now());";
             }
             if (!empty($sql)){
                 common::excuteUpdateSql($sql);
@@ -444,14 +523,331 @@ class report {
             common::echo_json_encode(200,array("msg"=>"success","Data" => ''));
             exit;
         }
+
+        if ($operate == "report_schedule"){
+            $serial_no = common::check_input($_POST ['serial_no']); 
+            $schedule = common::excuteObjectSql("select schedule_order_field,validity_type,valid_from,valid_to,
+                    data_reference_field,data_range_type,dynamic_start_offset,dynamic_end_offset,fixed_start_date,fixed_end_date,
+                    delivery_frequency,TO_CHAR(daily_time, 'HH24:MI') as daily_time,
+                    array_to_json(weekly_day) as weekly_days_json,TO_CHAR(weekly_time, 'HH24:MI') as weekly_time,
+                    array_to_json(monthly_day) as monthly_day_json,TO_CHAR(monthly_time, 'HH24:MI') as monthly_time,
+                    quarterly_month,quarterly_day,TO_CHAR(quarterly_time, 'HH24:MI') as quarterly_time,
+                    array_to_json(yearly_month) as yearly_month_json,yearly_day, TO_CHAR(yearly_time, 'HH24:MI') as yearly_time,
+                    timezone,email_recipients
+                from public.kln_report_template where  serial_no = '$serial_no'");
+
+            $schedule["weekly_days"] = json_decode($schedule["weekly_days"],true);
+            $schedule["monthly_day"] = json_decode($schedule["monthly_day"],true);
+            $schedule["yearly_month"] = json_decode($schedule["yearly_month"],true);
+
+            //处理成VUE 识别的格式
+            $data = array();
+            $data["validityPeriod"] = array("type"=>$schedule["validity_type"],"startDate"=>$schedule["valid_from"],"endDate"=>$schedule["valid_to"]);
+            if($schedule["data_range_type"] == "dynamic_rolling"){
+                $data["timeRange"] = array("fieldType"=>$schedule["data_reference_field"],"type"=>$schedule["data_range_type"],
+                    "startDate"=>$schedule["dynamic_start_offset"],"endDate"=>$schedule["dynamic_end_offset"]);
+            }else{
+                $data["timeRange"] = array("fieldType"=>$schedule["data_reference_field"],"type"=>$schedule["data_range_type"],
+                    "startDate"=>$schedule["fixed_start_date"],"endDate"=>$schedule["fixed_end_date"]);
+            }
+            $time = "";
+            $week = array();
+            $month = array();
+            $quarterMonth = "";
+            $day = array();
+            $delivery_frequency = $schedule["delivery_frequency"];
+            if($delivery_frequency ==  'daily'){
+                $time =  $schedule["daily_time"];
+            }elseif($delivery_frequency ==  'weekly'){
+                $week= json_decode($schedule["weekly_days_json"],true);
+                $time =  $schedule["weekly_time"];
+            }elseif($delivery_frequency ==  'monthly'){
+                $monthlyDay= json_decode($schedule["monthly_day_json"],true);
+                $time =  $schedule["monthly_time"];
+            }elseif($delivery_frequency ==  'quarterly'){
+                $quarterMonth =  $schedule["quarterly_month"];
+                $day =  $schedule["quarterly_day"];
+                $time =  $schedule["quarterly_time"];
+            }elseif($delivery_frequency ==  'yearly'){
+                $month= json_decode($schedule["yearly_month_json"],true);
+                $day = $schedule["yearly_day"];
+                $time =  $schedule["yearly_time"];
+            }
+            $data["deliveryFrequency"] = array("emailRecipients"=>$schedule["email_recipients"],"orderBy"=>$schedule["schedule_order_field"],
+                "timezone"=>$schedule["timezone"],"deliveryFrequency"=>$schedule["delivery_frequency"],
+                "scheduleDetails" =>array("time"=>$time,"week"=>$week,"month"=>$month,"monthlyDay" =>$monthlyDay,"quarterMonth"=>$quarterMonth,"day"=>$day));
+            common::echo_json_encode(200,array("msg"=>"success","showData" => $data));
+            exit;
+        }
+
+        if ($operate == "report_schedule_search"){
+            $dataReturn = array();
+            $serial_no = common::check_input($_POST ['serial_no']);
+            $data_reference_field =  strtolower(common::check_input($_POST ['fieldType']));
+            $data_range_type =  common::check_input($_POST ['type']);
+            if($data_range_type == "dynamic_rolling"){
+                $dynamic_start_offset =  common::check_input($_POST ['startDate']);
+                $dynamic_end_offset =  common::check_input($_POST ['endDate']);
+                $fixed_start_date =  "null";
+                $fixed_end_date =  "null";
+            }else{
+                $dynamic_start_offset =  "null";
+                $dynamic_end_offset =  "null";
+                $fixed_start_date =  common::check_input($_POST ['startDate']);
+                $fixed_end_date =  common::check_input($_POST ['endDate']);
+            }
+            $schedule_order_field =  common::check_input($_POST ['orderBy']);
+
+
+            //查询列名
+            $tableColumns = array();
+            $CustomFiled = "";
+            $reportFiled = common::excuteListSql("select * from public.kln_report_field_config where template_serial_no = '".$serial_no."' 
+                and is_enabled = true order by id ");
+            foreach($reportFiled as $filed){
+                $temp = array();
+                $temp['field'] = $filed['field_display_name'];
+                $temp['title'] = $filed['field_display_name_user'];
+                $temp['type'] = $filed['field_db'] == "Status" ? "status" : "normal";
+                $temp['formatter'] = "";
+                $tableColumns[] = $temp;
+
+                //用户自定义字段
+                if($filed['field_type'] == 'Custom'){
+                    $CustomFiled = " , '".$filed['custom_fixed_value']."' AS \"".$filed['field_display_name_user']."\"";
+                }
+            }
+            $dataReturn['tableColumns'] =  $tableColumns;
+            
+            
+            //形成sql
+            $schedule_search = common::excuteObjectSql("select schedule_order_field,data_reference_field,data_range_type,
+                    dynamic_start_offset,dynamic_end_offset,
+                    fixed_start_date,fixed_end_date,
+                    report_sql,count_sql
+                from public.kln_report_template where  serial_no = '$serial_no'");
+
+            $report_sql = $schedule_search["report_sql"];
+            $count_sql = $schedule_search["count_sql"];
+
+            $klnOceanSearchKLN = ' where ' . common::searchExtendHand_KLN("ocean", $_SESSION["ONLINE_USER"]);
+            $orderby = "";
+            if(!empty($data_range_type)){
+                //代表有用户设置的查询参数
+                if($data_range_type == "dynamic_rolling"){
+                    $klnOceanSearchKLN .= " and ".$data_reference_field." >= CURRENT_DATE - ".$dynamic_start_offset." and  ".$data_reference_field." <= CURRENT_DATE + ".$dynamic_end_offset."";
+                } else {
+                    if(!empty($fixed_start_date)){
+                        $klnOceanSearchKLN .= " and ".$data_reference_field." >= '".common::usDate2sqlDate($fixed_start_date)."'";
+                    }
+                    if(!empty($fixed_end_date)){
+                        $klnOceanSearchKLN .= " and  ".$data_reference_field." <=  '".common::usDate2sqlDate($fixed_end_date)."'";
+                    }
+                }
+                if(!empty($schedule_order_field)){
+                    $orderby = " order by \"".strtoupper($schedule_order_field)."\"";
+                }
+            } else {
+                if($schedule_search["data_range_type"] == "dynamic_rolling"){
+                    $field_db = strtolower($schedule_search["data_reference_field"]);
+                    $dynamic_start_offset_db = empty($schedule_search["dynamic_start_offset"]) ? 0 : $schedule_search["dynamic_start_offset"];
+                    $dynamic_end_offset_db = empty($schedule_search["dynamic_end_offset"]) ? 0 : $schedule_search["dynamic_end_offset"];
+                    $klnOceanSearchKLN .= " and ".$field_db." >= CURRENT_DATE - ".$dynamic_start_offset_db." and  ".$field_db." <= CURRENT_DATE + ".$dynamic_end_offset_db."";
+                } else {
+                    $field_db = strtolower($schedule_search["data_reference_field"]);
+                    $fixed_start_date_db = $schedule_search["fixed_start_date"];
+                    $fixed_end_date_db = $schedule_search["fixed_end_date"];
+
+                    if(!empty($fixed_start_date_db)){
+                        $klnOceanSearchKLN .= " and ".$field_db." >= '".$fixed_start_date_db."'";
+                    }
+                    if(!empty($fixed_end_date_db)){
+                        $klnOceanSearchKLN .= " and  ".$field_db." <=  '".$fixed_end_date_db."'";
+                    }
+                }
+
+                if(!empty($schedule_search["schedule_order_field"])){
+                    $orderby = " order by \"".strtoupper($schedule_search["schedule_order_field"])."\"";
+                }
+            }
+
+            $count_sql = str_replace('<{klnOceanSearchKLN}>', $klnOceanSearchKLN, $count_sql);
+            $count_sql = str_replace('<{ocItemSearchKLN}>', " ", $count_sql);
+            $count_sql = str_replace('<{vvSearchKLN}>', " ", $count_sql);
+
+            $report_sql = str_replace('<{klnOceanSearchKLN}>', $klnOceanSearchKLN, $report_sql);
+            $report_sql = str_replace('<{ocItemSearchKLN}>', " ", $report_sql);
+            $report_sql = str_replace('<{CustomFiled}>', $CustomFiled, $report_sql);
+            $report_sql = str_replace('<{vvSearchKLN}>', " ", $report_sql);
+
+            //查询sql
+            $cp = common::check_input($_POST ['cp']); //current_page
+            $ps = common::check_input($_POST ['ps']); //ps
+            if (empty($ps))
+                $ps = 10;
+            if (empty($cp))
+                $cp = 1;
+           
+            $rc = $_POST ['rc'];
+            if ($rc == -1) {
+                $count_sql = str_replace('<{orderby}>', "", $count_sql);
+                error_log($count_sql);
+                $rc = common::excuteOneSql($count_sql);
+            }
+            $tp = ceil($rc / $ps);
+            if ($rc > 0) {
+                $report_sql = str_replace('<{orderby}>', $orderby, $report_sql);
+                $tmp_search_without_limit = $report_sql;
+                $report_sql .= " limit " . $ps . " offset " . ($cp - 1) * $ps;
+                error_log($report_sql);
+
+                $rs = common::excuteListSql($report_sql);
+                $arrTmp = array('searchData' => $rs, 
+                        'rc' => intval($rc),
+                        'ps' => intval($ps),
+                        'cp' => intval($cp),
+                        'tp' => intval($tp));
+            }else{
+                $arrTmp = array('searchData' => array(),
+                        'rc' => intval($rc),
+                        'ps' => intval($ps),
+                        'cp' => intval($cp),
+                        'tp' => intval($tp));
+            }
+            $dataReturn['tableData'] =  $arrTmp;
+            common::echo_json_encode(200, $dataReturn);
+            exit();
+            exit;
+        }
+
+        if ($operate == "report_schedule_save"){
+            $serial_no = common::check_input($_POST ['serial_no']);
+
+            $validity_type =  common::check_input($_POST ['validityPeriodType']);
+            $valid_from = "null";
+            $valid_to = "null";
+            if($validity_type == "custom"){
+                $valid_from =  "'".common::check_input($_POST ['validityPeriodStartDate'])."'";
+                $valid_to =  "'".common::check_input($_POST ['validityPeriodEndDate'])."'";
+            }
+
+            $data_reference_field =  strtolower(common::check_input($_POST ['fieldType']));
+            $data_range_type =  common::check_input($_POST ['type']);
+            if($data_range_type == "dynamic_rolling"){
+                $dynamic_start_offset =  "'".common::check_input($_POST ['startDate'])."'";
+                $dynamic_end_offset =  "'".common::check_input($_POST ['endDate'])."'";
+                $fixed_start_date =  "null";
+                $fixed_end_date =  "null";
+            }else{
+                $dynamic_start_offset =  "null";
+                $dynamic_end_offset =  "null";
+                $fixed_start_date =  "'".common::check_input($_POST ['startDate'])."'";
+                $fixed_end_date =  "'".common::check_input($_POST ['endDate'])."'";
+            }
+            $schedule_order_field =  common::check_input($_POST ['orderBy']);
+            if(empty($schedule_order_field)){
+                $schedule_order_field = "ETD";
+            }
+
+            $email_recipients =  common::check_input($_POST ['emailRecipients']);
+
+            $timezone =  common::check_input($_POST ['timezone']);
+            $daily_time = "null";
+            $weekly_day = "null";
+            $weekly_time = "null";
+            $monthly_day = "null";
+            $monthly_time = "null";
+            $quarterly_month= "null";
+            $quarterly_day= "null";
+            $quarterly_time = "null";
+            $yearly_month = "null";
+            $yearly_day = "null";
+            $yearly_time = "null";
+            $delivery_frequency =  common::check_input($_POST ['deliveryFrequency']);
+            if($delivery_frequency ==  'daily'){
+                $daily_time =  "'".common::check_input($_POST ['time'])."'";
+            }elseif($delivery_frequency ==  'weekly'){
+                $weekly_day= common::toPgTextArrayLiteral($_POST ['week']);
+                $weekly_time = "'".common::check_input($_POST ['time'])."'";
+            }elseif($delivery_frequency ==  'monthly'){
+                $monthly_day= common::toPgTextArrayLiteral($_POST ['monthlyDay']);
+                $monthly_time = "'".common::check_input($_POST ['time'])."'";
+            }elseif($delivery_frequency ==  'quarterly'){
+                //检查数组是否有值
+                $quarterly_month =  "'".common::check_input($_POST ['quarterMonth'])."'";
+                $quarterly_day =  "'".common::check_input($_POST ['day'])."'";
+                $quarterly_time = "'".common::check_input($_POST ['time'])."'";
+            }elseif($delivery_frequency ==  'yearly'){
+                $yearly_month= common::toPgTextArrayLiteral($_POST ['yearlyMonth']);
+                $yearly_day = "'".common::check_input($_POST ['day'])."'";
+                $yearly_time = "'".common::check_input($_POST ['time'])."'";
+            }
+            $sql = "";
+            $klnOceanSearchKLN = ' where ' . common::searchExtendHand_KLN("ocean", $_SESSION["ONLINE_USER"]);
+            if (!empty($serial_no)){
+                $updateSqlSet = " schedule_order_field = '".$schedule_order_field."', 
+                                  validity_type = '".$validity_type."', 
+                                  valid_from = ".$valid_from.", 
+                                  valid_to = ".$valid_to.", 
+
+                                  data_reference_field = '".$data_reference_field."', 
+                                  data_range_type = '".$data_range_type."', 
+                                  dynamic_start_offset = ".$dynamic_start_offset.", 
+                                  dynamic_end_offset = ".$dynamic_end_offset.", 
+                                  fixed_start_date = ".$fixed_start_date.", 
+                                  fixed_end_date = ".$fixed_end_date.", 
+
+                                  delivery_frequency = '".$delivery_frequency."', 
+                                  daily_time = ".$daily_time.", 
+                                  weekly_day = ".$weekly_day.", 
+                                  weekly_time = ".$weekly_time.", 
+                                  monthly_day = ".$monthly_day.", 
+                                  monthly_time = ".$monthly_time.", 
+                                  quarterly_month = ".$quarterly_month.", 
+                                  quarterly_day = ".$quarterly_day.", 
+                                  quarterly_time = ".$quarterly_time.", 
+                                  yearly_month = ".$yearly_month.", 
+                                  yearly_day = ".$yearly_day.", 
+                                  yearly_time = ".$yearly_time.",
+                                  timezone = '".$timezone."',
+                                  email_recipients = '".$email_recipients."',
+                                  next_run_time = null,
+                                  search_extend_hand = '".common::check_input($klnOceanSearchKLN)."',
+                                  modify_by = '"._getLoginName()."',
+                                  update_time = now()";
+                //代表update 
+                $sql .= "update public.kln_report_template set ".$updateSqlSet."
+                    where serial_no = '$serial_no';";
+            } 
+            if (!empty($sql)){
+                common::excuteUpdateSql($sql);
+                //执行成功后,处理next_run_time 
+                $config = common::excuteObjectSql("select delivery_frequency,daily_time,
+                        array_to_json(weekly_day) as weekly_day_json,weekly_time,
+                        array_to_json(monthly_day) as monthly_day_json,monthly_time,
+                        quarterly_month,quarterly_day,quarterly_time,
+                        array_to_json(yearly_month) as yearly_month_json,yearly_day,yearly_time,timezone 
+                    from public.kln_report_template where  serial_no = '$serial_no';");
+
+                $config["weekly_day"] = json_decode($config["weekly_day_json"],true);
+                $config["monthly_day"] = json_decode($config["monthly_day_json"],true);
+                $config["yearly_month"] = json_decode($config["yearly_month_json"],true); 
+                $next = common::calculateNextRunTime($config);
+                $next_run_time = $next->format('Y-m-d H:i:s');   
+                common::excuteUpdateSql("update public.kln_report_template set next_run_time = '$next_run_time' where  serial_no = '$serial_no';");
+                
+                $data = array("msg" =>"success");
+            }
+            common::echo_json_encode(200,$data);                
+            exit();
+        }
     }
 
     /**
      * 根据提交的参数动态的拼接filter sql
      */
     public function returnFilterSql($filtersList){
-        $klnOceanDb = common::getReportRealDBFiled("klnOceanDb");
-        $ocItemDb = common::getReportRealDBFiled("ocItemDb");
+        $klnVipDb = common::getReportRealDBFiled();
 
         $vvSearchKLN = " where 1=1 ";
         $klnOceanSearchKLN = ' where ' . common::searchExtendHand_KLN("ocean", $_SESSION["ONLINE_USER"]);
@@ -459,47 +855,67 @@ class report {
 
         foreach($filtersList as $fiter){
             if(!empty($_POST[$fiter['field']])){
-                $key = array_search($fiter['field'], $klnOceanDb);
-                $ockey = array_search($fiter['field'], $ocItemDb);
+                $key = array_search($fiter['field'], $klnVipDb);
                 if($key !== false){
                     //找到给key
+                    $temp_sql_where = "";
                     if ($fiter['data_type'] == "string"){
-                        $klnOceanSearchKLN .= " and ".$key." = '". common::check_input($_POST[$fiter['field']])."'"; 
+                        $temp_sql_where .= " and ".$key." = '". common::check_input($_POST[$fiter['field']])."'"; 
                     } elseif ($fiter['data_type'] == "number"){
-                        $klnOceanSearchKLN .= " and ".$key." >= '". common::check_input($_POST[$fiter['field']."_from"])."'"; 
-                        $klnOceanSearchKLN .= " and ".$key." <= '". common::check_input($_POST[$fiter['field']."_to"])."'"; 
+                        $temp_arr = $_POST [$fiter['field']];
+                        if(!empty($temp_arr[0])){
+                            $temp_sql_where .= " and ".$key."::integer >= '". common::check_input($temp_arr[0])."'"; 
+                        }
+                        if(!empty($temp_arr[1])){
+                            $temp_sql_where .= " and ".$key."::integer <= '". common::check_input($temp_arr[1])."'"; 
+                        } 
                     } elseif ($fiter['data_type'] == "date"){
-                        $date_from = common::check_input(common::usDate2sqlDate($_POST [$fiter['field']."_from"]) . ' 00:00:00');
-                        $date_to = common::check_input(common::usDate2sqlDate($_POST [$fiter['field']."_to"]) . ' 23:59:59');
-                        $klnOceanSearchKLN .= " and ".$key." >= '". $date_from."'"; 
-                        $klnOceanSearchKLN .= " and ".$key." <= '". $date_to."'"; 
+                        $temp_arr = $_POST [$fiter['field']];
+                        if(!empty($temp_arr[0])){
+                            $date_from = common::check_input(common::usDate2sqlDate($temp_arr[0]) . ' 00:00:00');
+                            $temp_sql_where .= " and ".$key." >= '". $date_from."'"; 
+                        }
+                        if(!empty($temp_arr[1])){
+                            $date_to = common::check_input(common::usDate2sqlDate($temp_arr[1]) . ' 23:59:59');
+                            $temp_sql_where .= " and ".$key." <= '". $date_to."'"; 
+                        }
                     }
-                } elseif ($ockey !== false){
-                    //找到给key
-                    if ($fiter['data_type'] == "string"){
-                        $ocItemSearchKLN .= " and ".$ockey." = '". common::check_input($_POST[$fiter['field']])."'"; 
-                    } elseif ($fiter['data_type'] == "number"){
-                        $ocItemSearchKLN .= " and ".$ockey." >= '". common::check_input($_POST[$fiter['field']."_from"])."'"; 
-                        $ocItemSearchKLN .= " and ".$ockey." <= '". common::check_input($_POST[$fiter['field']."_to"])."'"; 
-                    } elseif ($fiter['data_type'] == "date"){
-                        $date_from = common::check_input(common::usDate2sqlDate($_POST[$fiter['field']."_from"]) . ' 00:00:00');
-                        $date_to = common::check_input(common::usDate2sqlDate($_POST[$fiter['field']."_to"]) . ' 23:59:59');
-                        $ocItemSearchKLN .= " and ".$ockey." >= '". $date_from."'"; 
-                        $ocItemSearchKLN .= " and ".$ockey." <= '". $date_to."'"; 
+
+                    if(utils::startWith($key,"oc") || utils::startWith($key,"oi")){
+                        $ocItemSearchKLN .= $temp_sql_where;
+                    } else {
+                        $klnOceanSearchKLN .= $temp_sql_where;
                     }
                 } else {
+                    //获取POST name  用户去别名得字段,放在vvSearchKLN上
+                    $_post_field = $fiter['field'];
+                    $_post_field = strtolower($_post_field);
+                    $_post_field = preg_replace('/[^a-z0-9]+/', '_', $_post_field); // 非字母数字 → _
+                    $_post_field = trim($_post_field, '_');
+
                     if ($fiter['data_type'] == "string"){
-                        $vvSearchKLN .= " and \"".$fiter['field']."\" ilike '%". common::check_input($_POST [$fiter['field']])."%'"; 
+                        $vvSearchKLN .= " and \"".$fiter['field']."\" ilike '%". common::check_input($_POST [$_post_field])."%'"; 
                     } elseif ($fiter['data_type'] == "number"){
-                        $vvSearchKLN .= " and \"".$fiter['field']."\" >= '". common::check_input($_POST [$fiter['field']."_from"])."'"; 
-                        $vvSearchKLN .= " and \"".$fiter['field']."\" <= '". common::check_input($_POST [$fiter['field']."_to"])."'"; 
+                        $temp_arr = $_POST [$_post_field];
+                        if(!empty($temp_arr[0])){
+                            $vvSearchKLN .= " and \"".$fiter['field']."\"::integer >= '". common::check_input($temp_arr[0])."'"; 
+                        }
+                        if(!empty($temp_arr[1])){
+                            $vvSearchKLN .= " and \"".$fiter['field']."\"::integer <= '". common::check_input($temp_arr[1])."'"; 
+                        }  
                     } elseif ($fiter['data_type'] == "date"){
-                        $date_from = common::check_input(common::usDate2sqlDate($_POST [$fiter['field']."_from"]) . ' 00:00:00');
-                        $date_to = common::check_input(common::usDate2sqlDate($_POST [$fiter['field']."_to"]) . ' 23:59:59');
+                        $temp_arr = $_POST [$_post_field];
                         //先判断日期字符串是否为空,这里则有做 是因为sql 整合了柜子315时间,和 milestone的时间, 只能text转date
-                        $vvSearchKLN .= " and COALESCE(\"".$fiter['field']."\",''::text)<> ''::text ";  
-                        $vvSearchKLN .= " and to_timestamp(\"".$fiter['field']."\", 'MM/DD/YYYY HH24:MI:SS') >= '". $date_from."'"; 
-                        $vvSearchKLN .= " and to_timestamp(\"".$fiter['field']."\", 'MM/DD/YYYY HH24:MI:SS') <= '". $date_to."'"; 
+                        if(!empty($temp_arr[0])){
+                            $date_from = common::check_input(common::usDate2sqlDate($temp_arr[0]) . ' 00:00:00');
+                            $vvSearchKLN .= " and COALESCE(\"".$fiter['field']."\",''::text)<> ''::text ";  
+                            $vvSearchKLN .= " and to_timestamp(\"".$fiter['field']."\", 'MM/DD/YYYY HH24:MI:SS') >= '". $date_from."'"; 
+                        }
+                        if(!empty($temp_arr[1])){
+                            $date_to = common::check_input(common::usDate2sqlDate($temp_arr[1]) . ' 23:59:59');
+                            $vvSearchKLN .= " and COALESCE(\"".$fiter['field']."\",''::text)<> ''::text ";  
+                            $vvSearchKLN .= " and to_timestamp(\"".$fiter['field']."\", 'MM/DD/YYYY HH24:MI:SS') <= '". $date_to."'"; 
+                        }
                     }
                 }
             }

+ 312 - 77
utils/common.class.php

@@ -4412,84 +4412,79 @@ class common {
         return $columns;
     }
 
-    public static function getReportRealDBFiled($type){
+    public static function getReportRealDBFiled(){
         $kln_ocean = [
-            "oo.tracking_no"=>"Tracking No.",
-            "oo.m_bol"=>"MBOL/MAWB No.",
-            "oo.h_bol"=>"HBOL/HAWB No.",
-            "oo.invoice_no"=>"Invoice No.",
-            "oo.booking_no"=>"Booking No.",
-            //"oo.po_no"=>"Shipment PO No.",
-            "oo.quote_no"=>"Quote No.",
-            "oo.carrier_booking"=>"Carrier Booking No.",
-            "oo.contract"=>"Contract No.",
-            "oo.manifest_hbol"=>"Manifest HBOL",
-            "oo.transport_mode"=>"Transportation Mode",
-            "oo.service"=>"Service Type",
-            //"oe.manifest_type"=>"Shipment Type",
-            "oo.ex_im"=>"EX/IM",
-            "oo.incoterms"=>"Incoterms",
-            //"oe.loadterm"=>"Load Terms",
-            "oo.status"=>"Status",
-            "oo.carbon_emission"=>"CO2 Emission",
-            "oo.qty"=>"Shipment Qty",
-            "oo.piece_count"=>"Shipment Gross Weight",
-            "oo.weight"=>"Chargeable Weight",
-            //"oe.volume"=>"Shipment Volume",
-            "oo.shipper"=>"Shipper", 
-            "oo.shipper_id"=>"Shipper ID",
-            "oo.consignee"=>"Consignee",
-            "oo.consignee_id"=>"Consignee ID",
-            "oo.notify_party"=>"Notify party",
-            "oo.notify_party_id"=>"Notify party ID",
-            "oo.billto"=>"Bill to",
-            "oo.group_name"=>"Group Name",
-            "oo.origin"=>"Origin Agent",
-            "oo.agent"=>"Destination Agent",
-            "oo.dest_op"=>"Destination Operator",
-            "oo.sales_rep"=>"Sales",
-            "oo.etd"=>"ETD",
-            "oo.eta"=>"ETA",
-            "oo.created_time"=>"Creation Time",
-            "oo.atd"=>"ATD",
-            "oo.ata"=>"ATA",
-            "oo.shipper_city"=>"Shipper City",
-            "oo.consignee_city"=>"Consignee City",
-            "oo.place_of_receipt_exp"=>"Place of Receipt",
-            "oo.port_of_loading"=>"Port of Loading",
-            "oo.port_of_discharge"=>"Port of Discharge",
-            "oo.place_of_delivery_exp"=>"Place of delivery",
-            "oo.port_of_transshipment_name"=>"Port of Transhipment",
-            "oo.carrier"=>"Carrier",
-            //"oo.voyage"=>"Voyage/Flight",
-            //"oo.vessel"=>"Vessel/Airline",
-            "oo.ams_status"=>"ACE-M1 Status",
-            "oo.isisf"=>"Is ISF",
-            "oo.obl_set"=>"OBL_SET"];
-
-        $other = [
-            "oc.ctnr"=>"Container No.",
-            "oc.size"=>"Container Size",
-            "oc.qty"=>"Container Qty",
-            "oc.unit"=>"Container Unit",
-            "oc.grs_kgs"=>"Container Weight",
-            "oc.cbm"=>"Container Volume",
-            "oc.po_no"=>"Container PO No.",
-            "oc.item_no"=>"Item No.",
-            "oc.invoice_no"=>"Invoice No.",
-            "oi.po_no"=>"Item PO No.",
-            "oi.sku_no"=>"SKU NO.",
-            "oi.quantity"=>"Item Qty",
-            "oi.unit"=>"Item Unit",
-            "oi.grs_kgs"=>"Item Weight",
-            "oi.vol_cbm"=>"Item Volume",
-            "oi.description"=>"Description",
-            "oi.inner_pcs"=>"Inner PCS"]; 
-            
-        if($type == "klnOceanDb") 
-            return  $kln_ocean; 
-        else 
-            return  $other; 
+            "oo.tracking_no" => "tracking_no",
+            "oo.m_bol" => "mbol_mawb_no",
+            "oo.h_bol" => "hbol_hawb_no",
+            "oo.invoice_no" => "invoice_no",
+            "oo.booking_no" => "booking_no",
+            //"oo.po_no"=>"shipment_po_no",
+            "oo.quote_no" => "quote_no",
+            "oo.carrier_booking" => "carrier_booking_no",
+            "oo.contract" => "contract_no",
+            "oo.manifest_hbol" => "manifest_hbol",
+            "oo.transport_mode" => "transportation_mode",
+            "oo.service" => "service_type",
+            //"oe.manifest_type"=>"shipment_type",
+            "oo.ex_im" => "ex_im",
+            "oo.incoterms" => "incoterms",
+            //"oe.loadterm"=>"load_terms",
+            "oo.status" => "status",
+            "oo.carbon_emission" => "co2_emission",
+            "oo.qty" => "shipment_qty",
+            "oo.piece_count" => "shipment_gross_weight",
+            "oo.weight" => "chargeable_weight",
+            //"oe.volume"=>"shipment_volume",
+            "oo.shipper" => "shipper",
+            "oo.shipper_id" => "shipper_id",
+            "oo.consignee" => "consignee",
+            "oo.consignee_id" => "consignee_id",
+            "oo.notify_party" => "notify_party",
+            "oo.notify_party_id" => "notify_party_id",
+            "oo.billto" => "bill_to",
+            "oo.group_name" => "group_name",
+            "oo.origin" => "origin_agent",
+            "oo.agent" => "destination_agent",
+            "oo.dest_op" => "destination_operator",
+            "oo.sales_rep" => "sales",
+            "oo.etd" => "etd",
+            "oo.eta" => "eta",
+            "oo.created_time" => "creation_time",
+            "oo.atd" => "atd",
+            "oo.ata" => "ata",
+            "oo.shipper_city" => "shipper_city",
+            "oo.consignee_city" => "consignee_city",
+            "oo.place_of_receipt_exp" => "place_of_receipt",
+            "oo.port_of_loading" => "port_of_loading",
+            "oo.port_of_discharge" => "port_of_discharge",
+            "oo.place_of_delivery_exp" => "place_of_delivery",
+            "oo.port_of_transshipment_name" => "port_of_transhipment",
+            "oo.carrier" => "carrier",
+            //"oo.voyage"=>"voyage_flight",
+            //"oo.vessel"=>"vessel_airline",
+            "oo.ams_status" => "ace_m1_status",
+            "oo.isisf" => "is_isf",
+            "oo.obl_set" => "obl_set",
+            "oc.ctnr" => "container_no",
+            "oc.size" => "container_size",
+            "oc.qty" => "container_qty",
+            "oc.unit" => "container_unit",
+            "oc.grs_kgs" => "container_weight",
+            "oc.cbm" => "container_volume",
+            "oc.po_no" => "container_po_no",
+            "oc.item_no" => "item_no",
+            "oc.invoice_no" => "invoice_no",
+            "oi.po_no" => "item_po_no",
+            "oi.sku_no" => "sku_no",
+            "oi.quantity" => "item_qty",
+            "oi.unit" => "item_unit",
+            "oi.grs_kgs" => "item_weight",
+            "oi.vol_cbm" => "item_volume",
+            "oi.description" => "description",
+            "oi.inner_pcs" => "inner_pcs"
+        ];
+        return  $kln_ocean; 
     }
 
     public static function toPgTextArrayLiteral(array $arr): string {
@@ -4499,5 +4494,245 @@ class common {
         $quoted = array_map(fn($v) => '"' . addcslashes((string)$v, '"\\') . '"', $arr);
         return "'{" . implode(',', $quoted) . "}'";
     }
+
+    /**
+     * 根据用户配置计算下一次执行时间(返回 UTC 时间戳,无时区)
+     *
+     * @param array $config 来自数据库的一行配置
+     * @return \DateTimeImmutable 返回 UTC 时间(无时区),可用于存储到 next_run_time
+     */
+    public static function calculateNextRunTime(array $config): \DateTimeImmutable
+    {
+        // ────────────────────────────────────────────────
+        // 第一步:解析用户时区(支持 'UTC+08', 'Asia/Shanghai' 等)
+        // ────────────────────────────────────────────────
+        $userTz = common::parseUserTimezone($config['timezone'] ?? 'UTC');
+
+        // 获取当前时间(在用户时区中)
+        $nowInUserTz = new \DateTimeImmutable('now', $userTz);
+
+        // ────────────────────────────────────────────────
+        // 第二步:解析时间字符串(如 '09:00:00' → [9,0,0])
+        // ────────────────────────────────────────────────
+        $parseTime = function (?string $timeStr): array {
+            if (!$timeStr || !preg_match('/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/', $timeStr, $m)) {
+                return [9, 0, 0]; // 默认 09:00
+            }
+            return [(int)$m[1], (int)$m[2], isset($m[3]) ? (int)$m[3] : 0];
+        };
+
+        // ────────────────────────────────────────────────
+        // 第三步:根据频率计算下一个执行时间(在用户时区中)
+        // ────────────────────────────────────────────────
+        $next = null;
+        $freq = $config['delivery_frequency'] ?? 'daily';
+
+        switch ($freq) {
+            case 'daily':
+                [$h, $m, $s] = $parseTime($config['daily_time'] ?? '09:00:00');
+                $next = $nowInUserTz->setTime($h, $m, $s);
+                if ($next <= $nowInUserTz) {
+                    $next = $next->modify('+1 day');
+                }
+                break;
+
+            case 'weekly':
+                [$h, $m, $s] = $parseTime($config['weekly_time'] ?? '09:00:00');
+                $scheduledDays = array_map('intval', (array)($config['weekly_day'] ?? []));
+                if (empty($scheduledDays)) {
+                    $scheduledDays = [1]; // 默认周一
+                }
+
+                $now = $nowInUserTz;
+                $next = null;
+
+                //搜索 8 天:确保包含“下周同一天”
+                for ($i = 0; $i <= 7; $i++) {
+                    $candidate = (clone $now)->modify("+$i days")->setTime($h, $m, $s);
+                    if (in_array((int)$candidate->format('N'), $scheduledDays)) {
+                        if ($candidate >= $now) {
+                            $next = $candidate;
+                            break;
+                        }
+                    }
+                }
+                // fallback:仅当 $scheduledDays 为空或极端错误时触发
+                if (!$next) {
+                    // 安全兜底:取最小周几,安排到下周
+                    $dow = min($scheduledDays ?: [1]);
+                    $next = (clone $now)
+                        ->modify('next monday')
+                        ->modify('+' . ($dow - 1) . ' days')
+                        ->setTime($h, $m, $s);
+                }
+                break;
+
+            case 'monthly':
+                [$h, $m, $s] = $parseTime($config['monthly_time'] ?? '09:00:00');
+                $days = array_map('intval', (array)($config['monthly_day'] ?? []));
+                if (empty($days)) {
+                    $days = [1]; // 默认每月1号
+                }
+
+                $targetDay = null;
+                // 尝试在当前月找一个 >= 今天的日期
+                foreach ($days as $day) {
+                    $temp = $nowInUserTz->setDate(
+                        (int)$nowInUserTz->format('Y'),
+                        (int)$nowInUserTz->format('m'),
+                        $day
+                    )->setTime($h, $m, $s);
+                    if ($temp >= $nowInUserTz) {
+                        $targetDay = $day;
+                        break;
+                    }
+                }
+
+                if ($targetDay !== null) {
+                    $next = $nowInUserTz->setDate(
+                        (int)$nowInUserTz->format('Y'),
+                        (int)$nowInUserTz->format('m'),
+                        $targetDay
+                    )->setTime($h, $m, $s);
+
+                    // 处理无效日期(如 2月31日 → 自动变为月末)
+                    if ((int)$next->format('d') !== $targetDay) {
+                        $next = $next->modify('last day of this month')->setTime($h, $m, $s);
+                    }
+                } else {
+                    // 当前月没有合适日期,跳到下个月
+                    $nextMonth = $nowInUserTz->modify('first day of next month');
+                    $targetDay = $days[0]; // 取第一个
+                    $next = $nextMonth->setDate(
+                        (int)$nextMonth->format('Y'),
+                        (int)$nextMonth->format('m'),
+                        $targetDay
+                    )->setTime($h, $m, $s);
+
+                    if ((int)$next->format('d') !== $targetDay) {
+                        $next = $next->modify('last day of this month')->setTime($h, $m, $s);
+                    }
+                }
+                break;
+
+            case 'quarterly':
+                [$h, $m, $s] = $parseTime($config['quarterly_time'] ?? '09:00:00');
+                $monthInQuarter = max(1, min(3, (int)($config['quarterly_month'] ?? 1))); // 1~3
+                $day = max(1, min(31, (int)($config['quarterly_day'] ?? 1)));
+
+                $currentYear = (int)$nowInUserTz->format('Y');
+                $currentMonth = (int)$nowInUserTz->format('m');
+                $currentQuarter = ceil($currentMonth / 3);
+                $targetMonthThisQuarter = ($currentQuarter - 1) * 3 + $monthInQuarter;
+
+                // 构造本季度目标日期
+                $next = (new \DateTimeImmutable("{$currentYear}-01-01", $userTz))
+                    ->setDate($currentYear, $targetMonthThisQuarter, $day)
+                    ->setTime($h, $m, $s);
+
+                // 修正无效日期
+                if ((int)$next->format('d') !== $day) {
+                    $next = $next->modify('last day of this month')->setTime($h, $m, $s);
+                }
+
+                if ($next <= $nowInUserTz) {
+                    // 跳到下一季度
+                    $nextQuarter = $currentQuarter + 1;
+                    if ($nextQuarter > 4) {
+                        $nextQuarter = 1;
+                        $currentYear++;
+                    }
+                    $targetMonthNextQuarter = ($nextQuarter - 1) * 3 + $monthInQuarter;
+                    $next = (new \DateTimeImmutable("{$currentYear}-01-01", $userTz))
+                        ->setDate($currentYear, $targetMonthNextQuarter, $day)
+                        ->setTime($h, $m, $s);
+
+                    if ((int)$next->format('d') !== $day) {
+                        $next = $next->modify('last day of this month')->setTime($h, $m, $s);
+                    }
+                }
+                break;
+
+            case 'yearly':
+                [$h, $m, $s] = $parseTime($config['yearly_time'] ?? '09:00:00');
+                $months = array_map('intval', (array)($config['yearly_month'] ?? []));
+                $day = max(1, (int)($config['yearly_day'] ?? 1));
+
+                if (empty($months)) {
+                    $months = [1];
+                }
+
+                $targetMonth = null;
+                foreach ($months as $month) {
+                    $temp = $nowInUserTz->setDate((int)$nowInUserTz->format('Y'), $month, $day);
+                    if ($temp >= $nowInUserTz) {
+                        $targetMonth = $month;
+                        break;
+                    }
+                }
+
+                if ($targetMonth !== null) {
+                    $next = $nowInUserTz->setDate((int)$nowInUserTz->format('Y'), $targetMonth, $day)->setTime($h, $m, $s);
+                    if ((int)$next->format('m') !== $targetMonth) {
+                        $next = $next->modify('last day of this month')->setTime($h, $m, $s);
+                    }
+                } else {
+                    // 明年
+                    $targetMonth = $months[0];
+                    $next = $nowInUserTz->modify('+1 year')
+                        ->setDate((int)$nowInUserTz->modify('+1 year')->format('Y'), $targetMonth, $day)
+                        ->setTime($h, $m, $s);
+                    if ((int)$next->format('m') !== $targetMonth) {
+                        $next = $next->modify('last day of this month')->setTime($h, $m, $s);
+                    }
+                }
+                break;
+
+            default:
+                $next = $nowInUserTz->setTime(9, 0, 0);
+                if ($next <= $nowInUserTz) {
+                    $next = $next->modify('+1 day');
+                }
+                break;
+        }
+
+        // ────────────────────────────────────────────────
+        // 第四步:转成 UTC 时间(无时区),用于存入数据库
+        // ────────────────────────────────────────────────
+        return $next->setTimezone(new \DateTimeZone('UTC'));
+    }
+
+    /**
+     * 解析用户输入的时区(如 'UTC+08' 或 'Asia/Shanghai')为 DateTimeZone
+     */
+    public static function parseUserTimezone(string $tzStr): \DateTimeZone
+    { 
+        $tzStr = trim($tzStr);
+
+        // 1. 尝试标准 IANA 时区
+        if (@in_array($tzStr, timezone_identifiers_list())) {
+            return new \DateTimeZone($tzStr);
+        }
+
+        // 2. 处理 UTC±H[H] 或 UTC±H[H]:MM
+        if (preg_match('/^UTC([+-])(\d{1,2})(?::(\d{2}))?$/', $tzStr, $matches)) {
+            $sign = $matches[1];
+            $hours = (int)$matches[2];
+            $minutes = isset($matches[3]) ? (int)$matches[3] : 0;
+
+            if ($hours >= 0 && $hours <= 14 && $minutes < 60) {
+                // 构造 +HH:MM 或 -HH:MM 格式(PHP 原生支持)
+                $offsetStr = sprintf('%s%02d:%02d', $sign, $hours, $minutes);
+                try {
+                    return new \DateTimeZone($offsetStr);
+                } catch (\Exception $e) {
+                    // fallback
+                }
+            }
+        }
+
+        // 3. 默认
+        return new \DateTimeZone('UTC');
+    }
 }
 ?>

Some files were not shown because too many files changed in this diff