fix(testing): GranaryBackend 白名单放行 edit 与 detail 动作并支持 ids 参数

这个提交包含在:
wikiboss
2026-11-02 14:30:42 +08:00
父节点 43da3cc0ee
当前提交 4a1e4f1d93
共有 5 个文件被更改,包括 145 次插入 和 16 次删除

查看文件

@@ -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"
},