fix(testing): GranaryBackend 白名单放行 edit 与 detail 动作并支持 ids 参数
这个提交包含在:
@@ -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('检测记录不存在');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助获取库区热点坐标
|
||||
*/
|
||||
|
||||
@@ -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('预测周期不存在');
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
在新工单中引用
屏蔽一个用户