@@ -450,6 +450,195 @@ private async Task<int> SaveParamsAsync(long ownerId, string ownerCode, int para
450450 private static string GetStr ( JsonElement el , string prop )
451451 => el . TryGetProperty ( prop , out var v ) && v . ValueKind == JsonValueKind . String ? v . GetString ( ) ?? "" : "" ;
452452
453+ // ==================== 报表智能生成 ====================
454+
455+ /// <summary>
456+ /// 根据需求生成报表(数据集 + 查询参数 + 排版),返回与报表设计器一致的 layoutJson 结构。
457+ /// </summary>
458+ public async Task < object > GenerateReportAsync ( string requirement , string ? reportName ,
459+ List < Dictionary < string , object ? > > ? dataViews , List < Dictionary < string , object ? > > ? dataSources ,
460+ List < Dictionary < string , object ? > > ? flows , List < Dictionary < string , object ? > > ? apis ,
461+ long providerId = 0 , string ? model = null )
462+ {
463+ if ( string . IsNullOrWhiteSpace ( requirement ) )
464+ throw new Exception ( "请先描述报表需求" ) ;
465+
466+ var systemPrompt = """
467+ 你是报表设计助手。根据用户需求生成报表定义 JSON(与报表设计器 layoutJson 结构一致)。
468+ 只输出 JSON(不要 markdown 围栏、不要解释),结构如下:
469+ {
470+ "page": { "size": "A4", "orientation": "portrait", "margin": {}, "header": "", "footer": "", "bgImage": "" },
471+ "params": [ // 查询参数(用户可筛选,与数据集 SQL 中 @参数名 对应)
472+ { "name": "beginDate", "label": "开始日期", "type": "date", "default": "" },
473+ { "name": "status", "label": "状态", "type": "select", "options": "paid,pending", "default": "" }
474+ ],
475+ "datasets": [ // 数据集(尽量复用已有数据视图)
476+ {
477+ "id": "ds1",
478+ "name": "订单数据",
479+ "sourceType": "dataview", // dataview=复用数据视图 / sql=自定义SQL / flow=流程 / api=接口
480+ "sourceRef": "12", // dataview=视图id;flow=flowKey;api=methodCode
481+ "customSql": "", // sql 类型时填 SQL(用 @参数名 引用查询参数,参数为空时不过滤)
482+ "dataSourceId": 3, // sql 类型时填数据源 id(必须来自可用数据源清单)
483+ "fields": []
484+ }
485+ ],
486+ "rows": [
487+ { "height": 35, "type": "title", "dataset": "", "expand": "" },
488+ { "height": 28, "type": "header", "dataset": "", "expand": "" },
489+ { "height": 25, "type": "data", "dataset": "ds1", "expand": "auto", "zebra": true },
490+ { "height": 25, "type": "footer", "dataset": "ds1", "expand": "" }
491+ ],
492+ "cols": [ { "width": 60 }, { "width": 150 }, { "width": 100 } ],
493+ "cells": [
494+ { "r": 0, "c": 0, "value": "销售报表", "colspan": 3, "style": { "bold": true, "align": "center", "fontSize": 16 } },
495+ { "r": 1, "c": 0, "value": "编号", "style": { "bold": true } },
496+ { "r": 1, "c": 1, "value": "名称" },
497+ { "r": 1, "c": 2, "value": "金额" },
498+ { "r": 2, "c": 0, "value": "${rowIndex}" },
499+ { "r": 2, "c": 1, "value": "${ds1.name}" }, // 数据行占位符:${字段} 或 ${数据集.字段}
500+ { "r": 2, "c": 2, "value": "${amount}" },
501+ { "r": 3, "c": 0, "value": "合计", "colspan": 2 },
502+ { "r": 3, "c": 2, "value": "${amount:SUM}", "style": { "bold": true } }
503+ ]
504+ }
505+
506+ 规则:
507+ 1. 数据集优先复用「可用数据视图」:sourceType=dataview、sourceRef=视图 id,不得编造;视图不满足时用 sourceType=sql + 可用数据源 id 写自定义 SQL(注意数据源方言:sqlite/mysql/postgresql/sqlserver);需要流程/接口数据时用 flow/api 类型并复用清单中的 flowKey/methodCode。
508+ 2. 数据集 SQL 中需要筛选的字段用 @参数名 引用 params 中的查询参数(如 WHERE status = @status),参数类型取 text/number/date/select(select 配 options 逗号分隔)。
509+ 3. 行结构固定:第 1 行 title(合并全部列)、第 2 行 header(列名)、数据行 type=data + dataset + expand=auto + zebra=true(可用多行)、最后 footer 行。
510+ 4. 数据行单元格用 ${字段名} 或 ${数据集.字段} 占位符;序号列用 ${rowIndex}。
511+ 5. 汇总/合计用聚合占位符:${金额:SUM}、${字段:AVG}、${字段:COUNT}、MIN/MAX;需要条件计算时用公式 =IF(条件, 真, 假)(如 =IF(${amount} > 100, '大额', '普通'))。
512+ 6. cols 宽度按内容合理分配(序号 60、文本 150、数字 100 左右)。
513+ 7. 标题行居中加粗 16 号,表头加粗;列名用中文。
514+ 8. 只输出 JSON 本体。
515+ """ ;
516+
517+ var userPrompt = $ """
518+ ## 报表需求
519+ { requirement . Trim ( ) }
520+
521+ ## 可用数据视图(dataview)
522+ { BuildSimpleList ( dataViews , "id" , "name" , "groupName" , "sql" ) }
523+
524+ ## 可用数据源(自定义SQL用)
525+ { BuildSimpleList ( dataSources , "id" , "dataSourceName" , "dataSourceType" , null ) }
526+
527+ ## 已发布流程(flow)
528+ { BuildSimpleList ( flows , "flowKey" , "flowName" , null , null ) }
529+
530+ ## 可用接口(api)
531+ { BuildSimpleList ( apis , "methodCode" , "methodName" , "suiteCode" , "url" ) }
532+
533+ 请生成报表 JSON。
534+ """ ;
535+
536+ var content = await ChatAsync ( systemPrompt , userPrompt , providerId , modelOverride : model ) ;
537+ return ExtractAndValidateReport ( content , reportName ) ;
538+ }
539+
540+ /// <summary>提取报表查询参数(name/label/type/options/default)。</summary>
541+ private static List < Dictionary < string , object ? > > ExtractReportParams ( JsonElement root )
542+ {
543+ var list = new List < Dictionary < string , object ? > > ( ) ;
544+ if ( ! root . TryGetProperty ( "params" , out var arr ) || arr . ValueKind != JsonValueKind . Array ) return list ;
545+ foreach ( var p in arr . EnumerateArray ( ) )
546+ {
547+ if ( p . ValueKind != JsonValueKind . Object ) continue ;
548+ var name = GetStr ( p , "name" ) ;
549+ if ( string . IsNullOrEmpty ( name ) ) continue ;
550+ list . Add ( new Dictionary < string , object ? >
551+ {
552+ [ "name" ] = name ,
553+ [ "label" ] = GetStr ( p , "label" ) is { Length : > 0 } l ? l : name ,
554+ [ "type" ] = GetStr ( p , "type" ) is { Length : > 0 } t ? t : "text" ,
555+ [ "options" ] = GetStr ( p , "options" ) ,
556+ [ "default" ] = GetStr ( p , "default" )
557+ } ) ;
558+ }
559+ return list ;
560+ }
561+
562+ private static string BuildSimpleList ( List < Dictionary < string , object ? > > ? items , string code , string name , string ? extra , string ? extra2 )
563+ {
564+ if ( items == null || items . Count == 0 ) return "(无)" ;
565+ var sb = new StringBuilder ( ) ;
566+ foreach ( var it in items )
567+ {
568+ sb . Append ( $ "- { code } ={ it . GetValueOrDefault ( code ) } { name } ={ it . GetValueOrDefault ( name ) } ") ;
569+ if ( extra != null ) sb . Append ( $ " { extra } ={ it . GetValueOrDefault ( extra ) } ") ;
570+ if ( extra2 != null )
571+ {
572+ var v = it . GetValueOrDefault ( extra2 ) ? . ToString ( ) ;
573+ if ( ! string . IsNullOrEmpty ( v ) && v . Length > 60 ) v = v [ ..60 ] + "..." ;
574+ sb . Append ( $ " { extra2 } ={ v } ") ;
575+ }
576+ sb . AppendLine ( ) ;
577+ }
578+ return sb . ToString ( ) ;
579+ }
580+
581+ /// <summary>报表 JSON 校验归一化:结构完整 + 数据行展开标记补全。</summary>
582+ private static object ExtractAndValidateReport ( string content , string ? reportName )
583+ {
584+ var json = ExtractJson ( content ) ;
585+ using var doc = JsonDocument . Parse ( json ) ;
586+ var root = doc . RootElement ;
587+ var layout = new Dictionary < string , object ? > ( ) ;
588+
589+ // page
590+ if ( root . TryGetProperty ( "page" , out var pg ) && pg . ValueKind == JsonValueKind . Object )
591+ layout [ "page" ] = CloneToObject ( pg ) ;
592+ else
593+ layout [ "page" ] = new Dictionary < string , object ? > { [ "size" ] = "A4" , [ "orientation" ] = "portrait" } ;
594+
595+ // params(报表查询参数:name/label/type/options/default)
596+ layout [ "params" ] = ExtractReportParams ( root ) ;
597+ var datasets = new List < object ? > ( ) ;
598+ if ( root . TryGetProperty ( "datasets" , out var dsArr ) && dsArr . ValueKind == JsonValueKind . Array )
599+ datasets . AddRange ( dsArr . EnumerateArray ( ) . Select ( CloneToObject ) ) ;
600+ layout [ "datasets" ] = datasets ;
601+
602+ // rows:数据行绑定数据集时补 expand=auto
603+ var rows = new List < object ? > ( ) ;
604+ if ( root . TryGetProperty ( "rows" , out var rowsArr ) && rowsArr . ValueKind == JsonValueKind . Array )
605+ {
606+ foreach ( var r in rowsArr . EnumerateArray ( ) )
607+ {
608+ if ( r . ValueKind != JsonValueKind . Object ) continue ;
609+ var row = r . EnumerateObject ( ) . ToDictionary ( p => p . Name , p => CloneToObject ( p . Value ) ) ;
610+ var type = row . GetValueOrDefault ( "type" ) ? . ToString ( ) ?? "" ;
611+ if ( type == "data" && ! string . IsNullOrEmpty ( row . GetValueOrDefault ( "dataset" ) ? . ToString ( ) )
612+ && string . IsNullOrEmpty ( row . GetValueOrDefault ( "expand" ) ? . ToString ( ) ) )
613+ row [ "expand" ] = "auto" ;
614+ rows . Add ( row ) ;
615+ }
616+ }
617+ layout [ "rows" ] = rows ;
618+
619+ // cols / cells
620+ layout [ "cols" ] = root . TryGetProperty ( "cols" , out var colsArr ) && colsArr . ValueKind == JsonValueKind . Array
621+ ? colsArr . EnumerateArray ( ) . Select ( CloneToObject ) . ToList ( )
622+ : new List < object ? > ( ) ;
623+ layout [ "cells" ] = root . TryGetProperty ( "cells" , out var cellsArr ) && cellsArr . ValueKind == JsonValueKind . Array
624+ ? cellsArr . EnumerateArray ( ) . Select ( CloneToObject ) . ToList ( )
625+ : new List < object ? > ( ) ;
626+
627+ if ( rows . Count == 0 || ( ( List < object ? > ) layout [ "cols" ] ! ) . Count == 0 )
628+ throw new Exception ( "AI 返回的报表缺少行/列定义,请调整需求重试" ) ;
629+
630+ var layoutJson = JsonSerializer . Serialize ( layout ) ;
631+ return new
632+ {
633+ reportName = reportName ?? "" ,
634+ layoutJson ,
635+ paramsList = layout [ "params" ] ,
636+ datasets = datasets ,
637+ rows = rows . Count ,
638+ cols = ( ( List < object ? > ) layout [ "cols" ] ! ) . Count
639+ } ;
640+ }
641+
453642 // ==================== 自定义助手 ====================
454643
455644 /// <summary>
0 commit comments