<?php
define('APP_PATH','/workspace/application/');require '/workspace/env.php';require '/workspace/thinkphp/base.php';require_once '/workspace/application/common.php';\think\Config::set(require APP_PATH.'database.php','database');\think\Config::set('meal_type',3);
function qaRatio($a,$b){return $b?round($a*100/$b,2):null;}
function qaPer100($d,$f){return $d['weight']>0?(float)$d[$f]*100/$d['weight']:0;}
function qaNutrition($d){
 $rules=[1=>['低GL',['gl:3']],6=>['高膳食纤维',['dietary_fiber:1']],8=>['高蛋白',['protein:1']],15=>['高维生素',['va:1','vb1:1','vb2:1','vc:1','ve:1']],17=>['低脂肪',['fat:3']]];
 if(!isset($rules[$d['category_one']]))return ['label'=>null,'matched'=>false];
 $rule=$rules[$d['category_one']];$match=(bool)array_intersect($rule[1],$d['tags']);if((int)$d['category_one']===1)$match=$match&&qaPer100($d,'gl')>0&&qaPer100($d,'gl')<10;
 return ['label'=>$rule[0],'matched'=>$match];
}
function qaFood($r){
 $c=(int)$r['category'];$s=(int)$r['minute_sort'];
 if(($c===1&&$s===21)||($c===12&&$s===57))return ['staple','杂粮'];
 if($c===1)return ['staple','米面制品'];if($c===2)return ['staple',$s===22?'薯类':'淀粉类'];
 if($c===6)return ['vegetable',$s===38?'深色蔬菜':'浅色蔬菜'];if($c===7)return ['vegetable',$s===41?'海藻':'菌菇'];if($c===8)return ['vegetable','水果'];
 if(in_array($c,[3,4],true))return ['meat','畜禽肉'];if($c===5)return ['meat','海产'];if($c===10)return ['meat','蛋类'];
 if($c===9)return ['dairy','坚果'];if($c===11)return ['dairy','奶类'];if($c===12)return ['dairy','豆制品'];return null;
}
function qaStructures($dishes,$recipes,$events){
 $defs=['staple'=>[30,['米面制品','杂粮','薯类','淀粉类']],'vegetable'=>[50,['深色蔬菜','浅色蔬菜','菌菇','海藻','水果']],'meat'=>[50,['海产','畜禽瘦肉','蛋类','肥瘦相间','肥肉']]];$out=[];
 foreach($defs as $key=>$def){$items=[];$detail=array_fill_keys($def[1],0);$priority=0;
  foreach($dishes as $d){$rows=$recipes[$d['uuid']]??[];$sum=0;foreach($rows as $r){$class=qaFood($r);if($class&&$class[0]===$key)$sum+=(float)$r['composition_ratio'];}if($sum<50)continue;
   foreach($rows as $r){$class=qaFood($r);if(!$class||$class[0]!==$key||(float)$r['composition_ratio']<10)continue;$id=$d['uuid'].':'.$class[1].':'.trim($r['composition_name']);if(isset($items[$id]))continue;$items[$id]=true;$kind=$class[1];if($kind==='畜禽肉'){$fat=qaPer100($d,'fat');$kind=$fat<=10?'畜禽瘦肉':($fat<30?'肥瘦相间':'肥肉');}$detail[$kind]++;if(in_array($kind,['杂粮','深色蔬菜','海产','畜禽瘦肉','蛋类'],true))$priority++;}
  }
  $n=count($items);$out[$key]=['count'=>$n,'priority_count'=>$priority,'ratio'=>qaRatio($priority,$n),'threshold'=>$def[0],'reasonable'=>$n?$priority*100>=$n*$def[0]:null,'detail'=>$detail];
 }
 $days=[];$eventsSeen=[];foreach($events as $e)foreach($recipes[$e['dishes_uuid']]??[] as $r){$class=qaFood($r);if(!$class||$class[0]!=='dairy'||(float)$r['composition_ratio']<=0)continue;$kind=$class[1];$days[$kind][$e['menu_date']]=true;$eventsSeen[$kind][$e['menu_date'].':'.$e['meal_times'].':'.$e['dishes_uuid']]=true;}
 $out['dairy']=[];foreach(['奶类','豆制品','坚果'] as $kind)$out['dairy'][$kind]=['days'=>count($days[$kind]??[]),'count'=>count($eventsSeen[$kind]??[])];return $out;
}
function qaPopularity($dishes,$start,$end){
 $ids=array_column($dishes,'uuid');$values=array_fill_keys($ids,0.0);$recordCounts=array_fill_keys($ids,0);if(!$ids)return ['values'=>$values,'counts'=>$recordCounts,'people'=>0,'records'=>0,'median'=>null,'quadrants'=>[]];
 $q=function()use($ids,$start,$end){return \think\Db::name('date_menu')->where('restaurant_id',1)->where('menu_date','>=',$start)->where('menu_date','<=',$end)->where('meal_type',3)->where('dishes_uuid','in',$ids);};
 $people=(int)$q()->count('DISTINCT staff_uuid');$rows=$q()->where('meal_times','in',[1,2,3,4])->field('dishes_uuid,menu_date,meal_times,COUNT(*) AS n')->group('dishes_uuid,menu_date,meal_times')->select();$meals=[];
 foreach($rows as $r){$recordCounts[$r['dishes_uuid']]+=(int)$r['n'];$meals[$r['dishes_uuid']][$r['menu_date']][]=$people?round((int)$r['n']/$people,2):0;}
 foreach($meals as $uuid=>$dates){$daily=[];foreach($dates as $v)$daily[]=round(array_sum($v)/count($v),2);$values[$uuid]=round(array_sum($daily)/count($daily),2)*100;}
 $sorted=array_values($values);sort($sorted,SORT_NUMERIC);$median=$sorted[(int)floor(count($sorted)/2)];$quad=['retain'=>[],'promote'=>[],'improve'=>[],'reduce'=>[]];
 foreach($dishes as $d){$good=qaNutrition($d)['matched'];$high=$values[$d['uuid']]>$median;$quad[$good?($high?'retain':'promote'):($high?'improve':'reduce')][$d['uuid']]=true;}
 return ['values'=>$values,'counts'=>$recordCounts,'people'=>$people,'records'=>array_sum($recordCounts),'median'=>$median,'quadrants'=>$quad];
}
function qaCheckStructures($expected,$actual){$errors=[];foreach(['staple','vegetable','meat'] as $key){foreach(['count','priority_count','ratio','threshold','reasonable'] as $field)if($expected[$key][$field]!==$actual[$key][$field])$errors[]=$key.'.'.$field;foreach($actual[$key]['list'] as $r){if(($expected[$key]['detail'][$r['name']]??null)!==$r['count']||qaRatio($r['count'],$expected[$key]['count'])!==$r['ratio'])$errors[]=$key.'.detail.'.$r['name'];}}
 foreach($actual['dairy']['list'] as $r)if($expected['dairy'][$r['name']]['days']!==$r['days']||$expected['dairy'][$r['name']]['count']!==$r['count'])$errors[]='dairy.'.$r['name'];return $errors;}
\think\Db::execute('SET TRANSACTION READ ONLY');\think\Db::startTrans();
try{
 $start='2026-09-12';$end='2026-10-12';$store=getStoreId();$p=\think\Config::get('database.prefix');
 $raw=\think\Db::name('dishes')->where('store_id',$store)->where('is_del',1)->where('status',1)->where('is_delete',0)->field('id,uuid,name,category_one,category_two,weight,fat,protein,dietary_fiber,purine,gl,va,vb1,vb2,vc,ve,cook_method')->select();$pool=[];foreach($raw as $d){$d['tags']=[];$pool[$d['uuid']]=$d;}
 $tags=\think\Db::name('dishes_tag')->where('store_id',$store)->where('is_delete',0)->field('dishes_id,nutrient,level')->select();$tagmap=[];foreach($tags as $t)$tagmap[$t['dishes_id']][]=$t['nutrient'].':'.$t['level'];foreach($pool as &$d)$d['tags']=$tagmap[$d['id']]??[];unset($d);
 $events=\think\Db::query('SELECT DISTINCT r.menu_date,r.meal_times,r.dishes_uuid FROM '.$p.'recipes r JOIN '.$p.'dishes d ON d.uuid=r.dishes_uuid WHERE r.restaurant_id=1 AND r.menu_date BETWEEN ? AND ? AND d.store_id=? AND d.is_del=1 AND d.status=1 AND d.is_delete=0 ORDER BY r.menu_date,r.meal_times,r.dishes_uuid',[$start,$end,$store]);$supplied=[];foreach($events as $e)$supplied[$e['dishes_uuid']]=$pool[$e['dishes_uuid']];
 $foodRows=\think\Db::query('SELECT r.dishes_uuid,r.composition_name,r.foods_ratio AS composition_ratio,f.id AS food_id,f.category,f.category_two AS minute_sort FROM '.$p.'dish r JOIN '.$p.'dishes d ON d.id=r.dishes_id LEFT JOIN '.$p.'foods_composition f ON f.id=r.foods_id AND f.store_id=r.store_id WHERE r.store_id=? AND d.store_id=? AND r.is_delete=0 AND d.is_del=1 AND d.status=1 AND d.is_delete=0',[$store,$store]);$recipes=[];$bad=[];foreach($foodRows as $r){if($r['food_id']===null)$bad[$r['dishes_uuid']]=true;$recipes[$r['dishes_uuid']][]=$r;}foreach($bad as $uuid=>$v)unset($pool[$uuid]);
 $dict=\think\Db::name('config_enum')->where('enum_type','4')->where('is_delete',0)->field('enum_key,enum_name,recommend,`order`')->select();$dict=is_array($dict)?$dict:$dict->toArray();usort($dict,function($a,$b){return (int)$a['order']<=>(int)$b['order'];});$cooks=array_column($dict,null,'enum_key');
 $actual=(new \app\p\logic\MenuNutrition)->analyze(1,$start,$end);
 $fullPop=qaPopularity($supplied,$start,$end);$structures=qaStructures($supplied,$recipes,$events);$failures=[];$checks=[];$audit=[];
 $errors=qaCheckStructures($structures,$actual['structures']);$checks['full_structures']=!$errors;foreach($errors as $e)$failures[]='structure.'.$e;
 foreach($actual['popularity'] as $d){$uuid=$d['dish_id'];$ok=$d['value']===$fullPop['values'][$uuid]&&$d['selection_count']===$fullPop['counts'][$uuid]&&$d['nutrition']===qaNutrition($supplied[$uuid]);if(!$ok)$failures[]='popularity '.$d['name'];$audit[]=['dish_id'=>$uuid,'name'=>$d['name'],'value'=>$fullPop['values'][$uuid],'records'=>$fullPop['counts'][$uuid],'nutrition'=>qaNutrition($supplied[$uuid])];}$checks['all_popularity']=!count(array_filter($failures,function($x){return strpos($x,'popularity ')===0;}));
 $weekEvents=[];foreach($events as $e){$date=new DateTimeImmutable($e['menu_date']);$w=$date->modify('-'.($date->format('N')-1).' days')->format('Y-m-d');$weekEvents[$w][]=$e;}ksort($weekEvents);$checks['week_keys']=array_keys($weekEvents)===array_column($actual['recommendations'],'week_start');
 $weeks=[];$unusedBetter=[];$selectedTotal=0;$actualWeeks=array_column($actual['recommendations'],null,'week_start');
 $common=null;foreach($pool as $d){if((int)$d['category_one']!==15||$d['weight']<=0||!qaNutrition($d)['matched'])continue;$t=[];foreach(['va','vb1','vb2','vc','ve'] as $f)if(in_array($f.':1',$d['tags'],true))$t[]=$f;$common=$common===null?$t:array_values(array_intersect($common,$t));}$vitamin=count($common??[])===1?$common[0]:null;
 $defs=['staple'=>[1,'gl',true],'vegetable'=>[6,'dietary_fiber',false],'meat'=>[8,'protein',false],'fruit'=>[15,$vitamin,false],'soup'=>[17,'fat',true],'cooking'=>[null,null,false],'tag_staple'=>[1,'gl',true],'tag_vegetable'=>[6,'dietary_fiber',false],'tag_meat'=>[8,'protein',false]];
 foreach($weekEvents as $w=>$we){$from=max($start,$w);$to=min($end,(new DateTimeImmutable($w))->modify('+6 days')->format('Y-m-d'));$wd=[];foreach($we as $e)$wd[$e['dishes_uuid']]=$supplied[$e['dishes_uuid']];$wp=qaPopularity($wd,$from,$to);$ws=qaStructures($wd,$recipes,$we);$plan=$actualWeeks[$w];$errors=[];$used=[];$groupsOut=[];$eligible=[];
  if($plan['analysis_start']!==$from||$plan['analysis_end']!==$to||$plan['has_records']!==($wp['people']>0))$errors[]='week range or record state';
  foreach($defs as $key=>$rule){$isStructure=in_array($key,['staple','vegetable','meat'],true);foreach($pool as $uuid=>$d){if($d['weight']<=0)continue;$okay=false;if($isStructure){if($ws[$key]['reasonable']===true)continue;$one=qaStructures([$d],$recipes,[]);$okay=(int)$d['category_one']===$rule[0]&&$one[$key]['count']>0&&$one[$key]['priority_count']>0;}elseif($key==='cooking')$okay=(int)($cooks[$d['cook_method']]['recommend']??0)===1;else $okay=(int)$d['category_one']===$rule[0]&&qaNutrition($d)['matched'];if($okay)$eligible[$key][$uuid]=$rule[1]?qaPer100($d,$rule[1]):null;}}
  if($wp['people'])foreach($wp['quadrants'] as $key=>$ids){$defs[$key]=[null,null,in_array($key,['promote','reduce'],true)];foreach($ids as $uuid=>$v)if(isset($pool[$uuid])&&$pool[$uuid]['weight']>0)$eligible[$key][$uuid]=$wp['values'][$uuid];}
  foreach($plan['groups'] as $key=>$group){$list=$group['list'];if(count($list)>3)$errors[]=$key.' >3';if(!isset($defs[$key]))$errors[]='unknown group '.$key;$previous=null;$names=[];
   if(in_array($key,['staple','vegetable','meat'],true)){$status=$ws[$key]['reasonable']===true?'maintain':($ws[$key]['reasonable']===false?'increase':'unknown');if($group['status']!==$status)$errors[]=$key.' status';if($status==='maintain'&&$list)$errors[]=$key.' satisfied but recommended';}
   foreach($list as $item){$uuid=$item['dish_id'];if(isset($used[$uuid]))$errors[]='duplicate '.$item['name'];$used[$uuid]=true;$names[]=$item['name'];$selectedTotal++;if(!isset($eligible[$key])||!array_key_exists($uuid,$eligible[$key])){$errors[]='ineligible '.$key.':'.$item['name'];continue;}$v=$eligible[$key][$uuid];$pair=['value'=>$v,'id'=>(int)$item['id']];if($previous!==null){$cmp=$previous['value']<=>$pair['value'];if(($defs[$key][2]?$cmp>0:$cmp<0)||($cmp===0&&$previous['id']>$pair['id']))$errors[]='unsorted '.$key;}$previous=$pair;}
   $groupsOut[$key]=['status'=>$group['status'],'names'=>$names];
  }
  foreach($plan['groups'] as $key=>$group){$list=$group['list'];if(count($list)<3){foreach($eligible[$key]??[] as $uuid=>$v)if(!isset($used[$uuid]))$errors[]='unfilled '.$key.':'.$pool[$uuid]['name'];}elseif(isset($defs[$key])&&$defs[$key][1]!==null){$last=end($list);$lastValue=$eligible[$key][$last['dish_id']];foreach($eligible[$key]??[] as $uuid=>$v){if(isset($used[$uuid]))continue;$cmp=$v<=>$lastValue;if(($defs[$key][2]?$cmp<0:$cmp>0)||($cmp===0&&(int)$pool[$uuid]['id']<(int)$last['id']))$unusedBetter[]=['week'=>$w,'group'=>$key,'selected_worst'=>['name'=>$last['name'],'value'=>$lastValue,'id'=>(int)$last['id']],'unused_better'=>['name'=>$pool[$uuid]['name'],'value'=>$v,'id'=>(int)$pool[$uuid]['id']]];}}}
  $weeks[]=['week_start'=>$w,'analysis_start'=>$from,'analysis_end'=>$to,'supply_events'=>count($we),'dishes'=>count($wd),'people'=>$wp['people'],'selection_records'=>$wp['records'],'median'=>$wp['median'],'quadrant_counts'=>array_map('count',$wp['quadrants']),'structures'=>$ws,'groups'=>$groupsOut,'errors'=>$errors];foreach($errors as $e)$failures[]=$w.' '.$e;
 }
 $cookCount=[];$steam=0;$recommended=0;foreach($events as $e){$d=$supplied[$e['dishes_uuid']];$cook=$cooks[$d['cook_method']];$name=$cook['enum_name'];$cookCount[$name]=($cookCount[$name]??0)+1;if(in_array($name,['蒸','煮','炖'],true))$steam++;if((int)$cook['recommend']===1)$recommended++;}$checks['cooking_facts']=$actual['cooking']['ratio']===qaRatio($recommended,count($events))&&$actual['cooking']['reasonable']===($recommended*100>=count($events)*90);$checks['selection_state']=$actual['has_records']===($fullPop['people']>0);
 $summary=['filter'=>['restaurant_id'=>1,'start'=>$start,'end'=>$end],'stage'=>['dish_count'=>count($supplied),'people'=>$fullPop['people'],'selection_records'=>$fullPop['records'],'median'=>$fullPop['median'],'quadrant_counts'=>array_map('count',$fullPop['quadrants']),'zero_without_records'=>count(array_filter($fullPop['counts'],function($n){return $n===0;})),'structures'=>$structures,'steam_count'=>$steam,'steam_ratio'=>qaRatio($steam,count($events)),'recommended_count'=>$recommended,'recommended_ratio'=>qaRatio($recommended,count($events))],'checks'=>$checks,'candidate_items_checked'=>$selectedTotal,'weeks'=>$weeks,'failures'=>$failures,'unused_better_candidates'=>$unusedBetter,'pending_rule'=>'跨类别最适合归属判据尚待业务确认，未以当前fit优先级代替业务验收'];
 file_put_contents('/out/quadrant-advice-result.json',json_encode($summary,JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT));file_put_contents('/out/actual-analysis.json',json_encode($actual,JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT));file_put_contents('/out/popularity-oracle.json',json_encode($audit,JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT));
 echo json_encode(['stage'=>$summary['stage'],'checks'=>$checks,'candidate_items_checked'=>$selectedTotal,'weeks'=>count($weeks),'failures'=>$failures,'unused_better_count'=>count($unusedBetter),'unused_better_examples'=>array_slice($unusedBetter,0,6)],JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT).PHP_EOL;
}finally{\think\Db::rollback();}
