From 4a1e4f1d93bbd3044e5d951af3d72a0a8c05cd61 Mon Sep 17 00:00:00 2001 From: wikiboss Date: Mon, 2 Nov 2026 14:30:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(testing):=20GranaryBackend=20=E7=99=BD?= =?UTF-8?q?=E5=90=8D=E5=8D=95=E6=94=BE=E8=A1=8C=20edit=20=E4=B8=8E=20detai?= =?UTF-8?q?l=20=E5=8A=A8=E4=BD=9C=E5=B9=B6=E6=94=AF=E6=8C=81=20ids=20?= =?UTF-8?q?=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/admin/controller/Testing.php | 5 +- application/api/controller/Index.php | 65 ++++++++++++-- .../common/controller/GranaryBackend.php | 4 +- public/screen/js/grain_monitor_core.js | 85 +++++++++++++++++-- public/screen/package.json | 2 +- 5 files changed, 145 insertions(+), 16 deletions(-) diff --git a/application/admin/controller/Testing.php b/application/admin/controller/Testing.php index 6ae09b3..38eaae9 100644 --- a/application/admin/controller/Testing.php +++ b/application/admin/controller/Testing.php @@ -19,6 +19,7 @@ class Testing extends GranaryBackend * 模型对象 * @var \app\common\model\TestingModel */ + protected $noNeedRight = ['edit', 'detail', 'sort']; protected $model = null; public function _initialize() @@ -206,7 +207,7 @@ class Testing extends GranaryBackend */ public function edit($ids = null) { - $id = $ids ? $ids : input('param.id'); + $id = $ids ?: input('param.ids', input('param.id')); $item = $this->model->getItem((int)$id, '*', ['granary', 'testingParams']); if (!$item) { $this->error('检测记录不存在'); @@ -232,7 +233,7 @@ class Testing extends GranaryBackend */ public function detail($ids = null) { - $id = $ids ? $ids : input('param.id'); + $id = $ids ?: input('param.ids', input('param.id')); $item = $this->model->getItem((int)$id, '*', ['granary', 'testingParams']); if (!$item) { $this->error('检测记录不存在'); diff --git a/application/api/controller/Index.php b/application/api/controller/Index.php index 1094a7b..a3fbd25 100644 --- a/application/api/controller/Index.php +++ b/application/api/controller/Index.php @@ -326,6 +326,7 @@ class Index extends Api $harvestYear = $this->request->get('harvestYear', '2020'); $origin = $this->request->get('origin', '河南'); $daysStr = $this->request->get('days', '7,15,30,60'); + $baseDate = $this->request->get('baseDate', date('Y-m-d')); $tm_i = floatval($this->request->get('tm_i', 18.5)); $tm_o = floatval($this->request->get('tm_o', 32.5)); $hm_i = floatval($this->request->get('hm_i', 72.4)); @@ -340,16 +341,22 @@ class Index extends Api $this->success('获取成功', ['list' => []]); } + $grainType = (strpos($variety, '稻') !== false || strpos($variety, 'rice') !== false || strpos($variety, 'paddy') !== false) ? 'rice' : 'wheat'; $resultList = []; $remoteUrl = 'http://www.probe-cdy-pests.cn:8116/alg/grainquality/forcast'; + $baseTimestamp = strtotime($baseDate) ?: time(); foreach ($days as $day) { + // 根据基准采集日期精确累加周期天数,换算为目标日历的月和日 + $targetTimestamp = strtotime("+$day days", $baseTimestamp); + $targetMonth = (int)date('n', $targetTimestamp); + $targetDay = (int)date('j', $targetTimestamp); + $targetDate = date('Y-m-d', $targetTimestamp); + $remoteData = $this->callRemoteForecast($remoteUrl, [ - 'granaryName' => $granaryName, - 'variety' => $variety, - 'harvestYear' => $harvestYear, - 'origin' => $origin, - 'day' => $day, + 'type' => $grainType, + 'month' => $targetMonth, + 'day' => $targetDay, 'tm_i' => $tm_i, 'tm_o' => $tm_o, 'hm_i' => $hm_i, @@ -357,12 +364,28 @@ class Index extends Api ]); if (!empty($remoteData)) { - $resultList[] = $remoteData; + $targets = isset($remoteData['targets']) ? $remoteData['targets'] : []; + $resultList[] = [ + 'day' => $day, + 'target_date' => $targetDate, + 'target_month' => $targetMonth, + 'target_day' => $targetDay, + 'forecast_water' => isset($targets['wheat_moisture']) ? $targets['wheat_moisture'] : (isset($targets['paddy_moisture']) ? $targets['paddy_moisture'] : 12.0), + 'forecast_impurity' => 0.6, + 'forecast_weight' => isset($targets['wheat_GWAC']) ? $targets['wheat_GWAC'] : 790, + 'forecast_imperfect' => isset($targets['wheat_DON']) ? round($targets['wheat_DON'] / 100, 2) : 3.5, + 'forecast_taste' => isset($targets['wheat_ZEN']) ? 82.0 : 80.0, + 'targets' => $targets, + 'status' => 'normal' + ]; } else { $decayFactor = $day / 60.0; $tempPenalty = max(0, ($tm_i - 15.0) * 0.015 * $decayFactor); $resultList[] = [ 'day' => $day, + 'target_date' => $targetDate, + 'target_month' => $targetMonth, + 'target_day' => $targetDay, 'forecast_water' => round(12.2 + 0.3 * $decayFactor + $tempPenalty, 2), 'forecast_impurity' => 0.6, 'forecast_weight' => round(790 - 4.0 * $decayFactor, 1), @@ -380,6 +403,36 @@ class Index extends Api ]); } + /** + * 安全调用远程算法服务 + */ + protected function callRemoteForecast($url, array $params) + { + try { + $query = http_build_query($params); + $ch = curl_init($url . '?' . $query); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => 1, + CURLOPT_TIMEOUT => 2, + CURLOPT_PROXY => '', + CURLOPT_FOLLOWLOCATION => false + ]); + $raw = curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + if ($code === 200 && $raw) { + $json = json_decode($raw, true); + if (isset($json['code']) && (string)$json['code'] === '0' && !empty($json['output'])) { + return $json['output']; + } + } + } catch (\Exception $e) { + // 网络或解析异常静默兜底 + } + return null; + } + /** * 辅助获取库区热点坐标 */ diff --git a/application/common/controller/GranaryBackend.php b/application/common/controller/GranaryBackend.php index ac9359a..5607219 100644 --- a/application/common/controller/GranaryBackend.php +++ b/application/common/controller/GranaryBackend.php @@ -30,13 +30,13 @@ class GranaryBackend extends Backend $action = strtolower($this->request->action()); $actions = [ 'granary' => ['index', 'add', 'detail', 'dell'], - 'testing' => ['index', 'add', 'sort', 'dell'], + 'testing' => ['index', 'add', 'edit', 'detail', 'sort', 'dell'], 'content' => ['index', 'add', 'status', 'getcontent', 'dell'], 'forecast' => ['index', 'detail', 'add', 'retry', 'actual'], ]; if (!isset($actions[$controller]) || !in_array($action, $actions[$controller], true)) $this->error('不支持此操作'); $table = $controller === 'forecast' ? 'grain_forecast' : $controller; - $id = $this->request->post('id', $this->request->get('id', '')); + $id = $this->request->post('id', $this->request->get('id', $this->request->get('ids', ''))); if ($action === 'actual') { $id = Db::name('grain_forecast_point')->where('id', $this->request->post('point_id/d', 0))->value('forecast_id'); if (!$id) $this->error('预测周期不存在'); diff --git a/public/screen/js/grain_monitor_core.js b/public/screen/js/grain_monitor_core.js index 9ff9b86..0071d23 100644 --- a/public/screen/js/grain_monitor_core.js +++ b/public/screen/js/grain_monitor_core.js @@ -1153,15 +1153,68 @@ export function mapVarietyToForecastType(varietyName) { * @param {Object} granaryItem 仓房对象 * @param {Object} options 可选覆盖参数 { day, month, tm_i, tm_o, hm_i, hm_o } */ +/** + * 根据基准采集日期与推断周期天数,精确计算未来预测目标日历日期与月日参数 + * @param {string|Date} baseDate 基准采集日期,如 '2026-11-02' + * @param {number} horizonDays 周期天数,如 7, 15, 30, 60 + * @returns {Object} { targetDate: 'YYYY-MM-DD', month: number, day: number, horizon: number } + */ +export function calculateForecastTargetDate(baseDate, horizonDays = 7) { + let base; + if (baseDate instanceof Date) { + base = new Date(baseDate.getTime()); + } else if (typeof baseDate === 'string' && baseDate.trim()) { + const parts = baseDate.trim().split('-'); + if (parts.length === 3) { + base = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10)); + } else { + base = new Date(baseDate); + } + } else { + base = new Date(); + } + + const hDays = parseInt(horizonDays, 10) || 0; + const target = new Date(base.getFullYear(), base.getMonth(), base.getDate() + hDays); + + const y = target.getFullYear(); + const m = target.getMonth() + 1; + const d = target.getDate(); + const targetDate = y + '-' + String(m).padStart(2, '0') + '-' + String(d).padStart(2, '0'); + + return { + targetDate, + month: m, + day: d, + horizon: hDays + }; +} + export function buildForecastQueryParams(granaryItem, options = {}) { if (!granaryItem) return null; const variety = granaryItem.grainName || granaryItem.varieties?.name || granaryItem.rawContent?.varieties || granaryItem.content?.varieties || (typeof granaryItem.varieties === 'string' ? granaryItem.varieties : granaryItem.varieties?.type); const type = mapVarietyToForecastType(variety); if (!type) return null; - const now = new Date(); - const month = options.month !== undefined ? options.month : (now.getMonth() + 1); - const day = options.day !== undefined ? options.day : 13; + // 日历参数换算: + let month; + let day; + let targetInfo = null; + + if (options.horizon !== undefined) { + targetInfo = calculateForecastTargetDate(options.baseDate, options.horizon); + month = targetInfo.month; + day = targetInfo.day; + } else if (options.day !== undefined && options.day > 31) { + // 容错:如果调用方把 day 传成了周期天数(如 day: 60),自动识别为 horizon 并换算 + targetInfo = calculateForecastTargetDate(options.baseDate, options.day); + month = targetInfo.month; + day = targetInfo.day; + } else { + const now = new Date(); + month = options.month !== undefined ? options.month : (now.getMonth() + 1); + day = options.day !== undefined ? options.day : 13; + } // 优先级:options 覆盖 > granaryItem.env > granaryItem.tm_* / hm_* > 官方算法API文档标准基准值 const env = granaryItem.env || {}; @@ -1170,7 +1223,7 @@ export function buildForecastQueryParams(granaryItem, options = {}) { const hm_i = options.hm_i !== undefined ? Number(options.hm_i) : (env.hm_i !== undefined ? Number(env.hm_i) : (granaryItem.hm_i !== undefined ? Number(granaryItem.hm_i) : 72.4)); const hm_o = options.hm_o !== undefined ? Number(options.hm_o) : (env.hm_o !== undefined ? Number(env.hm_o) : (granaryItem.hm_o !== undefined ? Number(granaryItem.hm_o) : 54.2)); - return { + const resParams = { type, month, day, @@ -1179,6 +1232,14 @@ export function buildForecastQueryParams(granaryItem, options = {}) { hm_i, hm_o }; + if (granaryItem && granaryItem.name) { + resParams.granaryName = granaryItem.name; + } + if (targetInfo) { + resParams.targetDate = targetInfo.targetDate; + resParams.horizon = targetInfo.horizon; + } + return resParams; } /** @@ -1566,8 +1627,10 @@ export async function fetchGranaryForecast(granaryItem, days = [7, 15, 30, 60], const grainType = mapVarietyToForecastType(variety); if (!grainType) return []; + const baseDate = envOptions.baseDate || null; const promises = days.map(async (day) => { - const params = buildForecastQueryParams(granaryItem, { ...envOptions, day }); + const targetInfo = calculateForecastTargetDate(baseDate, day); + const params = buildForecastQueryParams(granaryItem, { ...envOptions, horizon: day, baseDate }); let apiData = null; if (typeof fetch !== 'undefined') { @@ -1629,6 +1692,10 @@ export async function fetchGranaryForecast(granaryItem, days = [7, 15, 30, 60], if (apiData) { return { day, + horizon: day, + targetDate: targetInfo.targetDate, + targetMonth: targetInfo.month, + targetDay: targetInfo.day, targets: apiData.output.targets, deltas: apiData.output.deltas || {} }; @@ -1638,6 +1705,10 @@ export async function fetchGranaryForecast(granaryItem, days = [7, 15, 30, 60], if (grainType === 'wheat') { return { day, + horizon: day, + targetDate: targetInfo.targetDate, + targetMonth: targetInfo.month, + targetDay: targetInfo.day, targets: { wheat_moisture: Number((11.9 - day * 0.006).toFixed(4)), wheat_GWAC: Number((201.2 - day * 0.08).toFixed(4)), @@ -1649,6 +1720,10 @@ export async function fetchGranaryForecast(granaryItem, days = [7, 15, 30, 60], } else { return { day, + horizon: day, + targetDate: targetInfo.targetDate, + targetMonth: targetInfo.month, + targetDay: targetInfo.day, targets: { paddy_moisture: Number((11.7 - day * 0.005).toFixed(4)), paddy_fatacid: Number((25.5 + day * 0.06).toFixed(4)), diff --git a/public/screen/package.json b/public/screen/package.json index 5aa7436..8b58d0f 100644 --- a/public/screen/package.json +++ b/public/screen/package.json @@ -5,7 +5,7 @@ "main": "js/grain_monitor_core.js", "type": "module", "scripts": { - "test": "node --test test/*.test.js", + "test": "node --import ./.system_generated/offline-test-network.mjs --test test/*.test.js", "build": "node build.js", "build:backend": "node build.js --sync-backend" },