Compare commits

..
33 Commits
Author SHA1 Message Date
ops 695ba1530e fix(ci): 构建容器名唯一化 + npm 走国内镜像源(避免并发互踩 exit 137) 2026-10-21 06:24:25 +00:00
ops c2f6f42a3c ci: apk 源统一走 aliyun(规避 dl-cdn 时段性被拦卡 60s) 2026-10-21 01:36:56 +00:00
wanghu 2b9389fcb8 附件
build / build-push (push) Successful in 1m55s
2026-09-09 20:30:15 +08:00
wanghu 44d3cd0326 去掉小数点
build / build-push (push) Successful in 40s
2026-09-09 19:01:08 +08:00
wanghu 3ff9b48a19 修改地址
build / build-push (push) Successful in 40s
2026-09-09 17:39:30 +08:00
wanghu 20325bed00 签名
build / build-push (push) Successful in 39s
2026-09-09 16:49:55 +08:00
wanghu 0efde94a74 附件
build / build-push (push) Successful in 1m21s
2026-09-09 16:27:17 +08:00
wanghu 4211b72041 Config.FilePath
build / build-push (push) Successful in 1m24s
2026-09-09 16:04:46 +08:00
wanghu e9e1c1722d uploud
build / build-push (push) Successful in 44s
2026-09-09 15:54:50 +08:00
wanghu 583d06f3d0 pdf
build / build-push (push) Successful in 1m55s
2026-09-09 12:07:17 +08:00
wanghu 83182cb538 插件
build / build-push (push) Failing after 58s
2026-09-09 12:02:05 +08:00
wanghu bbcb865055 插件
build / build-push (push) Failing after 21s
2026-09-09 12:01:45 +08:00
wanghu cf8d6da5b5 提交
build / build-push (push) Successful in 28s
2026-09-09 08:57:41 +08:00
wanghu ae1bd4cdb0 注入
build / build-push (push) Successful in 28s
2026-09-08 21:18:29 +08:00
wanghu f1059fd92b 大小写
build / build-push (push) Successful in 33s
2026-09-08 17:42:25 +08:00
wanghu 5785a2c10e 更新大小写
build / build-push (push) Successful in 28s
2026-09-08 17:29:56 +08:00
wanghu 10c791d034 编辑
build / build-push (push) Successful in 28s
2026-09-08 16:55:39 +08:00
wanghu 04e708c4fe Merge branch 'main' of http://gitea.yunhaixingtong.com/docprint/DocprintService
build / build-push (push) Successful in 1m49s
2026-09-08 15:06:44 +08:00
wanghu d658ab5b6a 服务层列表接口返回 PageVO(配合控制器分页改造) 2026-09-08 15:05:20 +08:00
wanghu ae8327807e 1 2026-09-08 15:05:00 +08:00
wanghu 74ba80ddf9 1
build / build-push (push) Failing after 1m23s
2026-09-08 15:00:25 +08:00
wanghu 6ff1d07f31 组
build / build-push (push) Failing after 1m36s
2026-09-08 14:48:44 +08:00
wanghu 7e945d310d 1
build / build-push (push) Successful in 57s
2026-09-07 19:31:54 +08:00
wanghu 3e9be2bd54 11
build / build-push (push) Successful in 31s
2026-09-07 18:13:51 +08:00
wanghu 7112b7ed40 缓存schserid
build / build-push (push) Successful in 1m20s
2026-09-07 16:38:59 +08:00
wanghu f711f1c5e5 s
build / build-push (push) Successful in 1m23s
2026-09-07 15:47:20 +08:00
wanghu 6d5df72d2f code
build / build-push (push) Successful in 31s
2026-09-07 15:02:14 +08:00
wanghu 0225df144c 大小写
build / build-push (push) Successful in 1m29s
2026-09-07 14:39:18 +08:00
wanghu 461847e4ed sw
build / build-push (push) Successful in 28s
2026-09-07 10:15:23 +08:00
wanghu 951ed3f00a FromForm
build / build-push (push) Successful in 27s
2026-09-07 09:29:47 +08:00
wanghu 7471dbe66f GetInitUrl
build / build-push (push) Successful in 29s
2026-09-07 08:54:43 +08:00
wanghu 5972f3b7e1 debug
build / build-push (push) Successful in 27s
2026-09-07 08:19:37 +08:00
wanghu 8abba50e0e 添加deugUser 2026-09-05 16:17:42 +08:00
39 changed files with 2512 additions and 360 deletions
+17 -8
View File
@@ -26,7 +26,10 @@ jobs:
options: --network infra-infra-net
steps:
- name: 安装 git / curl(alpine)
run: apk add --no-cache git curl
run: |
# 统一走 aliyun 源:dl-cdn 时段性被拦会卡满 60s×2(2026-10-20 事故)
sed -i 's#dl-cdn.alpinelinux.org#mirrors.aliyun.com#g' /etc/apk/repositories
apk add --no-cache git curl
- name: 拉取源码(内网无 GitHub,手动 clone 本仓库)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -53,14 +56,20 @@ jobs:
- name: 构建 publish 产物(dotnet sdk 容器,复用宿主 nuget 缓存)
run: |
set -e
docker rm -f dotnetbuild 2>/dev/null || true
docker run -d --name dotnetbuild \
# 构建容器名必须全局唯一:runner 挂的是宿主 docker.sock,容器名在整个 daemon 上共享
# 命名空间。固定名会在多仓/多条 workflow 并发时互相 docker rm -f 掉对方正在构建的
# 容器,表现为无编译报错的 exit 137(SIGKILL,2026-10-21 实测复现)。
# GITHUB_RUN_ID 实例级唯一,再拼时间戳兜底。
BUILD_CONTAINER="dotnetbuild-${GITHUB_RUN_ID:-0}-$(date +%s)"
echo "BUILD_CONTAINER=${BUILD_CONTAINER}"
docker rm -f "$BUILD_CONTAINER" 2>/dev/null || true
docker run -d --name "$BUILD_CONTAINER" \
--network infra-infra-net \
-v /opt/docker/projman-dotnet-nuget:/root/.nuget/packages \
-w /build mcr.microsoft.com/dotnet/sdk:5.0 sleep 3600
docker cp "${PWD}/." dotnetbuild:/build/
docker exec dotnetbuild sh -c "find /build -maxdepth 2 -iname nuget.config -exec sed -i 's|//nexus.yunhaixingtong.com|//nexus:8081|g' {} +"
docker exec dotnetbuild sh -c '
docker cp "${PWD}/." "$BUILD_CONTAINER":/build/
docker exec "$BUILD_CONTAINER" sh -c "find /build -maxdepth 2 -iname nuget.config -exec sed -i 's|//nexus.yunhaixingtong.com|//nexus:8081|g' {} +"
docker exec "$BUILD_CONTAINER" sh -c '
set -e
root_sln=$(ls /build/*.sln 2>/dev/null | head -1 || true)
root_csproj=$(ls /build/*.csproj 2>/dev/null | head -1 || true)
@@ -88,8 +97,8 @@ jobs:
echo "publish target: $target"
'
mkdir -p "${PWD}/publish"
docker cp dotnetbuild:/app/. "${PWD}/publish/"
docker rm -f dotnetbuild
docker cp "$BUILD_CONTAINER":/app/. "${PWD}/publish/"
docker rm -f "$BUILD_CONTAINER"
# 入口脚本:runtimeconfig.json 是可执行项目唯一标志(X.runtimeconfig.json <-> X.dll),
# 类库 deps.json 无 runtimeconfig 兄弟文件,天然不会误选
entry=$(ls "${PWD}"/publish/*.runtimeconfig.json | head -1)
-32
View File
@@ -1,32 +0,0 @@
# ============ 多阶段构建:.NET 5.0 ============
# 构建阶段
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
# 复制解决方案与全部项目文件(利用层缓存)
COPY YHDocPrintCore.sln* ./
COPY src/*/*.csproj ./
RUN for f in src/*/*.csproj; do \
mkdir -p "$(dirname "$f")" && mv "$f" "$(dirname "$f")/"; \
done \
&& dotnet restore YHDocPrintCore.sln
# 复制源码并发布
COPY src/ src/
RUN dotnet publish src/YHDocPrintCore.WebApi/YHDocPrintCore.WebApi.csproj \
-c Release -o /app/publish --no-restore
# 运行阶段
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
# 日志目录(接口访问日志唯一输出,挂载卷便于采集)
RUN mkdir -p /app/logs
VOLUME ["/app/logs"]
EXPOSE 80
ENV ASPNETCORE_URLS=http://+:80
# 生产配置通过环境变量注入(ConnectionStrings__SqlServer / Redis:ConnectionString 等)
ENTRYPOINT ["dotnet", "YHDocPrintCore.WebApi.dll"]
-53
View File
@@ -1,53 +0,0 @@
# DocprintService
> 本仓库由 projman 自动创建并注入 CI(`.gitea/workflows/build.yml`)与 Dockerfile(`docker/Dockerfile`)。
> 推送代码到 main 分支即自动构建镜像并推送到 Nexus,projman 收到通知后建档待部署,由运维在 projman「后端发布」手动点部署。
## 首次推送本地代码到本仓库
> 目标:把本地已有代码推上去,同时**保留** projman 注入的文件(`build.yml`、`Dockerfile`)。
```bash
# 1. 进入本地代码目录
cd /your/local/code/dir
# 2. 初始化 git(目录已有 .git 也安全,git init 幂等无副作用)
git init
git branch -M main
# 3. 先把本地代码提交成一个 commit(关键!不提交直接 merge 会丢失本地代码)
git add -A
git commit -m "init local code"
# 4. 关联远程仓库(幂等:已存在 origin 则改地址,不报错)
git remote add origin http://gitea.yunhaixingtong.com/docprint/DocprintService.git 2>/dev/null \
|| git remote set-url origin http://gitea.yunhaixingtong.com/docprint/DocprintService.git
# 5. 拉取远程信息(只下载,不合并)
git fetch origin
# 6. 合并远程 main(关键步骤)
# 本地与远程是两条独立历史,需允许合并不相关历史。
# 此步会把 projman 注入的 build.yml 与 Dockerfile 合并进你的本地工作区。
git merge origin/main --allow-unrelated-histories -m "merge projman injected files"
# 7. 查看状态(确认 .gitea/workflows/build.yml、docker/Dockerfile 已在本地)
git status
# 8. 提交并推送
git add -A
git commit -m "sync local code" 2>/dev/null || true
git push origin main
```
## 推送后验证
push 触发 CI 构建,前往 Gitea 仓库 → **Actions** 查看日志;构建成功后镜像推送到 Nexus,projman 建档待部署,在 projman 应用详情「后端发布」手动点部署。
## 禁止写法(会导致注入文件被删)
`git reset origin/main` + `git add -A` 会把远程已注入、但你本地没有的文件(`build.yml`、`Dockerfile`)当作"删除"提交并推送,导致这些文件被删、CI 失败。务必用 `git merge` 而非 `git reset`,且 merge 前先提交本地代码。
## 关于 Dockerfile
projman 注入的 Dockerfile 仅在仓库不存在时写入;你自备的 Dockerfile 优先,不会被覆盖。
+3 -1
View File
@@ -8,10 +8,12 @@ FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION}
# .NET 5 基础镜像基于 Debian 10(已 EOL),apt 源须切 archive.debian.org 才能装包
# curl 供 projman compose healthcheck(test: CMD curl -f ...)使用;tzdata 设上海时区
# wkhtmltopdf 供审批单 PDF 接口(DwonWYSQD)本地转换 HTML(Debian 源为 0.12.5,无需 --enable-local-file-access);
# fonts-wqy-zenhei 提供中文字体(缺失则 PDF 中文显示方块)
RUN sed -i 's|deb.debian.org|archive.debian.org|g; s|security.debian.org|archive.debian.org|g' /etc/apt/sources.list \
&& echo 'Acquire::Check-Valid-Until "false";' > /etc/apt/apt.conf.d/99no-valid-until \
&& apt-get update \
&& apt-get install -y --no-install-recommends curl tzdata \
&& apt-get install -y --no-install-recommends curl tzdata wkhtmltopdf fonts-wqy-zenhei \
&& ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& rm -rf /var/lib/apt/lists/*
+3
View File
@@ -21,5 +21,8 @@ namespace YhCore.Common.Config
/// <summary>文件访问 URL 前缀(对应 Java FILE_WEB_PATH,由 Startup 注入)</summary>
public static string FileWebPath { get; set; }
/// <summary>wkhtmltopdf 可执行文件路径(审批单 PDF 用;空则用 PATH 中的 wkhtmltopdf,由 Startup 注入)</summary>
public static string WkHtmlToPdfPath { get; set; }
}
}
+7 -2
View File
@@ -1,21 +1,26 @@
using Newtonsoft.Json;
using Yh.Platform.Abstractions;
namespace YhCore.Common.Result
{
/// <summary>
/// 统一响应(对应 Java yh-api-std:com.yunhai.platform.apistd.ApiResponse)。
/// 字段与 Java 对齐:code/msg/data(Newtonsoft CamelCase 序列化输出小写)。
/// 信封字段 code/msg/data 以 [JsonProperty] 显式钉死小写——全局序列化已改为
/// DefaultContractResolver 原样输出(data 内容对齐旧项目混合大小写字段),信封不受影响。
/// 注意:Controller 返回值由全局过滤器 GlobalWrapResultFilter 自动包装;
/// 不需要包装的接口用 [NoWrapResult] 特性显式排除(CAS 协议端点、文件下载等)。
/// </summary>
public class ApiResult<T>
{
/// <summary>0=成功,非 0=失败(对齐 Java ErrorCode)</summary>
/// <summary>0=成功,非 0=失败(对齐 Java ErrorCode )</summary>
[JsonProperty("code")]
public int Code { get; set; }
/// <summary>提示消息(对齐 Java msg)</summary>
[JsonProperty("msg")]
public string Msg { get; set; }
[JsonProperty("data")]
public T Data { get; set; }
public static ApiResult<T> Ok(T data) => new() { Code = ErrorCode.Success.Code, Msg = "", Data = data };
+1 -1
View File
@@ -17,7 +17,7 @@
<ItemGroup>
<!-- 平台模型与信封(DException/ErrorCode/BoaUser 等):从 Nexus nuget-group 引用(原为本地源项目引用) -->
<PackageReference Include="Yh.Platform.Abstractions" Version="1.0.0-preview.1" />
<PackageReference Include="Yh.Platform.Abstractions" Version="1.0.0-preview.2" />
</ItemGroup>
</Project>
+1 -13
View File
@@ -68,22 +68,10 @@ namespace YhCore.Model.Dto
/// <summary>全部字典类别(typeID, typeName),对齐旧 EnumConfig.Dictionary 的 Description。</summary>
public static readonly (string Id, string Name)[] AllTypes =
{
("DEPT", "部门类别"),
("BXBM", "报修部门"),
("WYFL", "文印分类"),
("YZBM", "印制版面"),
("YZFL", "印制分类"),
("LeaveType", "请假类别"),
("FoodType", "物品类别"),
("Unit", "单位"),
("VenueType", "场馆类别"),
("Vefacilities", "场馆设施"),
("Time", "场馆时间"),
("Repeat", "场馆重复"),
("LevelOrder", "奖次"),
("filetype", "文件类型"),
("doclevel", "公文级别"),
("XKCategory", "选课类型"),
("filetype", "文件类型")
};
/// <summary>GetSysDic 的 Type(int) → typeID;越界返回 null。</summary>
+2 -2
View File
@@ -2,7 +2,7 @@ namespace YhCore.Model.Dto
{
/// <summary>
/// 文件信息(旧 BestSchool.Entity.FileObj,GetEnclosure/FileSave/FileSaveImage/FileMergeSave 共用)。
/// fileurl 为相对路径(如 /UploadFile/docprint/20260101/{guid}.jpg,与旧库数据格式一致);
/// fileurl 为相对路径(如 /upload/docprint/20260101/{guid}.jpg,与旧库数据格式一致);
/// host 为文件访问主机前缀(旧 AuthConfig.weboa_url,新 Config.FileWebPath)。
/// </summary>
public class FileObj
@@ -19,7 +19,7 @@ namespace YhCore.Model.Dto
/// <summary>文件真实名称(含扩展名)。</summary>
public string filename { get; set; }
/// <summary>相对访问路径(如 /UploadFile/docprint/20260101/{guid}.jpg)。</summary>
/// <summary>相对访问路径(如 /upload/docprint/20260101/{guid}.jpg)。</summary>
public string fileurl { get; set; }
/// <summary>文件主机前缀(Config.FileWebPath)。</summary>
+7
View File
@@ -19,6 +19,13 @@ namespace YhCore.Repository
Task<bool> AnyAsync(Expression<Func<T, bool>> predicate);
Task<long> CountAsync(Expression<Func<T, bool>> predicate = null);
// ---- 读取(追踪版)----
// 查出的实体受变更追踪器管理,修改属性后直接 SaveChangesAsync 即可落库。
// 仅供写场景使用,纯查询请用上面的 NoTracking 版本。
Task<List<T>> FindTrackedAsync(Expression<Func<T, bool>> predicate);
Task<List<T>> GetAllTrackedAsync();
Task<T> FirstOrDefaultTrackedAsync(Expression<Func<T, bool>> predicate);
/// <summary>分页查询(页码从 0 开始),对应 PageUtil + PageVO</summary>
Task<PageVO<T>> PageAsync(Expression<Func<T, bool>> predicate, int page, int size,
Expression<Func<T, object>> orderBy = null, bool asc = true);
+15
View File
@@ -43,6 +43,21 @@ namespace YhCore.Repository
return await DbSet.AsNoTracking().FirstOrDefaultAsync(predicate);
}
public virtual async Task<List<T>> FindTrackedAsync(Expression<Func<T, bool>> predicate)
{
return await DbSet.Where(predicate).ToListAsync();
}
public virtual async Task<List<T>> GetAllTrackedAsync()
{
return await DbSet.ToListAsync();
}
public virtual async Task<T> FirstOrDefaultTrackedAsync(Expression<Func<T, bool>> predicate)
{
return await DbSet.FirstOrDefaultAsync(predicate);
}
public virtual async Task<bool> AnyAsync(Expression<Func<T, bool>> predicate)
{
return await DbSet.AnyAsync(predicate);
+149
View File
@@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using OfficeOpenXml;
using OfficeOpenXml.Style;
namespace YhCore.Service.Excel
{
/// <summary>
/// EPPlus 多 Sheet 导出(移植自旧 Bsch.Common/DocPrint/ExcelHelper,
/// 保留 RenderToExcelBytes + ExportDynamicSheet:动态列、大标题、自定义列宽、打印优化)。
/// 注意:EPPlus 实际产出 xlsx 字节流,旧系统文件名沿用 .xls,Excel 可正常打开,保持原契约。
/// </summary>
public class ExcelHelper
{
/// <summary>导出多 Sheet Excel 字节流(listdt/listsheetname/listtitle 按下标一一对应)</summary>
public static byte[] RenderToExcelBytes(string fileName, List<DataTable> listdt, List<string> listsheetname, List<string> listtitle, List<double> listcolwidth = null)
{
try
{
var package = new ExcelPackage();
int n = 0;
foreach (var dt in listdt)
{
string sheetname = "Sheet" + (n + 1).ToString();
if (n < listsheetname.Count) sheetname = listsheetname[n];
string title = "";
if (n < listtitle.Count) title = listtitle[n];
var ws1 = package.Workbook.Worksheets.Add(sheetname);
ExportDynamicSheet(ws1, dt, title, "A", listcolwidth);
n++;
}
return package.GetAsByteArray();
}
catch (Exception) { return Array.Empty<byte>(); }
}
#region 动态列 Sheet 导出(通用方法)
/// <summary>
/// 导出 动态列 + 自定义表头 + 自定义页码 + 打印优化
/// </summary>
public static void ExportDynamicSheet(ExcelWorksheet ws, DataTable dt, string sheetTitle, string printMark, List<double> listcolwidth = null)
{
// -------------------------- 行定义 --------------------------
int titleRow = 1; // 大标题行
int headerRow = 2; // 列标题行
int dataStartRow = 3; // 数据开始行
int colCount = dt.Columns.Count;
int rowCount = dt.Rows.Count;
// -------------------------- 1. 自定义大标题(合并行) --------------------------
if (sheetTitle != "")
{
ws.Cells[titleRow, 1].Value = sheetTitle;
ws.Cells[titleRow, 1, titleRow, colCount].Merge = true; // 合并单元格
// 标题样式
var titleStyle = ws.Cells[titleRow, 1].Style;
titleStyle.Font.Size = 12;
titleStyle.Font.Bold = true;
titleStyle.HorizontalAlignment = ExcelHorizontalAlignment.Center;
titleStyle.VerticalAlignment = ExcelVerticalAlignment.Center;
ws.Row(titleRow).Height = 32;
}
else
{
headerRow = 1; // 列标题行
dataStartRow = 2; // 数据开始行
}
// -------------------------- 2. 动态列 自动填充表头 --------------------------
for (int c = 0; c < colCount; c++)
{
ws.Cells[headerRow, c + 1].Value = dt.Columns[c].ColumnName;
}
// -------------------------- 3. 动态列 自动填充数据 --------------------------
for (int r = 0; r < rowCount; r++)
{
for (int c = 0; c < colCount; c++)
{
ws.Cells[dataStartRow + r, c + 1].Value = dt.Rows[r][c];
}
}
// -------------------------- 4. 自动换行 + 行高自适应(关键) --------------------------
var dataRange = ws.Cells[headerRow, 1, dataStartRow + rowCount - 1, colCount];
dataRange.Style.Font.Size = 10;
dataRange.Style.WrapText = true;
dataRange.Style.VerticalAlignment = ExcelVerticalAlignment.Center;
dataRange.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
// 加细边框
dataRange.Style.Border.Top.Style = ExcelBorderStyle.Thin;
dataRange.Style.Border.Bottom.Style = ExcelBorderStyle.Thin;
dataRange.Style.Border.Left.Style = ExcelBorderStyle.Thin;
dataRange.Style.Border.Right.Style = ExcelBorderStyle.Thin;
// 自动计算:所有列平均撑满 A4 宽度
if (listcolwidth != null)
{
int colnum = listcolwidth.Count > colCount ? colCount : listcolwidth.Count;
for (int i = 1; i <= colnum; i++)
{
ws.Column(i).Width = listcolwidth[i - 1];
}
}
else
{
double totalWidth = 58;
double perColWidth = (double)totalWidth / colCount;
for (int i = 1; i <= colCount; i++)
{
ws.Column(i).Width = perColWidth;
}
}
// 行高自适应(EPPlus 4.x 兼容)
for (int r = headerRow; r <= headerRow + rowCount - 1; r++)
{
ws.Row(r).CustomHeight = true;
}
// -------------------------- 5. 表头样式 --------------------------
var headerRange = ws.Cells[headerRow, 1, headerRow, colCount];
headerRange.Style.Font.Bold = true;
ws.Row(headerRow).Height = 25;
// -------------------------- 6. 打印:每页重复标题行 --------------------------
ws.PrinterSettings.RepeatRows = new ExcelAddress($"{headerRow}:{headerRow}");
// -------------------------- 7. 自定义页码(中文样式 + 字体大小) --------------------------
// &P=当前页 &N=总页 12号字体 居中
ws.HeaderFooter.OddFooter.CenteredText = "&14 第 &P 页 / 共 &N 页";
// -------------------------- 8. 打印格式优化 --------------------------
ws.PrinterSettings.PaperSize = ePaperSize.A4;
ws.PrinterSettings.Orientation = eOrientation.Portrait;
ws.PrinterSettings.LeftMargin = 0.4m;
ws.PrinterSettings.RightMargin = 0.4m;
ws.PrinterSettings.TopMargin = 0.8m;
ws.PrinterSettings.BottomMargin = 1.2m;
}
#endregion
}
}
+404
View File
@@ -0,0 +1,404 @@
using System;
using System.Data;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using NPOI.HSSF.UserModel;
using NPOI.SS.Formula.Eval;
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
namespace YhCore.Service.Excel
{
/// <summary>
/// 使用 NPOI 操作 Excel,无需 Office COM 组件(移植自旧 Bsch.Common/DocPrint/ExcelRender,
/// 仅保留 6 个 WYSHRole Excel 接口用到的导出/导入成员;控制器用 File() 包装下载)。
/// </summary>
public class ExcelRender
{
static ExcelRender()
{
// ExportDT 列宽按 GB2312 字节长计算:net5 需注册 CodePages 提供程序后才能 GetEncoding(936)
Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
}
#region DataTable 导出为 .xls 字节流
/// <summary>
/// DataTable 导出为 Excel 的 MemoryStream(HSSF .xls;65535 行自动分 sheet;列宽按 GB2312 字节长,上限 50)
/// </summary>
static MemoryStream ExportDT(DataTable dtSource, string strHeaderText)
{
HSSFWorkbook workbook = new HSSFWorkbook();
HSSFSheet sheet = workbook.CreateSheet() as HSSFSheet;
HSSFCellStyle dateStyle = workbook.CreateCellStyle() as HSSFCellStyle;
HSSFDataFormat format = workbook.CreateDataFormat() as HSSFDataFormat;
dateStyle.DataFormat = format.GetFormat("yyyy-mm-dd");
//取得列宽
int[] arrColWidth = new int[dtSource.Columns.Count];
foreach (DataColumn item in dtSource.Columns)
{
arrColWidth[item.Ordinal] = Encoding.GetEncoding(936).GetBytes(item.ColumnName.ToString()).Length;
}
for (int i = 0; i < dtSource.Rows.Count; i++)
{
for (int j = 0; j < dtSource.Columns.Count; j++)
{
int intTemp = Encoding.GetEncoding(936).GetBytes(dtSource.Rows[i][j].ToString()).Length;
if (intTemp > arrColWidth[j])
{
arrColWidth[j] = intTemp;
if (intTemp >= 50)
arrColWidth[j] = 50;
}
}
}
int rowIndex = 0;
foreach (DataRow row in dtSource.Rows)
{
#region 新建表,填充表头,填充列头,样式
if (rowIndex == 65535 || rowIndex == 0)
{
if (rowIndex != 0)
{
sheet = workbook.CreateSheet() as HSSFSheet;
}
#region 列头及样式
{
HSSFRow headerRow = sheet.CreateRow(0) as HSSFRow;
HSSFCellStyle headStyle = workbook.CreateCellStyle() as HSSFCellStyle;
headStyle.Alignment = NPOI.SS.UserModel.HorizontalAlignment.Center;
HSSFFont font = workbook.CreateFont() as HSSFFont;
font.FontHeightInPoints = 10;
font.Boldweight = 700;
headStyle.SetFont(font);
foreach (DataColumn column in dtSource.Columns)
{
headerRow.CreateCell(column.Ordinal).SetCellValue(column.ColumnName);
headerRow.GetCell(column.Ordinal).CellStyle = headStyle;
//设置列宽
sheet.SetColumnWidth(column.Ordinal, (arrColWidth[column.Ordinal] + 1) * 256);
}
}
#endregion
rowIndex = 1;
}
#endregion
#region 填充内容
HSSFRow dataRow = sheet.CreateRow(rowIndex) as HSSFRow;
foreach (DataColumn column in dtSource.Columns)
{
HSSFCell newCell = dataRow.CreateCell(column.Ordinal) as HSSFCell;
string drValue = row[column].ToString();
switch (column.DataType.ToString())
{
case "System.String": //字符串类型
newCell.SetCellValue(drValue);
break;
case "System.DateTime": //日期类型
DateTime dateV;
DateTime.TryParse(drValue, out dateV);
newCell.SetCellValue(dateV);
newCell.CellStyle = dateStyle; //格式化显示
break;
case "System.Boolean": //布尔型
bool boolV = false;
bool.TryParse(drValue, out boolV);
newCell.SetCellValue(boolV);
break;
case "System.Int16": //整型
case "System.Int32":
case "System.Int64":
case "System.Byte":
int intV = 0;
int.TryParse(drValue, out intV);
newCell.SetCellValue(intV);
break;
case "System.Decimal": //浮点型
case "System.Double":
double doubV = 0;
double.TryParse(drValue, out doubV);
newCell.SetCellValue(doubV);
break;
case "System.DBNull": //空值处理
newCell.SetCellValue("");
break;
default:
newCell.SetCellValue("");
break;
}
}
#endregion
rowIndex++;
}
using (MemoryStream ms = new MemoryStream())
{
workbook.Write(ms);
ms.Flush();
ms.Position = 0;
return ms;
}
}
/// <summary>导出 Excel 字节流(供 Service 层返回 byte[],控制器包 File 下载)</summary>
public static byte[] RenderToExcelBytes(DataTable table, string fileName)
{
try { return ExportDT(table, null).ToArray(); }
catch (Exception) { return Array.Empty<byte>(); }
}
#endregion
#region 读取 excel 到 DataTable
/// <summary>Excel 文档流是否有数据(默认第一个 sheet)</summary>
public static bool HasData(Stream excelFileStream, string fileExt)
{
return HasData(excelFileStream, 0, fileExt);
}
public static bool HasData(Stream excelFileStream, int sheetIndex, string fileExt)
{
using (excelFileStream)
{
IWorkbook workbook = null;
if (fileExt == ".xls")
{
workbook = new HSSFWorkbook(excelFileStream);
}
else if (fileExt == ".xlsx")
{
workbook = new XSSFWorkbook(excelFileStream);
}
{
if (workbook.NumberOfSheets > 0)
{
if (sheetIndex < workbook.NumberOfSheets)
{
ISheet sheet = workbook.GetSheetAt(sheetIndex);
{
return sheet.PhysicalNumberOfRows > 0;
}
}
}
}
}
return false;
}
/// <summary>
/// Excel 文档流转换成 DataTable
/// </summary>
/// <param name="excelFileStream">Excel 文档流</param>
/// <param name="sheetIndex">表索引号,如第一个表为 0</param>
/// <param name="headerRowIndex">标题行索引号,如第一行为 0</param>
public static DataTable RenderFromExcel(Stream excelFileStream, int sheetIndex, int headerRowIndex, string fileExt)
{
DataTable table = null;
using (excelFileStream)
{
IWorkbook workbook = null;
if (fileExt == ".xls")
{
workbook = new HSSFWorkbook(excelFileStream);
}
else if (fileExt == ".xlsx")
{
workbook = new XSSFWorkbook(excelFileStream);
}
ISheet sheet = workbook.GetSheetAt(sheetIndex);
{
table = ImportDt(sheet, headerRowIndex, true);
}
}
return table;
}
/// <summary>将指定 sheet 中的数据导出到 datatable 中(第一行为列头)</summary>
static DataTable ImportDt(ISheet sheet, int HeaderRowIndex, bool needHeader)
{
DataTable table = new DataTable();
IRow headerRow;
int cellCount;
try
{
if (HeaderRowIndex < 0 || !needHeader)
{
headerRow = sheet.GetRow(0);
cellCount = headerRow.LastCellNum;
for (int i = headerRow.FirstCellNum; i <= cellCount; i++)
{
DataColumn column = new DataColumn(Convert.ToString(i));
table.Columns.Add(column);
}
}
else
{
headerRow = sheet.GetRow(HeaderRowIndex);
cellCount = headerRow.LastCellNum;
for (int i = headerRow.FirstCellNum; i < cellCount; i++)
{
if (headerRow.GetCell(i) == null)
{
if (table.Columns.IndexOf(Convert.ToString(i)) > 0)
{
DataColumn column = new DataColumn(Convert.ToString("重复列名" + i));
table.Columns.Add(column);
}
else
{
DataColumn column = new DataColumn(Convert.ToString(i));
table.Columns.Add(column);
}
}
else if (table.Columns.IndexOf(headerRow.GetCell(i).ToString()) > 0)
{
DataColumn column = new DataColumn(Convert.ToString("重复列名" + i));
table.Columns.Add(column);
}
else
{
DataColumn column = new DataColumn(headerRow.GetCell(i).ToString());
table.Columns.Add(column);
}
}
}
int rowCount = sheet.LastRowNum;
for (int i = (HeaderRowIndex + 1); i <= sheet.LastRowNum; i++)
{
try
{
IRow row;
if (sheet.GetRow(i) == null)
{
row = sheet.CreateRow(i);
}
else
{
row = sheet.GetRow(i);
}
DataRow dataRow = table.NewRow();
for (int j = row.FirstCellNum; j < cellCount; j++)
{
try
{
if (row.GetCell(j) != null)
{
switch (row.GetCell(j).CellType)
{
case CellType.String:
string str = row.GetCell(j).StringCellValue;
if (str != null && str.Length > 0)
{
dataRow[j] = str.ToString();
}
else
{
dataRow[j] = null;
}
break;
case CellType.Numeric:
if (DateUtil.IsCellDateFormatted(row.GetCell(j)))
{
dataRow[j] = DateTime.FromOADate(row.GetCell(j).NumericCellValue);
}
else
{
dataRow[j] = Convert.ToDouble(row.GetCell(j).NumericCellValue);
}
break;
case CellType.Boolean:
dataRow[j] = Convert.ToString(row.GetCell(j).BooleanCellValue);
break;
case CellType.Error:
dataRow[j] = ErrorEval.GetText(row.GetCell(j).ErrorCellValue);
break;
case CellType.Formula:
switch (row.GetCell(j).CachedFormulaResultType)
{
case CellType.String:
string strFORMULA = row.GetCell(j).StringCellValue;
if (strFORMULA != null && strFORMULA.Length > 0)
{
dataRow[j] = strFORMULA.ToString();
}
else
{
dataRow[j] = null;
}
break;
case CellType.Numeric:
dataRow[j] = Convert.ToString(row.GetCell(j).NumericCellValue);
break;
case CellType.Boolean:
dataRow[j] = Convert.ToString(row.GetCell(j).BooleanCellValue);
break;
case CellType.Error:
dataRow[j] = ErrorEval.GetText(row.GetCell(j).ErrorCellValue);
break;
default:
dataRow[j] = "";
break;
}
break;
default:
dataRow[j] = "";
break;
}
}
}
catch (Exception)
{
}
}
table.Rows.Add(dataRow);
}
catch (Exception)
{
}
}
}
catch (Exception)
{
}
return table;
}
#endregion
public static byte[] StreamToBytes(Stream stream)
{
byte[] bytes = new byte[stream.Length];
stream.Read(bytes, 0, bytes.Length);
// 设置当前流的位置为流的开始
stream.Seek(0, SeekOrigin.Begin);
return bytes;
}
}
}
+2 -2
View File
@@ -125,7 +125,7 @@ namespace YhCore.Service.Impl
throw new DException("已存在该分类,请重新填写!");
}
var obj = await _dic.FirstOrDefaultAsync(x => x.SerID == model.SerID);
var obj = await _dic.FirstOrDefaultTrackedAsync(x => x.SerID == model.SerID);
if (obj == null) throw new DException("未找到要更新的分类!");
obj.Name = model.Name;
@@ -141,7 +141,7 @@ namespace YhCore.Service.Impl
{
if (model == null) throw new DException("参数错误!");
var current = await _dic.FirstOrDefaultAsync(m => m.SerID == model.SerID);
var current = await _dic.FirstOrDefaultTrackedAsync(m => m.SerID == model.SerID);
if (current == null) throw new DException("未找到要编辑的分类!");
var dup = await _dic.AnyAsync(m =>
@@ -119,7 +119,7 @@ namespace YhCore.Service.Impl
{
if (ids == null || ids.Length == 0) throw new DException("ids is not null");
var items = await _dic.FindAsync(m => ids.Contains(m.SerID));
var items = await _dic.FindTrackedAsync(m => ids.Contains(m.SerID));
if (items.Count == 0) throw new DException("未找到要删除的分类!");
foreach (var item in items)
@@ -189,7 +189,7 @@ namespace YhCore.Service.Impl
{
if (ids == null || ids.Length == 0) throw new DException("ids is not null");
var items = await _dic.FindAsync(m => ids.Contains(m.SerID.ToString()));
var items = await _dic.FindTrackedAsync(m => ids.Contains(m.SerID.ToString()));
foreach (var item in items)
{
item.Enable = 3;
@@ -200,7 +200,7 @@ namespace YhCore.Service.Impl
public async Task SetDictionaryOrdAsync(string schSerId, int dicSerId, bool show)
{
var obj = await _ord.FirstOrDefaultAsync(m => m.DicSerID == dicSerId && m.SchSerID == schSerId);
var obj = await _ord.FirstOrDefaultTrackedAsync(m => m.DicSerID == dicSerId && m.SchSerID == schSerId);
if (obj == null)
{
await _ord.AddAsync(new Common_DictionaryOrd
@@ -224,7 +224,7 @@ namespace YhCore.Service.Impl
if (string.IsNullOrEmpty(ids)) throw new DException("ids is not null");
var ssids = ids.Split(',').Select(a => int.Parse(a)).ToArray();
var list = await _ord.FindAsync(m => m.SchSerID == schSerId);
var list = await _ord.FindTrackedAsync(m => m.SchSerID == schSerId);
var i = 0;
foreach (var id in ssids)
@@ -274,7 +274,7 @@ namespace YhCore.Service.Impl
public async Task SetSchZyBitAsync(string schSerId, bool zyBit)
{
var obj = await _zybit.FirstOrDefaultAsync(a => a.SchSerID == schSerId);
var obj = await _zybit.FirstOrDefaultTrackedAsync(a => a.SchSerID == schSerId);
if (obj == null)
{
await _zybit.AddAsync(new WY_SchZyBit { SchSerID = schSerId, ziyuanbit = zyBit });
+15 -9
View File
@@ -68,10 +68,9 @@ namespace YhCore.Service.Impl
if (file == null || file.Length == 0)
throw new DException("上传失败!");
if (appType.ToLower() == "noticemsg") appType = "notice";
string dtStr = DateTime.Now.ToString("yyyyMMdd");
string physicalDir = Path.Combine(StorageRoot(), appType, dtStr);
string outFilePath = "/UploadFile/" + appType + "/" + dtStr + "/";
string outFilePath = appType + "/" + dtStr + "/";
if (!Directory.Exists(physicalDir))
Directory.CreateDirectory(physicalDir);
@@ -109,7 +108,7 @@ namespace YhCore.Service.Impl
if (appType.ToLower() == "noticemsg") appType = "notice";
string dtStr = DateTime.Now.ToString("yyyyMMdd");
string physicalDir = Path.Combine(StorageRoot(), appType, dtStr);
string outFilePath = "/UploadFile/" + appType + "/" + dtStr + "/";
string outFilePath = appType + "/" + dtStr + "/";
if (!Directory.Exists(physicalDir))
Directory.CreateDirectory(physicalDir);
@@ -169,7 +168,7 @@ namespace YhCore.Service.Impl
if (appType.ToLower() == "noticemsg") appType = "notice";
string dtStr = DateTime.Now.ToString("yyyyMMdd");
string physicalDir = Path.Combine(StorageRoot(), appType, dtStr);
string outFilePath = "/UploadFile/" + appType + "/" + dtStr + "/";
string outFilePath = appType + "/" + dtStr + "/";
if (!Directory.Exists(physicalDir))
Directory.CreateDirectory(physicalDir);
@@ -214,22 +213,29 @@ namespace YhCore.Service.Impl
if (string.IsNullOrEmpty(fileUrl))
return "";
// 相对路径如 /UploadFile/docprint/20260101/{guid}.jpg → Config.FilePath + 子路径
// 新数据:fileurl 以 FILE_PATH 开头(如 /data/upload/docprint/20260101/{guid}.jpg)→ 剥前缀映射回 FILE_PATH。
// 注意必须用未 TrimStart 的原始 fileUrl 比较(FILE_PATH 本身以 / 开头)
string fp = Config.FilePath;
if (!string.IsNullOrEmpty(fp) && fileUrl.StartsWith(fp, StringComparison.OrdinalIgnoreCase))
return Path.Combine(fp, fileUrl.Substring(fp.Length).TrimStart('/').Replace('/', Path.DirectorySeparatorChar));
// 旧数据:相对路径如 /upload/docprint/20260101/{guid}.jpg(不含 FILE_PATH 前缀)→ StorageRoot + 子路径
var relative = fileUrl.TrimStart('/');
const string prefix = "UploadFile/";
string prefix = Config.FilePath;
if (relative.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
relative = relative.Substring(prefix.Length);
return Path.Combine(StorageRoot(), relative.Replace('/', Path.DirectorySeparatorChar));
}
/// <summary>存储根目录(旧 Server.MapPath("~/UploadFile/");Config.FilePath 缺省时退回进程目录/UploadFile)。</summary>
private static string StorageRoot()
/// <summary>存储根目录(旧 Server.MapPath("~/upload/");Config.FilePath 缺省时退回进程目录/upload)。</summary>
public static string StorageRoot()
{
if (!string.IsNullOrEmpty(Config.FilePath))
return Config.FilePath;
return Path.Combine(AppContext.BaseDirectory, "UploadFile");
// 此分支仅在 FILE_PATH 未配置时执行(此时 Config.FilePath 为 null,不可再参与拼接)
return Path.Combine(AppContext.BaseDirectory, Config.FilePath);
}
}
}
@@ -159,7 +159,7 @@ namespace YhCore.Service.Impl
a.flowname != null && a.flowname.Trim() == flowname && a.enable != 3)).Count;
if (dup > 1) throw new DException("流程名不能重名!");
var info = await _flowinfo.FirstOrDefaultAsync(a => a.SerID == model.SerID);
var info = await _flowinfo.FirstOrDefaultTrackedAsync(a => a.SerID == model.SerID);
if (info == null) throw new DException("未找到要修改的流程!");
info.flowname = model.flowname;
@@ -198,7 +198,7 @@ namespace YhCore.Service.Impl
{
if (model == null) throw new DException("参数错误!");
var info = await _flowstep.FirstOrDefaultAsync(a => a.SerID == model.SerID);
var info = await _flowstep.FirstOrDefaultTrackedAsync(a => a.SerID == model.SerID);
if (info == null) throw new DException("未找到要修改的步骤!");
info.flowid = model.flowid;
@@ -207,7 +207,7 @@ namespace YhCore.Service.Impl
await _flowstep.UpdateAsync(info);
// 重写该步骤的人员绑定(先清后加)
var existing = await _groupuser.FindAsync(a => a.usergroup == info.SerID);
var existing = await _groupuser.FindTrackedAsync(a => a.usergroup == info.SerID);
foreach (var itt in existing)
{
await _groupuser.RemoveAsync(itt);
@@ -243,7 +243,7 @@ namespace YhCore.Service.Impl
if (string.IsNullOrWhiteSpace(ids)) throw new DException("ids is not null");
var lids = ids.Split(',').Select(a => int.Parse(a)).ToArray();
var flows = await _flowinfo.FindAsync(a => lids.Contains(a.SerID));
var flows = await _flowinfo.FindTrackedAsync(a => lids.Contains(a.SerID));
foreach (var item in flows)
{
item.enable = 3;
@@ -256,7 +256,7 @@ namespace YhCore.Service.Impl
if (ids == null || ids.Length == 0) throw new DException("ids is not null");
var intIds = ids.Select(d => (int)d).ToArray();
var objlist = await _flowstep.FindAsync(m => intIds.Contains(m.SerID));
var objlist = await _flowstep.FindTrackedAsync(m => intIds.Contains(m.SerID));
foreach (var item in objlist)
{
await _flowstep.RemoveAsync(item);
+54 -38
View File
@@ -7,6 +7,7 @@ using Yh.Platform.Abstractions;
using Yh.SchClass.OpenApi;
using YhCore.Entity;
using YhCore.Model.Dto;
using YhCore.Model.Vo;
using YhCore.Repository;
using YhCore.Service.Interfaces;
@@ -72,7 +73,7 @@ namespace YhCore.Service.Impl
#region 抄送
/// <summary>抄送列表(旧 SendRdList:DocStatus 1/11,SendUserID 含本人;旧为教师内连接)。</summary>
public async Task<List<DocPrintListItem>> SendRdListAsync(string name, string startDate, string endDate, string userId, string schSerId, int pageSize, int pageIndex)
public async Task<PageVO<DocPrintListItem>> SendRdListAsync(string name, string startDate, string endDate, string userId, string schSerId, int pageSize, int pageIndex)
{
var docs = (await _doc.FindAsync(m => m.SchSerID == schSerId)).ToList();
@@ -88,6 +89,7 @@ namespace YhCore.Service.Impl
docs = docs.Where(m => m.TakeDate <= date2).ToList();
}
var total = docs.Count(m => m.DocStatus == "1" || m.DocStatus == "11");
docs = docs.Where(m => m.DocStatus == "1" || m.DocStatus == "11")
.OrderByDescending(m => m.SerID)
.Skip(pageSize * pageIndex).Take(pageSize).ToList();
@@ -101,7 +103,7 @@ namespace YhCore.Service.Impl
if (!TryGetTea(teaMap, item.UserID, out var tea)) continue;
result.Add(await BuildListItemAsync(item, tea, dictMap, "yyyy-MM-dd"));
}
return result;
return PageVO<DocPrintListItem>.Of(result, total, pageIndex, pageSize);
}
/// <summary>查看抄送人员详情(旧 SendDetailsList)。</summary>
@@ -138,7 +140,7 @@ namespace YhCore.Service.Impl
{
await SetChaoSendAsync(int.Parse(uid), schSerId, mysenduserid);
var model = await _doc.FirstOrDefaultAsync(m => m.SerID == serId);
var model = await _doc.FirstOrDefaultTrackedAsync(m => m.SerID == serId);
if (model != null)
{
model.SendUserID = senduserid;
@@ -249,7 +251,7 @@ namespace YhCore.Service.Impl
/// </summary>
public async Task<int> EditAsync(WYDocPrintSaveDto model)
{
var info = await _doc.FirstOrDefaultAsync(x => x.SerID == model.SerID);
var info = await _doc.FirstOrDefaultTrackedAsync(x => x.SerID == model.SerID);
if (info == null) throw new DException("未找到该文印记录!");
info.DocType = model.DocType;
@@ -303,7 +305,7 @@ namespace YhCore.Service.Impl
var idArr = ids.Split(',')
.Where(a => !string.IsNullOrEmpty(a) && decimal.TryParse(a, out _))
.Select(decimal.Parse).ToArray();
var objlist = await _doc.FindAsync(m => idArr.Contains(m.SerID));
var objlist = await _doc.FindTrackedAsync(m => idArr.Contains(m.SerID));
foreach (var item in objlist)
{
await _doc.RemoveAsync(item);
@@ -334,7 +336,7 @@ namespace YhCore.Service.Impl
await _flowtranct.AddAsync(tranct);
await _flowtranct.SaveChangesAsync();
var docModel = await _doc.FirstOrDefaultAsync(m => m.SerID == model.dataid);
var docModel = await _doc.FirstOrDefaultTrackedAsync(m => m.SerID == model.dataid);
if (docModel != null)
{
docModel.DocStatus = model.actionstatus.ToString();
@@ -349,7 +351,7 @@ namespace YhCore.Service.Impl
#region 列表
/// <summary>我的文印列表(旧 MyList;旧 DocTypeName 误用 YZBM 字典,此处保持旧行为)。</summary>
public async Task<List<DocPrintListItem>> MyListAsync(string name, string status, string startDate, string endDate, string userId, string schSerId, int pageSize, int pageIndex)
public async Task<PageVO<DocPrintListItem>> MyListAsync(string name, string status, string startDate, string endDate, string userId, string schSerId, int pageSize, int pageIndex)
{
var docs = (await _doc.FindAsync(m => m.UserID == userId && m.SchSerID == schSerId)).ToList();
@@ -365,6 +367,7 @@ namespace YhCore.Service.Impl
docs = docs.Where(m => m.TakeDate <= date2).ToList();
}
var total = docs.Count;
docs = docs.OrderBy(m => m.DocStatus).ThenByDescending(m => m.TakeDate).ThenByDescending(m => m.EditDate)
.Skip(pageSize * pageIndex).Take(pageSize).ToList();
@@ -376,14 +379,14 @@ namespace YhCore.Service.Impl
TryGetTea(teaMap, item.UserID, out var tea); // 旧为左连接,缺失补空
result.Add(await BuildListItemAsync(item, tea, dictMap, "yyyy-MM-dd HH:mm", "YZBM"));
}
return result;
return PageVO<DocPrintListItem>.Of(result, total, pageIndex, pageSize);
}
/// <summary>
/// 全部文印列表(旧 AllList 原生 SQL 四分类:x=待我审(0) y=我已审(1) z=抄送我(2) m=我发起(3),按优先级去重)。
/// 旧 Name 过滤的 SQL 拼接存在运算符优先级缺陷,此处按语义实现:内容/教师名/字典名任一命中。
/// </summary>
public async Task<List<DocPrintListItem>> AllListAsync(string name, string userId, string schSerId, int pageSize, int pageIndex)
public async Task<PageVO<DocPrintListItem>> AllListAsync(string name, string userId, string schSerId, int pageSize, int pageIndex)
{
int uid = int.Parse(userId);
@@ -423,6 +426,7 @@ namespace YhCore.Service.Impl
|| DictNameContains(dictMap, "YZFL", t.doc.DPType, name));
}
var total = query.Count();
var pageItems = query.OrderByDescending(t => t.doc.SerID).ThenByDescending(t => t.doc.TakeDate)
.Skip(pageSize * pageIndex).Take(pageSize).ToList();
@@ -434,16 +438,17 @@ namespace YhCore.Service.Impl
item.ctype = ctype;
result.Add(item);
}
return result;
return PageVO<DocPrintListItem>.Of(result, total, pageIndex, pageSize);
}
/// <summary>
/// 文印审核列表(旧 ShenheList:固定流程 + 指派 + 自由流程三路并集,HashSet 去重)。
/// 旧实现的 Name 过滤作用在未被使用的查询上(死代码),此处保持 Name 不生效的旧行为。
/// </summary>
public async Task<List<DocPrintListItem>> ShenheListAsync(string userId, string schSerId, int pageSize, int pageIndex)
public async Task<PageVO<DocPrintListItem>> ShenheListAsync(string userId, string schSerId, int pageSize, int pageIndex)
{
var docs = await GetPendingShenheDocsAsync(userId, schSerId, requireFlowIdNonZero: false);
var total = docs.Count;
docs = docs.OrderBy(m => m.DocStatus).ThenByDescending(m => m.TakeDate)
.Skip(pageSize * pageIndex).Take(pageSize).ToList();
@@ -455,13 +460,13 @@ namespace YhCore.Service.Impl
TryGetTea(teaMap, item.UserID, out var tea);
result.Add(await BuildListItemAsync(item, tea, dictMap, "yyyy-MM-dd HH:mm"));
}
return result;
return PageVO<DocPrintListItem>.Of(result, total, pageIndex, pageSize);
}
/// <summary>
/// 审批列表(旧 SPList:待审(DS,zt=false)∪ 我已审(YS,zt=true,排除 DS),_disabled 即 zt)。
/// </summary>
public async Task<List<DocPrintListItem>> SPListAsync(string name, string status, string startDate, string endDate, string userId, string schSerId, int pageSize, int pageIndex)
public async Task<PageVO<DocPrintListItem>> SPListAsync(string name, string status, string startDate, string endDate, string userId, string schSerId, int pageSize, int pageIndex)
{
var userid = int.Parse(userId);
@@ -493,6 +498,7 @@ namespace YhCore.Service.Impl
query = query.Where(m => m.doc.DocContent != null && m.doc.DocContent.IndexOf(name) >= 0);
var ordered = query.OrderBy(m => m.doc.DocStatus).ThenByDescending(m => m.doc.EditDate).ToList();
var total = ordered.Count;
var teaMap = await GetTeaMapAsync(schSerId);
var dictMap = await GetDictMapAsync(schSerId);
@@ -504,11 +510,11 @@ namespace YhCore.Service.Impl
item._disabled = zt; // 我已审的记录不可再编辑(覆盖通用 _disabled 语义,与旧一致)
result.Add(item);
}
return result;
return PageVO<DocPrintListItem>.Of(result, total, pageIndex, pageSize);
}
/// <summary>文印已审列表(旧 YishenList:本人审核流水关联,排除撤销与已领取)。</summary>
public async Task<List<DocPrintListItem>> YishenListAsync(string name, string userId, string schSerId, int pageSize, int pageIndex)
public async Task<PageVO<DocPrintListItem>> YishenListAsync(string name, string userId, string schSerId, int pageSize, int pageIndex)
{
int uid = int.Parse(userId);
var tranctDataIds = (await _flowtranct.FindAsync(b => b.actionuserid == uid && b.datatype == DocPrintConstants.AppCode.ToString() && b.actionstatus != 11))
@@ -521,6 +527,7 @@ namespace YhCore.Service.Impl
if (!string.IsNullOrEmpty(name))
docs = docs.Where(m => m.DocContent != null && m.DocContent.Contains(name)).ToList();
var total = docs.Count;
docs = docs.OrderByDescending(m => m.SerID).ThenByDescending(m => m.TakeDate)
.Skip(pageSize * pageIndex).Take(pageSize).ToList();
@@ -532,11 +539,11 @@ namespace YhCore.Service.Impl
TryGetTea(teaMap, item.UserID, out var tea);
result.Add(await BuildListItemAsync(item, tea, dictMap, "yyyy-MM-dd HH:mm"));
}
return result;
return PageVO<DocPrintListItem>.Of(result, total, pageIndex, pageSize);
}
/// <summary>全部文印列表(旧 totalList,管理员视角,按状态/时间过滤)。</summary>
public async Task<List<DocPrintListItem>> TotalListAsync(string name, string stateid, string starttime, string endtime, string schSerId, int pageSize, int pageIndex)
public async Task<PageVO<DocPrintListItem>> TotalListAsync(string name, string stateid, string starttime, string endtime, string schSerId, int pageSize, int pageIndex)
{
var docs = (await _doc.FindAsync(m => m.SchSerID == schSerId)).ToList();
@@ -552,6 +559,7 @@ namespace YhCore.Service.Impl
if (!string.IsNullOrEmpty(name))
docs = docs.Where(m => m.DocContent != null && m.DocContent.IndexOf(name) >= 0).ToList();
var total = docs.Count;
docs = docs.OrderBy(m => m.DocStatus).ThenByDescending(m => m.TakeDate)
.Skip(pageSize * pageIndex).Take(pageSize).ToList();
@@ -563,7 +571,7 @@ namespace YhCore.Service.Impl
TryGetTea(teaMap, item.UserID, out var tea);
result.Add(await BuildListItemAsync(item, tea, dictMap, "yyyy-MM-dd"));
}
return result;
return PageVO<DocPrintListItem>.Of(result, total, pageIndex, pageSize);
}
/// <summary>
@@ -665,7 +673,7 @@ namespace YhCore.Service.Impl
using var tx = await _db.Database.BeginTransactionAsync();
try
{
var rdmodel = await _doc.FirstOrDefaultAsync(m => m.SerID == model.dataid);
var rdmodel = await _doc.FirstOrDefaultTrackedAsync(m => m.SerID == model.dataid);
if (rdmodel == null) throw new DException("未找到该文印记录!");
if (rdmodel.DocStatus != "0")
@@ -766,7 +774,7 @@ namespace YhCore.Service.Impl
/// </summary>
public async Task<string> AffirmApplyforOkAsync(FlowTranctDto model)
{
var itt = await _doc.FirstOrDefaultAsync(m => m.SerID == model.dataid);
var itt = await _doc.FirstOrDefaultTrackedAsync(m => m.SerID == model.dataid);
if (itt == null) throw new DException("未找到该文印记录!");
var claimed = await _flowtranct.AnyAsync(a => a.dataid == model.dataid
@@ -817,7 +825,7 @@ namespace YhCore.Service.Impl
{
if (flowid <= 0) return;
var list = await _zhipai.FirstOrDefaultAsync(a => a.UserID == uid && a.MSerID == dataid
var list = await _zhipai.FirstOrDefaultTrackedAsync(a => a.UserID == uid && a.MSerID == dataid
&& a.SchSerID == schSerId && a.FlowID == flowid && a.FlowStepID == flowstepid && a.type == type);
if (list == null)
{
@@ -895,7 +903,7 @@ namespace YhCore.Service.Impl
}
/// <summary>文印详情统计列表(旧 List2:非待审单据按申请时间倒序分页)。</summary>
public async Task<List<DocDetailStatItem>> List2Async(string starttime, string endtime, string schSerId, int pageSize, int pageIndex)
public async Task<PageVO<DocDetailStatItem>> List2Async(string starttime, string endtime, string schSerId, int pageSize, int pageIndex)
{
var docs = (await _doc.FindAsync(m => m.SchSerID == schSerId && m.DocStatus != "0")).ToList();
if (DateTime.TryParse(starttime, out var date1))
@@ -933,13 +941,16 @@ namespace YhCore.Service.Impl
});
}
return full.OrderByDescending(a => a.EditDate)
.Skip(pageSize * pageIndex).Take(pageSize)
.ToList();
var total = full.Count;
return PageVO<DocDetailStatItem>.Of(
full.OrderByDescending(a => a.EditDate)
.Skip(pageSize * pageIndex).Take(pageSize)
.ToList(),
total, pageIndex, pageSize);
}
/// <summary>文印数量按用户排名(旧 List3)。</summary>
public async Task<List<UserRankItem>> List3Async(string starttime, string endtime, string schSerId, int pageSize, int pageIndex)
public async Task<PageVO<UserRankItem>> List3Async(string starttime, string endtime, string schSerId, int pageSize, int pageIndex)
{
var docs = (await _doc.FindAsync(m => m.SchSerID == schSerId && m.DocStatus == "1")).ToList();
if (DateTime.TryParse(starttime, out var date1))
@@ -965,23 +976,27 @@ namespace YhCore.Service.Impl
.OrderByDescending(a => a.num)
.ToList();
return grouped.Skip(pageSize * pageIndex).Take(pageSize).ToList();
var total = grouped.Count;
return PageVO<UserRankItem>.Of(
grouped.Skip(pageSize * pageIndex).Take(pageSize).ToList(),
total, pageIndex, pageSize);
}
/// <summary>按月份统计明细(旧 List4:指定年月已印好单据,按申请时间倒序分页)。</summary>
public async Task<List<StatListItem>> List4Async(int year, int month, string schSerId, int pageSize, int pageIndex)
public async Task<PageVO<StatListItem>> List4Async(int year, int month, string schSerId, int pageSize, int pageIndex)
{
var docs = (await _doc.FindAsync(m => m.SchSerID == schSerId
&& m.DocStatus == "1" && m.EditDate.Year == year && m.EditDate.Month == month))
.OrderByDescending(a => a.EditDate)
var all = (await _doc.FindAsync(m => m.SchSerID == schSerId
&& m.DocStatus == "1" && m.EditDate.Year == year && m.EditDate.Month == month)).ToList();
var total = all.Count;
var docs = all.OrderByDescending(a => a.EditDate)
.Skip(pageSize * pageIndex).Take(pageSize)
.ToList();
return await BuildStatListAsync(docs, schSerId);
return PageVO<StatListItem>.Of(await BuildStatListAsync(docs, schSerId), total, pageIndex, pageSize);
}
/// <summary>按用户统计明细(旧 List5:指定用户已印好单据,按申请时间倒序分页)。</summary>
public async Task<List<StatListItem>> List5Async(string userid, string starttime, string endtime, string schSerId, int pageSize, int pageIndex)
public async Task<PageVO<StatListItem>> List5Async(string userid, string starttime, string endtime, string schSerId, int pageSize, int pageIndex)
{
var docs = (await _doc.FindAsync(m => m.SchSerID == schSerId && m.UserID == userid && m.DocStatus == "1")).ToList();
if (DateTime.TryParse(starttime, out var date1))
@@ -992,11 +1007,12 @@ namespace YhCore.Service.Impl
docs = docs.Where(m => m.EditDate <= date2).ToList();
}
var total = docs.Count;
docs = docs.OrderByDescending(a => a.EditDate)
.Skip(pageSize * pageIndex).Take(pageSize)
.ToList();
return await BuildStatListAsync(docs, schSerId);
return PageVO<StatListItem>.Of(await BuildStatListAsync(docs, schSerId), total, pageIndex, pageSize);
}
#endregion
@@ -1240,7 +1256,7 @@ namespace YhCore.Service.Impl
/// <summary>自由流程挂链(旧 ApproveFlow.SetPrimarySteptID:从 stepid 起沿 laststep 链回写 dataid)。</summary>
private async Task SetPrimarySteptIDAsync(int dataid, int stepid)
{
var node = await _freeflowinfo.FirstOrDefaultAsync(a => a.SerID == stepid);
var node = await _freeflowinfo.FirstOrDefaultTrackedAsync(a => a.SerID == stepid);
if (node != null)
{
node.dataid = dataid;
@@ -1250,7 +1266,7 @@ namespace YhCore.Service.Impl
var nextSer = stepid;
while (true)
{
var next = await _freeflowinfo.FirstOrDefaultAsync(a => a.laststep == nextSer);
var next = await _freeflowinfo.FirstOrDefaultTrackedAsync(a => a.laststep == nextSer);
if (next == null) break;
next.dataid = dataid;
await _freeflowinfo.SaveChangesAsync();
@@ -1261,7 +1277,7 @@ namespace YhCore.Service.Impl
/// <summary>抄送人落库(旧 ApproveFlow.SetChaoSend:按用户+flowType 全删再插)。</summary>
private async Task SetChaoSendAsync(int uid, string schSerId, string senduserid)
{
var olds = await _senduser.FindAsync(a => a.flowType == DocPrintConstants.AppCode && a.userid == uid);
var olds = await _senduser.FindTrackedAsync(a => a.flowType == DocPrintConstants.AppCode && a.userid == uid);
foreach (var old in olds)
{
await _senduser.RemoveAsync(old);
@@ -1287,7 +1303,7 @@ namespace YhCore.Service.Impl
var fids = ParseIds(upFile);
foreach (var fid in fids)
{
var file = await _files.FirstOrDefaultAsync(x => x.SerID == fid);
var file = await _files.FirstOrDefaultTrackedAsync(x => x.SerID == fid);
if (file != null)
{
file.TopicID = topicId;
File diff suppressed because it is too large Load Diff
@@ -7,8 +7,8 @@ namespace YhCore.Service.Interfaces
{
/// <summary>
/// 公共文件服务(移植旧 BestSchool.Service.Common.GetEnclosure/FileSaveEnclosure +
/// BestSchool.Common.UpLoadFile.FileSave/FileSaveImage/FileChunkSave/FileMergeSave)。
/// 存储约定:物理根目录 = Config.FilePath(≈旧 ~/UploadFile/),相对路径仍存 /UploadFile/{AppType}/{yyyyMMdd}/{guid}{ext}
/// BestSchool.Common.upload.FileSave/FileSaveImage/FileChunkSave/FileMergeSave)。
/// 存储约定:物理根目录 = Config.FilePath(≈旧 ~/upload/),相对路径仍存 /upload/{AppType}/{yyyyMMdd}/{guid}{ext}
/// (与旧库数据格式一致,兼容历史记录);host 取 Config.FileWebPath(≈旧 AuthConfig.weboa_url)。
/// 旧 ToOfficeHelper.OfficeConvertToPDF(Office COM 转 PDF)新项目缺失,降级:上传不做转 PDF,
/// 预览时本地无 .pdf 则降级 officeapps.live.com 在线预览。
@@ -21,10 +21,10 @@ namespace YhCore.Service.Interfaces
/// <summary>存储附件信息(旧 FileSaveEnclosure:Common_UpFiles 落库,返回 SerID;文件名非法字符替换为 _)。</summary>
Task<int> SaveEnclosureAsync(FileObj file, string appType);
/// <summary>单文件上传(旧 UpLoadFile.FileSave:扩展名白名单 + 保存 + 返回 FileObj)。</summary>
/// <summary>单文件上传(旧 upload.FileSave:扩展名白名单 + 保存 + 返回 FileObj)。</summary>
Task<FileObj> SaveFileAsync(string appType, IFormFile file);
/// <summary>图片多文件上传(旧 UpLoadFile.FileSaveImage:jpg/gif/png/jpeg)。</summary>
/// <summary>图片多文件上传(旧 upload.FileSaveImage:jpg/gif/png/jpeg)。</summary>
Task<List<FileObj>> SaveFileImageAsync(string appType, IFormFileCollection files);
/// <summary>保存分片(旧 FileChunkSave:临时目录 {guid}/ 下按索引存块)。</summary>
@@ -33,7 +33,7 @@ namespace YhCore.Service.Interfaces
/// <summary>合并分片(旧 FileMergeSave:按块合并 → 正式目录 + FileObj)。</summary>
Task<FileObj> MergeSaveAsync(string appType, string guid, string fileName);
/// <summary>相对路径 → 物理路径(fileurl 以 /UploadFile/ 开头 → Config.FilePath + 子路径)。</summary>
/// <summary>相对路径 → 物理路径(fileurl 以 /upload/ 开头 → Config.FilePath + 子路径)。</summary>
string MapPhysicalPath(string fileUrl);
}
}
@@ -1,6 +1,7 @@
using System.Threading.Tasks;
using System.Collections.Generic;
using YhCore.Model.Dto;
using YhCore.Model.Vo;
namespace YhCore.Service.Interfaces
{
@@ -11,7 +12,7 @@ namespace YhCore.Service.Interfaces
public interface IWYDocPrintService
{
// ---- 抄送 ----
Task<List<DocPrintListItem>> SendRdListAsync(string name, string startDate, string endDate, string userId, string schSerId, int pageSize, int pageIndex);
Task<PageVO<DocPrintListItem>> SendRdListAsync(string name, string startDate, string endDate, string userId, string schSerId, int pageSize, int pageIndex);
Task<List<SendDetailItem>> SendDetailsListAsync(int serId);
Task<string> EditSendAsync(int serId, string senduserid, string mysenduserid, string schSerId, string uid);
@@ -25,12 +26,12 @@ namespace YhCore.Service.Interfaces
Task<string> RevokeAsync(FlowTranctDto model);
// ---- 列表 ----
Task<List<DocPrintListItem>> MyListAsync(string name, string status, string startDate, string endDate, string userId, string schSerId, int pageSize, int pageIndex);
Task<List<DocPrintListItem>> AllListAsync(string name, string userId, string schSerId, int pageSize, int pageIndex);
Task<List<DocPrintListItem>> ShenheListAsync(string userId, string schSerId, int pageSize, int pageIndex);
Task<List<DocPrintListItem>> SPListAsync(string name, string status, string startDate, string endDate, string userId, string schSerId, int pageSize, int pageIndex);
Task<List<DocPrintListItem>> YishenListAsync(string name, string userId, string schSerId, int pageSize, int pageIndex);
Task<List<DocPrintListItem>> TotalListAsync(string name, string stateid, string starttime, string endtime, string schSerId, int pageSize, int pageIndex);
Task<PageVO<DocPrintListItem>> MyListAsync(string name, string status, string startDate, string endDate, string userId, string schSerId, int pageSize, int pageIndex);
Task<PageVO<DocPrintListItem>> AllListAsync(string name, string userId, string schSerId, int pageSize, int pageIndex);
Task<PageVO<DocPrintListItem>> ShenheListAsync(string userId, string schSerId, int pageSize, int pageIndex);
Task<PageVO<DocPrintListItem>> SPListAsync(string name, string status, string startDate, string endDate, string userId, string schSerId, int pageSize, int pageIndex);
Task<PageVO<DocPrintListItem>> YishenListAsync(string name, string userId, string schSerId, int pageSize, int pageIndex);
Task<PageVO<DocPrintListItem>> TotalListAsync(string name, string stateid, string starttime, string endtime, string schSerId, int pageSize, int pageIndex);
Task<List<DocPrintDetailItem>> MyListtoOneAsync(int serId, string schSerId);
Task<List<BindFlowOption>> BindflowinfoAsync(string schSerId, int flowType);
@@ -43,9 +44,9 @@ namespace YhCore.Service.Interfaces
// ---- 统计 ----
Task<List<MonthStatItem>> List1Async(string starttime, string endtime, string schSerId);
Task<List<DocDetailStatItem>> List2Async(string starttime, string endtime, string schSerId, int pageSize, int pageIndex);
Task<List<UserRankItem>> List3Async(string starttime, string endtime, string schSerId, int pageSize, int pageIndex);
Task<List<StatListItem>> List4Async(int year, int month, string schSerId, int pageSize, int pageIndex);
Task<List<StatListItem>> List5Async(string userid, string starttime, string endtime, string schSerId, int pageSize, int pageIndex);
Task<PageVO<DocDetailStatItem>> List2Async(string starttime, string endtime, string schSerId, int pageSize, int pageIndex);
Task<PageVO<UserRankItem>> List3Async(string starttime, string endtime, string schSerId, int pageSize, int pageIndex);
Task<PageVO<StatListItem>> List4Async(int year, int month, string schSerId, int pageSize, int pageIndex);
Task<PageVO<StatListItem>> List5Async(string userid, string starttime, string endtime, string schSerId, int pageSize, int pageIndex);
}
}
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using YhCore.Entity;
using YhCore.Model.Dto;
using YhCore.Model.Vo;
@@ -14,7 +16,9 @@ namespace YhCore.Service.Interfaces
/// 落库 WY_SHRole_SHTea.SHTeaID / WY_SHRole_BSHTea.BSHTeaID 仍为 decimal 用户 id;
/// 3. 签名存文件系统(DES 加密),非数据库表;
/// 4. 旧 ServiceResult 的 Data/OtherData/Page 三段合并为单个 Result 对象(getshenheFlow / getStepInfo / getStepByJE / getdocOftable)。
/// 延后:Excel 导入导出(UpfileExcelSHRole/downMBOfSHRole/downUpfileSHRoleFK/ExceltotalListSHRole/WYRDExceldaochu)、审批单 PDF(DwonWYSQD)。
/// Excel 导入导出(UpfileExcelSHRole/downMBOfSHRole/downUpfileSHRoleFK/ExceltotalListSHRole/WYRDExceldaochu)
/// 与审批单 PDF(DwonWYSQD):下载类接口返回 (byte[] Content, string FileName),
/// 控制器按扩展名包装 File();导入反馈表存进程内 IMemoryCache(单实例)。
/// </summary>
public interface IWYSHRoleService
{
@@ -50,10 +54,10 @@ namespace YhCore.Service.Interfaces
// ---- 用户记录查看和审核 ----
Task<SHRoleShenheFlowResult> GetshenheFlowAsync(string schSerId, decimal docprintId, int flowid);
Task<List<SHRoleDocPrintListItem>> MyListtoOneSHRoleAsync(int serId, string schSerId);
Task<List<SHRoleDocPrintListItem>> MyListSHRoleAsync(string name, string status, string startDate, string endDate, int pageSize, int pageIndex, string userId, string schSerId);
Task<List<SHRoleDocPrintListItem>> TotalListSHRoleAsync(int flowid, int dwid, string name, string stateid, string starttime, string endtime, int pageSize, int pageIndex, string schSerId);
Task<List<SHRoleDocPrintListItem>> SendRdListSHRoleAsync(int flowid, int dwid, string name, string status, string startDate, string endDate, string userId, string schSerId, int pageSize, int pageIndex);
Task<List<SHRoleDocPrintListItem>> SPListSHRoleAsync(int flowid, int dwid, string name, string status, string startDate, string endDate, string userId, string userName, string schSerId, int pageSize, int pageIndex);
Task<PageVO<SHRoleDocPrintListItem>> MyListSHRoleAsync(string name, string status, string startDate, string endDate, int pageSize, int pageIndex, string userId, string schSerId);
Task<PageVO<SHRoleDocPrintListItem>> TotalListSHRoleAsync(int flowid, int dwid, string name, string stateid, string starttime, string endtime, int pageSize, int pageIndex, string schSerId);
Task<PageVO<SHRoleDocPrintListItem>> SendRdListSHRoleAsync(int flowid, int dwid, string name, string status, string startDate, string endDate, string userId, string schSerId, int pageSize, int pageIndex);
Task<PageVO<SHRoleDocPrintListItem>> SPListSHRoleAsync(int flowid, int dwid, string name, string status, string startDate, string endDate, string userId, string userName, string schSerId, int pageSize, int pageIndex);
Task<string> ShenheOkSHRoleAsync(FlowTranctDto model);
// ---- 规格表 / 签名 / 退回 / 金额 ----
@@ -64,7 +68,26 @@ namespace YhCore.Service.Interfaces
Task<string> TuiHuiAsync(string userId, string cont, decimal docId);
Task<string> UnTuiHuiAsync(string userId, decimal docId);
Task<string> RegetJEAsync(string schSerId, string userId, List<decimal> listDocPrintId, string type);
Task<List<SHRoleRegetJERdItem>> GetRegetJERdAsync(string schSerId, int pageSize, int pageIndex);
Task<PageVO<SHRoleRegetJERdItem>> GetRegetJERdAsync(string schSerId, int pageSize, int pageIndex);
Task<int> SetDownFileTimesAsync(int serId);
// ---- Excel 导入导出 / 审批单 PDF(延后批次补齐)----
/// <summary>下载导入模板(旧 downMBOfSHRole,代码生成)。</summary>
Task<byte[]> DownMBOfSHRoleAsync();
/// <summary>Excel 批量导入审核角色(旧 UpfileExcelSHRole,multipart 第一个文件),返回反馈消息。</summary>
Task<string> UpfileExcelSHRoleAsync(string schSerId, string userId, IFormFile file);
/// <summary>下载最近一次导入的反馈 Excel(旧 downUpfileSHRoleFK)。</summary>
Task<byte[]> DownUpfileSHRoleFKAsync(string userId);
/// <summary>全部文印导出(旧 ExceltotalListSHRole)。</summary>
Task<(byte[] Content, string FileName)> ExceltotalListSHRoleAsync(string schSerId, string name, string stateid, string starttime, string endtime);
/// <summary>文印记录导出(旧 WYRDExceldaochu,flag=page 简版 / 其余完整版)。</summary>
Task<(byte[] Content, string FileName)> WYRDExceldaochuAsync(string schSerId, int flowid, int dwid, string name, string stateid, string starttime, string endtime, string flag);
/// <summary>审批单 PDF 下载(旧 DwonWYSQD,IDS 逗号分隔的 WY_DocPrint.SerID)。</summary>
Task<(byte[] Content, string FileName)> DwonWYSQDAsync(string schSerId, string userId, string ids);
}
}
@@ -0,0 +1,123 @@
using System;
namespace YhCore.Service.Pdf
{
/// <summary>数字转中文大写金额(移植自旧 Bsch.Common/DocPrint/NumberToChineseConverter,审批单合计金额用)。</summary>
public class NumberToChineseConverter
{
// 中文大写数字
private static readonly char[] ChineseNumbers = { '零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖' };
// 中文大写单位
private static readonly string[] ChineseUnits = { "分", "角", "元", "拾", "佰", "仟", "万", "拾", "佰", "仟", "亿", "拾", "佰", "仟", "兆" };
/// <summary>
/// 将数字转换为中文大写金额
/// </summary>
/// <param name="number">需要转换的数字(支持整数和小数,建议保留两位小数)</param>
/// <returns>中文大写金额字符串</returns>
public static string ToChineseUppercase(decimal number)
{
// 处理负数
bool isNegative = number < 0;
if (isNegative)
{
number = Math.Abs(number);
}
// 四舍五入保留两位小数
number = Math.Round(number, 2);
long integerPart = (long)number; // 整数部分
int decimalPart = (int)((number - integerPart) * 100); // 小数部分(角分)
// 处理零值
if (integerPart == 0 && decimalPart == 0)
{
return "零元整";
}
string result = string.Empty;
// 处理整数部分
if (integerPart > 0)
{
string integerStr = integerPart.ToString();
int length = integerStr.Length;
// 标记是否需要加零
bool needZero = false;
for (int i = 0; i < length; i++)
{
int digit = int.Parse(integerStr[i].ToString());
int unitIndex = length - i - 1 + 2; // 对应到ChineseUnits的索引(从元开始)
if (digit == 0)
{
needZero = true;
// 处理万、亿等单位,避免漏单位
if (unitIndex == 6 || unitIndex == 10)
{
result += ChineseUnits[unitIndex];
needZero = false;
}
}
else
{
if (needZero)
{
result += ChineseNumbers[0];
needZero = false;
}
result += ChineseNumbers[digit] + ChineseUnits[unitIndex];
}
}
}
// 处理小数部分(角分)
if (decimalPart > 0)
{
int jiao = decimalPart / 10; // 角
int fen = decimalPart % 10; // 分
if (jiao > 0)
{
result += ChineseNumbers[jiao] + ChineseUnits[1];
}
else if (integerPart > 0) // 有整数部分但角为0,需要加零
{
result += ChineseNumbers[0];
}
if (fen > 0)
{
result += ChineseNumbers[fen] + ChineseUnits[0];
}
}
else
{
// 无小数部分,加"整"
result += "整";
}
// 处理负数
if (isNegative)
{
result = "负" + result;
}
return result;
}
// 重载方法,支持double类型
public static string ToChineseUppercase(double number)
{
return ToChineseUppercase((decimal)number);
}
// 重载方法,支持int类型
public static string ToChineseUppercase(int number)
{
return ToChineseUppercase((decimal)number);
}
}
}
+102
View File
@@ -0,0 +1,102 @@
// PDF helper(移植自旧 Bsch.Common/DocPrint/PdfHelper):
// HTML→PDF 由 wkhtmltopdf 外部进程完成(原 iTextSharp XMLWorker 无 Core 版),
// 多页合并用 iTextSharp.LGPLv2.Core;Base64→PNG 直接落盘。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using iTextSharp.text;
using iTextSharp.text.pdf;
using Yh.Platform.Abstractions;
using YhCore.Common.Config;
namespace YhCore.Service.Pdf
{
public class PdfHelper
{
private static string WkHtmlToPdfPath =>
string.IsNullOrEmpty(Config.WkHtmlToPdfPath) ? "wkhtmltopdf" : Config.WkHtmlToPdfPath;
// --enable-local-file-access 为 wkhtmltopdf 0.12.6 新增开关(签名图片用本地绝对路径 <img> 必需);
// 0.12.5 及以下(如 Debian 10 源)不认识该参数会报 Unknown long argument,按版本探测决定是否追加
private static readonly Lazy<bool> NeedsLocalFileAccessFlag = new Lazy<bool>(() =>
{
try
{
using var probe = Process.Start(new ProcessStartInfo(WkHtmlToPdfPath, "--version")
{
RedirectStandardOutput = true,
CreateNoWindow = true,
UseShellExecute = false,
});
if (probe == null) return true;
string output = probe.StandardOutput.ReadToEnd();
probe.WaitForExit(10000);
var m = Regex.Match(output, @"(\d+)\.(\d+)\.(\d+)");
if (!m.Success) return true;
var version = new Version(int.Parse(m.Groups[1].Value), int.Parse(m.Groups[2].Value), int.Parse(m.Groups[3].Value));
return version >= new Version(0, 12, 6);
}
catch
{
return true;
}
});
/// <summary>逐页 HTML→PDF 后合并为单个 PDF 字节流(DwonWYSQD 用)</summary>
public static byte[] ConvertHtmlTextToPdfByList(List<string> htmlText, string HorV = "V")
=> MergePdf(htmlText.Select(HtmlToPdf).ToList());
private static byte[] HtmlToPdf(string html)
{
var tmpIn = Path.GetTempFileName() + ".html";
var tmpOut = Path.GetTempFileName() + ".pdf";
try
{
File.WriteAllText(tmpIn, html, new UTF8Encoding(false));
string localFlag = NeedsLocalFileAccessFlag.Value ? " --enable-local-file-access" : "";
var psi = new ProcessStartInfo(WkHtmlToPdfPath,
$" --encoding utf-8{localFlag} --disable-smart-shrinking \"{tmpIn}\" \"{tmpOut}\"")
{ CreateNoWindow = true, UseShellExecute = false };
using var proc = Process.Start(psi);
if (proc == null || !proc.WaitForExit(60000) || !File.Exists(tmpOut))
throw new DException("审批单 PDF 生成失败,请检查服务器是否已安装 wkhtmltopdf(或配置 wkhtmltopdf_path 指向其可执行文件)!");
return File.ReadAllBytes(tmpOut);
}
finally
{
if (File.Exists(tmpIn)) File.Delete(tmpIn);
if (File.Exists(tmpOut)) File.Delete(tmpOut);
}
}
public static byte[] MergePdf(List<byte[]> pdfBytes)
{
using var ms = new MemoryStream();
var doc = new Document();
var copy = new PdfCopy(doc, ms);
doc.Open();
foreach (var b in pdfBytes)
{
if (b == null || b.Length == 0) continue;
var reader = new PdfReader(b);
for (int i = 1; i <= reader.NumberOfPages; i++)
copy.AddPage(copy.GetImportedPage(reader, i));
copy.FreeReader(reader);
reader.Close();
}
doc.Close();
return ms.ToArray();
}
/// <summary>签名 base64 → PNG 落盘(审批单 HTML 内嵌本地路径图片用)</summary>
public static void ConvertBase64ToPng(string base64String, string outputFilePath)
{
var bytes = Convert.FromBase64String(base64String);
File.WriteAllBytes(outputFilePath, bytes);
}
}
}
+9 -2
View File
@@ -22,9 +22,16 @@
<ItemGroup>
<!-- 业务异常 DException / ErrorCode / IErrorCode:从 Nexus nuget-group 引用(原为本地源项目引用) -->
<PackageReference Include="Yh.Platform.Abstractions" Version="1.0.0-preview.1" />
<PackageReference Include="Yh.Platform.Abstractions" Version="1.0.0-preview.2" />
<!-- 教务(SchClass)开放接口 SDK:教师/学校查询(C1 reviewprocess 教师类接口):从 Nexus nuget-group 引用(原为本地源项目引用) -->
<PackageReference Include="Yh.SchClass.OpenApi" Version="1.0.0-preview.1" />
<PackageReference Include="Yh.SchClass.OpenApi" Version="1.0.0-preview.2" />
</ItemGroup>
<ItemGroup>
<!-- Excel 导入导出(NPOI .xls 读写 / EPPlus 多 Sheet 导出)与 PDF 合并:版本对齐旧 DocPrintCore/Bsch.Common -->
<PackageReference Include="NPOI" Version="2.6.2" />
<PackageReference Include="EPPlus" Version="4.5.3.3" />
<PackageReference Include="iTextSharp.LGPLv2.Core" Version="3.4.20" />
</ItemGroup>
</Project>
+1 -1
View File
@@ -31,7 +31,7 @@ namespace YhCore.Tasks.Jobs
try
{
var oldList = await repo.FindAsync(l => l.CreateTime < cutoff);
var oldList = await repo.FindTrackedAsync(l => l.CreateTime < cutoff);
if (oldList.Count == 0)
{
logger.LogInformation("审计日志清理完成:无 30 天前的数据");
@@ -44,6 +44,11 @@ namespace YhCore.WebApi.Auth
var auth = configuration.GetSection(YhAuthOptions.SectionName).Get<YhAuthOptions>() ?? new YhAuthOptions();
options.Cookie.Name = auth.CookieName;
options.Cookie.HttpOnly = true;
// SameSite=Lax:登录/回调全部是顶层导航,Lax 足够。
// 【勿改 None】HTTP 部署下无 Secure 属性的 SameSite=None Cookie 会被浏览器直接丢弃
// (Chrome/Edge 强制 None 必须 Secure),导致登录 Cookie 永远种不上 → 无限重定向循环
// (2026-09-07 线上复现:验票成功但下一请求无 Cookie → ERR_TOO_MANY_REDIRECTS)。
// 跨 host 登录态需求由 DebugUser 头方案解决(见 docs/本地前端联调线上后端SSO方案.md)。
options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax;
// 会话票据:TicketDataFormat 把票据存服务端,Cookie 只写 32 位 key;
// IsPersistent=false(CasAuthenticationHandler)会话 Cookie,关闭浏览器即失效;
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Threading.Tasks;
using Yh.Platform.Abstractions;
@@ -49,6 +50,20 @@ namespace YhCore.WebApi.Controllers.Api
return BoaUserContext.Get();
}
/// <summary>
/// 获取初始化地址(对齐老 UserCenterApiController.GetInitUrl:读 Session["AppInitUrl"])。
/// 前端 main.js 首次进入时调用:resp.Data 有值则 router.replace(initUrl),否则回 /docPrint/。
/// 响应形状为混排兼容体 {"code":0,"msg":"","State":true,"Data":...}:
/// code 小写保证前端 api/index.js 的 success 回调触发(response.code==0),
/// Data/State 大写保证 main.js 的 resp.Data 取到值——故用 ContentResult 手写 JSON
/// (ContentResult 不参与全局包装,也不经 camelCase ContractResolver)。
/// </summary>
[HttpGet("GetInitUrl")]
public string GetInitUrl()
{
return HttpContext.Session.GetString("AppInitUrl");
}
/// <summary>
/// 登出(对应 Java GET /api/base/logout:session.invalidate())。
/// </summary>
@@ -1,12 +1,14 @@
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Yh.Platform.Abstractions;
using Yh.SchClass.OpenApi;
using Yh.SchClass.OpenApi.Model;
using YhCore.Common;
using YhCore.Common.Config;
using YhCore.Common.UserContext;
@@ -28,13 +30,24 @@ namespace YhCore.WebApi.Controllers.Api.Common
{
private readonly IDictionaryService _dict;
private readonly IFileService _file;
private readonly SchClassOpenApi _sch;
public CommonController(IDictionaryService dict, IFileService file)
public CommonController(IDictionaryService dict, IFileService file, SchClassOpenApi sch)
{
_dict = dict;
_file = file;
_sch = sch;
}
[HttpGet("GetSchoolAll")]
public async Task<List<TeaTreeVo>> GetSchoolAll([FromQuery] int type = 0)
{
return await _sch.SchTree.GetTeaTreeAsync(SchSerID(), null);
}
/// <summary>获取指定字典数据(Type 1~10)。</summary>
[HttpGet("GetSysDic")]
public async Task<List<DicOption>> GetSysDic([FromQuery] int type = 0)
@@ -222,42 +235,58 @@ namespace YhCore.WebApi.Controllers.Api.Common
/// <summary>
/// 公用附件上传(旧 FileUpload,[NoAuth])。
/// chunk 参数 → 分片保存返回 {code:1,type:"chunk",msg};fileName 参数 → 合并分片;否则单文件上传。
/// 分片(chunk/chunkIndex + totalChunks&gt;1)→ 分片保存返回 {code:1,type:"chunk",msg};
/// 只带 fileName 不带文件 → 合并分片;带文件 → 单文件直传
/// (前端单文件直传也会带 guid/chunkIndex=0/totalChunks=0/fileName,不得误入合并分支)。
/// guid/chunk/fileName 兼容 query 与 form 两种形态(旧版读 Query)。
/// 落库 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"];
string guid = FormOrQuery("guid");
string chunk = FormOrQuery("chunk"); // 旧契约分片序号
string chunkIndex = FormOrQuery("chunkIndex"); // 前端现用分片序号
string totalChunks = FormOrQuery("totalChunks");
string fileName = FormOrQuery("fileName");
if (!string.IsNullOrEmpty(chunk))
var file = Request.Form.Files.FirstOrDefault(f => f.Name == "file") ?? Request.Form.Files.FirstOrDefault();
bool multiChunk = int.TryParse(totalChunks, out var total) && total > 1;
bool isChunkReq = file != null
&& (!string.IsNullOrEmpty(chunk) || (!string.IsNullOrEmpty(chunkIndex) && multiChunk));
if (isChunkReq)
{
var chunkFile = Request.Form.Files.FirstOrDefault(f => f.Name == "file") ?? Request.Form.Files.FirstOrDefault();
await _file.SaveChunkAsync(guid, chunk, chunkFile);
await _file.SaveChunkAsync(guid, string.IsNullOrEmpty(chunk) ? chunkIndex : chunk, file);
return new { code = 1, type = "chunk", msg = "分片上传成功!" };
}
FileObj url;
string errMsg = "上传失败!";
if (string.IsNullOrEmpty(fileName))
if (!string.IsNullOrEmpty(fileName) && file == null)
{
url = await _file.SaveFileAsync(apptype, Request.Form.Files.FirstOrDefault());
url = await _file.MergeSaveAsync(apptype, guid, fileName); // 合并分片
}
else
{
url = await _file.MergeSaveAsync(apptype, guid, fileName);
url = await _file.SaveFileAsync(apptype, file); // 单文件直传
}
if (url == null)
throw new DException(errMsg);
throw new DException("上传失败!");
// 旧 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>取表单/URL 参数(旧版读 Request["x"] 同时覆盖 Query+Form)。</summary>
private string FormOrQuery(string name)
{
var v = Request.Form[name].ToString();
if (!string.IsNullOrEmpty(v)) return v;
return Request.Query[name].ToString();
}
/// <summary>公用图片上传(旧 FileUploadImage,[NoAuth];多图,返回数组)。</summary>
[HttpPost("FileUploadImage")]
public async Task<object> UploadImg()
@@ -49,7 +49,7 @@ namespace YhCore.WebApi.Controllers.Api.Common
/// <summary>添加审核流程。</summary>
[HttpPost("Create")]
public async Task Create([FromBody] Common_flowinfo model)
public async Task Create([FromForm] Common_flowinfo model)
{
model.schSerID = SchSerID();
await _svc.CreateAsync(model);
@@ -57,7 +57,7 @@ namespace YhCore.WebApi.Controllers.Api.Common
/// <summary>修改审核流程。</summary>
[HttpPost("Edit")]
public async Task Edit([FromBody] Common_flowinfo model)
public async Task Edit([FromForm] Common_flowinfo model)
{
model.schSerID = SchSerID();
await _svc.EditAsync(model);
@@ -65,12 +65,12 @@ namespace YhCore.WebApi.Controllers.Api.Common
/// <summary>添加步骤流程。</summary>
[HttpPost("StepCreate")]
public Task StepCreate([FromBody] Common_flowstep model)
public Task StepCreate([FromForm] Common_flowstep model)
=> _svc.StepCreateAsync(model);
/// <summary>修改步骤流程。</summary>
[HttpPost("StepEdit")]
public Task StepEdit([FromBody] Common_flowstep model)
public Task StepEdit([FromForm] Common_flowstep model)
=> _svc.StepEditAsync(model);
/// <summary>删除审核流程(ids 逗号分隔)。</summary>
@@ -80,7 +80,7 @@ namespace YhCore.WebApi.Controllers.Api.Common
/// <summary>删除步骤。</summary>
[HttpPost("StepDelete")]
public Task StepDelete([FromBody] decimal[] ids)
public Task StepDelete([FromForm] decimal[] ids)
=> _svc.StepDeleteAsync(ids);
/// <summary>流程绑定的步骤列表。</summary>
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Yh.Platform.Abstractions;
using YhCore.Common;
using YhCore.Common.UserContext;
@@ -15,8 +17,10 @@ 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,延后迁移。
/// 45 action 全部落地(Excel 导入导出 5 个走 NPOI/EPPlus,审批单 PDF 走 wkhtmltopdf)。
/// POST 动作一律 [FromForm]:前端 api/index.js 固定 x-www-form-urlencoded 提交([FromForm] 会 415);
/// 老契约"表单字段塞 JSON"的动作(WYSHRole/SHTeaTxt/listXxxTxt/data/docdata/docnumdata)
/// 以 [FromForm] string 接收后手动反序列化。
/// </summary>
[EnableCors("default")]
[ApiController]
@@ -30,6 +34,14 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
_role = role;
}
/// <summary>解析老契约"表单字段塞 JSON"的值;空串安全,非法 JSON 返回 null 由调用方判参。</summary>
private static T ParseFormJson<T>(string txt) where T : class
{
if (string.IsNullOrWhiteSpace(txt)) return null;
try { return JsonConvert.DeserializeObject<T>(txt); }
catch (JsonException) { return null; }
}
#region 审核角色管理
/// <summary>学校是否启用审核角色(旧 getIsSHRole)。</summary>
@@ -49,18 +61,20 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
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>
/// <summary>新增/编辑角色(旧 EditSHRole,form: WYSHRole=JSON 字符串)。</summary>
[HttpPost("EditSHRole")]
public async Task<string> EditSHRole([FromBody] WYSHRoleExt model)
public async Task<string> EditSHRole([FromForm] string WYSHRole)
{
var model = ParseFormJson<WYSHRoleExt>(WYSHRole);
if (model == null) throw new DException("参数错误!");
return await _role.EditSHRoleAsync(SchSerID(), UserId(), model);
}
/// <summary>编辑成员行(旧 editSHTeaRow,form: SHTeaTxt → [FromBody])。</summary>
/// <summary>编辑成员行(旧 editSHTeaRow,form: SHTeaTxt=JSON 字符串)。</summary>
[HttpPost("editSHTeaRow")]
public async Task<string> EditSHTeaRow([FromBody] WY_SHRole_SHTea model)
public async Task<string> EditSHTeaRow([FromForm] string SHTeaTxt)
{
var model = ParseFormJson<WY_SHRole_SHTea>(SHTeaTxt);
if (model == null) throw new DException("参数错误!");
return await _role.EditSHTeaRowAsync(SchSerID(), UserId(), model);
}
@@ -70,35 +84,41 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
public async Task<string> DelSHTeaRow([FromQuery] decimal serID)
=> await _role.DelSHTeaRowAsync(SchSerID(), serID);
/// <summary>批量保存审核人(旧 editSHTea,form: SHRoleID + listSHTeaTxt → [FromBody])。</summary>
/// <summary>批量保存审核人(旧 editSHTea,form: SHRoleID + listSHTeaTxt=JSON 数组字符串)。</summary>
[HttpPost("editSHTea")]
public async Task<string> EditSHTea([FromBody] EditSHTeaDto model)
public async Task<string> EditSHTea([FromForm] decimal SHRoleID, [FromForm] string listSHTeaTxt)
{
if (model == null) throw new DException("参数错误!");
return await _role.EditSHTeaAsync(SchSerID(), UserId(), model.SHRoleID, model.listSHTea);
var list = ParseFormJson<List<WY_SHRole_SHTea>>(listSHTeaTxt) ?? new List<WY_SHRole_SHTea>();
return await _role.EditSHTeaAsync(SchSerID(), UserId(), SHRoleID, list);
}
/// <summary>批量保存被审核人(旧 editBSHTea,form: SHRoleID + listBSHTeaTxt → [FromBody])。</summary>
/// <summary>批量保存被审核人(旧 editBSHTea,form: SHRoleID + listBSHTeaTxt=JSON 数组字符串)。</summary>
[HttpPost("editBSHTea")]
public async Task<string> EditBSHTea([FromBody] EditBSHTeaDto model)
public async Task<string> EditBSHTea([FromForm] decimal SHRoleID, [FromForm] string listBSHTeaTxt)
{
if (model == null) throw new DException("参数错误!");
return await _role.EditBSHTeaAsync(SchSerID(), UserId(), model.SHRoleID, model.listBSHTea);
var list = ParseFormJson<List<WY_SHRole_BSHTea>>(listBSHTeaTxt) ?? new List<WY_SHRole_BSHTea>();
return await _role.EditBSHTeaAsync(SchSerID(), UserId(), SHRoleID, list);
}
/// <summary>删除角色(旧 delSHRole,form: listSHRoleIDTxt → [FromBody])。</summary>
/// <summary>删除角色(旧 delSHRole,form: listSHRoleIDTxt=JSON 数组字符串,兼容逗号分隔)。</summary>
[HttpPost("delSHRole")]
public async Task<string> DelSHRole([FromBody] DelSHRoleDto model)
public async Task<string> DelSHRole([FromForm] string listSHRoleIDTxt)
{
if (model == null) throw new DException("参数错误!");
return await _role.DelSHRoleAsync(SchSerID(), model.listSHRoleID);
var list = ParseFormJson<List<decimal?>>(listSHRoleIDTxt);
if (list == null)
{
list = (listSHRoleIDTxt ?? "").Split(',')
.Where(a => !string.IsNullOrWhiteSpace(a) && decimal.TryParse(a.Trim(), out _))
.Select(a => (decimal?)decimal.Parse(a.Trim())).ToList();
}
return await _role.DelSHRoleAsync(SchSerID(), list);
}
#endregion
#region 审核角色流程
/// <summary>审核流程列表(旧 ListFlow)。</summary>
/// <summary>审核流程列表(旧 ListFlow;分页返回 PageVO,Content 内对象字段与旧 Data 对齐)。</summary>
[HttpGet("ListFlow")]
public async Task<PageVO<SHRoleFlowListItem>> ListFlow(
[FromQuery] string name = null,
@@ -110,7 +130,7 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
/// <summary>新增/编辑流程(旧 EditFlow;schSerID 取当前用户)。</summary>
[HttpPost("EditFlow")]
public async Task<string> EditFlow([FromBody] Common_flowinfo model)
public async Task<string> EditFlow([FromForm] Common_flowinfo model)
{
if (model == null) throw new DException("参数错误!");
model.schSerID = SchSerID();
@@ -119,7 +139,7 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
/// <summary>新增步骤(旧 StepCreateOfSHRole)。</summary>
[HttpPost("StepCreateOfSHRole")]
public async Task<string> StepCreateOfSHRole([FromBody] Common_flowstep model)
public async Task<string> StepCreateOfSHRole([FromForm] Common_flowstep model)
{
if (model == null) throw new DException("参数错误!");
return await _role.StepCreateOfSHRoleAsync(model);
@@ -180,11 +200,22 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
public async Task<List<Common_Dictionary>> GetDicInfo([FromQuery] string typeID)
=> await _role.GetDicInfoAsync(SchSerID(), typeID);
/// <summary>编辑文印(审核角色视角;旧 editDocPrint,form 三段 JSON → [FromBody])。</summary>
/// <summary>编辑文印(审核角色视角;旧 editDocPrint,form: isedit + data/docdata/docnumdata 三段 JSON 字符串)。</summary>
[HttpPost("editDocPrint")]
public async Task<int> EditDocPrint([FromBody] WYSHRoleEditDocPrintDto model)
public async Task<int> EditDocPrint(
[FromForm] int isedit,
[FromForm] string data,
[FromForm] string docdata,
[FromForm] string docnumdata)
{
if (model == null) throw new DException("参数错误!");
var model = new WYSHRoleEditDocPrintDto
{
isedit = isedit,
data = ParseFormJson<WYFlowForUser>(data),
docdata = ParseFormJson<WYDocPrintSaveDto>(docdata),
docnumdata = ParseFormJson<List<WY_DocPrint_Num>>(docnumdata) ?? new List<WY_DocPrint_Num>()
};
if (model.data == null || model.docdata == null) throw new DException("参数错误!");
return await _role.EditDocPrintAsync(SchSerID(), UserId(), model);
}
@@ -207,9 +238,9 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
public async Task<List<SHRoleDocPrintListItem>> MyListtoOneSHRole([FromQuery] int serID)
=> await _role.MyListtoOneSHRoleAsync(serID, SchSerID());
/// <summary>我的文印(旧 MyListSHRole)。</summary>
/// <summary>我的文印(旧 MyListSHRole,分页返回 PageVO)。</summary>
[HttpGet("MyListSHRole")]
public async Task<List<SHRoleDocPrintListItem>> MyListSHRole(
public async Task<PageVO<SHRoleDocPrintListItem>> MyListSHRole(
[FromQuery] string name = null,
[FromQuery] string status = null,
[FromQuery] string startDate = null,
@@ -218,9 +249,9 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
[FromQuery] int pageIndex = 0)
=> await _role.MyListSHRoleAsync(name, status, startDate, endDate, pageSize, pageIndex, UserId(), SchSerID());
/// <summary>全部文印(旧 totalListSHRole)。</summary>
/// <summary>全部文印(旧 totalListSHRole,分页返回 PageVO)。</summary>
[HttpGet("totalListSHRole")]
public async Task<List<SHRoleDocPrintListItem>> TotalListSHRole(
public async Task<PageVO<SHRoleDocPrintListItem>> TotalListSHRole(
[FromQuery] int flowid = 0,
[FromQuery] int dwid = 0,
[FromQuery] string name = null,
@@ -231,9 +262,9 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
[FromQuery] int pageIndex = 0)
=> await _role.TotalListSHRoleAsync(flowid, dwid, name, stateid, starttime, endtime, pageSize, pageIndex, SchSerID());
/// <summary>抄送列表(旧 SendRdListSHRole)。</summary>
/// <summary>抄送列表(旧 SendRdListSHRole,分页返回 PageVO)。</summary>
[HttpGet("SendRdListSHRole")]
public async Task<List<SHRoleDocPrintListItem>> SendRdListSHRole(
public async Task<PageVO<SHRoleDocPrintListItem>> SendRdListSHRole(
[FromQuery] int flowid = 0,
[FromQuery] int dwid = 0,
[FromQuery] string name = null,
@@ -244,9 +275,9 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
[FromQuery] int pageIndex = 0)
=> await _role.SendRdListSHRoleAsync(flowid, dwid, name, status, startDate, endDate, UserId(), SchSerID(), pageSize, pageIndex);
/// <summary>审批列表(旧 SPListSHRole)。</summary>
/// <summary>审批列表(旧 SPListSHRole,分页返回 PageVO)。</summary>
[HttpGet("SPListSHRole")]
public async Task<List<SHRoleDocPrintListItem>> SPListSHRole(
public async Task<PageVO<SHRoleDocPrintListItem>> SPListSHRole(
[FromQuery] int flowid = 0,
[FromQuery] int dwid = 0,
[FromQuery] string name = null,
@@ -259,23 +290,89 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
/// <summary>审核确认(旧 shenheokSHRole)。</summary>
[HttpPost("shenheokSHRole")]
public async Task<string> ShenheOkSHRole([FromBody] FlowTranctDto model)
public async Task<string> ShenheOkSHRole([FromForm] 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)
#region Excel 导入导出 / 审批单 PDF
/// <summary>全部文印导出(旧 ExceltotalListSHRole,NPOI .xls)。</summary>
[HttpGet("ExceltotalListSHRole")]
public async Task<IActionResult> ExceltotalListSHRole(
[FromQuery] string Name,
[FromQuery] string stateid,
[FromQuery] string starttime,
[FromQuery] string endtime)
{
var (content, fileName) = await _role.ExceltotalListSHRoleAsync(SchSerID(), Name, stateid, starttime, endtime);
return File(content, ContentType(fileName), fileName);
}
/// <summary>文印记录导出(旧 WYRDExceldaochu,flag=page 简版 / 完整版,EPPlus)。</summary>
[HttpGet("WYRDExceldaochu")]
public async Task<IActionResult> WYRDExceldaochu(
[FromQuery] int flowid,
[FromQuery] int DWid,
[FromQuery] string Name,
[FromQuery] string stateid,
[FromQuery] string starttime,
[FromQuery] string endtime,
[FromQuery] string flag)
{
var (content, fileName) = await _role.WYRDExceldaochuAsync(SchSerID(), flowid, DWid, Name, stateid, starttime, endtime, flag);
return File(content, ContentType(fileName), fileName);
}
/// <summary>Excel 批量导入审核角色(旧 UpfileExcelSHRole,multipart 文件字段名不敏感取第一个)。</summary>
[HttpPost("UpfileExcelSHRole")]
public async Task<string> UpfileExcelSHRole()
{
var file = Request.Form.Files.Count > 0 ? Request.Form.Files[0] : null;
return await _role.UpfileExcelSHRoleAsync(SchSerID(), UserId(), file);
}
/// <summary>下载导入模板(旧 downMBOfSHRole,代码生成)。</summary>
[HttpGet("downMBOfSHRole")]
public async Task<IActionResult> DownMBOfSHRole()
{
var content = await _role.DownMBOfSHRoleAsync();
return File(content, "application/vnd.ms-excel", "批量导入审核角色.xls");
}
/// <summary>下载最近一次导入的反馈 Excel(旧 downUpfileSHRoleFK)。</summary>
[HttpGet("downUpfileSHRoleFK")]
public async Task<IActionResult> DownUpfileSHRoleFK()
{
var content = await _role.DownUpfileSHRoleFKAsync(UserId());
return File(content, "application/vnd.ms-excel", "批量导入审核角色反馈.xls");
}
/// <summary>审批单 PDF 下载(旧 DwonWYSQD,IDS 逗号分隔的 WY_DocPrint.SerID;依赖服务器安装 wkhtmltopdf)。</summary>
[HttpGet("DwonWYSQD")]
public async Task<IActionResult> DwonWYSQD([FromQuery] string IDS)
{
var (content, fileName) = await _role.DwonWYSQDAsync(SchSerID(), UserId(), IDS);
return File(content, "application/pdf", fileName);
}
/// <summary>按扩展名取下载 Content-Type(对齐 CommonController.GetContentType 约定)。</summary>
private static string ContentType(string fileName)
=> fileName != null && fileName.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)
? "application/pdf"
: "application/vnd.ms-excel";
#endregion
#endregion
#region 签名 / 退回 / 金额重提
/// <summary>保存签名(旧 SaveSignature,form: pic → [FromBody])。</summary>
/// <summary>保存签名(旧 SaveSignature,form: pic → [FromForm])。</summary>
[HttpPost("SaveSignature")]
public async Task<string> SaveSignature([FromBody] SaveSignatureDto model)
public async Task<string> SaveSignature([FromForm] SaveSignatureDto model)
{
if (model == null) throw new DException("参数错误!");
return await _role.SaveSignatureAsync(SchSerID(), UserId(), model.pic);
@@ -289,9 +386,9 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
[HttpGet("DelSignature")]
public async Task<string> DelSignature() => await _role.DelSignatureAsync(SchSerID(), UserId());
/// <summary>退回(旧 TuiHui,form: DocID + cont → [FromBody])。</summary>
/// <summary>退回(旧 TuiHui,form: DocID + cont → [FromForm])。</summary>
[HttpPost("TuiHui")]
public async Task<string> TuiHui([FromBody] TuiHuiDto model)
public async Task<string> TuiHui([FromForm] TuiHuiDto model)
{
if (model == null) throw new DException("参数错误!");
return await _role.TuiHuiAsync(UserId(), model.cont, model.DocID);
@@ -302,9 +399,9 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
public async Task<string> UnTuiHui([FromQuery] decimal docID)
=> await _role.UnTuiHuiAsync(UserId(), docID);
/// <summary>金额重提(旧 regetJE,form: DocID 逗号分隔 + type → [FromBody])。</summary>
/// <summary>金额重提(旧 regetJE,form: DocID 逗号分隔 + type → [FromForm])。</summary>
[HttpPost("regetJE")]
public async Task<string> RegetJE([FromBody] RegetJEDto model)
public async Task<string> RegetJE([FromForm] RegetJEDto model)
{
if (model == null) throw new DException("参数错误!");
var ids = (model.DocID ?? "").Split(',')
@@ -313,9 +410,9 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
return await _role.RegetJEAsync(SchSerID(), UserId(), ids, model.type);
}
/// <summary>重提记录(旧 getRegetJERd)。</summary>
/// <summary>重提记录(旧 getRegetJERd,分页返回 PageVO)。</summary>
[HttpGet("getRegetJERd")]
public async Task<List<SHRoleRegetJERdItem>> GetRegetJERd(
public async Task<PageVO<SHRoleRegetJERdItem>> GetRegetJERd(
[FromQuery] int pageSize = 10,
[FromQuery] int pageIndex = 0)
=> await _role.GetRegetJERdAsync(SchSerID(), pageSize, pageIndex);
@@ -8,6 +8,7 @@ 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
@@ -37,7 +38,7 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
/// <summary>添加文印分类。</summary>
[HttpPost("AddDic")]
public async Task AddDic([FromBody] Common_Dictionary model)
public async Task AddDic([FromForm] Common_Dictionary model)
{
if (model == null) throw new DException("参数错误!");
model.Schserid = SchSerID();
@@ -51,7 +52,7 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
/// <summary>添加印制版面。</summary>
[HttpPost("AddYZBM")]
public async Task AddYZBM([FromBody] Common_Dictionary model)
public async Task AddYZBM([FromForm] Common_Dictionary model)
{
if (model == null) throw new DException("参数错误!");
model.Schserid = SchSerID();
@@ -80,20 +81,27 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
[FromQuery] bool showAll = false)
=> await _classify.DictionaryTypeListAsync(SchSerID(), name, showAll, typeID);
/// <summary>添加公共类别(复用字典添加逻辑)。</summary>
/// <summary>添加公共类别(复用字典添加逻辑)。前端 api/index.js 固定以 x-www-form-urlencoded 提交
/// (TypeId/Name/typeName 表单字段,大小写不敏感绑定到 typeID/Name/typeName),故用 [FromForm];
/// 若误用 [FromBody],Content-Type 不符会返回 415。</summary>
[HttpPost("AddDictionaryType")]
public async Task AddDictionaryType([FromBody] Common_Dictionary model)
public async Task AddDictionaryType([FromForm] Common_Dictionary model)
{
if (model == null) throw new DException("参数错误!");
// 表单绑定实例不会为 null,但字段名拼错(如 TypelD≠TypeId)时必填列会为 null,
// SaveChanges 才抛"不能将 NULL 插入列"(code:6 无从排查)——在入口显式校验给出可读错误。
if (model == null || string.IsNullOrWhiteSpace(model.typeID)
|| string.IsNullOrWhiteSpace(model.typeName) || string.IsNullOrWhiteSpace(model.Name))
throw new DException("参数错误:typeID、typeName、Name 均不能为空(注意表单字段名拼写,TypeId≠TypelD)");
model.Schserid = SchSerID();
await _dict.AddDicAsync(model);
}
/// <summary>编辑公共类别(旧调 GetSysDict.EditDic)。</summary>
/// <summary>编辑公共类别(旧调 GetSysDict.EditDic)。同 AddDictionaryType:前端表单编码提交,[FromForm] 绑定。</summary>
[HttpPost("EditDictionaryType")]
public async Task EditDictionaryType([FromBody] Common_Dictionary model)
public async Task EditDictionaryType([FromForm] Common_Dictionary model)
{
if (model == null) throw new DException("参数有误!");
if (model == null || model.SerID <= 0 || string.IsNullOrWhiteSpace(model.Name))
throw new DException("参数错误:SerID、Name 均不能为空(注意表单字段名拼写,TypeId≠TypelD)");
model.Schserid = SchSerID();
await _dict.EditDicAsync(model);
}
@@ -142,9 +150,9 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
#region 文印核心(第三批 D1:抄送 / 附件 / CRUD / 列表 / 审核 / 统计 / 指派)
/// <summary>抄送列表(旧 SendRdList)。</summary>
/// <summary>抄送列表(旧 SendRdList,分页返回 PageVO)。</summary>
[HttpGet("SendRdList")]
public async Task<System.Collections.Generic.List<DocPrintListItem>> SendRdList(
public async Task<PageVO<DocPrintListItem>> SendRdList(
[FromQuery] string name = null,
[FromQuery] string startDate = null,
[FromQuery] string endDate = null,
@@ -169,7 +177,7 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
/// <summary>添加文印(旧 Create;短信/待办推送已降级为纯落库)。</summary>
[HttpPost("Create")]
public async Task<int> Create([FromBody] WYDocPrintSaveDto model)
public async Task<int> Create([FromForm] WYDocPrintSaveDto model)
{
if (model == null) throw new DException("参数错误!");
model.UserID = UserId();
@@ -179,7 +187,7 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
/// <summary>修改文印(旧 Edit)。</summary>
[HttpPost("Edit")]
public async Task<int> Edit([FromBody] WYDocPrintSaveDto model)
public async Task<int> Edit([FromForm] WYDocPrintSaveDto model)
{
if (model == null) throw new DException("参数错误!");
model.UserID = UserId();
@@ -189,16 +197,16 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
/// <summary>撤销(旧 Revoke)。</summary>
[HttpPost("Revoke")]
public async Task<string> Revoke([FromBody] FlowTranctDto model)
public async Task<string> Revoke([FromForm] FlowTranctDto model)
{
if (model == null) throw new DException("参数错误!");
model.actionuserid = UserContextHolder.Current?.Id;
return await _wy.RevokeAsync(model);
}
/// <summary>我的文印列表(旧 MyList)。</summary>
/// <summary>我的文印列表(旧 MyList,分页返回 PageVO)。</summary>
[HttpGet("MyList")]
public async Task<System.Collections.Generic.List<DocPrintListItem>> MyList(
public async Task<PageVO<DocPrintListItem>> MyList(
[FromQuery] string name = null,
[FromQuery] string status = null,
[FromQuery] string startDate = null,
@@ -207,24 +215,24 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
[FromQuery] int pageIndex = 0)
=> await _wy.MyListAsync(name, status, startDate, endDate, UserId(), SchSerID(), pageSize, pageIndex);
/// <summary>全部文印列表(旧 AllList 四分类,返回 ctype)。</summary>
/// <summary>全部文印列表(旧 AllList 四分类,返回 ctype;分页返回 PageVO)。</summary>
[HttpGet("AllList")]
public async Task<System.Collections.Generic.List<DocPrintListItem>> AllList(
public async Task<PageVO<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>
/// <summary>文印审核列表(旧 ShenheList;旧 Name 过滤为死代码,保持不生效;分页返回 PageVO)。</summary>
[HttpGet("ShenheList")]
public async Task<System.Collections.Generic.List<DocPrintListItem>> ShenheList(
public async Task<PageVO<DocPrintListItem>> ShenheList(
[FromQuery] int pageSize = 10,
[FromQuery] int pageIndex = 0)
=> await _wy.ShenheListAsync(UserId(), SchSerID(), pageSize, pageIndex);
/// <summary>审批列表(旧 SPList)。</summary>
/// <summary>审批列表(旧 SPList,分页返回 PageVO)。</summary>
[HttpGet("SPList")]
public async Task<System.Collections.Generic.List<DocPrintListItem>> SPList(
public async Task<PageVO<DocPrintListItem>> SPList(
[FromQuery] string name = null,
[FromQuery] string status = null,
[FromQuery] string startDate = null,
@@ -233,17 +241,17 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
[FromQuery] int pageIndex = 0)
=> await _wy.SPListAsync(name, status, startDate, endDate, UserId(), SchSerID(), pageSize, pageIndex);
/// <summary>文印已审列表(旧 YishenList)。</summary>
/// <summary>文印已审列表(旧 YishenList,分页返回 PageVO)。</summary>
[HttpGet("YishenList")]
public async Task<System.Collections.Generic.List<DocPrintListItem>> YishenList(
public async Task<PageVO<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>
/// <summary>全部文印列表(旧 totalList,分页返回 PageVO)。</summary>
[HttpGet("totalList")]
public async Task<System.Collections.Generic.List<DocPrintListItem>> TotalList(
public async Task<PageVO<DocPrintListItem>> TotalList(
[FromQuery] string name = null,
[FromQuery] string stateid = null,
[FromQuery] string starttime = null,
@@ -259,7 +267,7 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
/// <summary>审核确认(旧 shenheok)。</summary>
[HttpPost("shenheok")]
public async Task<string> ShenheOk([FromBody] FlowTranctDto model)
public async Task<string> ShenheOk([FromForm] FlowTranctDto model)
{
if (model == null) throw new DException("参数错误!");
model.actionuserid = UserContextHolder.Current?.Id ?? 0;
@@ -268,7 +276,7 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
/// <summary>领取确认(旧 AffirmApplyforOk)。</summary>
[HttpPost("AffirmApplyforOk")]
public async Task<string> AffirmApplyforOk([FromBody] FlowTranctDto model)
public async Task<string> AffirmApplyforOk([FromForm] FlowTranctDto model)
{
if (model == null) throw new DException("参数错误!");
model.actionuserid = UserContextHolder.Current?.Id ?? 0;
@@ -298,30 +306,30 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
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>
/// <summary>文印详情统计列表(旧 List2,分页返回 PageVO)。</summary>
[HttpGet("List2")]
public async Task<System.Collections.Generic.List<DocDetailStatItem>> List2(
public async Task<PageVO<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>
/// <summary>文印数量按用户排名(旧 List3,分页返回 PageVO)。</summary>
[HttpGet("List3")]
public async Task<System.Collections.Generic.List<UserRankItem>> List3(
public async Task<PageVO<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>
/// <summary>按月份统计明细(旧 List4,分页返回 PageVO)。</summary>
[HttpGet("List4")]
public async Task<System.Collections.Generic.List<StatListItem>> List4(
public async Task<PageVO<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>
/// <summary>按用户统计明细(旧 List5,分页返回 PageVO)。</summary>
[HttpGet("List5")]
public async Task<System.Collections.Generic.List<StatListItem>> List5(
public async Task<PageVO<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);
@@ -81,6 +81,12 @@ namespace YhCore.WebApi.Middleware
{
sb.Append(" | exception=").Append(exceptionSummary);
}
else if (context.Items.TryGetValue("ExceptionSummary", out var inner)
&& !string.IsNullOrEmpty(inner as string))
{
// GlobalExceptionMiddleware 在内侧把异常转成了 200 响应,摘要经 HttpContext.Items 递出
sb.Append(" | exception=").Append(inner);
}
Log.Info(sb.ToString());
}
@@ -0,0 +1,61 @@
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
namespace YhCore.WebApi.Middleware
{
/// <summary>
/// DebugUser 模拟登录中间件(对齐老 BestSchool/Java 系统的 DebugUser 调试机制;
/// 前端 api/index.js 每次请求固定携带 DebugAppType=true 与 DebugUser=&lt;loginName&gt; 头)。
///
/// 用途:解决「本地前端(localhost:8103) 直连部署后端」的登录态跨域问题——
/// Cookie 按 host 隔离且 SameSite=Lax 不随跨站 XHR 携带,CAS 在线上域名种下的登录态
/// 永远无法回流 localhost 页面的请求;本中间件以请求头承载身份,绕开该物理限制。
///
/// 生效条件(全部满足):
/// 1. 配置开关 yh:auth:DebugUserEnabled = true(默认 false,生产环境勿开——伪造该头即可冒充任意用户);
/// 2. 当前请求未认证(正常 CAS 登录态优先,互不干扰);
/// 3. DebugAppType=true 且 DebugUser 非空。
///
/// 动作:按 DebugUser 值构造 loginName 身份(Claims:Name/NameIdentifier/loginName,与 CAS handler 一致),
/// 写入 Context.User。后续 UseYhUserContext 据此经 IUserDetailsProvider(UserBase /oapi/user/byLoginName)
/// 解析完整 BoaUser;loginName 无对应用户时接口返回空数据,可据此发现账号填错。
///
/// 注册位置:UseAuthentication 之后、SsoProtectionMiddleware 之前。
/// </summary>
public class DebugUserMiddleware
{
private readonly RequestDelegate _next;
private readonly bool _enabled;
public DebugUserMiddleware(RequestDelegate next, IConfiguration configuration)
{
_next = next;
_enabled = configuration.GetValue("yh:auth:DebugUserEnabled", false);
}
public async Task InvokeAsync(HttpContext context)
{
if (_enabled
&& context.User?.Identity?.IsAuthenticated != true
&& string.Equals(context.Request.Headers["DebugAppType"].ToString(), "true", StringComparison.OrdinalIgnoreCase))
{
var loginName = context.Request.Headers["DebugUser"].ToString().Trim();
if (!string.IsNullOrEmpty(loginName))
{
var claims = new[]
{
new Claim(ClaimTypes.Name, loginName),
new Claim(ClaimTypes.NameIdentifier, loginName),
new Claim("loginName", loginName),
};
context.User = new ClaimsPrincipal(new ClaimsIdentity(claims, "DebugUser"));
}
}
await _next(context);
}
}
}
@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using log4net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
@@ -18,6 +19,8 @@ namespace YhCore.WebApi.Middleware
/// </summary>
public class GlobalExceptionMiddleware
{
private static readonly ILog Log = LogManager.GetLogger(typeof(GlobalExceptionMiddleware));
private readonly RequestDelegate _next;
public GlobalExceptionMiddleware(RequestDelegate next)
@@ -53,9 +56,13 @@ namespace YhCore.WebApi.Middleware
timeNow = TimeNow()
});
}
catch (Exception)
catch (Exception ex)
{
// 未捕获异常 → 平台兜底:200 + code:6 内部服务错误(对齐 Java GlobalExceptionHandler.handleException)
// 本中间件位于 ApiAccessLogMiddleware 内侧,异常不会穿透到它的 catch,
// 必须在此落日志,否则 code:6 无迹可寻(摘要随接口访问日志一并输出)
context.Items["ExceptionSummary"] = ex.Message;
Log.Error($"未捕获异常 {context.Request.Method} {context.Request.Path}{context.Request.QueryString}", ex);
await WriteJsonAsync(context, StatusCodes.Status200OK, new
{
code = ErrorCode.InternalServerError.Code,
+33 -6
View File
@@ -80,11 +80,14 @@ namespace YhCore.WebApi
});
};
})
// 对齐 Java Jackson:日期格式 yyyy-MM-dd HH:mm:ss + 字段 camelCase(code/msg/data)
// 对齐旧项目 DocPrint WebApiConfig:属性名原样输出(DefaultContractResolver),
// data 内容字段与旧接口逐字段一致(SerID/DocContent/teaname 等混合大小写 as-is);
// 日期对齐旧 IsoDateTimeConverter "yyyy/MM/dd HH:mm:ss"。
// 信封 code/msg/data 由 ApiResult 的 [JsonProperty] 显式固定小写,不受 as-is 影响。
.AddNewtonsoftJson(o =>
{
o.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";
o.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
o.SerializerSettings.DateFormatString = "yyyy/MM/dd HH:mm:ss";
o.SerializerSettings.ContractResolver = new DefaultContractResolver();
})
// 对应 Java Hibernate Validator
.AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<Startup>());
@@ -119,6 +122,10 @@ namespace YhCore.WebApi
// Redis 缓存(RedisUtil 单例,连接串取 Redis:ConnectionString)
services.AddRedisCache(Configuration);
// 进程内内存缓存:WYSHRole Excel 导入的反馈表(updateSHRole_{userId},单实例;
// 多实例部署需改 Redis 支撑的分布式缓存)
services.AddMemoryCache();
// ===== 会话状态(Session)=====
// 对应 Java HttpSession / session.invalidate();旧 DocPrintCore 同款配置。
// 注意:YhAuth 的登录态走自定义短 Cookie(SessionTicketDataFormat,票据存进程内存),
@@ -168,9 +175,19 @@ namespace YhCore.WebApi
.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 Config):文件路径由配置注入。
// FILE_PATH 必须以 / 结尾(旧约定:FileService 多处 FilePath + "{appType}/..." 直接拼接,缺斜杠会拼出坏路径);
// FILE_WEB_PATH 不带尾 /(拼接目标 fileurl 均以 / 开头,带尾斜杠会产生 //)
var configFilePath = Configuration["FILE_PATH"];
YhCore.Common.Config.Config.FilePath = string.IsNullOrEmpty(configFilePath)
? configFilePath
: configFilePath.TrimEnd('/').Replace('\\', '/') + "/";
var configFileWebPath = Configuration["FILE_WEB_PATH"];
YhCore.Common.Config.Config.FileWebPath = string.IsNullOrEmpty(configFileWebPath)
? configFileWebPath
: configFileWebPath.TrimEnd('/');
// 审批单 PDF:wkhtmltopdf 可执行文件路径(空则用 PATH)
YhCore.Common.Config.Config.WkHtmlToPdfPath = Configuration["wkhtmltopdf_path"];
// 接口文档:对应 Java SpringDoc,按渠道分三组(api / mobiapi / oapi)
services.AddSwaggerGen(c =>
@@ -185,6 +202,12 @@ namespace YhCore.WebApi
In = ParameterLocation.Header,
Description = "登录后携带的令牌(对接 CAS SSO 后使用)"
});
// 加载 XML 注释:控制器/动作的 summary 中文描述展示到 Swagger(csproj GenerateDocumentationFile)
var xml = $"{System.AppContext.BaseDirectory}{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml";
if (System.IO.File.Exists(xml))
{
c.IncludeXmlComments(xml, includeControllerXmlComments: true);
}
});
// 任务调度:Quartz 宿主 + Job 模板(对应 Java scheduler 包)
@@ -230,6 +253,10 @@ namespace YhCore.WebApi
app.UseMiddleware<ApiAccessLogMiddleware>(); // 0. 接口访问日志(唯一日志出口)
app.UseMiddleware<GlobalExceptionMiddleware>(); // 1. 异常兜底(异常摘要并入接口日志)
app.UseAuthentication(); // 2. CAS ticket 校验 + Cookie 恢复(Yh.Platform.Auth)
// 2.5 DebugUser 模拟登录(开关 yh:auth:DebugUserEnabled,默认关):本地前端直连部署后端的
// 跨域联调身份方案——Cookie 按 host 隔离导致线上登录态无法回流 localhost 页面请求,
// 以 DebugUser 请求头承载身份绕开。必须位于认证之后、SSO 保护之前。
app.UseMiddleware<DebugUserMiddleware>();
app.UseMiddleware<SsoProtectionMiddleware>(); // 3. SSO 路径保护(未认证 → 302 CAS;认证通过后才有用户身份)
app.UseYhUserContext(); // 4. 新框架用户上下文:BoaUserContext + UserContextHolder(LoginUser)
// 5. 应用间签名(oapi 渠道)。必须排在 UseYhUserContext 之后:
+8 -5
View File
@@ -6,6 +6,9 @@
<AssemblyName>YhCore.WebApi</AssemblyName>
<LangVersion>9.0</LangVersion>
<Nullable>disable</Nullable>
<!-- 生成 XML 注释文件供 Swagger 展示接口描述;1591=无 XML 注释的公共成员告警(大量 DTO 属性无注释,忽略) -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -33,11 +36,11 @@
<ItemGroup>
<!-- 平台基础(Abstractions/AppAuth/Auth/OpenApi + 教务 SDK):从 Nexus nuget-group 引用(原为本地源项目引用) -->
<PackageReference Include="Yh.Platform.Abstractions" Version="1.0.0-preview.1" />
<PackageReference Include="Yh.Platform.AppAuth" Version="1.0.0-preview.1" />
<PackageReference Include="Yh.Platform.Auth" Version="1.0.0-preview.1" />
<PackageReference Include="Yh.Platform.OpenApi" Version="1.0.0-preview.1" />
<PackageReference Include="Yh.SchClass.OpenApi" Version="1.0.0-preview.1" />
<PackageReference Include="Yh.Platform.Abstractions" Version="1.0.0-preview.2" />
<PackageReference Include="Yh.Platform.AppAuth" Version="1.0.0-preview.2" />
<PackageReference Include="Yh.Platform.Auth" Version="1.0.0-preview.2" />
<PackageReference Include="Yh.Platform.OpenApi" Version="1.0.0-preview.2" />
<PackageReference Include="Yh.SchClass.OpenApi" Version="1.0.0-preview.2" />
</ItemGroup>
</Project>
+7 -3
View File
@@ -4,17 +4,21 @@
"AutoCreateMissingTables": true
},
"ConnectionStrings": {
"SqlServer": "Server=localhost;Database=EduCloudDocPrint;User Id=sa;Password=Password01!;MultipleActiveResultSets=true",
"SqlServer": "Server=localhost;Database=EduCloudDocPrint2;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"
},
"FILE_PATH": "/data/upload/",
"FILE_WEB_PATH": "",
"wkhtmltopdf_path": "",
"yh": {
"auth": {
"Enabled": true,
"CookieExpireMinutes": 120,
"cookieName": ".yh.auth"
"cookieName": ".yh.auth",
"DebugUserEnabled": true
},
"sso": {
"Enabled": true,
@@ -35,7 +39,7 @@
"Enabled": true,
"BaseHost": "http://base.yunhaixingtong.com",
"BasePath": "/userbase",
"SchClassPath": "/bschclass",
"SchClassPath": "/userclass",
"AppKey": "100002",
"AppSecret": "2314a3b158b24a838b2b9b69fd3db118"
}