bag
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using YhCore.Common;
|
||||
using YhCore.Common.UserContext;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Vo;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.WebApi.Controllers.Api
|
||||
{
|
||||
/// <summary>
|
||||
/// 日志查询接口(对应 Java 版 controller/api/LogController,路由 /api/log/)。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route(ApiRoutes.ApiPath + "/log")]
|
||||
public class LogController : ControllerBase
|
||||
{
|
||||
private readonly ILogService _logService;
|
||||
|
||||
public LogController(ILogService logService)
|
||||
{
|
||||
_logService = logService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 系统操作日志搜索(对应 Java GET /api/log/systemSearch)。
|
||||
/// </summary>
|
||||
/// <param name="keyword">关键字(可空)</param>
|
||||
/// <param name="page">页码,从 0 开始(对应 Spring Pageable.page)</param>
|
||||
/// <param name="size">每页大小(对应 Spring Pageable.size)</param>
|
||||
[HttpGet("systemSearch")]
|
||||
public PageVO<SysOperationLog> SystemSearch(
|
||||
[FromQuery] string keyword,
|
||||
[FromQuery] int page = 0,
|
||||
[FromQuery] int size = 20)
|
||||
{
|
||||
return _logService.Search(UserContextHolder.GetSchId(), keyword, true, page, size);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 操作日志搜索(对应 Java GET /api/log/search)。
|
||||
/// </summary>
|
||||
[HttpGet("search")]
|
||||
public PageVO<SysOperationLog> Search(
|
||||
[FromQuery] string keyword,
|
||||
[FromQuery] int page = 0,
|
||||
[FromQuery] int size = 20)
|
||||
{
|
||||
return _logService.Search(UserContextHolder.GetSchId(), keyword, false, page, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Yh.SchClass.OpenApi;
|
||||
using Yh.SchClass.OpenApi.Model;
|
||||
using YhCore.Common;
|
||||
|
||||
namespace YhCore.WebApi.Controllers.Api
|
||||
{
|
||||
/// <summary>
|
||||
/// 教务(SchClass)开放接口演示控制器:引用 Yh.SchClass.OpenApi.dll(Library),
|
||||
/// 对应 Java 教务 bschclassmanage 的 /oapi 端点(配置节 yh:schclass:open)。
|
||||
/// <para>SDK 门面 <see cref="SchClassOpenApi"/> 聚合 10 个客户端,本控制器演示其中 5 个。</para>
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route(ApiRoutes.ApiPath + "/com/schClass")]
|
||||
public class SchClassController : ControllerBase
|
||||
{
|
||||
private readonly SchClassOpenApi _sch;
|
||||
|
||||
public SchClassController(SchClassOpenApi sch)
|
||||
{
|
||||
_sch = sch ?? throw new ArgumentNullException(nameof(sch));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 全部学校列表(GET /oapi/SchInfo/AllList)。
|
||||
/// </summary>
|
||||
[HttpGet("schools")]
|
||||
public async Task<List<SchInfoVo>> Schools()
|
||||
{
|
||||
return await _sch.SchInfo.AllListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按学校外部序列号查询学校(GET /oapi/SchInfo/GetSchFirst)。
|
||||
/// </summary>
|
||||
/// <param name="schId">学校外部序列号</param>
|
||||
[HttpGet("school")]
|
||||
public async Task<SchInfoVo> School([FromQuery] string schId)
|
||||
{
|
||||
return await _sch.SchInfo.GetSchFirstAsync(schId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按学校查询教师列表(GET /oapi/OATea/GetSchTeainfo)。
|
||||
/// </summary>
|
||||
/// <param name="schSerId">学校外部序列号</param>
|
||||
/// <param name="schId">学校内部 ID(优先使用)</param>
|
||||
[HttpGet("teachers")]
|
||||
public async Task<List<TeaInfoVo>> Teachers(
|
||||
[FromQuery] string schSerId,
|
||||
[FromQuery] int? schId)
|
||||
{
|
||||
return await _sch.TeaInfo.GetSchTeaInfoAsync(schSerId, schId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按学校查询班级列表(GET /oapi/VSchClass/GetVClassList)。
|
||||
/// </summary>
|
||||
/// <param name="schSerId">学校外部序列号</param>
|
||||
[HttpGet("classes")]
|
||||
public async Task<List<Dictionary<string, object>>> Classes([FromQuery] string schSerId)
|
||||
{
|
||||
var rows = await _sch.VClass.GetVClassListAsync(schSerId);
|
||||
return rows.Select(ToPlainRow).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按学校查询学生列表(GET /oapi/OAStu/GetSchStuList)。
|
||||
/// </summary>
|
||||
/// <param name="schserid">学校外部序列号</param>
|
||||
/// <param name="schgroupid">学校分组 ID(可空)</param>
|
||||
[HttpGet("students")]
|
||||
public async Task<List<Dictionary<string, object>>> Students(
|
||||
[FromQuery] string schserid,
|
||||
[FromQuery] string schgroupid)
|
||||
{
|
||||
var rows = await _sch.StuInfo.GetSchStuListAsync(schserid, schgroupid);
|
||||
return rows.Select(ToPlainRow).ToList();
|
||||
}
|
||||
|
||||
// ===================== JsonElement → 普通对象 =====================
|
||||
// SDK 行类型 SchClassRow = Dictionary<string, JsonElement>(Java Map<String,Object> 兼容);
|
||||
// 直接返回会被 Newtonsoft 序列化成 JsonElement 内部结构,这里转成原生对象。
|
||||
|
||||
private static Dictionary<string, object> ToPlainRow(Dictionary<string, JsonElement> row)
|
||||
=> row.ToDictionary(kv => kv.Key, kv => ToPlain(kv.Value));
|
||||
|
||||
private static object ToPlain(JsonElement el)
|
||||
{
|
||||
switch (el.ValueKind)
|
||||
{
|
||||
case JsonValueKind.String:
|
||||
return el.GetString();
|
||||
case JsonValueKind.Number:
|
||||
return el.TryGetInt32(out var i) ? (object)i : el.GetDouble();
|
||||
case JsonValueKind.True:
|
||||
return true;
|
||||
case JsonValueKind.False:
|
||||
return false;
|
||||
case JsonValueKind.Null:
|
||||
return null;
|
||||
case JsonValueKind.Array:
|
||||
return el.EnumerateArray().Select(ToPlain).ToList();
|
||||
case JsonValueKind.Object:
|
||||
return el.EnumerateObject().ToDictionary(p => p.Name, p => ToPlain(p.Value));
|
||||
default:
|
||||
return el.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Yh.Platform.Abstractions;
|
||||
using Yh.Platform.Auth;
|
||||
using Yh.Platform.OpenApi;
|
||||
using YhCore.Common;
|
||||
|
||||
namespace YhCore.WebApi.Controllers.Api
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户基础接口(对应 Java 版 controller/api/UserController,路由 /api/base/)。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route(ApiRoutes.ApiPath + "/base")]
|
||||
public class UserController : ControllerBase
|
||||
{
|
||||
private readonly IOpenApiUserClient _openApiUserClient;
|
||||
|
||||
public UserController(IOpenApiUserClient openApiUserClient)
|
||||
{
|
||||
_openApiUserClient = openApiUserClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户可见的应用模块(对应 Java GET /api/base/getUserMod,
|
||||
/// 内部调用 YschBaseOpenApi.getUserModule)。
|
||||
/// </summary>
|
||||
[HttpGet("getUserMod")]
|
||||
public async Task<List<BoaModule>> GetUserMod()
|
||||
{
|
||||
var boa = BoaUserContext.Get();
|
||||
return await _openApiUserClient.GetUserModuleAsync(boa?.Id ?? 0, boa?.School?.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前登录用户信息(对应 Java GET /api/base/getUserInfo,
|
||||
/// Java 版直接返回认证切面注入的 authUserWithDept)。
|
||||
/// </summary>
|
||||
[HttpGet("getUserInfo")]
|
||||
public BoaUser GetUserInfo()
|
||||
{
|
||||
return BoaUserContext.Get();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 登出(对应 Java GET /api/base/logout:session.invalidate())。
|
||||
/// </summary>
|
||||
[HttpGet("logout")]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
// 清除本地 Cookie 登录态
|
||||
await HttpContext.SignOutAsync(YhAuthDefaults.CookieScheme);
|
||||
|
||||
if (HttpContext.Features.Get<ISessionFeature>() != null)
|
||||
{
|
||||
HttpContext.Session.Clear();
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Yh.Platform.Abstractions;
|
||||
using YhCore.Common;
|
||||
using YhCore.Common.Config;
|
||||
using YhCore.Common.UserContext;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.WebApi.Controllers.Api.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// 公共字典 / 文件(对应旧 Areas/Common/CommonController,路由 api/base)。
|
||||
/// 字典部分 8 接口 + 文件部分 4 接口(FileDown / FilePreview / FileUpload / FileUploadImage)已落地;
|
||||
/// 旧 [NoAuth] 语义:无需用户上下文,schId 由前端传入或取当前用户。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route(ApiRoutes.ApiPath + "/base")]
|
||||
public class CommonController : ControllerBase
|
||||
{
|
||||
private readonly IDictionaryService _dict;
|
||||
private readonly IFileService _file;
|
||||
|
||||
public CommonController(IDictionaryService dict, IFileService file)
|
||||
{
|
||||
_dict = dict;
|
||||
_file = file;
|
||||
}
|
||||
|
||||
/// <summary>获取指定字典数据(Type 1~10)。</summary>
|
||||
[HttpGet("GetSysDic")]
|
||||
public async Task<List<DicOption>> GetSysDic([FromQuery] int type = 0)
|
||||
{
|
||||
var typeId = DictionaryTypeHelper.FromSysDicType(type);
|
||||
if (typeId == null)
|
||||
{
|
||||
throw new DException("参数错误");
|
||||
}
|
||||
|
||||
return await _dict.GetDictListAsync(typeId, SchSerID());
|
||||
}
|
||||
|
||||
/// <summary>字典类别列表([NoAuth],排除部门)。</summary>
|
||||
[HttpGet("GetDicTypeList")]
|
||||
public async Task<List<DicOption>> GetDicTypeList() => await _dict.GetDicTypeListAsync();
|
||||
|
||||
/// <summary>字典列表管理(Type=0/DEPT 取全校;其他仅超管 bestsch)。</summary>
|
||||
[HttpGet("GetDictMangeList")]
|
||||
public async Task<List<DicManageItem>> GetDictMangeList([FromQuery] string type)
|
||||
{
|
||||
var typeId = DictionaryTypeHelper.ResolveTypeId(type);
|
||||
if (typeId == null)
|
||||
{
|
||||
throw new DException("参数错误");
|
||||
}
|
||||
|
||||
if (DictionaryTypeHelper.IsDept(typeId))
|
||||
{
|
||||
return await _dict.GetDictMangeListAsync("DEPT", SchSerID());
|
||||
}
|
||||
|
||||
//if (UserContextHolder.Current.() =1 )
|
||||
//{
|
||||
// throw new DException("非超管不得操作!");
|
||||
//}
|
||||
|
||||
return await _dict.GetDictMangeListAsync(typeId, "");
|
||||
}
|
||||
|
||||
/// <summary>添加字典。</summary>
|
||||
[HttpPost("AddDic")]
|
||||
public async Task AddDic([FromBody] Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
|
||||
var info = new Common_Dictionary
|
||||
{
|
||||
typeID = model.typeID,
|
||||
typeName = DictionaryTypeHelper.GetTypeName(model.typeID),
|
||||
Name = model.Name,
|
||||
Schserid = SchSerID()
|
||||
};
|
||||
|
||||
await _dict.AddDicAsync(info);
|
||||
}
|
||||
|
||||
/// <summary>更新字典。</summary>
|
||||
[HttpPost("UpdateDic")]
|
||||
public async Task UpdateDic([FromBody] Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数有误!");
|
||||
model.Schserid = SchSerID();
|
||||
await _dict.UpdateDicAsync(model);
|
||||
}
|
||||
|
||||
/// <summary>编辑字典名称。</summary>
|
||||
[HttpPost("EditDic")]
|
||||
public async Task EditDic([FromBody] Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数有误!");
|
||||
model.Schserid = SchSerID();
|
||||
await _dict.EditDicAsync(model);
|
||||
}
|
||||
|
||||
/// <summary>添加发文部门。</summary>
|
||||
[HttpPost("AddDeptDic")]
|
||||
public async Task AddDeptDic([FromBody] Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数有误!");
|
||||
|
||||
var info = new Common_Dictionary
|
||||
{
|
||||
typeID = "DEPT",
|
||||
typeName = "部门类别",
|
||||
Name = model.Name,
|
||||
Schserid = SchSerID()
|
||||
};
|
||||
|
||||
await _dict.AddDicAsync(info);
|
||||
}
|
||||
|
||||
/// <summary>更新发文部门。</summary>
|
||||
[HttpPost("UpdateDeptDic")]
|
||||
public async Task UpdateDeptDic([FromBody] Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数有误!");
|
||||
model.Schserid = SchSerID();
|
||||
await _dict.UpdateDicAsync(model);
|
||||
}
|
||||
|
||||
#region 文件:下载 / 预览 / 上传
|
||||
|
||||
/// <summary>
|
||||
/// 公用下载文件(旧 FileDown,[NoAuth])。yl=true 且为图片/PDF 时重定向到文件 URL(webapp 预览),
|
||||
/// 否则物理文件下载。
|
||||
/// </summary>
|
||||
[HttpGet("FileDown")]
|
||||
public async Task<IActionResult> FileDown([FromQuery] int id, [FromQuery] bool yl = false)
|
||||
{
|
||||
if (id == 0)
|
||||
return Ok();
|
||||
|
||||
var file = await _file.GetEnclosureAsync(id);
|
||||
|
||||
string[] previewArry = { ".jpg", ".gif", ".png", ".pdf", ".jpeg", ".bmp" };
|
||||
if (yl)
|
||||
{
|
||||
// 旧 sch_weboa_url.Replace("notice/api", AppType + "/") + fileurl → 新 FileWebPath + fileurl
|
||||
string fileUrl = (Config.FileWebPath ?? "") + file.fileurl;
|
||||
string suffName = Path.GetExtension(fileUrl);
|
||||
if (previewArry.Any(x => x == suffName.ToLower()))
|
||||
return Redirect(fileUrl);
|
||||
}
|
||||
|
||||
// WebHelper.DownFile:物理文件下载
|
||||
string filePath = _file.MapPhysicalPath(file.fileurl);
|
||||
if (!System.IO.File.Exists(filePath))
|
||||
return Content("请求的文件不存在!");
|
||||
|
||||
string contentType = GetContentType(filePath);
|
||||
string outputFileName = file.filename ?? "file";
|
||||
return PhysicalFile(filePath, contentType, outputFileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 公用预览 office 文件(旧 FilePreview / CommonPreview,[NoAuth])。
|
||||
/// type=1 → 返回可用预览 url(本地 .pdf 存在优先);type=2 → 重定向本地 pdf 或 officeapps.live.com;
|
||||
/// 其余 → 重定向替换后缀为 .pdf 的地址。跳转返回 RedirectResult(全局包装过滤器不拦截)。
|
||||
/// </summary>
|
||||
[HttpGet("FilePreview")]
|
||||
public async Task<object> FilePreview([FromQuery] int id, [FromQuery] string type = null)
|
||||
{
|
||||
if (id == 0)
|
||||
throw new DException("文件ID错误!");
|
||||
|
||||
var file = await _file.GetEnclosureAsync(id);
|
||||
|
||||
// 公文另做处理
|
||||
if (file.fileurl != null && file.fileurl.IndexOf("gw.") > -1)
|
||||
{
|
||||
string fileUrl = "http://" + file.fileurl;
|
||||
string suffName = Path.GetExtension(fileUrl);
|
||||
string newName = fileUrl.Remove(fileUrl.LastIndexOf(suffName, StringComparison.Ordinal), suffName.Length) + ".pdf";
|
||||
return Redirect(newName);
|
||||
}
|
||||
|
||||
string fileUrl2 = (Config.FileWebPath ?? "") + file.fileurl;
|
||||
string suffName2 = Path.GetExtension(fileUrl2);
|
||||
if (!string.IsNullOrEmpty(type))
|
||||
{
|
||||
// 单独读取文件是否存在
|
||||
string newFile = file.fileurl;
|
||||
string pdfFile = newFile.Remove(newFile.LastIndexOf(suffName2, StringComparison.Ordinal), suffName2.Length) + ".pdf";
|
||||
string outFile = _file.MapPhysicalPath(pdfFile);
|
||||
|
||||
if (type == "1")
|
||||
{
|
||||
if (System.IO.File.Exists(outFile)) return (Config.FileWebPath ?? "") + pdfFile;
|
||||
return fileUrl2;
|
||||
}
|
||||
if (type == "2")
|
||||
{
|
||||
string pdfUrl = string.Empty;
|
||||
if (System.IO.File.Exists(outFile)) pdfUrl = (Config.FileWebPath ?? "") + pdfFile;
|
||||
else pdfUrl = "https://view.officeapps.live.com/op/view.aspx?src=" + fileUrl2;
|
||||
return Redirect(pdfUrl);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
string newName2 = fileUrl2.Remove(fileUrl2.LastIndexOf(suffName2, StringComparison.Ordinal), suffName2.Length) + ".pdf";
|
||||
return Redirect(newName2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 公用附件上传(旧 FileUpload,[NoAuth])。
|
||||
/// chunk 参数 → 分片保存返回 {code:1,type:"chunk",msg};fileName 参数 → 合并分片;否则单文件上传。
|
||||
/// 落库 Common_UpFiles(TopicType=AppCode),返回 {fileurl,host,fileid,name,type}。
|
||||
/// </summary>
|
||||
[HttpPost("FileUpload")]
|
||||
public async Task<object> Upload()
|
||||
{
|
||||
string apptype = "docprint"; // 旧 WebHelper.AppType 固定 docprint
|
||||
string guid = Request.Form["guid"];
|
||||
string chunk = Request.Form["chunk"];
|
||||
string fileName = Request.Form["fileName"];
|
||||
|
||||
if (!string.IsNullOrEmpty(chunk))
|
||||
{
|
||||
var chunkFile = Request.Form.Files.FirstOrDefault(f => f.Name == "file") ?? Request.Form.Files.FirstOrDefault();
|
||||
await _file.SaveChunkAsync(guid, chunk, chunkFile);
|
||||
return new { code = 1, type = "chunk", msg = "分片上传成功!" };
|
||||
}
|
||||
|
||||
FileObj url;
|
||||
string errMsg = "上传失败!";
|
||||
if (string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
url = await _file.SaveFileAsync(apptype, Request.Form.Files.FirstOrDefault());
|
||||
}
|
||||
else
|
||||
{
|
||||
url = await _file.MergeSaveAsync(apptype, guid, fileName);
|
||||
}
|
||||
if (url == null)
|
||||
throw new DException(errMsg);
|
||||
|
||||
// 旧 WebHelper.AppCode == "1102" ? "1002" : AppCode → docprint 固定 1003
|
||||
int serid = await _file.SaveEnclosureAsync(url, DocPrintConstants.AppCode.ToString());
|
||||
return new { fileurl = url.fileurl, host = url.host, fileid = serid, name = url.filename, type = url.filetype };
|
||||
}
|
||||
|
||||
/// <summary>公用图片上传(旧 FileUploadImage,[NoAuth];多图,返回数组)。</summary>
|
||||
[HttpPost("FileUploadImage")]
|
||||
public async Task<object> UploadImg()
|
||||
{
|
||||
string apptype = "docprint";
|
||||
var urlList = await _file.SaveFileImageAsync(apptype, Request.Form.Files);
|
||||
if (urlList.Count == 0)
|
||||
throw new DException("上传失败!");
|
||||
|
||||
var result = new List<object>();
|
||||
foreach (var item in urlList)
|
||||
{
|
||||
int serid = await _file.SaveEnclosureAsync(item, DocPrintConstants.AppCode.ToString());
|
||||
result.Add(new { fileurl = item.fileurl, host = item.host, fileid = serid, name = item.filename, type = item.filetype });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>按扩展名取 ContentType(替代旧 MimeMapping.GetMimeMapping)。</summary>
|
||||
private static string GetContentType(string filePath)
|
||||
{
|
||||
string ext = Path.GetExtension(filePath).ToLower();
|
||||
return ext switch
|
||||
{
|
||||
".jpg" or ".jpeg" => "image/jpeg",
|
||||
".gif" => "image/gif",
|
||||
".png" => "image/png",
|
||||
".bmp" => "image/bmp",
|
||||
".pdf" => "application/pdf",
|
||||
".txt" => "text/plain",
|
||||
".doc" => "application/msword",
|
||||
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
".xls" => "application/vnd.ms-excel",
|
||||
".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
".ppt" => "application/vnd.ms-powerpoint",
|
||||
".pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
".zip" => "application/zip",
|
||||
".rar" => "application/x-rar-compressed",
|
||||
".mp4" => "video/mp4",
|
||||
_ => "application/octet-stream",
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static string SchSerID() => UserContextHolder.Current.SchSerID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using YhCore.Common;
|
||||
using YhCore.Common.UserContext;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.WebApi.Controllers.Api.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// 自由审批流控制器(对应旧 Areas/Common/FreeApproveController,路由 api/common/freeapprove)。
|
||||
/// 7 接口:自由流程节点查询/保存、审核意见、审核回显、抄送人。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route(ApiRoutes.ApiPath + "/common/freeapprove")]
|
||||
public class FreeApproveController : ControllerBase
|
||||
{
|
||||
private readonly IFreeApproveService _svc;
|
||||
|
||||
public FreeApproveController(IFreeApproveService svc)
|
||||
{
|
||||
_svc = svc;
|
||||
}
|
||||
|
||||
/// <summary>获取当前自由流程节点(dataid 为空时取最大 dataid)。</summary>
|
||||
[HttpGet("GetFleeFlow")]
|
||||
public Task<List<FreeFlowNode>> GetFleeFlow([FromQuery] int? dataid = null)
|
||||
=> _svc.GetFleeFlowAsync(UserContextHolder.Current?.Id ?? 0, dataid);
|
||||
|
||||
/// <summary>保存自由流程审核人,返回首步骤 SerID。</summary>
|
||||
[HttpGet("EditFleeFlow")]
|
||||
public Task<int> EditFleeFlow([FromQuery] string FlowUids, [FromQuery] int? dataid = null, [FromQuery] bool IsFlowNull = false)
|
||||
=> _svc.EditFleeFlowAsync(UserContextHolder.Current?.Id ?? 0, SchSerID(), dataid, FlowUids, IsFlowNull);
|
||||
|
||||
/// <summary>添加审核意见。</summary>
|
||||
[HttpGet("AddShenheRemark")]
|
||||
public Task<string> AddShenheRemark([FromQuery] int? dataid, [FromQuery] string remark, [FromQuery] int freestepid)
|
||||
=> _svc.AddShenheRemarkAsync(UserContextHolder.Current?.Id ?? 0, dataid, remark, freestepid);
|
||||
|
||||
/// <summary>获取后续自由流程节点。</summary>
|
||||
[HttpGet("GetAfterFleeFlow")]
|
||||
public Task<List<FreeFlowNode>> GetAfterFleeFlow([FromQuery] int? dataid, [FromQuery] int freestepid)
|
||||
=> _svc.GetAfterFleeFlowAsync(dataid, freestepid);
|
||||
|
||||
/// <summary>编辑后续自由流程,返回末步骤 SerID。</summary>
|
||||
[HttpGet("EditAfterFleeFlow")]
|
||||
public Task<int> EditAfterFleeFlow([FromQuery] int? dataid, [FromQuery] int freestepid, [FromQuery] string FlowUids)
|
||||
=> _svc.EditAfterFleeFlowAsync(dataid, freestepid, FlowUids);
|
||||
|
||||
/// <summary>自由流程审核回显(发起人 + 审核人步骤)。</summary>
|
||||
[HttpGet("ShenheFlow")]
|
||||
public Task<List<FreeShenheInfo>> ShenheFlow([FromQuery] int uid, [FromQuery] string ctime, [FromQuery] int dataid,
|
||||
[FromQuery] int? freestepid = null, [FromQuery] int? flowid = null)
|
||||
=> _svc.ShenheFlowAsync(uid, ctime, dataid, freestepid, flowid);
|
||||
|
||||
/// <summary>抄送人列表(按 uids 查用户)。</summary>
|
||||
[HttpGet("ChaoSendList")]
|
||||
public Task<List<ChaoSendUser>> ChaoSendList([FromQuery] string uids)
|
||||
=> _svc.ChaoSendListAsync(uids);
|
||||
|
||||
private static string SchSerID() => UserContextHolder.Current.SchSerID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Yh.SchClass.OpenApi.Model;
|
||||
using YhCore.Common;
|
||||
using YhCore.Common.UserContext;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Model.Vo;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.WebApi.Controllers.Api.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// 审核流程管理控制器(对应旧 Areas/Common/reviewprocessController,路由 api/common/reviewprocess)。
|
||||
/// 23 接口:流程/步骤 CRUD、教师查询、流程判断、用户角色。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route(ApiRoutes.ApiPath + "/common/reviewprocess")]
|
||||
public class ReviewprocessController : ControllerBase
|
||||
{
|
||||
private readonly IReviewProcessService _svc;
|
||||
|
||||
public ReviewprocessController(IReviewProcessService svc)
|
||||
{
|
||||
_svc = svc;
|
||||
}
|
||||
|
||||
#region 流程与步骤
|
||||
|
||||
/// <summary>审核流程列表(分页)。</summary>
|
||||
[HttpGet("List")]
|
||||
public Task<PageVO<FlowListItem>> List([FromQuery] string Name, [FromQuery] int PageSize,
|
||||
[FromQuery] int PageIndex, [FromQuery] int flowType, [FromQuery] bool Edit = false)
|
||||
=> _svc.ListAsync(Name, PageSize, PageIndex, flowType, Edit, SchSerID());
|
||||
|
||||
/// <summary>审核流程列表【局版】(降级为当前学校)。</summary>
|
||||
[HttpGet("List2")]
|
||||
public Task<PageVO<FlowListItem>> List2([FromQuery] string Name, [FromQuery] int PageSize,
|
||||
[FromQuery] int PageIndex, [FromQuery] int flowType, [FromQuery] bool Edit = false)
|
||||
=> _svc.List2Async(Name, PageSize, PageIndex, flowType, Edit, SchSerID());
|
||||
|
||||
/// <summary>步骤流程列表。</summary>
|
||||
[HttpGet("StepList")]
|
||||
public Task<PageVO<StepListItem>> StepList([FromQuery] string Name, [FromQuery] int Serid)
|
||||
=> _svc.StepListAsync(Name, Serid);
|
||||
|
||||
/// <summary>添加审核流程。</summary>
|
||||
[HttpPost("Create")]
|
||||
public async Task Create([FromBody] Common_flowinfo model)
|
||||
{
|
||||
model.schSerID = SchSerID();
|
||||
await _svc.CreateAsync(model);
|
||||
}
|
||||
|
||||
/// <summary>修改审核流程。</summary>
|
||||
[HttpPost("Edit")]
|
||||
public async Task Edit([FromBody] Common_flowinfo model)
|
||||
{
|
||||
model.schSerID = SchSerID();
|
||||
await _svc.EditAsync(model);
|
||||
}
|
||||
|
||||
/// <summary>添加步骤流程。</summary>
|
||||
[HttpPost("StepCreate")]
|
||||
public Task StepCreate([FromBody] Common_flowstep model)
|
||||
=> _svc.StepCreateAsync(model);
|
||||
|
||||
/// <summary>修改步骤流程。</summary>
|
||||
[HttpPost("StepEdit")]
|
||||
public Task StepEdit([FromBody] Common_flowstep model)
|
||||
=> _svc.StepEditAsync(model);
|
||||
|
||||
/// <summary>删除审核流程(ids 逗号分隔)。</summary>
|
||||
[HttpGet("Delete")]
|
||||
public Task Delete([FromQuery] string ids)
|
||||
=> _svc.DeleteAsync(ids);
|
||||
|
||||
/// <summary>删除步骤。</summary>
|
||||
[HttpPost("StepDelete")]
|
||||
public Task StepDelete([FromBody] decimal[] ids)
|
||||
=> _svc.StepDeleteAsync(ids);
|
||||
|
||||
/// <summary>流程绑定的步骤列表。</summary>
|
||||
[HttpGet("BindstepList")]
|
||||
public Task<List<BindStepItem>> BindstepList([FromQuery] int Serid)
|
||||
=> _svc.BindstepListAsync(Serid);
|
||||
|
||||
/// <summary>绑定步骤的人员。</summary>
|
||||
[HttpGet("BindGroupuserList")]
|
||||
public Task<List<BindGroupuserItem>> BindGroupuserList([FromQuery] int flowid, [FromQuery] int prestep)
|
||||
=> _svc.BindGroupuserListAsync(flowid, prestep);
|
||||
|
||||
/// <summary>绑定流程(流程 + 抄送人)。</summary>
|
||||
[HttpGet("Bindflowinfo")]
|
||||
public Task<List<BindFlowItem>> Bindflowinfo([FromQuery] int flowType)
|
||||
=> _svc.BindflowinfoAsync(SchSerID(), flowType);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 教师 / 人员
|
||||
|
||||
/// <summary>当前学校教师列表。</summary>
|
||||
[HttpGet("GetTeaList")]
|
||||
public Task<List<TeaOption>> GetTeaList()
|
||||
=> _svc.GetTeaListAsync(SchSerID());
|
||||
|
||||
/// <summary>学校 + 教育局教师列表(降级为当前学校)。</summary>
|
||||
[HttpGet("GetTeaListSup")]
|
||||
public Task<List<TeaOption>> GetTeaListSup()
|
||||
=> _svc.GetTeaListSupAsync(SchSerID());
|
||||
|
||||
/// <summary>教师信息分页。</summary>
|
||||
[HttpGet("GetUserinfo")]
|
||||
public Task<PageVO<TeaInfoVo>> GetUserinfo([FromQuery] string Name, [FromQuery] int statetype,
|
||||
[FromQuery] int PageSize, [FromQuery] int PageIndex)
|
||||
=> _svc.GetUserinfoAsync(SchSerID(), Name, statetype, PageSize, PageIndex);
|
||||
|
||||
/// <summary>非中层领导人员。</summary>
|
||||
[HttpGet("GetNoLeadingMiddleInfo")]
|
||||
public Task<string[]> GetNoLeadingMiddleInfo()
|
||||
=> _svc.GetNoLeadingMiddleInfoAsync(SchSerID());
|
||||
|
||||
/// <summary>非中层领导人员【局版】(降级为当前学校)。</summary>
|
||||
[HttpGet("GetNoLeadingMiddleInfoSup")]
|
||||
public Task<string[]> GetNoLeadingMiddleInfoSup()
|
||||
=> _svc.GetNoLeadingMiddleInfoSupAsync(SchSerID());
|
||||
|
||||
/// <summary>设置/取消中层领导(教务 SDK 未提供,暂不支持)。</summary>
|
||||
[HttpGet("changeLeadingMiddleStatus")]
|
||||
public Task ChangeLeadingMiddleStatus([FromQuery] int state, [FromQuery] string UserTeaSerIDstr)
|
||||
=> _svc.ChangeLeadingMiddleStatusAsync(state, UserTeaSerIDstr);
|
||||
|
||||
/// <summary>附件数据源。</summary>
|
||||
[HttpGet("filelist")]
|
||||
public Task<List<FileListItem>> Filelist([FromQuery] string ids)
|
||||
=> _svc.FilelistAsync(ids);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 流程判断 / 用户
|
||||
|
||||
/// <summary>该校审核流程是固定还是自由。</summary>
|
||||
[HttpGet("IsShenheflow")]
|
||||
public Task<ShenheFlowResult> IsShenheflow()
|
||||
=> _svc.IsShenheflowAsync(SchSerID(), UserContextHolder.Current?.Id ?? 0);
|
||||
|
||||
/// <summary>绑定抄送默认人。</summary>
|
||||
[HttpGet("IsSendUser")]
|
||||
public Task<List<SendUserItem>> IsSendUser()
|
||||
=> _svc.IsSendUserAsync(SchSerID(), UserContextHolder.Current?.Id ?? 0);
|
||||
|
||||
/// <summary>当前用户基础信息。</summary>
|
||||
[HttpGet("GetUser")]
|
||||
public Task<CurrentUserInfo> GetUser()
|
||||
=> _svc.GetUserAsync();
|
||||
|
||||
/// <summary>用户流程角色(1 有流程,0 无)。</summary>
|
||||
[HttpGet("GetUserFlowRole")]
|
||||
public Task<int> GetUserFlowRole()
|
||||
=> _svc.GetUserFlowRoleAsync(UserContextHolder.Current?.Id ?? 0);
|
||||
|
||||
#endregion
|
||||
|
||||
private static string SchSerID() => UserContextHolder.Current.SchSerID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Yh.Platform.Abstractions;
|
||||
using YhCore.Common;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.WebApi.Controllers.Api.DocPrint
|
||||
{
|
||||
/// <summary>
|
||||
/// 文印统计看板控制器(对应旧 Areas/DocPrint/DocPrintStatisticsController,路由 api/docprint/statistics)。
|
||||
/// 旧类级 [NoAuth]:schId 由前端传入(非当前用户),新项目 SSO 关闭时匿名放行、开启时按配置保护,
|
||||
/// 语义一致。ExportRecord(Excel 导出,NPOI)延后迁移。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route(ApiRoutes.ApiPath + "/docprint/statistics")]
|
||||
public class DocPrintStatisticsController : ControllerBase
|
||||
{
|
||||
private readonly IDocPrintStatisticsService _statistics;
|
||||
|
||||
public DocPrintStatisticsController(IDocPrintStatisticsService statistics)
|
||||
{
|
||||
_statistics = statistics;
|
||||
}
|
||||
|
||||
/// <summary>打印总数/消耗/最多类型统计(旧 GetDocPrintCount)。</summary>
|
||||
[HttpGet("GetDocPrintCount")]
|
||||
public async Task<System.Collections.Generic.List<DocPrintCountItem>> GetDocPrintCount(
|
||||
[FromQuery] int schId,
|
||||
[FromQuery] DateTime? startDate = null,
|
||||
[FromQuery] DateTime? endDate = null)
|
||||
=> await _statistics.GetDocPrintCountAsync(schId, startDate, endDate);
|
||||
|
||||
/// <summary>饼图(按类型,消耗/数量,旧 GetPieChart)。</summary>
|
||||
[HttpGet("GetPieChart")]
|
||||
public async Task<System.Collections.Generic.List<PieChartSeries>> GetPieChart(
|
||||
[FromQuery] int schId,
|
||||
[FromQuery] DateTime? startDate = null,
|
||||
[FromQuery] DateTime? endDate = null)
|
||||
=> await _statistics.GetPieChartAsync(schId, startDate, endDate);
|
||||
|
||||
/// <summary>打印类型列表(旧 GetDocPrintType)。</summary>
|
||||
[HttpGet("GetDocPrintType")]
|
||||
public async Task<System.Collections.Generic.List<DocPrintTypeItem>> GetDocPrintType([FromQuery] int schId)
|
||||
=> await _statistics.GetDocPrintTypeAsync(schId);
|
||||
|
||||
/// <summary>分页明细(旧 GetDocPrintList;旧 1 基 pageIndex 内部转 0 基)。</summary>
|
||||
[HttpGet("GetDocPrintList")]
|
||||
public async Task<DocPrintListResult> GetDocPrintList(
|
||||
[FromQuery] int schId,
|
||||
[FromQuery] string docType = "ALL",
|
||||
[FromQuery] string likeName = null,
|
||||
[FromQuery] int pageIndex = 1,
|
||||
[FromQuery] int pageSize = 10,
|
||||
[FromQuery] DateTime? startDate = null,
|
||||
[FromQuery] DateTime? endDate = null)
|
||||
{
|
||||
if (pageSize <= 0) throw new DException("pageSize 必须大于 0");
|
||||
return await _statistics.GetDocPrintListAsync(schId, docType, likeName, pageIndex, pageSize, startDate, endDate);
|
||||
}
|
||||
|
||||
// 延后:ExportRecord(Excel 导出,NPOI)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using YhCore.Common;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Repository;
|
||||
|
||||
namespace YhCore.WebApi.Controllers.Api.DocPrint
|
||||
{
|
||||
/// <summary>
|
||||
/// 启动预热控制器(对应旧 Areas/DocPrint/LoadingController,路由 api/docprint/loading)。
|
||||
/// 无业务逻辑:执行一次 Base_Log 查询触发数据库连接建立,防止首请求慢。
|
||||
/// 旧接口 void 无返回体;新项目由 GlobalWrapResultFilter 统一包装为 {code:0,msg:"",data:null}。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route(ApiRoutes.ApiPath + "/docprint/loading")]
|
||||
public class LoadingController : ControllerBase
|
||||
{
|
||||
private readonly IRepository<Base_Log> _log;
|
||||
|
||||
public LoadingController(IRepository<Base_Log> log)
|
||||
{
|
||||
_log = log;
|
||||
}
|
||||
|
||||
/// <summary>预热(执行一次 Base_Log 查询)。</summary>
|
||||
[HttpGet("List")]
|
||||
public async Task List()
|
||||
{
|
||||
await _log.FirstOrDefaultAsync(a => a.Id > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Yh.Platform.Abstractions;
|
||||
using YhCore.Common;
|
||||
using YhCore.Common.UserContext;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Model.Vo;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.WebApi.Controllers.Api.DocPrint
|
||||
{
|
||||
/// <summary>
|
||||
/// 审核角色 + 流程配置控制器(对应旧 Areas/DocPrint/WYSHRoleController,路由 api/docprint/wyshrole)。
|
||||
/// 45 action 中本批落地 39 个;Excel 导入导出(ExceltotalListSHRole/WYRDExceldaochu/UpfileExcelSHRole/
|
||||
/// downMBOfSHRole/downUpfileSHRoleFK)与审批单 PDF(DwonWYSQD)依赖 NPOI/PdfHelper,延后迁移。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route(ApiRoutes.ApiPath + "/docprint/wyshrole")]
|
||||
public class WYSHRoleController : ControllerBase
|
||||
{
|
||||
private readonly IWYSHRoleService _role;
|
||||
|
||||
public WYSHRoleController(IWYSHRoleService role)
|
||||
{
|
||||
_role = role;
|
||||
}
|
||||
|
||||
#region 审核角色管理
|
||||
|
||||
/// <summary>学校是否启用审核角色(旧 getIsSHRole)。</summary>
|
||||
[HttpGet("getIsSHRole")]
|
||||
public async Task<string> GetIsSHRole() => await _role.GetIsSHRoleAsync(SchSerID());
|
||||
|
||||
/// <summary>保存启用开关(旧 saveIsSHRole)。</summary>
|
||||
[HttpGet("saveIsSHRole")]
|
||||
public async Task<string> SaveIsSHRole([FromQuery] string val) => await _role.SaveIsSHRoleAsync(SchSerID(), val);
|
||||
|
||||
/// <summary>审核角色列表(旧 getSHRole)。</summary>
|
||||
[HttpGet("getSHRole")]
|
||||
public async Task<List<WYSHRoleExt>> GetSHRole() => await _role.GetSHRoleAsync(SchSerID());
|
||||
|
||||
/// <summary>角色改名(旧 UpdateSHRoleName)。</summary>
|
||||
[HttpGet("UpdateSHRoleName")]
|
||||
public async Task<string> UpdateSHRoleName([FromQuery] decimal serID, [FromQuery] string sHRoleName)
|
||||
=> await _role.UpdateSHRoleNameAsync(SchSerID(), UserId(), serID, sHRoleName);
|
||||
|
||||
/// <summary>新增/编辑角色(旧 EditSHRole,form: WYSHRole=JSON → [FromBody])。</summary>
|
||||
[HttpPost("EditSHRole")]
|
||||
public async Task<string> EditSHRole([FromBody] WYSHRoleExt model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
return await _role.EditSHRoleAsync(SchSerID(), UserId(), model);
|
||||
}
|
||||
|
||||
/// <summary>编辑成员行(旧 editSHTeaRow,form: SHTeaTxt → [FromBody])。</summary>
|
||||
[HttpPost("editSHTeaRow")]
|
||||
public async Task<string> EditSHTeaRow([FromBody] WY_SHRole_SHTea model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
return await _role.EditSHTeaRowAsync(SchSerID(), UserId(), model);
|
||||
}
|
||||
|
||||
/// <summary>删除成员行(旧 delSHTeaRow)。</summary>
|
||||
[HttpGet("delSHTeaRow")]
|
||||
public async Task<string> DelSHTeaRow([FromQuery] decimal serID)
|
||||
=> await _role.DelSHTeaRowAsync(SchSerID(), serID);
|
||||
|
||||
/// <summary>批量保存审核人(旧 editSHTea,form: SHRoleID + listSHTeaTxt → [FromBody])。</summary>
|
||||
[HttpPost("editSHTea")]
|
||||
public async Task<string> EditSHTea([FromBody] EditSHTeaDto model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
return await _role.EditSHTeaAsync(SchSerID(), UserId(), model.SHRoleID, model.listSHTea);
|
||||
}
|
||||
|
||||
/// <summary>批量保存被审核人(旧 editBSHTea,form: SHRoleID + listBSHTeaTxt → [FromBody])。</summary>
|
||||
[HttpPost("editBSHTea")]
|
||||
public async Task<string> EditBSHTea([FromBody] EditBSHTeaDto model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
return await _role.EditBSHTeaAsync(SchSerID(), UserId(), model.SHRoleID, model.listBSHTea);
|
||||
}
|
||||
|
||||
/// <summary>删除角色(旧 delSHRole,form: listSHRoleIDTxt → [FromBody])。</summary>
|
||||
[HttpPost("delSHRole")]
|
||||
public async Task<string> DelSHRole([FromBody] DelSHRoleDto model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
return await _role.DelSHRoleAsync(SchSerID(), model.listSHRoleID);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 审核角色流程
|
||||
|
||||
/// <summary>审核流程列表(旧 ListFlow)。</summary>
|
||||
[HttpGet("ListFlow")]
|
||||
public async Task<PageVO<SHRoleFlowListItem>> ListFlow(
|
||||
[FromQuery] string name = null,
|
||||
[FromQuery] int pageSize = 10,
|
||||
[FromQuery] int pageIndex = 0,
|
||||
[FromQuery] int flowType = 0,
|
||||
[FromQuery] bool edit = false)
|
||||
=> await _role.ListFlowAsync(name, pageSize, pageIndex, SchSerID(), flowType, edit);
|
||||
|
||||
/// <summary>新增/编辑流程(旧 EditFlow;schSerID 取当前用户)。</summary>
|
||||
[HttpPost("EditFlow")]
|
||||
public async Task<string> EditFlow([FromBody] Common_flowinfo model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
model.schSerID = SchSerID();
|
||||
return await _role.EditFlowAsync(model);
|
||||
}
|
||||
|
||||
/// <summary>新增步骤(旧 StepCreateOfSHRole)。</summary>
|
||||
[HttpPost("StepCreateOfSHRole")]
|
||||
public async Task<string> StepCreateOfSHRole([FromBody] Common_flowstep model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
return await _role.StepCreateOfSHRoleAsync(model);
|
||||
}
|
||||
|
||||
/// <summary>步骤详情(旧 getStepInfo)。</summary>
|
||||
[HttpGet("getStepInfo")]
|
||||
public async Task<SHRoleStepInfo> GetStepInfo([FromQuery] decimal flowid)
|
||||
=> await _role.GetStepInfoAsync(SchSerID(), flowid);
|
||||
|
||||
/// <summary>删除步骤(旧 StepDelete)。</summary>
|
||||
[HttpGet("StepDelete")]
|
||||
public async Task<string> StepDelete([FromQuery] int id)
|
||||
=> await _role.StepDeleteAsync(new List<int> { id });
|
||||
|
||||
/// <summary>改步骤名/条件/金额/按钮显隐(旧 updateStepName)。</summary>
|
||||
[HttpGet("updateStepName")]
|
||||
public async Task<string> UpdateStepName(
|
||||
[FromQuery] int serID,
|
||||
[FromQuery] int flowid,
|
||||
[FromQuery] string stepName,
|
||||
[FromQuery] int cond = 0,
|
||||
[FromQuery] string isshowJE = "0",
|
||||
[FromQuery] string isshowBtn = "1")
|
||||
=> await _role.UpdateStepNameAsync(SchSerID(), serID, flowid, stepName, cond, isshowJE, isshowBtn);
|
||||
|
||||
/// <summary>步骤绑定角色(旧 AddStepSHRole)。</summary>
|
||||
[HttpGet("AddStepSHRole")]
|
||||
public async Task<string> AddStepSHRole([FromQuery] int flowid, [FromQuery] int stepid, [FromQuery] decimal shRoleid)
|
||||
=> await _role.AddStepSHRoleAsync(SchSerID(), flowid, stepid, shRoleid);
|
||||
|
||||
/// <summary>步骤解绑角色(旧 delStepSHRole)。</summary>
|
||||
[HttpGet("delStepSHRole")]
|
||||
public async Task<string> DelStepSHRole([FromQuery] int stepid, [FromQuery] decimal shRoleid)
|
||||
=> await _role.DelStepSHRoleAsync(SchSerID(), stepid, shRoleid);
|
||||
|
||||
/// <summary>按金额取步骤(旧 getStepByJE)。</summary>
|
||||
[HttpGet("getStepByJE")]
|
||||
public async Task<SHRoleStepByJEResult> GetStepByJE([FromQuery] int flowid, [FromQuery] int je)
|
||||
=> await _role.GetStepByJEAsync(SchSerID(), UserId(), flowid, je);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 文印申请页面
|
||||
|
||||
/// <summary>流程发起人列表(旧 getLCOfUser)。</summary>
|
||||
[HttpGet("getLCOfUser")]
|
||||
public async Task<List<WYFlowForUser>> GetLCOfUser()
|
||||
=> await _role.GetLCOfUserAsync(SchSerID(), UserId(), UserName());
|
||||
|
||||
/// <summary>当前用户是否发起人(旧 IsSendUserSHRole)。</summary>
|
||||
[HttpGet("IsSendUserSHRole")]
|
||||
public async Task<List<SendUserItem>> IsSendUserSHRole([FromQuery] int flowid)
|
||||
=> await _role.IsSendUserSHRoleAsync(SchSerID(), UserContextHolder.Current?.Id ?? 0, flowid);
|
||||
|
||||
/// <summary>字典信息(旧 getDicInfo)。</summary>
|
||||
[HttpGet("getDicInfo")]
|
||||
public async Task<List<Common_Dictionary>> GetDicInfo([FromQuery] string typeID)
|
||||
=> await _role.GetDicInfoAsync(SchSerID(), typeID);
|
||||
|
||||
/// <summary>编辑文印(审核角色视角;旧 editDocPrint,form 三段 JSON → [FromBody])。</summary>
|
||||
[HttpPost("editDocPrint")]
|
||||
public async Task<int> EditDocPrint([FromBody] WYSHRoleEditDocPrintDto model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
return await _role.EditDocPrintAsync(SchSerID(), UserId(), model);
|
||||
}
|
||||
|
||||
/// <summary>审核流程回显(旧 getshenheFlow)。</summary>
|
||||
[HttpGet("getshenheFlow")]
|
||||
public async Task<SHRoleShenheFlowResult> GetshenheFlow([FromQuery] decimal docprintID, [FromQuery] int flowid)
|
||||
=> await _role.GetshenheFlowAsync(SchSerID(), docprintID, flowid);
|
||||
|
||||
/// <summary>文印表格数据(旧 getdocOftable)。</summary>
|
||||
[HttpGet("getdocOftable")]
|
||||
public async Task<SHRoleDocOfTableResult> GetDocOftable([FromQuery] decimal docPrintID)
|
||||
=> await _role.GetDocOftableAsync(SchSerID(), docPrintID);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 审核角色列表与审批
|
||||
|
||||
/// <summary>单条详情(旧 MyListtoOneSHRole)。</summary>
|
||||
[HttpGet("MyListtoOneSHRole")]
|
||||
public async Task<List<SHRoleDocPrintListItem>> MyListtoOneSHRole([FromQuery] int serID)
|
||||
=> await _role.MyListtoOneSHRoleAsync(serID, SchSerID());
|
||||
|
||||
/// <summary>我的文印(旧 MyListSHRole)。</summary>
|
||||
[HttpGet("MyListSHRole")]
|
||||
public async Task<List<SHRoleDocPrintListItem>> MyListSHRole(
|
||||
[FromQuery] string name = null,
|
||||
[FromQuery] string status = null,
|
||||
[FromQuery] string startDate = null,
|
||||
[FromQuery] string endDate = null,
|
||||
[FromQuery] int pageSize = 10,
|
||||
[FromQuery] int pageIndex = 0)
|
||||
=> await _role.MyListSHRoleAsync(name, status, startDate, endDate, pageSize, pageIndex, UserId(), SchSerID());
|
||||
|
||||
/// <summary>全部文印(旧 totalListSHRole)。</summary>
|
||||
[HttpGet("totalListSHRole")]
|
||||
public async Task<List<SHRoleDocPrintListItem>> TotalListSHRole(
|
||||
[FromQuery] int flowid = 0,
|
||||
[FromQuery] int dwid = 0,
|
||||
[FromQuery] string name = null,
|
||||
[FromQuery] string stateid = null,
|
||||
[FromQuery] string starttime = null,
|
||||
[FromQuery] string endtime = null,
|
||||
[FromQuery] int pageSize = 10,
|
||||
[FromQuery] int pageIndex = 0)
|
||||
=> await _role.TotalListSHRoleAsync(flowid, dwid, name, stateid, starttime, endtime, pageSize, pageIndex, SchSerID());
|
||||
|
||||
/// <summary>抄送列表(旧 SendRdListSHRole)。</summary>
|
||||
[HttpGet("SendRdListSHRole")]
|
||||
public async Task<List<SHRoleDocPrintListItem>> SendRdListSHRole(
|
||||
[FromQuery] int flowid = 0,
|
||||
[FromQuery] int dwid = 0,
|
||||
[FromQuery] string name = null,
|
||||
[FromQuery] string status = null,
|
||||
[FromQuery] string startDate = null,
|
||||
[FromQuery] string endDate = null,
|
||||
[FromQuery] int pageSize = 10,
|
||||
[FromQuery] int pageIndex = 0)
|
||||
=> await _role.SendRdListSHRoleAsync(flowid, dwid, name, status, startDate, endDate, UserId(), SchSerID(), pageSize, pageIndex);
|
||||
|
||||
/// <summary>审批列表(旧 SPListSHRole)。</summary>
|
||||
[HttpGet("SPListSHRole")]
|
||||
public async Task<List<SHRoleDocPrintListItem>> SPListSHRole(
|
||||
[FromQuery] int flowid = 0,
|
||||
[FromQuery] int dwid = 0,
|
||||
[FromQuery] string name = null,
|
||||
[FromQuery] string status = null,
|
||||
[FromQuery] string startDate = null,
|
||||
[FromQuery] string endDate = null,
|
||||
[FromQuery] int pageSize = 10,
|
||||
[FromQuery] int pageIndex = 0)
|
||||
=> await _role.SPListSHRoleAsync(flowid, dwid, name, status, startDate, endDate, UserId(), UserName(), SchSerID(), pageSize, pageIndex);
|
||||
|
||||
/// <summary>审核确认(旧 shenheokSHRole)。</summary>
|
||||
[HttpPost("shenheokSHRole")]
|
||||
public async Task<string> ShenheOkSHRole([FromBody] FlowTranctDto model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
model.actionuserid = UserContextHolder.Current?.Id ?? 0;
|
||||
return await _role.ShenheOkSHRoleAsync(model);
|
||||
}
|
||||
|
||||
// 延后:ExceltotalListSHRole / WYRDExceldaochu / UpfileExcelSHRole / downMBOfSHRole / downUpfileSHRoleFK(Excel,NPOI)
|
||||
// 延后:DwonWYSQD(审批单 PDF,PdfHelper + Server.MapPath)
|
||||
|
||||
#endregion
|
||||
|
||||
#region 签名 / 退回 / 金额重提
|
||||
|
||||
/// <summary>保存签名(旧 SaveSignature,form: pic → [FromBody])。</summary>
|
||||
[HttpPost("SaveSignature")]
|
||||
public async Task<string> SaveSignature([FromBody] SaveSignatureDto model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
return await _role.SaveSignatureAsync(SchSerID(), UserId(), model.pic);
|
||||
}
|
||||
|
||||
/// <summary>获取签名(旧 GetSignature)。</summary>
|
||||
[HttpGet("GetSignature")]
|
||||
public async Task<string> GetSignature() => await _role.GetSignatureAsync(SchSerID(), UserId());
|
||||
|
||||
/// <summary>删除签名(旧 DelSignature)。</summary>
|
||||
[HttpGet("DelSignature")]
|
||||
public async Task<string> DelSignature() => await _role.DelSignatureAsync(SchSerID(), UserId());
|
||||
|
||||
/// <summary>退回(旧 TuiHui,form: DocID + cont → [FromBody])。</summary>
|
||||
[HttpPost("TuiHui")]
|
||||
public async Task<string> TuiHui([FromBody] TuiHuiDto model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
return await _role.TuiHuiAsync(UserId(), model.cont, model.DocID);
|
||||
}
|
||||
|
||||
/// <summary>撤销退回(旧 unTuiHui)。</summary>
|
||||
[HttpGet("unTuiHui")]
|
||||
public async Task<string> UnTuiHui([FromQuery] decimal docID)
|
||||
=> await _role.UnTuiHuiAsync(UserId(), docID);
|
||||
|
||||
/// <summary>金额重提(旧 regetJE,form: DocID 逗号分隔 + type → [FromBody])。</summary>
|
||||
[HttpPost("regetJE")]
|
||||
public async Task<string> RegetJE([FromBody] RegetJEDto model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
var ids = (model.DocID ?? "").Split(',')
|
||||
.Where(a => !string.IsNullOrEmpty(a) && decimal.TryParse(a, out _))
|
||||
.Select(decimal.Parse).ToList();
|
||||
return await _role.RegetJEAsync(SchSerID(), UserId(), ids, model.type);
|
||||
}
|
||||
|
||||
/// <summary>重提记录(旧 getRegetJERd)。</summary>
|
||||
[HttpGet("getRegetJERd")]
|
||||
public async Task<List<SHRoleRegetJERdItem>> GetRegetJERd(
|
||||
[FromQuery] int pageSize = 10,
|
||||
[FromQuery] int pageIndex = 0)
|
||||
=> await _role.GetRegetJERdAsync(SchSerID(), pageSize, pageIndex);
|
||||
|
||||
/// <summary>记录下载次数(旧 setDownFileTimes)。</summary>
|
||||
[HttpGet("setDownFileTimes")]
|
||||
public async Task<int> SetDownFileTimes([FromQuery] int serID)
|
||||
=> await _role.SetDownFileTimesAsync(serID);
|
||||
|
||||
#endregion
|
||||
|
||||
private static string SchSerID() => UserContextHolder.Current.SchSerID;
|
||||
|
||||
private static string UserId() => UserContextHolder.Current?.Id.ToString() ?? "0";
|
||||
|
||||
private static string UserName() => UserContextHolder.Current?.UserName ?? "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Yh.Platform.Abstractions;
|
||||
using YhCore.Common;
|
||||
using YhCore.Common.UserContext;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.WebApi.Controllers.Api.DocPrint
|
||||
{
|
||||
/// <summary>
|
||||
/// 文印业务主控制器(对应旧 Areas/DocPrint/WY_listController,路由 api/docprint/wy_list)。
|
||||
/// 第一批:分类 / 版面 / 字典 14 接口;第三批 D1:文印 CRUD / 列表 / 审核 / 统计 / 指派 24 接口;
|
||||
/// Excel 导出 4 接口延后批次迁移。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route(ApiRoutes.ApiPath + "/docprint/wy_list")]
|
||||
public class WY_listController : ControllerBase
|
||||
{
|
||||
private readonly IDocPrintClassifyService _classify;
|
||||
private readonly IDictionaryService _dict;
|
||||
private readonly IWYDocPrintService _wy;
|
||||
|
||||
public WY_listController(IDocPrintClassifyService classify, IDictionaryService dict, IWYDocPrintService wy)
|
||||
{
|
||||
_classify = classify;
|
||||
_dict = dict;
|
||||
_wy = wy;
|
||||
}
|
||||
|
||||
#region 分类 / 版面 / 字典
|
||||
|
||||
/// <summary>添加文印分类。</summary>
|
||||
[HttpPost("AddDic")]
|
||||
public async Task AddDic([FromBody] Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
model.Schserid = SchSerID();
|
||||
await _classify.AddDicAsync(model);
|
||||
}
|
||||
|
||||
/// <summary>文印分类列表。</summary>
|
||||
[HttpGet("CategoryList")]
|
||||
public async Task<System.Collections.Generic.List<CategoryItem>> CategoryList([FromQuery] string name = null)
|
||||
=> await _classify.CategoryListAsync(SchSerID(), name);
|
||||
|
||||
/// <summary>添加印制版面。</summary>
|
||||
[HttpPost("AddYZBM")]
|
||||
public async Task AddYZBM([FromBody] Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
model.Schserid = SchSerID();
|
||||
await _classify.AddYZBMAsync(model);
|
||||
}
|
||||
|
||||
/// <summary>印制版面列表。</summary>
|
||||
[HttpGet("YZBMList")]
|
||||
public async Task<System.Collections.Generic.List<CategoryItem>> YZBMList([FromQuery] string name = null)
|
||||
=> await _classify.YZBMListAsync(SchSerID(), name);
|
||||
|
||||
/// <summary>删除文印分类/版面(ids 逗号分隔)。</summary>
|
||||
[HttpGet("DeleteDic")]
|
||||
public async Task DeleteDic([FromQuery] string ids)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ids)) throw new DException("ids is not null");
|
||||
decimal[] idArr = ids.Split(',').Select(a => decimal.Parse(a)).ToArray();
|
||||
await _classify.DeleteDicAsync(idArr);
|
||||
}
|
||||
|
||||
/// <summary>公共类别列表。</summary>
|
||||
[HttpGet("DictionaryTypeList")]
|
||||
public async Task<System.Collections.Generic.List<DictionaryTypeItem>> DictionaryTypeList(
|
||||
[FromQuery] string typeID,
|
||||
[FromQuery] string name = "",
|
||||
[FromQuery] bool showAll = false)
|
||||
=> await _classify.DictionaryTypeListAsync(SchSerID(), name, showAll, typeID);
|
||||
|
||||
/// <summary>添加公共类别(复用字典添加逻辑)。</summary>
|
||||
[HttpPost("AddDictionaryType")]
|
||||
public async Task AddDictionaryType([FromBody] Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
model.Schserid = SchSerID();
|
||||
await _dict.AddDicAsync(model);
|
||||
}
|
||||
|
||||
/// <summary>编辑公共类别(旧调 GetSysDict.EditDic)。</summary>
|
||||
[HttpPost("EditDictionaryType")]
|
||||
public async Task EditDictionaryType([FromBody] Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数有误!");
|
||||
model.Schserid = SchSerID();
|
||||
await _dict.EditDicAsync(model);
|
||||
}
|
||||
|
||||
/// <summary>删除公共类别(软删除)。</summary>
|
||||
[HttpGet("DeleteDictionaryType")]
|
||||
public async Task DeleteDictionaryType([FromQuery] string ids)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ids)) throw new DException("ids is not null");
|
||||
string[] idArr = ids.Split(',');
|
||||
await _classify.DeleteDictionaryTypeAsync(idArr);
|
||||
}
|
||||
|
||||
/// <summary>公共类别显隐。</summary>
|
||||
[HttpGet("SetDictionaryOrd")]
|
||||
public async Task SetDictionaryOrd([FromQuery] int dicSerID, [FromQuery] bool show)
|
||||
=> await _classify.SetDictionaryOrdAsync(SchSerID(), dicSerID, show);
|
||||
|
||||
/// <summary>公共类别排序。</summary>
|
||||
[HttpGet("OrderDic")]
|
||||
public async Task OrderDic([FromQuery] string ids)
|
||||
=> await _classify.OrderDicAsync(ids, SchSerID());
|
||||
|
||||
/// <summary>结果链接(依赖资源平台,已降级)。</summary>
|
||||
[HttpGet("GetResultUrl")]
|
||||
public async Task<object> GetResultUrl([FromQuery] int dataId)
|
||||
{
|
||||
var user = UserContextHolder.Current;
|
||||
return await _classify.GetResultUrlAsync(dataId, user?.Id ?? 0, 0, SchSerID());
|
||||
}
|
||||
|
||||
/// <summary>学校文印资源开关。</summary>
|
||||
[HttpGet("GetSchZyBit")]
|
||||
public async Task<bool> GetSchZyBit() => await _classify.GetSchZyBitAsync(SchSerID());
|
||||
|
||||
/// <summary>设置学校文印资源开关。</summary>
|
||||
[HttpGet("SetSchZyBit")]
|
||||
public async Task SetSchZyBit([FromQuery] bool zyBit) => await _classify.SetSchZyBitAsync(SchSerID(), zyBit);
|
||||
|
||||
/// <summary>待审数量角标。</summary>
|
||||
[HttpGet("GetShnheCount")]
|
||||
public async Task<int> GetShnheCount()
|
||||
=> await _classify.GetShnheCountAsync(UserContextHolder.Current?.Id.ToString(), SchSerID());
|
||||
|
||||
#endregion
|
||||
|
||||
#region 文印核心(第三批 D1:抄送 / 附件 / CRUD / 列表 / 审核 / 统计 / 指派)
|
||||
|
||||
/// <summary>抄送列表(旧 SendRdList)。</summary>
|
||||
[HttpGet("SendRdList")]
|
||||
public async Task<System.Collections.Generic.List<DocPrintListItem>> SendRdList(
|
||||
[FromQuery] string name = null,
|
||||
[FromQuery] string startDate = null,
|
||||
[FromQuery] string endDate = null,
|
||||
[FromQuery] int pageSize = 10,
|
||||
[FromQuery] int pageIndex = 0)
|
||||
=> await _wy.SendRdListAsync(name, startDate, endDate, UserId(), SchSerID(), pageSize, pageIndex);
|
||||
|
||||
/// <summary>查看抄送人员详情(旧 SendDetailsList)。</summary>
|
||||
[HttpGet("SendDetailsList")]
|
||||
public async Task<System.Collections.Generic.List<SendDetailItem>> SendDetailsList([FromQuery] int serID)
|
||||
=> await _wy.SendDetailsListAsync(serID);
|
||||
|
||||
/// <summary>修改抄送人(旧 EditSend)。</summary>
|
||||
[HttpGet("EditSend")]
|
||||
public async Task<string> EditSend([FromQuery] int serID, [FromQuery] string senduserid, [FromQuery] string mysenduserid)
|
||||
=> await _wy.EditSendAsync(serID, senduserid, mysenduserid, SchSerID(), UserId());
|
||||
|
||||
/// <summary>获取附件数据源(旧 filelist)。</summary>
|
||||
[HttpGet("filelist")]
|
||||
public async Task<System.Collections.Generic.List<DocPrintFileItem>> FileList([FromQuery] string ids)
|
||||
=> await _wy.FileListAsync(ids);
|
||||
|
||||
/// <summary>添加文印(旧 Create;短信/待办推送已降级为纯落库)。</summary>
|
||||
[HttpPost("Create")]
|
||||
public async Task<int> Create([FromBody] WYDocPrintSaveDto model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
model.UserID = UserId();
|
||||
model.SchSerID = SchSerID();
|
||||
return await _wy.CreateAsync(model);
|
||||
}
|
||||
|
||||
/// <summary>修改文印(旧 Edit)。</summary>
|
||||
[HttpPost("Edit")]
|
||||
public async Task<int> Edit([FromBody] WYDocPrintSaveDto model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
model.UserID = UserId();
|
||||
model.SchSerID = SchSerID();
|
||||
return await _wy.EditAsync(model);
|
||||
}
|
||||
|
||||
/// <summary>撤销(旧 Revoke)。</summary>
|
||||
[HttpPost("Revoke")]
|
||||
public async Task<string> Revoke([FromBody] FlowTranctDto model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
model.actionuserid = UserContextHolder.Current?.Id;
|
||||
return await _wy.RevokeAsync(model);
|
||||
}
|
||||
|
||||
/// <summary>我的文印列表(旧 MyList)。</summary>
|
||||
[HttpGet("MyList")]
|
||||
public async Task<System.Collections.Generic.List<DocPrintListItem>> MyList(
|
||||
[FromQuery] string name = null,
|
||||
[FromQuery] string status = null,
|
||||
[FromQuery] string startDate = null,
|
||||
[FromQuery] string endDate = null,
|
||||
[FromQuery] int pageSize = 10,
|
||||
[FromQuery] int pageIndex = 0)
|
||||
=> await _wy.MyListAsync(name, status, startDate, endDate, UserId(), SchSerID(), pageSize, pageIndex);
|
||||
|
||||
/// <summary>全部文印列表(旧 AllList 四分类,返回 ctype)。</summary>
|
||||
[HttpGet("AllList")]
|
||||
public async Task<System.Collections.Generic.List<DocPrintListItem>> AllList(
|
||||
[FromQuery] string name = null,
|
||||
[FromQuery] int pageSize = 10,
|
||||
[FromQuery] int pageIndex = 0)
|
||||
=> await _wy.AllListAsync(name, UserId(), SchSerID(), pageSize, pageIndex);
|
||||
|
||||
/// <summary>文印审核列表(旧 ShenheList;旧 Name 过滤为死代码,保持不生效)。</summary>
|
||||
[HttpGet("ShenheList")]
|
||||
public async Task<System.Collections.Generic.List<DocPrintListItem>> ShenheList(
|
||||
[FromQuery] int pageSize = 10,
|
||||
[FromQuery] int pageIndex = 0)
|
||||
=> await _wy.ShenheListAsync(UserId(), SchSerID(), pageSize, pageIndex);
|
||||
|
||||
/// <summary>审批列表(旧 SPList)。</summary>
|
||||
[HttpGet("SPList")]
|
||||
public async Task<System.Collections.Generic.List<DocPrintListItem>> SPList(
|
||||
[FromQuery] string name = null,
|
||||
[FromQuery] string status = null,
|
||||
[FromQuery] string startDate = null,
|
||||
[FromQuery] string endDate = null,
|
||||
[FromQuery] int pageSize = 10,
|
||||
[FromQuery] int pageIndex = 0)
|
||||
=> await _wy.SPListAsync(name, status, startDate, endDate, UserId(), SchSerID(), pageSize, pageIndex);
|
||||
|
||||
/// <summary>文印已审列表(旧 YishenList)。</summary>
|
||||
[HttpGet("YishenList")]
|
||||
public async Task<System.Collections.Generic.List<DocPrintListItem>> YishenList(
|
||||
[FromQuery] string name = null,
|
||||
[FromQuery] int pageSize = 10,
|
||||
[FromQuery] int pageIndex = 0)
|
||||
=> await _wy.YishenListAsync(name, UserId(), SchSerID(), pageSize, pageIndex);
|
||||
|
||||
/// <summary>全部文印列表(旧 totalList)。</summary>
|
||||
[HttpGet("totalList")]
|
||||
public async Task<System.Collections.Generic.List<DocPrintListItem>> TotalList(
|
||||
[FromQuery] string name = null,
|
||||
[FromQuery] string stateid = null,
|
||||
[FromQuery] string starttime = null,
|
||||
[FromQuery] string endtime = null,
|
||||
[FromQuery] int pageSize = 10,
|
||||
[FromQuery] int pageIndex = 0)
|
||||
=> await _wy.TotalListAsync(name, stateid, starttime, endtime, SchSerID(), pageSize, pageIndex);
|
||||
|
||||
/// <summary>获取当前一条文印详情(旧 MyListtoOne;附件明细并入条目 tempresultfile)。</summary>
|
||||
[HttpGet("MyListtoOne")]
|
||||
public async Task<System.Collections.Generic.List<DocPrintDetailItem>> MyListtoOne([FromQuery] int serID)
|
||||
=> await _wy.MyListtoOneAsync(serID, SchSerID());
|
||||
|
||||
/// <summary>审核确认(旧 shenheok)。</summary>
|
||||
[HttpPost("shenheok")]
|
||||
public async Task<string> ShenheOk([FromBody] FlowTranctDto model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
model.actionuserid = UserContextHolder.Current?.Id ?? 0;
|
||||
return await _wy.ShenheOkAsync(model);
|
||||
}
|
||||
|
||||
/// <summary>领取确认(旧 AffirmApplyforOk)。</summary>
|
||||
[HttpPost("AffirmApplyforOk")]
|
||||
public async Task<string> AffirmApplyforOk([FromBody] FlowTranctDto model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
model.actionuserid = UserContextHolder.Current?.Id ?? 0;
|
||||
return await _wy.AffirmApplyforOkAsync(model);
|
||||
}
|
||||
|
||||
/// <summary>删除文印(旧 Delete)。</summary>
|
||||
[HttpGet("Delete")]
|
||||
public async Task Delete([FromQuery] string ids)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ids)) throw new DException("ids is not null");
|
||||
await _wy.DeleteAsync(ids);
|
||||
}
|
||||
|
||||
/// <summary>绑定流程(旧 Bindflowinfo)。</summary>
|
||||
[HttpGet("Bindflowinfo")]
|
||||
public async Task<System.Collections.Generic.List<BindFlowOption>> Bindflowinfo([FromQuery] int flowType)
|
||||
=> await _wy.BindflowinfoAsync(SchSerID(), flowType);
|
||||
|
||||
/// <summary>查询指派(旧 GetUserZhipai)。</summary>
|
||||
[HttpGet("GetUserZhipai")]
|
||||
public async Task<string> GetUserZhipai([FromQuery] int? flowid, [FromQuery] int? flowstepid, [FromQuery] string type)
|
||||
=> await _wy.GetUserZhipaiAsync(UserContextHolder.Current?.Id ?? 0, SchSerID(), flowid, flowstepid, type);
|
||||
|
||||
/// <summary>文印数量按月统计(旧 List1)。</summary>
|
||||
[HttpGet("List1")]
|
||||
public async Task<System.Collections.Generic.List<MonthStatItem>> List1([FromQuery] string starttime = null, [FromQuery] string endtime = null)
|
||||
=> await _wy.List1Async(starttime, endtime, SchSerID());
|
||||
|
||||
/// <summary>文印详情统计列表(旧 List2)。</summary>
|
||||
[HttpGet("List2")]
|
||||
public async Task<System.Collections.Generic.List<DocDetailStatItem>> List2(
|
||||
[FromQuery] string starttime = null, [FromQuery] string endtime = null,
|
||||
[FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0)
|
||||
=> await _wy.List2Async(starttime, endtime, SchSerID(), pageSize, pageIndex);
|
||||
|
||||
/// <summary>文印数量按用户排名(旧 List3)。</summary>
|
||||
[HttpGet("List3")]
|
||||
public async Task<System.Collections.Generic.List<UserRankItem>> List3(
|
||||
[FromQuery] string starttime = null, [FromQuery] string endtime = null,
|
||||
[FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0)
|
||||
=> await _wy.List3Async(starttime, endtime, SchSerID(), pageSize, pageIndex);
|
||||
|
||||
/// <summary>按月份统计明细(旧 List4)。</summary>
|
||||
[HttpGet("List4")]
|
||||
public async Task<System.Collections.Generic.List<StatListItem>> List4(
|
||||
[FromQuery] int year, [FromQuery] int month,
|
||||
[FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0)
|
||||
=> await _wy.List4Async(year, month, SchSerID(), pageSize, pageIndex);
|
||||
|
||||
/// <summary>按用户统计明细(旧 List5)。</summary>
|
||||
[HttpGet("List5")]
|
||||
public async Task<System.Collections.Generic.List<StatListItem>> List5(
|
||||
[FromQuery] string userid = null, [FromQuery] string starttime = null, [FromQuery] string endtime = null,
|
||||
[FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0)
|
||||
=> await _wy.List5Async(userid, starttime, endtime, SchSerID(), pageSize, pageIndex);
|
||||
|
||||
// 延后批次:ExceltotalList / Exceldaochu1 / Exceldaochu2 / Exceldaochu3(Excel 导出,方案待定)
|
||||
|
||||
#endregion
|
||||
|
||||
private static string SchSerID() => UserContextHolder.Current.SchSerID;
|
||||
|
||||
private static string UserId() => UserContextHolder.Current?.Id.ToString() ?? "0";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Yh.Platform.Abstractions;
|
||||
using Yh.Platform.Auth;
|
||||
using Yh.Platform.OpenApi;
|
||||
using YhCore.Common;
|
||||
|
||||
namespace YhCore.WebApi.Controllers.Api
|
||||
{
|
||||
/// <summary>
|
||||
/// 登录用户接口(对应 Java 版 controller/api/LoginUserController,路由 /api/GetUserMod 等)。
|
||||
/// 注意:Java 版路由以大写开头(GetUserMod / GetUserInfo / Logout),此处保持 URL 大小写一致。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route(ApiRoutes.ApiPath)]
|
||||
public class LoginUserController : ControllerBase
|
||||
{
|
||||
private readonly IOpenApiUserClient _openApiUserClient;
|
||||
|
||||
public LoginUserController(IOpenApiUserClient openApiUserClient)
|
||||
{
|
||||
_openApiUserClient = openApiUserClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户可见的应用模块(对应 Java GET /api/GetUserMod,
|
||||
/// 内部调用 YschBaseOpenApi.getUserModule(id, schoolId))。
|
||||
/// </summary>
|
||||
[HttpGet("GetUserMod")]
|
||||
public async Task<List<BoaModule>> GetUserMod()
|
||||
{
|
||||
var boa = BoaUserContext.Get();
|
||||
return await _openApiUserClient.GetUserModuleAsync(boa?.Id ?? 0, boa?.School?.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前登录用户信息(对应 Java GET /api/GetUserInfo,
|
||||
/// Java 版直接返回认证切面注入的 authUserWithDept;未登录时为 null)。
|
||||
/// 返回新框架用户上下文解析的完整 BoaUser(含 School/CurrentRole/RoleIds),与 Java 返回结构一致。
|
||||
/// </summary>
|
||||
[HttpGet("GetUserInfo")]
|
||||
public BoaUser GetUserInfo()
|
||||
{
|
||||
return BoaUserContext.Get();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否登录(对应 Java GET /api/checkLogin):
|
||||
/// 已登录 → 重定向到运维工作台 /work-view.html(兼容 context-path,.NET 用相对路径);
|
||||
/// 未登录 → 401 + {"code":401,"msg":"未登录"}(通常 CAS 拦截器会先行 302 到 CAS 登录页)。
|
||||
/// </summary>
|
||||
[HttpGet("checkLogin")]
|
||||
public IActionResult CheckLogin()
|
||||
{
|
||||
if (BoaUserContext.Get()?.Id > 0)
|
||||
{
|
||||
// 对应 Java request.getContextPath() + "/work-view.html"(.NET 无 contextPath,用根相对路径)
|
||||
return Redirect("/work-view.html");
|
||||
}
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = "{\"code\":401,\"msg\":\"未登录\"}",
|
||||
ContentType = "application/json;charset=UTF-8",
|
||||
StatusCode = StatusCodes.Status401Unauthorized
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 登出(对应 Java GET /api/Logout:session.invalidate())。
|
||||
/// 清除本地 Cookie 认证(新框架 YhCookie 方案)+ 可选跳转到 CAS 单点登出(CasServerUrlPrefix + logout)。
|
||||
/// TODO:按需启用单点登出(去 yh:sso:cas-server-url-prefix + logout 退出所有 SSO 应用)。
|
||||
/// </summary>
|
||||
[HttpGet("Logout")]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
// 清除本地 Cookie 登录态(对应 session.invalidate())
|
||||
await HttpContext.SignOutAsync(YhAuthDefaults.CookieScheme);
|
||||
|
||||
// 仅在已启用 Session 中间件时再清一份
|
||||
if (HttpContext.Features.Get<ISessionFeature>() != null)
|
||||
{
|
||||
HttpContext.Session.Clear();
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using YhCore.Common.Result;
|
||||
|
||||
namespace YhCore.WebApi.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// 排除统一包装特性:标注在 Controller 或 Action 上,该处返回值不参与全局包装。
|
||||
/// 适用:CAS 协议端点(XML/跳转)、文件下载、已手工构造响应体的接口。
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
public class NoWrapResultAttribute : Attribute, IFilterMetadata
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 全局统一响应包装过滤器:所有 Controller 的 ObjectResult 正常返回
|
||||
/// 自动包装为 ApiResult{code=0, msg="", data=...}。
|
||||
/// 规则:
|
||||
/// - [NoWrapResult] 标注的 Controller/Action 跳过;
|
||||
/// - 非 2xx 状态码(参数校验 400 等)不包装,保持错误结构 {code,msg,data,timeNow};
|
||||
/// - 已是 ApiResult<T> 的返回值不重复包装(防双层嵌套);
|
||||
/// - ContentResult / Redirect / File 等非对象结果不包装(checkLogin 401、OAuth/CAS 协议端点等);
|
||||
/// - null 返回值包装为 {"code":0,"msg":"","data":null}(原为 204)。
|
||||
/// </summary>
|
||||
public class GlobalWrapResultFilter : IAsyncResultFilter
|
||||
{
|
||||
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
|
||||
{
|
||||
// 1) 显式排除
|
||||
if (context.Filters.Any(f => f is NoWrapResultAttribute))
|
||||
{
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) 只包装对象结果
|
||||
if (context.Result is ObjectResult objectResult)
|
||||
{
|
||||
var statusCode = objectResult.StatusCode ?? StatusCodes.Status200OK;
|
||||
var isErrorStatus = statusCode < 200 || statusCode >= 300;
|
||||
|
||||
if (!isErrorStatus && !IsApiResult(objectResult.Value))
|
||||
{
|
||||
context.Result = new ObjectResult(ApiResult<object>.Ok(objectResult.Value))
|
||||
{
|
||||
StatusCode = StatusCodes.Status200OK
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (context.Result is OkResult || context.Result is EmptyResult)
|
||||
{
|
||||
// Ok()(200 空 body)/ EmptyResult(204)→ 统一包装为 {"code":0,"msg":"","data":null}
|
||||
context.Result = new ObjectResult(ApiResult<object>.Ok())
|
||||
{
|
||||
StatusCode = StatusCodes.Status200OK
|
||||
};
|
||||
}
|
||||
|
||||
await next();
|
||||
}
|
||||
|
||||
/// <summary>值是否已是 ApiResult<T>(防双层包装)</summary>
|
||||
private static bool IsApiResult(object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var type = value.GetType();
|
||||
return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ApiResult<>);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Newtonsoft.Json;
|
||||
using Yh.Platform.Abstractions;
|
||||
using YhCore.Common.UserContext;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Repository;
|
||||
|
||||
namespace YhCore.WebApi.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// 系统审计日志过滤器(对应 Java SysOperationLogAspect)。
|
||||
/// 全量拦截所有 Controller Action(不依赖标注),每次请求后异步写入 sys_operation_log 表。
|
||||
/// 与 Java 对齐的行为:
|
||||
/// - 排除日志查询控制器自身(防递归刷屏);
|
||||
/// - 成功路径记录响应 GZIP + 业务状态码(0 成功 / 非 0 失败);
|
||||
/// - 异常路径记录错误码 + 异常堆栈摘要(截断 8000),异常继续向外传播;
|
||||
/// - 落库失败不影响主流程(审计不阻断业务)。
|
||||
/// 注:请求/响应脱敏(Java SensitiveDataUtil)未实现,仅做长度截断,后续可接入。
|
||||
/// </summary>
|
||||
public class SysOperationLogFilter : IAsyncActionFilter, IAsyncResultFilter
|
||||
{
|
||||
private const string AuditEntityKey = "__sys_operation_log_entity";
|
||||
private const string AuditStopwatchKey = "__sys_operation_log_stopwatch";
|
||||
|
||||
private const int MaxResponseSize = 10240; // 响应 JSON 截断长度(对齐 Java DEFAULT_MAX_RESPONSE_SIZE)
|
||||
private const int MaxErrorMsg = 8000; // 异常信息截断长度(对齐 Java MAX_ERROR_MSG)
|
||||
private const int MaxParamsSize = 4096; // 请求参数截断长度(Java 无此限制,此处防超大请求体)
|
||||
|
||||
/// <summary>排除的控制器(日志查询自身,防递归)</summary>
|
||||
private static readonly string[] ExcludeControllers =
|
||||
{
|
||||
"YhCore.WebApi.Controllers.Api.LogController",
|
||||
};
|
||||
|
||||
private static readonly string[] DeleteKeywords = { "delete", "del", "remove" };
|
||||
private static readonly string[] CreateKeywords = { "add", "create", "insert", "save", "import", "upload" };
|
||||
|
||||
private readonly IRepository<SysOperationLog> _repo;
|
||||
|
||||
public SysOperationLogFilter(IRepository<SysOperationLog> repo)
|
||||
{
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
||||
{
|
||||
if (IsExcluded(context))
|
||||
{
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
var entity = BuildEntity(context);
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
context.HttpContext.Items[AuditEntityKey] = entity;
|
||||
context.HttpContext.Items[AuditStopwatchKey] = stopwatch;
|
||||
|
||||
var executed = await next();
|
||||
|
||||
stopwatch.Stop();
|
||||
entity.CostTime = stopwatch.ElapsedMilliseconds;
|
||||
|
||||
if (executed.Exception != null)
|
||||
{
|
||||
// 异常路径:Result 过滤器不会执行,在此记录错误码与摘要后放行(异常继续传播,由 GlobalExceptionMiddleware 兜底)
|
||||
entity.StatusCode = ResolveErrorCode(executed.Exception);
|
||||
entity.ErrorMsg = BuildErrorMsg(executed.Exception);
|
||||
await SaveQuietlyAsync(entity);
|
||||
}
|
||||
// 成功路径:StatusCode / ResponseData 由 OnResultExecutionAsync 填充并落库
|
||||
}
|
||||
|
||||
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
|
||||
{
|
||||
if (!context.HttpContext.Items.TryGetValue(AuditEntityKey, out var raw) || raw is not SysOperationLog entity)
|
||||
{
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.HttpContext.Items.TryGetValue(AuditStopwatchKey, out var sw) && sw is Stopwatch stopwatch)
|
||||
{
|
||||
entity.CostTime = stopwatch.ElapsedMilliseconds;
|
||||
}
|
||||
|
||||
// 从结果对象提取业务状态码与响应体
|
||||
switch (context.Result)
|
||||
{
|
||||
case ObjectResult objectResult:
|
||||
entity.StatusCode = ResolveStatusCode(objectResult.Value);
|
||||
entity.ResponseData = BuildResponseData(objectResult.Value);
|
||||
break;
|
||||
case OkResult:
|
||||
case EmptyResult:
|
||||
case StatusCodeResult statusCodeResult when statusCodeResult.StatusCode >= 200 && statusCodeResult.StatusCode < 300:
|
||||
entity.StatusCode = ErrorCode.Success.Code;
|
||||
break;
|
||||
default:
|
||||
// 非对象结果(重定向/文件等)仅记 200 默认成功,不序列化响应体
|
||||
entity.StatusCode = ErrorCode.Success.Code;
|
||||
break;
|
||||
}
|
||||
|
||||
await next();
|
||||
|
||||
await SaveQuietlyAsync(entity);
|
||||
}
|
||||
|
||||
// ==================== 构建 ====================
|
||||
|
||||
private static bool IsExcluded(ActionExecutingContext context)
|
||||
{
|
||||
if (context.ActionDescriptor is not ControllerActionDescriptor descriptor)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var fullName = descriptor.ControllerTypeInfo.FullName;
|
||||
return ExcludeControllers.Any(c => string.Equals(fullName, c, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private static SysOperationLog BuildEntity(ActionExecutingContext context)
|
||||
{
|
||||
var descriptor = (ControllerActionDescriptor)context.ActionDescriptor;
|
||||
var http = context.HttpContext;
|
||||
var request = http.Request;
|
||||
var user = UserContextHolder.Current;
|
||||
|
||||
var uri = request.Path.HasValue ? request.Path.Value : "";
|
||||
var httpMethod = request.Method ?? "";
|
||||
var actionName = descriptor.ActionName ?? "";
|
||||
|
||||
var (level, operationType) = ResolveLevelAndType(httpMethod, uri, actionName);
|
||||
|
||||
return new SysOperationLog
|
||||
{
|
||||
CreateTime = DateTime.Now,
|
||||
UserId = user?.Id ?? 0,
|
||||
UserName = string.IsNullOrEmpty(user?.UserName) ? "anonymous" : user.UserName,
|
||||
SchId = user?.SchoolId,
|
||||
RequestIp = http.Connection.RemoteIpAddress?.ToString() ?? "",
|
||||
RequestUri = uri,
|
||||
RequestMethod = httpMethod,
|
||||
ClassMethod = $"{descriptor.ControllerTypeInfo.FullName}.{actionName}",
|
||||
LogLevel = level,
|
||||
OperationType = operationType,
|
||||
UserAgent = request.Headers["User-Agent"].ToString(),
|
||||
RequestParams = BuildRequestParams(context),
|
||||
StatusCode = ErrorCode.Success.Code
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>请求参数序列化(过滤基础设施类型,对齐 Java buildRequestParams)</summary>
|
||||
private static string BuildRequestParams(ActionExecutingContext context)
|
||||
{
|
||||
var args = context.ActionArguments.Values
|
||||
.Where(v => v != null && !IsInfrastructureType(v))
|
||||
.ToList();
|
||||
|
||||
if (args.Count == 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(args);
|
||||
return json.Length > MaxParamsSize
|
||||
? json.Substring(0, MaxParamsSize) + "...[截断]"
|
||||
: json;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsInfrastructureType(object value)
|
||||
{
|
||||
var type = value.GetType();
|
||||
return typeof(IFormFile).IsAssignableFrom(type)
|
||||
|| type == typeof(CancellationToken)
|
||||
|| typeof(HttpRequest).IsAssignableFrom(type)
|
||||
|| typeof(HttpResponse).IsAssignableFrom(type);
|
||||
}
|
||||
|
||||
/// <summary>响应 JSON → GZIP 压缩(对齐 Java fillResponse + GzipUtil.compress)</summary>
|
||||
private static byte[] BuildResponseData(object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(value);
|
||||
if (json.Length > MaxResponseSize)
|
||||
{
|
||||
json = json.Substring(0, MaxResponseSize) + "...[截断]";
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
using var output = new MemoryStream();
|
||||
using (var gzip = new GZipStream(output, CompressionLevel.Fastest, true))
|
||||
{
|
||||
gzip.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
return output.ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>业务状态码:返回值带 int Code 属性则取之(ApiResult 等),否则默认 0 成功</summary>
|
||||
private static int ResolveStatusCode(object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return ErrorCode.Success.Code;
|
||||
}
|
||||
|
||||
var codeProperty = value.GetType().GetProperty("Code", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||
if (codeProperty != null && codeProperty.PropertyType == typeof(int))
|
||||
{
|
||||
try
|
||||
{
|
||||
return (int)codeProperty.GetValue(value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略取值异常,回落默认成功
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorCode.Success.Code;
|
||||
}
|
||||
|
||||
/// <summary>异常错误码:DException 取业务码,其余取平台兜底码 6(对齐 Java resolveErrorCode)</summary>
|
||||
private static int ResolveErrorCode(Exception exception)
|
||||
{
|
||||
if (exception is DException dex && dex.Code != null)
|
||||
{
|
||||
return dex.Code.Code;
|
||||
}
|
||||
|
||||
return ErrorCode.InternalServerError.Code;
|
||||
}
|
||||
|
||||
/// <summary>异常信息摘要:message + 堆栈(截断 8000,对齐 Java buildErrorMsg)</summary>
|
||||
private static string BuildErrorMsg(Exception exception)
|
||||
{
|
||||
var msg = exception.Message;
|
||||
var head = string.IsNullOrWhiteSpace(msg) ? exception.GetType().Name : msg;
|
||||
var text = head + Environment.NewLine + exception.StackTrace;
|
||||
return text.Length > MaxErrorMsg ? text.Substring(0, MaxErrorMsg) : text;
|
||||
}
|
||||
|
||||
/// <summary>日志级别/类型推断(复刻 Java OperationLogResolver):GET=查询、DELETE=删除、关键字兜底、其余更新</summary>
|
||||
private static (int level, string type) ResolveLevelAndType(string httpMethod, string uri, string actionName)
|
||||
{
|
||||
var method = string.IsNullOrEmpty(httpMethod) ? "" : httpMethod.ToUpperInvariant();
|
||||
if (method == "")
|
||||
{
|
||||
return (2, "UPDATE");
|
||||
}
|
||||
|
||||
if (method == "GET")
|
||||
{
|
||||
return (3, "QUERY");
|
||||
}
|
||||
|
||||
if (method == "DELETE")
|
||||
{
|
||||
return (1, "DELETE");
|
||||
}
|
||||
|
||||
var text = (uri ?? "").ToLowerInvariant() + " " + (actionName ?? "").ToLowerInvariant();
|
||||
if (ContainsAny(text, DeleteKeywords))
|
||||
{
|
||||
return (1, "DELETE");
|
||||
}
|
||||
|
||||
if (ContainsAny(text, CreateKeywords))
|
||||
{
|
||||
return (2, "CREATE");
|
||||
}
|
||||
|
||||
return (2, "UPDATE");
|
||||
}
|
||||
|
||||
private static bool ContainsAny(string text, IEnumerable<string> keywords)
|
||||
{
|
||||
foreach (var keyword in keywords)
|
||||
{
|
||||
if (text.Contains(keyword, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>异步落库,失败不影响主流程(对齐 Java 操作日志不阻断业务)</summary>
|
||||
private async Task SaveQuietlyAsync(SysOperationLog entity)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _repo.AddAsync(entity);
|
||||
await _repo.SaveChangesAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略:落库失败仅影响审计,不影响接口结果
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using YhCore.Common.UserContext;
|
||||
|
||||
namespace YhCore.WebApi.Middleware
|
||||
{
|
||||
/// <summary>
|
||||
/// 接口访问日志中间件(唯一日志出口,方案 §6.2)。
|
||||
/// 记录:请求时间 / 渠道 / 路径与方法 / Query / Body(敏感字段脱敏)/
|
||||
/// 客户端 IP / 当前用户 / 响应状态码 / 耗时 / 异常摘要。
|
||||
/// 其余一切日志(SQL、框架、业务 Debug)均已关闭。
|
||||
/// </summary>
|
||||
public class ApiAccessLogMiddleware
|
||||
{
|
||||
private static readonly ILog Log = LogManager.GetLogger(typeof(ApiAccessLogMiddleware));
|
||||
|
||||
/// <summary>需要脱敏的入参 key(对应 Java SensitiveDataUtil)</summary>
|
||||
private static readonly string[] SensitiveKeys = { "password", "pwd", "sign", "token", "secret" };
|
||||
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<ApiAccessLogMiddleware> _fallbackLogger;
|
||||
|
||||
public ApiAccessLogMiddleware(RequestDelegate next, ILogger<ApiAccessLogMiddleware> fallbackLogger)
|
||||
{
|
||||
_next = next;
|
||||
_fallbackLogger = fallbackLogger;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var request = context.Request;
|
||||
|
||||
// 请求体读取(可重复读)
|
||||
var body = await ReadBodyAsync(request);
|
||||
|
||||
string exceptionSummary = null;
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptionSummary = ex.Message;
|
||||
throw; // 异常继续向上,由 GlobalExceptionMiddleware 处理响应
|
||||
}
|
||||
finally
|
||||
{
|
||||
sw.Stop();
|
||||
WriteLog(context, body, sw.ElapsedMilliseconds, exceptionSummary);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteLog(HttpContext context, string body, long elapsedMs, string exceptionSummary)
|
||||
{
|
||||
var request = context.Request;
|
||||
var user = UserContextHolder.Current;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("time=").Append(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"));
|
||||
sb.Append(" | channel=").Append(ResolveChannel(request.Path.Value));
|
||||
sb.Append(" | method=").Append(request.Method);
|
||||
sb.Append(" | path=").Append(request.Path.Value);
|
||||
sb.Append(" | query=").Append(MaskSensitive(request.QueryString.Value));
|
||||
if (!string.IsNullOrEmpty(body))
|
||||
{
|
||||
sb.Append(" | body=").Append(MaskSensitive(body));
|
||||
}
|
||||
sb.Append(" | ip=").Append(context.Connection.RemoteIpAddress?.ToString() ?? "-");
|
||||
sb.Append(" | user=").Append(user?.Id.ToString() ?? "-");
|
||||
sb.Append(" | status=").Append(context.Response.StatusCode);
|
||||
sb.Append(" | cost=").Append(elapsedMs).Append("ms");
|
||||
if (!string.IsNullOrEmpty(exceptionSummary))
|
||||
{
|
||||
sb.Append(" | exception=").Append(exceptionSummary);
|
||||
}
|
||||
|
||||
Log.Info(sb.ToString());
|
||||
}
|
||||
|
||||
/// <summary>渠道解析:/api → api、/mobiapi → mobiapi、/oapi → oapi、其余 -</summary>
|
||||
private static string ResolveChannel(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) return "-";
|
||||
if (path.StartsWith("/api/", StringComparison.OrdinalIgnoreCase)) return "api";
|
||||
if (path.StartsWith("/mobiapi/", StringComparison.OrdinalIgnoreCase)) return "mobiapi";
|
||||
if (path.StartsWith("/oapi/", StringComparison.OrdinalIgnoreCase)) return "oapi";
|
||||
return "-";
|
||||
}
|
||||
|
||||
/// <summary>对查询串/请求体做敏感字段脱敏(key=value 中的 value 替换为 ***)</summary>
|
||||
private static string MaskSensitive(string raw)
|
||||
{
|
||||
if (string.IsNullOrEmpty(raw)) return raw;
|
||||
|
||||
var parts = raw.Split('&');
|
||||
for (var i = 0; i < parts.Length; i++)
|
||||
{
|
||||
var eq = parts[i].IndexOf('=');
|
||||
if (eq <= 0) continue;
|
||||
var key = parts[i].Substring(0, eq);
|
||||
if (SensitiveKeys.Any(k => key.Contains(k, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
parts[i] = key + "=***";
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join("&", parts);
|
||||
}
|
||||
|
||||
private static async Task<string> ReadBodyAsync(HttpRequest request)
|
||||
{
|
||||
if (!request.Body.CanRead
|
||||
|| !request.ContentLength.HasValue
|
||||
|| request.ContentLength <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
request.EnableBuffering();
|
||||
try
|
||||
{
|
||||
using var reader = new StreamReader(request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true);
|
||||
var body = await reader.ReadToEndAsync();
|
||||
request.Body.Position = 0; // 复位,供后续中间件/模型绑定读取
|
||||
return body.Length > 2048 ? body.Substring(0, 2048) + "...(truncated)" : body;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Yh.Platform.AppAuth;
|
||||
using Yh.Platform.Auth;
|
||||
|
||||
namespace YhCore.WebApi.Middleware
|
||||
{
|
||||
/// <summary>
|
||||
/// 应用间签名校验中间件(对应 Java yh-app-auth-spring-boot-starter,仅对 /oapi 渠道生效)。
|
||||
/// 基于新框架 <see cref="AppAuthVerifier"/> 完成请求级签名校验(X-App-Key / X-Timestamp / X-Signature
|
||||
/// 三件套 + 时间戳过期 + 防重放);Enabled=false(yh:app-auth)时跳过。
|
||||
/// 签名协议见 yh-platform-net contract/sign-protocol.md。
|
||||
/// </summary>
|
||||
public class AppAuthMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly AppAuthVerifier _verifier;
|
||||
private readonly AppAuthOptions _options;
|
||||
|
||||
public AppAuthMiddleware(RequestDelegate next, AppAuthVerifier verifier, IOptions<AppAuthOptions> options)
|
||||
{
|
||||
_next = next;
|
||||
_verifier = verifier;
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
if (_options.Enabled)
|
||||
{
|
||||
var path = context.Request.Path.Value ?? string.Empty;
|
||||
if (path.StartsWith("/oapi", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var appKey = GetHeader(context, "X-App-Key");
|
||||
var timestamp = GetHeader(context, "X-Timestamp");
|
||||
var signature = GetHeader(context, "X-Signature");
|
||||
var nonce = GetHeader(context, "X-Nonce");
|
||||
|
||||
if (string.IsNullOrEmpty(appKey) || string.IsNullOrEmpty(timestamp) || string.IsNullOrEmpty(signature))
|
||||
{
|
||||
await RejectAsync(context, "缺少签名头(X-App-Key / X-Timestamp / X-Signature)");
|
||||
return;
|
||||
}
|
||||
|
||||
// canonical target:path + 排序 query(对齐 Java AppSignInterceptor,挂根路径无 basePath)
|
||||
var canonicalTarget = SignCanonical.BuildTarget(
|
||||
new Uri(context.Request.Scheme + "://" + context.Request.Host + context.Request.Path + context.Request.QueryString),
|
||||
basePath: null);
|
||||
|
||||
var ok = await _verifier.VerifyAsync(
|
||||
appKey, timestamp, signature,
|
||||
context.Request.Method, canonicalTarget,
|
||||
string.IsNullOrEmpty(nonce) ? null : nonce,
|
||||
context.RequestAborted);
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
await RejectAsync(context, "签名校验失败(签名错误 / 时间戳过期 / 重复请求)");
|
||||
return;
|
||||
}
|
||||
|
||||
// 验签通过:写入调用方应用身份(对齐 LoginUser.AppKey,业务可经 UserContextHolder 读取)
|
||||
UserContextHolder.Set(new LoginUser { AppKey = appKey });
|
||||
}
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
|
||||
private static async Task RejectAsync(HttpContext context, string message)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
context.Response.ContentType = "application/json; charset=utf-8";
|
||||
await context.Response.WriteAsync(JsonSerializer.Serialize(new
|
||||
{
|
||||
code = 4,
|
||||
msg = message,
|
||||
data = (object)null,
|
||||
}));
|
||||
}
|
||||
|
||||
private static string GetHeader(HttpContext context, string name)
|
||||
{
|
||||
return context.Request.Headers.TryGetValue(name, out var value) ? value.ToString() : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using Yh.Platform.Abstractions;
|
||||
|
||||
namespace YhCore.WebApi.Middleware
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局异常中间件(对应 Java @ControllerAdvice)。
|
||||
/// 捕获未处理异常 → 返回统一错误响应 {code, msg, data, timeNow}(对齐 bschclassmanage ValidationExceptionHandler
|
||||
/// 与平台 GlobalExceptionHandler);异常摘要由接口访问日志(ApiAccessLogMiddleware)统一记录,本中间件不单独输出日志(方案 §6.2 / §6.4)。
|
||||
/// 规则(对齐 Java):
|
||||
/// DException(业务异常,含 /oapi 业务失败信封)→ HTTP 200 + {code, msg, data, timeNow}
|
||||
/// JSON 解析失败 / 请求体异常 → HTTP 400 + {code:1, msg:"请求体(JSON)解析失败: ...", data:null, timeNow}
|
||||
/// 未捕获异常(平台兜底) → HTTP 200 + {code:6(INTERNAL_SERVER_ERROR), msg:"内部服务错误", data:null, timeNow}
|
||||
/// </summary>
|
||||
public class GlobalExceptionMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
public GlobalExceptionMiddleware(RequestDelegate next)
|
||||
{
|
||||
_next = next;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
catch (DException dex)
|
||||
{
|
||||
// 业务异常(含 /oapi 信封失败)→ 200 + {code, msg, data, timeNow}(code 原样透传,对齐 Java handleDException)
|
||||
await WriteJsonAsync(context, StatusCodes.Status200OK, new
|
||||
{
|
||||
code = dex.Code?.Code ?? ErrorCode.Error.Code,
|
||||
msg = dex.Message,
|
||||
data = dex.EnvelopeData,
|
||||
timeNow = TimeNow()
|
||||
});
|
||||
}
|
||||
catch (BadHttpRequestException ex)
|
||||
{
|
||||
// @RequestBody 反序列化失败(JSON 格式错 / 字段类型不符)→ 400 + 真实原因透出(对齐 Java handleNotReadable)
|
||||
await WriteJsonAsync(context, StatusCodes.Status400BadRequest, new
|
||||
{
|
||||
code = ErrorCode.Error.Code,
|
||||
msg = $"请求体(JSON)解析失败: {ex.Message}",
|
||||
data = (object)null,
|
||||
timeNow = TimeNow()
|
||||
});
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 未捕获异常 → 平台兜底:200 + code:6 内部服务错误(对齐 Java GlobalExceptionHandler.handleException)
|
||||
await WriteJsonAsync(context, StatusCodes.Status200OK, new
|
||||
{
|
||||
code = ErrorCode.InternalServerError.Code,
|
||||
msg = ErrorCode.InternalServerError.Msg,
|
||||
data = (object)null,
|
||||
timeNow = TimeNow()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task WriteJsonAsync(HttpContext context, int statusCode, object body)
|
||||
{
|
||||
context.Response.StatusCode = statusCode;
|
||||
context.Response.ContentType = "application/json; charset=utf-8";
|
||||
await context.Response.WriteAsync(JsonConvert.SerializeObject(body));
|
||||
}
|
||||
|
||||
/// <summary>时间戳(对齐 Java timeNow 格式 yyyy-MM-dd HH:mm:ss)</summary>
|
||||
private static string TimeNow() => DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Yh.Platform.Auth;
|
||||
|
||||
namespace YhCore.WebApi.Middleware
|
||||
{
|
||||
/// <summary>
|
||||
/// SSO 路径保护中间件(对应 Java yh-auth-spring-boot-starter 的 /api/* 保护行为)。
|
||||
/// 规则(对齐 yh:sso 配置节):
|
||||
/// - yh:sso:enabled=false(默认):所有路径匿名放行,不影响开发联调;
|
||||
/// - yh:sso:enabled=true:匹配 /api/* / /mobiapi/* / 根路径(/, 浏览器 SSO 入口) 且未认证的请求
|
||||
/// → Challenge → Yh.Platform.Auth CasAuthenticationHandler 302 重定向到 CAS 登录页;
|
||||
/// - CAS 回调路径(yh:sso:signInCallbackPath,默认 /signin-cas)未认证时同样 Challenge:
|
||||
/// 直接访问回调(无 ticket)进入登录流程;携带失效/已消费 ticket 的回调则返回 401 错误页,
|
||||
/// 不再次 Challenge——避免“回调→登录→新票据→再失败”无限重定向循环(自动登录型 CAS 下必现);
|
||||
/// 回调校验成功(已认证 + 302)则短路管道,防止 MVC 端点匹配 404 覆盖 handler 的回跳 302;
|
||||
/// - Swagger 路径始终放行,方便本地接口调试;
|
||||
/// - /oapi 开放渠道始终放行:其准入由 AppAuthMiddleware 签名验签负责(X-App-Key / X-Timestamp /
|
||||
/// X-Signature 票据),无需用户登录态,SSO 登录保护永不覆盖该渠道(即使 yh:sso:urlPattern 误配
|
||||
/// /oapi/* 也不拦截),保证应用间调用不受浏览器登录流程影响。
|
||||
/// 调用方:Startup.Configure 中 UseAuthentication + UseYhUserContext 之后、UseAuthorization 之前。
|
||||
/// </summary>
|
||||
public class SsoProtectionMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public SsoProtectionMiddleware(RequestDelegate next, IConfiguration configuration)
|
||||
{
|
||||
_next = next;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var sso = _configuration.GetSection("yh:sso");
|
||||
var isSwaggerPath = context.Request.Path.StartsWithSegments("/swagger");
|
||||
var isOapiPath = context.Request.Path.StartsWithSegments("/oapi");
|
||||
var isRootEntry = context.Request.Path == "/";
|
||||
var isCasCallback = context.Request.Path.StartsWithSegments(sso["signInCallbackPath"] ?? "/signin-cas");
|
||||
|
||||
// 已认证(含 Cookie 恢复登录态)直接放行
|
||||
if (context.User?.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
// 回调路径刚完成票据校验:CasAuthenticationHandler 已写入 Cookie 并 Response.Redirect(302 + Location),
|
||||
// 但 handler 返回 NoResult 不短路管道——若继续 _next 走 MVC 端点匹配,/signin-cas 无对应 action,
|
||||
// 会返回 404 覆盖 302,浏览器看到"登录成功却 404"。此处已认证 + 已 302 即短路,保住回跳响应。
|
||||
if (isCasCallback && context.Response.StatusCode == 302 &&
|
||||
!string.IsNullOrEmpty(context.Response.Headers["Location"]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// oapi 开放渠道:无需用户登录,准入由 AppAuthMiddleware 签名验签负责(票据 = 签名头),
|
||||
// 未带票据的请求由 AppAuthMiddleware 返回 401(而非 SSO 302),保证直接访问语义。
|
||||
if (isOapiPath)
|
||||
{
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var enabled = sso["enabled"] != null
|
||||
? bool.TryParse(sso["enabled"], out var en) && en
|
||||
: _configuration.GetValue("Yh:Sso:Enabled", false);
|
||||
|
||||
if (enabled && !isSwaggerPath &&
|
||||
(isRootEntry || isCasCallback || IsProtectedPath(context.Request.Path.Value, sso)))
|
||||
{
|
||||
// 请求携带 ticket 却仍未认证 → 说明 CAS 票据校验失败(一次性票据已被消费/过期,
|
||||
// 或 service 与服务端登记不一致)。此时若再次 Challenge,自动登录型 CAS 会立刻签发
|
||||
// 新票据跳回 → 再次失败 → 无限重定向循环。直接返回 401 错误页(带重新登录入口)打破循环。
|
||||
if (context.Request.Query.ContainsKey("ticket"))
|
||||
{
|
||||
context.Response.StatusCode = 401;
|
||||
context.Response.ContentType = "text/html; charset=utf-8";
|
||||
await context.Response.WriteAsync(
|
||||
"<!DOCTYPE html><html lang=\"zh\"><head><meta charset=\"utf-8\"><title>登录失效</title></head>" +
|
||||
"<body style=\"font-family:system-ui;text-align:center;padding-top:80px\">" +
|
||||
"<h2>登录票据校验失败或已过期</h2>" +
|
||||
"<p>CAS 登录票据为一次性凭证,刷新或重复使用同一地址会导致校验失败。</p>" +
|
||||
"<p><a href=\"/\">重新登录</a></p></body></html>");
|
||||
return;
|
||||
}
|
||||
// 未认证 → Challenge → 302 重定向 CAS 登录页(ticket 校验与回跳由新框架 CasAuthenticationHandler 处理)
|
||||
await context.ChallengeAsync(YhAuthDefaults.CasScheme);
|
||||
return;
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
|
||||
/// <summary>路径是否命中保护模式(yh:sso:urlPattern / yh:sso:mobiApiPattern,默认 /api/*、/mobiapi/*,通配后缀匹配,忽略大小写)</summary>
|
||||
private static bool IsProtectedPath(string path, IConfigurationSection sso)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var urlPattern = sso["urlPattern"] ?? "/api/*";
|
||||
var mobiPattern = sso["mobiApiPattern"] ?? "/mobiapi/*";
|
||||
return MatchesPattern(path, urlPattern) || MatchesPattern(path, mobiPattern);
|
||||
}
|
||||
|
||||
private static bool MatchesPattern(string path, string pattern)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pattern) || string.IsNullOrEmpty(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 形如 /api/* → 前缀匹配;否则精确匹配(忽略大小写)
|
||||
if (pattern.EndsWith("/*", StringComparison.Ordinal))
|
||||
{
|
||||
var prefix = pattern.Substring(0, pattern.Length - 1); // 去掉 *
|
||||
return path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return path.Equals(pattern, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace YhCore.WebApi
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
CreateHostBuilder(args).Build().Run();
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"profiles": {
|
||||
"YhCore.WebApi": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "http://localhost:51835"
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
},
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:58057/",
|
||||
"sslPort": 44399
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using FluentValidation.AspNetCore;
|
||||
using log4net.Config;
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using YhCore.Common.Cache;
|
||||
using YhCore.Repository;
|
||||
using YhCore.Repository.Extensions;
|
||||
using YhCore.Service.Impl;
|
||||
using YhCore.Service.Interfaces;
|
||||
using YhCore.Tasks;
|
||||
using YhCore.WebApi.Filters;
|
||||
using YhCore.WebApi.Middleware;
|
||||
|
||||
using YhCore.Common.Config;
|
||||
using YhCore.Common.Exceptions;
|
||||
using YhCore.Common.UserContext;
|
||||
using Yh.Platform.Abstractions;
|
||||
using Yh.SchClass.OpenApi;
|
||||
using Yh.Platform.Auth;
|
||||
|
||||
namespace YhCore.WebApi
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
|
||||
// log4net 初始化(唯一日志出口:接口访问日志)
|
||||
var log4netConfig = Path.Combine(AppContext.BaseDirectory, "log4net.config");
|
||||
if (File.Exists(log4netConfig))
|
||||
{
|
||||
XmlConfigurator.Configure(new FileInfo(log4netConfig));
|
||||
}
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddControllers(options =>
|
||||
{
|
||||
// 系统审计日志:全量拦截 Controller(对齐 Java SysOperationLogAspect,内部排除日志查询控制器)
|
||||
options.Filters.Add(new TypeFilterAttribute(typeof(SysOperationLogFilter)));
|
||||
// 全局统一响应包装:Controller 正常返回自动包 ApiResult{code,msg,data}
|
||||
options.Filters.Add<GlobalWrapResultFilter>();
|
||||
})
|
||||
.ConfigureApiBehaviorOptions(options =>
|
||||
{
|
||||
// 模型校验失败 → 400 + {code, msg, data, timeNow}(对齐 Java ValidationExceptionHandler 参数校验返回)
|
||||
options.InvalidModelStateResponseFactory = context =>
|
||||
{
|
||||
var msgs = context.ModelState.Values
|
||||
.SelectMany(v => v.Errors)
|
||||
.Select(e => e.ErrorMessage)
|
||||
.Where(m => !string.IsNullOrEmpty(m))
|
||||
.ToList();
|
||||
return new BadRequestObjectResult(new
|
||||
{
|
||||
code = ErrorCode.Error.Code,
|
||||
msg = string.Join(";", msgs),
|
||||
data = (object)null,
|
||||
timeNow = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
|
||||
});
|
||||
};
|
||||
})
|
||||
// 对齐 Java Jackson:日期格式 yyyy-MM-dd HH:mm:ss + 字段 camelCase(code/msg/data)
|
||||
.AddNewtonsoftJson(o =>
|
||||
{
|
||||
o.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";
|
||||
o.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
|
||||
})
|
||||
// 对应 Java Hibernate Validator
|
||||
.AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<Startup>());
|
||||
|
||||
// 对象映射:对应 Java ModelMapper
|
||||
services.AddAutoMapper(typeof(Startup));
|
||||
|
||||
// 对应 Java @CrossOrigin(originPatterns={"*"}, allowCredentials="true");
|
||||
// AllowAnyOrigin 与 AllowCredentials 不能同时生效,接入 CAS 后按 yh.sso.allow-origins 收紧
|
||||
services.AddCors(options => options.AddPolicy("default", builder => builder
|
||||
.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()));
|
||||
|
||||
// 数据层:默认 SQL Server 主库,DatabaseProvider=MySql 可切换
|
||||
services.AddDataLayer(Configuration);
|
||||
|
||||
// 仓储与 Dapper 通道(对应 Java Repository 自动装配)
|
||||
services.AddRepositories();
|
||||
|
||||
// 系统审计日志过滤器(全量拦截,TypeFilterAttribute 从 DI 解析依赖)
|
||||
services.AddScoped<SysOperationLogFilter>();
|
||||
|
||||
// Redis 缓存(RedisUtil 单例,连接串取 Redis:ConnectionString)
|
||||
services.AddRedisCache(Configuration);
|
||||
|
||||
// 框架期占位实现;业务实现就绪后在 Extensions 中统一替换注册
|
||||
services.AddScoped<ILogService, LogServiceStub>();
|
||||
|
||||
// ===== DocPrint 业务服务(对应旧 BestSchool.Service / BestSchool.DocPrint) =====
|
||||
services.AddScoped<IDictionaryService, DictionaryService>();
|
||||
services.AddScoped<IDocPrintClassifyService, DocPrintClassifyService>();
|
||||
services.AddScoped<IReviewProcessService, ReviewProcessService>();
|
||||
services.AddScoped<IFreeApproveService, FreeApproveService>();
|
||||
services.AddScoped<IWYDocPrintService, WYDocPrintService>();
|
||||
services.AddScoped<IWYSHRoleService, WYSHRoleService>();
|
||||
services.AddScoped<IDocPrintStatisticsService, DocPrintStatisticsService>();
|
||||
services.AddScoped<IFileService, FileService>();
|
||||
|
||||
// ===== 基础平台(yh-platform-net 新框架,对应 Java yh-platform) =====
|
||||
// 聚合注册:/oapi 客户端(Yh:Open)+ 应用签名鉴权(Yh:AppAuth)+ Cookie/CAS 认证(Yh:Sso/Yh:Auth)。
|
||||
// AddYhAuth 内部含 AddYhOpenApi;AddYhPlatform = AddYhAppAuth + AddYhAuth。
|
||||
// 静态门面 YschBaseOpenApi 由宿主启动时自动装配(YschFacadeWiring: IHostedService)。
|
||||
services.AddYhPlatform(Configuration);
|
||||
|
||||
// 教务(SchClass)开放接口客户端 SDK(Yh.SchClass.OpenApi.dll,配置节 yh:schclass:open)
|
||||
services.AddSchClassOpenApi(Configuration);
|
||||
|
||||
// 统一配置管理(YhCore.Common.Config):数据库 / Redis / Swagger / 文件路径
|
||||
services.AddOptions<YhCore.Common.Config.DatabaseConfig>()
|
||||
.Bind(Configuration);
|
||||
services.AddOptions<YhCore.Common.Config.RedisConfig>()
|
||||
.Bind(Configuration.GetSection("Redis"));
|
||||
services.AddOptions<YhCore.Common.Config.SwaggerConfig>()
|
||||
.Bind(Configuration.GetSection("Swagger"));
|
||||
services.AddOptions<YhCore.Common.Config.FileConfig>()
|
||||
.Bind(Configuration);
|
||||
// 静态门面(对齐 Java Config):文件路径由配置注入
|
||||
YhCore.Common.Config.Config.FilePath = Configuration["FILE_PATH"];
|
||||
YhCore.Common.Config.Config.FileWebPath = Configuration["FILE_WEB_PATH"];
|
||||
|
||||
// 接口文档:对应 Java SpringDoc,按渠道分三组(api / mobiapi / oapi)
|
||||
services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("api", new OpenApiInfo { Title = "YhCore API", Version = "v1" });
|
||||
c.SwaggerDoc("mobiapi", new OpenApiInfo { Title = "YhCore MobiApi", Version = "v1" });
|
||||
c.SwaggerDoc("oapi", new OpenApiInfo { Title = "YhCore OApi", Version = "v1" });
|
||||
c.AddSecurityDefinition("Token", new OpenApiSecurityScheme
|
||||
{
|
||||
Name = "Authorization",
|
||||
Type = SecuritySchemeType.ApiKey,
|
||||
In = ParameterLocation.Header,
|
||||
Description = "登录后携带的令牌(对接 CAS SSO 后使用)"
|
||||
});
|
||||
});
|
||||
|
||||
// 任务调度:Quartz 宿主 + Job 模板(对应 Java scheduler 包)
|
||||
services.AddQuartzHosted();
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
// 对应 Java @CrossOrigin(originPatterns={"*"}, allowCredentials="true")
|
||||
app.UseCors("default");
|
||||
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(c =>
|
||||
{
|
||||
c.SwaggerEndpoint("/swagger/api/swagger.json", "API");
|
||||
c.SwaggerEndpoint("/swagger/mobiapi/swagger.json", "MobiApi");
|
||||
c.SwaggerEndpoint("/swagger/oapi/swagger.json", "OApi");
|
||||
});
|
||||
|
||||
// 管道顺序:先路由,再认证;CAS 回调必须在认证阶段消费 ticket 并写入 Cookie。
|
||||
app.UseRouting();
|
||||
|
||||
app.UseMiddleware<ApiAccessLogMiddleware>(); // 0. 接口访问日志(唯一日志出口)
|
||||
app.UseMiddleware<GlobalExceptionMiddleware>(); // 1. 异常兜底(异常摘要并入接口日志)
|
||||
app.UseAuthentication(); // 2. CAS ticket 校验 + Cookie 恢复(Yh.Platform.Auth)
|
||||
app.UseMiddleware<SsoProtectionMiddleware>(); // 3. SSO 路径保护(未认证 → 302 CAS;认证通过后才有用户身份)
|
||||
app.UseYhUserContext(); // 4. 新框架用户上下文:BoaUserContext + UserContextHolder(LoginUser)
|
||||
// 5. 应用间签名(oapi 渠道)。必须排在 UseYhUserContext 之后:
|
||||
// UserContextMiddleware 的 finally 会无条件 Clear UserContextHolder,
|
||||
// 若 AppAuth 在其之前写入 LoginUser{AppKey} 会被清空,业务层读不到应用身份。
|
||||
app.UseMiddleware<AppAuthMiddleware>();
|
||||
app.Use(LegacyUserContextAdapter); // 6. 兼容旧 YhCore.Common.UserContextHolder(业务依赖 UserContext)
|
||||
app.UseAuthorization();
|
||||
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapControllers();
|
||||
|
||||
// 根路径登录状态页:SSO 开启时未登录会先被中间件 302 到 CAS;登录后在此显示当前用户。
|
||||
endpoints.MapGet("/", async context =>
|
||||
{
|
||||
var user = context.User;
|
||||
if (user?.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
context.Response.StatusCode = 401;
|
||||
await context.Response.WriteAsync("未登录(SSO 未开启或 Cookie 失效)");
|
||||
return;
|
||||
}
|
||||
|
||||
var name = user.Identity?.Name ?? "-";
|
||||
var id = user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "-";
|
||||
var school = user.FindFirst("schoolname")?.Value ?? "-";
|
||||
context.Response.ContentType = "text/html; charset=utf-8";
|
||||
await context.Response.WriteAsync(
|
||||
"<html><body style=\"font-family:system-ui;padding:40px\">" +
|
||||
"<h2>YhCore SSO 登录成功</h2>" +
|
||||
$"<p>用户:{System.Net.WebUtility.HtmlEncode(name)}</p>" +
|
||||
$"<p>用户Id:{System.Net.WebUtility.HtmlEncode(id)}</p>" +
|
||||
$"<p>学校:{System.Net.WebUtility.HtmlEncode(school)}</p>" +
|
||||
"<p><a href=\"/swagger/index.html\">打开接口文档</a> | <a href=\"/api/Logout\">退出登录</a></p>" +
|
||||
"</body></html>");
|
||||
});
|
||||
});
|
||||
|
||||
// 数据库自动补表:实体模型里有而数据库没有的表,启动时自动创建。
|
||||
// 开关 Database:AutoCreateMissingTables(默认 true);失败只记录不影响启动。
|
||||
// 放在 Configure 末尾而非 Main:此处 Web 宿主配置/DI 已完整就绪,DbContext 连接串有效。
|
||||
if (Configuration.GetValue("Database:AutoCreateMissingTables", true))
|
||||
{
|
||||
using (var scope = app.ApplicationServices.CreateScope())
|
||||
{
|
||||
try
|
||||
{
|
||||
var created = DatabaseInitializer.EnsureMissingTables(
|
||||
scope.ServiceProvider.GetRequiredService<YHDbContext>());
|
||||
if (created.Count > 0)
|
||||
{
|
||||
Console.WriteLine(
|
||||
"[DatabaseInitializer] auto-created missing table(s): "
|
||||
+ string.Join(", ", created));
|
||||
logger.LogInformation(
|
||||
"[DatabaseInitializer] auto-created missing table(s): {Tables}",
|
||||
string.Join(", ", created));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("[DatabaseInitializer] ERROR: " + ex);
|
||||
logger.LogError(ex,
|
||||
"[DatabaseInitializer] auto-create skipped due to error (app continues)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容适配:把新框架 <see cref="BoaUserContext"/> 的当前用户转写为旧
|
||||
/// UserContextHolder(YhCore.Common.UserContext),保证 LogController /
|
||||
/// SysOperationLogFilter / ApiAccessLogMiddleware 等既有业务代码零改动继续工作。
|
||||
/// SchSerID(学校 UUID)通过教务 SDK SchInfo.GetSchFirstAsync 反查填充,进程内缓存避免每请求调用。
|
||||
/// </summary>
|
||||
private static async System.Threading.Tasks.Task LegacyUserContextAdapter(HttpContext context, Func<System.Threading.Tasks.Task> next)
|
||||
{
|
||||
var boa = BoaUserContext.Get();
|
||||
if (boa != null)
|
||||
{
|
||||
var userContext = new YhCore.Common.UserContext.UserContext
|
||||
{
|
||||
Id = boa.Id ?? 0,
|
||||
UserName = boa.Name,
|
||||
Account = boa.Account,
|
||||
SchoolId = boa.School?.Id,
|
||||
SchoolName = boa.School?.Name,
|
||||
UserType = boa.Type,
|
||||
Role = boa.RoleIds?.FirstOrDefault() ?? boa.CurrentRole?.RoleId,
|
||||
FullBoaUser = boa,
|
||||
};
|
||||
|
||||
if ((userContext.SchoolId ?? 0) > 0)
|
||||
{
|
||||
userContext.SchSerID = await ResolveSchSerIDAsync(context, userContext.SchoolId.Value);
|
||||
}
|
||||
|
||||
YhCore.Common.UserContext.UserContextHolder.Current = userContext;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await next();
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 请求结束清理,避免 AsyncLocal 泄漏到连接池复用线程
|
||||
YhCore.Common.UserContext.UserContextHolder.Current = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>学校 UUID 反查缓存(SchoolId → (值, 缓存时间戳))。成功永久缓存;失败负缓存 60s 后重试。</summary>
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<int, (string Value, long Ticks)> SchSerIdCache =
|
||||
new System.Collections.Concurrent.ConcurrentDictionary<int, (string Value, long Ticks)>();
|
||||
|
||||
/// <summary>失败负缓存有效期(毫秒):教务服务不可达时避免每请求触发,又能在服务恢复后自动重试。</summary>
|
||||
private const long SchSerIdNegativeCacheMs = 60_000;
|
||||
|
||||
/// <summary>
|
||||
/// 通过教务 SDK 反查学校 UUID(SchInfoVo.SchSerId);失败或未命中返回 null。
|
||||
/// 注意:SDK HttpClient 配置 ConnectTimeout=1500ms,教务地址不可达时(如本地联调 BaseHost=localhost:8086
|
||||
/// 未起服务)会抛 TaskCanceledException——属预期路径,捕获后走负缓存,不阻断请求。
|
||||
/// </summary>
|
||||
private static async System.Threading.Tasks.Task<string> ResolveSchSerIDAsync(HttpContext context, int schoolId)
|
||||
{
|
||||
var now = System.DateTime.UtcNow.Ticks;
|
||||
if (SchSerIdCache.TryGetValue(schoolId, out var cached))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(cached.Value))
|
||||
return cached.Value;
|
||||
// 负缓存:有效期内不再重试;过期后放行重新反查(教务服务可能已恢复)
|
||||
if (now - cached.Ticks < SchSerIdNegativeCacheMs * System.TimeSpan.TicksPerMillisecond)
|
||||
return null;
|
||||
}
|
||||
|
||||
string result = null;
|
||||
try
|
||||
{
|
||||
var sch = context.RequestServices.GetService(typeof(Yh.SchClass.OpenApi.SchClassOpenApi))
|
||||
as Yh.SchClass.OpenApi.SchClassOpenApi;
|
||||
if (sch != null)
|
||||
{
|
||||
var info = await sch.SchInfo.GetUserSchFirstAsync(schoolId, System.Threading.CancellationToken.None);
|
||||
result = info?.SchSerId;
|
||||
}
|
||||
}
|
||||
catch (System.OperationCanceledException)
|
||||
{
|
||||
// 连接超时(ConnectTimeout)/ 请求取消:教务不可达,走负缓存,SchSerID 保持 null
|
||||
LogSchSerIdResolve(context, schoolId, "connect timeout");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
LogSchSerIdResolve(context, schoolId, ex.GetType().Name + ": " + ex.Message);
|
||||
}
|
||||
|
||||
SchSerIdCache[schoolId] = (result ?? "", now);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>反查失败诊断日志(Debug 级,避免干扰正常请求日志)。</summary>
|
||||
private static void LogSchSerIdResolve(HttpContext context, int schoolId, string reason)
|
||||
{
|
||||
try
|
||||
{
|
||||
var logger = context.RequestServices.GetService(typeof(Microsoft.Extensions.Logging.ILoggerFactory))
|
||||
as Microsoft.Extensions.Logging.ILoggerFactory;
|
||||
logger?.CreateLogger("YhCore.WebApi.Startup").LogDebug(
|
||||
"Resolve SchSerID for schoolId={SchoolId} failed: {Reason}", schoolId, reason);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 日志失败不影响请求
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<RootNamespace>YhCore.WebApi</RootNamespace>
|
||||
<AssemblyName>YhCore.WebApi</AssemblyName>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- 接口文档:对应 Java SpringDoc OpenAPI -->
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
|
||||
<!-- 对象映射:对应 Java ModelMapper -->
|
||||
<PackageReference Include="AutoMapper" Version="11.0.1" />
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
|
||||
<!-- 参数校验:对应 Java Hibernate Validator -->
|
||||
<PackageReference Include="FluentValidation.AspNetCore" Version="10.3.6" />
|
||||
<!-- 日志落地:仅接口访问日志 -->
|
||||
<PackageReference Include="log4net" Version="2.0.15" />
|
||||
<!-- JSON 序列化:对应 Java Jackson/fastjson -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.17" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\YhCore.Common\YhCore.Common.csproj" />
|
||||
<ProjectReference Include="..\YhCore.Entity\YhCore.Entity.csproj" />
|
||||
<ProjectReference Include="..\YhCore.Model\YhCore.Model.csproj" />
|
||||
<ProjectReference Include="..\YhCore.Repository\YhCore.Repository.csproj" />
|
||||
<ProjectReference Include="..\YhCore.Service\YhCore.Service.csproj" />
|
||||
<ProjectReference Include="..\YhCore.Task\YhCore.Task.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Yh.Platform.Abstractions">
|
||||
<HintPath>..\..\..\Library\Yh.Platform.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Yh.Platform.AppAuth">
|
||||
<HintPath>..\..\..\Library\Yh.Platform.AppAuth.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Yh.Platform.Auth">
|
||||
<HintPath>..\..\..\Library\Yh.Platform.Auth.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Yh.Platform.OpenApi">
|
||||
<HintPath>..\..\..\Library\Yh.Platform.OpenApi.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Yh.SchClass.OpenApi">
|
||||
<HintPath>..\..\..\Library\Yh.SchClass.OpenApi.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"DatabaseProvider": "SqlServer",
|
||||
"Database": {
|
||||
"AutoCreateMissingTables": true
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"SqlServer": "Server=localhost;Database=EduCloudDocPrint;User Id=sa;Password=Password01!;MultipleActiveResultSets=true",
|
||||
"Mysql": "Server=localhost;Port=3306;Database=EduCloudDocPrint;User Id=root;Password=123123;CharSet=utf8mb4"
|
||||
},
|
||||
"Redis": {
|
||||
"ConnectionString": "localhost:6379,defaultDatabase=0"
|
||||
},
|
||||
"yh": {
|
||||
"auth": {
|
||||
"Enabled": true,
|
||||
"CookieExpireMinutes": 1,
|
||||
"cookieName": ".yh.auth"
|
||||
},
|
||||
"sso": {
|
||||
"Enabled": true,
|
||||
"CasServerUrlPrefix": "http://sso.yunhaixingtong.com/cas",
|
||||
"AppBaseUrl": "http://localhost:51835",
|
||||
"SignInCallbackPath": "/signin-cas",
|
||||
"TicketValidateTimeoutMs": 10000,
|
||||
"UrlPattern": "/api/*",
|
||||
"MobiApiPattern": "/mobiapi/*"
|
||||
},
|
||||
"app-auth": {
|
||||
"Enabled": true,
|
||||
"AppKey": "100002",
|
||||
"AppSecret": "2314a3b158b24a838b2b9b69fd3db118",
|
||||
"ExpireMs": 300000
|
||||
},
|
||||
"open": {
|
||||
"Enabled": true,
|
||||
"BaseHost": "http://base.yunhaixingtong.com",
|
||||
"BasePath": "/userbase",
|
||||
"SchClassPath": "/bschclass",
|
||||
"AppKey": "100002",
|
||||
"AppSecret": "2314a3b158b24a838b2b9b69fd3db118"
|
||||
}
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "None",
|
||||
"Microsoft": "None",
|
||||
"Microsoft.Hosting.Lifetime": "None",
|
||||
"Microsoft.AspNetCore.Authentication": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<log4net>
|
||||
<!-- 唯一日志出口:接口访问日志。按天滚动单个文件,不做级别分文件(方案 §6.2) -->
|
||||
<appender name="ApiAccessLogAppender" type="log4net.Appender.RollingFileAppender">
|
||||
<file type="log4net.Util.PatternString" value="logs/api-access-%date{yyyyMMdd}.log" />
|
||||
<appendToFile value="true" />
|
||||
<rollingStyle value="Date" />
|
||||
<datePattern value="yyyyMMdd" />
|
||||
<maxSizeRollBackups value="30" />
|
||||
<layout type="log4net.Layout.PatternLayout">
|
||||
<conversionPattern value="%date{yyyy-MM-dd HH:mm:ss.fff} | %message%newline" />
|
||||
</layout>
|
||||
</appender>
|
||||
|
||||
<root>
|
||||
<level value="ALL" />
|
||||
<appender-ref ref="ApiAccessLogAppender" />
|
||||
</root>
|
||||
</log4net>
|
||||
Reference in New Issue
Block a user