新增质量模块(工艺执行书 / 样茧试缫)与报表中心

质量模块:
- 11 张工艺执行书表:封面、试产、选剥茧(含混茧明细)、试缫与大样、煮茧(含分车间配温、副煮车间)、自动缫(含大样)、后缫
- 10 张样茧试缫表:原料小样/综合样主表 + 选茧、解舒、茧幅、切剖、清洁洁净、茧丝百回纤度
- 服务与接口、主控指针回写(列表 √ 标记)、封面编号自增、报表数据包
- 前端 8 张报表打印版式、7 类工艺单据与 6 类调查录入弹窗、字段驱动表单/表格组件
- 菜单种子新增「质量」目录,样茧子表增加 YjssType 判别列,避免原料小样与综合样自增 Id 重号串数据

报表中心:
- 报表模板管理、锐浪 grhtml5 加载与预览、全屏设计面板、数据源 Provider(落丝记录、职工花名册)

其他:同步工作进度 1-11 文档、开发数据库快照、.gitignore 忽略运行日志目录
This commit is contained in:
yanpeng200212
2026-10-22 10:30:37 +08:00
parent 8c4c464362
commit a8890c81e9
92 changed files with 14785 additions and 70 deletions
@@ -0,0 +1,134 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos.QC;
using F9WebAiTest.Application.Services.QC;
using F9WebAiTest.Domain.Entities.QC.Gyzxs;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9WebAiTest.Api.Controllers.QC;
/// <summary>
/// 质量模块 —— 工艺执行书(对应 F9 生产\质量\工艺执行书)。
/// 主列表按「庄口 + 年度」展示,每张子单据一个入口;保存子单据会联动主控表。
/// </summary>
[ApiController]
[Route("api/qc/gyzxs")]
[Authorize]
public class GyzxsController : ControllerBase
{
private readonly IGyzxsService _svc;
public GyzxsController(IGyzxsService svc) => _svc = svc;
// ---------- 主列表 ----------
[HttpGet]
public async Task<IActionResult> GetPaged([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20,
[FromQuery] int year = 0, [FromQuery] string keyword = "")
=> Ok(ApiResult<PageResult<GyzxsDocVersionDto>>.Ok(await _svc.GetDocVersionPagedAsync(pageIndex, pageSize, year, keyword)));
[HttpPost("version")]
public async Task<IActionResult> CreateVersion([FromQuery] long gyzkId)
{
try { return Ok(ApiResult<GyzxsDocVersionDto>.Ok(await _svc.CreateDocVersionAsync(gyzkId), "创建成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpDelete("version/{id}")]
public async Task<IActionResult> DeleteVersion(long id)
{
try { await _svc.DeleteDocVersionAsync(id); return Ok(ApiResult<object>.Ok(null, "删除成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 封面 ----------
[HttpGet("fengmian")]
public async Task<IActionResult> GetFengMian([FromQuery] long gyzkId)
=> Ok(ApiResult<GyzxsFengMian>.Ok(await _svc.GetFengMianAsync(gyzkId)));
[HttpPost("fengmian")]
public async Task<IActionResult> SaveFengMian([FromBody] GyzxsFengMian dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveFengMianAsync(dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 试产(试缫工艺单) ----------
[HttpGet("shichan")]
public async Task<IActionResult> GetShichan([FromQuery] long gyzkId, [FromQuery] string guige = "")
=> Ok(ApiResult<GyzxsShichan>.Ok(await _svc.GetShichanAsync(gyzkId, guige)));
[HttpPost("shichan")]
public async Task<IActionResult> SaveShichan([FromBody] GyzxsShichan dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveShichanAsync(dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 混剥选茧 ----------
[HttpGet("xbj")]
public async Task<IActionResult> GetXbj([FromQuery] long gyzkId)
=> Ok(ApiResult<GyzxsXbjDto>.Ok(await _svc.GetXbjAsync(gyzkId)));
[HttpPost("xbj")]
public async Task<IActionResult> SaveXbj([FromBody] GyzxsXbjDto dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveXbjAsync(dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 试缫与大样 ----------
[HttpGet("ssdy")]
public async Task<IActionResult> GetXbjSsDy([FromQuery] long gyzkId, [FromQuery] string guige = "")
=> Ok(ApiResult<GyzxsXbjSsDy>.Ok(await _svc.GetXbjSsDyAsync(gyzkId, guige)));
[HttpPost("ssdy")]
public async Task<IActionResult> SaveXbjSsDy([FromBody] GyzxsXbjSsDy dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveXbjSsDyAsync(dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 煮茧 ----------
[HttpGet("zhujian")]
public async Task<IActionResult> GetZhuJian([FromQuery] long gyzkId)
=> Ok(ApiResult<GyzxsZhuJianDto>.Ok(await _svc.GetZhuJianAsync(gyzkId)));
[HttpPost("zhujian")]
public async Task<IActionResult> SaveZhuJian([FromBody] GyzxsZhuJianDto dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveZhuJianAsync(dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 自动缫 ----------
[HttpGet("zds")]
public async Task<IActionResult> GetZds([FromQuery] long gyzkId, [FromQuery] string guige = "")
=> Ok(ApiResult<GyzxsZdsDto>.Ok(await _svc.GetZdsAsync(gyzkId, guige)));
[HttpPost("zds")]
public async Task<IActionResult> SaveZds([FromBody] GyzxsZdsDto dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveZdsAsync(dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 后缫(工艺执行书) ----------
[HttpGet("hs")]
public async Task<IActionResult> GetHs([FromQuery] long gyzkId, [FromQuery] string guige = "")
=> Ok(ApiResult<GyzxsHs>.Ok(await _svc.GetHsAsync(gyzkId, guige)));
[HttpPost("hs")]
public async Task<IActionResult> SaveHs([FromBody] GyzxsHs dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveHsAsync(dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 报表数据 ----------
/// <summary>报表数据(type: fengmian / gyzxs / xbj / shichan / ssdy / zhujian / zds / zdsdy)</summary>
[HttpGet("print/{type}")]
public async Task<IActionResult> GetPrintData(string type, [FromQuery] long gyzkId, [FromQuery] string guige = "")
{
try { return Ok(ApiResult<GyzxsPrintDto>.Ok(await _svc.GetPrintDataAsync(type, gyzkId, guige))); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
}
@@ -0,0 +1,139 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos.QC;
using F9WebAiTest.Application.Services.QC;
using F9WebAiTest.Domain.Entities.QC.Yjss;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9WebAiTest.Api.Controllers.QC;
/// <summary>
/// 质量模块 —— 样茧试缫(对应 F9 生产\质量\样茧试缫)。
/// type: 1=原料小样 2=综合样。
/// </summary>
[ApiController]
[Route("api/qc/yjss")]
[Authorize]
public class YjssController : ControllerBase
{
private readonly IYjssService _svc;
public YjssController(IYjssService svc) => _svc = svc;
// ---------- 主列表 / 主表 ----------
[HttpGet]
public async Task<IActionResult> GetPaged([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20,
[FromQuery] int type = 1, [FromQuery] int year = 0, [FromQuery] string keyword = "")
=> Ok(ApiResult<PageResult<YjssListItemDto>>.Ok(await _svc.GetPagedAsync(pageIndex, pageSize, type, year, keyword)));
[HttpPost("ylxy")]
public async Task<IActionResult> SaveYlxy([FromBody] YjssMainYlxy dto)
{
try { return Ok(ApiResult<YjssListItemDto>.Ok(await _svc.SaveYlxyAsync(dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpPost("zhy")]
public async Task<IActionResult> SaveZhy([FromBody] YjssMainZhy dto)
{
try { return Ok(ApiResult<YjssListItemDto>.Ok(await _svc.SaveZhyAsync(dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpDelete("{type}/{id}")]
public async Task<IActionResult> Delete(int type, long id)
{
try { await _svc.DeleteAsync(type, id); return Ok(ApiResult<object>.Ok(null, "删除成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 选茧调查 ----------
[HttpGet("xjdc/{yjssId}")]
public async Task<IActionResult> GetXjdc(long yjssId, [FromQuery] int type = 1)
=> Ok(ApiResult<YjssDcXjdc>.Ok(await _svc.GetXjdcAsync(type, yjssId)));
[HttpPost("xjdc")]
public async Task<IActionResult> SaveXjdc([FromQuery] int type, [FromBody] YjssDcXjdc dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveXjdcAsync(type, dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 解舒调查 ----------
[HttpGet("jsdc/{yjssId}")]
public async Task<IActionResult> GetJsdc(long yjssId, [FromQuery] int type = 1)
=> Ok(ApiResult<List<YjssDcJsdc>>.Ok(await _svc.GetJsdcAsync(type, yjssId)));
[HttpPost("jsdc")]
public async Task<IActionResult> SaveJsdc([FromQuery] int type, [FromBody] YjssDcJsdc dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveJsdcAsync(type, dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpDelete("jsdc/{type}/{id}")]
public async Task<IActionResult> DeleteJsdc(int type, long id)
{
try { await _svc.DeleteJsdcAsync(type, id); return Ok(ApiResult<object>.Ok(null, "删除成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 茧幅调查 ----------
[HttpGet("jfdc/{yjssId}")]
public async Task<IActionResult> GetJfdc(long yjssId, [FromQuery] int type = 1)
=> Ok(ApiResult<YjssJfdcDto>.Ok(await _svc.GetJfdcAsync(type, yjssId)));
[HttpPost("jfdc")]
public async Task<IActionResult> SaveJfdc([FromQuery] int type, [FromBody] YjssJfdcDto dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveJfdcAsync(type, dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 切剖调查 ----------
[HttpGet("qpdc/{yjssId}")]
public async Task<IActionResult> GetQpdc(long yjssId, [FromQuery] int type = 1)
=> Ok(ApiResult<YjssDcQpdc>.Ok(await _svc.GetQpdcAsync(type, yjssId)));
[HttpPost("qpdc")]
public async Task<IActionResult> SaveQpdc([FromQuery] int type, [FromBody] YjssDcQpdc dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveQpdcAsync(type, dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 清洁洁净 ----------
[HttpGet("qjjj/{yjssId}")]
public async Task<IActionResult> GetQjjj(long yjssId, [FromQuery] int type = 1)
=> Ok(ApiResult<YjssDcQjjj>.Ok(await _svc.GetQjjjAsync(type, yjssId)));
[HttpPost("qjjj")]
public async Task<IActionResult> SaveQjjj([FromQuery] int type, [FromBody] YjssDcQjjj dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveQjjjAsync(type, dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 茧丝百回纤度 ----------
[HttpGet("jsbhxd/{yjssId}")]
public async Task<IActionResult> GetJsbhxd(long yjssId, [FromQuery] int type = 1)
=> Ok(ApiResult<YjssJsbhxdDto>.Ok(await _svc.GetJsbhxdAsync(type, yjssId)));
[HttpPost("jsbhxd")]
public async Task<IActionResult> SaveJsbhxd([FromQuery] int type, [FromBody] YjssJsbhxdDto dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveJsbhxdAsync(type, dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 整页 / 报表 ----------
[HttpGet("full/{type}/{id}")]
public async Task<IActionResult> GetFull(int type, long id)
{
try { return Ok(ApiResult<YjssFullDto>.Ok(await _svc.GetFullAsync(type, id))); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpGet("jsdc-summary/{yjssId}")]
public async Task<IActionResult> GetJsdcSummary(long yjssId, [FromQuery] int type = 1)
=> Ok(ApiResult<Dictionary<string, object?>>.Ok(await _svc.GetJsdcSummaryAsync(type, yjssId)));
}
@@ -0,0 +1,351 @@
using System.Security.Claims;
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Application.Dtos.SYS.Reports;
using F9WebAiTest.Application.Services.Auth;
using F9WebAiTest.Application.Services.Reports;
using F9WebAiTest.Domain.Entities.SYS.RBAC;
using F9WebAiTest.Domain.Enums.SYS.RBAC;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9WebAiTest.Api.Controllers;
/// <summary>报表中心:报表定义维护 + 模板下载 + 数据查询(Grid++Report 7 Web 嵌入)</summary>
[ApiController]
[Route("api/sys/report")]
[Authorize]
public class SysReportController : ControllerBase
{
private readonly IReportService _reportService;
private readonly IFreeSql _fsql;
private readonly IJwtService _jwt;
private readonly IConfiguration _config;
public SysReportController(IReportService reportService, IFreeSql fsql, IJwtService jwt, IConfiguration config)
{
_reportService = reportService;
_fsql = fsql;
_jwt = jwt;
_config = config;
}
// ==================== 报表管理 ====================
[HttpGet]
public async Task<IActionResult> GetPaged(
[FromQuery] string like = "",
[FromQuery] string category = "",
[FromQuery] bool? isActive = null,
[FromQuery] int pageIndex = 1,
[FromQuery] int pageSize = 50)
{
var data = await _reportService.GetPagedAsync(like ?? "", category ?? "", isActive, pageIndex, pageSize);
return Ok(ApiResult<object>.Ok(data));
}
[HttpGet("all")]
public async Task<IActionResult> GetAllActive()
=> Ok(ApiResult<object>.Ok(await _reportService.GetAllActiveAsync()));
[HttpGet("{id}")]
public async Task<IActionResult> GetById(long id)
{
try { return Ok(ApiResult<object>.Ok(await _reportService.GetByIdAsync(id))); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
}
[HttpPost]
public async Task<IActionResult> Create([FromBody] ReportCreateDto dto)
{
try { return Ok(ApiResult<object>.Ok(new { id = await _reportService.CreateAsync(dto) }, "新增成功")); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(long id, [FromBody] ReportUpdateDto dto)
{
try { await _reportService.UpdateAsync(id, dto); return Ok(ApiResult<object>.Ok(new { id }, "修改成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
try { await _reportService.DeleteAsync(id); return Ok(ApiResult<object>.Ok(new { id }, "删除成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
}
/// <summary>上传 .grf 模板文件。
/// 常规编辑流程已改为「模板随报表一起保存」(POST/PUT 携带 templateJson),
/// 本接口保留给 f9designer:// 本机设计器协议回传使用。</summary>
[HttpPost("{id}/template")]
[RequestSizeLimit(50 * 1024 * 1024)]
public async Task<IActionResult> UploadTemplate(long id, IFormFile file)
{
if (file == null || file.Length == 0) return BadRequest(ApiResult<object>.Fail("请选择要上传的 .grf 模板文件", 400));
if (!file.FileName.EndsWith(".grf", StringComparison.OrdinalIgnoreCase))
return BadRequest(ApiResult<object>.Fail("仅支持上传 .grf 模板文件", 400));
try
{
using var ms = new MemoryStream();
await file.CopyToAsync(ms);
await _reportService.UploadTemplateAsync(id, ms.ToArray(), file.FileName);
return Ok(ApiResult<object>.Ok(new { size = file.Length }, "上传成功"));
}
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
/// <summary>
/// 直接保存模板文本(前端在线编辑「模板源码」后保存,免下载 / 上传 .grf)。
/// 与上传接口的区别:这里提交的是文本(JSON 或 "_WR" 压缩文本),不是文件流。
/// </summary>
[HttpPut("{id}/template/content")]
[RequestSizeLimit(50 * 1024 * 1024)]
public async Task<IActionResult> SaveTemplateContent(long id, [FromBody] ReportTemplateContentDto dto)
{
try
{
var size = await _reportService.SaveTemplateContentAsync(id, dto?.Content ?? "");
return Ok(ApiResult<object>.Ok(new { id, size, hasTemplate = true }, "模板已保存"));
}
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
/// <summary>下载 .grf 模板文件。保留给 f9designer:// 本机设计器协议(下载 → 编辑 → 自动回传)使用。</summary>
[HttpGet("{id}/template/download")]
public async Task<IActionResult> DownloadTemplate(long id)
{
var r = await _fsql.Select<Domain.Entities.SYS.Reports.Report>().Where(x => x.Id == id && !x.IsDeleted).FirstAsync();
var content = r == null ? null : await _reportService.GetTemplateContentAsync(r);
if (string.IsNullOrEmpty(content))
return NotFound(ApiResult<object>.Fail("模板尚未上传", 404));
return File(ToGrfBytes(content), "application/octet-stream", $"{r!.Code}.grf");
}
// ==================== 新增报表辅助:数据源 + 结构探测 ====================
/// <summary>数据源清单(IReportDataProvider 注册表 + 各数据源声明的查询参数)</summary>
[HttpGet("datasources")]
public async Task<IActionResult> GetDataSources()
=> Ok(ApiResult<object>.Ok(await _reportService.GetDataSourcesAsync()));
/// <summary>
/// 探测数据源结构:按 DsName(或 DataSql)执行一次查询,
/// 返回可自动配置的查询参数 + 明细字段(列)+ 少量样本行。
/// </summary>
[HttpPost("scaffold")]
public async Task<IActionResult> Scaffold([FromBody] ReportProbeRequestDto dto)
{
var result = await _reportService.ProbeAsync(dto ?? new ReportProbeRequestDto());
return Ok(ApiResult<object>.Ok(result));
}
// ==================== 模板与数据公开端点(供 HTML5 渲染 / 客户端调用) ====================
// grhtml5.insertReportViewer 的模板参数是普通 GET,浏览器无法附加 Authorization header,
// 故模板/数据接口接受 ?access_token=xxx 方式鉴权(仅内部网络使用,与 /hubs 相同的处理思路)。
/// <summary>下载报表 .grf 模板(带权限校验,支持 query token 鉴权)。
/// 额外暴露 template.json / template.grf 别名:Grid++ 的 JS 库与客户端对 URL 中不含
/// ".grf/.txt/.xml/.json" 的请求会用 POST(取模板会误入上传接口)。</summary>
[HttpGet("{id}/template")]
[HttpGet("{id}/template.json")]
[HttpGet("{id}/template.grf")]
[AllowAnonymous]
public async Task<IActionResult> GetTemplate(long id, [FromQuery(Name = "access_token")] string? accessToken = null)
{
var (userId, isAdmin) = await ResolveCallerAsync(accessToken);
if (userId <= 0) return Unauthorized(new { code = 401, message = "未授权" });
var r = await _fsql.Select<Domain.Entities.SYS.Reports.Report>().Where(x => x.Id == id && !x.IsDeleted).FirstAsync();
var content = r == null ? null : await _reportService.GetTemplateContentAsync(r);
if (string.IsNullOrEmpty(content))
return NotFound(new { code = 404, message = "模板尚未上传" });
if (!await CanAccessReportAsync(userId, isAdmin, r!)) return ForbidReport();
// 不输出 BOM:HTML5 渲染库按 { 或 _WR 前缀判断模板格式,BOM 会破坏该判断
return File(System.Text.Encoding.UTF8.GetBytes(content!), "application/json; charset=utf-8", $"{r!.Code}.grf");
}
/// <summary>执行报表数据查询(GET/POST 均可)。data.json 为 GET 别名(客户端 URL 方法判定约定)</summary>
[HttpGet("{id}/data")]
[HttpGet("{id}/data.json")]
[AllowAnonymous]
public async Task<IActionResult> GetData(long id, [FromQuery] string? access_token = null, [FromQuery] string? params_ = null)
=> await RunDataAsync(id, access_token, params_);
[HttpPost("{id}/data")]
[AllowAnonymous]
public async Task<IActionResult> PostData(long id, [FromQuery(Name = "access_token")] string? accessToken = null)
{
string? paramsJson = null;
try
{
Request.EnableBuffering();
Request.Body.Position = 0;
using var reader = new StreamReader(Request.Body, leaveOpen: true);
var body = await reader.ReadToEndAsync();
if (!string.IsNullOrEmpty(body)) paramsJson = body;
}
catch { /* 忽略读取错误 */ }
return await RunDataAsync(id, accessToken, paramsJson);
}
private async Task<IActionResult> RunDataAsync(long id, string? accessToken, string? paramsJson)
{
var (userId, isAdmin) = await ResolveCallerAsync(accessToken);
if (userId <= 0) return Unauthorized(new { code = 401, message = "未授权" });
var r = await _fsql.Select<Domain.Entities.SYS.Reports.Report>().Where(x => x.Id == id && !x.IsDeleted).FirstAsync();
if (r == null) return NotFound(new { code = 404, message = "报表不存在" });
if (!await CanAccessReportAsync(userId, isAdmin, r)) return ForbidReport();
var p = ParseParams(paramsJson);
try
{
var data = await _reportService.ExecuteDataAsync(r, p);
return Ok(data);
}
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
private static System.Collections.Generic.Dictionary<string, object?> ParseParams(string? json)
{
var result = new System.Collections.Generic.Dictionary<string, object?>(StringComparer.Ordinal);
if (string.IsNullOrWhiteSpace(json)) return result;
try
{
using var doc = System.Text.Json.JsonDocument.Parse(json);
if (doc.RootElement.ValueKind == System.Text.Json.JsonValueKind.Object)
{
foreach (var prop in doc.RootElement.EnumerateObject())
result[prop.Name] = System.Text.Json.JsonSerializer.Deserialize<object?>(prop.Value.GetRawText());
}
else if (doc.RootElement.ValueKind == System.Text.Json.JsonValueKind.Array)
{
// 支持 [{name,value}] 形式(与 Grid++ WEB 客户端参数一致)
foreach (var el in doc.RootElement.EnumerateArray())
{
if (el.TryGetProperty("name", out var n) && el.TryGetProperty("value", out var v))
result[n.GetString() ?? ""] = System.Text.Json.JsonSerializer.Deserialize<object?>(v.GetRawText());
}
}
}
catch { /* 忽略解析错误,返回空 */ }
return result;
}
// ==================== 报表中心(用户视图) ====================
/// <summary>当前用户可见的报表(按权限过滤)</summary>
[HttpGet("visible")]
public async Task<IActionResult> GetVisible()
{
var userId = GetUserId();
var isAdmin = await IsAdminAsync(userId);
var perms = await GetPermissionsAsync(userId, isAdmin);
return Ok(ApiResult<object>.Ok(await _reportService.GetVisibleReportsAsync(userId, isAdmin, perms)));
}
/// <summary>当前用户预览某报表(带数据)</summary>
[HttpGet("{id}/preview")]
public async Task<IActionResult> Preview(long id, [FromQuery] string? params_ = null)
{
var userId = GetUserId();
var isAdmin = await IsAdminAsync(userId);
var perms = await GetPermissionsAsync(userId, isAdmin);
try
{
await _reportService.EnsureReportAccessAsync(userId, isAdmin, perms, id);
var r = await _fsql.Select<Domain.Entities.SYS.Reports.Report>().Where(x => x.Id == id && !x.IsDeleted).FirstAsync();
if (r == null) return NotFound(ApiResult<object>.Fail("报表不存在", 404));
if (r.ReportFileId == 0)
return BadRequest(ApiResult<object>.Fail("该报表尚未配置模板", 400));
var p = ParseParams(params_);
var data = await _reportService.ExecuteDataAsync(r, p);
return Ok(ApiResult<object>.Ok(new
{
report = new
{
r.Id,
r.Code,
r.Name,
r.Category,
r.Description,
config = r.Config,
hasTemplate = true
},
templateUrl = $"/api/sys/report/{r.Id}/template.json",
data
}));
}
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
// ==================== 内部辅助 ====================
private long GetUserId() => long.Parse(User.FindFirst("id")!.Value);
private async Task<bool> IsAdminAsync(long userId)
{
var roleIds = await _fsql.Select<UserRole>().Where(ur => ur.UserId == userId).ToListAsync(x => x.RoleId);
if (roleIds.Count == 0) return false;
return await _fsql.Select<Role>().Where(r => roleIds.Contains(r.Id) && r.Code == "admin").AnyAsync();
}
private async Task<System.Collections.Generic.HashSet<string>> GetPermissionsAsync(long userId, bool isAdmin)
{
if (isAdmin)
return new System.Collections.Generic.HashSet<string>(
await _fsql.Select<Permission>().Where(p => !p.IsDeleted && !string.IsNullOrEmpty(p.Code)).ToListAsync(p => p.Code));
var roleIds = await _fsql.Select<UserRole>().Where(ur => ur.UserId == userId).ToListAsync(x => x.RoleId);
if (roleIds.Count == 0) return new();
var permIds = await _fsql.Select<RolePermission>().Where(rp => roleIds.Contains(rp.RoleId) && !rp.IsDeleted).ToListAsync(x => x.PermissionId);
if (permIds.Count == 0) return new();
var codes = await _fsql.Select<Permission>().Where(p => permIds.Contains(p.Id) && !p.IsDeleted && !string.IsNullOrEmpty(p.Code)).ToListAsync(p => p.Code);
return new System.Collections.Generic.HashSet<string>(codes);
}
/// <summary>用 query 中的 access_token 解析用户;优先尝试从 Authorization 头解析;解析失败返回 0。</summary>
private async Task<(long UserId, bool IsAdmin)> ResolveCallerAsync(string? accessToken)
{
// 优先用当前 HTTP 请求上下文(JWT 中间件已解析)
if (User?.Identity?.IsAuthenticated == true)
return (GetUserId(), await IsAdminAsync(GetUserId()));
if (string.IsNullOrEmpty(accessToken)) return (0, false);
try
{
var (uid, _) = _jwt.ValidateAccessToken(accessToken);
return (uid, await IsAdminAsync(uid));
}
catch { return (0, false); }
}
private async Task<bool> CanAccessReportAsync(long userId, bool isAdmin, Domain.Entities.SYS.Reports.Report r)
{
if (isAdmin) return true;
if (!r.IsActive) return false;
var perms = await GetPermissionsAsync(userId, isAdmin);
return perms.Contains($"report:{r.Code}:visble");
}
private IActionResult ForbidReport() => StatusCode(403, new { code = 403, message = "无权访问该报表" });
/// <summary>模板文本 → .grf 字节(带 UTF-8 BOM,与锐浪设计器保存格式一致)</summary>
private static byte[] ToGrfBytes(string content)
{
var text = content.Length > 0 && content[0] == '\uFEFF' ? content.Substring(1) : content;
var enc = new System.Text.UTF8Encoding(true);
var pre = enc.GetPreamble();
var body = enc.GetBytes(text);
var data = new byte[pre.Length + body.Length];
Buffer.BlockCopy(pre, 0, data, 0, pre.Length);
Buffer.BlockCopy(body, 0, data, pre.Length, body.Length);
return data;
}
}