新增质量模块(工艺执行书 / 样茧试缫)与报表中心
质量模块: - 11 张工艺执行书表:封面、试产、选剥茧(含混茧明细)、试缫与大样、煮茧(含分车间配温、副煮车间)、自动缫(含大样)、后缫 - 10 张样茧试缫表:原料小样/综合样主表 + 选茧、解舒、茧幅、切剖、清洁洁净、茧丝百回纤度 - 服务与接口、主控指针回写(列表 √ 标记)、封面编号自增、报表数据包 - 前端 8 张报表打印版式、7 类工艺单据与 6 类调查录入弹窗、字段驱动表单/表格组件 - 菜单种子新增「质量」目录,样茧子表增加 YjssType 判别列,避免原料小样与综合样自增 Id 重号串数据 报表中心: - 报表模板管理、锐浪 grhtml5 加载与预览、全屏设计面板、数据源 Provider(落丝记录、职工花名册) 其他:同步工作进度 1-11 文档、开发数据库快照、.gitignore 忽略运行日志目录
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -33,13 +33,16 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
ValidAudience = builder.Configuration["Jwt:Audience"],
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
|
||||
};
|
||||
// SignalR 通过 query string 传递 token,需从 access_token 读取
|
||||
// SignalR 与报表模板/数据下载通过 query string 传递 token
|
||||
options.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
var accessToken = context.Request.Query["access_token"];
|
||||
if (!string.IsNullOrEmpty(accessToken) && context.HttpContext.Request.Path.StartsWithSegments("/hubs"))
|
||||
if (string.IsNullOrEmpty(accessToken)) return Task.CompletedTask;
|
||||
var path = context.HttpContext.Request.Path;
|
||||
if (path.StartsWithSegments("/hubs")
|
||||
|| path.StartsWithSegments("/api/sys/report"))
|
||||
{
|
||||
context.Token = accessToken;
|
||||
}
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user