Compare commits
97
Commits
102fef1d18
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
695ba1530e | ||
|
|
c2f6f42a3c | ||
|
|
2b9389fcb8 | ||
|
|
44d3cd0326 | ||
|
|
3ff9b48a19 | ||
|
|
20325bed00 | ||
|
|
0efde94a74 | ||
|
|
4211b72041 | ||
|
|
e9e1c1722d | ||
|
|
583d06f3d0 | ||
|
|
83182cb538 | ||
|
|
bbcb865055 | ||
|
|
cf8d6da5b5 | ||
|
|
ae1bd4cdb0 | ||
|
|
f1059fd92b | ||
|
|
5785a2c10e | ||
|
|
10c791d034 | ||
|
|
04e708c4fe | ||
|
|
d658ab5b6a | ||
|
|
ae8327807e | ||
|
|
74ba80ddf9 | ||
|
|
6ff1d07f31 | ||
|
|
7e945d310d | ||
|
|
3e9be2bd54 | ||
|
|
7112b7ed40 | ||
|
|
f711f1c5e5 | ||
|
|
6d5df72d2f | ||
|
|
0225df144c | ||
|
|
461847e4ed | ||
|
|
951ed3f00a | ||
|
|
7471dbe66f | ||
|
|
5972f3b7e1 | ||
|
|
8abba50e0e | ||
|
|
3ac70fb699 | ||
|
|
28b3ab80a2 | ||
|
|
18e10a004f | ||
|
|
a73fb4a214 | ||
|
|
b91fd1b514 | ||
|
|
e9c4b64240 | ||
|
|
ff31293df3 | ||
|
|
6920ed4c0d | ||
|
|
70ccd1cb8e | ||
|
|
986b24180a | ||
|
|
a697b85643 | ||
|
|
2ea71d90e1 | ||
|
|
9fe45c810e | ||
|
|
b7789a1914 | ||
|
|
77da929895 | ||
|
|
37f50b7e61 | ||
|
|
12a3f240eb | ||
|
|
f43a0450ac | ||
|
|
07b817ccca | ||
|
|
acff097027 | ||
|
|
3c34db6a65 | ||
|
|
4d6c07c34c | ||
|
|
a8e02ab176 | ||
|
|
ce5e806adb | ||
|
|
c8632e25c2 | ||
|
|
02a752ad87 | ||
|
|
262f0da9c2 | ||
|
|
f21601fc68 | ||
|
|
063ff2ac6a | ||
|
|
8db5870084 | ||
|
|
2728235dc7 | ||
|
|
58392d95d0 | ||
|
|
bf5c2e1b4d | ||
|
|
5fc313b707 | ||
|
|
20bdcaf63e | ||
|
|
12fc0c5ce8 | ||
|
|
2650b87cdc | ||
|
|
a3d97d0988 | ||
|
|
151e62be68 | ||
|
|
ca1e125896 | ||
|
|
fc9d53ed10 | ||
|
|
e0ae71ab81 | ||
|
|
a238bd8587 | ||
|
|
13df04c6af | ||
|
|
65bf4460d1 | ||
|
|
ceb1dc5a83 | ||
|
|
ffedc2acd6 | ||
|
|
c6d9619990 | ||
|
|
f34e69480f | ||
|
|
c55ef5a1f5 | ||
|
|
be6dcf259c | ||
|
|
ac24d318a2 | ||
|
|
3d372d5246 | ||
|
|
4433b081bd | ||
|
|
27a6fc078f | ||
|
|
f19f4e220b | ||
|
|
117b58359b | ||
|
|
9938260704 | ||
|
|
ce436cbb32 | ||
|
|
5a414b14d2 | ||
|
|
a19eb89ec3 | ||
|
|
352da81300 | ||
|
|
abbe322cad | ||
|
|
87a4eed341 |
@@ -0,0 +1,9 @@
|
||||
**/bin/
|
||||
**/obj/
|
||||
**/target/
|
||||
**/.vs/
|
||||
**/.idea/
|
||||
**/logs/
|
||||
*.user
|
||||
*.suo
|
||||
.DS_Store
|
||||
@@ -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)
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Rr]elease/
|
||||
x64/
|
||||
x86/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
artifacts/
|
||||
|
||||
# Visual Studio cache/options
|
||||
.vs/
|
||||
.idea/
|
||||
*.DotSettings.user
|
||||
|
||||
# NuGet
|
||||
*.nupkg
|
||||
*.snupkg
|
||||
**/[Pp]ackages/*
|
||||
!**/[Pp]ackages/build/
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Rider
|
||||
*.sln.iml
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 业务侧特有
|
||||
# 临时验证目录(Mock CAS 脚本、curl 日志、清理清单等)
|
||||
Temp/
|
||||
# 项目记忆与本地技能(含会话数据,不入库)
|
||||
.workbuddy/
|
||||
# 日志
|
||||
*.log
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<!-- 统一从 Nexus 拉包:nuget-group 聚合了内部 hosted + nuget.org 代理 -->
|
||||
<add key="nexus-nuget-group" value="http://nexus.yunhaixingtong.com/repository/nuget-group/index.json" protocolVersion="3" allowInsecureConnections="true" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
@@ -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 优先,不会被覆盖。
|
||||
@@ -0,0 +1,129 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YhCore.Common", "src\YhCore.Common\YhCore.Common.csproj", "{84233313-0D7F-4266-9FBF-A84F5EE2524C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YhCore.Entity", "src\YhCore.Entity\YhCore.Entity.csproj", "{A70BFD7D-318A-4CC4-A926-4CD94DED2833}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YhCore.Model", "src\YhCore.Model\YhCore.Model.csproj", "{51A59F85-64FE-4250-94AC-0EB758A2FAE6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YhCore.Repository", "src\YhCore.Repository\YhCore.Repository.csproj", "{D4ECD214-4AB7-4D1F-B371-EBD20998164E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YhCore.Service", "src\YhCore.Service\YhCore.Service.csproj", "{A8B2534C-75CE-46CE-AA5F-32593BBEA36A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YhCore.Task", "src\YhCore.Task\YhCore.Task.csproj", "{83FEE60E-7F7A-4031-B86E-C98EA30287C0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YhCore.WebApi", "src\YhCore.WebApi\YhCore.WebApi.csproj", "{1EB570F6-D7F7-4874-AF49-DA43242DA4AB}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{84233313-0D7F-4266-9FBF-A84F5EE2524C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{84233313-0D7F-4266-9FBF-A84F5EE2524C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{84233313-0D7F-4266-9FBF-A84F5EE2524C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{84233313-0D7F-4266-9FBF-A84F5EE2524C}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{84233313-0D7F-4266-9FBF-A84F5EE2524C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{84233313-0D7F-4266-9FBF-A84F5EE2524C}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{84233313-0D7F-4266-9FBF-A84F5EE2524C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{84233313-0D7F-4266-9FBF-A84F5EE2524C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{84233313-0D7F-4266-9FBF-A84F5EE2524C}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{84233313-0D7F-4266-9FBF-A84F5EE2524C}.Release|x64.Build.0 = Release|Any CPU
|
||||
{84233313-0D7F-4266-9FBF-A84F5EE2524C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{84233313-0D7F-4266-9FBF-A84F5EE2524C}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A70BFD7D-318A-4CC4-A926-4CD94DED2833}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A70BFD7D-318A-4CC4-A926-4CD94DED2833}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A70BFD7D-318A-4CC4-A926-4CD94DED2833}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A70BFD7D-318A-4CC4-A926-4CD94DED2833}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A70BFD7D-318A-4CC4-A926-4CD94DED2833}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A70BFD7D-318A-4CC4-A926-4CD94DED2833}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A70BFD7D-318A-4CC4-A926-4CD94DED2833}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A70BFD7D-318A-4CC4-A926-4CD94DED2833}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A70BFD7D-318A-4CC4-A926-4CD94DED2833}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A70BFD7D-318A-4CC4-A926-4CD94DED2833}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A70BFD7D-318A-4CC4-A926-4CD94DED2833}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A70BFD7D-318A-4CC4-A926-4CD94DED2833}.Release|x86.Build.0 = Release|Any CPU
|
||||
{51A59F85-64FE-4250-94AC-0EB758A2FAE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{51A59F85-64FE-4250-94AC-0EB758A2FAE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{51A59F85-64FE-4250-94AC-0EB758A2FAE6}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{51A59F85-64FE-4250-94AC-0EB758A2FAE6}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{51A59F85-64FE-4250-94AC-0EB758A2FAE6}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{51A59F85-64FE-4250-94AC-0EB758A2FAE6}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{51A59F85-64FE-4250-94AC-0EB758A2FAE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{51A59F85-64FE-4250-94AC-0EB758A2FAE6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{51A59F85-64FE-4250-94AC-0EB758A2FAE6}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{51A59F85-64FE-4250-94AC-0EB758A2FAE6}.Release|x64.Build.0 = Release|Any CPU
|
||||
{51A59F85-64FE-4250-94AC-0EB758A2FAE6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{51A59F85-64FE-4250-94AC-0EB758A2FAE6}.Release|x86.Build.0 = Release|Any CPU
|
||||
{D4ECD214-4AB7-4D1F-B371-EBD20998164E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D4ECD214-4AB7-4D1F-B371-EBD20998164E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D4ECD214-4AB7-4D1F-B371-EBD20998164E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{D4ECD214-4AB7-4D1F-B371-EBD20998164E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{D4ECD214-4AB7-4D1F-B371-EBD20998164E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{D4ECD214-4AB7-4D1F-B371-EBD20998164E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{D4ECD214-4AB7-4D1F-B371-EBD20998164E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D4ECD214-4AB7-4D1F-B371-EBD20998164E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D4ECD214-4AB7-4D1F-B371-EBD20998164E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{D4ECD214-4AB7-4D1F-B371-EBD20998164E}.Release|x64.Build.0 = Release|Any CPU
|
||||
{D4ECD214-4AB7-4D1F-B371-EBD20998164E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{D4ECD214-4AB7-4D1F-B371-EBD20998164E}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A8B2534C-75CE-46CE-AA5F-32593BBEA36A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A8B2534C-75CE-46CE-AA5F-32593BBEA36A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A8B2534C-75CE-46CE-AA5F-32593BBEA36A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A8B2534C-75CE-46CE-AA5F-32593BBEA36A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A8B2534C-75CE-46CE-AA5F-32593BBEA36A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A8B2534C-75CE-46CE-AA5F-32593BBEA36A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A8B2534C-75CE-46CE-AA5F-32593BBEA36A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A8B2534C-75CE-46CE-AA5F-32593BBEA36A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A8B2534C-75CE-46CE-AA5F-32593BBEA36A}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A8B2534C-75CE-46CE-AA5F-32593BBEA36A}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A8B2534C-75CE-46CE-AA5F-32593BBEA36A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A8B2534C-75CE-46CE-AA5F-32593BBEA36A}.Release|x86.Build.0 = Release|Any CPU
|
||||
{83FEE60E-7F7A-4031-B86E-C98EA30287C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{83FEE60E-7F7A-4031-B86E-C98EA30287C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{83FEE60E-7F7A-4031-B86E-C98EA30287C0}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{83FEE60E-7F7A-4031-B86E-C98EA30287C0}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{83FEE60E-7F7A-4031-B86E-C98EA30287C0}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{83FEE60E-7F7A-4031-B86E-C98EA30287C0}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{83FEE60E-7F7A-4031-B86E-C98EA30287C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{83FEE60E-7F7A-4031-B86E-C98EA30287C0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{83FEE60E-7F7A-4031-B86E-C98EA30287C0}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{83FEE60E-7F7A-4031-B86E-C98EA30287C0}.Release|x64.Build.0 = Release|Any CPU
|
||||
{83FEE60E-7F7A-4031-B86E-C98EA30287C0}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{83FEE60E-7F7A-4031-B86E-C98EA30287C0}.Release|x86.Build.0 = Release|Any CPU
|
||||
{1EB570F6-D7F7-4874-AF49-DA43242DA4AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1EB570F6-D7F7-4874-AF49-DA43242DA4AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1EB570F6-D7F7-4874-AF49-DA43242DA4AB}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{1EB570F6-D7F7-4874-AF49-DA43242DA4AB}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{1EB570F6-D7F7-4874-AF49-DA43242DA4AB}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{1EB570F6-D7F7-4874-AF49-DA43242DA4AB}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{1EB570F6-D7F7-4874-AF49-DA43242DA4AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1EB570F6-D7F7-4874-AF49-DA43242DA4AB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1EB570F6-D7F7-4874-AF49-DA43242DA4AB}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{1EB570F6-D7F7-4874-AF49-DA43242DA4AB}.Release|x64.Build.0 = Release|Any CPU
|
||||
{1EB570F6-D7F7-4874-AF49-DA43242DA4AB}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{1EB570F6-D7F7-4874-AF49-DA43242DA4AB}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{84233313-0D7F-4266-9FBF-A84F5EE2524C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{A70BFD7D-318A-4CC4-A926-4CD94DED2833} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{51A59F85-64FE-4250-94AC-0EB758A2FAE6} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{D4ECD214-4AB7-4D1F-B371-EBD20998164E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{A8B2534C-75CE-46CE-AA5F-32593BBEA36A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{83FEE60E-7F7A-4031-B86E-C98EA30287C0} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{1EB570F6-D7F7-4874-AF49-DA43242DA4AB} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
+3
-1
@@ -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/*
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
# 底层框架代码审查报告
|
||||
|
||||
> 审查对象:`YHDocPrintCore`(业务侧,YhCore.\*)+ `YHBasePlatformDotnet`(底层框架,Yh.Platform.\*)
|
||||
> 审查日期:2026-09-01 | 审查范围:Startup / 中间件 / 公共类 / 框架注册与验签链路 / CAS 认证 / OpenApi 客户端
|
||||
|
||||
## 结论
|
||||
|
||||
整体迁移质量良好:DI 生命周期、配置绑定、异常链路、签名协议与 Java 侧对齐度高,测试套件 98/98 通过。
|
||||
审查发现 **5 个 P1 契约缺陷**(3 个静态审查 + 2 个冒烟触发)与 **4 个 P2 健壮性问题**,均已修复。
|
||||
|
||||
---
|
||||
|
||||
## 一、P1 问题(已修复)
|
||||
|
||||
### 1.1 管道顺序缺陷:/oapi 应用身份(AppKey)被下游中间件清空
|
||||
|
||||
**位置**:`YhCore.WebApi/Startup.cs`(管道顺序)+ `Yh.Platform.Auth/UserContextMiddleware.cs`
|
||||
|
||||
**现象**:原管道 `AppAuth(2) → Authentication(3) → UseYhUserContext(4) → ...`。`AppAuthMiddleware` 验签通过后写入 `UserContextHolder.Set(LoginUser{AppKey})`(位置 2),而 `UserContextMiddleware` 的 `finally` 在请求结束前**无条件** `UserContextHolder.Clear()`(位置 4 更内层,其 finally 在整条链路末尾执行)。结果:`/oapi` 业务 Controller 中 `UserContextHolder.Get()` 恒为 `null`,`AppAuthMiddleware` 注释承诺的"业务可经 UserContextHolder 读取 AppKey"失效。
|
||||
|
||||
**修复**:调整管道顺序,`AppAuth` 移到 `UseYhUserContext` 之后——`Authentication → SsoProtection → UseYhUserContext → AppAuth → LegacyAdapter → Authorization`。`UserContextMiddleware` 的 finally 在 AppAuth 写入之后才执行,AppKey 可存活到业务层;且不再被误清空。
|
||||
|
||||
```
|
||||
修复前: ApiAccessLog → GlobalException → AppAuth → Auth → YhUserContext → Legacy → Sso → Authorization
|
||||
修复后: ApiAccessLog → GlobalException → Auth → Sso → YhUserContext → AppAuth → Legacy → Authorization
|
||||
```
|
||||
|
||||
### 1.2 业务自定义错误码被 `ErrorCodeUtil.From` 吞成 Error=1
|
||||
|
||||
**位置**:`YhCore.Common/Exceptions/BizException.cs`
|
||||
|
||||
**现象**:`ErrorCodeUtil.From(int)` 对未知码 `_ => ErrorCode.Error`(Code=1)。业务 `throw new BizException("库存不足", 5001)` 经映射后 `Code` 变成 1,前端收到 `code:1` 而非 `code:5001`,错误码契约失效。
|
||||
|
||||
**修复**:未知码返回保留原始 code 的 `RawErrorCode(code, "")`(内部 record),msg 由业务消息承载。已知码(0/2/3/4/5/6/405)映射不变。
|
||||
|
||||
### 1.3 CAS 回跳开放重定向
|
||||
|
||||
**位置**:`Yh.Platform.Auth/CasAuthenticationHandler.cs`
|
||||
|
||||
**现象**:`HandleAuthenticateAsync` 中 `Response.Redirect(returnUrl)` 未校验来源,攻击者可诱导已登录用户访问 `https://app/api/x?returnUrl=https://evil.com`,登录后浏览器被跳转到外站(开放重定向)。
|
||||
|
||||
**修复**:新增 `IsLocalUrl` 校验(仅放行 `/` 开头的本地相对路径,拒绝 `//host`、`/\host`、绝对地址),Challenge 拼 service 与回调回跳两处均校验;非法 returnUrl 回退 `/`。
|
||||
|
||||
### 1.4 UserContextMiddleware 构造注入 Scoped 服务,服务启动即崩溃
|
||||
|
||||
**位置**:`Yh.Platform.Auth/UserContextMiddleware.cs`
|
||||
|
||||
**现象**:`IUserDetailsProvider` 注册为 **Scoped**,而中间件在**构造函数**注入它。ASP.NET Core 的 `UseMiddleware<T>` 在管道构建(启动)时用 `ActivatorUtilities` 从**根容器**激活中间件,构造含 Scoped 依赖直接抛 `InvalidOperationException: Cannot resolve scoped service 'Yh.Platform.Auth.IUserDetailsProvider' from root provider`,**WebApi 起不来**(此前迁移冒烟未重启实例,缺陷被掩盖;本轮验证 /oapi 时首次触发)。
|
||||
|
||||
**修复**:改为 ASP.NET Core 官方模式——构造仅保留 `RequestDelegate`(Singleton),Scoped 依赖 `IUserDetailsProvider` 移到 `InvokeAsync(HttpContext, IUserDetailsProvider)` **方法注入**(每请求由 DI 从请求作用域解析)。同步更新 `AuthContextTests` 两处调用。
|
||||
|
||||
### 1.5 服务端验签密钥链不读配置,合法签名恒 401
|
||||
|
||||
**位置**:`Yh.Platform.AppAuth/SignKeyResolver.cs`
|
||||
|
||||
**现象**:`GetSecretAsync` 解析链仅 `Redis(sign:app:{appKey}) → IMemoryCache → UserBase 兜底`,**不消费 `yh:app-auth` 的 AppKey/AppSecret**;且 `AppAuthOptions.TrustedApps` 对应的 `PropertyAppSecretProvider` 从未被注册(`IAppSecretProvider` 写死为 `SignKeyResolverAppSecretProvider`)。本地联调无 Redis 预置 key、无 UserBase → secret 恒为 null → **任意合法签名都 401**。此前冒烟只断言"无签名/假签名 → 401",未验证"合法签名 → 通过",契约缺陷未被发现。
|
||||
|
||||
**修复**:解析链头部增加「配置内置应用」层——`appKey == yh:app-auth:AppKey` 时直接使用配置的 `AppSecret`(覆盖"A 以自身密钥调 B 的 /oapi,B 用配置密钥验签"的自服务/联调场景);第三方应用密钥仍走 Redis → Cache → UserBase。配置密钥从此对服务端验签生效。
|
||||
|
||||
---
|
||||
|
||||
## 二、P2 问题(已修复)
|
||||
|
||||
### 2.1 MemoryReplayGuard 并发下防重放失效
|
||||
|
||||
**位置**:`Yh.Platform.AppAuth/ReplayGuard.cs`
|
||||
|
||||
**现象**:`TryGetValue` 检查后 `_seen[key] = ...` 写入,非原子。两个并发相同签名请求(同 appKey/timestamp/nonce)可同时通过检查同时放行,防重放被绕过。
|
||||
|
||||
**修复**:改为 `ConcurrentDictionary.TryAdd` 原子插入;未过期命中拒绝,过期条目允许覆盖。
|
||||
|
||||
### 2.2 SignCanonical 非法 URL 编码导致 500
|
||||
|
||||
**位置**:`Yh.Platform.AppAuth/SignCanonical.cs`
|
||||
|
||||
**现象**:`Uri.UnescapeDataString` 对未编码的 `%`(如 `?a=100%`)抛 `UriFormatException`,`AppAuthMiddleware` 验签前调用 `BuildTarget` 未捕获 → 兜底返回 `code:6` 内部错误,而非 `401`。
|
||||
|
||||
**修复**:`UrlDecode` 捕获 `UriFormatException` 原样返回;恶意参数验签必失败 → 正确返回 401。
|
||||
|
||||
### 2.3 CasAuthenticationOptions.TicketValidateTimeoutMs 配置未生效
|
||||
|
||||
**位置**:`Yh.Platform.Auth/CasServiceTicketValidator.cs` + `AuthServiceCollectionExtensions.cs` + `CasAuthenticationHandler.cs`
|
||||
|
||||
**现象**:配置项定义了(默认 5000ms)但 validator 硬编码 `5000`,改配置无效。
|
||||
|
||||
**修复(两阶段)**:
|
||||
1. `CasServiceTicketValidator` 构造函数接收 `timeoutMs`,注册时从 `IOptions<CasAuthenticationOptions>` 传入;
|
||||
2. **补漏(9/1 SSO 故障排查发现)**:`CasAuthenticationHandler` 此前绕过 DI、自建 validator 时只传 URL 不传超时 → 配置仍不生效。改为**构造注入 DI 注册的 `CasServiceTicketValidator`**,注册源升级 `IOptionsMonitor<CasAuthenticationOptions>.CurrentValue`。业务侧 `yh:sso:TicketValidateTimeoutMs` 提升到 10000(实测 CAS serviceValidate 延迟 0.5~4.5s+ 波动,5s 默认值存在偶发超时风险)。
|
||||
|
||||
### 2.4 AppAuthMiddleware 手拼 JSON 转义不完整
|
||||
|
||||
**位置**:`YhCore.WebApi/Middleware/AppAuthMiddleware.cs`
|
||||
|
||||
**现象**:`"{\"code\":4,\"msg\":\"" + message.Replace("\"", "\\\"") + ...` 仅转义引号,含反斜杠/换行时产出非法 JSON(当前 message 均为内部常量,风险低)。
|
||||
|
||||
**修复**:改用 `System.Text.Json.JsonSerializer.Serialize`。
|
||||
|
||||
---
|
||||
|
||||
## 三、观察项(未改动,建议后续处理)
|
||||
|
||||
| # | 位置 | 说明 | 建议 |
|
||||
|---|---|---|---|
|
||||
| 3.1 | `OpenApiOptions.Enabled` / `YhAuthOptions.Enabled` | 配置项定义了但注册链路未消费,`yh:open:Enabled=false` 仍会注册客户端并装配门面 | 消费方明确后接入(涉及门面装配语义,需测试覆盖) |
|
||||
| 3.2 | `Startup` CORS | `AllowAnyOrigin` 宽松,注释已标注待 CAS 接入后按 `yh.sso.allow-origins` 收紧 | 上线前提供允许源清单后收紧 |
|
||||
| 3.3 | 反代部署 | `BuildServiceUrl` 依赖 `request.Scheme/Host`,反代后 HTTPS 终止时 service 可能错配 | 生产配置 `AppBaseUrl` 或加 `UseForwardedHeaders` |
|
||||
| 3.4 | `CasServiceTicketValidator` | 回调低频,每次 new `SocketsHttpHandler`,可接受 | 高并发可换共享 `HttpClient` |
|
||||
| 3.5 | `GlobalExceptionMiddleware` | `OperationCanceledException`(客户端断开)走兜底 `code:6` | 若需区分可前置 catch |
|
||||
| 3.6 | `OpenApiClientBase` | `GetRawText()` 二次反序列化,大响应内存开销 | 数据量大时改流式/直接反序列化 |
|
||||
| 3.7 | `SsoProtectionMiddleware` | 直接读 `IConfiguration` 而非 `IOptions`,且保留旧节 `Yh:Sso:Enabled` 兼容 | 框架稳定后移除旧节回退逻辑 |
|
||||
|
||||
---
|
||||
|
||||
## 四、验证结果
|
||||
|
||||
- ✅ 框架重编(Release):**0 警告 0 错误**
|
||||
- ✅ 框架测试套件:**98/98 通过**(Abstractions 8 / AppAuth 42 / Auth 11 / OpenApi 37)——9 处修复零回归
|
||||
- ✅ 业务侧编译(Debug):**0 错误**(仅 net5.0 EOL 警告 NETSDK1138)
|
||||
- ✅ 框架 Release net5.0 产物已拷回 `E:\YHXT\net\Library`(时间戳 2026-09-01 10:03)
|
||||
- ✅ **/oapi 冒烟矩阵**(实例 51835 实启验证):
|
||||
|
||||
| 用例 | 结果 | 断言 |
|
||||
|---|---|---|
|
||||
| 无签名 `GET /oapi/hello` | 401 `{code:4,"msg":"缺少签名头…"}` | 未带票据被拒 |
|
||||
| 合法签名 `GET /oapi/hello`(无 Cookie) | **404**(直达路由层) | 验签通过、**匿名直访成功** |
|
||||
| 合法签名 + 排序 query | 404 | 签名口径含 query 正确 |
|
||||
| 假签名 | 401 `{code:4}` | 票据非法被拒 |
|
||||
| 无 Cookie `GET /api/xxx` | **302** → CAS 登录页 | SSO 保护不受影响 |
|
||||
| 无 Cookie `GET /mobiapi/xxx` | **302** → CAS 登录页 | SSO 保护不受影响 |
|
||||
|
||||
> 备注:编译期间遇到 `MSB3021/MSB3027` 文件锁定,根因为**上次冒烟测试遗留的 YhCore.WebApi 实例进程(PID 21104,监听 51999)**仍持有 `bin` 下 DLL 句柄,`Stop-Process` 结束后编译恢复。排查路径:`netstat -ano | grep <PID>` → 确认连 Redis → taskkill。
|
||||
|
||||
## 五、修复文件清单
|
||||
|
||||
**业务侧(YHDocPrintCore)**
|
||||
- `src/YhCore.WebApi/Startup.cs` — 管道顺序调整(1.1)
|
||||
- `src/YhCore.Common/Exceptions/BizException.cs` — 未知码透传(1.2)
|
||||
- `src/YhCore.WebApi/Middleware/AppAuthMiddleware.cs` — JSON 序列化(2.4)
|
||||
- `src/YhCore.WebApi/Middleware/SsoProtectionMiddleware.cs` — 显式放行 /oapi(渠道契约,1.1 配套)
|
||||
|
||||
**框架侧(YHBasePlatformDotnet)**
|
||||
- `src/Yh.Platform.Auth/CasAuthenticationHandler.cs` — 开放重定向修复(1.3)
|
||||
- `src/Yh.Platform.Auth/UserContextMiddleware.cs` — Scoped 构造注入改方法注入(1.4)
|
||||
- `tests/Yh.Platform.Auth.Tests/AuthContextTests.cs` — 同步新调用约定(1.4)
|
||||
- `src/Yh.Platform.AppAuth/SignKeyResolver.cs` — 配置内置应用密钥层(1.5)
|
||||
- `src/Yh.Platform.AppAuth/ReplayGuard.cs` — 并发防重放(2.1)
|
||||
- `src/Yh.Platform.AppAuth/SignCanonical.cs` — 非法编码容错(2.2)
|
||||
- `src/Yh.Platform.Auth/CasServiceTicketValidator.cs` — 超时配置接入(2.3)
|
||||
- `src/Yh.Platform.Auth/AuthServiceCollectionExtensions.cs` — 超时配置接入(2.3)
|
||||
|
||||
---
|
||||
|
||||
## 六、追加:BizException 退役(9/1)
|
||||
|
||||
业务异常统一改为**直接使用框架 `DException`**(`Yh.Platform.Abstractions`),`BizException` 包装类已删除:
|
||||
|
||||
- `src/YhCore.Common/Exceptions/BizException.cs` **已删除**;`ErrorCodeUtil.From(int)` 独立为 `ErrorCodeUtil.cs`(int → IErrorCode 映射保留,业务抛码仍可用)
|
||||
- `GlobalExceptionMiddleware`:原先逻辑完全相同的 `catch (BizException)` + `catch (DException)` 合并为单个 `catch (DException)`
|
||||
- `WrapBizExceptionAttribute` 更名 `WrapDExceptionAttribute`(当前无业务标注,改名安全)
|
||||
- 业务写法:`throw new DException(ErrorCodeUtil.From(5001), "库存不足")`;仅提示语用 `new DException("提示语")`(默认 Custom=5)
|
||||
|
||||
---
|
||||
|
||||
## 七、追加:SSO 单点登录失败排查(9/1)
|
||||
|
||||
**现象**:浏览器打开 `http://localhost:51835/signin-cas?ticket=ST-7-...` 报"单点登录失败"。
|
||||
|
||||
### 根因(四层)
|
||||
|
||||
1. **CAS 票据单次有效**:`ST-7` 来自更早会话(00:41 首个实例),已被消费或过期。curl 直调 `serviceValidate` 实测 CAS 返回 `<cas:authenticationFailure code="INVALID_TICKET">Ticket 'ST-7-...' not recognized</cas:authenticationFailure>`——**任何重复校验必然失败**,这是 CAS 协议设计(防重放),非缺陷。
|
||||
2. **应用当时未运行**:51835 端口无监听进程(netstat 确认),回调 URL 无法被服务。
|
||||
3. **UX 缺陷**:修复前失效票据回调 → `AuthenticateResult.Fail` → 未认证 → 路径不在保护集 → 落 MVC 裸 **404**,用户感知为"登录失败"。
|
||||
4. **潜在超时风险**:实测 CAS serviceValidate 延迟 **0.5~4.5s+ 波动**,框架默认超时 5s 且此前配置未真正生效(见 2.3 补漏),CAS 慢时登录偶发失败。
|
||||
|
||||
### 修复(框架 2 处 + 业务 2 处,第一轮)
|
||||
|
||||
- 框架 `CasAuthenticationHandler`:**构造注入 DI 注册的 `CasServiceTicketValidator`**(2.3 补漏),并补回旧版有、迁移时丢失的 **`Context.User = principal`**(SignInAsync 只写 Cookie 不改当前请求身份,缺它后续中间件误判未认证)。
|
||||
- 框架 `AuthServiceCollectionExtensions`:validator 注册源 `IOptions` → `IOptionsMonitor<CasAuthenticationOptions>.CurrentValue`。
|
||||
- 业务 `SsoProtectionMiddleware`:CAS 回调路径(`yh:sso:signInCallbackPath`)纳入保护集——失效票据回调**自动 302 弹回 CAS 登录页重新登录**,不再裸 404。
|
||||
- 业务 `appsettings.json`:`yh:sso:TicketValidateTimeoutMs: 10000`(吸收 0.5~4.5s+ 延迟波动)。
|
||||
|
||||
### 验证(实例 51835 实启,第一轮)
|
||||
|
||||
| 用例 | 修复前 | 修复后 |
|
||||
|---|---|---|
|
||||
| 旧票据 `GET /signin-cas?ticket=ST-7-...` | **404** | **302 → CAS 登录页**(自动重登) |
|
||||
| `GET /` | 302 → CAS | 302 → CAS(不变) |
|
||||
| 无 Cookie `GET /api/GetUserInfo` | 302 → CAS | 302 → CAS(不变) |
|
||||
| 无签名 `GET /oapi/Test` | 401 | 401(不变) |
|
||||
| `GET /swagger/index.html` | 200 | 200(不变) |
|
||||
|
||||
⚠️ **此轮验证只覆盖了失败路径**:真实成功路径(票据校验 → Cookie → 回跳)依赖真实 CAS 账号,无法端到端验证。随后用本地 Mock CAS 服务器补测,**立刻暴露一个更深层的框架级缺陷**(见附录八)——第一轮的"302 弹回登录页"只是防循环表象,票据校验实际从未被触发。
|
||||
|
||||
> **给使用者的操作指引**:CAS 票据单次有效且时效短——登录流程必须**从受保护页面(`/` 或 `/api/xxx`)发起**,不要收藏/刷新带 `ticket` 的回调 URL(刷新必失败)。失败后回到 `/` 重新走一遍即可。
|
||||
|
||||
## 八、追加:SSO 成功路径端到端验证——DefaultScheme 缺陷(9/1 第二轮)
|
||||
|
||||
**背景**:附录七修复后,用本地 Mock CAS(`Temp/mock-cas.js`,自动登录 + 单次票据语义)通过环境变量 `yh__sso__CasServerUrlPrefix` 覆盖指向 Mock,对**成功路径**做全链路验证。结果发现:票据校验请求**从未送达** CAS(Mock 日志只有 `/cas/login`,无 `/serviceValidate`),回调 2ms 即返回——校验在网络请求之前就被跳过。
|
||||
|
||||
### 根因(框架级,P0)
|
||||
|
||||
`AuthServiceCollectionExtensions.AddAuthentication` 中 **`DefaultAuthenticateScheme = CookieScheme`**。`UseAuthentication` 每请求只调用 `AuthenticateAsync(默认 scheme)` 恢复 Cookie 登录态;**CAS 票据校验(`CasAuthenticationHandler.HandleAuthenticateAsync` 的 ticket 分支)在回调请求上没有任何触发点**:
|
||||
|
||||
```
|
||||
回调 /signin-cas?ticket=ST-… 进入
|
||||
└─ UseAuthentication → 只查 Cookie(无)→ 未认证
|
||||
└─ SsoProtectionMiddleware:带 ticket + 未认证 → 直接 401 防循环(附录七引入)
|
||||
└─ CasAuthenticationHandler.HandleAuthenticateAsync 从未执行 → 票据从不校验 → 登录永远失败
|
||||
```
|
||||
|
||||
即:第一轮"防循环"把**合法票据路径一并拦截**,与 `DefaultScheme` 错误叠加,导致任何 CAS 登录都不可能成功(Mock 端到端立即暴露,真实账号难以察觉)。
|
||||
|
||||
### 修复(框架 1 处 + 业务 1 处)
|
||||
|
||||
- **框架 `AuthServiceCollectionExtensions`**:`DefaultAuthenticateScheme` 改为 `YhAuthDefaults.CasScheme`。`CasAuthenticationHandler.HandleAuthenticateAsync` 第一步就是 `AuthenticateAsync(CookieScheme)` 恢复登录态,因此语义等价"先 Cookie 恢复、后 ticket 校验",每请求行为不变、仅补上票据校验触发点。
|
||||
- **业务 `SsoProtectionMiddleware`**:回调路径**已认证且已 302**(handler 验票成功 `Response.Redirect`)时**短路管道**——handler 返回 `NoResult` 不短路,继续走 MVC 端点匹配会因 `/signin-cas` 无 action 返回 **404 覆盖 302**(实测现象),短路保住回跳响应。
|
||||
|
||||
### 验证(Mock CAS 端到端,实例 51835 实启)
|
||||
|
||||
| # | 场景 | 结果 |
|
||||
|---|---|---|
|
||||
| A | `/` → Mock 登录 → 回调验票 → Cookie → 回跳 `/` | **302 → 200**,Cookie 落盘 ✓ |
|
||||
| B | 带 Cookie `GET /api/GetUserInfo` | **200**(登录态生效)✓ |
|
||||
| C | 复用已消费票据 `GET /signin-cas?ticket=同前` | **401 防循环页** ✓(Mock 第二次校验返回 INVALID_TICKET) |
|
||||
| D | 无 Cookie `GET /api/GetUserInfo` | 302 → CAS ✓ |
|
||||
| E | 无签名 `GET /oapi/Test` | 401(SSO 不覆盖)✓ |
|
||||
|
||||
Mock 日志互证:`02:35:09 /cas/serviceValidate?ticket=ST-MOCK-…` 送达并成功、票据被消费;复用同一票据时 Mock 返回 `INVALID_TICKET` → 应用 401。
|
||||
|
||||
框架重编 0 错误、测试 **98/98 通过**、业务编译 0 错误;`Yh.Platform.Auth.dll` 已更新至 `E:\YHXT\net\Library`。
|
||||
|
||||
> **教训**:SSO 类链路缺陷必须覆盖**成功路径**验证——失败路径的"防循环 401/302"表象可能掩盖"校验从未发生"的框架级问题。无真实账号时用 Mock CAS 全链路补测是必要手段。
|
||||
|
||||
---
|
||||
|
||||
## 附录九:登录态 Cookie 会话改造(9/1 需求:关闭失效 / 1 分钟过期 / 32 位)
|
||||
|
||||
**需求**:① 关闭浏览器 Cookie 失效;② 过期时间 1 分钟;③ Cookie 长度为 32 位。
|
||||
|
||||
**实现(框架 `Yh.Platform.Auth` 3 处 + 业务 1 处)**:
|
||||
|
||||
| 文件 | 变更 |
|
||||
|---|---|
|
||||
| `SessionTicketDataFormat.cs`(新增) | 自定义 `ISecureDataFormat<AuthenticationTicket>`:`Protect` 把完整票据存服务端内存、返回 **32 位 key**(Guid N 格式);`Unprotect` 查表并校验 `ExpiresUtc`(过期返回 null 并清理)。key 为 128 位随机熵,不可枚举 |
|
||||
| `AuthServiceCollectionExtensions.cs` | Cookie 注册:`options.TicketDataFormat = new SessionTicketDataFormat()`;`ExpireTimeSpan` 由 `yh:auth:CookieExpireMinutes` 决定(默认 1 分钟);`SlidingExpiration = false`(固定过期不滑动) |
|
||||
| `CasAuthenticationHandler.cs` | `IsPersistent: true → false`(会话级 Cookie,关闭浏览器失效;旧版 `_legacy` 本为 false,系迁移回归,已修正) |
|
||||
| 业务 `appsettings.json` | `CookieExpireHours: 8` → `CookieExpireMinutes: 1`(注:文件曾被外部覆盖一次,CookieName 现为用户保存的 `.yh.auth`) |
|
||||
|
||||
**关键技术发现**:**.NET 5 的 `CookieAuthenticationOptions.SessionStore` 并不能缩短 Cookie**——官方源码(v5.0.17 `CookieAuthenticationHandler.HandleSignInAsync`)显示即便配置了 SessionStore,cookie 值仍是 `TicketDataFormat.Protect(精简票据)` 的加密长串(`CfDJ8…`),只是把完整用户票据移到了服务端。**要实现"Cookie 32 位",必须自定义 `TicketDataFormat`(Protect 落库返回 key)**,而非依赖 SessionStore。首次尝试(SessionTicketStore + SessionStore)经最小复现程序(独立 net5.0 Web 项目直连框架 DLL)证实无效后放弃。
|
||||
|
||||
**验证(Mock CAS 端到端 + 真实配置冒烟)**:
|
||||
|
||||
| 需求 | 验证结果 |
|
||||
|---|---|
|
||||
| ③ Cookie 32 位 | `Set-Cookie: .yh.auth=3a3f40993606438e86d8e756b99a6f07` = **32 字符** ✓ |
|
||||
| ① 关闭浏览器失效 | Set-Cookie 无 `Max-Age`/`Expires`(会话 Cookie)+ `IsPersistent=false` ✓ |
|
||||
| ② 1 分钟过期 | 登录后立即带 Cookie 访问 `/` → 200;**65 秒后** → **302 弹回 CAS 登录页**(服务端票据过期)✓ |
|
||||
|
||||
框架双目标(net5.0/net8.0)编译 0 错误 0 警告、测试 **98/98 通过**、业务编译 0 错误;DLL 已更新至 `E:/YHXT/net/Library`。
|
||||
|
||||
**注意**:票据存进程内存(ConcurrentDictionary)——应用重启后所有登录态失效(用户需重新登录);多实例部署时各实例票据互不可见(如需跨实例共享可换 Redis 实现)。
|
||||
|
||||
### 附录九补充:退出即吊销——显式登出服务端票据立即失效(9/1 第二轮)
|
||||
|
||||
**背景**:附录九初版只覆盖"登录态自然过期/关浏览器"。审查发现缺口:`SignOutAsync`(业务 `GET /api/Logout`)只删 Cookie,**服务端内存票据残留到 ExpireTimeSpan 过期**(默认 1 分钟)——期间若旧 Cookie key 被恢复/复用,登录态仍有效。
|
||||
|
||||
**修复(框架 `Yh.Platform.Auth` 2 处)**:
|
||||
|
||||
| 文件 | 变更 |
|
||||
|---|---|
|
||||
| `SessionTicketDataFormat.cs` | 新增 `Revoke(string? key)`:删除指定票据(显式退出立即失效);新增分钟级摊还清理 `CleanupExpiredIfDue()`(Unprotect/Revoke 路径触发,Interlocked 门闩保证并发下每分钟最多一次全量扫描,防内存膨胀) |
|
||||
| `AuthServiceCollectionExtensions.cs` | `SessionTicketDataFormat` 提升为 DI 单例(`services.AddSingleton`,TicketDataFormat 与事件共享同一票据表);Cookie 注册挂 `options.Events.OnSigningOut`:SignOutAsync 时从请求 Cookie 读 32 位 key → `Revoke(key)` → 服务端票据**立即删除** |
|
||||
|
||||
**语义闭环**:① 关闭浏览器 → 会话 Cookie 删除(服务端无法感知浏览器关闭,1 分钟过期兜底);② 显式退出(/api/Logout)→ Cookie 删除 **+** 服务端票据立即吊销,不等过期。
|
||||
|
||||
**验证(Mock CAS 端到端,实例 51835 实启)**:
|
||||
|
||||
| 步骤 | 期望 | 实测 |
|
||||
|---|---|---|
|
||||
| 登录流 → Cookie(32 位 key) | 200 + Cookie 落盘 | ✓ |
|
||||
| 带 Cookie 访问 `/api/GetUserInfo` | 200 | ✓ |
|
||||
| `GET /api/Logout`(带 Cookie) | 200 | ✓ |
|
||||
| **Logout 后旧 key 手动放回访问** | **302 弹回 CAS**(服务端已吊销;旧行为下会 200) | ✓ **302** |
|
||||
|
||||
框架测试 **106/106 通过**(Auth 19 = 原 11 + 新增 `SessionTicketDataFormatTests` 8 项:32 位 key 往返 / 未知 key null / Revoke 立即失效 / 过期 null / 未过期正常 / Revoke 不影响其他票据等),双目标编译 0 错误 0 警告;`Yh.Platform.Auth.dll`(Release net5.0)已更新至 `E:/YHXT/net/Library`。
|
||||
@@ -0,0 +1,86 @@
|
||||
# YhCore 开发规范
|
||||
|
||||
> .NET 5.0 后端框架(参照 Java 版 bschclassmanage 迁移),仅框架无业务逻辑。
|
||||
|
||||
## 1. 解决方案结构
|
||||
|
||||
```
|
||||
YhCore.sln
|
||||
├─ src/
|
||||
│ ├─ YhCore.WebApi # 启动项:Controllers / Middleware / Filters / Attributes / Auth
|
||||
│ ├─ YhCore.Service # 业务服务层:Interfaces / Impl(不含业务时用 *Stub 占位)
|
||||
│ ├─ YhCore.Repository # 数据层:YHDbContext / IRepository<T> / Specification / DapperChannel
|
||||
│ ├─ YhCore.Entity # 数据库实体(旧 EF6 POCO 迁移而来,无导航属性)
|
||||
│ ├─ YhCore.Model # DTO / VO / Boa 平台占位模型
|
||||
│ ├─ YhCore.Common # 工具:ApiRoutes / UserContext / RedisUtil / PageUtil / ApiResult
|
||||
│ └─ YhCore.Tasks # 调度:QuartzHostedService / Jobs
|
||||
└─ lib / docker / docs / sqlscript
|
||||
```
|
||||
|
||||
**依赖方向(单向,禁止反向引用)**:WebApi → Service → Repository → Entity;WebApi → Model ← Service;Common 不依赖任何上层。
|
||||
|
||||
**底层框架(Yh.Platform.\*)**:CAS 认证、用户上下文(BoaUserContext)、应用验签(AppAuthVerifier)、用户中心(OpenApi)均由底层框架提供:
|
||||
- 引用 DLL:`Yh.Platform.Abstractions` / `Yh.Platform.AppAuth` / `Yh.Platform.Auth` / `Yh.Platform.OpenApi`(lib 目录,源码在 `E:\YHXT\net\YHBasePlatformDotnet`)
|
||||
- `Startup` 用 `services.AddYhPlatform(Configuration)` 聚合注册(= AddYhAppAuth + AddYhAuth[含 AddYhOpenApi]),管道用 `app.UseYhUserContext()` 注入用户上下文
|
||||
- 旧自研 CAS/验签/用户中心实现已退役到 `src/_legacy/`(不参与编译,仅留档备份);业务代码取当前用户用 `BoaUserContext.Get()`
|
||||
|
||||
**`/oapi` 开放渠道契约**:应用间调用渠道,**无需用户登录**,凭签名票据直接访问:
|
||||
- 票据 = 请求头 `X-App-Key` / `X-Timestamp` / `X-Signature`(`signature = Base64(HmacSHA256(METHOD + "\n" + path[?sortedQuery] + "\n" + timestamp, secret))`,详见框架仓库 `YHBasePlatformDotnet\BasePlatformDotnet\contract\sign-protocol.md`)
|
||||
- 未带票据/票据非法 → `401 {code:4,msg:"缺少签名头…"/"签名校验失败…"}`;验签通过 → 放行到业务(无用户上下文,业务只可读 `UserContextHolder.Get().AppKey` 应用身份)
|
||||
- SSO 登录保护**永不拦截** `/oapi`(SsoProtectionMiddleware 显式放行,即使 `yh:sso:urlPattern` 误配也不影响);服务端验签密钥解析链:配置内置应用(`yh:app-auth` AppKey/AppSecret)→ Redis `sign:app:{appKey}` → 本地缓存 → UserBase 兜底
|
||||
|
||||
**`/oapi` 冒烟验证方法**:本地联调/自测票据可用 `scripts/oapi-sign.js` 生成(对齐签名协议):
|
||||
- 生成票据:`node scripts/oapi-sign.js GET "/oapi/hello?a=2&b=1"` → 输出 `appKey/ts/signature` 三头
|
||||
- 直接出 curl:`node scripts/oapi-sign.js --curl POST "/oapi/xxx"` → 复制替换 `PORT` 即可
|
||||
- 密钥默认取开发联调密钥(`yh:app-auth`),生产环境用 `APP_KEY` / `APP_SECRET` 环境变量覆盖,**禁止提交真实密钥到仓库**
|
||||
- 断言基线:无票据 → 401 `{code:4}`;合法票据 → 直达业务(404/业务结果);假票据 → 401;`/api`、`/mobiapi` 无 Cookie → 302 CAS(SSO 保护不受影响)
|
||||
|
||||
## 2. 强制规范
|
||||
|
||||
| 约定 | 说明 |
|
||||
|---|---|
|
||||
| **接口路径** | 一律用 `[Route(ApiRoutes.ApiPath + "/xxx")]`,禁止硬编码 `"api/..."`;渠道前缀见 `ApiRoutes`(api/mobiapi/oapi) |
|
||||
| **接口返回结构** | 与 Java 版保持一致(裸返回),**全局 `GlobalWrapResultFilter` 自动统一包装**(ApiResult{code,msg,data});需要裸返回用 `[NoWrapResult]` 显式排除;异常由 `GlobalExceptionMiddleware` 统一转失败响应 |
|
||||
| **业务异常** | Service 层抛 `new DException(ErrorCodeUtil.From(code), "提示语")`(框架 `Yh.Platform.Abstractions`);仅提示语可用便捷构造 `new DException("提示语")`(默认 Custom=5);中间件自动转统一失败响应 |
|
||||
| **日志** | **只允许接口访问日志**(ApiAccessLogMiddleware 自动记录)。业务代码禁止调用 `ILogger` 写日志;异常摘要由接口日志统一带出 |
|
||||
| **缓存 Key** | 必须用 `RedisConstants.KeyOf(...)` 生成;过期时间用 `RedisConstants.CACHE_EXPIRE_*` |
|
||||
| **分页** | 页码从 0 开始(与 Spring Pageable 对齐);数据库分页用 `IRepository<T>.PageAsync`,内存分页用 `PageUtil` |
|
||||
| **动态条件** | 用 `Specification.True<T>().And(...).AndIf(cond, ...)` 拼装 |
|
||||
| **操作日志** | **接口级审计日志,全局自动**:`SysOperationLogFilter`(Startup 全局注册)每次 Controller 请求后落库 `sys_operation_log`(对齐 Java `SysOperationLog`,含 URI/参数/响应 GZIP/耗时/状态码/异常),无需任何标注;级别按 HTTP 方法推导(GET=QUERY/POST=ADD/PUT=EDIT/DELETE=DELETE);`LogController` 已显式排除防递归 |
|
||||
| **实体主键** | 实体无 `[Key]` 时按约定推断(Id → SerID);新增表实体建议显式标注 |
|
||||
| **数据库** | 默认 SQL Server 主库;`DatabaseProvider=MySql` 切换;**缺表自动补建**(`Database:AutoCreateMissingTables`,默认 true:启动时对比 EF 模型与数据库,缺失的表按模型自动 CREATE,幂等;已存在表的结构变更仍走 sqlscript 脚本) |
|
||||
| **数据库写操作** | 注意 `RepositoryBase` 的 Add/Update 需调用 `SaveChangesAsync()` 提交(UnitOfWork 模式) |
|
||||
|
||||
## 3. 常用命令
|
||||
|
||||
```bash
|
||||
dotnet build # 编译
|
||||
dotnet run --project src/YhCore.WebApi # 本地运行(Swagger: /swagger/index.html)
|
||||
dotnet watch run --project src/YhCore.WebApi
|
||||
```
|
||||
|
||||
## 4. 环境配置(appsettings.json 或环境变量覆盖)
|
||||
|
||||
| 配置 | 默认值 | 说明 |
|
||||
|---|---|---|
|
||||
| `DatabaseProvider` | `SqlServer` | `SqlServer` / `MySql` |
|
||||
| `Database:AutoCreateMissingTables` | `true` | 启动时自动补建 EF 模型中有而数据库缺失的表(仅 SQL Server;幂等;已存在表不触碰,结构变更仍走 sqlscript) |
|
||||
| `ConnectionStrings__SqlServer` | localhost/EduCloudDocPrint | 主库连接串(生产用环境变量,勿提交明文密码) |
|
||||
| `ConnectionStrings__Mysql` | localhost:3306 | MySQL 备用连接串 |
|
||||
| `Redis:ConnectionString` | localhost:6379,defaultDatabase=0 | Redis 连接串 |
|
||||
| `yh:auth:CookieName` | `.yh.auth` | 认证 Cookie 名 |
|
||||
| `yh:auth:CookieExpireMinutes` | `1` | 登录态固定过期(分钟),不滑动续期;票据存服务端内存(SessionTicketDataFormat),Cookie 只写 32 位 key;会话级 Cookie(IsPersistent=false),关闭浏览器即失效;**显式退出(/api/Logout)时服务端票据立即吊销**(OnSigningOut 事件,不等过期兜底) |
|
||||
| `yh:sso:Enabled` | `true` | CAS SSO 开关(拦截 urlPattern / mobiApiPattern) |
|
||||
| `yh:sso:CasServerUrlPrefix` | - | CAS 服务端地址前缀(Challenge 跳转用) |
|
||||
| `yh:app-auth:Enabled` | `true` | /oapi 应用验签开关(X-App-Key / X-Timestamp / X-Signature) |
|
||||
| `yh:open:AppKey` / `yh:open:AppSecret` | 100002 | OpenApi 调用方凭证 |
|
||||
|
||||
## 5. 待办(TODO)
|
||||
|
||||
- [x] CAS 认证:已由 `Yh.Platform.Auth`(CasScheme)接管,旧本地实现退役到 `_legacy/Auth`
|
||||
- [x] 应用签名:`AppAuthMiddleware` 已接入 `AppAuthVerifier` + `SignCanonical` 真实验签
|
||||
- [x] 用户中心:`IUserCenterService` 已替换为 `Yh.Platform.OpenApi`(`BoaUserContext.Get()` 取上下文)
|
||||
- [ ] `ILogService` 真实查询实现(替换 LogServiceStub)
|
||||
- [x] Quartz Job 按业务启用:`CleanupLogJob` 已实现(每日凌晨 3:30 删除 30 天前 `sys_operation_log`),`QuartzHostedService.InitJobs` 已注册 `cleanup-log-job`(cron `0 30 3 * * ?`)
|
||||
- [ ] 审计日志增强(`SysOperationLogFilter` 目前按 HTTP 方法推导级别,`Module/Operation` 两列恒空,可接入业务字典映射;日志查询走 `LogController` 但 `LogServiceStub` 尚未实现真实分页查询)
|
||||
- [ ] .NET 5 已 EOL:框架代码避免使用 5.0 专属 API,便于日后升 6/8
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env node
|
||||
// /oapi 签名票据生成器(对齐 yh-platform-net SignUtil.SignRequest / SignCanonical 协议)
|
||||
//
|
||||
// 协议:signature = Base64(HmacSHA256(METHOD + "\n" + path[?sortedQuery] + "\n" + timestamp, secret))
|
||||
// - query 按 k=v 整体字典序排序后拼接参与签名
|
||||
// - timestamp 为毫秒级 Unix 时间戳(服务端校验 5 分钟窗口)
|
||||
//
|
||||
// 用法:
|
||||
// node scripts/oapi-sign.js [METHOD] [path?query]
|
||||
// node scripts/oapi-sign.js --curl POST /oapi/print/submit # 输出可直接复制的 curl 命令
|
||||
//
|
||||
// 密钥来源(优先级):环境变量 APP_KEY / APP_SECRET → 文件默认值(开发联调密钥)
|
||||
// 生产环境务必通过环境变量注入,勿提交真实密钥。
|
||||
const crypto = require('crypto');
|
||||
|
||||
const APP_KEY = process.env.APP_KEY || '100002';
|
||||
const APP_SECRET = process.env.APP_SECRET || '2314a3b158b24a838b2b9b69fd3db118';
|
||||
|
||||
// 排序 query(对齐 SignCanonical.SortRawQuery:k=v 整体排序,空值保留 = 号)
|
||||
function sortQuery(raw) {
|
||||
if (!raw) return '';
|
||||
return raw.split('&').filter(Boolean).map(p => {
|
||||
const i = p.indexOf('=');
|
||||
return (i < 0 ? p : p.slice(0, i)) + '=' + (i < 0 ? '' : p.slice(i + 1));
|
||||
}).sort().join('&');
|
||||
}
|
||||
|
||||
function sign(method, pathWithQuery, ts) {
|
||||
const qi = pathWithQuery.indexOf('?');
|
||||
const target = qi >= 0
|
||||
? pathWithQuery.slice(0, qi) + '?' + sortQuery(pathWithQuery.slice(qi + 1))
|
||||
: pathWithQuery;
|
||||
const source = method.toUpperCase() + '\n' + target + '\n' + ts;
|
||||
const sig = crypto.createHmac('sha256', APP_SECRET).update(source, 'utf8').digest('base64');
|
||||
return { ts, target, signature: sig };
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
let method = 'GET';
|
||||
let pathWithQuery = '/oapi/hello';
|
||||
let curlMode = false;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const a = args[i];
|
||||
if (a === '--curl') { curlMode = true; continue; }
|
||||
if (!/^-/.test(a)) {
|
||||
if (method === 'GET' && !/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)$/i.test(a) && !a.startsWith('/')) {
|
||||
// 第一个非选项参数若不像 METHOD 也不像路径,则视为路径
|
||||
pathWithQuery = a;
|
||||
} else if (a.startsWith('/')) {
|
||||
pathWithQuery = a;
|
||||
} else {
|
||||
method = a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { ts, target, signature } = sign(method, pathWithQuery, Date.now().toString());
|
||||
|
||||
if (curlMode) {
|
||||
const u = target.replace(/^\//, '');
|
||||
console.log(`curl -i -X ${method.toUpperCase()} "http://localhost:PORT/${u}" \\`);
|
||||
console.log(` -H "X-App-Key: ${APP_KEY}" \\`);
|
||||
console.log(` -H "X-Timestamp: ${ts}" \\`);
|
||||
console.log(` -H "X-Signature: ${signature}"`);
|
||||
} else {
|
||||
console.log(JSON.stringify({ appKey: APP_KEY, method, target, ts, signature, curl: [
|
||||
'-H', `X-App-Key: ${APP_KEY}`,
|
||||
'-H', `X-Timestamp: ${ts}`,
|
||||
'-H', `X-Signature: ${signature}`
|
||||
].join(' ') }, null, 2));
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
-- ============================================================
|
||||
-- 数据变更日志表(A_DataLog)
|
||||
-- 对应实体:YhCore.Entity.A_DataLog(旧 EF6 EDMX 生成 POCO,主键约定 SerID)
|
||||
-- 用途:数据变更审计预留表(当前无业务代码使用,仅 DbContext 注册)
|
||||
-- 数据库:SQL Server(EduCloudDocPrint)
|
||||
-- 风格:对齐同库 Java 遗留表(SerID 主键自增、varchar 字符串、可空)
|
||||
-- ============================================================
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'A_DataLog')
|
||||
BEGIN
|
||||
CREATE TABLE [dbo].[A_DataLog] (
|
||||
[SerID] INT IDENTITY(1,1) NOT NULL CONSTRAINT [PK_A_DataLog] PRIMARY KEY,
|
||||
[SchSerID] VARCHAR(200) NULL,
|
||||
[ModuleName] VARCHAR(50) NULL,
|
||||
[TableName] VARCHAR(50) NULL,
|
||||
[OldSerID] INT NULL,
|
||||
[NowSerID] INT NULL,
|
||||
[RId] INT NULL
|
||||
);
|
||||
|
||||
PRINT 'A_DataLog table created.';
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
PRINT 'A_DataLog table already exists, skip.';
|
||||
END
|
||||
@@ -0,0 +1,55 @@
|
||||
-- ============================================================
|
||||
-- 系统审计日志表(sys_operation_log)
|
||||
-- 对应实体:YhCore.Entity.SysOperationLog(EF Core,OnModelCreating ToTable 映射)
|
||||
-- 对应 Java:bschclassmanage 的 entity.SysOperationLog / sqlscript/sys_operation_log.sql
|
||||
-- 用途:SysOperationLogFilter 全量记录 HTTP 接口日志 / CleanupLogJob 30 天清理
|
||||
-- 说明:本表取代旧业务操作日志表 OperationLogRecord(旧表无数据,直接删除)
|
||||
-- 数据库:SQL Server(EduCloudDocPrint)
|
||||
-- ============================================================
|
||||
|
||||
-- 1) 清理旧业务操作日志表(OperationLogRecord,已被 sys_operation_log 取代)
|
||||
IF OBJECT_ID(N'[dbo].[OperationLogRecord]', N'U') IS NOT NULL
|
||||
BEGIN
|
||||
DROP TABLE [dbo].[OperationLogRecord];
|
||||
PRINT 'Legacy OperationLogRecord table dropped.';
|
||||
END
|
||||
|
||||
-- 2) 创建审计日志表(幂等)
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'sys_operation_log')
|
||||
BEGIN
|
||||
CREATE TABLE [dbo].[sys_operation_log] (
|
||||
[Id] BIGINT IDENTITY(1,1) NOT NULL CONSTRAINT [PK_sys_operation_log] PRIMARY KEY,
|
||||
[LogLevel] TINYINT NOT NULL, -- 1-重要(删除) 2-次要(新增/编辑) 3-一般(查询)
|
||||
[OperationType] NVARCHAR(16) NOT NULL, -- DELETE/CREATE/UPDATE/QUERY
|
||||
[UserId] INT NULL, -- 操作人 ID
|
||||
[UserName] NVARCHAR(64) NULL, -- 操作人姓名
|
||||
[SchId] INT NULL, -- 学校 ID(多校隔离)
|
||||
[RequestIp] NVARCHAR(64) NOT NULL, -- 客户端 IP
|
||||
[RequestUri] NVARCHAR(512) NOT NULL, -- 请求 URI
|
||||
[RequestMethod] NVARCHAR(16) NOT NULL, -- HTTP 方法
|
||||
[ClassMethod] NVARCHAR(512) NOT NULL, -- 类名.方法名
|
||||
[RequestParams] NVARCHAR(MAX) NULL, -- 请求参数(JSON,截断)
|
||||
[ResponseData] VARBINARY(MAX) NULL, -- GZIP 压缩后的响应 JSON
|
||||
[StatusCode] INT NOT NULL, -- 业务状态码:0 成功 / 非 0 失败
|
||||
[CostTime] BIGINT NOT NULL, -- 耗时(ms)
|
||||
[UserAgent] NVARCHAR(512) NULL,
|
||||
[ErrorMsg] NVARCHAR(MAX) NULL, -- 异常信息(截断 8000)
|
||||
[RetryCount] INT NULL DEFAULT 0, -- 重试次数(对齐 Java 双轨字段,.NET 暂不写入)
|
||||
[LastRetryStatus] INT NULL, -- 上次重试状态:1 成功 / 0 失败
|
||||
[LastRetryTime] DATETIME NULL, -- 上次重试时间
|
||||
[CreateTime] DATETIME NOT NULL -- 记录时间
|
||||
);
|
||||
|
||||
-- 查询/清理索引(对齐 Java idx_sol_*)
|
||||
CREATE INDEX [IX_sol_CreateTime] ON [dbo].[sys_operation_log] ([CreateTime]);
|
||||
CREATE INDEX [IX_sol_UserId_Time] ON [dbo].[sys_operation_log] ([UserId], [CreateTime]);
|
||||
CREATE INDEX [IX_sol_SchId_Time] ON [dbo].[sys_operation_log] ([SchId], [CreateTime]);
|
||||
CREATE INDEX [IX_sol_Uri_Time] ON [dbo].[sys_operation_log] ([RequestUri], [CreateTime]);
|
||||
CREATE INDEX [IX_sol_Level_Time] ON [dbo].[sys_operation_log] ([LogLevel], [CreateTime]);
|
||||
|
||||
PRINT 'sys_operation_log table created.';
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
PRINT 'sys_operation_log table already exists, skip.';
|
||||
END
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace YhCore.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// 渠道路由前缀常量(对应 Java 版 config.Config 枚举的
|
||||
/// API_PATH / MOBILEAPI_PATH / OAPI)。
|
||||
/// Controller 一律通过常量引用,禁止硬编码 "api/..." 字符串:
|
||||
/// <code>[Route(ApiRoutes.ApiPath + "/log")]</code>
|
||||
/// </summary>
|
||||
public static class ApiRoutes
|
||||
{
|
||||
/// <summary>PC 端接口前缀(对应 Config.API_PATH)</summary>
|
||||
public const string ApiPath = "api";
|
||||
|
||||
/// <summary>移动端接口前缀(对应 Config.MOBILEAPI_PATH)</summary>
|
||||
public const string MobiApiPath = "mobiapi";
|
||||
|
||||
/// <summary>开放接口前缀(对应 Config.OAPI)</summary>
|
||||
public const string OApiPath = "oapi";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace YhCore.Common.Cache
|
||||
{
|
||||
/// <summary>
|
||||
/// Redis Key 规范常量(对应 Java 版 utils.RedisConstants)。
|
||||
/// 所有业务 Key 必须基于 <see cref="CachePrefix"/> 通过 <see cref="KeyOf"/> 生成,禁止散落硬编码。
|
||||
/// </summary>
|
||||
public static class RedisConstants
|
||||
{
|
||||
/// <summary>本项目缓存前缀(Java 班级管理为 bsch.classmanage,本项目为 yh.docprint)</summary>
|
||||
public const string CachePrefix = "yh.docprint";
|
||||
|
||||
// ---- 常用过期时间(秒),对应 Java CACHE_EXPIRE_* ----
|
||||
public const long CACHE_EXPIRE_1M = 60;
|
||||
public const long CACHE_EXPIRE_5M = 5 * 60;
|
||||
public const long CACHE_EXPIRE_10M = 10 * 60;
|
||||
public const long CACHE_EXPIRE_30M = 3 * CACHE_EXPIRE_10M;
|
||||
public const long CACHE_EXPIRE_1H = 60 * 60;
|
||||
public const long CACHE_EXPIRE_1D = 24 * CACHE_EXPIRE_1H;
|
||||
public const long CACHE_EXPIRE_7D = 7 * CACHE_EXPIRE_1D;
|
||||
public const long CACHE_EXPIRE_30D = 30 * CACHE_EXPIRE_1D;
|
||||
|
||||
/// <summary>分布式锁 Key 前缀</summary>
|
||||
public const string LockPrefix = CachePrefix + ".lock";
|
||||
|
||||
/// <summary>
|
||||
/// 生成规范 Key:CachePrefix + "." + 各段拼接。
|
||||
/// 例:KeyOf("sch", "list") → "yh.docprint.sch.list"
|
||||
/// </summary>
|
||||
public static string KeyOf(params string[] parts)
|
||||
{
|
||||
return CachePrefix + "." + string.Join(".", parts);
|
||||
}
|
||||
|
||||
/// <summary>生成分布式锁 Key</summary>
|
||||
public static string LockKeyOf(string name)
|
||||
{
|
||||
return LockPrefix + "." + name;
|
||||
}
|
||||
|
||||
// TODO:业务缓存 Key 按模块在此追加(参照 Java 版 SYSTEM_* / SCHOOL_* 的命名风格)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace YhCore.Common.Cache
|
||||
{
|
||||
/// <summary>
|
||||
/// Redis 缓存注册扩展(对应 Java RedisConfig 的 Bean 装配)。
|
||||
/// 连接串取配置 "Redis:ConnectionString",如 "localhost:6379,defaultDatabase=0"。
|
||||
/// </summary>
|
||||
public static class RedisServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddRedisCache(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var connStr = configuration["Redis:ConnectionString"];
|
||||
if (string.IsNullOrWhiteSpace(connStr))
|
||||
{
|
||||
connStr = "localhost:6379,defaultDatabase=0";
|
||||
}
|
||||
|
||||
// 连接多路复用器全局单例(对应 Java 单例 RedisTemplate)
|
||||
services.AddSingleton<IConnectionMultiplexer>(
|
||||
ConnectionMultiplexer.Connect(connStr));
|
||||
services.AddSingleton<RedisUtil>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace YhCore.Common.Cache
|
||||
{
|
||||
/// <summary>
|
||||
/// Redis 操作封装(对应 Java 版 utils.RedisUtil)。
|
||||
/// 基于 StackExchange.Redis,对象值统一 JSON 序列化(Newtonsoft,与全局 JSON 方案一致);
|
||||
/// Incr/Decr 计数类 Key 存裸数字,不走 JSON。
|
||||
/// 注册为单例(内部复用 IConnectionMultiplexer,线程安全)。
|
||||
/// </summary>
|
||||
public class RedisUtil
|
||||
{
|
||||
private readonly IConnectionMultiplexer _connection;
|
||||
|
||||
public RedisUtil(IConnectionMultiplexer connection)
|
||||
{
|
||||
_connection = connection;
|
||||
}
|
||||
|
||||
private IDatabase Db => _connection.GetDatabase();
|
||||
|
||||
// ==================== Key / 过期 ====================
|
||||
|
||||
/// <summary>设置过期时间(秒),对应 expire(key, time)</summary>
|
||||
public bool Expire(string key, long seconds)
|
||||
{
|
||||
return Db.KeyExpire(key, TimeSpan.FromSeconds(seconds));
|
||||
}
|
||||
|
||||
/// <summary>获取剩余过期时间(秒;0=已过期或无此 key;-1=无过期时间),对应 getExpire</summary>
|
||||
public long GetExpire(string key)
|
||||
{
|
||||
var ttl = Db.KeyTimeToLive(key);
|
||||
return ttl.HasValue ? (long)ttl.Value.TotalSeconds : -1;
|
||||
}
|
||||
|
||||
/// <summary>判断 key 是否存在,对应 hasKey</summary>
|
||||
public bool HasKey(string key)
|
||||
{
|
||||
return Db.KeyExists(key);
|
||||
}
|
||||
|
||||
/// <summary>删除一个或多个 key,对应 del(key...)</summary>
|
||||
public void Del(params string[] keys)
|
||||
{
|
||||
Db.KeyDelete(keys.Select(k => (RedisKey)k).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>按模式扫描 key(对应 keys(pattern);生产慎用通配全库)</summary>
|
||||
public List<string> Keys(string pattern)
|
||||
{
|
||||
var server = _connection.GetServer(_connection.GetEndPoints().First());
|
||||
return server.Keys(pattern: pattern).Select(k => (string)k).ToList();
|
||||
}
|
||||
|
||||
// ==================== String ====================
|
||||
|
||||
/// <summary>读取并反序列化为指定类型;不存在或反序列化失败返回 default</summary>
|
||||
public T Get<T>(string key)
|
||||
{
|
||||
var value = Db.StringGet(key);
|
||||
if (value.IsNullOrEmpty)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return JsonConvert.DeserializeObject<T>(value);
|
||||
}
|
||||
|
||||
/// <summary>读取并反序列化为 List<T>,对应 getList</summary>
|
||||
public List<T> GetList<T>(string key)
|
||||
{
|
||||
return Get<List<T>>(key);
|
||||
}
|
||||
|
||||
/// <summary>写入(不过期),对应 set(key, value)</summary>
|
||||
public bool Set(string key, object value)
|
||||
{
|
||||
return Db.StringSet(key, Serialize(value));
|
||||
}
|
||||
|
||||
/// <summary>写入并设置过期时间(秒),对应 set(key, value, time)</summary>
|
||||
public bool Set(string key, object value, long seconds)
|
||||
{
|
||||
return Db.StringSet(key, Serialize(value), TimeSpan.FromSeconds(seconds));
|
||||
}
|
||||
|
||||
/// <summary>自增(delta 可为负),对应 incr/decr</summary>
|
||||
public long Incr(string key, long delta = 1)
|
||||
{
|
||||
return delta >= 0 ? Db.StringIncrement(key, delta) : Db.StringDecrement(key, -delta);
|
||||
}
|
||||
|
||||
/// <summary>自减,对应 decr</summary>
|
||||
public long Decr(string key, long delta = 1)
|
||||
{
|
||||
return Db.StringDecrement(key, delta);
|
||||
}
|
||||
|
||||
// ==================== Hash ====================
|
||||
|
||||
public T HGet<T>(string key, string field)
|
||||
{
|
||||
var value = Db.HashGet(key, field);
|
||||
return value.IsNullOrEmpty ? default : JsonConvert.DeserializeObject<T>(value);
|
||||
}
|
||||
|
||||
/// <summary>获取整个 Hash(field → 原始 JSON 字符串),对应 hmget</summary>
|
||||
public Dictionary<string, string> HGetAll(string key)
|
||||
{
|
||||
return Db.HashGetAll(key).ToDictionary(e => (string)e.Name, e => (string)e.Value);
|
||||
}
|
||||
|
||||
public bool HSet(string key, string field, object value)
|
||||
{
|
||||
return Db.HashSet(key, field, Serialize(value));
|
||||
}
|
||||
|
||||
/// <summary>写入并设置整个 key 过期时间(秒),对应 hset(key, item, value, time)</summary>
|
||||
public bool HSet(string key, string field, object value, long seconds)
|
||||
{
|
||||
var ok = HSet(key, field, value);
|
||||
if (ok)
|
||||
{
|
||||
Expire(key, seconds);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
/// <summary>批量写入,对应 hmset</summary>
|
||||
public void HMSet(string key, Dictionary<string, object> map, long? seconds = null)
|
||||
{
|
||||
var entries = map.Select(kv => new HashEntry(kv.Key, Serialize(kv.Value))).ToArray();
|
||||
Db.HashSet(key, entries);
|
||||
if (seconds.HasValue)
|
||||
{
|
||||
Expire(key, seconds.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public void HDel(string key, params string[] fields)
|
||||
{
|
||||
Db.HashDelete(key, fields.Select(f => (RedisValue)f).ToArray());
|
||||
}
|
||||
|
||||
public bool HHasKey(string key, string field)
|
||||
{
|
||||
return Db.HashExists(key, field);
|
||||
}
|
||||
|
||||
public double HIncr(string key, string field, double by = 1)
|
||||
{
|
||||
return Db.HashIncrement(key, field, by);
|
||||
}
|
||||
|
||||
public double HDecr(string key, string field, double by = 1)
|
||||
{
|
||||
return Db.HashDecrement(key, field, by);
|
||||
}
|
||||
|
||||
// ==================== Set ====================
|
||||
|
||||
public List<T> SGet<T>(string key)
|
||||
{
|
||||
return Db.SetMembers(key)
|
||||
.Select(v => JsonConvert.DeserializeObject<T>(v))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public bool SHasKey(string key, object value)
|
||||
{
|
||||
return Db.SetContains(key, Serialize(value));
|
||||
}
|
||||
|
||||
/// <summary>向 Set 添加成员,返回新增数量,对应 sSet</summary>
|
||||
public long SAdd(string key, params object[] values)
|
||||
{
|
||||
return Db.SetAdd(key, values.Select(Serialize).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>添加并设置过期时间(秒),对应 sSetAndTime</summary>
|
||||
public long SAdd(string key, long seconds, params object[] values)
|
||||
{
|
||||
var count = SAdd(key, values);
|
||||
Expire(key, seconds);
|
||||
return count;
|
||||
}
|
||||
|
||||
public long SSize(string key)
|
||||
{
|
||||
return Db.SetLength(key);
|
||||
}
|
||||
|
||||
/// <summary>移除成员,返回移除数量,对应 setRemove</summary>
|
||||
public long SRemove(string key, params object[] values)
|
||||
{
|
||||
return Db.SetRemove(key, values.Select(Serialize).ToArray());
|
||||
}
|
||||
|
||||
// ==================== List ====================
|
||||
|
||||
/// <summary>按区间读取(-1 表示末尾),对应 lGet</summary>
|
||||
public List<T> LGet<T>(string key, long start = 0, long end = -1)
|
||||
{
|
||||
return Db.ListRange(key, start, end)
|
||||
.Select(v => JsonConvert.DeserializeObject<T>(v))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public long LSize(string key)
|
||||
{
|
||||
return Db.ListLength(key);
|
||||
}
|
||||
|
||||
public T LGetIndex<T>(string key, long index)
|
||||
{
|
||||
var value = Db.ListGetByIndex(key, index);
|
||||
return value.IsNullOrEmpty ? default : JsonConvert.DeserializeObject<T>(value);
|
||||
}
|
||||
|
||||
/// <summary>右推入单个元素,对应 lSet(key, value)</summary>
|
||||
public long LPush(string key, object value, long? seconds = null)
|
||||
{
|
||||
var len = Db.ListRightPush(key, Serialize(value));
|
||||
if (seconds.HasValue)
|
||||
{
|
||||
Expire(key, seconds.Value);
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
/// <summary>右推入整个列表(覆盖式写入对应 Java lSet(key, list))</summary>
|
||||
public long LPushRange(string key, IEnumerable<object> values, long? seconds = null)
|
||||
{
|
||||
var len = Db.ListRightPush(key, values.Select(Serialize).ToArray());
|
||||
if (seconds.HasValue)
|
||||
{
|
||||
Expire(key, seconds.Value);
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
/// <summary>按索引更新,对应 lUpdateIndex</summary>
|
||||
public void LSetByIndex(string key, long index, object value)
|
||||
{
|
||||
Db.ListSetByIndex(key, index, Serialize(value));
|
||||
}
|
||||
|
||||
/// <summary>移除 count 个等于 value 的元素,对应 lRemove</summary>
|
||||
public long LRemove(string key, long count, object value)
|
||||
{
|
||||
return Db.ListRemove(key, Serialize(value), count);
|
||||
}
|
||||
|
||||
// ==================== 分布式锁 ====================
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取分布式锁(SET NX EX)。token 建议使用 Guid,
|
||||
/// 释放时必须传入同一 token(防止误删他人锁)。
|
||||
/// </summary>
|
||||
public bool TryLock(string name, string token, int expireSeconds = 30)
|
||||
{
|
||||
return Db.StringSet(RedisConstants.LockKeyOf(name), token,
|
||||
TimeSpan.FromSeconds(expireSeconds), When.NotExists);
|
||||
}
|
||||
|
||||
/// <summary>释放锁(Lua 校验 token 后删除,原子操作)</summary>
|
||||
public bool ReleaseLock(string name, string token)
|
||||
{
|
||||
const string script =
|
||||
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
|
||||
var result = (int)Db.ScriptEvaluate(script,
|
||||
new RedisKey[] { RedisConstants.LockKeyOf(name) },
|
||||
new RedisValue[] { token });
|
||||
return result == 1;
|
||||
}
|
||||
|
||||
private static RedisValue Serialize(object value)
|
||||
{
|
||||
return value is string s ? s : JsonConvert.SerializeObject(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace YhCore.Common.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局配置门面(对齐 Java com.yunhai.bschclassmanage.config.Config 枚举:
|
||||
/// API_PATH / MOBILEAPI_PATH / OAPI + FILE_WEB_PATH / FILE_PATH)。
|
||||
/// 路由常量复用 <see cref="YhCore.Common.ApiRoutes"/>;文件路径由 Startup 从配置初始化。
|
||||
/// </summary>
|
||||
public static class ApiPathConfig
|
||||
{
|
||||
/// <summary>PC 端接口前缀(对应 Java Config.API_PATH)</summary>
|
||||
public const string ApiPath = "api";
|
||||
|
||||
/// <summary>移动端接口前缀(对应 Java Config.MOBILEAPI_PATH)</summary>
|
||||
public const string MobiApiPath = "mobiapi";
|
||||
|
||||
/// <summary>开放接口前缀(对应 Java Config.OAPI)</summary>
|
||||
public const string OApiPath = "oapi";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace YhCore.Common.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局配置静态门面(对齐 Java com.yunhai.bschclassmanage.config.Config 枚举:
|
||||
/// API_PATH / MOBILEAPI_PATH / OAPI + FILE_WEB_PATH / FILE_PATH)。
|
||||
/// 路由常量复用 <see cref="YhCore.Common.ApiRoutes"/>;文件路径由 Startup 从配置初始化。
|
||||
/// </summary>
|
||||
public static class Config
|
||||
{
|
||||
/// <summary>PC 端接口前缀(对应 Java Config.API_PATH)</summary>
|
||||
public const string ApiPath = "api";
|
||||
|
||||
/// <summary>移动端接口前缀(对应 Java Config.MOBILEAPI_PATH)</summary>
|
||||
public const string MobiApiPath = "mobiapi";
|
||||
|
||||
/// <summary>开放接口前缀(对应 Java Config.OAPI)</summary>
|
||||
public const string OApiPath = "oapi";
|
||||
|
||||
/// <summary>文件存储根路径(对应 Java FILE_PATH,由 Startup 注入)</summary>
|
||||
public static string FilePath { get; set; }
|
||||
|
||||
/// <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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace YhCore.Common.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据库配置(对应 appsettings DatabaseProvider + ConnectionStrings 节)。
|
||||
/// </summary>
|
||||
public class DatabaseConfig
|
||||
{
|
||||
/// <summary>数据库提供者:SqlServer / MySql</summary>
|
||||
public string DatabaseProvider { get; set; } = "SqlServer";
|
||||
|
||||
/// <summary>SQL Server 连接串</summary>
|
||||
public string SqlServer { get; set; }
|
||||
|
||||
/// <summary>MySQL 连接串</summary>
|
||||
public string Mysql { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace YhCore.Common.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件存储配置(对应 appsettings FILE_PATH / FILE_WEB_PATH,对齐 Java config.FileConfig)。
|
||||
/// </summary>
|
||||
public class FileConfig
|
||||
{
|
||||
/// <summary>文件存储根路径(对应 Java FILE_PATH)</summary>
|
||||
public string FilePath { get; set; }
|
||||
|
||||
/// <summary>文件访问 URL 前缀(对应 Java FILE_WEB_PATH)</summary>
|
||||
public string FileWebPath { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace YhCore.Common.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// Redis 配置(对应 appsettings Redis 节,对齐 Java config.RedisConfig)。
|
||||
/// </summary>
|
||||
public class RedisConfig
|
||||
{
|
||||
/// <summary>连接串(如 localhost:6379,defaultDatabase=0)</summary>
|
||||
public string ConnectionString { get; set; } = "localhost:6379,defaultDatabase=0";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace YhCore.Common.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// Swagger 配置(对应 appsettings Swagger 节,对齐 Java config.SwaggerConfig)。
|
||||
/// </summary>
|
||||
public class SwaggerConfig
|
||||
{
|
||||
public string Title { get; set; } = "YhCore API";
|
||||
|
||||
public string Description { get; set; }
|
||||
|
||||
public string Version { get; set; } = "v1";
|
||||
|
||||
/// <summary>分组(api / mobiapi / oapi)</summary>
|
||||
public string[] Groups { get; set; } = { ApiPathConfig.ApiPath, ApiPathConfig.MobiApiPath, ApiPathConfig.OApiPath };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Yh.Platform.Abstractions;
|
||||
|
||||
namespace YhCore.Common.Exceptions
|
||||
{
|
||||
/// <summary>int 错误码 → 新框架 <see cref="IErrorCode"/> 映射(对齐 Java ErrorCode 常量值)。</summary>
|
||||
/// <remarks>
|
||||
/// 用途:业务直接抛 <see cref="DException"/> 时,把数值错误码转成 IErrorCode:
|
||||
/// <code>throw new DException(ErrorCodeUtil.From(5001), "库存不足");</code>
|
||||
/// </remarks>
|
||||
public static class ErrorCodeUtil
|
||||
{
|
||||
public const int Default = 1;
|
||||
|
||||
/// <summary>按数值映射平台已知错误码;未知码保留原值(避免业务自定义错误码被吞成 Error=1)。</summary>
|
||||
public static IErrorCode From(int code) => code switch
|
||||
{
|
||||
0 => ErrorCode.Success,
|
||||
2 => ErrorCode.ParamError,
|
||||
3 => ErrorCode.Forbidden,
|
||||
4 => ErrorCode.Unauthorized,
|
||||
5 => ErrorCode.Custom,
|
||||
6 => ErrorCode.InternalServerError,
|
||||
405 => ErrorCode.HttpMethodNotAllowed,
|
||||
_ => new RawErrorCode(code, ""),
|
||||
};
|
||||
|
||||
/// <summary>未知码透传实现:保留调用方原始 code,msg 由业务消息承载。</summary>
|
||||
private sealed record RawErrorCode(int Code, string Msg) : IErrorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using Newtonsoft.Json;
|
||||
using Yh.Platform.Abstractions;
|
||||
|
||||
namespace YhCore.Common.Result
|
||||
{
|
||||
/// <summary>
|
||||
/// 统一响应(对应 Java yh-api-std:com.yunhai.platform.apistd.ApiResponse)。
|
||||
/// 信封字段 code/msg/data 以 [JsonProperty] 显式钉死小写——全局序列化已改为
|
||||
/// DefaultContractResolver 原样输出(data 内容对齐旧项目混合大小写字段),信封不受影响。
|
||||
/// 注意:Controller 返回值由全局过滤器 GlobalWrapResultFilter 自动包装;
|
||||
/// 不需要包装的接口用 [NoWrapResult] 特性显式排除(CAS 协议端点、文件下载等)。
|
||||
/// </summary>
|
||||
public class ApiResult<T>
|
||||
{
|
||||
/// <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 };
|
||||
|
||||
public static ApiResult<T> Ok() => new() { Code = ErrorCode.Success.Code, Msg = "" };
|
||||
|
||||
/// <summary>失败(对齐 Java fail(IErrorCode) / fail(int, String))</summary>
|
||||
public static ApiResult<T> Fail(int code, string msg) => new() { Code = code, Msg = msg };
|
||||
|
||||
/// <summary>失败(保留旧签名:msg 在前,默认错误码 ERROR=1)</summary>
|
||||
public static ApiResult<T> Fail(string msg, int code = 1) => new() { Code = code, Msg = msg };
|
||||
|
||||
/// <summary>失败(对齐 Java fail(IErrorCode[, String]),msg 缺省取错误码默认消息)</summary>
|
||||
public static ApiResult<T> Fail(IErrorCode errorCode, string msg = null) => new()
|
||||
{
|
||||
Code = errorCode.Code,
|
||||
Msg = string.IsNullOrEmpty(msg) ? errorCode.Msg : msg
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>无数据体的统一响应便捷类</summary>
|
||||
public static class ApiResult
|
||||
{
|
||||
public static ApiResult<object> Ok(object data = null) => ApiResult<object>.Ok(data);
|
||||
public static ApiResult<object> Fail(string msg, int code = 1) => ApiResult<object>.Fail(msg, code);
|
||||
public static ApiResult<object> Fail(int code, string msg) => ApiResult<object>.Fail(code, msg);
|
||||
public static ApiResult<object> Fail(IErrorCode errorCode, string msg = null) => ApiResult<object>.Fail(errorCode, msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
namespace YhCore.Common.UserContext
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前登录用户上下文(对应 Java 版 com.yunhai.bschclassmanage.utils.UserContext)。
|
||||
/// 由 UserContextMiddleware 在请求入口写入,全链路通过 <see cref="UserContextHolder"/> 读取。
|
||||
/// </summary>
|
||||
public class UserContext
|
||||
{
|
||||
/// <summary>用户 ID(对应 BoaUser.id)</summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>用户姓名</summary>
|
||||
public string UserName { get; set; }
|
||||
|
||||
/// <summary>登录账号</summary>
|
||||
public string Account { get; set; }
|
||||
|
||||
/// <summary>所属学校 ID(对应 Java UserContext.getSchId())</summary>
|
||||
public int? SchoolId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属学校 UUID 标识(旧 SysUser.schId / SchInfoVo.SchSerId,如 "xxxx-xxxx-xxxx")。
|
||||
/// 由 LegacyUserContextAdapter 通过教务 SDK GetSchFirstAsync 反查填充(进程内缓存,避免每请求调用);
|
||||
/// 反查失败或无学校时为 null。
|
||||
/// </summary>
|
||||
public string SchSerID { get; set; }
|
||||
|
||||
/// <summary>所属学校名称</summary>
|
||||
public string SchoolName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户类型(对应 Java UserContext.getType() / BoaUser.Type;1=教师等,具体枚举见用户中心约定)。
|
||||
/// </summary>
|
||||
public int? UserType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色 ID(对应 Java UserContext.getRoleId():取 BoaUser.RoleIds 首个,回退 CurrentRole.RoleId)。
|
||||
/// </summary>
|
||||
public int? Role { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户中心返回的完整 BoaUser(JSON 从认证 Claim 反序列化,GetUserInfo 直接返回)。
|
||||
/// </summary>
|
||||
public object FullBoaUser { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Threading;
|
||||
|
||||
namespace YhCore.Common.UserContext
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户上下文持有器。基于 <see cref="AsyncLocal{T}"/> 实现,
|
||||
/// 是 Java ThreadLocal 方案在 .NET async 环境下的安全等价物。
|
||||
/// </summary>
|
||||
public static class UserContextHolder
|
||||
{
|
||||
private static readonly AsyncLocal<UserContext> _current = new AsyncLocal<UserContext>();
|
||||
|
||||
/// <summary>当前请求的用户;未登录或中间件未写入时为 null。</summary>
|
||||
public static UserContext Current
|
||||
{
|
||||
get => _current.Value;
|
||||
set => _current.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>当前用户学校 ID;无上下文时返回 0(对应 Java UserContext.getSchId())。</summary>
|
||||
public static int GetSchId() => _current.Value?.SchoolId ?? 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace YhCore.Common.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页元信息(对应 Java 版 model.tool.PageInfo)。
|
||||
/// C# 属性为 PascalCase,ASP.NET Core 默认 camelCase 序列化后
|
||||
/// JSON 字段与 Java 版一致(pageNum/pageSize/total/totalPages/hasNext/hasPrev)。
|
||||
/// </summary>
|
||||
public class PageInfo
|
||||
{
|
||||
/// <summary>当前页码(从 0 开始,与 Spring Pageable 一致)</summary>
|
||||
public int PageNum { get; set; }
|
||||
|
||||
/// <summary>每页大小</summary>
|
||||
public int PageSize { get; set; }
|
||||
|
||||
/// <summary>总记录数</summary>
|
||||
public int Total { get; set; }
|
||||
|
||||
/// <summary>总页数</summary>
|
||||
public int TotalPages { get; set; }
|
||||
|
||||
/// <summary>是否有下一页</summary>
|
||||
public bool HasNext { get; set; }
|
||||
|
||||
/// <summary>是否有上一页</summary>
|
||||
public bool HasPrev { get; set; }
|
||||
|
||||
public PageInfo()
|
||||
{
|
||||
}
|
||||
|
||||
public PageInfo(int pageNum, int pageSize, int total, int totalPages, bool hasNext, bool hasPrev)
|
||||
{
|
||||
PageNum = pageNum;
|
||||
PageSize = pageSize;
|
||||
Total = total;
|
||||
TotalPages = totalPages;
|
||||
HasNext = hasNext;
|
||||
HasPrev = hasPrev;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace YhCore.Common.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// 内存分页工具(对应 Java 版 utils.PageUtil)。
|
||||
/// 用于已对全量数据在内存中的场景(如外部接口聚合结果)做分页;
|
||||
/// 数据库分页请在 Repository 层用 IQueryable Skip/Take 下推,不要本类。
|
||||
/// 页码从 0 开始,与 Spring Pageable 一致。
|
||||
/// </summary>
|
||||
public static class PageUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// 对列表做内存分页,返回当前页数据(对应 Java paginate)。
|
||||
/// 空列表或页码越界时返回空列表。
|
||||
/// </summary>
|
||||
/// <param name="list">全量数据</param>
|
||||
/// <param name="pageNum">页码,从 0 开始</param>
|
||||
/// <param name="pageSize">每页大小</param>
|
||||
public static List<T> Paginate<T>(IReadOnlyCollection<T> list, int pageNum, int pageSize)
|
||||
{
|
||||
if (list == null || list.Count == 0 || pageSize <= 0)
|
||||
{
|
||||
return new List<T>();
|
||||
}
|
||||
|
||||
var fromIndex = pageNum * pageSize;
|
||||
if (fromIndex >= list.Count)
|
||||
{
|
||||
return new List<T>();
|
||||
}
|
||||
|
||||
return list.Skip(fromIndex).Take(pageSize).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取分页元信息(对应 Java getPageInfo)。
|
||||
/// </summary>
|
||||
/// <param name="list">原始数据列表</param>
|
||||
/// <param name="pageNum">页码,从 0 开始</param>
|
||||
/// <param name="pageSize">每页大小</param>
|
||||
public static PageInfo GetPageInfo<T>(IReadOnlyCollection<T> list, int pageNum, int pageSize)
|
||||
{
|
||||
var total = list?.Count ?? 0;
|
||||
var totalPages = pageSize > 0 ? (int)Math.Ceiling((double)total / pageSize) : 0;
|
||||
return new PageInfo(
|
||||
pageNum,
|
||||
pageSize,
|
||||
total,
|
||||
totalPages,
|
||||
hasNext: pageNum < totalPages - 1,
|
||||
hasPrev: pageNum > 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 内存分页 + 元信息一次完成(Paginate 与 GetPageInfo 的组合便捷方法)。
|
||||
/// </summary>
|
||||
public static (List<T> Items, PageInfo Info) PaginateWithInfo<T>(
|
||||
IReadOnlyCollection<T> list, int pageNum, int pageSize)
|
||||
{
|
||||
return (Paginate(list, pageNum, pageSize), GetPageInfo(list, pageNum, pageSize));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<RootNamespace>YhCore.Common</RootNamespace>
|
||||
<AssemblyName>YhCore.Common</AssemblyName>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.6.122" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- 平台模型与信封(DException/ErrorCode/BoaUser 等):从 Nexus nuget-group 引用(原为本地源项目引用) -->
|
||||
<PackageReference Include="Yh.Platform.Abstractions" Version="1.0.0-preview.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,25 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class A_DataLog
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public string ModuleName { get; set; }
|
||||
public string TableName { get; set; }
|
||||
public Nullable<int> OldSerID { get; set; }
|
||||
public Nullable<int> NowSerID { get; set; }
|
||||
public Nullable<int> RId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class AppMsgRecord
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public int AppSerID { get; set; }
|
||||
public string AppName { get; set; }
|
||||
public string AppCode { get; set; }
|
||||
public string MsgType { get; set; }
|
||||
public string MsgContent { get; set; }
|
||||
public string MsgError { get; set; }
|
||||
public string UserID { get; set; }
|
||||
public string SUserID { get; set; }
|
||||
public string SUserPhone { get; set; }
|
||||
public string SendType { get; set; }
|
||||
public string TabCode { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public Nullable<System.DateTime> Datetime { get; set; }
|
||||
public Nullable<int> Read { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Base_Log
|
||||
{
|
||||
public Nullable<System.DateTime> Date { get; set; }
|
||||
public Nullable<int> Thread { get; set; }
|
||||
public string LEVEL { get; set; }
|
||||
public string Logger { get; set; }
|
||||
public string MESSAGE { get; set; }
|
||||
public string EXCEPTION { get; set; }
|
||||
public int Id { get; set; }
|
||||
public string AppName { get; set; }
|
||||
public string UserId { get; set; }
|
||||
public string Type { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_Dictionary
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public string typeID { get; set; }
|
||||
public string typeName { get; set; }
|
||||
public string CodeID { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Des { get; set; }
|
||||
public int Enable { get; set; }
|
||||
public string Schserid { get; set; }
|
||||
public Nullable<System.Guid> PID { get; set; }
|
||||
public string Path { get; set; }
|
||||
public Nullable<int> Sort { get; set; }
|
||||
public string Code { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_DictionaryOrd
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public int DicSerID { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public Nullable<bool> Open { get; set; }
|
||||
public Nullable<int> Ord { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_Groupuser
|
||||
{
|
||||
public int id { get; set; }
|
||||
public Nullable<int> usergroup { get; set; }
|
||||
public Nullable<int> userid { get; set; }
|
||||
public Nullable<int> flowid { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public Nullable<int> IsOk { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_Groupuser_SHRole
|
||||
{
|
||||
public int id { get; set; }
|
||||
public Nullable<int> usergroup { get; set; }
|
||||
public Nullable<decimal> SHRoleid { get; set; }
|
||||
public Nullable<int> flowid { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public Nullable<int> IsOk { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_SendUserinfo
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public Nullable<int> userid { get; set; }
|
||||
public string Senduserid { get; set; }
|
||||
public string schserid { get; set; }
|
||||
public Nullable<System.DateTime> createTime { get; set; }
|
||||
public Nullable<int> flowType { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_UpFiles
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string TopicType { get; set; }
|
||||
public Nullable<int> TopicID { get; set; }
|
||||
public string FileTrueName { get; set; }
|
||||
public string FileName { get; set; }
|
||||
public Nullable<int> FileSize { get; set; }
|
||||
public string FileType { get; set; }
|
||||
public Nullable<System.DateTime> DateAndTime { get; set; }
|
||||
public Nullable<int> DownTimes { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_UserFlowZhipai
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public int UserID { get; set; }
|
||||
public int MSerID { get; set; }
|
||||
public Nullable<int> FlowID { get; set; }
|
||||
public Nullable<int> FlowStepID { get; set; }
|
||||
public string zhipai { get; set; }
|
||||
public string type { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_flowinfo
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string flowname { get; set; }
|
||||
public string flowdes { get; set; }
|
||||
public int enable { get; set; }
|
||||
public int flowType { get; set; }
|
||||
public System.DateTime deitdate { get; set; }
|
||||
public string schSerID { get; set; }
|
||||
public string deptid { get; set; }
|
||||
public string typeid { get; set; }
|
||||
public string ChaoSend { get; set; }
|
||||
public Nullable<int> StepSend { get; set; }
|
||||
public string WYPageGG { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_flowstep
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public Nullable<int> flowid { get; set; }
|
||||
public string StepName { get; set; }
|
||||
public Nullable<int> ordernum { get; set; }
|
||||
public Nullable<int> prestep { get; set; }
|
||||
public Nullable<int> action_user { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public Nullable<int> cond { get; set; }
|
||||
public string isshowJE { get; set; }
|
||||
public string isshowBtn { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_flowtranctInfo
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string datatype { get; set; }
|
||||
public int dataid { get; set; }
|
||||
public int stepid { get; set; }
|
||||
public Nullable<int> actionuserid { get; set; }
|
||||
public short actionstatus { get; set; }
|
||||
public Nullable<System.DateTime> actiondate { get; set; }
|
||||
public string remark { get; set; }
|
||||
public string undertaker { get; set; }
|
||||
public string Pid { get; set; }
|
||||
public string DoneUserID { get; set; }
|
||||
public string filespath { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_flowuser
|
||||
{
|
||||
public int id { get; set; }
|
||||
public Nullable<int> usergroup { get; set; }
|
||||
public Nullable<int> userid { get; set; }
|
||||
public Nullable<int> flowid { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_freeGroupuser
|
||||
{
|
||||
public int id { get; set; }
|
||||
public Nullable<int> usergroup { get; set; }
|
||||
public Nullable<int> userid { get; set; }
|
||||
public Nullable<int> flowid { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public Nullable<int> IsOk { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_freeflowinfo
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public Nullable<int> userid { get; set; }
|
||||
public string shenheuserid { get; set; }
|
||||
public string schserid { get; set; }
|
||||
public Nullable<int> steptype { get; set; }
|
||||
public Nullable<System.DateTime> createTime { get; set; }
|
||||
public Nullable<int> flowType { get; set; }
|
||||
public Nullable<int> dataid { get; set; }
|
||||
public Nullable<int> laststep { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
/// <summary>
|
||||
/// 系统审计日志(对应 Java 版 entity.SysOperationLog,表 sys_operation_log)。
|
||||
/// HTTP 接口级全量日志:由 SysOperationLogFilter 在每次 Controller 请求后写入。
|
||||
/// 字段与 Java 实体一一对齐;重试三字段(RetryCount/LastRetryStatus/LastRetryTime)
|
||||
/// 为 Java 双轨同步保留,.NET 侧当前不写入(保持 NULL/默认)。
|
||||
/// 表名映射见 YHDbContext.OnModelCreating:ToTable("sys_operation_log")。
|
||||
/// </summary>
|
||||
public class SysOperationLog
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>日志级别:1-重要(删除) 2-次要(新增/编辑) 3-一般(查询)</summary>
|
||||
public int LogLevel { get; set; }
|
||||
|
||||
/// <summary>操作类型:DELETE / CREATE / UPDATE / QUERY</summary>
|
||||
public string OperationType { get; set; }
|
||||
|
||||
/// <summary>操作人 ID(来自 UserContext.Id)</summary>
|
||||
public int? UserId { get; set; }
|
||||
|
||||
/// <summary>操作人姓名(来自 UserContext.UserName,匿名时 anonymous)</summary>
|
||||
public string UserName { get; set; }
|
||||
|
||||
/// <summary>学校 ID(多校隔离,来自 UserContext.SchoolId)</summary>
|
||||
public int? SchId { get; set; }
|
||||
|
||||
/// <summary>客户端 IP</summary>
|
||||
public string RequestIp { get; set; }
|
||||
|
||||
/// <summary>请求 URI</summary>
|
||||
public string RequestUri { get; set; }
|
||||
|
||||
/// <summary>HTTP 方法</summary>
|
||||
public string RequestMethod { get; set; }
|
||||
|
||||
/// <summary>类名.方法名</summary>
|
||||
public string ClassMethod { get; set; }
|
||||
|
||||
/// <summary>请求参数(JSON 序列化,含截断)</summary>
|
||||
public string RequestParams { get; set; }
|
||||
|
||||
/// <summary>GZIP 压缩后的响应 JSON</summary>
|
||||
public byte[] ResponseData { get; set; }
|
||||
|
||||
/// <summary>业务状态码:0 成功 / 非 0 失败(全局返回对象 code)</summary>
|
||||
public int StatusCode { get; set; }
|
||||
|
||||
/// <summary>耗时(ms)</summary>
|
||||
public long CostTime { get; set; }
|
||||
|
||||
/// <summary>User-Agent</summary>
|
||||
public string UserAgent { get; set; }
|
||||
|
||||
/// <summary>异常信息(截断 8000)</summary>
|
||||
public string ErrorMsg { get; set; }
|
||||
|
||||
/// <summary>重试次数(对齐 Java 双轨字段,暂不写入)</summary>
|
||||
public int? RetryCount { get; set; }
|
||||
|
||||
/// <summary>上次重试状态:1 成功 / 0 失败(对齐 Java 双轨字段,暂不写入)</summary>
|
||||
public int? LastRetryStatus { get; set; }
|
||||
|
||||
/// <summary>上次重试时间(对齐 Java 双轨字段,暂不写入)</summary>
|
||||
public DateTime? LastRetryTime { get; set; }
|
||||
|
||||
/// <summary>记录时间</summary>
|
||||
public DateTime CreateTime { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class WY_DocPrint
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public string DocTitle { get; set; }
|
||||
public string DocType { get; set; }
|
||||
public string DocStyle { get; set; }
|
||||
public int DocNum { get; set; }
|
||||
public string DocContent { get; set; }
|
||||
public string UserID { get; set; }
|
||||
public System.DateTime EditDate { get; set; }
|
||||
public string ChkUserID { get; set; }
|
||||
public string DocStatus { get; set; }
|
||||
public string ChkDate { get; set; }
|
||||
public string UpFile { get; set; }
|
||||
public string DPType { get; set; }
|
||||
public Nullable<System.DateTime> TakeDate { get; set; }
|
||||
public string DocCode { get; set; }
|
||||
public Nullable<int> DocStep { get; set; }
|
||||
public Nullable<int> flowid { get; set; }
|
||||
public Nullable<int> rid { get; set; }
|
||||
public Nullable<int> reid { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public string explain { get; set; }
|
||||
public string grade { get; set; }
|
||||
public string Examtype { get; set; }
|
||||
public Nullable<int> DocFix { get; set; }
|
||||
public Nullable<int> DocShare { get; set; }
|
||||
public string FreeUserID { get; set; }
|
||||
public string SendUserID { get; set; }
|
||||
public Nullable<int> freestepid { get; set; }
|
||||
public string zhipai { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class WY_DocPrint_Num
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public Nullable<decimal> DocPrintID { get; set; }
|
||||
public string YSLXName { get; set; }
|
||||
public string YSLXtypeID { get; set; }
|
||||
public Nullable<double> YSLXOrder { get; set; }
|
||||
public string YSGGName { get; set; }
|
||||
public string YSGGtypeID { get; set; }
|
||||
public Nullable<double> YSGGOrder { get; set; }
|
||||
public Nullable<int> DocNum { get; set; }
|
||||
public Nullable<int> DocPage { get; set; }
|
||||
public Nullable<int> DocFix { get; set; }
|
||||
public Nullable<int> DocShare { get; set; }
|
||||
public Nullable<double> UnitPrice { get; set; }
|
||||
public Nullable<double> TotalPrice { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class WY_DocPrint_SH
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public Nullable<decimal> DocPrintSerID { get; set; }
|
||||
public Nullable<decimal> flowSerID { get; set; }
|
||||
public Nullable<decimal> stepSerID { get; set; }
|
||||
public Nullable<int> stepOrder { get; set; }
|
||||
public Nullable<decimal> SHTeaID { get; set; }
|
||||
public string SHTeaName { get; set; }
|
||||
public string AndOr { get; set; }
|
||||
public string isSH { get; set; }
|
||||
public Nullable<System.DateTime> inserttime { get; set; }
|
||||
public string writor { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class WY_SHRole
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public string SchSerId { get; set; }
|
||||
public string SHRoleName { get; set; }
|
||||
public Nullable<System.DateTime> inserttime { get; set; }
|
||||
public string writor { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class WY_SHRole_BSHTea
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public Nullable<decimal> SHRoleID { get; set; }
|
||||
public Nullable<decimal> BSHTeaID { get; set; }
|
||||
public string BSHTeaName { get; set; }
|
||||
public Nullable<System.DateTime> inserttime { get; set; }
|
||||
public string writor { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class WY_SHRole_SHTea
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public Nullable<decimal> SHRoleID { get; set; }
|
||||
public Nullable<decimal> SHTeaID { get; set; }
|
||||
public string SHTeaName { get; set; }
|
||||
public string AndOr { get; set; }
|
||||
public string photo { get; set; }
|
||||
public Nullable<System.DateTime> inserttime { get; set; }
|
||||
public string writor { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class WY_SchZyBit
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public Nullable<bool> ziyuanbit { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class WY_reGetJERd
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public Nullable<decimal> DocPrintID { get; set; }
|
||||
public string DocTitle { get; set; }
|
||||
public string DocContent { get; set; }
|
||||
public Nullable<System.DateTime> EditDatetime { get; set; }
|
||||
public string writor { get; set; }
|
||||
public string type { get; set; }
|
||||
public string UnitPrice { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<RootNamespace>YhCore.Entity</RootNamespace>
|
||||
<AssemblyName>YhCore.Entity</AssemblyName>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>disable</Nullable>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,117 @@
|
||||
using System;
|
||||
|
||||
namespace YhCore.Model.Dto
|
||||
{
|
||||
/// <summary>字典下拉框选项(旧 GetDictList 的 {k=CodeID, v=Name})。</summary>
|
||||
public class DicOption
|
||||
{
|
||||
public string K { get; set; }
|
||||
public string V { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>字典管理列表项(旧 GetDictMangeList 的 {SerID, CodeID, Name, Des, Enable})。</summary>
|
||||
public class DicManageItem
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public string CodeID { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Des { get; set; }
|
||||
public int Enable { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>文印分类 / 印制版面列表项(旧 CategoryList / YZBMList)。</summary>
|
||||
public class CategoryItem
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public string CodeID { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Des { get; set; }
|
||||
public int Enable { get; set; }
|
||||
public int? Sort { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public string UserSchSerID { get; set; }
|
||||
public bool _disabled { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>公共类别列表项(旧 DictionaryTypeList)。</summary>
|
||||
public class DictionaryTypeItem
|
||||
{
|
||||
public string Code { get; set; }
|
||||
public decimal SerID { get; set; }
|
||||
public string CodeID { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Des { get; set; }
|
||||
public int Enable { get; set; }
|
||||
public int? Sort { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public string UserSchSerID { get; set; }
|
||||
public bool _disabled { get; set; }
|
||||
public bool Status { get; set; }
|
||||
public bool Open { get; set; }
|
||||
public int Ord { get; set; }
|
||||
public string Path { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 字典类型常量与映射(对齐旧 EnumConfig.Dictionary 枚举及 CommonController 的硬编码 switch)。
|
||||
/// </summary>
|
||||
public static class DictionaryTypeHelper
|
||||
{
|
||||
/// <summary>GetSysDic 入参 Type(1~10) → 字典 typeID(对齐旧 switch)。</summary>
|
||||
private static readonly string[] SysDic =
|
||||
{ null, "DEPT", "WYFL", "YZBM", "YZFL", "LeaveType", "FoodType", "Unit", "LevelOrder", "filetype", "doclevel" };
|
||||
|
||||
/// <summary>旧 EnumConfig.Dictionary 枚举序值 → typeID(用于 GetDictMangeList 的 Type 参数)。</summary>
|
||||
private static readonly string[] EnumOrdinal =
|
||||
{ "DEPT", "BXBM", "WYFL", "YZBM", "YZFL", "LeaveType", "FoodType", "Unit", "VenueType", "Vefacilities", "Time", "Repeat", "LevelOrder", "filetype", "doclevel", "XKCategory" };
|
||||
|
||||
/// <summary>全部字典类别(typeID, typeName),对齐旧 EnumConfig.Dictionary 的 Description。</summary>
|
||||
public static readonly (string Id, string Name)[] AllTypes =
|
||||
{
|
||||
("WYFL", "文印分类"),
|
||||
("YZBM", "印制版面"),
|
||||
("YZFL", "印制分类"),
|
||||
("filetype", "文件类型")
|
||||
};
|
||||
|
||||
/// <summary>GetSysDic 的 Type(int) → typeID;越界返回 null。</summary>
|
||||
public static string FromSysDicType(int type) => type >= 1 && type <= 10 ? SysDic[type] : null;
|
||||
|
||||
/// <summary>GetDictMangeList 的 Type 参数 → typeID(支持枚举名、枚举序值、typeID 直传)。</summary>
|
||||
public static string ResolveTypeId(string type)
|
||||
{
|
||||
if (string.IsNullOrEmpty(type)) return null;
|
||||
foreach (var t in AllTypes)
|
||||
{
|
||||
if (t.Id == type) return t.Id;
|
||||
}
|
||||
|
||||
if (int.TryParse(type, out var n) && n >= 0 && n < EnumOrdinal.Length)
|
||||
{
|
||||
return EnumOrdinal[n];
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
/// <summary>typeID → typeName(描述);未知则原样返回。</summary>
|
||||
public static string GetTypeName(string typeId)
|
||||
{
|
||||
foreach (var t in AllTypes)
|
||||
{
|
||||
if (t.Id == typeId) return t.Name;
|
||||
}
|
||||
|
||||
return typeId;
|
||||
}
|
||||
|
||||
public static bool IsDept(string typeId) => typeId == "DEPT";
|
||||
|
||||
/// <summary>Des(排序字符串)→ int,解析失败/空回退 int.MaxValue(对齐旧 int.Parse 的降级,避免非数值崩)。</summary>
|
||||
public static int ParseSort(string des)
|
||||
{
|
||||
if (string.IsNullOrEmpty(des)) return int.MaxValue;
|
||||
return int.TryParse(des, out var n) ? n : int.MaxValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace YhCore.Model.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// 文印统计看板 DTO(移植旧 BestSchool.DocPrint.DocPrintStatistics,5 接口)。
|
||||
/// 旧实现以 dynamic 匿名对象返回 {k,v} 结构,新项目统一为强类型 DTO。
|
||||
/// 说明:旧 GetSchSerID(int) 走 HTTP 把 int schId 映射为 GUID;新库 SchSerID 约定已是 int 字符串,
|
||||
/// 故此处直接用 schId.ToString() 匹配,不再依赖外部转换。
|
||||
/// </summary>
|
||||
|
||||
/// <summary>统计项(旧 GetDocPrintCount 的 {k,v},v 为数量或类型名)。</summary>
|
||||
public class DocPrintCountItem
|
||||
{
|
||||
public string k { get; set; }
|
||||
public object v { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>饼图分类项(旧 GetPieChart 的 series.list 元素 {k,v})。</summary>
|
||||
public class PieChartKv
|
||||
{
|
||||
public string k { get; set; }
|
||||
public object v { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>饼图系列(旧 GetPieChart 的 {title,list})。</summary>
|
||||
public class PieChartSeries
|
||||
{
|
||||
public string title { get; set; }
|
||||
public List<PieChartKv> list { get; set; } = new List<PieChartKv>();
|
||||
}
|
||||
|
||||
/// <summary>印刷类型下拉项(旧 GetDocPrintType 的 {CodeID,Name})。</summary>
|
||||
public class DocPrintTypeItem
|
||||
{
|
||||
public string CodeID { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>文印统计列表项(旧 GetDocPrintList 的投影)。</summary>
|
||||
public class DocPrintStatItem
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string DocContent { get; set; }
|
||||
public string DocType { get; set; }
|
||||
public int DocNum { get; set; }
|
||||
public string DocTypeName { get; set; }
|
||||
public string DocStyle { get; set; }
|
||||
public string DocStyleName { get; set; }
|
||||
public string EditDate { get; set; }
|
||||
public string TakeDate { get; set; }
|
||||
public string UserID { get; set; }
|
||||
public string teaname { get; set; }
|
||||
public string UpFile { get; set; }
|
||||
public string DocTitle { get; set; }
|
||||
public string DocStatus { get; set; }
|
||||
public string DocStatusName { get; set; }
|
||||
public int? DocStep { get; set; }
|
||||
public int? flowid { get; set; }
|
||||
public string FreeUserID { get; set; }
|
||||
public string SendUserID { get; set; }
|
||||
public int? freestepid { get; set; }
|
||||
public int? DocShare { get; set; }
|
||||
public string explain { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>文印统计列表结果(旧 GetDocPrintList 的 {tlist,totle})。</summary>
|
||||
public class DocPrintListResult
|
||||
{
|
||||
public List<DocPrintStatItem> tlist { get; set; } = new List<DocPrintStatItem>();
|
||||
public int totle { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace YhCore.Model.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件信息(旧 BestSchool.Entity.FileObj,GetEnclosure/FileSave/FileSaveImage/FileMergeSave 共用)。
|
||||
/// fileurl 为相对路径(如 /upload/docprint/20260101/{guid}.jpg,与旧库数据格式一致);
|
||||
/// host 为文件访问主机前缀(旧 AuthConfig.weboa_url,新 Config.FileWebPath)。
|
||||
/// </summary>
|
||||
public class FileObj
|
||||
{
|
||||
/// <summary>附件表 SerID(SaveEnclosure 落库后回填)。</summary>
|
||||
public int fileid { get; set; }
|
||||
|
||||
/// <summary>文件大小(字节;旧 FileMergeSave 存的是 MB 整数,保持旧语义)。</summary>
|
||||
public int filesize { get; set; }
|
||||
|
||||
/// <summary>扩展名(含点,如 ".jpg")。</summary>
|
||||
public string filetype { get; set; }
|
||||
|
||||
/// <summary>文件真实名称(含扩展名)。</summary>
|
||||
public string filename { get; set; }
|
||||
|
||||
/// <summary>相对访问路径(如 /upload/docprint/20260101/{guid}.jpg)。</summary>
|
||||
public string fileurl { get; set; }
|
||||
|
||||
/// <summary>文件主机前缀(Config.FileWebPath)。</summary>
|
||||
public string host { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace YhCore.Model.Dto
|
||||
{
|
||||
/// <summary>自由流程审核人(旧 ApproveFlow.GetFleeFlow/GetAfterFleeFlow 的 user 数组元素)。</summary>
|
||||
public class FreeFlowUser
|
||||
{
|
||||
public int userid { get; set; }
|
||||
public string name { get; set; }
|
||||
public string photo { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>自由流程节点(旧 GetFleeFlow 列表元素:{ user: [...] })。</summary>
|
||||
public class FreeFlowNode
|
||||
{
|
||||
public List<FreeFlowUser> user { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>审核流程步骤信息(旧 ApproveFlow.FreeShenheinfo,Remark 序列化后为 camelCase 的 remark)。</summary>
|
||||
public class FreeShenheInfo
|
||||
{
|
||||
public string steptname { get; set; }
|
||||
public string steptstatus { get; set; }
|
||||
public string photo { get; set; }
|
||||
public string Remark { get; set; }
|
||||
public string actiondate { get; set; }
|
||||
public string actionstatus { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>抄送人(旧 ChaoSendList 返回 { uid, photo, name, status })。</summary>
|
||||
public class ChaoSendUser
|
||||
{
|
||||
public int uid { get; set; }
|
||||
public string photo { get; set; }
|
||||
public string name { get; set; }
|
||||
public bool status { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace YhCore.Model.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// DocPrint 应用常量(旧 WebHelper.AppCode / AuthConfig.AppKeyMap["docprint"][3])。
|
||||
/// </summary>
|
||||
public static class DocPrintConstants
|
||||
{
|
||||
/// <summary>文印申请 AppCode,即审核流程 / 自由流程的 flowType。</summary>
|
||||
public const int AppCode = 1003;
|
||||
}
|
||||
|
||||
/// <summary>审核流程列表项(旧 Reviewprocess.List / List2 的匿名类型)。</summary>
|
||||
public class FlowListItem
|
||||
{
|
||||
public string SerID { get; set; }
|
||||
public string flowname { get; set; }
|
||||
public string deptid { get; set; }
|
||||
public string typeid { get; set; }
|
||||
public int enable { get; set; }
|
||||
public string enablename { get; set; }
|
||||
public string miaoshu { get; set; }
|
||||
public string deitdate { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>步骤流程列表项(旧 StepList 的匿名类型)。</summary>
|
||||
public class StepListItem
|
||||
{
|
||||
public string SerID { get; set; }
|
||||
public string StepName { get; set; }
|
||||
public string flowid { get; set; }
|
||||
public int? prestep { get; set; }
|
||||
public string TeaName { get; set; }
|
||||
public string Teaid { get; set; }
|
||||
public bool istrue { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>流程绑定步骤项(旧 BindstepList)。</summary>
|
||||
public class BindStepItem
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string StepName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>绑定步骤人员项(旧 BindGroupuserList)。</summary>
|
||||
public class BindGroupuserItem
|
||||
{
|
||||
public int StepId { get; set; }
|
||||
public int? userid { get; set; }
|
||||
public string StepName { get; set; }
|
||||
public string TeaName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>抄送人(旧 Bindflowinfo 的 ChaoSendArr 元素)。</summary>
|
||||
public class ChaoSendItem
|
||||
{
|
||||
public string uid { get; set; }
|
||||
public string name { get; set; }
|
||||
public string photo { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>绑定流程项(旧 Bindflowinfo)。</summary>
|
||||
public class BindFlowItem
|
||||
{
|
||||
public string SerID { get; set; }
|
||||
public string flowname { get; set; }
|
||||
public List<ChaoSendItem> ChaoSendArr { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>教师下拉选项(旧 GetTeaList 的 {value,label})。</summary>
|
||||
public class TeaOption
|
||||
{
|
||||
public int value { get; set; }
|
||||
public string label { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>附件项(旧 filelist)。</summary>
|
||||
public class FileListItem
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string FileTrueName { get; set; }
|
||||
public string FileName { get; set; }
|
||||
public int? FileSize { get; set; }
|
||||
public string FileType { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>自由流程审核人(旧 IsShenheflow 的 OtherData 元素)。</summary>
|
||||
public class FreeFlowItem
|
||||
{
|
||||
public string shenheuserid { get; set; }
|
||||
public string teamnamestr { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>审核流程类型判断结果(旧 IsShenheflow:Data=flowflag,OtherData=自由流程审核人)。</summary>
|
||||
public class ShenheFlowResult
|
||||
{
|
||||
public bool flowflag { get; set; }
|
||||
public List<FreeFlowItem> freeflowlist { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>绑定抄送人(旧 IsSendUser)。</summary>
|
||||
public class SendUserItem
|
||||
{
|
||||
public int uid { get; set; }
|
||||
public string photo { get; set; }
|
||||
public string name { get; set; }
|
||||
public bool status { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>当前用户信息(旧 GetUser 返回 SysUser,新框架取 UserContext 有限字段)。</summary>
|
||||
public class CurrentUserInfo
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public string Account { get; set; }
|
||||
public int? SchoolId { get; set; }
|
||||
public string SchoolName { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace YhCore.Model.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// 文印新增/修改入参(旧 BestSchool.Entity.Model.WY_DocPrintDto : WY_DocPrint + ctype/sms)。
|
||||
/// SerID 仅修改时使用;UserID/SchSerID 由控制器从登录态覆盖。
|
||||
/// </summary>
|
||||
public class WYDocPrintSaveDto
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public string DocTitle { get; set; }
|
||||
public string DocType { get; set; }
|
||||
public string DocStyle { get; set; }
|
||||
public int DocNum { get; set; }
|
||||
public string DocContent { get; set; }
|
||||
public string UserID { get; set; }
|
||||
public string UpFile { get; set; }
|
||||
public string DPType { get; set; }
|
||||
public DateTime? TakeDate { get; set; }
|
||||
public int? DocStep { get; set; }
|
||||
public int? flowid { get; set; }
|
||||
public int? rid { get; set; }
|
||||
public int? reid { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public string explain { get; set; }
|
||||
public string grade { get; set; }
|
||||
public string Examtype { get; set; }
|
||||
public int? DocFix { get; set; }
|
||||
public int? DocShare { get; set; }
|
||||
public string FreeUserID { get; set; }
|
||||
public string SendUserID { get; set; }
|
||||
public int? freestepid { get; set; }
|
||||
public string zhipai { get; set; }
|
||||
/// <summary>旧 WY_DocPrintDto.ctype(AllList 四分类标记),保存时不用,仅保持入参兼容。</summary>
|
||||
public int ctype { get; set; }
|
||||
/// <summary>旧 sms 短信开关;新项目无短信/待办通道,仅保留入参不生效。</summary>
|
||||
public bool sms { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 审核动作入参(旧 Common_flowtranctInfoDto : Common_flowtranctInfo + SendUserID/MySendUserID/sms),
|
||||
/// 用于 Revoke / shenheok / AffirmApplyforOk;actionuserid 由控制器从登录态覆盖。
|
||||
/// </summary>
|
||||
public class FlowTranctDto
|
||||
{
|
||||
public int dataid { get; set; }
|
||||
public int stepid { get; set; }
|
||||
public short actionstatus { get; set; }
|
||||
public int? actionuserid { get; set; }
|
||||
public string remark { get; set; }
|
||||
public string undertaker { get; set; }
|
||||
public string Pid { get; set; }
|
||||
public string DoneUserID { get; set; }
|
||||
public string filespath { get; set; }
|
||||
public string SendUserID { get; set; }
|
||||
public string MySendUserID { get; set; }
|
||||
/// <summary>旧 sms 短信开关;新项目无短信通道,仅保留入参不生效。</summary>
|
||||
public bool sms { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 文印列表项(旧 MyList/SendRdList/SPList/ShenheList/YishenList/totalList/AllList 的匿名投影,
|
||||
/// 序列化 camelCase 后字段名与旧 JSON 一致;ctype 仅 AllList 填充)。
|
||||
/// </summary>
|
||||
public class DocPrintListItem
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string DocContent { get; set; }
|
||||
public string DocType { get; set; }
|
||||
public string DocTypeName { get; set; }
|
||||
public string DocStyle { get; set; }
|
||||
public string DocStyleName { get; set; }
|
||||
public string DPType { get; set; }
|
||||
public string DPTypeName { get; set; }
|
||||
public string EditDate { get; set; }
|
||||
public string TakeDate { get; set; }
|
||||
public string UserID { get; set; }
|
||||
public string teaname { get; set; }
|
||||
public string TeaPhoto { get; set; }
|
||||
public int? DocShare { get; set; }
|
||||
public string UpFile { get; set; }
|
||||
public string DocTitle { get; set; }
|
||||
public string DocStatus { get; set; }
|
||||
public string DocStatusName { get; set; }
|
||||
public int? DocStep { get; set; }
|
||||
public int? flowid { get; set; }
|
||||
public bool _disabled { get; set; }
|
||||
public string FreeUserID { get; set; }
|
||||
public string SendUserID { get; set; }
|
||||
public int? freestepid { get; set; }
|
||||
public string explain { get; set; }
|
||||
/// <summary>AllList 四分类标记:0=待我审 / 1=我已审 / 2=抄送我 / 3=我发起;仅 AllList 返回,其余列表为 null。</summary>
|
||||
public int? ctype { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 文印单条详情(旧 MyListtoOne 的匿名投影:DocStep/flowid 为字符串,含完整业务字段;
|
||||
/// 旧 OtherData.tempresultfile(附件明细)并入 data,因新统一响应无 OtherData 槽位)。
|
||||
/// </summary>
|
||||
public class DocPrintDetailItem
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string DocContent { get; set; }
|
||||
public string DocType { get; set; }
|
||||
public string DocTypeName { get; set; }
|
||||
public string DocStyle { get; set; }
|
||||
public string DocStyleName { get; set; }
|
||||
public string DPType { get; set; }
|
||||
public string DPTypeName { get; set; }
|
||||
public string EditDate { get; set; }
|
||||
public string TakeDate { get; set; }
|
||||
public string UserID { get; set; }
|
||||
public string teaname { get; set; }
|
||||
public string TeaPhoto { get; set; }
|
||||
public string UpFile { get; set; }
|
||||
public string DocTitle { get; set; }
|
||||
public string DocStatus { get; set; }
|
||||
public string DocStatusName { get; set; }
|
||||
/// <summary>旧实现为 item.DocStep.ToString()(null → 空串)。</summary>
|
||||
public string DocStep { get; set; }
|
||||
public string explain { get; set; }
|
||||
/// <summary>旧实现为 item.flowid.ToString()(null → 空串)。</summary>
|
||||
public string flowid { get; set; }
|
||||
public int? DocShare { get; set; }
|
||||
public int? DocFix { get; set; }
|
||||
public int DocNum { get; set; }
|
||||
public bool _disabled { get; set; }
|
||||
public string FreeUserID { get; set; }
|
||||
public string SendUserID { get; set; }
|
||||
public int? freestepid { get; set; }
|
||||
public string zhipai { get; set; }
|
||||
public int? rid { get; set; }
|
||||
public int? reid { get; set; }
|
||||
public string grade { get; set; }
|
||||
public string Examtype { get; set; }
|
||||
/// <summary>旧 OtherData.tempresultfile:附件明细(office 文件替换为 .pdf 预览名)。</summary>
|
||||
public List<DocPrintFileItem> tempresultfile { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>抄送人详情(旧 SendDetailsList 的 {TeaName, TeaPhoto})。</summary>
|
||||
public class SendDetailItem
|
||||
{
|
||||
public string TeaName { get; set; }
|
||||
public string TeaPhoto { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>附件项(旧 WY_list.filelist / MyListtoOne.tempresultfile)。</summary>
|
||||
public class DocPrintFileItem
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string FileTrueName { get; set; }
|
||||
public string FileName { get; set; }
|
||||
public int? FileSize { get; set; }
|
||||
public string FileType { get; set; }
|
||||
public int DownTimes { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>绑定流程下拉项(旧 Bindflowinfo 的 {SerID: string, flowname})。</summary>
|
||||
public class BindFlowOption
|
||||
{
|
||||
public string SerID { get; set; }
|
||||
public string flowname { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>文印数量按月统计项(旧 List1 / Exceldaochu1 的 {dateyear, datemonth, num, DocNum})。</summary>
|
||||
public class MonthStatItem
|
||||
{
|
||||
public int dateyear { get; set; }
|
||||
public int datemonth { get; set; }
|
||||
public int num { get; set; }
|
||||
public int DocNum { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>文印详情统计项(旧 List2 匿名投影)。</summary>
|
||||
public class DocDetailStatItem
|
||||
{
|
||||
public string UserID { get; set; }
|
||||
public string teaname { get; set; }
|
||||
public string DocContent { get; set; }
|
||||
public string DocType { get; set; }
|
||||
public string DocTypeName { get; set; }
|
||||
public string DocStyle { get; set; }
|
||||
public string DocStyleName { get; set; }
|
||||
public string DPType { get; set; }
|
||||
public string DPTypeName { get; set; }
|
||||
public string EditDate { get; set; }
|
||||
public string UpFile { get; set; }
|
||||
public string DocTitle { get; set; }
|
||||
public int DocNum { get; set; }
|
||||
public string DocStatus { get; set; }
|
||||
public string DocStatusName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>文印数量按用户排名项(旧 List3 / Exceldaochu3 的 {UserID, teaname, num, DocNum})。</summary>
|
||||
public class UserRankItem
|
||||
{
|
||||
public string UserID { get; set; }
|
||||
public string teaname { get; set; }
|
||||
public int num { get; set; }
|
||||
public int DocNum { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>按月/按用户统计明细项(旧 List4 / List5 匿名投影,editstatus 对应旧 Common.GetEditDeleteStatus)。</summary>
|
||||
public class StatListItem
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string DocContent { get; set; }
|
||||
public string DocType { get; set; }
|
||||
public string DocTypeName { get; set; }
|
||||
public string DocStyle { get; set; }
|
||||
public string DocStyleName { get; set; }
|
||||
public string DPType { get; set; }
|
||||
public string DPTypeName { get; set; }
|
||||
public string EditDate { get; set; }
|
||||
public string UserID { get; set; }
|
||||
public string teaname { get; set; }
|
||||
public string UpFile { get; set; }
|
||||
public string DocTitle { get; set; }
|
||||
public string DocStatus { get; set; }
|
||||
public string DocStatusName { get; set; }
|
||||
/// <summary>旧 Common.GetEditDeleteStatus(status, step):一审(且非通过/拒绝态)可编辑 → false。</summary>
|
||||
public bool editstatus { get; set; }
|
||||
public int? DocStep { get; set; }
|
||||
public int? flowid { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using YhCore.Entity;
|
||||
|
||||
namespace YhCore.Model.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// 审核角色扩展对象(旧 WY_SHRoleP.cs 文件尾 WYSHRoleExt):角色 + 审核人 + 被审核人树形结构。
|
||||
/// SerID == 0 表示新增,否则更新。
|
||||
/// </summary>
|
||||
public class WYSHRoleExt
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public string SchSerId { get; set; }
|
||||
public string SHRoleName { get; set; }
|
||||
public List<WY_SHRole_SHTea> SHTea { get; set; } = new List<WY_SHRole_SHTea>();
|
||||
public List<WY_SHRole_BSHTea> BSHTea { get; set; } = new List<WY_SHRole_BSHTea>();
|
||||
public DateTime? inserttime { get; set; }
|
||||
public string writor { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>步骤 + 审核教师(旧 WYStepSHTea,SHState 形如 "待审核_0" / "同意_1")。</summary>
|
||||
public class WYStepSHTea
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public int? flowid { get; set; }
|
||||
public string StepName { get; set; }
|
||||
public string SHState { get; set; }
|
||||
public string SHtime { get; set; }
|
||||
public int? ordernum { get; set; }
|
||||
public int? prestep { get; set; }
|
||||
public int? cond { get; set; }
|
||||
public List<WY_SHRole_SHTea> SHTea { get; set; } = new List<WY_SHRole_SHTea>();
|
||||
}
|
||||
|
||||
/// <summary>用户可申请的流程(旧 WYFlowForUser,getLCOfUser / editDocPrint 的 data 段)。</summary>
|
||||
public class WYFlowForUser
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public string txt { get; set; }
|
||||
public string flowname { get; set; }
|
||||
public string UserID { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public string DateNowStr { get; set; }
|
||||
public string ChaoSend { get; set; }
|
||||
public string ChaoSendName { get; set; }
|
||||
public int? StepSend { get; set; }
|
||||
public string WYPageGG { get; set; }
|
||||
public List<WYStepSHTea> StepSHTea { get; set; } = new List<WYStepSHTea>();
|
||||
}
|
||||
|
||||
/// <summary>审核轨迹展示项(旧 FreeShenheinfoSHRole)。</summary>
|
||||
public class FreeShenheinfoSHRole
|
||||
{
|
||||
public string steptname { get; set; }
|
||||
public string steptstatus { get; set; }
|
||||
public string photo { get; set; }
|
||||
public string Remark { get; set; }
|
||||
public string actiondate { get; set; }
|
||||
public string actionstatus { get; set; }
|
||||
public List<string> SignaturePic { get; set; } = new List<string>();
|
||||
}
|
||||
|
||||
/// <summary>审核角色流程列表项(旧 ListFlow 投影,含 ChaoSend/StepSend/WYPageGG)。</summary>
|
||||
public class SHRoleFlowListItem
|
||||
{
|
||||
public string SerID { get; set; }
|
||||
public string flowname { get; set; }
|
||||
public string deptid { get; set; }
|
||||
public string typeid { get; set; }
|
||||
public int enable { get; set; }
|
||||
public string enablename { get; set; }
|
||||
public string miaoshu { get; set; }
|
||||
public string deitdate { get; set; }
|
||||
public string ChaoSend { get; set; }
|
||||
public int? StepSend { get; set; }
|
||||
public string WYPageGG { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>步骤详情(旧 getStepInfo:Data=步骤,OtherData=步骤绑定的审核角色)。</summary>
|
||||
public class SHRoleStepInfo
|
||||
{
|
||||
public List<Common_flowstep> Steps { get; set; } = new List<Common_flowstep>();
|
||||
public List<Common_Groupuser_SHRole> GroupUsers { get; set; } = new List<Common_Groupuser_SHRole>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 审核轨迹结果(旧 getshenheFlow:Data=时间线,OtherData=步骤+审核教师明细,Page=当前步骤列表)。
|
||||
/// 新统一响应无 OtherData/Page 槽位,三部分合并为一个对象返回。
|
||||
/// </summary>
|
||||
public class SHRoleShenheFlowResult
|
||||
{
|
||||
public List<FreeShenheinfoSHRole> Flow { get; set; } = new List<FreeShenheinfoSHRole>();
|
||||
public List<WYStepSHTea> Steps { get; set; } = new List<WYStepSHTea>();
|
||||
public List<Common_flowstep> CurrentStep { get; set; } = new List<Common_flowstep>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 文印规格表结果(旧 getdocOftable:Data=规格表,OtherData=每规格默认页数,Page=首列合并行数数组)。
|
||||
/// 合并为一个对象返回。
|
||||
/// </summary>
|
||||
public class SHRoleDocOfTableResult
|
||||
{
|
||||
public List<WY_DocPrint_Num> Table { get; set; } = new List<WY_DocPrint_Num>();
|
||||
public List<int> MinPageNum { get; set; } = new List<int>();
|
||||
public List<int> RowSpan { get; set; } = new List<int>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 审核角色视角的文印列表项(旧 MyListSHRole / totalListSHRole / SendRdListSHRole / SPListSHRole 投影,
|
||||
/// 各列表仅填充自身所需字段,其余为默认值)。
|
||||
/// </summary>
|
||||
public class SHRoleDocPrintListItem
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string DocContent { get; set; }
|
||||
public string DocType { get; set; }
|
||||
public string DocTypeName { get; set; }
|
||||
public string DocStyle { get; set; }
|
||||
public string DocStyleName { get; set; }
|
||||
public string DPType { get; set; }
|
||||
public string DPTypeName { get; set; }
|
||||
public string EditDate { get; set; }
|
||||
public string TakeDate { get; set; }
|
||||
public string UserID { get; set; }
|
||||
public string teaname { get; set; }
|
||||
public string TeaPhoto { get; set; }
|
||||
public string UpFile { get; set; }
|
||||
public string DocTitle { get; set; }
|
||||
public string DocStatus { get; set; }
|
||||
public string DocStatusName { get; set; }
|
||||
public bool _disabled { get; set; }
|
||||
public int? DocStep { get; set; }
|
||||
public int? flowid { get; set; }
|
||||
public string FreeUserID { get; set; }
|
||||
public string SendUserID { get; set; }
|
||||
public int? freestepid { get; set; }
|
||||
public int? DocShare { get; set; }
|
||||
public string explain { get; set; }
|
||||
public int? rid { get; set; }
|
||||
public int? reid { get; set; }
|
||||
public string DWName { get; set; }
|
||||
public string ZBName { get; set; }
|
||||
public string grade { get; set; }
|
||||
public string Examtype { get; set; }
|
||||
public int DocNum { get; set; }
|
||||
public int? DocFix { get; set; }
|
||||
public string zhipai { get; set; }
|
||||
public List<WY_DocPrint_Num> DocPrintNum { get; set; } = new List<WY_DocPrint_Num>();
|
||||
public string SHRName { get; set; }
|
||||
public bool isshowje { get; set; }
|
||||
public string ChkDate { get; set; }
|
||||
public string firstSHDate { get; set; }
|
||||
public string firstSHDateTiket { get; set; }
|
||||
public int FJDownInfo { get; set; }
|
||||
/// <summary>附件明细(仅 MyListtoOneSHRole 填充,旧 OtherData.tempresultfile 并入 data)。</summary>
|
||||
public List<DocPrintFileItem> tempresultfile { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按金额取步骤结果(旧 getStepByJE:Data=可见步骤+审核教师,OtherData=通过金额过滤的全部步骤)。
|
||||
/// 新统一响应无 OtherData 槽位,两部分合并为一个对象返回。
|
||||
/// </summary>
|
||||
public class SHRoleStepByJEResult
|
||||
{
|
||||
public List<WYStepSHTea> StepSHTea { get; set; } = new List<WYStepSHTea>();
|
||||
public List<Common_flowstep> Steps { get; set; } = new List<Common_flowstep>();
|
||||
}
|
||||
|
||||
/// <summary>金额重算记录列表项(旧 getRegetJERd 投影)。</summary>
|
||||
public class SHRoleRegetJERdItem
|
||||
{
|
||||
public string teaname { get; set; }
|
||||
public string writor { get; set; }
|
||||
public int SerID { get; set; }
|
||||
public int? DcoPrintID { get; set; }
|
||||
public string DocTitle { get; set; }
|
||||
public string DocContent { get; set; }
|
||||
public string DocInfo { get; set; }
|
||||
public string EditDatetime { get; set; }
|
||||
public string datestr { get; set; }
|
||||
public string type { get; set; }
|
||||
public string typename { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public string UnitPrice { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>editDocPrint 入参(旧 form:isedit + data + docdata + docnumdata)。</summary>
|
||||
public class WYSHRoleEditDocPrintDto
|
||||
{
|
||||
public int isedit { get; set; }
|
||||
public WYFlowForUser data { get; set; }
|
||||
public WYDocPrintSaveDto docdata { get; set; }
|
||||
public List<WY_DocPrint_Num> docnumdata { get; set; } = new List<WY_DocPrint_Num>();
|
||||
}
|
||||
|
||||
/// <summary>editSHTea 入参(旧 form:SHRoleID + listSHTeaTxt)。</summary>
|
||||
public class EditSHTeaDto
|
||||
{
|
||||
public decimal SHRoleID { get; set; }
|
||||
public List<WY_SHRole_SHTea> listSHTea { get; set; } = new List<WY_SHRole_SHTea>();
|
||||
}
|
||||
|
||||
/// <summary>editBSHTea 入参(旧 form:SHRoleID + listBSHTeaTxt)。</summary>
|
||||
public class EditBSHTeaDto
|
||||
{
|
||||
public decimal SHRoleID { get; set; }
|
||||
public List<WY_SHRole_BSHTea> listBSHTea { get; set; } = new List<WY_SHRole_BSHTea>();
|
||||
}
|
||||
|
||||
/// <summary>delSHRole 入参(旧 form:listSHRoleIDTxt)。</summary>
|
||||
public class DelSHRoleDto
|
||||
{
|
||||
public List<decimal?> listSHRoleID { get; set; } = new List<decimal?>();
|
||||
}
|
||||
|
||||
/// <summary>SaveSignature 入参(旧 form:pic)。</summary>
|
||||
public class SaveSignatureDto
|
||||
{
|
||||
public string pic { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>TuiHui 入参(旧 form:DocID + cont)。</summary>
|
||||
public class TuiHuiDto
|
||||
{
|
||||
public decimal DocID { get; set; }
|
||||
public string cont { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>regetJE 入参(旧 form:DocID 逗号串 + type)。</summary>
|
||||
public class RegetJEDto
|
||||
{
|
||||
public string DocID { get; set; }
|
||||
public string type { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace YhCore.Model.Vo
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页返回对象(对应 Java 版 PageVO,字段与 Spring Page 序列化结构保持一致,
|
||||
/// 保证前端零改动切换)。
|
||||
/// </summary>
|
||||
public class PageVO<T>
|
||||
{
|
||||
/// <summary>当前页数据</summary>
|
||||
public List<T> Content { get; set; } = new List<T>();
|
||||
|
||||
/// <summary>总记录数</summary>
|
||||
public long TotalElements { get; set; }
|
||||
|
||||
/// <summary>总页数</summary>
|
||||
public int TotalPages { get; set; }
|
||||
|
||||
/// <summary>当前页码(从 0 开始,与 Spring Pageable 一致)</summary>
|
||||
public int Number { get; set; }
|
||||
|
||||
/// <summary>每页大小</summary>
|
||||
public int Size { get; set; }
|
||||
|
||||
public static PageVO<T> Of(List<T> content, long totalElements, int number, int size)
|
||||
{
|
||||
return new PageVO<T>
|
||||
{
|
||||
Content = content ?? new List<T>(),
|
||||
TotalElements = totalElements,
|
||||
TotalPages = size > 0 ? (int)((totalElements + size - 1) / size) : 0,
|
||||
Number = number,
|
||||
Size = size
|
||||
};
|
||||
}
|
||||
|
||||
public static PageVO<T> Empty(int number = 0, int size = 20) => Of(new List<T>(), 0, number, size);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<RootNamespace>YhCore.Model</RootNamespace>
|
||||
<AssemblyName>YhCore.Model</AssemblyName>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- 审核角色 DTO 直接引用实体(WY_SHRole_SHTea / Common_flowstep / WY_DocPrint_Num 等) -->
|
||||
<ProjectReference Include="..\YhCore.Entity\YhCore.Entity.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Dapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace YhCore.Repository.Dapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Dapper 通道(对应 Java 显式引入 JdbcTemplate 的场景,如 backupAllSnapshot 批量快照插入)。
|
||||
/// 与 EF Core 共用同一连接(YHDbContext.Database.GetDbConnection),连接生命周期由 DbContext 管理。
|
||||
/// </summary>
|
||||
public class DapperChannel
|
||||
{
|
||||
private readonly YHDbContext _context;
|
||||
|
||||
public DapperChannel(YHDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
private IDbConnection Connection => _context.Database.GetDbConnection();
|
||||
|
||||
/// <summary>执行非查询 SQL(增删改),返回受影响行数</summary>
|
||||
public async Task<int> ExecuteAsync(string sql, object param = null)
|
||||
{
|
||||
return await Connection.ExecuteAsync(sql, param);
|
||||
}
|
||||
|
||||
/// <summary>查询单值(如 SELECT COUNT(*))</summary>
|
||||
public async Task<T> ExecuteScalarAsync<T>(string sql, object param = null)
|
||||
{
|
||||
return await Connection.ExecuteScalarAsync<T>(sql, param);
|
||||
}
|
||||
|
||||
/// <summary>查询列表</summary>
|
||||
public async Task<IEnumerable<T>> QueryAsync<T>(string sql, object param = null)
|
||||
{
|
||||
return await Connection.QueryAsync<T>(sql, param);
|
||||
}
|
||||
|
||||
/// <summary>查询单条</summary>
|
||||
public async Task<T> QueryFirstOrDefaultAsync<T>(string sql, object param = null)
|
||||
{
|
||||
return await Connection.QueryFirstOrDefaultAsync<T>(sql, param);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量快照插入(对应 Java backupAllSnapshot 的批量插入场景)。
|
||||
/// 逐条循环 Insert 的通用实现;数据量大时可替换为 SqlBulkCopy / 表值参数提升性能。
|
||||
/// </summary>
|
||||
public async Task<int> BulkInsertAsync<T>(string table, IEnumerable<T> rows)
|
||||
{
|
||||
var affected = 0;
|
||||
foreach (var row in rows)
|
||||
{
|
||||
affected += await Connection.ExecuteAsync(
|
||||
$"INSERT INTO {table} ({string.Join(",", GetColumns(row))}) " +
|
||||
$"VALUES ({string.Join(",", GetColumns(row).Select(c => "@" + c))})", row);
|
||||
}
|
||||
|
||||
return affected;
|
||||
}
|
||||
|
||||
private static string[] GetColumns<T>(T row)
|
||||
{
|
||||
var props = row?.GetType().GetProperties() ?? System.Array.Empty<System.Reflection.PropertyInfo>();
|
||||
return props.Select(p => p.Name).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace YhCore.Repository
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据库自动补表初始化器:对比 EF Core 实体模型与数据库实际表,
|
||||
/// 把「实体里有而数据库没有」的表自动创建(仅补建缺失表,不触碰已存在表;
|
||||
/// 已存在表的结构变更仍走 sqlscript 脚本,与既有约定一致)。
|
||||
/// 免除新增实体后手写建表脚本。
|
||||
/// </summary>
|
||||
public static class DatabaseInitializer
|
||||
{
|
||||
/// <summary>
|
||||
/// 补建缺失表。返回本次创建的表名列表(空 = 无缺失表)。
|
||||
/// 仅支持 SQL Server(对象存在性守卫/脚本语法为 SQL Server 专用);
|
||||
/// 其他 Provider(如 MySQL)跳过不动作,避免破坏其脚本语法。
|
||||
/// </summary>
|
||||
public static List<string> EnsureMissingTables(YHDbContext context)
|
||||
{
|
||||
if (!context.Database.IsSqlServer())
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
// 提前捕获连接串:此后不依赖 EF 托管连接(GenerateCreateScript 等内部操作
|
||||
// 可能干扰 EF 连接状态),DDL 用独立 SqlConnection 执行
|
||||
var connectionString = context.Database.GetDbConnection().ConnectionString;
|
||||
|
||||
// 1. 实体模型期望的表(Keyless 实体不建表,与 EnsureCreated 语义一致)
|
||||
var expected = context.Model.GetEntityTypes()
|
||||
.Where(e => e.FindPrimaryKey() != null)
|
||||
.Select(e => e.GetTableName())
|
||||
.Where(n => !string.IsNullOrWhiteSpace(n))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (expected.Count == 0)
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
// 2. 数据库不存在时:EnsureCreated 一次创建库 + 全部表
|
||||
if (!context.Database.CanConnect())
|
||||
{
|
||||
if (context.Database.EnsureCreated())
|
||||
{
|
||||
return expected.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 数据库实际存在的表
|
||||
var existing = GetExistingTables(context);
|
||||
|
||||
// 4. 差集:缺失的表
|
||||
var missing = expected.Except(existing, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
if (missing.Count == 0)
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
// 5. 从 EF 生成的建库脚本中提取缺失表的 DDL 并执行
|
||||
return ExecuteMissingTableScripts(context, missing, connectionString);
|
||||
}
|
||||
|
||||
private static HashSet<string> GetExistingTables(YHDbContext context)
|
||||
{
|
||||
var tables = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var conn = context.Database.GetDbConnection();
|
||||
var wasOpen = conn.State == ConnectionState.Open;
|
||||
if (!wasOpen)
|
||||
{
|
||||
conn.Open();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText =
|
||||
"SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'";
|
||||
using var reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
tables.Add(reader.GetString(0));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 只关闭不释放:连接由 EF 托管,外部释放会破坏其内部状态
|
||||
if (!wasOpen)
|
||||
{
|
||||
conn.Close();
|
||||
}
|
||||
}
|
||||
|
||||
return tables;
|
||||
}
|
||||
|
||||
private static List<string> ExecuteMissingTableScripts(
|
||||
YHDbContext context, List<string> missing, string connectionString)
|
||||
{
|
||||
var missingSet = missing.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var script = context.Database.GenerateCreateScript();
|
||||
|
||||
// EF 生成的脚本以独立 GO 行为语句分隔;无 GO 时整体视为一段
|
||||
var segments = Regex.Split(script, @"(?m)^\s*GO\s*$", RegexOptions.IgnoreCase)
|
||||
.Select(s => s.Trim())
|
||||
.Where(s => s.Length > 0);
|
||||
|
||||
var statements = new List<string>();
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
var tableMatch = Regex.Match(segment,
|
||||
@"^\s*CREATE\s+TABLE\s+(?:\[([^\]]+)\]\.)?\[([^\]]+)\]",
|
||||
RegexOptions.IgnoreCase);
|
||||
if (tableMatch.Success)
|
||||
{
|
||||
var schema = tableMatch.Groups[1].Success ? tableMatch.Groups[1].Value : "dbo";
|
||||
var name = tableMatch.Groups[2].Value;
|
||||
if (missingSet.Contains(name))
|
||||
{
|
||||
// 守卫包裹:竞态/重复执行安全(与迁移幂等脚本语义一致)
|
||||
statements.Add(
|
||||
$"IF OBJECT_ID(N'[{schema}].[{name}]', N'U') IS NULL\nBEGIN\n{segment}\nEND;");
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var indexMatch = Regex.Match(segment,
|
||||
@"^\s*CREATE\s+(?:UNIQUE\s+)?INDEX\s+\[([^\]]+)\]\s+ON\s+(?:\[([^\]]+)\]\.)?\[([^\]]+)\]",
|
||||
RegexOptions.IgnoreCase);
|
||||
if (indexMatch.Success)
|
||||
{
|
||||
var indexName = indexMatch.Groups[1].Value;
|
||||
var schema = indexMatch.Groups[2].Success ? indexMatch.Groups[2].Value : "dbo";
|
||||
var tableName = indexMatch.Groups[3].Value;
|
||||
if (missingSet.Contains(tableName))
|
||||
{
|
||||
statements.Add(
|
||||
$"IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'{indexName}' AND object_id = OBJECT_ID(N'[{schema}].[{tableName}]'))\nBEGIN\n{segment}\nEND;");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (statements.Count == 0)
|
||||
{
|
||||
// 模型表与脚本段未匹配上(理论上不发生):保守起见不执行任何 DDL
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
// 用独立 SqlConnection 执行 DDL(不依赖 EF 托管连接,避免其状态干扰)
|
||||
using var conn = new SqlConnection(connectionString);
|
||||
conn.Open();
|
||||
|
||||
using (var tx = conn.BeginTransaction())
|
||||
{
|
||||
foreach (var sql in statements)
|
||||
{
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.Transaction = tx;
|
||||
cmd.CommandText = sql;
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
tx.Commit();
|
||||
}
|
||||
|
||||
return missing.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using YhCore.Repository.Dapper;
|
||||
|
||||
namespace YhCore.Repository.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据层注册扩展。按配置 DatabaseProvider 选择数据库提供方:
|
||||
/// "SqlServer"(默认,主库)或 "MySql"(可切换)。
|
||||
/// </summary>
|
||||
public static class DataLayerServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddDataLayer(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var provider = configuration["DatabaseProvider"];
|
||||
|
||||
services.AddDbContext<YHDbContext>(options =>
|
||||
{
|
||||
if (string.Equals(provider, "MySql", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// MySQL 切换通道(对应 Java 版 application.yml 的 MySQL 数据源)
|
||||
var conn = configuration.GetConnectionString("Mysql");
|
||||
options.UseMySql(conn, ServerVersion.AutoDetect(conn));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 默认主库:SQL Server(EduCloudDocPrint)
|
||||
options.UseSqlServer(configuration.GetConnectionString("SqlServer"));
|
||||
}
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量注册泛型仓储与 Dapper 通道(对应 Java Repository 包由 Spring 自动装配)。
|
||||
/// 注入方式:IRepository<WY_DocPrint> / DapperChannel。
|
||||
/// </summary>
|
||||
public static IServiceCollection AddRepositories(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped(typeof(IRepository<>), typeof(RepositoryBase<>));
|
||||
services.AddScoped<DapperChannel>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
using YhCore.Model.Vo;
|
||||
|
||||
namespace YhCore.Repository
|
||||
{
|
||||
/// <summary>
|
||||
/// 泛型仓储接口(对应 Java Spring Data JPA Repository)。
|
||||
/// </summary>
|
||||
public interface IRepository<T> where T : class
|
||||
{
|
||||
// ---- 读取 ----
|
||||
Task<T> GetByIdAsync(object id);
|
||||
Task<List<T>> GetAllAsync();
|
||||
Task<List<T>> FindAsync(Expression<Func<T, bool>> predicate);
|
||||
Task<T> FirstOrDefaultAsync(Expression<Func<T, bool>> predicate);
|
||||
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);
|
||||
|
||||
// ---- 写入 ----
|
||||
Task<T> AddAsync(T entity);
|
||||
Task AddRangeAsync(IEnumerable<T> entities);
|
||||
Task UpdateAsync(T entity);
|
||||
Task RemoveAsync(T entity);
|
||||
Task RemoveAsync(object id);
|
||||
Task<int> SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using YhCore.Model.Vo;
|
||||
|
||||
namespace YhCore.Repository
|
||||
{
|
||||
/// <summary>
|
||||
/// 泛型仓储基类(对应 Java Spring Data JPA 的 SimpleJpaRepository)。
|
||||
/// 基于 EF Core IQueryable,分页查询下推到数据库执行。
|
||||
/// </summary>
|
||||
public class RepositoryBase<T> : IRepository<T> where T : class
|
||||
{
|
||||
protected readonly YHDbContext Context;
|
||||
protected readonly DbSet<T> DbSet;
|
||||
|
||||
public RepositoryBase(YHDbContext context)
|
||||
{
|
||||
Context = context;
|
||||
DbSet = context.Set<T>();
|
||||
}
|
||||
|
||||
public virtual async Task<T> GetByIdAsync(object id)
|
||||
{
|
||||
return await DbSet.FindAsync(id);
|
||||
}
|
||||
|
||||
public virtual async Task<List<T>> GetAllAsync()
|
||||
{
|
||||
return await DbSet.AsNoTracking().ToListAsync();
|
||||
}
|
||||
|
||||
public virtual async Task<List<T>> FindAsync(Expression<Func<T, bool>> predicate)
|
||||
{
|
||||
return await DbSet.AsNoTracking().Where(predicate).ToListAsync();
|
||||
}
|
||||
|
||||
public virtual async Task<T> FirstOrDefaultAsync(Expression<Func<T, bool>> predicate)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
public virtual async Task<long> CountAsync(Expression<Func<T, bool>> predicate = null)
|
||||
{
|
||||
return predicate == null
|
||||
? await DbSet.LongCountAsync()
|
||||
: await DbSet.LongCountAsync(predicate);
|
||||
}
|
||||
|
||||
public virtual async Task<PageVO<T>> PageAsync(
|
||||
Expression<Func<T, bool>> predicate,
|
||||
int page, int size,
|
||||
Expression<Func<T, object>> orderBy = null,
|
||||
bool asc = true)
|
||||
{
|
||||
if (page < 0) page = 0;
|
||||
if (size <= 0) size = 20;
|
||||
|
||||
IQueryable<T> query = DbSet.AsNoTracking();
|
||||
if (predicate != null)
|
||||
{
|
||||
query = query.Where(predicate);
|
||||
}
|
||||
|
||||
var total = await query.LongCountAsync();
|
||||
|
||||
IOrderedQueryable<T> ordered;
|
||||
if (orderBy != null)
|
||||
{
|
||||
ordered = asc ? query.OrderBy(orderBy) : query.OrderByDescending(orderBy);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 无显式排序时按主键约定排序(Id → SerID),保证 Skip/Take 稳定
|
||||
var keyName = typeof(T).GetProperty("Id") != null ? "Id" : "SerID";
|
||||
ordered = query.OrderBy(e => EF.Property<object>(e, keyName));
|
||||
}
|
||||
|
||||
var items = await ordered.Skip(page * size).Take(size).ToListAsync();
|
||||
return PageVO<T>.Of(items, total, page, size);
|
||||
}
|
||||
|
||||
public virtual async Task<T> AddAsync(T entity)
|
||||
{
|
||||
var entry = await DbSet.AddAsync(entity);
|
||||
return entry.Entity;
|
||||
}
|
||||
|
||||
public virtual async Task AddRangeAsync(IEnumerable<T> entities)
|
||||
{
|
||||
await DbSet.AddRangeAsync(entities);
|
||||
}
|
||||
|
||||
public virtual Task UpdateAsync(T entity)
|
||||
{
|
||||
DbSet.Update(entity);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual Task RemoveAsync(T entity)
|
||||
{
|
||||
DbSet.Remove(entity);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual async Task RemoveAsync(object id)
|
||||
{
|
||||
var entity = await GetByIdAsync(id);
|
||||
if (entity != null)
|
||||
{
|
||||
DbSet.Remove(entity);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task<int> SaveChangesAsync()
|
||||
{
|
||||
return await Context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace YhCore.Repository.Specification
|
||||
{
|
||||
/// <summary>
|
||||
/// 动态条件表达式组合器(对应 Java Repository/specification 包的轻量实现)。
|
||||
/// 用于按需拼装 where 条件:
|
||||
/// <code>
|
||||
/// var spec = Specification.True<SchClass>()
|
||||
/// .And(c => c.SchoolId == schId)
|
||||
/// .AndIf(!string.IsNullOrEmpty(name), c => c.Name.Contains(name));
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public static class Specification
|
||||
{
|
||||
public static Expression<Func<T, bool>> True<T>() => _ => true;
|
||||
|
||||
public static Expression<Func<T, bool>> False<T>() => _ => false;
|
||||
|
||||
public static Expression<Func<T, bool>> And<T>(
|
||||
this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
|
||||
{
|
||||
if (left == null) return right;
|
||||
if (right == null) return left;
|
||||
|
||||
var body = Expression.AndAlso(left.Body, right.Body.ReplaceParameter(right.Parameters[0], left.Parameters[0]));
|
||||
return Expression.Lambda<Func<T, bool>>(body, left.Parameters);
|
||||
}
|
||||
|
||||
public static Expression<Func<T, bool>> Or<T>(
|
||||
this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
|
||||
{
|
||||
if (left == null) return right;
|
||||
if (right == null) return left;
|
||||
|
||||
var body = Expression.OrElse(left.Body, right.Body.ReplaceParameter(right.Parameters[0], left.Parameters[0]));
|
||||
return Expression.Lambda<Func<T, bool>>(body, left.Parameters);
|
||||
}
|
||||
|
||||
public static Expression<Func<T, bool>> Not<T>(this Expression<Func<T, bool>> expr)
|
||||
{
|
||||
if (expr == null) return null;
|
||||
return Expression.Lambda<Func<T, bool>>(Expression.Not(expr.Body), expr.Parameters);
|
||||
}
|
||||
|
||||
/// <summary>条件成立时才追加条件(Java Specification 常用写法)</summary>
|
||||
public static Expression<Func<T, bool>> AndIf<T>(
|
||||
this Expression<Func<T, bool>> spec, bool condition, Expression<Func<T, bool>> conditionExpr)
|
||||
{
|
||||
return condition ? spec.And(conditionExpr) : spec;
|
||||
}
|
||||
|
||||
/// <summary>条件不成立时才追加条件</summary>
|
||||
public static Expression<Func<T, bool>> AndIfNot<T>(
|
||||
this Expression<Func<T, bool>> spec, bool condition, Expression<Func<T, bool>> conditionExpr)
|
||||
{
|
||||
return spec.AndIf(!condition, conditionExpr);
|
||||
}
|
||||
|
||||
private static Expression ReplaceParameter(this Expression expression, ParameterExpression source, ParameterExpression target)
|
||||
{
|
||||
return new ParameterReplacer(source, target).Visit(expression);
|
||||
}
|
||||
|
||||
private class ParameterReplacer : ExpressionVisitor
|
||||
{
|
||||
private readonly ParameterExpression _source;
|
||||
private readonly ParameterExpression _target;
|
||||
|
||||
public ParameterReplacer(ParameterExpression source, ParameterExpression target)
|
||||
{
|
||||
_source = source;
|
||||
_target = target;
|
||||
}
|
||||
|
||||
protected override Expression VisitParameter(ParameterExpression node)
|
||||
{
|
||||
return node == _source ? _target : base.VisitParameter(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using YhCore.Entity;
|
||||
|
||||
namespace YhCore.Repository
|
||||
{
|
||||
/// <summary>
|
||||
/// EF Core 数据上下文。同时支持 SQL Server(主库)与 MySQL(可切换),
|
||||
/// 提供方在 Startup 通过 AddDataLayer 按 DatabaseProvider 配置选择。
|
||||
/// 生产环境禁用自动迁移,结构变更走 sqlscript 脚本。
|
||||
/// </summary>
|
||||
public class YHDbContext : DbContext
|
||||
{
|
||||
public YHDbContext(DbContextOptions<YHDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
// 文印业务
|
||||
public DbSet<WY_DocPrint> WY_DocPrint { get; set; }
|
||||
public DbSet<WY_DocPrint_Num> WY_DocPrint_Num { get; set; }
|
||||
public DbSet<WY_DocPrint_SH> WY_DocPrint_SH { get; set; }
|
||||
public DbSet<WY_reGetJERd> WY_reGetJERd { get; set; }
|
||||
public DbSet<WY_SchZyBit> WY_SchZyBit { get; set; }
|
||||
|
||||
// 审批流
|
||||
public DbSet<Common_flowinfo> Common_flowinfo { get; set; }
|
||||
public DbSet<Common_flowstep> Common_flowstep { get; set; }
|
||||
public DbSet<Common_flowtranctInfo> Common_flowtranctInfo { get; set; }
|
||||
public DbSet<Common_flowuser> Common_flowuser { get; set; }
|
||||
public DbSet<Common_freeflowinfo> Common_freeflowinfo { get; set; }
|
||||
public DbSet<Common_freeGroupuser> Common_freeGroupuser { get; set; }
|
||||
public DbSet<Common_Groupuser> Common_Groupuser { get; set; }
|
||||
public DbSet<Common_Groupuser_SHRole> Common_Groupuser_SHRole { get; set; }
|
||||
public DbSet<Common_SendUserinfo> Common_SendUserinfo { get; set; }
|
||||
public DbSet<Common_UserFlowZhipai> Common_UserFlowZhipai { get; set; }
|
||||
|
||||
// 审核角色
|
||||
public DbSet<WY_SHRole> WY_SHRole { get; set; }
|
||||
public DbSet<WY_SHRole_BSHTea> WY_SHRole_BSHTea { get; set; }
|
||||
public DbSet<WY_SHRole_SHTea> WY_SHRole_SHTea { get; set; }
|
||||
|
||||
// 基础数据
|
||||
public DbSet<Common_Dictionary> Common_Dictionary { get; set; }
|
||||
public DbSet<Common_DictionaryOrd> Common_DictionaryOrd { get; set; }
|
||||
public DbSet<Common_UpFiles> Common_UpFiles { get; set; }
|
||||
|
||||
// 日志
|
||||
public DbSet<A_DataLog> A_DataLog { get; set; }
|
||||
public DbSet<Base_Log> Base_Log { get; set; }
|
||||
public DbSet<AppMsgRecord> AppMsgRecord { get; set; }
|
||||
// 系统审计日志(HTTP 接口级,对齐 Java sys_operation_log,表名在 OnModelCreating 映射)
|
||||
public DbSet<SysOperationLog> SysOperationLog { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// 主键约定:实体为旧 EF6 EDMX 生成的纯 POCO(无 [Key] 标注),
|
||||
// 统一按属性名推断主键——优先 Id(不区分大小写,兼容小写 id),其次 SerID;
|
||||
// 两者皆无的实体按 Keyless 处理(仅用于查询)。
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
if (entityType.FindPrimaryKey() != null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var clrType = entityType.ClrType;
|
||||
const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase;
|
||||
var keyProperty = clrType.GetProperty("Id", flags)
|
||||
?? clrType.GetProperty("SerID", flags);
|
||||
|
||||
if (keyProperty != null)
|
||||
{
|
||||
modelBuilder.Entity(clrType).HasKey(keyProperty.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
modelBuilder.Entity(clrType).HasNoKey();
|
||||
}
|
||||
}
|
||||
|
||||
// decimal 主键(多数表的 SerID)EF Core 约定不自增;
|
||||
// 若数据库对应列为 IDENTITY,需在此补充 ValueGeneratedOnAdd 配置。
|
||||
// WY_DocPrint.SerID:旧 EF6 依赖数据库 IDENTITY 回填新增单据号,此处显式声明。
|
||||
// WY_SHRole* / WY_DocPrint_SH / WY_DocPrint_Num / WY_reGetJERd / Common_Dictionary
|
||||
// 同属旧库 IDENTITY 列(旧代码新增时均不显式赋值 SerID),一并补充。
|
||||
// ⚠️ decimal 默认映射 decimal(18,2),scale≠0 的 IDENTITY 列 SQL Server 不允许
|
||||
//(Msg 5720),必须显式声明 decimal(18,0)——否则自动补表整批失败回滚。
|
||||
modelBuilder.Entity<WY_DocPrint>().Property(e => e.SerID).HasColumnType("decimal(18,0)").ValueGeneratedOnAdd();
|
||||
modelBuilder.Entity<WY_SHRole>().Property(e => e.SerID).HasColumnType("decimal(18,0)").ValueGeneratedOnAdd();
|
||||
modelBuilder.Entity<WY_SHRole_SHTea>().Property(e => e.SerID).HasColumnType("decimal(18,0)").ValueGeneratedOnAdd();
|
||||
modelBuilder.Entity<WY_SHRole_BSHTea>().Property(e => e.SerID).HasColumnType("decimal(18,0)").ValueGeneratedOnAdd();
|
||||
modelBuilder.Entity<WY_DocPrint_SH>().Property(e => e.SerID).HasColumnType("decimal(18,0)").ValueGeneratedOnAdd();
|
||||
modelBuilder.Entity<WY_DocPrint_Num>().Property(e => e.SerID).HasColumnType("decimal(18,0)").ValueGeneratedOnAdd();
|
||||
modelBuilder.Entity<WY_reGetJERd>().Property(e => e.SerID).HasColumnType("decimal(18,0)").ValueGeneratedOnAdd();
|
||||
modelBuilder.Entity<Common_Dictionary>().Property(e => e.SerID).HasColumnType("decimal(18,0)").ValueGeneratedOnAdd();
|
||||
|
||||
// 系统审计日志表名映射:sys_operation_log(与 Java bschclassmanage 同表结构,双轨对齐)
|
||||
modelBuilder.Entity<SysOperationLog>().ToTable("sys_operation_log");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<RootNamespace>YhCore.Repository</RootNamespace>
|
||||
<AssemblyName>YhCore.Repository</AssemblyName>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- 主库:SQL Server -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.17" />
|
||||
<!-- 可切换:MySQL(Pomelo,对齐 EF Core 5) -->
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="5.0.4" />
|
||||
<!-- Dapper 通道:复杂报表/批量快照插入(对应 Java JdbcTemplate 场景) -->
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\YhCore.Entity\YhCore.Entity.csproj" />
|
||||
<ProjectReference Include="..\YhCore.Model\YhCore.Model.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Yh.Platform.Abstractions;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Repository;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.Service.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// 公共字典服务实现(移植旧 BestSchool.Service.GetSysDict)。
|
||||
/// </summary>
|
||||
public class DictionaryService : IDictionaryService
|
||||
{
|
||||
private readonly IRepository<Common_Dictionary> _dic;
|
||||
|
||||
public DictionaryService(IRepository<Common_Dictionary> dic)
|
||||
{
|
||||
_dic = dic;
|
||||
}
|
||||
|
||||
public async Task<List<DicOption>> GetDictListAsync(string typeId, string schId)
|
||||
{
|
||||
var has = await _dic.AnyAsync(d => d.typeID == typeId);
|
||||
if (!has)
|
||||
{
|
||||
throw new DException("尚未添加发文部门,请联系管理员添加!");
|
||||
}
|
||||
|
||||
var list = await _dic.FindAsync(d =>
|
||||
d.typeID == typeId &&
|
||||
(string.IsNullOrEmpty(schId) || d.Schserid == schId) &&
|
||||
d.Enable == 1);
|
||||
|
||||
return list
|
||||
.OrderBy(d => DictionaryTypeHelper.ParseSort(d.Des))
|
||||
.Select(d => new DicOption { K = d.CodeID, V = d.Name })
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public Task<List<DicOption>> GetDicTypeListAsync()
|
||||
{
|
||||
var list = DictionaryTypeHelper.AllTypes
|
||||
.Where(t => t.Id != "DEPT")
|
||||
.Select(t => new DicOption { K = t.Id, V = t.Name })
|
||||
.ToList();
|
||||
return Task.FromResult(list);
|
||||
}
|
||||
|
||||
public async Task<List<DicManageItem>> GetDictMangeListAsync(string typeId, string schId)
|
||||
{
|
||||
var has = await _dic.AnyAsync(d => d.typeID == typeId);
|
||||
if (!has)
|
||||
{
|
||||
throw new DException("字典类型错误,未找到该字典类型");
|
||||
}
|
||||
|
||||
var list = await _dic.FindAsync(d =>
|
||||
d.typeID == typeId &&
|
||||
(string.IsNullOrEmpty(schId) || d.Schserid == schId));
|
||||
|
||||
return list
|
||||
.OrderBy(d => string.IsNullOrEmpty(d.Des) ? 999 : DictionaryTypeHelper.ParseSort(d.Des))
|
||||
.Select(d => new DicManageItem
|
||||
{
|
||||
SerID = d.SerID,
|
||||
CodeID = d.CodeID,
|
||||
Name = d.Name,
|
||||
Des = d.Des,
|
||||
Enable = d.Enable
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task AddDicAsync(Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
if (string.IsNullOrWhiteSpace(model.Name)) throw new DException("输入字符串格式不正确!");
|
||||
|
||||
var listed = await _dic.AnyAsync(b =>
|
||||
b.typeID == model.typeID &&
|
||||
(b.Schserid == model.Schserid || b.Schserid == null || b.Schserid == "") &&
|
||||
b.Name == model.Name && b.Enable != 3);
|
||||
|
||||
if (listed || await IsDicnameAsync(model.typeID, model.Name))
|
||||
{
|
||||
throw new DException("已存在该分类,请重新填写!");
|
||||
}
|
||||
|
||||
var candidates = await _dic.FindAsync(x =>
|
||||
x.typeID == model.typeID && x.Enable != 3 &&
|
||||
(x.Schserid == model.Schserid || x.Schserid == null || x.Schserid == ""));
|
||||
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
model.Sort = 1;
|
||||
model.Des = "1";
|
||||
}
|
||||
else
|
||||
{
|
||||
var maxSort = candidates.Max(c => c.Sort ?? (int)c.SerID);
|
||||
model.Sort = maxSort + 1;
|
||||
model.Des = model.Sort.ToString();
|
||||
}
|
||||
|
||||
model.Enable = 1;
|
||||
model.CodeID = model.typeID + model.Sort.ToString();
|
||||
|
||||
await _dic.AddAsync(model);
|
||||
await _dic.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateDicAsync(Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
|
||||
var dup = await _dic.AnyAsync(m =>
|
||||
m.SerID != model.SerID && m.Name == model.Name &&
|
||||
(m.Schserid == model.Schserid || m.Schserid == null) && m.Enable != 3);
|
||||
|
||||
if (dup || await IsDicnameAsync(model.typeID, model.Name))
|
||||
{
|
||||
throw new DException("已存在该分类,请重新填写!");
|
||||
}
|
||||
|
||||
var obj = await _dic.FirstOrDefaultTrackedAsync(x => x.SerID == model.SerID);
|
||||
if (obj == null) throw new DException("未找到要更新的分类!");
|
||||
|
||||
obj.Name = model.Name;
|
||||
obj.Des = model.Des;
|
||||
obj.Sort = model.Sort;
|
||||
obj.Enable = model.Enable;
|
||||
|
||||
await _dic.UpdateAsync(obj);
|
||||
await _dic.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task EditDicAsync(Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
|
||||
var current = await _dic.FirstOrDefaultTrackedAsync(m => m.SerID == model.SerID);
|
||||
if (current == null) throw new DException("未找到要编辑的分类!");
|
||||
|
||||
var dup = await _dic.AnyAsync(m =>
|
||||
m.SerID != model.SerID && m.Enable != 3 && m.typeID == current.typeID &&
|
||||
m.Name == model.Name && (m.Schserid == model.Schserid || m.Schserid == null));
|
||||
|
||||
if (dup)
|
||||
{
|
||||
throw new DException("已存在该分类,请重新填写!");
|
||||
}
|
||||
|
||||
current.Name = model.Name;
|
||||
current.Code = model.Code;
|
||||
current.Path = model.Path;
|
||||
|
||||
await _dic.UpdateAsync(current);
|
||||
await _dic.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <summary>判断全局(Schserid 为空)是否已存在同名类别(对齐旧 IsDicname)。</summary>
|
||||
private async Task<bool> IsDicnameAsync(string typeId, string dicName)
|
||||
{
|
||||
var names = await _dic.FindAsync(b => b.typeID == typeId && b.Schserid == null);
|
||||
return names.Select(a => a.Name).Distinct().Contains(dicName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Yh.Platform.Abstractions;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Repository;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.Service.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// 文印分类 / 印制版面 / 公共类别服务实现
|
||||
/// (移植旧 BestSchool.DocPrint.WY_list 分类部分 + WY_Classify)。
|
||||
/// </summary>
|
||||
public class DocPrintClassifyService : IDocPrintClassifyService
|
||||
{
|
||||
private readonly IRepository<Common_Dictionary> _dic;
|
||||
private readonly IRepository<Common_DictionaryOrd> _ord;
|
||||
private readonly IRepository<WY_DocPrint> _doc;
|
||||
private readonly IRepository<Common_Groupuser> _groupuser;
|
||||
private readonly IRepository<Common_freeGroupuser> _freegroup;
|
||||
private readonly IRepository<WY_SchZyBit> _zybit;
|
||||
|
||||
public DocPrintClassifyService(
|
||||
IRepository<Common_Dictionary> dic,
|
||||
IRepository<Common_DictionaryOrd> ord,
|
||||
IRepository<WY_DocPrint> doc,
|
||||
IRepository<Common_Groupuser> groupuser,
|
||||
IRepository<Common_freeGroupuser> freegroup,
|
||||
IRepository<WY_SchZyBit> zybit)
|
||||
{
|
||||
_dic = dic;
|
||||
_ord = ord;
|
||||
_doc = doc;
|
||||
_groupuser = groupuser;
|
||||
_freegroup = freegroup;
|
||||
_zybit = zybit;
|
||||
}
|
||||
|
||||
#region 文印分类 / 印制版面
|
||||
|
||||
public async Task AddDicAsync(Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
|
||||
var listed = await _dic.AnyAsync(b =>
|
||||
b.typeID == model.typeID && b.Name == model.Name && b.Schserid == model.Schserid);
|
||||
if (listed) throw new DException("该分类已经存在!");
|
||||
|
||||
var max = (await _dic.FindAsync(x => x.typeID == model.typeID))
|
||||
.OrderByDescending(x => x.SerID).FirstOrDefault();
|
||||
model.Sort = max != null ? (max.Sort ?? 0) + 1 : 1;
|
||||
model.Enable = 1;
|
||||
model.CodeID = model.typeID + model.Sort;
|
||||
model.typeName = "印制分类";
|
||||
|
||||
await _dic.AddAsync(model);
|
||||
await _dic.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task AddYZBMAsync(Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
|
||||
var listed = await _dic.AnyAsync(b =>
|
||||
b.typeID == model.typeID && b.Name == model.Name && b.Schserid == model.Schserid);
|
||||
if (listed) throw new DException("该版面已经存在!");
|
||||
|
||||
var max = (await _dic.FindAsync(x => x.typeID == model.typeID))
|
||||
.OrderByDescending(x => x.SerID).FirstOrDefault();
|
||||
model.Sort = max != null ? (max.Sort ?? 0) + 1 : 1;
|
||||
model.Enable = 1;
|
||||
model.CodeID = model.typeID + model.Sort;
|
||||
model.typeName = "印制版面";
|
||||
|
||||
await _dic.AddAsync(model);
|
||||
await _dic.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<List<CategoryItem>> CategoryListAsync(string schSerId, string name)
|
||||
=> await QueryClassifyListAsync("印制分类", schSerId, name);
|
||||
|
||||
public async Task<List<CategoryItem>> YZBMListAsync(string schSerId, string name)
|
||||
=> await QueryClassifyListAsync("印制版面", schSerId, name);
|
||||
|
||||
private async Task<List<CategoryItem>> QueryClassifyListAsync(string typeName, string schSerId, string name)
|
||||
{
|
||||
var query = await _dic.FindAsync(m => m.typeName == typeName);
|
||||
|
||||
if (!string.IsNullOrEmpty(schSerId))
|
||||
{
|
||||
query = query.Where(m => m.Schserid == schSerId || m.Schserid == null || m.Schserid == "").ToList();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
query = query.Where(m => m.Name != null && m.Name.IndexOf(name) >= 0).ToList();
|
||||
}
|
||||
|
||||
return query
|
||||
.OrderBy(x => string.IsNullOrEmpty(x.Des) ? 999 : DictionaryTypeHelper.ParseSort(x.Des))
|
||||
.Select(item => new CategoryItem
|
||||
{
|
||||
SerID = item.SerID,
|
||||
CodeID = item.CodeID,
|
||||
Name = item.Name,
|
||||
Des = item.Des,
|
||||
Enable = item.Enable,
|
||||
Sort = item.Sort,
|
||||
SchSerID = item.Schserid,
|
||||
UserSchSerID = schSerId,
|
||||
_disabled = string.IsNullOrEmpty(item.Schserid)
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task DeleteDicAsync(decimal[] ids)
|
||||
{
|
||||
if (ids == null || ids.Length == 0) throw new DException("ids is not null");
|
||||
|
||||
var items = await _dic.FindTrackedAsync(m => ids.Contains(m.SerID));
|
||||
if (items.Count == 0) throw new DException("未找到要删除的分类!");
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var used = await _doc.AnyAsync(m => m.DPType == item.CodeID);
|
||||
if (used) throw new DException("该类别下有使用记录无法删除!");
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
await _dic.RemoveAsync(item);
|
||||
}
|
||||
|
||||
await _dic.SaveChangesAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 公共类别
|
||||
|
||||
public async Task<List<DictionaryTypeItem>> DictionaryTypeListAsync(string schSerId, string name, bool showAll, string typeId)
|
||||
{
|
||||
var resultList = await _dic.FindAsync(m => m.typeID == typeId && m.Enable == 1);
|
||||
|
||||
if (!string.IsNullOrEmpty(schSerId))
|
||||
{
|
||||
resultList = resultList.Where(m => m.Schserid == schSerId || m.Schserid == null || m.Schserid == "").ToList();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
resultList = resultList.Where(m => m.Name != null && m.Name.IndexOf(name) >= 0).ToList();
|
||||
}
|
||||
|
||||
var ordList = await _ord.FindAsync(a => a.SchSerID == schSerId);
|
||||
|
||||
var items = resultList.Select(item => new DictionaryTypeItem
|
||||
{
|
||||
Code = item.Code,
|
||||
SerID = item.SerID,
|
||||
CodeID = item.CodeID,
|
||||
Name = item.Name,
|
||||
Des = item.Des,
|
||||
Enable = item.Enable,
|
||||
Sort = item.Sort,
|
||||
SchSerID = item.Schserid,
|
||||
UserSchSerID = schSerId,
|
||||
_disabled = string.IsNullOrEmpty(item.Schserid),
|
||||
Status = false,
|
||||
Open = ordList.FirstOrDefault(a => a.DicSerID == (int)item.SerID)?.Open ?? true,
|
||||
Ord = ordList.FirstOrDefault(a => a.DicSerID == (int)item.SerID)?.Ord
|
||||
?? (string.IsNullOrEmpty(item.Des) ? 999 : DictionaryTypeHelper.ParseSort(item.Des)),
|
||||
Path = item.Path
|
||||
})
|
||||
.OrderBy(a => a.Ord)
|
||||
.ToList();
|
||||
|
||||
if (!showAll)
|
||||
{
|
||||
items = items.Where(a => a.Open).ToList();
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
public async Task DeleteDictionaryTypeAsync(string[] ids)
|
||||
{
|
||||
if (ids == null || ids.Length == 0) throw new DException("ids is not null");
|
||||
|
||||
var items = await _dic.FindTrackedAsync(m => ids.Contains(m.SerID.ToString()));
|
||||
foreach (var item in items)
|
||||
{
|
||||
item.Enable = 3;
|
||||
}
|
||||
|
||||
await _dic.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task SetDictionaryOrdAsync(string schSerId, int dicSerId, bool show)
|
||||
{
|
||||
var obj = await _ord.FirstOrDefaultTrackedAsync(m => m.DicSerID == dicSerId && m.SchSerID == schSerId);
|
||||
if (obj == null)
|
||||
{
|
||||
await _ord.AddAsync(new Common_DictionaryOrd
|
||||
{
|
||||
DicSerID = dicSerId,
|
||||
Open = show,
|
||||
SchSerID = schSerId
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
obj.Open = show;
|
||||
await _ord.UpdateAsync(obj);
|
||||
}
|
||||
|
||||
await _ord.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task OrderDicAsync(string ids, string schSerId)
|
||||
{
|
||||
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.FindTrackedAsync(m => m.SchSerID == schSerId);
|
||||
|
||||
var i = 0;
|
||||
foreach (var id in ssids)
|
||||
{
|
||||
var obj = list.FirstOrDefault(a => a.DicSerID == id && a.SchSerID == schSerId);
|
||||
if (obj == null)
|
||||
{
|
||||
await _ord.AddAsync(new Common_DictionaryOrd
|
||||
{
|
||||
DicSerID = id,
|
||||
Open = true,
|
||||
SchSerID = schSerId,
|
||||
Ord = i
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
obj.Ord = i;
|
||||
await _ord.UpdateAsync(obj);
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
await _ord.SaveChangesAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 文印资源开关 / 结果链接 / 待审角标
|
||||
|
||||
/// <summary>
|
||||
/// 结果链接:旧实现调用资源平台 ziyuan 的 /SaveResource/Batch 生成分享链接,
|
||||
/// 新项目未接入资源平台,按「未配置 ziyuan」的旧分支降级返回 false。
|
||||
/// </summary>
|
||||
public Task<object> GetResultUrlAsync(int dataId, int uid, int schId, string schSerId)
|
||||
{
|
||||
return Task.FromResult<object>(false);
|
||||
}
|
||||
|
||||
public async Task<bool> GetSchZyBitAsync(string schSerId)
|
||||
{
|
||||
// 旧实现仅在 ziyuan 配置存在时返回库值、否则 false;新项目未接入 ziyuan,直接返回库内开关值。
|
||||
var bit = await _zybit.FirstOrDefaultAsync(a => a.SchSerID == schSerId);
|
||||
return bit?.ziyuanbit ?? true;
|
||||
}
|
||||
|
||||
public async Task SetSchZyBitAsync(string schSerId, bool zyBit)
|
||||
{
|
||||
var obj = await _zybit.FirstOrDefaultTrackedAsync(a => a.SchSerID == schSerId);
|
||||
if (obj == null)
|
||||
{
|
||||
await _zybit.AddAsync(new WY_SchZyBit { SchSerID = schSerId, ziyuanbit = zyBit });
|
||||
}
|
||||
else
|
||||
{
|
||||
obj.ziyuanbit = zyBit;
|
||||
await _zybit.UpdateAsync(obj);
|
||||
}
|
||||
|
||||
await _zybit.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<int> GetShnheCountAsync(string userId, string schSerId)
|
||||
{
|
||||
if (!int.TryParse(userId, out var userid))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var docs = await _doc.FindAsync(a => a.SchSerID == schSerId && a.DocStatus == "0");
|
||||
var fixedGroupIds = (await _groupuser.FindAsync(b => b.userid == userid))
|
||||
.Select(b => b.usergroup).Where(g => g.HasValue).Select(g => g.Value).ToList();
|
||||
var freeGroupIds = (await _freegroup.FindAsync(b => b.userid == userid))
|
||||
.Select(b => b.usergroup).Where(g => g.HasValue).Select(g => g.Value).ToList();
|
||||
|
||||
var result = new HashSet<decimal>();
|
||||
foreach (var a in docs)
|
||||
{
|
||||
bool match = false;
|
||||
|
||||
// 固定流程:DocStep 命中本人组
|
||||
if (a.flowid != null && a.zhipai == null && a.DocStep.HasValue && fixedGroupIds.Contains(a.DocStep.Value))
|
||||
{
|
||||
match = true;
|
||||
}
|
||||
|
||||
// 指派:zhipai 含本人
|
||||
if (!match && a.zhipai != null && a.flowid != null && a.zhipai.Contains(userId))
|
||||
{
|
||||
match = true;
|
||||
}
|
||||
|
||||
// 自由流程:freestepid 命中本人自由组
|
||||
if (!match && a.freestepid.HasValue && a.freestepid.Value > 0 && freeGroupIds.Contains(a.freestepid.Value))
|
||||
{
|
||||
match = true;
|
||||
}
|
||||
|
||||
if (match)
|
||||
{
|
||||
result.Add(a.SerID);
|
||||
}
|
||||
}
|
||||
|
||||
return result.Count;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Yh.SchClass.OpenApi;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Repository;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.Service.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// 文印统计看板服务(移植旧 BestSchool.DocPrint.DocPrintStatistics,4 接口,ExportRecord 延后)。
|
||||
/// 旧 [NoAuth] 无用户上下文,schId 由前端传入;新库 SchSerID 已是 int 字符串,直接 schId.ToString() 匹配,
|
||||
/// 不再依赖旧 GetSchSerID(int) 的 HTTP 转换。roleId 参数旧实现零引用,已删除。
|
||||
/// 旧 StatusArray = {"1","11"}(印刷中/已印刷)。
|
||||
/// </summary>
|
||||
public class DocPrintStatisticsService : IDocPrintStatisticsService
|
||||
{
|
||||
private static readonly string[] StatusArray = { "1", "11" };
|
||||
|
||||
private readonly IRepository<WY_DocPrint> _doc;
|
||||
private readonly IRepository<Common_Dictionary> _dic;
|
||||
private readonly SchClassOpenApi _sch;
|
||||
|
||||
public DocPrintStatisticsService(
|
||||
IRepository<WY_DocPrint> doc,
|
||||
IRepository<Common_Dictionary> dic,
|
||||
SchClassOpenApi sch)
|
||||
{
|
||||
_doc = doc;
|
||||
_dic = dic;
|
||||
_sch = sch;
|
||||
}
|
||||
|
||||
public async Task<List<DocPrintCountItem>> GetDocPrintCountAsync(int schId, DateTime? startDate = null, DateTime? endDate = null)
|
||||
{
|
||||
var schSerId = schId.ToString();
|
||||
|
||||
var list = (await _doc.FindAsync(a =>
|
||||
a.SchSerID == schSerId && StatusArray.Contains(a.DocStatus))).ToList();
|
||||
|
||||
if (startDate != null)
|
||||
list = list.Where(a => a.EditDate >= startDate).ToList();
|
||||
if (endDate != null)
|
||||
list = list.Where(a => a.EditDate <= endDate).ToList();
|
||||
|
||||
var count = list.Count;
|
||||
var xhzsl = list.Select(a => a.DocNum).DefaultIfEmpty(0).Sum();
|
||||
|
||||
var doctypeId = list.GroupBy(a => a.DPType)
|
||||
.Select(a => new { type = a.Key, sum = a.Sum(s => s.DocNum) })
|
||||
.OrderByDescending(a => a.sum).FirstOrDefault()?.type ?? "";
|
||||
|
||||
string yzfl = "";
|
||||
if (!string.IsNullOrEmpty(doctypeId))
|
||||
{
|
||||
yzfl = (await _dic.FindAsync(g =>
|
||||
g.typeID == "YZFL" && g.CodeID == doctypeId &&
|
||||
(g.Schserid == schSerId || g.Schserid == null)))
|
||||
.OrderBy(g => g.Enable).Select(g => g.Name).FirstOrDefault() ?? "";
|
||||
}
|
||||
|
||||
return new List<DocPrintCountItem>
|
||||
{
|
||||
new DocPrintCountItem { k = "文印数量(条)", v = count },
|
||||
new DocPrintCountItem { k = "消耗总数(张)", v = xhzsl },
|
||||
new DocPrintCountItem { k = "最多消耗类型", v = yzfl },
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<List<PieChartSeries>> GetPieChartAsync(int schId, DateTime? startDate = null, DateTime? endDate = null)
|
||||
{
|
||||
var schSerId = schId.ToString();
|
||||
|
||||
var list = (await _doc.FindAsync(a =>
|
||||
a.SchSerID == schSerId && StatusArray.Contains(a.DocStatus))).ToList();
|
||||
|
||||
if (startDate != null)
|
||||
list = list.Where(a => a.EditDate >= startDate).ToList();
|
||||
if (endDate != null)
|
||||
list = list.Where(a => a.EditDate <= endDate).ToList();
|
||||
|
||||
var yzfl = (await _dic.FindAsync(g =>
|
||||
g.typeID == "YZFL" && (g.Schserid == schSerId || g.Schserid == null)))
|
||||
.OrderBy(g => g.Enable).ToList();
|
||||
|
||||
var yslx = list.GroupBy(a => a.DPType)
|
||||
.Select(a => new { k = a.Key, v = a.Sum(s => s.DocNum) }).ToList()
|
||||
.Select(a => new PieChartKv { k = yzfl.FirstOrDefault(s => s.CodeID == a.k)?.Name, v = a.v }).ToList();
|
||||
|
||||
var ysxh = list.GroupBy(a => a.DPType)
|
||||
.Select(a => new { k = a.Key, v = a.Count() }).ToList()
|
||||
.Select(a => new PieChartKv { k = yzfl.FirstOrDefault(s => s.CodeID == a.k)?.Name, v = a.v }).ToList();
|
||||
|
||||
return new List<PieChartSeries>
|
||||
{
|
||||
new PieChartSeries { title = "印刷类型", list = yslx },
|
||||
new PieChartSeries { title = "印刷消耗", list = ysxh },
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<List<DocPrintTypeItem>> GetDocPrintTypeAsync(int schId)
|
||||
{
|
||||
var schSerId = schId.ToString();
|
||||
|
||||
var list = await _dic.FindAsync(g =>
|
||||
g.typeID == "YZFL" && g.Enable == 1 && (g.Schserid == schSerId || g.Schserid == null));
|
||||
|
||||
return list.OrderBy(g => g.Des)
|
||||
.Select(a => new DocPrintTypeItem { CodeID = a.CodeID, Name = a.Name })
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<DocPrintListResult> GetDocPrintListAsync(int schId, string docType, string likeName, int pageIndex, int pageSize, DateTime? startDate = null, DateTime? endDate = null)
|
||||
{
|
||||
var schSerId = schId.ToString();
|
||||
if (pageSize <= 0) pageSize = 10;
|
||||
|
||||
var list = (await _doc.FindAsync(a =>
|
||||
a.SchSerID == schSerId && StatusArray.Contains(a.DocStatus))).ToList();
|
||||
|
||||
if (!string.IsNullOrEmpty(docType) && docType != "ALL")
|
||||
list = list.Where(a => a.DPType == docType).ToList();
|
||||
|
||||
if (startDate != null)
|
||||
list = list.Where(a => a.TakeDate >= startDate).ToList();
|
||||
if (endDate != null)
|
||||
list = list.Where(a => a.TakeDate <= endDate).ToList();
|
||||
|
||||
if (!string.IsNullOrEmpty(likeName))
|
||||
{
|
||||
var teaMap = await GetTeaMapAsync(schSerId);
|
||||
var userids = teaMap.Where(a => a.Value.Name.Contains(likeName))
|
||||
.Select(a => a.Key.ToString()).ToArray();
|
||||
list = list.Where(m =>
|
||||
(m.DocTitle != null && m.DocTitle.Contains(likeName)) ||
|
||||
(m.DocContent != null && m.DocContent.Contains(likeName)) ||
|
||||
(m.UserID != null && userids.Contains(m.UserID))).ToList();
|
||||
}
|
||||
|
||||
var totle = list.Count;
|
||||
if (totle == 0)
|
||||
return new DocPrintListResult { tlist = new List<DocPrintStatItem>(), totle = totle };
|
||||
|
||||
// 旧 1 基分页 → 0 基
|
||||
pageIndex -= 1;
|
||||
if (pageIndex < 0) pageIndex = 0;
|
||||
|
||||
var yzflList = (await _dic.FindAsync(g =>
|
||||
g.typeID == "YZFL" && (g.Schserid == schSerId || g.Schserid == null)))
|
||||
.OrderBy(g => g.Des)
|
||||
.Select(a => new DocPrintTypeItem { CodeID = a.CodeID, Name = a.Name }).ToList();
|
||||
|
||||
var yzbmList = (await _dic.FindAsync(g =>
|
||||
g.typeID == "YZBM" && (g.Schserid == schSerId || g.Schserid == null)))
|
||||
.OrderBy(g => g.Des)
|
||||
.Select(a => new DocPrintTypeItem { CodeID = a.CodeID, Name = a.Name }).ToList();
|
||||
|
||||
var teaMap2 = await GetTeaMapAsync(schSerId);
|
||||
var tlist = list.OrderByDescending(a => a.TakeDate)
|
||||
.Skip(pageIndex * pageSize).Take(pageSize)
|
||||
.Select(item => new DocPrintStatItem
|
||||
{
|
||||
SerID = (int)item.SerID,
|
||||
DocContent = item.DocContent,
|
||||
DocType = item.DPType,
|
||||
DocNum = item.DocNum,
|
||||
DocTypeName = yzflList.FirstOrDefault(a => a.CodeID == item.DPType)?.Name,
|
||||
DocStyle = item.DocStyle,
|
||||
DocStyleName = yzbmList.FirstOrDefault(a => a.CodeID == item.DocStyle)?.Name,
|
||||
EditDate = item.EditDate.ToString("yyyy-MM-dd HH:mm"),
|
||||
TakeDate = item.TakeDate?.ToString("yyyy-MM-dd"),
|
||||
UserID = item.UserID,
|
||||
teaname = GetTeaName(teaMap2, item.UserID),
|
||||
UpFile = item.UpFile,
|
||||
DocTitle = item.DocTitle,
|
||||
DocStatus = item.DocStatus,
|
||||
DocStatusName = item.DocStatus == "1" ? "印刷中" : "已印刷",
|
||||
DocStep = item.DocStep,
|
||||
flowid = item.flowid,
|
||||
FreeUserID = item.FreeUserID,
|
||||
SendUserID = item.SendUserID,
|
||||
freestepid = item.freestepid,
|
||||
DocShare = item.DocShare,
|
||||
explain = item.explain,
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return new DocPrintListResult { tlist = tlist, totle = totle };
|
||||
}
|
||||
|
||||
#region 辅助
|
||||
|
||||
private async Task<Dictionary<int, (string Name, string Photo)>> GetTeaMapAsync(string schSerId)
|
||||
{
|
||||
var teas = await _sch.TeaInfo.GetSchTeaInfoAsync(schSerId, null);
|
||||
return teas
|
||||
.Where(t => int.TryParse(t.TeaSerId, out _))
|
||||
.GroupBy(t => int.Parse(t.TeaSerId))
|
||||
.ToDictionary(g => g.Key, g => (g.First().Name ?? "", g.First().Photo ?? ""));
|
||||
}
|
||||
|
||||
private static string GetTeaName(Dictionary<int, (string Name, string Photo)> teaMap, string userId)
|
||||
{
|
||||
if (!int.TryParse(userId, out var uid)) return "";
|
||||
return teaMap.TryGetValue(uid, out var tea) ? tea.Name : "";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Yh.Platform.Abstractions;
|
||||
using YhCore.Common.Config;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Repository;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.Service.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// 公共文件服务实现(见 IFileService 注释)。
|
||||
/// 降级:Office→PDF(ToOfficeHelper)新项目缺失,上传不转 PDF;预览兜底 officeapps.live.com。
|
||||
/// </summary>
|
||||
public class FileService : IFileService
|
||||
{
|
||||
private readonly IRepository<Common_UpFiles> _upfiles;
|
||||
|
||||
public FileService(IRepository<Common_UpFiles> upfiles)
|
||||
{
|
||||
_upfiles = upfiles;
|
||||
}
|
||||
|
||||
public async Task<FileObj> GetEnclosureAsync(int id)
|
||||
{
|
||||
var model = await _upfiles.FirstOrDefaultAsync(x => x.SerID == id);
|
||||
if (model == null)
|
||||
throw new DException("文件ID错误!");
|
||||
|
||||
return new FileObj
|
||||
{
|
||||
fileurl = model.FileName,
|
||||
filename = model.FileTrueName,
|
||||
filetype = model.FileType,
|
||||
filesize = model.FileSize ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<int> SaveEnclosureAsync(FileObj file, string appType)
|
||||
{
|
||||
// 替换文件名中的特殊字符(旧 FileSaveEnclosure)
|
||||
string safeName = file.filename ?? "";
|
||||
foreach (var c in Path.GetInvalidFileNameChars())
|
||||
safeName = safeName.Replace(c, '_');
|
||||
safeName = safeName.Replace(",", "_").Replace(",", "_");
|
||||
|
||||
var model = new Common_UpFiles
|
||||
{
|
||||
FileName = file.fileurl,
|
||||
FileSize = file.filesize,
|
||||
FileTrueName = safeName,
|
||||
FileType = file.filetype,
|
||||
DateAndTime = DateTime.Now,
|
||||
TopicType = appType,
|
||||
};
|
||||
await _upfiles.AddAsync(model);
|
||||
await _upfiles.SaveChangesAsync();
|
||||
return model.SerID;
|
||||
}
|
||||
|
||||
public async Task<FileObj> SaveFileAsync(string appType, IFormFile file)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
throw new DException("上传失败!");
|
||||
|
||||
string dtStr = DateTime.Now.ToString("yyyyMMdd");
|
||||
string physicalDir = Path.Combine(StorageRoot(), appType, dtStr);
|
||||
string outFilePath = appType + "/" + dtStr + "/";
|
||||
|
||||
if (!Directory.Exists(physicalDir))
|
||||
Directory.CreateDirectory(physicalDir);
|
||||
|
||||
string guid = Guid.NewGuid().ToString();
|
||||
string fileName = Path.GetFileName(file.FileName);
|
||||
string suffixName = Path.GetExtension(file.FileName);
|
||||
|
||||
string[] suffArry = { "jpg", "gif", "png", "xls", "doc", "txt", "docx", "xlsx", "zip", "rar", "pdf", "ppt", "pptx" };
|
||||
suffArry = suffArry.Select(x => "." + x).ToArray();
|
||||
bool isSuffType = suffArry.Any(x => x == suffixName.ToLower());
|
||||
if (!isSuffType)
|
||||
throw new DException("不能上传" + suffixName + "格式的文件");
|
||||
|
||||
int fileSize = (int)file.Length;
|
||||
string savePath = Path.Combine(physicalDir, guid + suffixName);
|
||||
using (var stream = new FileStream(savePath, FileMode.Create))
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
}
|
||||
// 旧 ToOfficeHelper.OfficeConvertToPDF(Office COM)新项目缺失 → 降级不转 PDF
|
||||
|
||||
return new FileObj
|
||||
{
|
||||
fileurl = outFilePath + guid + suffixName,
|
||||
filename = fileName,
|
||||
filetype = suffixName,
|
||||
filesize = fileSize,
|
||||
host = Config.FileWebPath,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<List<FileObj>> SaveFileImageAsync(string appType, IFormFileCollection files)
|
||||
{
|
||||
if (appType.ToLower() == "noticemsg") appType = "notice";
|
||||
string dtStr = DateTime.Now.ToString("yyyyMMdd");
|
||||
string physicalDir = Path.Combine(StorageRoot(), appType, dtStr);
|
||||
string outFilePath = appType + "/" + dtStr + "/";
|
||||
|
||||
if (!Directory.Exists(physicalDir))
|
||||
Directory.CreateDirectory(physicalDir);
|
||||
|
||||
string[] suffArry = { "jpg", "gif", "png", "jpeg" };
|
||||
suffArry = suffArry.Select(x => "." + x).ToArray();
|
||||
|
||||
var list = new List<FileObj>();
|
||||
foreach (var file in files)
|
||||
{
|
||||
if (file == null || file.Length == 0) continue;
|
||||
string suffixName = Path.GetExtension(file.FileName);
|
||||
if (!suffArry.Any(x => x == suffixName.ToLower())) continue;
|
||||
|
||||
string guid = Guid.NewGuid().ToString();
|
||||
string fileName = Path.GetFileName(file.FileName);
|
||||
int fileSize = (int)file.Length;
|
||||
string savePath = Path.Combine(physicalDir, guid + suffixName);
|
||||
using (var stream = new FileStream(savePath, FileMode.Create))
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
list.Add(new FileObj
|
||||
{
|
||||
fileurl = outFilePath + guid + suffixName,
|
||||
filename = fileName,
|
||||
filetype = suffixName,
|
||||
filesize = fileSize,
|
||||
host = Config.FileWebPath,
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public async Task SaveChunkAsync(string guid, string chunkIndex, IFormFile chunk)
|
||||
{
|
||||
if (string.IsNullOrEmpty(guid) || chunk == null || chunk.Length == 0)
|
||||
throw new DException("上传失败!");
|
||||
|
||||
var dir = Path.Combine(StorageRoot(), guid);
|
||||
if (!Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
string filePath = Path.Combine(dir, chunkIndex);
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
{
|
||||
await chunk.CopyToAsync(stream);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FileObj> MergeSaveAsync(string appType, string guid, string fileName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(guid) || string.IsNullOrEmpty(fileName))
|
||||
throw new DException("上传失败!");
|
||||
|
||||
if (appType.ToLower() == "noticemsg") appType = "notice";
|
||||
string dtStr = DateTime.Now.ToString("yyyyMMdd");
|
||||
string physicalDir = Path.Combine(StorageRoot(), appType, dtStr);
|
||||
string outFilePath = appType + "/" + dtStr + "/";
|
||||
if (!Directory.Exists(physicalDir))
|
||||
Directory.CreateDirectory(physicalDir);
|
||||
|
||||
var uploadDir = Path.Combine(StorageRoot());
|
||||
var dir = Path.Combine(uploadDir, guid);
|
||||
if (!Directory.Exists(dir))
|
||||
throw new DException("上传失败!");
|
||||
|
||||
var files = Directory.GetFiles(dir);
|
||||
string fileRealName = Path.GetFileName(fileName);
|
||||
string suffixName = Path.GetExtension(fileRealName);
|
||||
string fileGuid = Guid.NewGuid().ToString();
|
||||
string outUrl = outFilePath + fileGuid + suffixName;
|
||||
string finalPath = Path.Combine(physicalDir, fileGuid + suffixName);
|
||||
|
||||
using (var fs = new FileStream(finalPath, FileMode.Create))
|
||||
{
|
||||
foreach (var part in files.OrderBy(x => x.Length).ThenBy(x => x))
|
||||
{
|
||||
var bytes = await File.ReadAllBytesAsync(part);
|
||||
fs.Write(bytes, 0, bytes.Length);
|
||||
File.Delete(part);
|
||||
}
|
||||
}
|
||||
int fileSize = (int)(new FileInfo(finalPath).Length / 1024 / 1024); // 旧实现以 MB 存储,保持一致
|
||||
Directory.Delete(dir);
|
||||
|
||||
// 旧 ToOfficeHelper.OfficeConvertToPDF 缺失 → 降级不转 PDF
|
||||
|
||||
return new FileObj
|
||||
{
|
||||
fileurl = outUrl,
|
||||
filename = fileRealName,
|
||||
filetype = suffixName,
|
||||
filesize = fileSize,
|
||||
host = Config.FileWebPath,
|
||||
};
|
||||
}
|
||||
|
||||
public string MapPhysicalPath(string fileUrl)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fileUrl))
|
||||
return "";
|
||||
|
||||
// 新数据: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('/');
|
||||
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("~/upload/");Config.FilePath 缺省时退回进程目录/upload)。</summary>
|
||||
public static string StorageRoot()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(Config.FilePath))
|
||||
return Config.FilePath;
|
||||
|
||||
// 此分支仅在 FILE_PATH 未配置时执行(此时 Config.FilePath 为 null,不可再参与拼接)
|
||||
return Path.Combine(AppContext.BaseDirectory, Config.FilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,595 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Yh.Platform.Abstractions;
|
||||
using Yh.SchClass.OpenApi;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Repository;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.Service.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// 自由审批流服务(移植旧 BestSchool.Service.ApproveFlow)。
|
||||
/// 教师查询改走教务 SDK(Yh.SchClass.OpenApi),教师标识用 teaId(TeaSerId)对齐新教务。
|
||||
/// </summary>
|
||||
public class FreeApproveService : IFreeApproveService
|
||||
{
|
||||
private readonly IRepository<Common_freeflowinfo> _freeflowinfo;
|
||||
private readonly IRepository<Common_freeGroupuser> _freegroup;
|
||||
private readonly IRepository<Common_flowtranctInfo> _flowtranct;
|
||||
private readonly IRepository<Common_flowstep> _flowstep;
|
||||
private readonly IRepository<Common_Groupuser> _groupuser;
|
||||
private readonly IRepository<Common_UserFlowZhipai> _userflowzhipai;
|
||||
private readonly YHDbContext _db;
|
||||
private readonly SchClassOpenApi _sch;
|
||||
|
||||
public FreeApproveService(
|
||||
IRepository<Common_freeflowinfo> freeflowinfo,
|
||||
IRepository<Common_freeGroupuser> freegroup,
|
||||
IRepository<Common_flowtranctInfo> flowtranct,
|
||||
IRepository<Common_flowstep> flowstep,
|
||||
IRepository<Common_Groupuser> groupuser,
|
||||
IRepository<Common_UserFlowZhipai> userflowzhipai,
|
||||
YHDbContext db,
|
||||
SchClassOpenApi sch)
|
||||
{
|
||||
_freeflowinfo = freeflowinfo;
|
||||
_freegroup = freegroup;
|
||||
_flowtranct = flowtranct;
|
||||
_flowstep = flowstep;
|
||||
_groupuser = groupuser;
|
||||
_userflowzhipai = userflowzhipai;
|
||||
_db = db;
|
||||
_sch = sch;
|
||||
}
|
||||
|
||||
public async Task<List<FreeFlowNode>> GetFleeFlowAsync(int userId, int? dataid)
|
||||
{
|
||||
int appid = DocPrintConstants.AppCode;
|
||||
|
||||
if ((dataid ?? 0) == 0)
|
||||
{
|
||||
var all = await _freeflowinfo.FindAsync(a => a.flowType == appid && a.userid == userId);
|
||||
dataid = all.Count == 0 ? 0 : all.Max(a => a.dataid ?? 0);
|
||||
}
|
||||
|
||||
if ((dataid ?? 0) <= 0) return new List<FreeFlowNode>();
|
||||
|
||||
var nodes = (await _freeflowinfo.FindAsync(a => a.flowType == appid && a.userid == userId && a.dataid == dataid))
|
||||
.OrderBy(a => a.SerID).ToList();
|
||||
return await BuildFreeFlowNodesAsync(nodes);
|
||||
}
|
||||
|
||||
public async Task<int> EditFleeFlowAsync(int userId, string schSerId, int? dataid, string flowUids, bool isFlowNull)
|
||||
{
|
||||
if (string.IsNullOrEmpty(flowUids))
|
||||
{
|
||||
if (!isFlowNull)
|
||||
throw new DException("请选择审核步骤人员!");
|
||||
flowUids = "";
|
||||
}
|
||||
|
||||
var flowUidArr = flowUids.Split('_');
|
||||
int appid = DocPrintConstants.AppCode;
|
||||
int firstStepId = 0;
|
||||
|
||||
using var tx = await _db.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
if ((dataid ?? 0) > 0)
|
||||
{
|
||||
var old = await _freeflowinfo.FindAsync(a => a.dataid == dataid && a.flowType == appid && a.userid == userId);
|
||||
foreach (var item in old)
|
||||
{
|
||||
await _freeflowinfo.RemoveAsync(item.SerID);
|
||||
}
|
||||
await _freeflowinfo.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var upSeptid = 0;
|
||||
var date = DateTime.Now;
|
||||
foreach (var fw in flowUidArr)
|
||||
{
|
||||
var free = new Common_freeflowinfo
|
||||
{
|
||||
laststep = upSeptid,
|
||||
userid = userId,
|
||||
createTime = date,
|
||||
dataid = dataid,
|
||||
schserid = schSerId,
|
||||
flowType = appid,
|
||||
};
|
||||
await _freeflowinfo.AddAsync(free);
|
||||
await _freeflowinfo.SaveChangesAsync();
|
||||
|
||||
if (upSeptid == 0)
|
||||
firstStepId = free.SerID;
|
||||
upSeptid = free.SerID;
|
||||
|
||||
foreach (var guserid in fw.Split(','))
|
||||
{
|
||||
await _freegroup.AddAsync(new Common_freeGroupuser
|
||||
{
|
||||
flowid = upSeptid,
|
||||
SchSerID = schSerId,
|
||||
usergroup = upSeptid,
|
||||
userid = string.IsNullOrEmpty(guserid) ? (int?)null : int.Parse(guserid),
|
||||
});
|
||||
}
|
||||
await _freegroup.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await tx.CommitAsync();
|
||||
return firstStepId;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await tx.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> AddShenheRemarkAsync(int userId, int? dataid, string remark, int freestepid)
|
||||
{
|
||||
var info = new Common_flowtranctInfo
|
||||
{
|
||||
actionstatus = -1,
|
||||
remark = remark,
|
||||
dataid = dataid ?? 0,
|
||||
stepid = freestepid,
|
||||
datatype = DocPrintConstants.AppCode.ToString(),
|
||||
actiondate = DateTime.Now,
|
||||
actionuserid = userId,
|
||||
};
|
||||
await _flowtranct.AddAsync(info);
|
||||
await _flowtranct.SaveChangesAsync();
|
||||
return "备注成功!";
|
||||
}
|
||||
|
||||
public async Task<List<FreeFlowNode>> GetAfterFleeFlowAsync(int? dataid, int freestepid)
|
||||
{
|
||||
int appid = DocPrintConstants.AppCode;
|
||||
|
||||
if ((dataid ?? 0) <= 0) return new List<FreeFlowNode>();
|
||||
|
||||
var nodes = (await _freeflowinfo.FindAsync(a => a.flowType == appid && a.SerID > freestepid && a.dataid == dataid))
|
||||
.OrderBy(a => a.SerID).ToList();
|
||||
return await BuildFreeFlowNodesAsync(nodes);
|
||||
}
|
||||
|
||||
public async Task<int> EditAfterFleeFlowAsync(int? dataid, int freestepid, string flowUids)
|
||||
{
|
||||
int appid = DocPrintConstants.AppCode;
|
||||
|
||||
// 删除 freestepid 之后的剩余步骤
|
||||
var nextSteps = await _freeflowinfo.FindAsync(a => a.dataid == dataid && a.SerID > freestepid);
|
||||
if (nextSteps.Count > 0)
|
||||
{
|
||||
foreach (var item in nextSteps)
|
||||
{
|
||||
await _freeflowinfo.RemoveAsync(item.SerID);
|
||||
}
|
||||
await _freeflowinfo.SaveChangesAsync();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(flowUids))
|
||||
return 0;
|
||||
|
||||
var flowUidArr = flowUids.Split('_');
|
||||
var laststept = await _freeflowinfo.FirstOrDefaultAsync(a => a.SerID == freestepid);
|
||||
if (laststept == null)
|
||||
throw new DException("未找到自由流程步骤!");
|
||||
|
||||
var upSeptid = freestepid;
|
||||
using var tx = await _db.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
var date = DateTime.Now;
|
||||
foreach (var fw in flowUidArr)
|
||||
{
|
||||
var free = new Common_freeflowinfo
|
||||
{
|
||||
laststep = upSeptid,
|
||||
userid = laststept.userid,
|
||||
createTime = date,
|
||||
dataid = dataid,
|
||||
schserid = laststept.schserid,
|
||||
flowType = appid,
|
||||
};
|
||||
await _freeflowinfo.AddAsync(free);
|
||||
await _freeflowinfo.SaveChangesAsync();
|
||||
|
||||
upSeptid = free.SerID;
|
||||
foreach (var guserid in fw.Split(','))
|
||||
{
|
||||
await _freegroup.AddAsync(new Common_freeGroupuser
|
||||
{
|
||||
flowid = upSeptid,
|
||||
SchSerID = laststept.schserid,
|
||||
usergroup = upSeptid,
|
||||
userid = string.IsNullOrEmpty(guserid) ? (int?)null : int.Parse(guserid),
|
||||
});
|
||||
}
|
||||
await _freegroup.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await tx.CommitAsync();
|
||||
return upSeptid;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await tx.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<FreeShenheInfo>> ShenheFlowAsync(int uid, string ctime, int dataid, int? freestepid, int? flowid)
|
||||
{
|
||||
int flowType = DocPrintConstants.AppCode;
|
||||
string appCode = DocPrintConstants.AppCode.ToString();
|
||||
|
||||
if ((freestepid ?? 0) > 0)
|
||||
return await BuildFreeShenheFlowAsync(uid, ctime, dataid, freestepid, flowType, appCode);
|
||||
|
||||
if ((flowid ?? 0) > 0)
|
||||
return await BuildFixShenheFlowAsync(uid, ctime, dataid, flowid, appCode);
|
||||
|
||||
return await BuildOnlyTranctShenheFlowAsync(uid, ctime, dataid, appCode);
|
||||
}
|
||||
|
||||
public async Task<List<ChaoSendUser>> ChaoSendListAsync(string uids)
|
||||
{
|
||||
var result = new List<ChaoSendUser>();
|
||||
|
||||
if (string.IsNullOrEmpty(uids) || uids == "undefined")
|
||||
return result;
|
||||
|
||||
var userids = uids.Split(',')
|
||||
.Where(a => !string.IsNullOrEmpty(a) && int.TryParse(a, out _))
|
||||
.Select(int.Parse).ToArray();
|
||||
if (userids.Length == 0)
|
||||
return result;
|
||||
|
||||
var teas = await _sch.TeaInfo.GetArrTeaInfoAsync(string.Join(",", userids));
|
||||
return teas.Select(a => new ChaoSendUser
|
||||
{
|
||||
uid = ParseTeaId(a.TeaSerId),
|
||||
photo = a.Photo,
|
||||
name = a.Name,
|
||||
status = false,
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
#region 审核流程回显
|
||||
|
||||
private async Task<List<FreeShenheInfo>> BuildFreeShenheFlowAsync(int uid, string ctime, int dataid, int? freestepid, int flowType, string appCode)
|
||||
{
|
||||
var freeflow = (await _freeflowinfo.FindAsync(a => a.dataid == dataid && a.flowType == flowType))
|
||||
.OrderBy(a => a.SerID).ToList();
|
||||
var freeflowids = freeflow.Select(a => a.SerID).ToArray();
|
||||
var freeflowGroup = (await _freegroup.FindAsync(a => freeflowids.Contains(a.usergroup ?? 0))).ToList();
|
||||
var freeshenhe = await _flowtranct.FindAsync(a => a.dataid == dataid && a.datatype == appCode);
|
||||
|
||||
var frees = new List<FreeShenheInfo>();
|
||||
var firstFlow = freeflow.FirstOrDefault();
|
||||
frees.Add(new FreeShenheInfo
|
||||
{
|
||||
steptname = await GetUserNameAsync(firstFlow?.userid?.ToString() ?? ""),
|
||||
steptstatus = "发起申请",
|
||||
photo = await GetUserPhotoAsync(firstFlow?.userid?.ToString() ?? ""),
|
||||
actiondate = firstFlow?.createTime?.ToString("yyyy-MM-dd HH:mm") ?? "",
|
||||
});
|
||||
|
||||
if (freeshenhe.Count(a => a.actionstatus > 2) == 0)
|
||||
{
|
||||
foreach (var item in freeflow)
|
||||
{
|
||||
var act = new FreeShenheInfo();
|
||||
var isShok = freeshenhe.FirstOrDefault(a => a.stepid == item.SerID);
|
||||
if (isShok != null)
|
||||
{
|
||||
act.steptname = await ChaoSendNameAsync(isShok.actionuserid?.ToString());
|
||||
act.photo = await GetUserPhotoAsync(isShok.actionuserid?.ToString());
|
||||
act.steptstatus = ActionstatusName(isShok.actionstatus);
|
||||
act.actionstatus = isShok.actionstatus.ToString();
|
||||
act.Remark = isShok.remark;
|
||||
act.actiondate = isShok.actiondate?.ToString("yyyy-MM-dd HH:mm") ?? "";
|
||||
|
||||
if (isShok.actionstatus == 2)
|
||||
{
|
||||
frees.Add(act);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var userids = ToUserIds(freeflowGroup.Where(a => a.usergroup == item.SerID).Select(a => a.userid));
|
||||
act.steptname = await ChaoSendNameAsync(string.Join(",", userids));
|
||||
act.photo = userids.Length == 1 ? await GetUserPhotoAsync(userids[0].ToString()) : "/static/image/site/moreperpeo.png";
|
||||
act.steptstatus = "待审核";
|
||||
act.actionstatus = "0";
|
||||
}
|
||||
frees.Add(act);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in freeshenhe)
|
||||
{
|
||||
frees.Add(new FreeShenheInfo
|
||||
{
|
||||
steptname = await ChaoSendNameAsync((item.actionuserid ?? uid).ToString()),
|
||||
photo = await GetUserPhotoAsync((item.actionuserid ?? uid).ToString()),
|
||||
steptstatus = ActionstatusName(item.actionstatus),
|
||||
actionstatus = item.actionstatus.ToString(),
|
||||
Remark = item.remark,
|
||||
actiondate = item.actiondate?.ToString("yyyy-MM-dd HH:mm") ?? "",
|
||||
});
|
||||
}
|
||||
|
||||
var zuihouyibu = freeshenhe.Where(a => a.actionstatus >= 2).OrderByDescending(a => a.SerID).FirstOrDefault();
|
||||
if (zuihouyibu != null && zuihouyibu.actionstatus == 4)
|
||||
{
|
||||
var isShok = freeshenhe.FirstOrDefault(a => a.SerID > zuihouyibu.SerID);
|
||||
if (isShok == null)
|
||||
{
|
||||
var userids = ToUserIds(freeflowGroup.Where(a => a.usergroup == freestepid).Select(a => a.userid));
|
||||
frees.Add(new FreeShenheInfo
|
||||
{
|
||||
steptname = await ChaoSendNameAsync(string.Join(",", userids)),
|
||||
photo = userids.Length == 1 ? await GetUserPhotoAsync(userids[0].ToString()) : "/static/image/site/moreperpeo.png",
|
||||
steptstatus = "待审核",
|
||||
actionstatus = "0",
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (zuihouyibu != null && zuihouyibu.actionstatus == 6)
|
||||
{
|
||||
foreach (var item in freeflow)
|
||||
{
|
||||
var isShok = freeshenhe.FirstOrDefault(a => a.stepid == item.SerID && a.SerID > zuihouyibu.SerID);
|
||||
if (isShok == null)
|
||||
{
|
||||
var userids = ToUserIds(freeflowGroup.Where(a => a.usergroup == item.SerID).Select(a => a.userid));
|
||||
frees.Add(new FreeShenheInfo
|
||||
{
|
||||
steptname = await ChaoSendNameAsync(string.Join(",", userids)),
|
||||
photo = userids.Length == 1 ? await GetUserPhotoAsync(userids[0].ToString()) : "/static/image/site/moreperpeo.png",
|
||||
steptstatus = "待审核",
|
||||
actionstatus = "0",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return frees;
|
||||
}
|
||||
|
||||
private async Task<List<FreeShenheInfo>> BuildFixShenheFlowAsync(int uid, string ctime, int dataid, int? flowid, string appCode)
|
||||
{
|
||||
var fixflow = (await _flowstep.FindAsync(a => a.flowid == flowid)).OrderBy(a => a.SerID).ToList();
|
||||
var fixflowids = fixflow.Select(a => a.SerID).ToArray();
|
||||
var fixflowGroup = (await _groupuser.FindAsync(a => fixflowids.Contains(a.usergroup ?? 0))).ToList();
|
||||
var fixshenhe = await _flowtranct.FindAsync(a => a.dataid == dataid && a.datatype == appCode);
|
||||
|
||||
var frees = new List<FreeShenheInfo>();
|
||||
frees.Add(new FreeShenheInfo
|
||||
{
|
||||
steptname = await GetUserNameAsync(uid.ToString()),
|
||||
steptstatus = "发起申请",
|
||||
actiondate = ctime,
|
||||
photo = await GetUserPhotoAsync(uid.ToString()),
|
||||
});
|
||||
|
||||
if (fixshenhe.Count(a => a.actionstatus > 2) == 0)
|
||||
{
|
||||
foreach (var item in fixflow)
|
||||
{
|
||||
var act = new FreeShenheInfo();
|
||||
var isShok = fixshenhe.FirstOrDefault(a => a.stepid == item.SerID);
|
||||
if (isShok != null)
|
||||
{
|
||||
act.steptname = await ChaoSendNameAsync(isShok.actionuserid?.ToString());
|
||||
act.photo = await GetUserPhotoAsync(isShok.actionuserid?.ToString());
|
||||
act.steptstatus = ActionstatusName(isShok.actionstatus);
|
||||
act.actionstatus = isShok.actionstatus.ToString();
|
||||
act.Remark = isShok.remark;
|
||||
act.actiondate = isShok.actiondate?.ToString("yyyy-MM-dd HH:mm") ?? "";
|
||||
|
||||
if (isShok.actionstatus == 2)
|
||||
{
|
||||
frees.Add(act);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var userids = ToUserIds(fixflowGroup.Where(a => a.usergroup == item.SerID).Select(a => a.userid));
|
||||
|
||||
var userzhipai = await _userflowzhipai.FirstOrDefaultAsync(
|
||||
a => a.MSerID == dataid && a.FlowID == flowid && a.FlowStepID == item.SerID);
|
||||
if (!string.IsNullOrEmpty(userzhipai?.zhipai))
|
||||
{
|
||||
userids = userzhipai.zhipai.Split(',')
|
||||
.Where(s => int.TryParse(s, out _))
|
||||
.Select(int.Parse).Distinct().ToArray();
|
||||
}
|
||||
|
||||
act.steptname = await ChaoSendNameAsync(string.Join(",", userids));
|
||||
act.photo = userids.Length == 1 ? await GetUserPhotoAsync(userids[0].ToString()) : "/static/image/site/moreperpeo.png";
|
||||
act.steptstatus = "待审核";
|
||||
act.actionstatus = "0";
|
||||
}
|
||||
frees.Add(act);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in fixshenhe)
|
||||
{
|
||||
frees.Add(new FreeShenheInfo
|
||||
{
|
||||
steptname = await ChaoSendNameAsync((item.actionuserid ?? uid).ToString()),
|
||||
photo = await GetUserPhotoAsync((item.actionuserid ?? uid).ToString()),
|
||||
steptstatus = ActionstatusName(item.actionstatus),
|
||||
actionstatus = item.actionstatus.ToString(),
|
||||
Remark = item.remark,
|
||||
actiondate = item.actiondate?.ToString("yyyy-MM-dd HH:mm") ?? "",
|
||||
});
|
||||
}
|
||||
|
||||
var zuihouyibu = fixshenhe.Where(a => a.actionstatus >= 2).OrderByDescending(a => a.SerID).FirstOrDefault();
|
||||
if (zuihouyibu != null && zuihouyibu.actionstatus == 4)
|
||||
{
|
||||
var isShok = fixshenhe.FirstOrDefault(a => a.SerID > zuihouyibu.SerID);
|
||||
if (isShok == null)
|
||||
{
|
||||
var lastStepId = fixflowids.OrderByDescending(p => p).FirstOrDefault();
|
||||
var userids = ToUserIds(fixflowGroup.Where(a => a.usergroup == lastStepId).Select(a => a.userid));
|
||||
frees.Add(new FreeShenheInfo
|
||||
{
|
||||
steptname = await ChaoSendNameAsync(string.Join(",", userids)),
|
||||
photo = userids.Length == 1 ? await GetUserPhotoAsync(userids[0].ToString()) : "/static/image/site/moreperpeo.png",
|
||||
steptstatus = "待审核",
|
||||
actionstatus = "0",
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (zuihouyibu != null && zuihouyibu.actionstatus == 6)
|
||||
{
|
||||
foreach (var item in fixflow)
|
||||
{
|
||||
var isShok = fixshenhe.FirstOrDefault(a => a.stepid == item.SerID && a.SerID > zuihouyibu.SerID);
|
||||
if (isShok == null)
|
||||
{
|
||||
var userids = ToUserIds(fixflowGroup.Where(a => a.usergroup == item.SerID).Select(a => a.userid));
|
||||
frees.Add(new FreeShenheInfo
|
||||
{
|
||||
steptname = await ChaoSendNameAsync(string.Join(",", userids)),
|
||||
photo = userids.Length == 1 ? await GetUserPhotoAsync(userids[0].ToString()) : "/static/image/site/moreperpeo.png",
|
||||
steptstatus = "待审核",
|
||||
actionstatus = "0",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return frees;
|
||||
}
|
||||
|
||||
private async Task<List<FreeShenheInfo>> BuildOnlyTranctShenheFlowAsync(int uid, string ctime, int dataid, string appCode)
|
||||
{
|
||||
var shenhe = await _flowtranct.FindAsync(a => a.dataid == dataid && a.datatype == appCode);
|
||||
|
||||
var frees = new List<FreeShenheInfo>();
|
||||
frees.Add(new FreeShenheInfo
|
||||
{
|
||||
steptname = await GetUserNameAsync(uid.ToString()),
|
||||
steptstatus = "发起申请",
|
||||
actiondate = ctime,
|
||||
photo = await GetUserPhotoAsync(uid.ToString()),
|
||||
});
|
||||
|
||||
if (shenhe.Count(a => a.actionstatus > 2) > 0)
|
||||
{
|
||||
foreach (var item in shenhe)
|
||||
{
|
||||
frees.Add(new FreeShenheInfo
|
||||
{
|
||||
steptname = await ChaoSendNameAsync(item.actionuserid?.ToString()),
|
||||
photo = await GetUserPhotoAsync(item.actionuserid?.ToString()),
|
||||
steptstatus = ActionstatusName(item.actionstatus),
|
||||
actionstatus = item.actionstatus.ToString(),
|
||||
Remark = item.remark,
|
||||
actiondate = item.actiondate?.ToString("yyyy-MM-dd HH:mm") ?? "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return frees;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 辅助
|
||||
|
||||
private async Task<List<FreeFlowNode>> BuildFreeFlowNodesAsync(List<Common_freeflowinfo> nodes)
|
||||
{
|
||||
var result = new List<FreeFlowNode>();
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
var userIds = (await _freegroup.FindAsync(n => n.flowid == node.SerID))
|
||||
.Select(n => n.userid).Where(u => u.HasValue).Select(u => u.Value).ToList();
|
||||
var users = new List<FreeFlowUser>();
|
||||
if (userIds.Count > 0)
|
||||
{
|
||||
var teas = await _sch.TeaInfo.GetArrTeaInfoAsync(string.Join(",", userIds));
|
||||
users = teas.Select(t => new FreeFlowUser
|
||||
{
|
||||
userid = ParseTeaId(t.TeaSerId),
|
||||
name = t.Name,
|
||||
photo = t.Photo,
|
||||
}).ToList();
|
||||
}
|
||||
result.Add(new FreeFlowNode { user = users });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int ParseTeaId(string teaSerId)
|
||||
=> int.TryParse(teaSerId, out var id) ? id : 0;
|
||||
|
||||
private static int[] ToUserIds(IEnumerable<int?> source)
|
||||
=> source.Select(u => u ?? 0).ToArray();
|
||||
|
||||
private async Task<string> GetUserNameAsync(string teaId)
|
||||
{
|
||||
if (!int.TryParse(teaId, out var id)) return "";
|
||||
var tea = await _sch.TeaInfo.GetTeaInfoAsync(id);
|
||||
return tea?.Name ?? "";
|
||||
}
|
||||
|
||||
private async Task<string> GetUserPhotoAsync(string teaId)
|
||||
{
|
||||
if (!int.TryParse(teaId, out var id)) return "";
|
||||
var tea = await _sch.TeaInfo.GetTeaInfoAsync(id);
|
||||
return tea?.Photo ?? "";
|
||||
}
|
||||
|
||||
private async Task<string> ChaoSendNameAsync(string uids)
|
||||
{
|
||||
if (string.IsNullOrEmpty(uids) || uids == "undefined") return "";
|
||||
var ids = uids.Split(',')
|
||||
.Where(s => int.TryParse(s, out _)).Select(int.Parse).Distinct().ToArray();
|
||||
if (ids.Length == 0) return "";
|
||||
var teas = await _sch.TeaInfo.GetArrTeaInfoAsync(string.Join(",", ids));
|
||||
return string.Join(",", teas.Select(t => t.Name).Where(n => !string.IsNullOrEmpty(n)));
|
||||
}
|
||||
|
||||
private static string ActionstatusName(int actionstatus)
|
||||
{
|
||||
return actionstatus == -1 ? "备注"
|
||||
: actionstatus == 0 ? "发起"
|
||||
: actionstatus == 1 ? "已同意"
|
||||
: actionstatus == 2 ? "拒绝"
|
||||
: actionstatus == 3 ? "已撤销"
|
||||
: actionstatus == 4 ? "撤销发起"
|
||||
: actionstatus == 5 ? "通过(撤销被拒绝)"
|
||||
: actionstatus == 6 ? "发起修改申请"
|
||||
: actionstatus == 7 ? "已领取"
|
||||
: actionstatus == 8 ? "已维修"
|
||||
: actionstatus == 9 ? "已归还"
|
||||
: actionstatus == 10 ? "已领取"
|
||||
: actionstatus == 11 ? "印刷中"
|
||||
: "其他";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Threading.Tasks;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Vo;
|
||||
using YhCore.Repository;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.Service.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// 日志查询服务(对应 Java 版 LogServiceImpl,替换框架期 LogServiceStub)。
|
||||
/// 查询 sys_operation_log:按学校隔离;systemScope=false 排除查询类(GET=LogLevel 3);
|
||||
/// keyword 模糊匹配用户名/URI/类方法/请求参数/异常/操作类型;按 CreateTime 倒序分页。
|
||||
/// </summary>
|
||||
public class LogService : ILogService
|
||||
{
|
||||
/// <summary>查询类日志级别(与 SysOperationLogFilter 推导一致:GET → 3/QUERY)</summary>
|
||||
private const int QueryLogLevel = 3;
|
||||
|
||||
/// <summary>每页大小上限(审计日志量大,防一次拉全表)</summary>
|
||||
private const int MaxPageSize = 200;
|
||||
|
||||
private readonly IRepository<SysOperationLog> _log;
|
||||
|
||||
public LogService(IRepository<SysOperationLog> log)
|
||||
{
|
||||
_log = log;
|
||||
}
|
||||
|
||||
public Task<PageVO<SysOperationLog>> SearchAsync(int schId, string keyword, bool systemScope, int page, int size)
|
||||
{
|
||||
if (page < 0) page = 0;
|
||||
if (size <= 0) size = 20;
|
||||
if (size > MaxPageSize) size = MaxPageSize;
|
||||
|
||||
var kw = keyword?.Trim();
|
||||
|
||||
return _log.PageAsync(
|
||||
a => a.SchId == schId
|
||||
// 普通操作日志:只看增删改(排除查询类);系统操作日志全量
|
||||
&& (systemScope || a.LogLevel != QueryLogLevel)
|
||||
// 关键字模糊匹配(EF 翻译为 SQL LIKE,NULL 列不命中)
|
||||
&& (string.IsNullOrEmpty(kw)
|
||||
|| a.UserName.Contains(kw)
|
||||
|| a.RequestUri.Contains(kw)
|
||||
|| a.ClassMethod.Contains(kw)
|
||||
|| a.RequestParams.Contains(kw)
|
||||
|| a.ErrorMsg.Contains(kw)
|
||||
|| a.OperationType.Contains(kw)),
|
||||
page, size,
|
||||
a => a.CreateTime, asc: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Yh.Platform.Abstractions;
|
||||
using Yh.SchClass.OpenApi;
|
||||
using Yh.SchClass.OpenApi.Model;
|
||||
using YhCore.Common.UserContext;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Model.Vo;
|
||||
using YhCore.Repository;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.Service.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// 审核流程管理服务(移植旧 BestSchool.Service.Reviewprocess + UserFlowRole)。
|
||||
/// 教师查询改走教务 SDK(Yh.SchClass.OpenApi),缺失 groupId 的局版接口降级为当前学校。
|
||||
/// </summary>
|
||||
public class ReviewProcessService : IReviewProcessService
|
||||
{
|
||||
private readonly IRepository<Common_flowinfo> _flowinfo;
|
||||
private readonly IRepository<Common_flowstep> _flowstep;
|
||||
private readonly IRepository<Common_Groupuser> _groupuser;
|
||||
private readonly IRepository<Common_freeflowinfo> _freeflowinfo;
|
||||
private readonly IRepository<Common_freeGroupuser> _freegroup;
|
||||
private readonly IRepository<Common_SendUserinfo> _senduser;
|
||||
private readonly IRepository<Common_UpFiles> _upfiles;
|
||||
private readonly SchClassOpenApi _sch;
|
||||
|
||||
public ReviewProcessService(
|
||||
IRepository<Common_flowinfo> flowinfo,
|
||||
IRepository<Common_flowstep> flowstep,
|
||||
IRepository<Common_Groupuser> groupuser,
|
||||
IRepository<Common_freeflowinfo> freeflowinfo,
|
||||
IRepository<Common_freeGroupuser> freegroup,
|
||||
IRepository<Common_SendUserinfo> senduser,
|
||||
IRepository<Common_UpFiles> upfiles,
|
||||
SchClassOpenApi sch)
|
||||
{
|
||||
_flowinfo = flowinfo;
|
||||
_flowstep = flowstep;
|
||||
_groupuser = groupuser;
|
||||
_freeflowinfo = freeflowinfo;
|
||||
_freegroup = freegroup;
|
||||
_senduser = senduser;
|
||||
_upfiles = upfiles;
|
||||
_sch = sch;
|
||||
}
|
||||
|
||||
#region 流程与步骤
|
||||
|
||||
public Task<PageVO<FlowListItem>> List2Async(string name, int pageSize, int pageIndex, int flowType, bool edit, string schSerId)
|
||||
=> ListAsync(name, pageSize, pageIndex, flowType, edit, schSerId);
|
||||
|
||||
public async Task<PageVO<FlowListItem>> ListAsync(string name, int pageSize, int pageIndex, int flowType, bool edit, string schSerId)
|
||||
{
|
||||
var resultList = await _flowinfo.FindAsync(m => m.flowType == flowType && m.schSerID == schSerId);
|
||||
|
||||
if (pageSize == 100 && pageIndex == 0)
|
||||
{
|
||||
if (!edit)
|
||||
resultList = resultList.Where(a => a.enable == 1).ToList();
|
||||
|
||||
var stepFlowIds = (await _flowstep.FindAsync(s => true))
|
||||
.Select(s => s.flowid).Where(f => f.HasValue).Select(f => f.Value).Distinct().ToHashSet();
|
||||
resultList = resultList.Where(a => stepFlowIds.Contains(a.SerID)).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!edit)
|
||||
resultList = resultList.Where(a => a.enable != 3).ToList();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
resultList = resultList.Where(m => m.flowname != null && m.flowname.IndexOf(name) >= 0).ToList();
|
||||
}
|
||||
|
||||
var ordered = resultList.OrderBy(m => m.SerID).ToList();
|
||||
var items = ordered.Skip(pageSize * pageIndex).Take(pageSize)
|
||||
.Select(item => new FlowListItem
|
||||
{
|
||||
SerID = item.SerID.ToString(),
|
||||
flowname = item.flowname,
|
||||
deptid = item.deptid,
|
||||
typeid = item.typeid,
|
||||
enable = item.enable,
|
||||
enablename = item.enable == 1 ? "是" : "否",
|
||||
miaoshu = item.flowdes,
|
||||
deitdate = item.deitdate.ToString("yyyy-MM-dd"),
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return PageVO<FlowListItem>.Of(items, ordered.Count, pageIndex, pageSize);
|
||||
}
|
||||
|
||||
public async Task<PageVO<StepListItem>> StepListAsync(string name, int serid)
|
||||
{
|
||||
var steps = await _flowstep.FindAsync(a => a.flowid == serid);
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
steps = steps.Where(m => m.StepName != null && m.StepName.IndexOf(name) >= 0).ToList();
|
||||
}
|
||||
|
||||
var ordered = steps.OrderBy(m => m.SerID).ToList();
|
||||
var items = new List<StepListItem>();
|
||||
foreach (var item in ordered)
|
||||
{
|
||||
items.Add(new StepListItem
|
||||
{
|
||||
SerID = item.SerID.ToString(),
|
||||
StepName = item.StepName,
|
||||
flowid = item.flowid?.ToString(),
|
||||
prestep = item.prestep,
|
||||
TeaName = await GetTeaNamesByStepAsync(item.SerID),
|
||||
Teaid = await GetTeaIdsByStepAsync(item.SerID),
|
||||
istrue = steps.Any(a => a.prestep == item.SerID),
|
||||
});
|
||||
}
|
||||
|
||||
return PageVO<StepListItem>.Of(items, ordered.Count, 0, ordered.Count == 0 ? 20 : ordered.Count);
|
||||
}
|
||||
|
||||
public async Task CreateAsync(Common_flowinfo model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
|
||||
var flowname = model.flowname?.Trim();
|
||||
var dup = await _flowinfo.AnyAsync(a =>
|
||||
a.schSerID == model.schSerID && a.flowType == model.flowType &&
|
||||
a.flowname != null && a.flowname.Trim() == flowname && a.enable != 3);
|
||||
if (dup) throw new DException("流程名不能重名!");
|
||||
|
||||
var info = new Common_flowinfo
|
||||
{
|
||||
flowname = model.flowname,
|
||||
deptid = model.deptid,
|
||||
typeid = model.typeid,
|
||||
flowdes = model.flowdes,
|
||||
enable = model.enable,
|
||||
flowType = model.flowType,
|
||||
deitdate = DateTime.Now,
|
||||
schSerID = model.schSerID,
|
||||
};
|
||||
|
||||
await _flowinfo.AddAsync(info);
|
||||
await _flowinfo.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task EditAsync(Common_flowinfo model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
|
||||
var flowname = model.flowname?.Trim();
|
||||
var dup = (await _flowinfo.FindAsync(a =>
|
||||
a.schSerID == model.schSerID && a.flowType == DocPrintConstants.AppCode &&
|
||||
a.flowname != null && a.flowname.Trim() == flowname && a.enable != 3)).Count;
|
||||
if (dup > 1) throw new DException("流程名不能重名!");
|
||||
|
||||
var info = await _flowinfo.FirstOrDefaultTrackedAsync(a => a.SerID == model.SerID);
|
||||
if (info == null) throw new DException("未找到要修改的流程!");
|
||||
|
||||
info.flowname = model.flowname;
|
||||
info.deptid = model.deptid;
|
||||
info.typeid = model.typeid;
|
||||
info.flowdes = model.flowdes;
|
||||
info.enable = model.enable;
|
||||
|
||||
await _flowinfo.UpdateAsync(info);
|
||||
await _flowinfo.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task StepCreateAsync(Common_flowstep model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
|
||||
var info = new Common_flowstep
|
||||
{
|
||||
StepName = model.StepName,
|
||||
prestep = model.prestep,
|
||||
flowid = model.flowid,
|
||||
ordernum = 0,
|
||||
};
|
||||
await _flowstep.AddAsync(info);
|
||||
await _flowstep.SaveChangesAsync();
|
||||
|
||||
if (string.IsNullOrEmpty(model.SchSerID))
|
||||
{
|
||||
throw new DException("添加失败!");
|
||||
}
|
||||
|
||||
await AddGroupUsersAsync(info.SerID, model.flowid, model.SchSerID);
|
||||
}
|
||||
|
||||
public async Task StepEditAsync(Common_flowstep model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
|
||||
var info = await _flowstep.FirstOrDefaultTrackedAsync(a => a.SerID == model.SerID);
|
||||
if (info == null) throw new DException("未找到要修改的步骤!");
|
||||
|
||||
info.flowid = model.flowid;
|
||||
info.StepName = model.StepName;
|
||||
info.prestep = model.prestep;
|
||||
await _flowstep.UpdateAsync(info);
|
||||
|
||||
// 重写该步骤的人员绑定(先清后加)
|
||||
var existing = await _groupuser.FindTrackedAsync(a => a.usergroup == info.SerID);
|
||||
foreach (var itt in existing)
|
||||
{
|
||||
await _groupuser.RemoveAsync(itt);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(model.SchSerID))
|
||||
{
|
||||
await _groupuser.SaveChangesAsync();
|
||||
throw new DException("修改失败!");
|
||||
}
|
||||
|
||||
await AddGroupUsersAsync(info.SerID, info.flowid, model.SchSerID);
|
||||
}
|
||||
|
||||
private async Task AddGroupUsersAsync(int stepSerId, int? flowId, string userIds)
|
||||
{
|
||||
foreach (var idStr in userIds.Split(','))
|
||||
{
|
||||
if (!int.TryParse(idStr.Trim(), out var userId)) continue;
|
||||
await _groupuser.AddAsync(new Common_Groupuser
|
||||
{
|
||||
usergroup = stepSerId,
|
||||
flowid = flowId,
|
||||
userid = userId,
|
||||
IsOk = 0,
|
||||
});
|
||||
}
|
||||
await _groupuser.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string ids)
|
||||
{
|
||||
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.FindTrackedAsync(a => lids.Contains(a.SerID));
|
||||
foreach (var item in flows)
|
||||
{
|
||||
item.enable = 3;
|
||||
}
|
||||
await _flowinfo.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task StepDeleteAsync(decimal[] ids)
|
||||
{
|
||||
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.FindTrackedAsync(m => intIds.Contains(m.SerID));
|
||||
foreach (var item in objlist)
|
||||
{
|
||||
await _flowstep.RemoveAsync(item);
|
||||
}
|
||||
await _flowstep.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<List<BindStepItem>> BindstepListAsync(int serid)
|
||||
{
|
||||
var steps = await _flowstep.FindAsync(a => a.flowid == serid);
|
||||
return steps.OrderBy(a => a.prestep)
|
||||
.Select(a => new BindStepItem { SerID = a.SerID, StepName = a.StepName })
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<List<BindGroupuserItem>> BindGroupuserListAsync(int flowid, int prestep)
|
||||
{
|
||||
var steps = await _flowstep.FindAsync(a => a.flowid == flowid && a.prestep == prestep);
|
||||
var result = new List<BindGroupuserItem>();
|
||||
foreach (var step in steps)
|
||||
{
|
||||
var users = await _groupuser.FindAsync(b => b.usergroup == step.SerID);
|
||||
foreach (var u in users)
|
||||
{
|
||||
result.Add(new BindGroupuserItem
|
||||
{
|
||||
StepId = step.SerID,
|
||||
userid = u.userid,
|
||||
StepName = step.StepName,
|
||||
TeaName = await GetTeaNameAsync(u.userid?.ToString()),
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<BindFlowItem>> BindflowinfoAsync(string schSerId, int flowType)
|
||||
{
|
||||
var flows = await _flowinfo.FindAsync(a => a.flowType == flowType && a.schSerID == schSerId && a.enable == 1);
|
||||
var result = new List<BindFlowItem>();
|
||||
foreach (var item in flows.OrderBy(m => m.SerID))
|
||||
{
|
||||
var chaoSend = new List<ChaoSendItem>();
|
||||
if (!string.IsNullOrEmpty(item.ChaoSend))
|
||||
{
|
||||
foreach (var uid in item.ChaoSend.Split(','))
|
||||
{
|
||||
if (string.IsNullOrEmpty(uid)) continue;
|
||||
chaoSend.Add(new ChaoSendItem
|
||||
{
|
||||
uid = uid,
|
||||
name = await GetTeaNameAsync(uid),
|
||||
photo = await GetTeaPhotoAsync(uid),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(new BindFlowItem
|
||||
{
|
||||
SerID = item.SerID.ToString(),
|
||||
flowname = item.flowname,
|
||||
ChaoSendArr = chaoSend,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 教师 / 人员
|
||||
|
||||
public async Task<List<TeaOption>> GetTeaListAsync(string schSerId)
|
||||
{
|
||||
var teas = await _sch.TeaInfo.GetSchTeaInfoAsync(schSerId, null);
|
||||
return teas.Select(a => new TeaOption { value = ParseTeaId(a.TeaSerId), label = a.Name }).ToList();
|
||||
}
|
||||
|
||||
public Task<List<TeaOption>> GetTeaListSupAsync(string schSerId)
|
||||
=> GetTeaListAsync(schSerId);
|
||||
|
||||
public async Task<PageVO<TeaInfoVo>> GetUserinfoAsync(string schSerId, string name, int statetype, int pageSize, int pageIndex)
|
||||
{
|
||||
var teas = await _sch.TeaInfo.GetSchTeaInfoAsync(schSerId, null);
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
teas = teas.Where(m => m.Name != null && m.Name.IndexOf(name) >= 0).ToList();
|
||||
}
|
||||
if (statetype > -1)
|
||||
{
|
||||
teas = teas.Where(m => m.IsLeadingMiddle == statetype).ToList();
|
||||
}
|
||||
|
||||
var ordered = teas.OrderBy(m => ParseTeaId(m.TeaSerId)).ToList();
|
||||
var items = ordered.Skip(pageSize * pageIndex).Take(pageSize).ToList();
|
||||
return PageVO<TeaInfoVo>.Of(items, ordered.Count, pageIndex, pageSize);
|
||||
}
|
||||
|
||||
public async Task<string[]> GetNoLeadingMiddleInfoAsync(string schSerId)
|
||||
{
|
||||
var teas = await _sch.TeaInfo.GetSchTeaInfoAsync(schSerId, null);
|
||||
return teas.Where(m => m.IsLeadingMiddle != 1)
|
||||
.Select(m => m.TeaSerId)
|
||||
.Where(id => !string.IsNullOrEmpty(id))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public Task<string[]> GetNoLeadingMiddleInfoSupAsync(string schSerId)
|
||||
=> GetNoLeadingMiddleInfoAsync(schSerId);
|
||||
|
||||
public Task ChangeLeadingMiddleStatusAsync(int state, string userTeaSerIdStr)
|
||||
{
|
||||
// 旧实现调用 OATea/EditIntheBearingStatus 写教师状态;教务 SDK 未提供该写接口,降级拒绝。
|
||||
throw new DException("该功能依赖教师状态写接口,教务 SDK 暂未提供,无法修改中层领导状态!");
|
||||
}
|
||||
|
||||
public async Task<List<FileListItem>> FilelistAsync(string ids)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ids)) return new List<FileListItem>();
|
||||
|
||||
var idStrings = ids.Split(',');
|
||||
var all = await _upfiles.FindAsync(a => true);
|
||||
return all.Where(a => idStrings.Contains(a.SerID.ToString()))
|
||||
.Select(a => new FileListItem
|
||||
{
|
||||
SerID = a.SerID,
|
||||
FileTrueName = string.IsNullOrEmpty(a.FileTrueName) ? a.FileTrueName : a.FileTrueName.Split('.')[0],
|
||||
FileName = a.FileName,
|
||||
FileSize = a.FileSize,
|
||||
FileType = a.FileType,
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 流程判断 / 用户
|
||||
|
||||
public async Task<ShenheFlowResult> IsShenheflowAsync(string schSerId, int userId)
|
||||
{
|
||||
var stepFlowIds = (await _flowstep.FindAsync(s => true))
|
||||
.Select(s => s.flowid).Where(f => f.HasValue).Select(f => f.Value).Distinct().ToHashSet();
|
||||
|
||||
var flowlist = await _flowinfo.FindAsync(a =>
|
||||
a.schSerID == schSerId && a.flowType == DocPrintConstants.AppCode && a.enable == 1);
|
||||
var hasFixed = flowlist.Any(a => stepFlowIds.Contains(a.SerID));
|
||||
|
||||
var result = new ShenheFlowResult { flowflag = hasFixed, freeflowlist = new List<FreeFlowItem>() };
|
||||
|
||||
if (!hasFixed)
|
||||
{
|
||||
var freeflowlist = await _freeflowinfo.FindAsync(a => a.userid == userId && a.flowType == DocPrintConstants.AppCode);
|
||||
foreach (var a in freeflowlist)
|
||||
{
|
||||
result.freeflowlist.Add(new FreeFlowItem
|
||||
{
|
||||
shenheuserid = a.shenheuserid,
|
||||
teamnamestr = await GetTeaNamesStrAsync(a.shenheuserid),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<SendUserItem>> IsSendUserAsync(string schSerId, int userId)
|
||||
{
|
||||
var result = new List<SendUserItem>();
|
||||
var senduserlist = await _senduser.FirstOrDefaultAsync(a => a.userid == userId && a.flowType == DocPrintConstants.AppCode);
|
||||
if (senduserlist == null || string.IsNullOrEmpty(senduserlist.Senduserid))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var userids = senduserlist.Senduserid.Split(',')
|
||||
.Where(a => !string.IsNullOrEmpty(a) && a != "undefined" && int.TryParse(a, out _))
|
||||
.Select(int.Parse).ToArray();
|
||||
|
||||
if (userids.Length == 0) return result;
|
||||
|
||||
var teas = await _sch.TeaInfo.GetArrTeaInfoAsync(string.Join(",", userids));
|
||||
return teas.Select(a => new SendUserItem
|
||||
{
|
||||
uid = ParseTeaId(a.TeaSerId),
|
||||
photo = a.Photo,
|
||||
name = a.Name,
|
||||
status = false,
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public Task<CurrentUserInfo> GetUserAsync()
|
||||
{
|
||||
var user = UserContextHolder.Current;
|
||||
return Task.FromResult(new CurrentUserInfo
|
||||
{
|
||||
Id = user?.Id ?? 0,
|
||||
UserName = user?.UserName,
|
||||
Account = user?.Account,
|
||||
SchoolId = user?.SchoolId,
|
||||
SchoolName = user?.SchoolName,
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<int> GetUserFlowRoleAsync(int userId)
|
||||
{
|
||||
var fixFlowIds = (await _flowinfo.FindAsync(f => f.enable == 1 && f.flowType == DocPrintConstants.AppCode))
|
||||
.Select(f => f.SerID).ToHashSet();
|
||||
var fixFlow = await _groupuser.AnyAsync(a => a.userid == userId && a.flowid.HasValue && fixFlowIds.Contains(a.flowid.Value));
|
||||
|
||||
var freeFlowIds = (await _freeflowinfo.FindAsync(f => f.flowType == DocPrintConstants.AppCode))
|
||||
.Select(f => f.SerID).ToHashSet();
|
||||
var freeFlow = await _freegroup.AnyAsync(a => a.userid == userId && a.flowid.HasValue && freeFlowIds.Contains(a.flowid.Value));
|
||||
|
||||
return (fixFlow || freeFlow) ? 1 : 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 教师名称/头像辅助
|
||||
|
||||
private static int ParseTeaId(string teaSerId)
|
||||
=> int.TryParse(teaSerId, out var id) ? id : 0;
|
||||
|
||||
private async Task<string> GetTeaNameAsync(string teaId)
|
||||
{
|
||||
if (!int.TryParse(teaId, out var id)) return "";
|
||||
var tea = await _sch.TeaInfo.GetTeaInfoAsync(id);
|
||||
return tea?.Name ?? "";
|
||||
}
|
||||
|
||||
private async Task<string> GetTeaPhotoAsync(string teaId)
|
||||
{
|
||||
if (!int.TryParse(teaId, out var id)) return "";
|
||||
var tea = await _sch.TeaInfo.GetTeaInfoAsync(id);
|
||||
return tea?.Photo ?? "";
|
||||
}
|
||||
|
||||
private async Task<string> GetTeaIdsByStepAsync(int stepSerId)
|
||||
{
|
||||
var ids = (await _groupuser.FindAsync(a => a.usergroup == stepSerId))
|
||||
.Select(a => a.userid).Where(u => u.HasValue).Select(u => u.Value).ToList();
|
||||
return string.Join(",", ids);
|
||||
}
|
||||
|
||||
private async Task<string> GetTeaNamesByStepAsync(int stepSerId)
|
||||
{
|
||||
var ids = (await _groupuser.FindAsync(a => a.usergroup == stepSerId))
|
||||
.Select(a => a.userid).Where(u => u.HasValue).Select(u => u.Value).ToList();
|
||||
return await GetTeaNamesByIdsAsync(ids);
|
||||
}
|
||||
|
||||
private async Task<string> GetTeaNamesByIdsAsync(IEnumerable<int> ids)
|
||||
{
|
||||
var list = ids.Distinct().ToList();
|
||||
if (list.Count == 0) return "";
|
||||
var teas = await _sch.TeaInfo.GetArrTeaInfoAsync(string.Join(",", list));
|
||||
return string.Join(",", teas.Select(t => t.Name).Where(n => !string.IsNullOrEmpty(n)));
|
||||
}
|
||||
|
||||
private async Task<string> GetTeaNamesStrAsync(string commaIds)
|
||||
{
|
||||
if (string.IsNullOrEmpty(commaIds)) return "";
|
||||
var ids = commaIds.Split(',')
|
||||
.Where(s => int.TryParse(s, out _)).Select(int.Parse).Distinct().ToList();
|
||||
if (ids.Count == 0) return "";
|
||||
return await GetTeaNamesByIdsAsync(ids);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
|
||||
namespace YhCore.Service.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 公共字典服务(对应旧 BestSchool.Service.GetSysDict)。
|
||||
/// </summary>
|
||||
public interface IDictionaryService
|
||||
{
|
||||
/// <summary>指定字典下拉框列表({k=CodeID, v=Name},Enable=1)。</summary>
|
||||
Task<List<DicOption>> GetDictListAsync(string typeId, string schId);
|
||||
|
||||
/// <summary>字典类别列表(排除 DEPT)。</summary>
|
||||
Task<List<DicOption>> GetDicTypeListAsync();
|
||||
|
||||
/// <summary>字典管理列表(含已删除,供超管管理)。</summary>
|
||||
Task<List<DicManageItem>> GetDictMangeListAsync(string typeId, string schId);
|
||||
|
||||
/// <summary>添加字典(typeID/typeName/Name/Schserid 由调用方填好,此处计算 Sort/Des/Enable/CodeID)。</summary>
|
||||
Task AddDicAsync(Common_Dictionary model);
|
||||
|
||||
/// <summary>更新字典(Name/Des/Sort/Enable)。</summary>
|
||||
Task UpdateDicAsync(Common_Dictionary model);
|
||||
|
||||
/// <summary>编辑字典名称(Name/Code/Path)。</summary>
|
||||
Task EditDicAsync(Common_Dictionary model);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
|
||||
namespace YhCore.Service.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 文印分类 / 印制版面 / 公共类别服务(对应旧 BestSchool.DocPrint.WY_list 分类部分 + WY_Classify)。
|
||||
/// </summary>
|
||||
public interface IDocPrintClassifyService
|
||||
{
|
||||
/// <summary>添加文印分类(typeName 固定「印制分类」)。</summary>
|
||||
Task AddDicAsync(Common_Dictionary model);
|
||||
|
||||
/// <summary>文印分类列表。</summary>
|
||||
Task<List<CategoryItem>> CategoryListAsync(string schSerId, string name);
|
||||
|
||||
/// <summary>添加印制版面(typeName 固定「印制版面」)。</summary>
|
||||
Task AddYZBMAsync(Common_Dictionary model);
|
||||
|
||||
/// <summary>印制版面列表。</summary>
|
||||
Task<List<CategoryItem>> YZBMListAsync(string schSerId, string name);
|
||||
|
||||
/// <summary>删除文印分类/版面(有使用记录则拒绝)。</summary>
|
||||
Task DeleteDicAsync(decimal[] ids);
|
||||
|
||||
/// <summary>公共类别列表(含排序/显隐)。</summary>
|
||||
Task<List<DictionaryTypeItem>> DictionaryTypeListAsync(string schSerId, string name, bool showAll, string typeId);
|
||||
|
||||
/// <summary>删除公共类别(软删除 Enable=3)。</summary>
|
||||
Task DeleteDictionaryTypeAsync(string[] ids);
|
||||
|
||||
/// <summary>公共类别显隐。</summary>
|
||||
Task SetDictionaryOrdAsync(string schSerId, int dicSerId, bool show);
|
||||
|
||||
/// <summary>公共类别排序。</summary>
|
||||
Task OrderDicAsync(string ids, string schSerId);
|
||||
|
||||
/// <summary>结果链接(依赖资源平台 ziyuan,新项目未接入,降级返回 false)。</summary>
|
||||
Task<object> GetResultUrlAsync(int dataId, int uid, int schId, string schSerId);
|
||||
|
||||
/// <summary>学校文印资源开关。</summary>
|
||||
Task<bool> GetSchZyBitAsync(string schSerId);
|
||||
|
||||
/// <summary>设置学校文印资源开关。</summary>
|
||||
Task SetSchZyBitAsync(string schSerId, bool zyBit);
|
||||
|
||||
/// <summary>待审数量角标。</summary>
|
||||
Task<int> GetShnheCountAsync(string userId, string schSerId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using YhCore.Model.Dto;
|
||||
|
||||
namespace YhCore.Service.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 文印统计看板服务(移植旧 BestSchool.DocPrint.DocPrintStatistics,5 接口)。
|
||||
/// 旧 [NoAuth] 无用户上下文,schId 由前端传入;新库 SchSerID 已是 int 字符串,直接 schId.ToString() 匹配。
|
||||
/// roleId 参数旧实现零引用,已删除。ExportRecord(Excel 导出,NPOI)延后。
|
||||
/// </summary>
|
||||
public interface IDocPrintStatisticsService
|
||||
{
|
||||
Task<List<DocPrintCountItem>> GetDocPrintCountAsync(int schId, DateTime? startDate = null, DateTime? endDate = null);
|
||||
Task<List<PieChartSeries>> GetPieChartAsync(int schId, DateTime? startDate = null, DateTime? endDate = null);
|
||||
Task<List<DocPrintTypeItem>> GetDocPrintTypeAsync(int schId);
|
||||
Task<DocPrintListResult> GetDocPrintListAsync(int schId, string docType, string likeName, int pageIndex, int pageSize, DateTime? startDate = null, DateTime? endDate = null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using YhCore.Model.Dto;
|
||||
|
||||
namespace YhCore.Service.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 公共文件服务(移植旧 BestSchool.Service.Common.GetEnclosure/FileSaveEnclosure +
|
||||
/// 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 在线预览。
|
||||
/// </summary>
|
||||
public interface IFileService
|
||||
{
|
||||
/// <summary>按附件 id 取文件信息(旧 GetEnclosure:Common_UpFiles → FileObj)。</summary>
|
||||
Task<FileObj> GetEnclosureAsync(int id);
|
||||
|
||||
/// <summary>存储附件信息(旧 FileSaveEnclosure:Common_UpFiles 落库,返回 SerID;文件名非法字符替换为 _)。</summary>
|
||||
Task<int> SaveEnclosureAsync(FileObj file, string appType);
|
||||
|
||||
/// <summary>单文件上传(旧 upload.FileSave:扩展名白名单 + 保存 + 返回 FileObj)。</summary>
|
||||
Task<FileObj> SaveFileAsync(string appType, IFormFile file);
|
||||
|
||||
/// <summary>图片多文件上传(旧 upload.FileSaveImage:jpg/gif/png/jpeg)。</summary>
|
||||
Task<List<FileObj>> SaveFileImageAsync(string appType, IFormFileCollection files);
|
||||
|
||||
/// <summary>保存分片(旧 FileChunkSave:临时目录 {guid}/ 下按索引存块)。</summary>
|
||||
Task SaveChunkAsync(string guid, string chunkIndex, IFormFile chunk);
|
||||
|
||||
/// <summary>合并分片(旧 FileMergeSave:按块合并 → 正式目录 + FileObj)。</summary>
|
||||
Task<FileObj> MergeSaveAsync(string appType, string guid, string fileName);
|
||||
|
||||
/// <summary>相对路径 → 物理路径(fileurl 以 /upload/ 开头 → Config.FilePath + 子路径)。</summary>
|
||||
string MapPhysicalPath(string fileUrl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using YhCore.Model.Dto;
|
||||
|
||||
namespace YhCore.Service.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 自由审批流服务(移植旧 BestSchool.Service.ApproveFlow)。
|
||||
/// 教师查询走教务 SDK(Yh.SchClass.OpenApi)。
|
||||
/// </summary>
|
||||
public interface IFreeApproveService
|
||||
{
|
||||
/// <summary>获取自己的自由流程节点(dataid 为空时取最大 dataid)。</summary>
|
||||
Task<List<FreeFlowNode>> GetFleeFlowAsync(int userId, int? dataid);
|
||||
|
||||
/// <summary>保存自由流程审核人,返回首步骤 SerID。</summary>
|
||||
Task<int> EditFleeFlowAsync(int userId, string schSerId, int? dataid, string flowUids, bool isFlowNull);
|
||||
|
||||
/// <summary>添加审核意见(写 flowtranctInfo,actionstatus=-1)。</summary>
|
||||
Task<string> AddShenheRemarkAsync(int userId, int? dataid, string remark, int freestepid);
|
||||
|
||||
/// <summary>获取 freestepid 之后的自由流程节点。</summary>
|
||||
Task<List<FreeFlowNode>> GetAfterFleeFlowAsync(int? dataid, int freestepid);
|
||||
|
||||
/// <summary>编辑 freestepid 之后的自由流程,返回末步骤 SerID。</summary>
|
||||
Task<int> EditAfterFleeFlowAsync(int? dataid, int freestepid, string flowUids);
|
||||
|
||||
/// <summary>查看发起人 + 审核人步骤(自由 / 固定 / 仅记录)。</summary>
|
||||
Task<List<FreeShenheInfo>> ShenheFlowAsync(int uid, string ctime, int dataid, int? freestepid, int? flowid);
|
||||
|
||||
/// <summary>抄送人列表(按 uids 查用户)。</summary>
|
||||
Task<List<ChaoSendUser>> ChaoSendListAsync(string uids);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Threading.Tasks;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Vo;
|
||||
|
||||
namespace YhCore.Service.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 日志查询服务(对应 Java 版 ILogService)。
|
||||
/// </summary>
|
||||
public interface ILogService
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页搜索系统审计日志(sys_operation_log,按 CreateTime 倒序)。
|
||||
/// </summary>
|
||||
/// <param name="schId">学校 ID(多校隔离)</param>
|
||||
/// <param name="keyword">关键字(可为空;模糊匹配用户名/URI/类方法/参数/异常/操作类型)</param>
|
||||
/// <param name="systemScope">true=系统操作日志(systemSearch,全量);false=普通操作日志(search,排除查询类)</param>
|
||||
/// <param name="page">页码,从 0 开始</param>
|
||||
/// <param name="size">每页大小</param>
|
||||
Task<PageVO<SysOperationLog>> SearchAsync(int schId, string keyword, bool systemScope, int page, int size);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Yh.SchClass.OpenApi.Model;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Model.Vo;
|
||||
|
||||
namespace YhCore.Service.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 审核流程管理服务(对应旧 BestSchool.Service.Reviewprocess + UserFlowRole)。
|
||||
/// </summary>
|
||||
public interface IReviewProcessService
|
||||
{
|
||||
/// <summary>审核流程列表(分页)。</summary>
|
||||
Task<PageVO<FlowListItem>> ListAsync(string name, int pageSize, int pageIndex, int flowType, bool edit, string schSerId);
|
||||
|
||||
/// <summary>审核流程列表【局版】(新框架无 groupId,降级为与 List 同源)。</summary>
|
||||
Task<PageVO<FlowListItem>> List2Async(string name, int pageSize, int pageIndex, int flowType, bool edit, string schSerId);
|
||||
|
||||
/// <summary>步骤流程列表。</summary>
|
||||
Task<PageVO<StepListItem>> StepListAsync(string name, int serid);
|
||||
|
||||
/// <summary>添加审核流程。</summary>
|
||||
Task CreateAsync(Common_flowinfo model);
|
||||
|
||||
/// <summary>修改审核流程。</summary>
|
||||
Task EditAsync(Common_flowinfo model);
|
||||
|
||||
/// <summary>添加步骤流程。</summary>
|
||||
Task StepCreateAsync(Common_flowstep model);
|
||||
|
||||
/// <summary>修改步骤流程。</summary>
|
||||
Task StepEditAsync(Common_flowstep model);
|
||||
|
||||
/// <summary>删除审核流程(软删除 enable=3)。</summary>
|
||||
Task DeleteAsync(string ids);
|
||||
|
||||
/// <summary>删除步骤。</summary>
|
||||
Task StepDeleteAsync(decimal[] ids);
|
||||
|
||||
/// <summary>流程绑定的步骤列表。</summary>
|
||||
Task<List<BindStepItem>> BindstepListAsync(int serid);
|
||||
|
||||
/// <summary>绑定步骤的人员。</summary>
|
||||
Task<List<BindGroupuserItem>> BindGroupuserListAsync(int flowid, int prestep);
|
||||
|
||||
/// <summary>绑定流程(流程 + 抄送人)。</summary>
|
||||
Task<List<BindFlowItem>> BindflowinfoAsync(string schSerId, int flowType);
|
||||
|
||||
/// <summary>当前学校教师列表。</summary>
|
||||
Task<List<TeaOption>> GetTeaListAsync(string schSerId);
|
||||
|
||||
/// <summary>学校 + 教育局教师列表(降级为当前学校)。</summary>
|
||||
Task<List<TeaOption>> GetTeaListSupAsync(string schSerId);
|
||||
|
||||
/// <summary>教师信息分页。</summary>
|
||||
Task<PageVO<TeaInfoVo>> GetUserinfoAsync(string schSerId, string name, int statetype, int pageSize, int pageIndex);
|
||||
|
||||
/// <summary>非中层领导人员(teaId 数组)。</summary>
|
||||
Task<string[]> GetNoLeadingMiddleInfoAsync(string schSerId);
|
||||
|
||||
/// <summary>非中层领导人员【局版】(降级为当前学校)。</summary>
|
||||
Task<string[]> GetNoLeadingMiddleInfoSupAsync(string schSerId);
|
||||
|
||||
/// <summary>设置/取消中层领导(教务 SDK 未提供写接口,暂不支持)。</summary>
|
||||
Task ChangeLeadingMiddleStatusAsync(int state, string userTeaSerIdStr);
|
||||
|
||||
/// <summary>附件数据源。</summary>
|
||||
Task<List<FileListItem>> FilelistAsync(string ids);
|
||||
|
||||
/// <summary>该校审核流程是固定还是自由。</summary>
|
||||
Task<ShenheFlowResult> IsShenheflowAsync(string schSerId, int userId);
|
||||
|
||||
/// <summary>绑定抄送默认人。</summary>
|
||||
Task<List<SendUserItem>> IsSendUserAsync(string schSerId, int userId);
|
||||
|
||||
/// <summary>当前用户基础信息。</summary>
|
||||
Task<CurrentUserInfo> GetUserAsync();
|
||||
|
||||
/// <summary>用户流程角色(1 有固定/自由流程,0 无)。</summary>
|
||||
Task<int> GetUserFlowRoleAsync(int userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Model.Vo;
|
||||
|
||||
namespace YhCore.Service.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 文印核心业务服务(移植旧 BestSchool.DocPrint.WY_list 除分类/字典外的部分:
|
||||
/// 抄送、附件、文印 CRUD、各类列表、审核动作、统计、指派)。
|
||||
/// </summary>
|
||||
public interface IWYDocPrintService
|
||||
{
|
||||
// ---- 抄送 ----
|
||||
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);
|
||||
|
||||
// ---- 附件 ----
|
||||
Task<List<DocPrintFileItem>> FileListAsync(string ids);
|
||||
|
||||
// ---- 文印 CRUD ----
|
||||
Task<int> CreateAsync(WYDocPrintSaveDto model);
|
||||
Task<int> EditAsync(WYDocPrintSaveDto model);
|
||||
Task DeleteAsync(string ids);
|
||||
Task<string> RevokeAsync(FlowTranctDto model);
|
||||
|
||||
// ---- 列表 ----
|
||||
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);
|
||||
|
||||
// ---- 审核动作 ----
|
||||
Task<string> ShenheOkAsync(FlowTranctDto model);
|
||||
Task<string> AffirmApplyforOkAsync(FlowTranctDto model);
|
||||
|
||||
// ---- 指派 ----
|
||||
Task<string> GetUserZhipaiAsync(int userid, string schSerId, int? flowid, int? flowstepid, string type);
|
||||
|
||||
// ---- 统计 ----
|
||||
Task<List<MonthStatItem>> List1Async(string starttime, string endtime, string schSerId);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
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;
|
||||
|
||||
namespace YhCore.Service.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 审核角色业务服务(移植旧 BestSchool.DocPrint.WY_SHRoleP,45 接口)。
|
||||
/// 降级说明:
|
||||
/// 1. 旧 SendMessage/SendUnread/Backlog(短信、已读未读、待办中心)新项目缺失,全部降级为纯落库;
|
||||
/// 2. 教师/头像改走教务 SDK(Yh.SchClass.OpenApi),教师标识用 int(TeaSerId 解析),
|
||||
/// 落库 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):下载类接口返回 (byte[] Content, string FileName),
|
||||
/// 控制器按扩展名包装 File();导入反馈表存进程内 IMemoryCache(单实例)。
|
||||
/// </summary>
|
||||
public interface IWYSHRoleService
|
||||
{
|
||||
// ---- 审核角色管理 ----
|
||||
Task<string> GetIsSHRoleAsync(string schSerId);
|
||||
Task<string> SaveIsSHRoleAsync(string schSerId, string val);
|
||||
Task<List<WYSHRoleExt>> GetSHRoleAsync(string schSerId);
|
||||
Task<string> UpdateSHRoleNameAsync(string schSerId, string userId, decimal serId, string roleName);
|
||||
Task<string> EditSHRoleAsync(string schSerId, string userId, WYSHRoleExt roleInfo);
|
||||
Task<string> EditSHTeaRowAsync(string schSerId, string userId, WY_SHRole_SHTea data);
|
||||
Task<string> DelSHTeaRowAsync(string schSerId, decimal serId);
|
||||
Task<string> EditSHTeaAsync(string schSerId, string userId, decimal shRoleId, List<WY_SHRole_SHTea> listSHTea);
|
||||
Task<string> EditBSHTeaAsync(string schSerId, string userId, decimal shRoleId, List<WY_SHRole_BSHTea> listBSHTea);
|
||||
Task<string> DelSHRoleAsync(string schSerId, List<decimal?> listSHRoleId);
|
||||
|
||||
// ---- 审核角色流程 ----
|
||||
Task<PageVO<SHRoleFlowListItem>> ListFlowAsync(string name, int pageSize, int pageIndex, string schSerId, int flowType, bool edit);
|
||||
Task<string> EditFlowAsync(Common_flowinfo model);
|
||||
Task<string> StepCreateOfSHRoleAsync(Common_flowstep model);
|
||||
Task<SHRoleStepInfo> GetStepInfoAsync(string schSerId, decimal flowid);
|
||||
Task<string> StepDeleteAsync(List<int> ids);
|
||||
Task<string> UpdateStepNameAsync(string schSerId, int serId, int flowid, string stepName, int cond, string isshowJE, string isshowBtn);
|
||||
Task<string> AddStepSHRoleAsync(string schSerId, int flowid, int stepid, decimal shRoleid);
|
||||
Task<string> DelStepSHRoleAsync(string schSerId, int stepid, decimal shRoleid);
|
||||
Task<SHRoleStepByJEResult> GetStepByJEAsync(string schSerId, string userId, int flowid, int je);
|
||||
|
||||
// ---- 文印申请页面 ----
|
||||
Task<List<WYFlowForUser>> GetLCOfUserAsync(string schSerId, string userId, string userName);
|
||||
Task<List<SendUserItem>> IsSendUserSHRoleAsync(string schSerId, int userId, int flowid);
|
||||
Task<List<Common_Dictionary>> GetDicInfoAsync(string schSerId, string typeId);
|
||||
Task<int> EditDocPrintAsync(string schSerId, string userId, WYSHRoleEditDocPrintDto model);
|
||||
|
||||
// ---- 用户记录查看和审核 ----
|
||||
Task<SHRoleShenheFlowResult> GetshenheFlowAsync(string schSerId, decimal docprintId, int flowid);
|
||||
Task<List<SHRoleDocPrintListItem>> MyListtoOneSHRoleAsync(int serId, string schSerId);
|
||||
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);
|
||||
|
||||
// ---- 规格表 / 签名 / 退回 / 金额 ----
|
||||
Task<SHRoleDocOfTableResult> GetDocOftableAsync(string schSerId, decimal docPrintId);
|
||||
Task<string> SaveSignatureAsync(string schSerId, string userId, string pic);
|
||||
Task<string> GetSignatureAsync(string schSerId, string userId);
|
||||
Task<string> DelSignatureAsync(string schSerId, string userId);
|
||||
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<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<RootNamespace>YhCore.Service</RootNamespace>
|
||||
<AssemblyName>YhCore.Service</AssemblyName>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- 文件上传:IFileService 使用 IFormFile / IFormFileCollection(ASP.NET Core 共享框架) -->
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\YhCore.Entity\YhCore.Entity.csproj" />
|
||||
<ProjectReference Include="..\YhCore.Model\YhCore.Model.csproj" />
|
||||
<ProjectReference Include="..\YhCore.Repository\YhCore.Repository.csproj" />
|
||||
<ProjectReference Include="..\YhCore.Common\YhCore.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- 业务异常 DException / ErrorCode / IErrorCode:从 Nexus nuget-group 引用(原为本地源项目引用) -->
|
||||
<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.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>
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Quartz;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Repository;
|
||||
|
||||
namespace YhCore.Tasks.Jobs
|
||||
{
|
||||
/// <summary>
|
||||
/// 审计日志清理 Job(对应 Java scheduler/SysOperationLogCleanupTask)。
|
||||
/// 定时删除 30 天前的系统审计日志(sys_operation_log 表)。
|
||||
/// </summary>
|
||||
public class CleanupLogJob : IJob
|
||||
{
|
||||
private const int BatchSize = 1000;
|
||||
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var services = (IServiceProvider)context.Scheduler.Context.Get("services");
|
||||
using var scope = services.CreateScope();
|
||||
var provider = scope.ServiceProvider;
|
||||
|
||||
var logger = provider.GetRequiredService<ILogger<CleanupLogJob>>();
|
||||
var repo = provider.GetRequiredService<IRepository<SysOperationLog>>();
|
||||
|
||||
var cutoff = DateTime.Now.AddDays(-30);
|
||||
logger.LogInformation("审计日志清理开始:删除 {Cutoff:yyyy-MM-dd HH:mm:ss} 之前的数据", cutoff);
|
||||
|
||||
try
|
||||
{
|
||||
var oldList = await repo.FindTrackedAsync(l => l.CreateTime < cutoff);
|
||||
if (oldList.Count == 0)
|
||||
{
|
||||
logger.LogInformation("审计日志清理完成:无 30 天前的数据");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation("审计日志清理:待删除 {Count} 条,分块提交(每批 {Batch} 条)", oldList.Count, BatchSize);
|
||||
|
||||
var removed = 0;
|
||||
foreach (var batch in Chunk(oldList, BatchSize))
|
||||
{
|
||||
foreach (var item in batch)
|
||||
{
|
||||
await repo.RemoveAsync(item);
|
||||
}
|
||||
removed += await repo.SaveChangesAsync();
|
||||
}
|
||||
|
||||
logger.LogInformation("审计日志清理完成:共删除 {Count} 条", removed);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 缺表/数据库异常时记录错误日志,不让 Job 崩溃(Quartz 连续失败会标记 Job 错误状态)
|
||||
logger.LogError(ex, "审计日志清理失败:{Message}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>将列表分块(net5.0 无 Enumerable.Chunk,自行实现)</summary>
|
||||
private static IEnumerable<List<T>> Chunk<T>(List<T> source, int size)
|
||||
{
|
||||
for (var i = 0; i < source.Count; i += size)
|
||||
{
|
||||
yield return source.GetRange(i, Math.Min(size, source.Count - i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Quartz;
|
||||
using YhCore.Tasks.Jobs;
|
||||
|
||||
namespace YhCore.Tasks
|
||||
{
|
||||
/// <summary>
|
||||
/// Quartz 调度宿主(对应 Java scheduler 包由 Spring 管理的调度器)。
|
||||
/// 承载框架内置 Job 模板;业务 Job 在 InitJobs 中追加注册。
|
||||
/// </summary>
|
||||
public class QuartzHostedService : IHostedService
|
||||
{
|
||||
private const string ServiceProviderKey = "services";
|
||||
|
||||
private readonly ISchedulerFactory _schedulerFactory;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private IScheduler _scheduler;
|
||||
|
||||
public QuartzHostedService(ISchedulerFactory schedulerFactory, IServiceProvider serviceProvider)
|
||||
{
|
||||
_schedulerFactory = schedulerFactory;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_scheduler = await _schedulerFactory.GetScheduler(cancellationToken);
|
||||
_scheduler.Context.Put(ServiceProviderKey, _serviceProvider); // Job 经此取 DI 服务
|
||||
|
||||
await InitJobs();
|
||||
|
||||
await _scheduler.Start(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_scheduler != null)
|
||||
{
|
||||
await _scheduler.Shutdown(waitForJobsToComplete: true, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InitJobs()
|
||||
{
|
||||
// ============ 定时任务注册 ============
|
||||
|
||||
// 1. 审计日志清理(对应 Java SysOperationLogCleanupTask:每日凌晨 1:00 删除 30 天前 sys_operation_log 数据;
|
||||
// 设定时间窗口 0-2 点,取 1:00 居中)
|
||||
await ScheduleJobAsync<CleanupLogJob>("cleanup-log-job", "0 0 1 * * ?");
|
||||
|
||||
// 2. 其他业务 Job 在此追加注册(示例:每 10 分钟执行一次)
|
||||
// await ScheduleJobAsync<TJob>("job-name", "0 */10 * * * ?");
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>注册一个 Cron 定时任务</summary>
|
||||
protected async Task ScheduleJobAsync<TJob>(string jobName, string cronExpression) where TJob : IJob
|
||||
{
|
||||
var job = JobBuilder.Create<TJob>()
|
||||
.WithIdentity(jobName)
|
||||
.Build();
|
||||
|
||||
var trigger = TriggerBuilder.Create()
|
||||
.WithIdentity(jobName + "-trigger")
|
||||
.WithCronSchedule(cronExpression)
|
||||
.Build();
|
||||
|
||||
await _scheduler.ScheduleJob(job, trigger);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Quartz;
|
||||
using Quartz.Impl;
|
||||
|
||||
namespace YhCore.Tasks
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务调度注册扩展:Quartz 调度器工厂 + 宿主服务(对应 Java scheduler 自动装配)。
|
||||
/// </summary>
|
||||
public static class QuartzServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddQuartzHosted(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<ISchedulerFactory, StdSchedulerFactory>();
|
||||
services.AddHostedService<QuartzHostedService>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<RootNamespace>YhCore.Task</RootNamespace>
|
||||
<AssemblyName>YhCore.Task</AssemblyName>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- 任务调度:对应 Java Quartz / scheduler 包 -->
|
||||
<PackageReference Include="Quartz" Version="3.3.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\YhCore.Common\YhCore.Common.csproj" />
|
||||
<ProjectReference Include="..\YhCore.Entity\YhCore.Entity.csproj" />
|
||||
<ProjectReference Include="..\YhCore.Repository\YhCore.Repository.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,346 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace YhCore.WebApi.Auth
|
||||
{
|
||||
/// <summary>
|
||||
/// 自研 CAS 远程认证 handler(本地化自 Yh.Platform.Auth,改造为平台旧版登录 URL 格式):
|
||||
/// <list type="bullet">
|
||||
/// <item>Challenge:302 到 CAS 登录页,service = {AppBaseUrl}{当前 path}[?除 ticket 外 query],即原访问 URL,
|
||||
/// 与 Java notice 的 service 形态一致(service=http://host/module/api/xxx);AppBaseUrl 含模块前缀
|
||||
/// (如 /docprint)时拼接结果天然带模块名。显式回跳语境(前端 yschreferer/Referer 携带页面地址)时
|
||||
/// 把回跳地址以 Java notice 同款分号矩阵段插入 path 与 query 之间:
|
||||
/// {path};redirectToReferer=<hex(回跳地址)>[?{原 query}]——不再以 {SignInCallbackPath} 回调端点作
|
||||
/// service(signin-cas 形态已弃用);hex 按 char 维度编码(对齐前端 strToHexCharCode / Java HexUtils),
|
||||
/// 分号段不参与 query 结构,含中文/特殊字符的页面地址也可无歧义承载。ASP.NET Core 对回调中分号段
|
||||
/// 的处理:带 ticket 校验成功后由中间件短路(不落 MVC 路由),路径分号段仅用于解析回跳目标。</item>
|
||||
/// <item>Authenticate:任意受保护路径带 ticket 的回调均按「path + 除 ticket 外的 query」重建 service
|
||||
/// 调 /serviceValidate 校验(service 即登录时的原访问 URL,含分号段则一并重建,CAS 在末尾追加
|
||||
/// ?ticket=/&ticket= 回跳)。成功后写入 Cookie 登录态并 302 到分号段 redirectToReferer(hex 解码,
|
||||
/// 当前形态)/ query redirectToReferer(旧 query 形态兼容)/ returnUrl(更早中继形态兼容)/
|
||||
/// 原 URL 去 ticket 版(无回跳目标);回跳目标 302 为纯解码地址,不追加任何参数。</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public class CasAuthenticationHandler : AuthenticationHandler<CasAuthenticationOptions>
|
||||
{
|
||||
/// <summary>旧中继形态(service={回调端点}?returnUrl=…)的回跳参数——已弃用,仅兼容历史 URL/缓存。</summary>
|
||||
private const string ReturnUrlParam = "returnUrl";
|
||||
|
||||
/// <summary>旧 query 形态(service=原访问 URL?redirectToReferer=<hex>,986b241 曾用)的回跳参数——
|
||||
/// 已改分号矩阵段承载,此处仅兼容读取历史 URL/缓存。</summary>
|
||||
private const string RedirectToRefererQueryParam = "redirectToReferer";
|
||||
|
||||
private readonly CasServiceTicketValidator _validator;
|
||||
|
||||
public CasAuthenticationHandler(
|
||||
IOptionsMonitor<CasAuthenticationOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
ISystemClock clock,
|
||||
CasServiceTicketValidator validator)
|
||||
: base(options, logger, encoder, clock)
|
||||
{
|
||||
_validator = validator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按当前请求重建登录时的 service:{root}{path}[?{除 ticket 外的 query}],即原访问 URL(notice 形态)。
|
||||
/// CAS 回调会在原 service 后追加 ?ticket=(已有 query 时 &ticket=),校验时必须还原为登录时的
|
||||
/// 原始 service,故剥掉 ticket;其余参数按 EscapeDataString 重新编码,与 Challenge 时构造保持一致。
|
||||
/// path 取 ResolveOriginalPath():分号段被 PathMatrixSegmentMiddleware 从 Request.Path 剥离后,
|
||||
/// 重建必须用 Items 保存的原始路径(含 ;redirectToReferer=<hex> 段),与登录 service 逐字节一致;
|
||||
/// {root} 取自 AppBaseUrl(含 /docprint 等模块前缀),拼接结果天然带模块名,不受反向代理剥离前缀影响。
|
||||
/// Challenge 侧构造新 service 用当前(已剥离的)路由路径,目标以分号段另行追加,见 HandleChallengeAsync。
|
||||
/// </summary>
|
||||
private string BuildServiceUrlFromOriginalPath()
|
||||
{
|
||||
return BuildServiceUrl(ResolveOriginalPath());
|
||||
}
|
||||
|
||||
/// <summary>Challenge 侧 service 基准:当前(已剥离分号段的)路由路径 + 除 ticket 外 query。</summary>
|
||||
private string BuildServiceUrlFromCurrentPath()
|
||||
{
|
||||
return BuildServiceUrl(Context.Request.Path.Value);
|
||||
}
|
||||
|
||||
private string BuildServiceUrl(string path)
|
||||
{
|
||||
var query = Context.Request.Query
|
||||
.Where(kv => !string.Equals(kv.Key, "ticket", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(kv => Uri.EscapeDataString(kv.Key) + "=" + Uri.EscapeDataString(kv.Value.ToString()))
|
||||
.ToList();
|
||||
var service = BuildCallbackRoot() + path;
|
||||
return query.Count > 0 ? service + "?" + string.Join("&", query) : service;
|
||||
}
|
||||
|
||||
/// <summary>原始完整路径(含 ;redirectToReferer= 分号段):优先 PathMatrixSegmentMiddleware 存入
|
||||
/// Items 的值,未经过该中间件时回退当前请求路径。</summary>
|
||||
private string ResolveOriginalPath()
|
||||
{
|
||||
if (Context.Items.TryGetValue(YhCasDefaults.OriginalPathItemKey, out var v)
|
||||
&& v is string original && !string.IsNullOrEmpty(original))
|
||||
{
|
||||
return original;
|
||||
}
|
||||
return Context.Request.Path.Value ?? "/";
|
||||
}
|
||||
|
||||
/// <summary>取回调 query 中的 returnUrl(旧中继形态兼容:service={回调端点}?returnUrl=… 的历史 URL/缓存;
|
||||
/// 本地相对路径或同源完整 http(s) URL,非法返回 null)。</summary>
|
||||
private string ResolveReturnUrl()
|
||||
{
|
||||
if (!Context.Request.Query.TryGetValue(ReturnUrlParam, out var value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var url = value.ToString();
|
||||
return CasHex.IsAllowedRedirectTarget(url, AllowedHost()) ? url : null;
|
||||
}
|
||||
|
||||
/// <summary>读回调 query 的 redirectToReferer(当前形态:service=原访问 URL 时由 CAS 原样回带的
|
||||
/// hex 回跳地址)并解码;缺失或 hex 非法返回 null。合法性由调用方统一 IsAllowedReturnTarget 把关。</summary>
|
||||
private string ResolveQueryRedirectToReferer()
|
||||
{
|
||||
if (!Context.Request.Query.TryGetValue(RedirectToRefererQueryParam, out var value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return CasHex.Decode(value.ToString());
|
||||
}
|
||||
|
||||
/// <summary>service 根地址:优先 AppBaseUrl 配置,为空时从当前请求推断。</summary>
|
||||
private string BuildCallbackRoot()
|
||||
{
|
||||
var root = Options.AppBaseUrl?.TrimEnd('/');
|
||||
if (string.IsNullOrEmpty(root))
|
||||
{
|
||||
root = Context.Request.Scheme + "://" + Context.Request.Host.Value;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
|
||||
{
|
||||
var casBase = Options.CasServerUrlPrefix;
|
||||
if (string.IsNullOrWhiteSpace(casBase))
|
||||
{
|
||||
Response.StatusCode = 500;
|
||||
return Response.WriteAsync("未配置 CAS 服务端地址(yh:sso:cas-server-url-prefix)");
|
||||
}
|
||||
properties.Items.TryGetValue("returnUrl", out var returnUrl);
|
||||
var target = returnUrl ?? properties.RedirectUri;
|
||||
string service;
|
||||
if (string.IsNullOrEmpty(target))
|
||||
{
|
||||
// 无显式回跳语境(浏览器/工具直访受保护接口,未带 yschreferer / Referer):
|
||||
// service = 原访问 URL(AppBaseUrl 前缀 + 当前 path + 除 ticket 外 query),
|
||||
// 与 Java notice 的 service 形态一致(service=http://host/module/api/xxx,无附加参数)。
|
||||
// AppBaseUrl 带模块前缀(如 http://host/docprint)时,拼接结果天然含模块名,
|
||||
// 不受反向代理剥离前缀影响;登录成功后 CAS 回跳该 URL + ticket,由本 handler 校验并
|
||||
// 302 去掉 ticket 落回原地址。
|
||||
service = BuildServiceUrlFromCurrentPath();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 显式回跳语境(前端 yschreferer 携带页面地址等):service 仍 = 原访问 URL(notice 形态,
|
||||
// 不再以 {SignInCallbackPath} 回调端点作 service),把回跳地址以 Java notice 同款的分号矩阵段
|
||||
// 形式插入 path 与 query 之间:{path};redirectToReferer=<hex>[?{原 query}]——CAS 会把它作为
|
||||
// service 一部分在回调时原样带回,回调侧 PathMatrixSegmentMiddleware 剥离分号段后由
|
||||
// ResolveRedirectToReferer 从 Items 的 hex 解码即得回跳目标(query 形态 ?redirectToReferer=<hex>
|
||||
// 的旧 URL/缓存仍兼容读取)。hex 按 char 维度编码(对齐前端 strToHexCharCode / Java HexUtils);
|
||||
// 分号段内不参与 query 结构,含中文/特殊字符的页面地址也可无歧义承载。
|
||||
service = BuildServiceUrlFromCurrentPath();
|
||||
var q = service.IndexOf('?');
|
||||
var segment = YhCasDefaults.RedirectToRefererMarker + CasHex.Encode(target);
|
||||
service = q < 0
|
||||
? service + segment
|
||||
: service.Substring(0, q) + segment + service.Substring(q);
|
||||
}
|
||||
// Uri.EscapeDataString 会把 ; / : = 全部编码(%3B/%2F/%3A/%3D),与平台旧版系统生成的
|
||||
// CAS 登录 URL 逐字节一致(UrlEncoder.Encode 会保留 ; 字面量)
|
||||
var loginUrl = (casBase.EndsWith('/') ? casBase : casBase + "/") + "login?service=" + Uri.EscapeDataString(service);
|
||||
Response.Redirect(loginUrl);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
// 1. 已有 Cookie 登录态直接放行。
|
||||
// 例外:已登录状态下又收到带 ticket 的 CAS 回调(刷新/复用回调地址、自动登录型 CAS 对同一
|
||||
// service 重复签发)——票据对该会话已无意义(且常已被消费),不重走验票;若带回跳分号段则
|
||||
// 直接 302 到解码地址,避免落入 MVC 路由 404。Context.User 手动同步:
|
||||
// NoResult 返回时 AuthenticationMiddleware 不会赋值,SsoProtectionMiddleware 需据此识别已认证。
|
||||
var cookieResult = await Context.AuthenticateAsync(YhCasDefaults.CookieScheme);
|
||||
if (cookieResult.Succeeded && cookieResult.Principal != null)
|
||||
{
|
||||
var revisitTicket = Context.Request.Query["ticket"].ToString();
|
||||
var revisitReferer = ResolveRedirectToReferer();
|
||||
if (!string.IsNullOrEmpty(revisitTicket) && CasHex.IsAllowedRedirectTarget(revisitReferer, AllowedHost()))
|
||||
{
|
||||
Context.User = cookieResult.Principal;
|
||||
Response.Redirect(StripTicketParams(revisitReferer));
|
||||
return AuthenticateResult.NoResult();
|
||||
}
|
||||
return AuthenticateResult.Success(cookieResult.Ticket!);
|
||||
}
|
||||
|
||||
// 2. CAS 回调:校验 ticket
|
||||
var ticketValue = Context.Request.Query["ticket"].ToString();
|
||||
if (string.IsNullOrEmpty(ticketValue))
|
||||
{
|
||||
return AuthenticateResult.NoResult();
|
||||
}
|
||||
|
||||
// service 必须与登录时完全一致:CAS 是在登录 service 后追加 ?ticket=(已有 query 时 &ticket=)回调。
|
||||
// 分号段已被 PathMatrixSegmentMiddleware 从 Request.Path 剥离,须以 Items 保存的原始路径重建。
|
||||
var service = BuildServiceUrlFromOriginalPath();
|
||||
string loginName;
|
||||
try
|
||||
{
|
||||
loginName = await _validator.ValidateAsync(ticketValue, service, Context.RequestAborted);
|
||||
}
|
||||
catch (Yh.Platform.Auth.AuthException e)
|
||||
{
|
||||
return AuthenticateResult.Fail(e);
|
||||
}
|
||||
if (string.IsNullOrEmpty(loginName))
|
||||
{
|
||||
return AuthenticateResult.Fail("CAS ticket 校验失败或已失效");
|
||||
}
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.Name, loginName),
|
||||
new(ClaimTypes.NameIdentifier, loginName),
|
||||
new("loginName", loginName),
|
||||
};
|
||||
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
|
||||
// 写入 Cookie 登录态(会话级:IsPersistent=false,关闭浏览器即失效;票据过期由 SessionStore + ExpireTimeSpan 控制)
|
||||
var props = new AuthenticationProperties { IsPersistent = false };
|
||||
await Context.SignInAsync(YhCasDefaults.CookieScheme, principal, props);
|
||||
// 同步当前请求上下文身份:SignInAsync 只写 Cookie 不改 context.User,
|
||||
// 后续中间件(SsoProtectionMiddleware)需据此识别“刚登录成功”,避免被误判未认证再次 Challenge
|
||||
Context.User = principal;
|
||||
|
||||
// 回跳原地址:
|
||||
// 1. 路径分号段 ;redirectToReferer=<hex>(当前形态:service=原 URL;redirectToReferer=<hex>,
|
||||
// CAS 原样回带)→ 解码跳该目标;
|
||||
// 2. query redirectToReferer=<hex>(ff31293 前的 query 形态 URL/缓存,兼容);
|
||||
// 3. query returnUrl(更早中继形态 service={回调端点}?returnUrl=…,兼容);
|
||||
// 4. 都没有(service=纯净原访问 URL 的 notice 形态:CAS 在 service 后追加 ?ticket= 回跳,
|
||||
// 本请求 Path + 除 ticket 外 query 即登录时原 URL)→ 302 去掉一次性 ticket 落回原地址。
|
||||
// 允许本地相对路径或同源完整 http(s) URL;非法(外站 host / 协议相对等)回根路径,防开放重定向。
|
||||
// 回跳目标 302 即为纯解码地址(不追加 ticket 等任何参数)——票据已在本次 /serviceValidate
|
||||
// 消费(一次性),登录态由 Cookie 承载,前端页自行调用户接口感知登录即可。
|
||||
var referer = ResolveRedirectToReferer()
|
||||
?? ResolveQueryRedirectToReferer()
|
||||
?? ResolveReturnUrl();
|
||||
if (referer != null)
|
||||
{
|
||||
// 有显式回跳目标:302 到纯解码地址(不追加任何参数),并剥离目标上历史残留的
|
||||
// ticket 参数(SPA 的 yschreferer 携带 window.location.href,曾在死循环中累积
|
||||
// 多个 ticket 进 fragment;回跳时清掉,避免污染的地址在页面间继续传播)。
|
||||
var target = CasHex.IsAllowedRedirectTarget(referer, AllowedHost()) ? StripTicketParams(referer) : "/";
|
||||
Response.Redirect(target);
|
||||
return AuthenticateResult.NoResult();
|
||||
}
|
||||
|
||||
// 无显式回跳目标(无 referer 挑战:service=原访问 URL,CAS 原样带 ticket 回调):
|
||||
// - 根路径回调(如 /?ticket=ST-xxx):Cookie 已写入、Context.User 已同步,直接返回放行管道,
|
||||
// 由根端点当场渲染登录态页面(200,零重定向)——即"验票登录且不再重定向";
|
||||
// 刷新/重放该地址时走 Cookie 恢复(票据不再参与),不会重复验票失败 401,杜绝重定向循环。
|
||||
// - 其余路径回调:仍 302 去掉 ticket 的干净原地址(清理地址栏,防刷新重复验票)。
|
||||
var cleanUrl = BuildServiceUrlFromOriginalPath();
|
||||
if (IsRootPath(ResolveOriginalPath()) && CasHex.IsAllowedRedirectTarget(cleanUrl, AllowedHost()))
|
||||
{
|
||||
return AuthenticateResult.NoResult();
|
||||
}
|
||||
Response.Redirect(CasHex.IsAllowedRedirectTarget(cleanUrl, AllowedHost()) ? cleanUrl : "/");
|
||||
return AuthenticateResult.NoResult();
|
||||
}
|
||||
|
||||
/// <summary>是否根路径(无 referer 挑战的 service=根地址形态,回调形如 /?ticket=ST-xxx)。</summary>
|
||||
private static bool IsRootPath(string path)
|
||||
=> string.IsNullOrEmpty(path) || path == "/";
|
||||
|
||||
/// <summary>
|
||||
/// 剥离回跳目标 URL 上的 ticket 参数(query 与 #fragment 内均处理)。
|
||||
/// SPA 地址的死循环残留形如 #/?ticket=ST-x&ticket=ST-y;按 (?|&)ticket=[^&#]* 正则全局删除,
|
||||
/// 再清理删除后悬空的连接符,保证 URL 结构完好。
|
||||
/// </summary>
|
||||
private static string StripTicketParams(string url)
|
||||
{
|
||||
if (string.IsNullOrEmpty(url) || url.IndexOf("ticket=", StringComparison.OrdinalIgnoreCase) < 0)
|
||||
{
|
||||
return url;
|
||||
}
|
||||
var stripped = System.Text.RegularExpressions.Regex.Replace(
|
||||
url, "[?&]ticket=[^&#]*", string.Empty, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||
stripped = stripped.Replace("?&", "?").Replace("&&", "&");
|
||||
if (stripped.EndsWith('&') || stripped.EndsWith('?'))
|
||||
{
|
||||
stripped = stripped.Remove(stripped.Length - 1);
|
||||
}
|
||||
return stripped;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析 ;redirectToReferer= 分号段的回跳目标:优先 PathMatrixSegmentMiddleware 存入 Items 的
|
||||
/// hex(分号段已从路由路径剥离,回调重建 service 仍用原始路径);未经过该中间件时回退解析当前
|
||||
/// 路径。无该段或 hex 非法返回 null。
|
||||
/// </summary>
|
||||
private string ResolveRedirectToReferer()
|
||||
{
|
||||
if (Context.Items.TryGetValue(YhCasDefaults.RefererHexItemKey, out var v)
|
||||
&& v is string hex && hex.Length > 0)
|
||||
{
|
||||
return CasHex.Decode(hex);
|
||||
}
|
||||
return ParseRedirectToReferer(Context.Request.Path.Value);
|
||||
}
|
||||
|
||||
/// <summary>从回调路径中解析 ;redirectToReferer= 后的 hex 并解码;无该段或编码非法返回 null。</summary>
|
||||
private static string ParseRedirectToReferer(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var marker = path.IndexOf(YhCasDefaults.RedirectToRefererMarker, StringComparison.OrdinalIgnoreCase);
|
||||
if (marker < 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var hex = path.Substring(marker + YhCasDefaults.RedirectToRefererMarker.Length);
|
||||
var term = hex.IndexOfAny(new[] { ';', '?', '#' });
|
||||
if (term >= 0)
|
||||
{
|
||||
hex = hex.Substring(0, term);
|
||||
}
|
||||
return CasHex.Decode(hex);
|
||||
}
|
||||
|
||||
/// <summary>service 根地址的 host(回跳白名单基准):AppBaseUrl 未配置时取当前请求 host。</summary>
|
||||
private string AllowedHost()
|
||||
{
|
||||
var root = Options.AppBaseUrl;
|
||||
if (string.IsNullOrEmpty(root))
|
||||
{
|
||||
root = Context.Request.Scheme + "://" + Context.Request.Host.Value;
|
||||
}
|
||||
return Uri.TryCreate(root, UriKind.Absolute, out var uri) ? uri.Host : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user