first commit

This commit is contained in:
yanpeng200212
2026-10-08 10:43:59 +08:00
commit 8430ba0452
288 changed files with 29595 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
# 依赖
node_modules/
# 构建产物
dist/
bin/
obj/
publish/
# 运行时临时文件 / 日志
*.log
_runlog/
_tmp_hubtest/
_rename_backup/
*.db-shm
*.db-wal
# 证书私钥
cert.pem
key.pem
# IDE / 系统
.vs/
.idea/
.vscode/
*.user
Thumbs.db
Desktop.ini
+169
View File
@@ -0,0 +1,169 @@
# f9-web-ai-test
企业级 RBAC 权限管理系统
一个基于 .NET 8 分层架构 + Vue 3 + Element Plus 的企业内部管理系统,采用经典的面包屑后台布局,实现完整的 RBAC(基于角色的访问控制)权限体系。
## 技术栈
- **后端**:.NET 8(分层架构)+ NSwag(OpenAPI 文档)
- **前端**:Vue 3 + Vite + Element Plus + Pinia + Vue Router(绿色主题)
- **数据库**:SQLite(EF Core)
- **认证**:JWT 双令牌(access token + refresh token)+ BCrypt 密码加密
- **API 客户端**:NSwag 自动生成 TypeScript 客户端(`src/api/client.ts`)
## 后端分层架构
```
backend/
├── F9WebAiTest.Domain/ # 实体层
│ ├── Common/ # 基类 + 仓储接口 IRepository
│ └── Entities/ # 实体(部门/职工/角色/菜单/关联/日志)
├── F9WebAiTest.Infrastructure/ # 仓储实现层
│ ├── Data/ # DbContext + 种子数据
│ └── Repositories/ # Repository 通用实现
├── F9WebAiTest.Application/ # 服务层
│ ├── Common/ # ApiResult / ServiceException
│ ├── Dtos/ # 数据传输对象
│ └── Services/ # 业务逻辑(认证/部门/职工/角色/菜单)
└── F9WebAiTest.Api/ # 表示层(Web API 控制器)
```
依赖方向:`Api → Application → Domain ← Infrastructure`
## RBAC 核心功能
| 模块 | 说明 |
|------|------|
| 认证登录 | JWT 登录、动态菜单、权限标识下发 |
| 部门管理 | 树形部门结构、负责人、子部门、排序 |
| 职工管理 | 职工增删改查、分页搜索、分配角色、重置密码 |
| 角色管理 | 角色增删改查、菜单权限分配(树形勾选) |
| 菜单管理 | 目录/菜单/按钮三级,动态路由、权限标识 |
| 权限控制 | 动态路由 + 按钮级权限指令 v-permission |
| 考勤打卡 | 上班签到/下班签退,迟到/早退自动判定 |
| 我的考勤 | 个人考勤记录 + 月度统计(出勤/迟到/早退/缺勤/请假) |
| 请假管理 | 请假申请、审批(批准/驳回)、多类型(事假/病假/年假等) |
| 考勤记录 | 管理员按日期/部门/姓名查询全员考勤 |
| 工艺庄口 | 庄口基础数据(年度/本地外地/基地外购/产地等) |
| 生丝规格 | 规格指标(中心范围/野纤/内控/劳动竞赛等上下限) |
| 生丝定级指标 | 等级指标(厂标/国标/个人,偏差/大差/清洁/洁净等) |
| 机组车台 | 车间机组与车台号配置 |
## 数据模型关系
```
Department (部门) 1 ── N Employee (职工)
Employee (职工) N ── N Role (角色) via EmployeeRole
Role (角色) N ── N Menu (菜单) via RoleMenu
Menu (菜单) 自引用树形(目录/菜单/按钮)
```
## 启动方式
### 1. 启动后端
```bash
cd backend/F9WebAiTest.Api
dotnet run
```
后端运行在 `http://localhost:3000`,Swagger 文档 `http://localhost:3000/swagger`。
### 2. 启动前端
```bash
cd web
npm install
npm run dev
```
前端运行在 `http://localhost:5173`,已配置代理 `/api` 转发到后端。
### 3. 生成 API 客户端(NSwag)
后端运行时,用 NSwag 从 OpenAPI 文档自动生成 TypeScript 客户端:
```bash
# 先安装 NSwag CLI 工具(只需一次)
dotnet tool install --global NSwag.ConsoleCore
# 生成 TypeScript 客户端到 src/api/client.ts
cd web
npm run gen:api
```
生成的 `src/api/client.ts` 包含所有 DTO 类型和 API 方法,类型安全地调用后端接口。
### 3. 访问系统
打开 `http://localhost:5173`
**默认账号**:`admin` / `123456`(超级管理员,拥有全部权限)
## 前端目录结构
```
web/src/
├── layout/index.vue # 主布局(侧边栏 + 面包屑 + 内容区)
├── router/index.js # 路由 + 动态路由构建
├── store/user.js # 用户状态(Pinia)
├── directives/permission.js # 权限指令 v-permission
├── utils/request.js # axios 封装(拦截器)
└── views/
├── login/ # 登录页
├── dashboard/ # 首页
└── system/
├── department/ # 部门管理
├── employee/ # 职工管理
├── role/ # 角色管理(含菜单分配)
├── menu/ # 菜单管理
└── log/ # 操作日志
```
## 权限使用示例
```vue
<!-- 按钮级权限控制 -->
<el-button v-permission="'system:user:add'">新增</el-button>
<!-- 多权限任一满足 -->
<el-button v-permission="['system:user:add', 'system:user:edit']">操作</el-button>
```
## API 接口
- `POST /api/auth/login` - 登录(返回 access token + refresh token)
- `POST /api/auth/refresh` - 刷新令牌(用 refresh token 换新 access token)
- `POST /api/auth/logout` - 退出(撤销 refresh token)
- `GET /api/auth/me` - 当前用户信息
- `GET /api/auth/menus` - 用户菜单树
- `GET /api/auth/permissions` - 用户权限标识
- `PUT /api/auth/password` - 修改密码
- `PUT /api/auth/profile` - 更新资料
- `GET /api/department/tree` - 部门树
- `POST /api/department` - 新增部门
- `PUT /api/department/:id` - 更新部门
- `DELETE /api/department/:id` - 删除部门
- `GET /api/employee` - 职工分页
- `POST /api/employee` - 新增职工
- `PUT /api/employee/:id` - 更新职工
- `DELETE /api/employee/:id` - 删除职工
- `PUT /api/employee/:id/password` - 重置密码
- `GET /api/role` - 角色列表
- `GET /api/role/:id/menus` - 角色菜单
- `POST /api/role` - 新增角色
- `PUT /api/role/:id` - 更新角色
- `DELETE /api/role/:id` - 删除角色
- `GET /api/menu/tree` - 菜单树
- `POST /api/menu` - 新增菜单
- `PUT /api/menu/:id` - 更新菜单
- `DELETE /api/menu/:id` - 删除菜单
- `POST /api/attendance/punch` - 打卡(签到/签退)
- `GET /api/attendance/my` - 我的考勤记录
- `GET /api/attendance/my/summary` - 我的考勤统计
- `GET /api/attendance` - 全员考勤记录(管理员)
- `POST /api/attendance/leave` - 申请请假
- `GET /api/attendance/leave/my` - 我的请假
- `GET /api/attendance/leave` - 全部请假(管理员)
- `PUT /api/attendance/leave/:id/approve` - 审批请假
+132
View File
@@ -0,0 +1,132 @@
using System.Diagnostics;
using System.Drawing.Drawing2D;
namespace TempleRun;
/// <summary>
/// 主窗体:负责游戏循环(定时器 + 高精度计时)、键盘输入、双缓冲绘制调度。
/// </summary>
public class GameForm : Form
{
private readonly GameWorld _world = new();
private readonly System.Windows.Forms.Timer _timer;
private readonly Stopwatch _watch = Stopwatch.StartNew();
private double _lastTime;
private double _fpsAccum;
private int _frameCount;
private bool _showFps;
public GameForm()
{
Text = "神庙逃亡 · Temple Run (WinForms)";
ClientSize = new Size(1024, 720);
MinimumSize = new Size(760, 540);
StartPosition = FormStartPosition.CenterScreen;
BackColor = Color.FromArgb(12, 12, 20);
DoubleBuffered = true;
ResizeRedraw = true;
AutoScaleMode = AutoScaleMode.None;
SetStyle(ControlStyles.AllPaintingInWmPaint
| ControlStyles.UserPaint
| ControlStyles.OptimizedDoubleBuffer, true);
try { Icon = BuildIcon(); } catch { /* 图标失败不影响运行 */ }
_lastTime = _watch.Elapsed.TotalSeconds;
_timer = new System.Windows.Forms.Timer { Interval = 15 }; // ≈66Hz
_timer.Tick += OnTick;
_timer.Start();
}
// ------------------------------------------------------------------ 主循环
private void OnTick(object sender, EventArgs e)
{
double now = _watch.Elapsed.TotalSeconds;
float dt = (float)(now - _lastTime);
_lastTime = now;
if (dt <= 0f) return;
if (dt > 0.06f) dt = 0.06f; // 卡顿/拖动窗口后不产生穿模
// FPS 统计
_frameCount++;
_fpsAccum += dt;
if (_fpsAccum >= 0.5)
{
_world.Fps = (float)(_frameCount / _fpsAccum);
_frameCount = 0;
_fpsAccum = 0;
}
_world.Update(dt);
Invalidate();
}
protected override void OnPaint(PaintEventArgs e)
{
GameRenderer.Draw(e.Graphics, ClientSize.Width, ClientSize.Height, _world, _showFps);
}
// ------------------------------------------------------------------ 输入
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
Keys key = keyData & Keys.KeyCode;
if (key == Keys.F1) { _showFps = !_showFps; Invalidate(); return true; }
if (key == Keys.F11)
{
WindowState = WindowState == FormWindowState.Maximized
? FormWindowState.Normal : FormWindowState.Maximized;
return true;
}
if (key == Keys.F12) { _world.Muted = !_world.Muted; return true; }
if (_world.HandleKey(key)) return true;
return base.ProcessCmdKey(ref msg, keyData);
}
protected override void OnMouseClick(MouseEventArgs e)
{
base.OnMouseClick(e);
// 鼠标左键也能触发"开始/重开",方便触屏设备
if (e.Button == MouseButtons.Left)
{
if (_world.State == RunState.Ready) _world.Start();
else if (_world.State == RunState.GameOver && _world.CanRestart)
{
_world.Start();
}
}
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
_timer.Stop();
_timer.Dispose();
GameRenderer.ResetCache();
base.OnFormClosed(e);
}
// ------------------------------------------------------------------ 图标
private static Icon BuildIcon()
{
using var bmp = new Bitmap(32, 32);
using (var g = Graphics.FromImage(bmp))
{
g.SmoothingMode = SmoothingMode.AntiAlias;
g.Clear(Color.Transparent);
using (var bg = new SolidBrush(Color.FromArgb(46, 34, 28)))
g.FillEllipse(bg, 0, 0, 31, 31);
using (var gold = new SolidBrush(Cfg.Gold))
g.FillEllipse(gold, 7, 7, 18, 18);
using (var dark = new Pen(Cfg.GoldDark, 2.4f))
g.DrawEllipse(dark, 7, 7, 18, 18);
using (var hi = new SolidBrush(Color.FromArgb(230, 255, 250, 220)))
g.FillEllipse(hi, 11, 10, 5, 4);
}
return Icon.FromHandle(bmp.GetHicon());
}
}
+202
View File
@@ -0,0 +1,202 @@
using System.Drawing.Drawing2D;
namespace TempleRun;
/// <summary>
/// 全局常量与可调参数。想改手感直接改这里。
/// 世界坐标约定:X 向右为正,Y 离地向上为正,Z 玩家前方为正。
/// 玩家永远停在 Z = 0,世界向 Z 负方向滚动。
/// </summary>
public static class Cfg
{
// ---- 相机(透视投影参数)----
public const float Focal = 520f; // 焦距:越大视野越窄、远处物体越大
public const float CamZ = 235f; // 相机位于玩家后方多远
public const float CamHeight = 142f; // 相机离地高度
public const float FarZ = 3000f; // 渲染最远距离
public const float MinDepth = 45f; // 相机空间最小深度,防止除零
// ---- 跑道 ----
public const int LaneCount = 3;
public const float LaneWidth = 112f; // 单条车道宽
public const float RoadHalfWidth = LaneWidth * 1.5f;
// ---- 玩家 ----
public const float PlayerHeight = 62f; // 站立高度
public const float SlideHeight = 30f; // 滑铲高度
public const float PlayerHalfDepth = 22f;
public const float JumpSpeed = 430f;
public const float Gravity = 950f;
public const float LaneLerp = 13f; // 换道插值速度
public const float SlideDuration = 0.55f;
// ---- 速度与难度 ----
public const float StartSpeed = 620f;
public const float MaxSpeed = 1900f;
public const float SpeedAccel = 3.4f; // 每秒自然加速
public const float SpeedPerCoin = 5f; // 每金币额外加速
public const float SpawnIntervalMin = 0.62f; // 相邻障碍组最小时间间隔(秒)
public const float SpawnIntervalMax = 1.15f;
// ---- 颜色(丛林日落 + 古神庙)----
public static readonly Color SkyTop = Color.FromArgb(30, 34, 72);
public static readonly Color SkyMid = Color.FromArgb(122, 78, 96);
public static readonly Color SkyHorizon = Color.FromArgb(240, 168, 96);
public static readonly Color Fog = Color.FromArgb(200, 152, 118);
public static readonly Color GroundNear = Color.FromArgb(44, 36, 28);
public static readonly Color GroundFar = Color.FromArgb(122, 100, 78);
public static readonly Color RoadA = Color.FromArgb(178, 150, 110);
public static readonly Color RoadB = Color.FromArgb(156, 130, 94);
public static readonly Color Stone = Color.FromArgb(120, 112, 104);
public static readonly Color Wood = Color.FromArgb(122, 82, 48);
public static readonly Color Vine = Color.FromArgb(74, 116, 62);
public static readonly Color Gold = Color.FromArgb(255, 202, 62);
public static readonly Color GoldDark = Color.FromArgb(196, 140, 24);
public static readonly Color Player = Color.FromArgb(226, 92, 62);
public static float LaneX(int lane) => (lane - 1) * LaneWidth;
public static Color Lerp(Color a, Color b, float t)
{
t = Math.Clamp(t, 0f, 1f);
return Color.FromArgb(
(int)(a.R + (b.R - a.R) * t),
(int)(a.G + (b.G - a.G) * t),
(int)(a.B + (b.B - a.B) * t));
}
}
public enum RunState { Ready, Playing, Paused, GameOver }
public enum ObstacleKind
{
/// <summary>矮障碍:必须跳跃</summary>
Low,
/// <summary>悬空横梁:必须滑铲</summary>
High,
/// <summary>整道石墙:必须换道</summary>
Full
}
/// <summary>透视相机:把世界坐标投影到屏幕坐标。</summary>
public static class Cam
{
public static float CenterX;
public static float HorizonY;
public static int Width;
public static int Height;
public static void Setup(int w, int h)
{
Width = w;
Height = h;
CenterX = w * 0.5f;
HorizonY = h * 0.30f;
}
/// <summary>
/// 投影一个世界点。
/// scale 为"每世界单位对应多少像素",用于缩放实体自身尺寸。
/// </summary>
public static void Project(float x, float y, float z, out float sx, out float sy, out float scale)
{
float depth = z + Cfg.CamZ;
if (depth < Cfg.MinDepth) depth = Cfg.MinDepth;
scale = Cfg.Focal / depth;
sx = CenterX + x * scale;
sy = HorizonY + (Cfg.CamHeight - y) * scale;
}
/// <summary>只投影地面上的点(y = 0)。</summary>
public static void ProjectGround(float x, float z, out float sx, out float sy, out float scale)
=> Project(x, 0f, z, out sx, out sy, out scale);
/// <summary>距离雾化系数:0 = 完全清晰,1 = 完全融入雾色。</summary>
public static float FogAmount(float z)
{
float t = Math.Clamp(z / Cfg.FarZ, 0f, 1f);
return t * t; // 平方衰减,近处保持通透
}
}
public class Obstacle
{
public float Z;
public int Lane;
public ObstacleKind Kind;
public bool Scored; // 是否已计入"躲避成功"
public float Width => Kind == ObstacleKind.Full ? 96f : 90f;
public float Depth => Kind == ObstacleKind.Full ? 54f : 56f;
public float BottomY => Kind == ObstacleKind.High ? 40f : 0f;
public float TopY => Kind switch
{
ObstacleKind.Low => 46f, // 低于跳跃高度,跳过去
ObstacleKind.High => 98f, // 悬空横梁,滑铲钻过去
_ => 108f // 整道石墙,只能换道
};
public float CenterX => Cfg.LaneX(Lane);
}
public class Coin
{
public float Z;
public int Lane;
public float Y = 36f;
public bool Taken;
public float Phase;
public float CenterX => Cfg.LaneX(Lane);
}
public class Particle
{
public float X, Y, Z;
public float Vx, Vy, Vz;
public float Life;
public float MaxLife;
public float Size;
public float GravityScale = 1f;
public Color Color;
}
/// <summary>路边装饰物:石柱、火把、树、雕像碎块。</summary>
public class SideProp
{
public float Z;
public int Side; // -1 左,+1 右
public int Type; // 0=石柱 1=火把 2=枯树 3=碎石
public float Height;
public float Phase;
public float CenterX => Side * (Cfg.RoadHalfWidth + 205f + (Type == 3 ? 40f : 0f));
}
public class Player
{
public float X; // 当前横向世界坐标(插值中)
public int TargetLane = 1; // 目标车道 0/1/2
public float Y; // 离地高度
public float Vy;
public bool OnGround = true;
public bool Sliding;
public float SlideTimer;
public float RunPhase; // 跑动动画相位
public bool Alive = true;
public float Tilt; // 换道时的侧倾(视觉)
public float Height => Sliding ? Cfg.SlideHeight : Cfg.PlayerHeight;
public void Reset()
{
TargetLane = 1;
X = Cfg.LaneX(1);
Y = 0;
Vy = 0;
OnGround = true;
Sliding = false;
SlideTimer = 0;
RunPhase = 0;
Alive = true;
Tilt = 0;
}
}
+902
View File
@@ -0,0 +1,902 @@
using System.Drawing.Drawing2D;
using System.Drawing.Text;
namespace TempleRun;
/// <summary>
/// 全部绘制逻辑。使用 GDI+ 手工做透视投影,不依赖任何 3D 库。
/// </summary>
public static class GameRenderer
{
// ---------------------------------------------------------------- 缓存对象
private static Font _fHuge, _fTitle, _fBig, _fMid, _fSmall, _fTiny;
private static readonly List<(float Z, int Type, object Obj)> _drawList = new(512);
private const int TypeObstacle = 0;
private const int TypeCoin = 1;
private static void EnsureFonts()
{
if (_fBig != null) return;
const string face = "Microsoft YaHei UI";
_fHuge = new Font(face, 46f, FontStyle.Bold, GraphicsUnit.Pixel);
_fTitle = new Font(face, 30f, FontStyle.Bold, GraphicsUnit.Pixel);
_fBig = new Font(face, 26f, FontStyle.Bold, GraphicsUnit.Pixel);
_fMid = new Font(face, 19f, FontStyle.Regular, GraphicsUnit.Pixel);
_fSmall = new Font(face, 15f, FontStyle.Regular, GraphicsUnit.Pixel);
_fTiny = new Font(face, 12f, FontStyle.Regular, GraphicsUnit.Pixel);
}
public static void ResetCache() { _fBig = null; }
// ---------------------------------------------------------------- 入口
public static void Draw(Graphics g, int w, int h, GameWorld world, bool showFps = false)
{
if (w <= 0 || h <= 0) return;
EnsureFonts();
Cam.Setup(w, h);
g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
g.Clear(Color.FromArgb(12, 12, 20));
// 屏幕震动
float shakeX = 0f, shakeY = 0f;
if (world.Shake > 0.15f)
{
float s = world.Shake;
shakeX = (float)(Random.Shared.NextDouble() * 2 - 1) * s;
shakeY = (float)(Random.Shared.NextDouble() * 2 - 1) * s * 0.7f;
}
GraphicsState st = g.Save();
g.TranslateTransform(shakeX, shakeY);
g.SmoothingMode = SmoothingMode.AntiAlias;
DrawSky(g, w, h);
DrawDistantScenery(g, w, h);
DrawGround(g, w, h, world);
DrawProps(g, world);
DrawEntities(g, world);
DrawParticles(g, world);
DrawPlayer(g, world);
g.Restore(st);
// 撞击红闪
if (world.HitFlash > 0.01f)
{
using var fb = new SolidBrush(Color.FromArgb((int)(130 * world.HitFlash), 220, 40, 30));
g.FillRectangle(fb, 0, 0, w, h);
}
DrawHud(g, w, h, world, showFps);
}
// ---------------------------------------------------------------- 投影辅助
private static PointF P(float x, float y, float z)
{
Cam.Project(x, y, z, out float sx, out float sy, out _);
return new PointF(sx, sy);
}
private static float PX(float x, float y, float z)
{
Cam.Project(x, y, z, out float sx, out _, out _);
return sx;
}
private static float PY(float x, float y, float z)
{
Cam.Project(x, y, z, out _, out float sy, out _);
return sy;
}
private static float PS(float x, float y, float z)
{
Cam.Project(x, y, z, out _, out _, out float s);
return s;
}
// ---------------------------------------------------------------- 天空
private static void DrawSky(Graphics g, int w, int h)
{
int skyBottom = (int)Cam.HorizonY + 30;
var rect = new Rectangle(0, -30, w, skyBottom + 30);
if (rect.Height < 2) return;
using var br = new LinearGradientBrush(rect, Color.Black, Color.White, 90f)
{
InterpolationColors = new ColorBlend(3)
{
Colors = new[] { Cfg.SkyTop, Cfg.SkyMid, Cfg.SkyHorizon },
Positions = new[] { 0f, 0.60f, 1f }
}
};
g.FillRectangle(br, rect);
// 太阳
float sunX = w * 0.74f;
float sunY = Cam.HorizonY - h * 0.115f;
float sunR = Math.Max(16f, h * 0.072f);
using (var path = new GraphicsPath())
{
path.AddEllipse(sunX - sunR * 3.4f, sunY - sunR * 3.4f, sunR * 6.8f, sunR * 6.8f);
using var glow = new PathGradientBrush(path)
{
CenterColor = Color.FromArgb(140, 255, 220, 150),
SurroundColors = new[] { Color.FromArgb(0, 255, 200, 120) }
};
g.FillPath(glow, path);
}
using (var sb = new SolidBrush(Color.FromArgb(255, 255, 240, 200)))
g.FillEllipse(sb, sunX - sunR, sunY - sunR, sunR * 2, sunR * 2);
// 云条
using (var cb = new SolidBrush(Color.FromArgb(46, 255, 226, 190)))
{
g.FillEllipse(cb, w * 0.10f, Cam.HorizonY - h * 0.20f, w * 0.16f, h * 0.022f);
g.FillEllipse(cb, w * 0.30f, Cam.HorizonY - h * 0.255f, w * 0.11f, h * 0.017f);
g.FillEllipse(cb, w * 0.62f, Cam.HorizonY - h * 0.16f, w * 0.20f, h * 0.020f);
}
}
// ---------------------------------------------------------------- 远景(山脊 + 神庙)
private static void DrawDistantScenery(Graphics g, int w, int h)
{
float hy = Cam.HorizonY;
// 远山脊
using (var far = new SolidBrush(Cfg.Lerp(Cfg.Fog, Color.FromArgb(72, 62, 104), 0.42f)))
{
using var path = new GraphicsPath();
path.AddPolygon(new[]
{
new PointF(-w * 0.05f, hy + 2),
new PointF(w * 0.08f, hy - h * 0.075f),
new PointF(w * 0.20f, hy - h * 0.012f),
new PointF(w * 0.33f, hy - h * 0.105f),
new PointF(w * 0.47f, hy - h * 0.020f),
new PointF(w * 0.60f, hy - h * 0.088f),
new PointF(w * 0.74f, hy - h * 0.028f),
new PointF(w * 0.88f, hy - h * 0.070f),
new PointF(w * 1.05f, hy + 2)
});
g.FillPath(far, path);
}
// 神庙剪影(左)
DrawTemple(g, w * 0.19f, hy + 4, w * 0.115f, h * 0.155f,
Cfg.Lerp(Cfg.Fog, Color.FromArgb(58, 48, 78), 0.55f), 5);
// 神庙剪影(右,稍小)
DrawTemple(g, w * 0.83f, hy + 4, w * 0.085f, h * 0.115f,
Cfg.Lerp(Cfg.Fog, Color.FromArgb(66, 56, 86), 0.46f), 4);
// 地平线雾带
using var haze = new LinearGradientBrush(
new Rectangle(0, (int)hy - 26, w, 58),
Color.FromArgb(0, Cfg.Fog), Color.FromArgb(190, Cfg.Fog), 90f);
g.FillRectangle(haze, 0, hy - 26, w, 58);
}
private static void DrawTemple(Graphics g, float cx, float baseY, float w, float h, Color c, int steps)
{
using var br = new SolidBrush(c);
float sh = h / steps;
for (int i = 0; i < steps; i++)
{
float frac = 1f - (float)i / steps * 0.70f;
float ww = w * frac;
float y = baseY - sh * (i + 1);
g.FillRectangle(br, cx - ww * 0.5f, y, ww, sh + 1.2f);
}
// 塔尖
float topW = w * 0.30f;
float topY = baseY - h;
g.FillPolygon(br, new[]
{
new PointF(cx - topW * 0.5f, topY + 1),
new PointF(cx, topY - sh * 0.85f),
new PointF(cx + topW * 0.5f, topY + 1)
});
}
// ---------------------------------------------------------------- 地面与跑道
private static void DrawGround(Graphics g, int w, int h, GameWorld world)
{
// 整体地面渐变
int gy = (int)Cam.HorizonY;
var rect = new Rectangle(0, gy - 2, w, h - gy + 4);
if (rect.Height > 1)
{
using var gb = new LinearGradientBrush(rect, Color.White, Color.White, 90f)
{
InterpolationColors = new ColorBlend(4)
{
Colors = new[] { Cfg.GroundFar, Cfg.Lerp(Cfg.GroundFar, Cfg.GroundNear, 0.35f), Cfg.GroundNear, Color.FromArgb(52, 40, 30) },
Positions = new[] { 0f, 0.18f, 0.62f, 1f }
}
};
g.FillRectangle(gb, rect);
}
SmoothingMode old = g.SmoothingMode;
g.SmoothingMode = SmoothingMode.None; // 条纹用无抗锯齿更快
const float period = 190f;
const float drawNear = -230f;
// 跑道两侧的暗色沟渠
DrawSideBand(g, w, -1);
DrawSideBand(g, w, +1);
// 跑道条纹(世界坐标固定,随世界滚动)
float offset = world.Distance % (period * 2f);
for (int i = -8; i < 40; i++)
{
float z0 = i * period * 2f - offset;
float z1 = z0 + period;
if (z1 < drawNear) continue;
if (z0 > Cfg.FarZ) break;
z0 = Math.Max(z0, drawNear);
z1 = Math.Min(z1, Cfg.FarZ);
if (z1 <= z0) continue;
float fog = Cam.FogAmount(z0);
Color baseColor = ((i % 2) + 2) % 2 == 0 ? Cfg.RoadA : Cfg.RoadB;
Color c = Cfg.Lerp(baseColor, Cfg.Fog, fog * 0.92f);
float lx0 = PX(-Cfg.RoadHalfWidth, 0, z0);
float rx0 = PX(+Cfg.RoadHalfWidth, 0, z0);
float ly0 = PY(0, 0, z0);
float ly1 = PY(0, 0, z1);
float lx1 = PX(-Cfg.RoadHalfWidth, 0, z1);
float rx1 = PX(+Cfg.RoadHalfWidth, 0, z1);
using var br = new SolidBrush(c);
g.FillPolygon(br, new[]
{
new PointF(lx0, ly0), new PointF(rx0, ly0),
new PointF(rx1, ly1), new PointF(lx1, ly1)
});
}
// 跑道边缘描边
DrawRoadEdge(g, -1);
DrawRoadEdge(g, +1);
// 车道分隔虚线
DrawLaneMark(g, -Cfg.LaneWidth * 0.5f, world, period);
DrawLaneMark(g, +Cfg.LaneWidth * 0.5f, world, period);
g.SmoothingMode = old;
}
private static void DrawRoadEdge(Graphics g, int side)
{
float x = side * Cfg.RoadHalfWidth;
var pts = new List<PointF>();
for (float z = Cfg.FarZ; z >= -230f; z -= 120f)
pts.Add(new PointF(PX(x, 0, z), PY(0, 0, z)));
if (pts.Count < 2) return;
using var pen = new Pen(Color.FromArgb(120, 58, 44, 32), 3.2f);
g.DrawLines(pen, pts.ToArray());
}
private static void DrawSideBand(Graphics g, int w, int side)
{
float xi = side * Cfg.RoadHalfWidth;
float xo = side * (Cfg.RoadHalfWidth + 170f);
float yFar = PY(0, 0, Cfg.FarZ);
float yNear = PY(0, 0, -230f);
var poly = new[]
{
new PointF(PX(xi, 0, Cfg.FarZ), yFar), new PointF(PX(xo, 0, Cfg.FarZ), yFar),
new PointF(PX(xo, 0, -230f), yNear), new PointF(PX(xi, 0, -230f), yNear)
};
using var br = new SolidBrush(Color.FromArgb(96, 34, 26, 20));
g.FillPolygon(br, poly);
}
private static void DrawLaneMark(Graphics g, float x, GameWorld world, float period)
{
float offset = world.Distance % (period * 2f);
for (int i = -8; i < 40; i++)
{
float z0 = i * period * 2f - offset;
float z1 = z0 + period * 0.45f;
if (z1 < -230f) continue;
if (z0 > Cfg.FarZ) break;
z0 = Math.Max(z0, -230f);
z1 = Math.Min(z1, Cfg.FarZ);
if (z1 <= z0) continue;
float fog = Cam.FogAmount(z0);
var c = Cfg.Lerp(Color.FromArgb(168, 146, 108), Cfg.Fog, fog * 0.9f);
using var br = new SolidBrush(Color.FromArgb((byte)(120 * (1 - fog) + 30), c));
g.FillPolygon(br, new[]
{
new PointF(PX(x - 4, 0, z0), PY(0, 0, z0)), new PointF(PX(x + 4, 0, z0), PY(0, 0, z0)),
new PointF(PX(x + 4, 0, z1), PY(0, 0, z1)), new PointF(PX(x - 4, 0, z1), PY(0, 0, z1))
});
}
}
// ---------------------------------------------------------------- 路边装饰
private static void DrawProps(Graphics g, GameWorld world)
{
var props = world.Props;
for (int i = 0; i < props.Count; i++)
{
var p = props[i];
if (p.Z < 70f) continue; // 太近会盖住跑道,直接跳过
float fog = Cam.FogAmount(p.Z);
if (fog > 0.96f) continue;
float wx = p.CenterX; // 世界坐标
float sx = PX(wx, 0f, p.Z); // ★ 屏幕 x(原来误用了世界坐标)
float top = PY(wx, p.Height, p.Z);
float bottom = PY(wx, 0f, p.Z);
float s = PS(wx, 0f, p.Z);
float wpx = Math.Min(40f * s, 220f); // 限制近处被夸张放大的尺寸
if (bottom < Cam.HorizonY - 60) continue;
switch (p.Type)
{
case 0: // 石柱
{
var body = Cfg.Lerp(Color.FromArgb(96, 86, 76), Cfg.Fog, fog);
using var br = new SolidBrush(body);
g.FillRectangle(br, sx - wpx * 0.5f, top, wpx, bottom - top);
using var cap = new SolidBrush(Cfg.Lerp(Color.FromArgb(124, 110, 96), Cfg.Fog, fog));
g.FillRectangle(cap, sx - wpx * 0.62f, top - wpx * 0.22f, wpx * 1.24f, wpx * 0.26f);
using var pen = new Pen(Color.FromArgb(80, 34, 28, 22), Math.Max(1f, s * 1.4f));
g.DrawLine(pen, sx - wpx * 0.18f, top, sx - wpx * 0.18f, bottom);
break;
}
case 1: // 火把
{
using var br = new SolidBrush(Cfg.Lerp(Color.FromArgb(88, 60, 38), Cfg.Fog, fog));
g.FillRectangle(br, sx - wpx * 0.16f, top, wpx * 0.32f, bottom - top);
float fr = wpx * 0.52f * (1f + 0.14f * MathF.Sin(world.Clock * 6f + p.Phase));
using var glow = new SolidBrush(Color.FromArgb((int)(130 * (1 - fog)), 255, 170, 62));
g.FillEllipse(glow, sx - fr, top - fr * 1.1f, fr * 2, fr * 2);
using var core = new SolidBrush(Color.FromArgb((int)(235 * (1 - fog)), 255, 234, 158));
g.FillEllipse(core, sx - fr * 0.42f, top - fr * 0.95f, fr * 0.84f, fr * 0.95f);
break;
}
case 2: // 枯树
{
var wood = Cfg.Lerp(Color.FromArgb(64, 50, 38), Cfg.Fog, fog);
using var br = new SolidBrush(wood);
float trunkW = Math.Max(2f, wpx * 0.20f);
g.FillRectangle(br, sx - trunkW * 0.5f, top, trunkW, bottom - top);
using var pen = new Pen(wood, Math.Max(1.6f, wpx * 0.11f))
{ StartCap = LineCap.Round, EndCap = LineCap.Round };
float midY = top + (bottom - top) * 0.42f;
g.DrawLine(pen, sx, midY, sx - wpx * 0.85f, midY - (bottom - top) * 0.30f);
g.DrawLine(pen, sx, midY + (bottom - top) * 0.12f, sx + wpx * 0.90f, midY - (bottom - top) * 0.20f);
break;
}
default: // 碎石
{
using var br = new SolidBrush(Cfg.Lerp(Color.FromArgb(84, 76, 68), Cfg.Fog, fog));
float hgt = (bottom - top) * 1.1f;
g.FillEllipse(br, sx - wpx * 0.7f, bottom - hgt, wpx * 1.4f, hgt);
break;
}
}
}
}
// ---------------------------------------------------------------- 实体(障碍 + 金币)
private static void DrawEntities(Graphics g, GameWorld world)
{
_drawList.Clear();
for (int i = 0; i < world.Obstacles.Count; i++)
{
var o = world.Obstacles[i];
if (o.Z > Cfg.FarZ + 200f || o.Z < -260f) continue;
_drawList.Add((o.Z, TypeObstacle, o));
}
for (int i = 0; i < world.Coins.Count; i++)
{
var c = world.Coins[i];
if (c.Taken || c.Z > Cfg.FarZ + 200f || c.Z < -260f) continue;
_drawList.Add((c.Z, TypeCoin, c));
}
_drawList.Sort(static (a, b) => b.Z.CompareTo(a.Z)); // 远 → 近
for (int i = 0; i < _drawList.Count; i++)
{
var it = _drawList[i];
if (it.Type == TypeObstacle) DrawObstacle(g, (Obstacle)it.Obj);
else DrawCoin(g, (Coin)it.Obj);
}
}
private static void DrawObstacle(Graphics g, Obstacle o)
{
float fog = Cam.FogAmount(o.Z);
if (fog > 0.97f) return;
float cx = o.CenterX;
float hw = o.Width * 0.5f;
float zn = o.Z - o.Depth * 0.5f;
float zf = o.Z + o.Depth * 0.5f;
float yb = o.BottomY, yt = o.TopY;
// 8 个顶点
PointF nlb = P(cx - hw, yb, zn), nrb = P(cx + hw, yb, zn);
PointF nlt = P(cx - hw, yt, zn), nrt = P(cx + hw, yt, zn);
PointF flb = P(cx - hw, yb, zf), frb = P(cx + hw, yb, zf);
PointF flt = P(cx - hw, yt, zf), frt = P(cx + hw, yt, zf);
// 落地阴影(让障碍"坐"在跑道上)
if (o.BottomY <= 1f)
{
Cam.Project(cx, 0.6f, o.Z, out float shx, out float shy, out float shs);
using var shb = new SolidBrush(Color.FromArgb(78, 0, 0, 0));
g.FillEllipse(shb, shx - o.Width * 0.62f * shs, shy - 8f * shs,
o.Width * 1.24f * shs, 16f * shs);
}
Color cFront, cTop, cSide;
switch (o.Kind)
{
case ObstacleKind.Low:
cFront = Color.FromArgb(78, 70, 62);
cTop = Color.FromArgb(120, 108, 94);
cSide = Color.FromArgb(56, 50, 44);
break;
case ObstacleKind.High:
cFront = Color.FromArgb(112, 72, 40);
cTop = Color.FromArgb(150, 106, 58);
cSide = Color.FromArgb(80, 52, 28);
break;
default:
cFront = Color.FromArgb(64, 60, 58);
cTop = Color.FromArgb(104, 96, 90);
cSide = Color.FromArgb(46, 44, 42);
break;
}
cFront = Cfg.Lerp(cFront, Cfg.Fog, fog);
cTop = Cfg.Lerp(cTop, Cfg.Fog, fog);
cSide = Cfg.Lerp(cSide, Cfg.Fog, fog);
// 顶面
using (var br = new SolidBrush(cTop))
g.FillPolygon(br, new[] { nlt, nrt, frt, flt });
// 侧面(朝向画面中心的那一面)
bool showLeft = cx > 0f;
using (var br = new SolidBrush(cSide))
{
var quad = showLeft
? new[] { nlb, nlt, flt, flb }
: new[] { nrb, nrt, frt, frb };
g.FillPolygon(br, quad);
}
// 近面
using (var br = new SolidBrush(cFront))
g.FillPolygon(br, new[] { nlb, nrb, nrt, nlt });
// 近面描边 + 细节
float lw = Math.Max(1f, PS(cx, 0, o.Z) * 1.6f);
using (var pen = new Pen(Color.FromArgb(150, 34, 26, 20), lw))
g.DrawPolygon(pen, new[] { nlb, nrb, nrt, nlt });
float s = PS(cx, 0, o.Z);
if (s > 0.35f)
{
if (o.Kind == ObstacleKind.High)
{
// 横梁上的藤蔓
using var vine = new Pen(Cfg.Lerp(Cfg.Vine, Cfg.Fog, fog), Math.Max(1.5f, s * 2.4f))
{ StartCap = LineCap.Round, EndCap = LineCap.Round };
float topY = (nlt.Y + nrt.Y) * 0.5f;
float botY = (nlb.Y + nrb.Y) * 0.5f;
for (int k = 0; k < 3; k++)
{
float fx = nlb.X + (nrb.X - nlb.X) * (0.22f + k * 0.28f);
g.DrawLine(vine, fx, topY + (botY - topY) * 0.30f, fx + s * 6f, botY + s * 10f);
}
}
else if (o.Kind == ObstacleKind.Full)
{
// 石墙刻纹
using var pen = new Pen(Color.FromArgb(90, 40, 34, 30), Math.Max(1f, s * 1.8f));
for (int k = 1; k <= 2; k++)
{
float t = k / 3f;
float y = nlb.Y + (nlt.Y - nlb.Y) * t;
g.DrawLine(pen, nlb.X + s * 4f, y, nrb.X - s * 4f, y);
}
}
else
{
// 矮石堆的碎石纹理
using var br2 = new SolidBrush(Color.FromArgb(70, 46, 38, 30));
g.FillEllipse(br2, nlb.X + (nrb.X - nlb.X) * 0.16f, nlb.Y - (nlb.Y - nlt.Y) * 0.55f,
(nrb.X - nlb.X) * 0.26f, (nlb.Y - nlt.Y) * 0.30f);
}
}
}
private static void DrawCoin(Graphics g, Coin c)
{
float fog = Cam.FogAmount(c.Z);
if (fog > 0.96f) return;
Cam.Project(c.CenterX, c.Y, c.Z, out float sx, out float sy, out float s);
float r = Math.Min(10f * s, 46f);
if (r < 0.6f) return;
float wobble = MathF.Abs(MathF.Cos(c.Phase));
float rx = r * (0.30f + 0.70f * wobble);
float ry = r;
int fade = (int)(255 * (1f - fog * 0.9f));
Color gold = Cfg.Lerp(Cfg.Gold, Cfg.Fog, fog * 0.7f);
Color goldDark = Cfg.Lerp(Cfg.GoldDark, Cfg.Fog, fog * 0.7f);
// 光晕
using (var glow = new SolidBrush(Color.FromArgb(Math.Max(0, fade / 6), gold)))
g.FillEllipse(glow, sx - ry * 1.4f, sy - ry * 1.4f, ry * 2.8f, ry * 2.8f);
using (var br = new SolidBrush(Color.FromArgb(fade, gold)))
g.FillEllipse(br, sx - rx, sy - ry, rx * 2, ry * 2);
using (var pen = new Pen(Color.FromArgb(fade, goldDark), Math.Max(1f, r * 0.16f)))
g.DrawEllipse(pen, sx - rx, sy - ry, rx * 2, ry * 2);
// 高光
if (r > 3f)
{
using var hl = new SolidBrush(Color.FromArgb((int)(fade * 0.75f), 255, 250, 220));
g.FillEllipse(hl, sx - rx * 0.42f, sy - ry * 0.52f, rx * 0.46f, ry * 0.40f);
}
}
// ---------------------------------------------------------------- 粒子
private static void DrawParticles(Graphics g, GameWorld world)
{
var list = world.Particles;
for (int i = 0; i < list.Count; i++)
{
var p = list[i];
float t = p.Life / p.MaxLife;
int alpha = (int)(255 * (1f - t * t));
if (alpha <= 4) continue;
Cam.Project(p.X, p.Y, p.Z, out float sx, out float sy, out float s);
float r = Math.Max(0.8f, p.Size * s * (1f - t * 0.45f));
using var br = new SolidBrush(Color.FromArgb(alpha, p.Color));
g.FillEllipse(br, sx - r, sy - r, r * 2, r * 2);
}
}
// ---------------------------------------------------------------- 玩家
private static void DrawPlayer(Graphics g, GameWorld world)
{
var h = world.Hero;
float footX = PX(h.X, h.Y, 0f);
float footY = PY(h.X, h.Y, 0f);
float headY = PY(h.X, h.Y + h.Height, 0f);
float s = PS(h.X, 0f, 0f);
float bodyH = Math.Max(3f, footY - headY);
// ---- 地面阴影 ----
float shX = PX(h.X, 0f, 0f);
float shY = PY(h.X, 0f, 0f);
float lift = Math.Clamp(h.Y / 220f, 0f, 1f);
float shW = 58f * s * (1f - lift * 0.55f);
float shH = 17f * s * (1f - lift * 0.50f);
if (shW > 1f)
{
using var sb = new SolidBrush(Color.FromArgb((int)(120 * (1f - lift)), 0, 0, 0));
g.FillEllipse(sb, shX - shW * 0.5f, shY - shH * 0.5f, shW, shH);
}
if (world.State == RunState.GameOver && !h.Alive)
{
// 倒地:简单压扁 + 变灰
DrawHero(g, footX, footY, bodyH, s, h, flattened: true);
return;
}
DrawHero(g, footX, footY, bodyH, s, h, flattened: false);
}
private static void DrawHero(Graphics g, float footX, float footY, float bodyH, float s,
Player h, bool flattened)
{
// 影子偏移(换道侧倾)
float lean = flattened ? 0f : h.Tilt;
float cx = footX + lean * bodyH * 0.45f;
Color cloth = flattened ? Color.FromArgb(150, 110, 100, 96) : Cfg.Player;
Color clothDark = flattened ? Color.FromArgb(150, 84, 74, 70) : Color.FromArgb(176, 62, 40);
Color skin = flattened ? Color.FromArgb(150, 170, 150, 132) : Color.FromArgb(242, 196, 152);
Color hair = Color.FromArgb(48, 36, 30);
float unit = bodyH / 62f; // 1 = 站立时的相对比例
using var penBody = new Pen(cloth, Math.Max(2.2f, bodyH * 0.155f))
{ StartCap = LineCap.Round, EndCap = LineCap.Round };
using var penLimb = new Pen(clothDark, Math.Max(2f, bodyH * 0.115f))
{ StartCap = LineCap.Round, EndCap = LineCap.Round };
float hipY = footY - bodyH * 0.42f;
float shoulderY = footY - bodyH * 0.76f;
if (flattened)
{
// 趴倒
float len = bodyH * 1.05f;
using var bodyPen = new Pen(cloth, Math.Max(2.4f, bodyH * 0.17f))
{ StartCap = LineCap.Round, EndCap = LineCap.Round };
g.DrawLine(bodyPen, cx - len * 0.32f, footY - bodyH * 0.16f, cx + len * 0.38f, footY - bodyH * 0.10f);
using var headBr = new SolidBrush(skin);
float hr = bodyH * 0.135f;
g.FillEllipse(headBr, cx + len * 0.34f - hr, footY - bodyH * 0.26f - hr, hr * 2, hr * 2);
return;
}
// ---- 腿 ----
float swing = MathF.Sin(h.RunPhase);
bool airborne = !h.OnGround;
float legLen = bodyH * 0.42f;
void DrawLeg(float sign)
{
float lift = airborne ? 0.42f : MathF.Max(0f, sign * swing) * 0.75f;
float fx = cx + sign * 9f * unit + (airborne ? 0f : sign * swing * 12f * unit);
float fy = footY - legLen * (airborne ? 0.45f : lift);
float kneeX = cx + sign * 6f * unit + (fx - cx) * 0.4f;
float kneeY = (hipY + fy) * 0.5f;
g.DrawLine(penLimb, cx + sign * 5f * unit, hipY, kneeX, kneeY);
g.DrawLine(penLimb, kneeX, kneeY, fx, fy);
}
DrawLeg(-1f);
DrawLeg(+1f);
// ---- 躯干 ----
float torsoW = bodyH * 0.30f;
using (var torso = new SolidBrush(cloth))
{
var rc = new RectangleF(cx - torsoW * 0.5f, shoulderY, torsoW, hipY - shoulderY);
using var rp = RoundRect(rc, torsoW * 0.34f);
g.FillPath(torso, rp);
}
// ---- 手臂(与腿反向摆动) ----
void DrawArm(float sign)
{
float a = -sign * swing;
float ex = cx + sign * (bodyH * 0.20f) + a * bodyH * 0.15f;
float ey = shoulderY + bodyH * 0.26f - MathF.Abs(a) * bodyH * 0.10f;
float midX = cx + sign * (bodyH * 0.19f);
float midY = shoulderY + bodyH * 0.13f;
g.DrawLine(penLimb, cx + sign * bodyH * 0.13f, shoulderY + bodyH * 0.03f, midX, midY);
g.DrawLine(penLimb, midX, midY, ex, ey);
}
DrawArm(-1f);
DrawArm(+1f);
// ---- 头 ----
float headR = bodyH * 0.145f;
float headCY = shoulderY - headR * 0.95f;
using (var hb = new SolidBrush(skin))
g.FillEllipse(hb, cx - headR, headCY - headR, headR * 2, headR * 2);
// 头发(上半圆)
using (var hb = new SolidBrush(hair))
{
var rc = new RectangleF(cx - headR * 1.02f, headCY - headR * 1.10f, headR * 2.04f, headR * 1.35f);
using var rp = RoundRect(rc, headR * 0.75f);
g.FillPath(hb, rp);
}
// 滑铲时的速度线
if (h.Sliding)
{
using var sl = new Pen(Color.FromArgb(130, 255, 240, 210), Math.Max(1.4f, bodyH * 0.05f))
{ StartCap = LineCap.Round, EndCap = LineCap.Round };
for (int i = 0; i < 3; i++)
{
float yy = footY - bodyH * (0.10f + i * 0.14f);
g.DrawLine(sl, cx - bodyH * 0.42f, yy, cx - bodyH * 0.18f, yy);
}
}
}
// ---------------------------------------------------------------- HUD
private static void DrawHud(Graphics g, int w, int h, GameWorld world, bool showFps)
{
g.SmoothingMode = SmoothingMode.AntiAlias;
// ---- 左上:分数 / 金币 ----
DrawChip(g, 18, 14, 236, 92);
string score = world.Score.ToString();
using (var sh = new SolidBrush(Color.FromArgb(130, 0, 0, 0)))
g.DrawString(score, _fHuge, sh, 32 + 2, 16 + 2);
using (var sb = new SolidBrush(Color.White))
g.DrawString(score, _fHuge, sb, 32, 16);
// 金币行
float coinY = 78;
float pop = 1f + world.CoinPop * 0.35f;
float cr = 9f * pop;
using (var gb = new SolidBrush(Cfg.Gold))
g.FillEllipse(gb, 34 - cr, coinY - cr, cr * 2, cr * 2);
using (var gp = new Pen(Cfg.GoldDark, 2f))
g.DrawEllipse(gp, 34 - cr, coinY - cr, cr * 2, cr * 2);
using (var sb = new SolidBrush(Color.FromArgb(255, 245, 210)))
g.DrawString("× " + world.CoinCount, _fMid, sb, 52, coinY - 13);
// ---- 右上:速度 / 最佳 ----
string speedTxt = "速度 ×" + (world.Speed / Cfg.StartSpeed).ToString("0.00");
SizeF st = g.MeasureString(speedTxt, _fMid);
DrawChip(g, w - st.Width - 44, 16, st.Width + 26, 34);
using (var sb = new SolidBrush(Color.FromArgb(255, 226, 170)))
g.DrawString(speedTxt, _fMid, sb, w - st.Width - 31, 24);
string bestTxt = "最佳 " + world.Best;
SizeF bt = g.MeasureString(bestTxt, _fSmall);
using (var sb = new SolidBrush(Color.FromArgb(210, 236, 226, 214)))
g.DrawString(bestTxt, _fSmall, sb, w - bt.Width - 22, 58);
if (world.Muted)
{
using var sb = new SolidBrush(Color.FromArgb(150, 236, 226, 214));
g.DrawString("已静音 (M)", _fTiny, sb, w - 96, 82);
}
// ---- 中间状态层 ----
switch (world.State)
{
case RunState.Ready: DrawReady(g, w, h, world); break;
case RunState.Paused: DrawPaused(g, w, h); break;
case RunState.GameOver: DrawGameOver(g, w, h, world); break;
}
// ---- 右下角操作提示 ----
using (var sb = new SolidBrush(Color.FromArgb(120, 240, 234, 226)))
g.DrawString("← → 换道 ↑ 跳跃 ↓ 滑铲 P 暂停 M 静音 F1 调试 F11 全屏",
_fTiny, sb, 18, h - 26);
if (showFps)
{
using var db = new SolidBrush(Color.FromArgb(185, 168, 255, 196));
g.DrawString(
$"FPS {world.Fps:0} 障碍 {world.Obstacles.Count} 金币 {world.Coins.Count}" +
$" 装饰 {world.Props.Count} 粒子 {world.Particles.Count} 速度 {world.Speed:0}",
_fTiny, db, 18, h - 46);
}
}
private static void DrawReady(Graphics g, int w, int h, GameWorld world)
{
float cy = h * 0.055f; // 贴顶排布,避免压住画面中央的跑者
DrawCentered(g, "神庙逃亡", _fHuge, w, cy + 3, Color.FromArgb(130, 0, 0, 0));
DrawCentered(g, "神庙逃亡", _fHuge, w, cy, Color.FromArgb(255, 232, 176));
DrawCentered(g, "TEMPLE RUN", _fTitle, w, cy + 58, Color.FromArgb(214, 176, 120));
string[] lines =
{
"← → 左右换道",
"↑ 或 空格 跳跃越过矮石堆",
"↓ 滑铲穿过悬空横梁",
"石墙无法越过,只能换道躲避",
"收集金币可提高速度倍率"
};
for (int i = 0; i < lines.Length; i++)
DrawCentered(g, lines[i], _fMid, w, cy + 118 + i * 30, Color.FromArgb(226, 216, 202));
// 闪烁提示
float blink = (MathF.Sin(world.Clock * 4.2f) + 1f) * 0.5f;
var c = Cfg.Lerp(Color.FromArgb(120, 255, 236, 190), Color.FromArgb(255, 255, 244, 214), blink);
DrawCentered(g, "按 空格 开始奔跑", _fBig, w, cy + 118 + lines.Length * 30 + 26, c);
}
private static void DrawPaused(Graphics g, int w, int h)
{
using (var ov = new SolidBrush(Color.FromArgb(140, 10, 10, 18)))
g.FillRectangle(ov, 0, 0, w, h);
DrawCentered(g, "已 暂 停", _fHuge, w, h * 0.38f, Color.White);
DrawCentered(g, "按 P / 空格 继续 按 R 重新开始", _fMid, w, h * 0.38f + 70,
Color.FromArgb(210, 226, 218, 206));
}
private static void DrawGameOver(Graphics g, int w, int h, GameWorld world)
{
bool newRecord = world.Score >= world.Best && world.Score > 0;
float pw = Math.Min(520f, w - 80f);
float ph = 330f;
float px = (w - pw) * 0.5f;
float py = h * 0.5f - ph * 0.5f;
using (var ov = new SolidBrush(Color.FromArgb(150, 8, 8, 14)))
g.FillRectangle(ov, 0, 0, w, h);
var panel = new RectangleF(px, py, pw, ph);
using (var pp = RoundRect(panel, 18f))
{
using var pb = new SolidBrush(Color.FromArgb(226, 26, 24, 34));
g.FillPath(pb, pp);
using var pen = new Pen(Color.FromArgb(200, 214, 168, 96), 2.4f);
g.DrawPath(pen, pp);
}
DrawCentered(g, "游 戏 结 束", _fTitle, w, py + 24, Color.FromArgb(255, 138, 106));
float y = py + 92;
DrawStatRow(g, px, pw, y, "本局得分", world.Score.ToString(),
newRecord ? Cfg.Gold : Color.White, _fBig);
DrawStatRow(g, px, pw, y + 46, "金币数量", world.CoinCount.ToString(), Color.FromArgb(255, 214, 120), _fBig);
DrawStatRow(g, px, pw, y + 92, "奔跑距离", ((int)(world.Distance / 10f)) + " m", Color.FromArgb(200, 226, 216, 200), _fBig);
DrawStatRow(g, px, pw, y + 138, "历史最佳", world.Best.ToString(), Color.FromArgb(150, 226, 214, 196), _fBig);
if (newRecord)
DrawCentered(g, "★ 新 纪 录 ★", _fMid, w, py + ph - 88, Cfg.Gold);
DrawCentered(g, "按 R / 空格 再来一局", _fMid, w, py + ph - 46,
Color.FromArgb(226, 236, 226, 210));
}
private static void DrawStatRow(Graphics g, float px, float pw, float y, string label, string value,
Color valueColor, Font vf)
{
using (var lb = new SolidBrush(Color.FromArgb(190, 208, 198, 184)))
g.DrawString(label, _fMid, lb, px + 36, y + 6);
SizeF vs = g.MeasureString(value, vf);
using (var vb = new SolidBrush(valueColor))
g.DrawString(value, vf, vb, px + pw - 36 - vs.Width, y);
}
private static void DrawCentered(Graphics g, string text, Font f, int w, float y, Color color)
{
using var br = new SolidBrush(color);
SizeF sz = g.MeasureString(text, f);
g.DrawString(text, f, br, (w - sz.Width) * 0.5f, y);
}
private static void DrawChip(Graphics g, float x, float y, float w, float h)
{
var r = new RectangleF(x, y, w, h);
using var p = RoundRect(r, 10f);
using var br = new SolidBrush(Color.FromArgb(118, 12, 14, 22));
g.FillPath(br, p);
using var pen = new Pen(Color.FromArgb(86, 226, 198, 140), 1.4f);
g.DrawPath(pen, p);
}
private static GraphicsPath RoundRect(RectangleF r, float radius)
{
var p = new GraphicsPath();
float d = Math.Min(radius * 2f, Math.Min(r.Width, r.Height));
p.AddArc(r.X, r.Y, d, d, 180, 90);
p.AddArc(r.Right - d, r.Y, d, d, 270, 90);
p.AddArc(r.Right - d, r.Bottom - d, d, d, 0, 90);
p.AddArc(r.X, r.Bottom - d, d, d, 90, 90);
p.CloseFigure();
return p;
}
}
+605
View File
@@ -0,0 +1,605 @@
using System.Media;
namespace TempleRun;
/// <summary>
/// 游戏世界:状态机 + 生成器 + 物理 + 碰撞。
/// 不含任何绘制代码,纯逻辑,便于单独测试。
/// </summary>
public class GameWorld
{
public RunState State = RunState.Ready;
public readonly Player Hero = new();
public readonly List<Obstacle> Obstacles = new();
public readonly List<Coin> Coins = new();
public readonly List<Particle> Particles = new();
public readonly List<SideProp> Props = new();
// 统计
public float Speed;
public float Distance; // 累计前进距离(世界单位)
public int CoinCount;
public int Best;
public int Score => (int)(Distance / 10f) + CoinCount * 10;
// 表现层效果
public float Shake; // 屏幕震动强度(像素)
public float HitFlash; // 撞击红闪 0..1
public float CoinPop; // 吃金币时的 HUD 弹跳 0..1
public bool Muted;
public float Clock; // 累计运行时间(用于 UI 呼吸/闪烁动画)
public float Fps; // 由窗体写入,仅用于调试显示
/// <summary>死亡动画播完才允许重开,避免手快误触</summary>
public bool CanRestart => _deathDelay <= 0f;
private float _nextSpawn; // 距下一组障碍还有多远
private float _nextProp; // 距下一组路边装饰还有多远
private float _deathDelay; // 死亡动画剩余时间
private float _lastCoinSfx;
private float _sfxClock;
private readonly Random _rng = new();
private readonly string _bestPath;
public GameWorld()
{
try
{
string dir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "TempleRun");
Directory.CreateDirectory(dir);
_bestPath = Path.Combine(dir, "best.txt");
if (File.Exists(_bestPath) && int.TryParse(File.ReadAllText(_bestPath), out int b))
Best = b;
}
catch { _bestPath = null; }
Reset();
}
// ------------------------------------------------------------------
// 生命周期
// ------------------------------------------------------------------
public void Reset()
{
Hero.Reset();
Obstacles.Clear();
Coins.Clear();
Particles.Clear();
Props.Clear();
Speed = Cfg.StartSpeed;
Distance = 0;
CoinCount = 0;
Shake = 0;
HitFlash = 0;
CoinPop = 0;
_deathDelay = 0;
_nextSpawn = 1400f;
_nextProp = 120f;
// 预铺一条"石柱长廊",避免开局两侧空荡
for (float z = 400; z < Cfg.FarZ; z += 165f)
SpawnProp(z);
State = RunState.Ready;
}
public void Start()
{
Reset();
State = RunState.Playing;
}
// ------------------------------------------------------------------
// 输入
// ------------------------------------------------------------------
public bool HandleKey(Keys key)
{
switch (State)
{
case RunState.Ready:
if (key is Keys.Space or Keys.Enter) { Start(); return true; }
break;
case RunState.Playing:
if (key is Keys.Left or Keys.A) { ShiftLane(-1); return true; }
if (key is Keys.Right or Keys.D) { ShiftLane(+1); return true; }
if (key is Keys.Up or Keys.W or Keys.Space) { Jump(); return true; }
if (key is Keys.Down or Keys.S) { Slide(); return true; }
if (key is Keys.P or Keys.Escape) { State = RunState.Paused; return true; }
break;
case RunState.Paused:
if (key is Keys.P or Keys.Escape or Keys.Space) { State = RunState.Playing; return true; }
if (key is Keys.R) { Start(); return true; }
break;
case RunState.GameOver:
if (!CanRestart) return true;
if (key is Keys.R or Keys.Enter or Keys.Space) { Start(); return true; }
break;
}
if (key is Keys.M) { Muted = !Muted; return true; }
return false;
}
private void ShiftLane(int dir)
{
if (!Hero.Alive) return;
int t = Math.Clamp(Hero.TargetLane + dir, 0, Cfg.LaneCount - 1);
if (t != Hero.TargetLane)
Hero.TargetLane = t;
}
private void Jump()
{
if (!Hero.Alive || !Hero.OnGround) return;
Hero.Vy = Cfg.JumpSpeed;
Hero.OnGround = false;
Hero.Sliding = false;
Hero.SlideTimer = 0;
SpawnDust(Hero.X, Hero.Y, 0f, 6);
}
private void Slide()
{
if (!Hero.Alive) return;
// 空中按下 → 快速下落(更顺手)
if (!Hero.OnGround) Hero.Vy = Math.Min(Hero.Vy, -420f);
Hero.Sliding = true;
Hero.SlideTimer = Cfg.SlideDuration;
SpawnDust(Hero.X, Hero.Y, 0f, 8);
}
// ------------------------------------------------------------------
// 主更新
// ------------------------------------------------------------------
public void Update(float dt)
{
_sfxClock += dt;
Clock += dt;
if (State == RunState.Paused) return;
if (State == RunState.Ready)
{
// 待机:背景缓慢滚动 + 原地跑,营造氛围
RollWorld(300f * dt);
Hero.RunPhase += dt * 7.5f;
UpdateParticles(dt);
DecayEffects(dt);
return;
}
if (State == RunState.GameOver)
{
UpdateParticles(dt);
DecayEffects(dt);
if (_deathDelay > 0) _deathDelay -= dt;
return;
}
// ---------------- 游戏中 ----------------
if (!Hero.Alive)
{
RollWorld(Speed * dt * 0.25f);
UpdateParticles(dt);
DecayEffects(dt);
_deathDelay -= dt;
if (_deathDelay <= 0f) FinishRun();
return;
}
// 难度递增
float target = Cfg.StartSpeed + Distance * 0.00035f + CoinCount * Cfg.SpeedPerCoin;
Speed = Math.Min(Cfg.MaxSpeed, Math.Max(Speed, target));
Speed += Cfg.SpeedAccel * dt;
Speed = Math.Min(Speed, Cfg.MaxSpeed);
float move = Speed * dt;
Distance += move;
UpdatePlayer(dt);
RollWorld(move);
UpdateEntities(dt, move);
Generate(move);
CheckCollisions();
UpdateParticles(dt);
DecayEffects(dt);
}
private void DecayEffects(float dt)
{
Shake = Math.Max(0f, Shake - dt * 60f);
HitFlash = Math.Max(0f, HitFlash - dt * 2.6f);
CoinPop = Math.Max(0f, CoinPop - dt * 4f);
}
// ------------------------------------------------------------------
// 玩家
// ------------------------------------------------------------------
private void UpdatePlayer(float dt)
{
float targetX = Cfg.LaneX(Hero.TargetLane);
float k = 1f - MathF.Exp(-Cfg.LaneLerp * dt); // 帧率无关的指数插值
float dx = targetX - Hero.X;
Hero.X += dx * k;
if (Math.Abs(dx) < 0.6f) Hero.X = targetX;
// 侧倾:换道时身体倾斜
float wantTilt = Math.Clamp(dx / Cfg.LaneWidth, -1f, 1f) * 0.32f;
Hero.Tilt += (wantTilt - Hero.Tilt) * Math.Min(1f, dt * 9f);
// 重力
if (!Hero.OnGround || Hero.Vy > 0f)
{
Hero.Vy -= Cfg.Gravity * dt;
Hero.Y += Hero.Vy * dt;
if (Hero.Y <= 0f)
{
Hero.Y = 0f;
Hero.Vy = 0f;
if (!Hero.OnGround)
{
Hero.OnGround = true;
SpawnDust(Hero.X, 0f, 0f, 8);
}
}
}
// 滑铲计时
if (Hero.Sliding)
{
Hero.SlideTimer -= dt;
if (Hero.SlideTimer <= 0f) Hero.Sliding = false;
}
// 跑动动画(速度越快腿摆得越快)
Hero.RunPhase += dt * (4.2f + Speed / 165f);
// 跑动扬尘
if (Hero.OnGround && !Hero.Sliding && _rng.NextDouble() < dt * 26)
SpawnDust(Hero.X + (float)(_rng.NextDouble() - 0.5) * 30f, 0f, -8f, 1);
}
/// <summary>世界向玩家方向滚动:所有实体 Z 减少。</summary>
private void RollWorld(float move)
{
Hero.X += 0f;
for (int i = Obstacles.Count - 1; i >= 0; i--)
Obstacles[i].Z -= move;
for (int i = Coins.Count - 1; i >= 0; i--)
Coins[i].Z -= move;
for (int i = Props.Count - 1; i >= 0; i--)
Props[i].Z -= move;
for (int i = Particles.Count - 1; i >= 0; i--)
Particles[i].Z -= move;
}
private void UpdateEntities(float dt, float move)
{
// 金币旋转 + 回收
for (int i = Coins.Count - 1; i >= 0; i--)
{
var c = Coins[i];
c.Phase += dt * 5.5f;
if (c.Taken || c.Z < -260f) Coins.RemoveAt(i);
}
// 障碍回收
for (int i = Obstacles.Count - 1; i >= 0; i--)
if (Obstacles[i].Z < -300f) Obstacles.RemoveAt(i);
// 装饰回收
for (int i = Props.Count - 1; i >= 0; i--)
if (Props[i].Z < -300f) Props.RemoveAt(i);
}
// ------------------------------------------------------------------
// 生成器
// ------------------------------------------------------------------
private void Generate(float move)
{
_nextSpawn -= move;
if (_nextSpawn <= 0f)
{
SpawnRow(Cfg.FarZ);
float sec = (float)(Cfg.SpawnIntervalMin +
_rng.NextDouble() * (Cfg.SpawnIntervalMax - Cfg.SpawnIntervalMin));
// 速度越快,按"时间间隔"换算成距离间隔,保证反应时间一致
_nextSpawn = Math.Max(420f, sec * Speed * 0.62f);
}
_nextProp -= move;
if (_nextProp <= 0f)
{
SpawnProp(Cfg.FarZ);
_nextProp = 130f + (float)_rng.NextDouble() * 150f;
}
}
/// <summary>生成一组障碍 + 可能附带的金币。保证至少留一条可通行车道。</summary>
private void SpawnRow(float z)
{
int laneCount = _rng.NextDouble() < 0.78 ? 1 : 2; // 1~2 条道放障碍
var lanes = new[] { 0, 1, 2 };
// 洗牌
for (int i = lanes.Length - 1; i > 0; i--)
{
int j = _rng.Next(i + 1);
(lanes[i], lanes[j]) = (lanes[j], lanes[i]);
}
var used = new List<int>();
for (int i = 0; i < laneCount; i++)
{
int lane = lanes[i];
used.Add(lane);
ObstacleKind kind;
double r = _rng.NextDouble();
// 两条道同时出现时,避免双"整道墙"把路堵死(剩 1 条空道仍可走,允许)
kind = r < 0.40 ? ObstacleKind.Low
: r < 0.72 ? ObstacleKind.High
: ObstacleKind.Full;
Obstacles.Add(new Obstacle { Z = z, Lane = lane, Kind = kind });
}
// 在空道上撒一串金币
if (_rng.NextDouble() < 0.72)
{
int freeLane = -1;
for (int i = 0; i < Cfg.LaneCount; i++)
if (!used.Contains(i)) { freeLane = i; break; }
if (freeLane >= 0)
{
int n = 3 + _rng.Next(4);
float dz = 52f;
// 金币以本组障碍为中心分布,稍微错开一点更自然
float start = z - (n - 1) * dz * 0.5f + (float)(_rng.NextDouble() - 0.5) * 40f;
for (int i = 0; i < n; i++)
{
Coins.Add(new Coin
{
Z = start + i * dz,
Lane = freeLane,
Phase = i * 0.6f
});
}
}
}
}
private void SpawnProp(float z)
{
int side = _rng.NextDouble() < 0.5 ? -1 : 1;
int type = _rng.Next(100) switch
{
< 42 => 0, // 石柱
< 62 => 1, // 火把
< 90 => 2, // 枯树
_ => 3 // 碎石
};
float h = type switch
{
0 => 150f + (float)_rng.NextDouble() * 90f,
1 => 92f,
2 => 130f + (float)_rng.NextDouble() * 70f,
_ => 26f + (float)_rng.NextDouble() * 22f
};
Props.Add(new SideProp
{
Z = z,
Side = side,
Type = type,
Height = h,
Phase = (float)_rng.NextDouble() * 6.28f
});
}
// ------------------------------------------------------------------
// 碰撞
// ------------------------------------------------------------------
private void CheckCollisions()
{
float pNear = -Cfg.PlayerHalfDepth;
float pFar = Cfg.PlayerHalfDepth;
float py0 = Hero.Y + 5f; // 判定区间略微收缩,手感宽容
float py1 = Hero.Y + Hero.Height - 5f;
float px = Hero.X;
const float halfBodyW = 15f;
// --- 障碍 ---
for (int i = 0; i < Obstacles.Count; i++)
{
var o = Obstacles[i];
float oNear = o.Z - o.Depth * 0.5f;
float oFar = o.Z + o.Depth * 0.5f;
if (oFar < pNear || oNear > pFar) continue; // Z 不重叠
if (Math.Abs(px - o.CenterX) > o.Width * 0.5f + halfBodyW) continue; // 不在同一道
// 高度区间是否相交
bool hit = py0 < o.TopY && py1 > o.BottomY;
//"整道墙"永远相交
if (o.Kind == ObstacleKind.Full) hit = true;
if (hit) { Kill(o); return; }
if (!o.Scored && o.Z < 0f)
{
o.Scored = true;
// 成功躲避的微小正反馈
Shake = Math.Max(Shake, 1.2f);
}
}
// --- 金币 ---
float coinNear = -34f, coinFar = 34f;
for (int i = 0; i < Coins.Count; i++)
{
var c = Coins[i];
if (c.Taken) continue;
if (c.Z < coinNear || c.Z > coinFar) continue;
if (Math.Abs(px - c.CenterX) > Cfg.LaneWidth * 0.45f) continue;
// 金币在 36 高度,跳跃或站立都能吃到,滑铲吃不到
float cy = c.Y;
if (cy < py0 - 22f || cy > py1 + 22f) continue;
c.Taken = true;
CoinCount++;
CoinPop = 1f;
Speed = Math.Min(Cfg.MaxSpeed, Speed + Cfg.SpeedPerCoin * 0.35f);
SpawnCoinBurst(c.CenterX, c.Y, c.Z);
PlayCoinSfx();
}
}
private void Kill(Obstacle o)
{
if (!Hero.Alive) return;
Hero.Alive = false;
Hero.Sliding = false;
_deathDelay = 0.85f;
Shake = 16f;
HitFlash = 1f;
SpawnDebris(o.CenterX, Hero.Y + 26f, 0f);
PlayHitSfx();
}
private void FinishRun()
{
State = RunState.GameOver;
if (Score > Best)
{
Best = Score;
try { if (_bestPath != null) File.WriteAllText(_bestPath, Best.ToString()); }
catch { /* 忽略写盘失败 */ }
}
}
// ------------------------------------------------------------------
// 粒子
// ------------------------------------------------------------------
public void AddParticle(Particle p) => Particles.Add(p);
private void SpawnDust(float x, float y, float z, int count)
{
for (int i = 0; i < count; i++)
{
Particles.Add(new Particle
{
X = x + (float)(_rng.NextDouble() - 0.5) * 26f,
Y = y + (float)_rng.NextDouble() * 8f,
Z = z + (float)(_rng.NextDouble() - 0.5) * 20f,
Vx = (float)(_rng.NextDouble() - 0.5) * 60f,
Vy = 20f + (float)_rng.NextDouble() * 55f,
Vz = -30f - (float)_rng.NextDouble() * 40f,
MaxLife = 0.35f + (float)_rng.NextDouble() * 0.25f,
Life = 0f,
Size = 2.4f + (float)_rng.NextDouble() * 3.2f,
GravityScale = 0.4f,
Color = Color.FromArgb(196, 172, 140)
});
}
}
private void SpawnCoinBurst(float x, float y, float z)
{
for (int i = 0; i < 10; i++)
{
float a = (float)(_rng.NextDouble() * Math.PI * 2);
float sp = 40f + (float)_rng.NextDouble() * 90f;
Particles.Add(new Particle
{
X = x, Y = y, Z = z,
Vx = (float)Math.Cos(a) * sp,
Vy = 50f + (float)_rng.NextDouble() * 100f,
Vz = (float)Math.Sin(a) * sp * 0.6f,
MaxLife = 0.45f + (float)_rng.NextDouble() * 0.3f,
Size = 2.8f + (float)_rng.NextDouble() * 3.4f,
GravityScale = 0.8f,
Color = _rng.NextDouble() < 0.5 ? Cfg.Gold : Color.FromArgb(255, 240, 170)
});
}
}
private void SpawnDebris(float x, float y, float z)
{
for (int i = 0; i < 26; i++)
{
float a = (float)(_rng.NextDouble() * Math.PI * 2);
float sp = 60f + (float)_rng.NextDouble() * 190f;
Particles.Add(new Particle
{
X = x, Y = y, Z = z,
Vx = (float)Math.Cos(a) * sp,
Vy = 40f + (float)_rng.NextDouble() * 210f,
Vz = (float)Math.Sin(a) * sp,
MaxLife = 0.6f + (float)_rng.NextDouble() * 0.5f,
Size = 3f + (float)_rng.NextDouble() * 5f,
GravityScale = 1f,
Color = _rng.NextDouble() < 0.5
? Color.FromArgb(210, 90, 60)
: Color.FromArgb(150, 140, 130)
});
}
}
private void UpdateParticles(float dt)
{
for (int i = Particles.Count - 1; i >= 0; i--)
{
var p = Particles[i];
p.Life += dt;
if (p.Life >= p.MaxLife) { Particles.RemoveAt(i); continue; }
p.X += p.Vx * dt;
p.Y += p.Vy * dt;
p.Z += p.Vz * dt;
p.Vy -= Cfg.Gravity * p.GravityScale * dt;
if (p.Y < 0f)
{
p.Y = 0f;
p.Vy *= -0.32f; // 轻微反弹
p.Vx *= 0.6f;
p.Vz *= 0.6f;
}
}
}
// ------------------------------------------------------------------
// 音效(系统提示音,零依赖;M 键静音)
// ------------------------------------------------------------------
private void PlayCoinSfx()
{
if (Muted || _sfxClock - _lastCoinSfx < 0.07f) return;
_lastCoinSfx = _sfxClock;
try { SystemSounds.Asterisk.Play(); } catch { }
}
private void PlayHitSfx()
{
if (Muted) return;
try { SystemSounds.Hand.Play(); } catch { }
}
}
+14
View File
@@ -0,0 +1,14 @@
namespace TempleRun;
internal static class Program
{
[STAThread]
private static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.SetHighDpiMode(HighDpiMode.SystemAware);
Application.Run(new GameForm());
}
}
+18
View File
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<AssemblyName>TempleRun</AssemblyName>
<RootNamespace>TempleRun</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<SatelliteResourceLanguages>zh-Hans</SatelliteResourceLanguages>
<!-- 关闭自动生成的 ApplicationConfiguration,改由 Program.cs 手工初始化 -->
<EnableWindowsTargeting>true</EnableWindowsTargeting>
</PropertyGroup>
</Project>
Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

+53
View File
@@ -0,0 +1,53 @@
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Windows.Forms
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class Win32 {
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; }
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT r);
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
}
"@
$root = "c:\Users\32453\CodeBuddy\20260825142915\TempleRun"
$exe = Join-Path $root "bin\Release\net8.0-windows\TempleRun.exe"
$out = Join-Path $root "_shot"
New-Item -ItemType Directory -Force -Path $out | Out-Null
$p = Start-Process -FilePath $exe -PassThru
Start-Sleep -Seconds 3
$p.Refresh()
function Grab([string]$name) {
$h = $p.MainWindowHandle
if ($h -eq [IntPtr]::Zero) { Write-Output "no window"; return }
[Win32]::SetForegroundWindow($h) | Out-Null
Start-Sleep -Milliseconds 700
$r = New-Object 'Win32+RECT'
[Win32]::GetWindowRect($h, [ref]$r) | Out-Null
$w = $r.Right - $r.Left
$ht = $r.Bottom - $r.Top
if ($w -le 0 -or $ht -le 0) { Write-Output "bad rect $w x $ht"; return }
$bmp = New-Object System.Drawing.Bitmap $w, $ht
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.CopyFromScreen($r.Left, $r.Top, 0, 0, $bmp.Size)
$path = Join-Path $out "$name.png"
$bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png)
$g.Dispose(); $bmp.Dispose()
Write-Output "saved $name : $w x $ht"
}
Grab "01_ready"
[System.Windows.Forms.SendKeys]::SendWait(" ")
Start-Sleep -Seconds 4
Grab "02_playing"
Start-Sleep -Seconds 4
Grab "03_playing2"
try { $p.Kill() } catch {}
Write-Output "exit=$($p.HasExited)"
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="TempleRun.app" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 / 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>
@@ -0,0 +1,103 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos;
using F9WebAiTest.Application.Dtos.Attendance;
using F9WebAiTest.Application.Services;
using F9WebAiTest.Application.Services.Attendance;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9WebAiTest.Api.Controllers.Attendance;
[ApiController]
[Route("api/attendance")]
[Authorize]
public class AttendanceController : ControllerBase
{
private readonly IAttendanceService _service;
public AttendanceController(IAttendanceService service)
{
_service = service;
}
// POST /api/attendance/punch 打卡
[HttpPost("punch")]
public async Task<IActionResult> Punch()
{
var result = await _service.PunchAsync(GetUserId());
return Ok(ApiResult<PunchResultDto>.Ok(result));
}
// GET /api/attendance/my 我的考勤记录
[HttpGet("my")]
public async Task<IActionResult> GetMy([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 10)
{
var result = await _service.GetMyRecordsAsync(GetUserId(), pageIndex, pageSize);
return Ok(ApiResult<PageResult<AttendanceDto>>.Ok(result));
}
// GET /api/attendance/my/summary 我的考勤统计
[HttpGet("my/summary")]
public async Task<IActionResult> GetMySummary([FromQuery] string month)
{
var result = await _service.GetMySummaryAsync(GetUserId(), month);
return Ok(ApiResult<AttendanceSummaryDto>.Ok(result));
}
// GET /api/attendance 全部考勤(管理员)
[HttpGet]
public async Task<IActionResult> GetAll([FromQuery] AttendanceQueryDto query)
{
var result = await _service.GetAllRecordsAsync(query);
return Ok(ApiResult<PageResult<AttendanceDto>>.Ok(result));
}
// POST /api/attendance/leave 申请请假
[HttpPost("leave")]
public async Task<IActionResult> ApplyLeave([FromBody] CreateLeaveDto dto)
{
try
{
var result = await _service.ApplyLeaveAsync(GetUserId(), dto);
return Ok(ApiResult<LeaveRequestDto>.Ok(result, "申请已提交"));
}
catch (ServiceException ex)
{
return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code));
}
}
// GET /api/attendance/leave/my 我的请假
[HttpGet("leave/my")]
public async Task<IActionResult> GetMyLeaves()
{
var result = await _service.GetMyLeavesAsync(GetUserId());
return Ok(ApiResult<List<LeaveRequestDto>>.Ok(result));
}
// GET /api/attendance/leave 全部请假(管理员)
[HttpGet("leave")]
public async Task<IActionResult> GetAllLeaves([FromQuery] string status)
{
var result = await _service.GetAllLeavesAsync(status);
return Ok(ApiResult<List<LeaveRequestDto>>.Ok(result));
}
// PUT /api/attendance/leave/{id}/approve 审批请假
[HttpPut("leave/{id}/approve")]
public async Task<IActionResult> ApproveLeave(long id, [FromBody] ApproveLeaveDto dto)
{
try
{
var approverName = User.FindFirst("realName")?.Value ?? "管理员";
var result = await _service.ApproveLeaveAsync(id, GetUserId(), approverName, dto);
return Ok(ApiResult<LeaveRequestDto>.Ok(result, "审批完成"));
}
catch (ServiceException ex)
{
return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code));
}
}
private long GetUserId() => long.Parse(User.FindFirst("id")!.Value);
}
@@ -0,0 +1,109 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos;
using F9WebAiTest.Application.Dtos.Auth;
using F9WebAiTest.Application.Services;
using F9WebAiTest.Application.Services.Auth;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9WebAiTest.Api.Controllers.Auth;
[ApiController]
[Route("api/auth")]
public class AuthController : ControllerBase
{
private readonly IAuthService _authService;
public AuthController(IAuthService authService)
{
_authService = authService;
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginDto dto)
{
try
{
var result = await _authService.LoginAsync(dto);
return Ok(ApiResult<LoginResultDto>.Ok(result, "登录成功"));
}
catch (ServiceException ex)
{
return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code));
}
}
[HttpPost("refresh")]
public async Task<IActionResult> Refresh([FromBody] RefreshTokenDto dto)
{
try
{
var result = await _authService.RefreshTokenAsync(dto.RefreshToken);
return Ok(ApiResult<LoginResultDto>.Ok(result, "刷新成功"));
}
catch (ServiceException ex)
{
return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code));
}
}
[Authorize]
[HttpPost("logout")]
public async Task<IActionResult> Logout([FromBody] RefreshTokenDto dto)
{
await _authService.RevokeRefreshTokenAsync(dto.RefreshToken);
return Ok(ApiResult<object>.Ok(null, "已退出登录"));
}
[Authorize]
[HttpGet("me")]
public async Task<IActionResult> GetCurrentUser()
{
var id = GetUserId();
var user = await _authService.GetCurrentUserAsync(id);
return Ok(ApiResult<EmployeeInfoDto>.Ok(user));
}
[Authorize]
[HttpGet("menus")]
public async Task<IActionResult> GetMenus()
{
var id = GetUserId();
var menus = await _authService.GetUserMenusAsync(id);
return Ok(ApiResult<List<MenuDto>>.Ok(menus));
}
[Authorize]
[HttpGet("permissions")]
public async Task<IActionResult> GetPermissions()
{
var id = GetUserId();
var perms = await _authService.GetUserPermissionsAsync(id);
return Ok(ApiResult<List<string>>.Ok(perms));
}
[Authorize]
[HttpPut("password")]
public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordDto dto)
{
try
{
await _authService.ChangePasswordAsync(GetUserId(), dto);
return Ok(ApiResult<object>.Ok(null, "密码修改成功"));
}
catch (ServiceException ex)
{
return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code));
}
}
[Authorize]
[HttpPut("profile")]
public async Task<IActionResult> UpdateProfile([FromBody] UpdateProfileDto dto)
{
await _authService.UpdateProfileAsync(GetUserId(), dto);
return Ok(ApiResult<object>.Ok(null, "资料更新成功"));
}
private long GetUserId() => long.Parse(User.FindFirst("id")!.Value);
}
@@ -0,0 +1,153 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos;
using F9WebAiTest.Application.Dtos.Base;
using F9WebAiTest.Application.Services;
using F9WebAiTest.Application.Services.Base;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9WebAiTest.Api.Controllers.Base;
// ==================== 工艺庄口 ====================
[ApiController]
[Route("api/craftzhuangkou")]
[Authorize]
public class CraftZhuangKouController : ControllerBase
{
private readonly ICraftZhuangKouService _svc;
public CraftZhuangKouController(ICraftZhuangKouService svc) => _svc = svc;
[HttpGet]
public async Task<IActionResult> GetPaged([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20, [FromQuery] string keyword = "", [FromQuery] int? year = null)
=> Ok(ApiResult<PageResult<CraftZhuangKouDto>>.Ok(await _svc.GetPagedAsync(pageIndex, pageSize, keyword, year)));
[HttpPost]
public async Task<IActionResult> Create([FromBody] CraftZhuangKouCreateDto dto)
{
try { return Ok(ApiResult<CraftZhuangKouDto>.Ok(await _svc.CreateAsync(dto), "创建成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(long id, [FromBody] CraftZhuangKouCreateDto dto)
{
try { return Ok(ApiResult<CraftZhuangKouDto>.Ok(await _svc.UpdateAsync(id, dto), "更新成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
try { await _svc.DeleteAsync(id); return Ok(ApiResult<object>.Ok(null, "删除成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
}
// ==================== 生丝规格 ====================
[ApiController]
[Route("api/shengsiguige")]
[Authorize]
public class ShengsiGuigeController : ControllerBase
{
private readonly IShengsiGuigeService _svc;
public ShengsiGuigeController(IShengsiGuigeService svc) => _svc = svc;
[HttpGet]
public async Task<IActionResult> GetPaged([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20, [FromQuery] string keyword = "")
=> Ok(ApiResult<PageResult<ShengsiGuigeDto>>.Ok(await _svc.GetPagedAsync(pageIndex, pageSize, keyword)));
[HttpGet("all")]
public async Task<IActionResult> GetAllGuige()
=> Ok(ApiResult<List<string>>.Ok(await _svc.GetAllGuigeAsync()));
[HttpPost]
public async Task<IActionResult> Create([FromBody] ShengsiGuigeDto dto)
{
try { return Ok(ApiResult<ShengsiGuigeDto>.Ok(await _svc.CreateAsync(dto), "创建成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(long id, [FromBody] ShengsiGuigeDto dto)
{
try { return Ok(ApiResult<ShengsiGuigeDto>.Ok(await _svc.UpdateAsync(id, dto), "更新成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
try { await _svc.DeleteAsync(id); return Ok(ApiResult<object>.Ok(null, "删除成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
}
// ==================== 生丝定级指标 ====================
[ApiController]
[Route("api/shengsidingjizhibiao")]
[Authorize]
public class ShengsiDingjizhibiaoController : ControllerBase
{
private readonly IShengsiDingjizhibiaoService _svc;
public ShengsiDingjizhibiaoController(IShengsiDingjizhibiaoService svc) => _svc = svc;
[HttpGet]
public async Task<IActionResult> GetPaged([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20, [FromQuery] string keyword = "", [FromQuery] int? standard = null, [FromQuery] string guige = "")
=> Ok(ApiResult<PageResult<ShengsiDingjizhibiaoDto>>.Ok(await _svc.GetPagedAsync(pageIndex, pageSize, keyword, standard, guige)));
[HttpPost]
public async Task<IActionResult> Create([FromBody] ShengsiDingjizhibiaoDto dto)
{
try { return Ok(ApiResult<ShengsiDingjizhibiaoDto>.Ok(await _svc.CreateAsync(dto), "创建成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(long id, [FromBody] ShengsiDingjizhibiaoDto dto)
{
try { return Ok(ApiResult<ShengsiDingjizhibiaoDto>.Ok(await _svc.UpdateAsync(id, dto), "更新成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
try { await _svc.DeleteAsync(id); return Ok(ApiResult<object>.Ok(null, "删除成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
}
// ==================== 机组车台 ====================
[ApiController]
[Route("api/jizuchetai")]
[Authorize]
public class JizuChetaiController : ControllerBase
{
private readonly IJizuChetaiService _svc;
public JizuChetaiController(IJizuChetaiService svc) => _svc = svc;
[HttpGet]
public async Task<IActionResult> GetPaged([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20, [FromQuery] string keyword = "")
=> Ok(ApiResult<PageResult<JizuChetaiDto>>.Ok(await _svc.GetPagedAsync(pageIndex, pageSize, keyword)));
[HttpPost]
public async Task<IActionResult> Create([FromBody] JizuChetaiDto dto)
{
try { return Ok(ApiResult<JizuChetaiDto>.Ok(await _svc.CreateAsync(dto), "创建成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(long id, [FromBody] JizuChetaiDto dto)
{
try { return Ok(ApiResult<JizuChetaiDto>.Ok(await _svc.UpdateAsync(id, dto), "更新成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
try { await _svc.DeleteAsync(id); return Ok(ApiResult<object>.Ok(null, "删除成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
}
@@ -0,0 +1,191 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos;
using F9WebAiTest.Application.Dtos.Notice;
using F9WebAiTest.Application.Services;
using F9WebAiTest.Application.Services.Auth;
using F9WebAiTest.Application.Services.Notice;
using F9WebAiTest.Domain.Entities;
using F9WebAiTest.Domain.Entities.Notice;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9WebAiTest.Api.Controllers.Notice;
// ==================== 消息类型管理(管理员) ====================
[ApiController]
[Route("api/notification/type")]
[Authorize]
public class NotificationTypeController : ControllerBase
{
private readonly INotificationService _svc;
public NotificationTypeController(INotificationService svc) => _svc = svc;
[HttpGet]
public async Task<IActionResult> GetList()
=> Ok(ApiResult<List<NotificationTypeDto>>.Ok(await _svc.GetTypeListAsync()));
[HttpPost]
public async Task<IActionResult> Create([FromBody] NotificationTypeCreateDto dto)
{
try { return Ok(ApiResult<NotificationTypeDto>.Ok(await _svc.CreateTypeAsync(dto, GetUserId()), "创建成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(long id, [FromBody] NotificationTypeCreateDto dto)
{
try { return Ok(ApiResult<NotificationTypeDto>.Ok(await _svc.UpdateTypeAsync(id, dto), "更新成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
try { await _svc.DeleteTypeAsync(id); return Ok(ApiResult<object>.Ok(null, "删除成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
private long GetUserId() => long.Parse(User.FindFirst("id")!.Value);
}
// ==================== 用户端:订阅 + 消息 ====================
[ApiController]
[Route("api/notification")]
[Authorize]
public class NotificationController : ControllerBase
{
private readonly INotificationService _svc;
private readonly IMediator _mediator;
private readonly IAuthService _authService;
public NotificationController(INotificationService svc, IMediator mediator, IAuthService authService)
{
_svc = svc;
_mediator = mediator;
_authService = authService;
}
// 可订阅的消息类型列表(有权限的才可见/可订阅)
[HttpGet("subscribe")]
public async Task<IActionResult> GetSubscribeList()
=> Ok(ApiResult<List<NotificationTypeDto>>.Ok(await _svc.GetSubscribeListAsync(GetUserId())));
// 订阅 / 取消订阅
[HttpPost("subscribe")]
public async Task<IActionResult> Subscribe([FromBody] SubscribeDto dto)
{
try
{
await _svc.SubscribeAsync(GetUserId(), dto.NotificationTypeId, dto.Subscribe);
return Ok(ApiResult<object>.Ok(null, dto.Subscribe ? "订阅成功" : "已取消订阅"));
}
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// 我的消息列表(readStatus: 0未读 1已读;category: 0通知 1待办 2公告)
[HttpGet("messages")]
public async Task<IActionResult> GetMyMessages([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20, [FromQuery] int? readStatus = null, [FromQuery] int? category = null)
=> Ok(ApiResult<PageResult<NotificationMessageDto>>.Ok(await _svc.GetMyMessagesAsync(GetUserId(), pageIndex, pageSize, readStatus, category)));
// 我的待办列表
[HttpGet("todos")]
public async Task<IActionResult> GetMyTodos([FromQuery] int? status = null)
=> Ok(ApiResult<List<TodoItemDto>>.Ok(await _svc.GetMyTodosAsync(GetUserId(), status)));
// 完成待办
[HttpPost("todos/{id}/complete")]
public async Task<IActionResult> CompleteTodo(long id, [FromBody] CompleteTodoDto dto)
{
try { await _svc.CompleteTodoAsync(GetUserId(), id, dto.HandleResult); return Ok(ApiResult<object>.Ok(null, "已完成")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// 标记已读
[HttpPut("messages/{id}/read")]
public async Task<IActionResult> MarkRead(long id)
{
try { await _svc.MarkReadAsync(GetUserId(), id); return Ok(ApiResult<object>.Ok(null)); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// 全部已读
[HttpPut("messages/read-all")]
public async Task<IActionResult> MarkAllRead()
{
await _svc.MarkAllReadAsync(GetUserId());
return Ok(ApiResult<object>.Ok(null));
}
// 未读数量
[HttpGet("unread-count")]
public async Task<IActionResult> GetUnreadCount()
=> Ok(ApiResult<UnreadCountDto>.Ok(new UnreadCountDto { Count = await _svc.GetUnreadCountAsync(GetUserId()) }));
// 触发一条消息(模拟业务系统触发,用于演示/测试)
[HttpPost("trigger")]
public async Task<IActionResult> Trigger([FromBody] TriggerDto dto)
{
try
{
var msgId = await _mediator.Send(new CreateNotificationCommand
{
NotificationTypeCode = dto.Code,
BusinessType = dto.BusinessType ?? "Manual",
BusinessId = dto.BusinessId ?? Guid.NewGuid().ToString("N"),
Title = dto.Title ?? "测试消息",
Content = dto.Content ?? "这是一条测试消息",
Priority = dto.Priority ?? NotificationPriority.Normal,
RoutePath = dto.RoutePath ?? "",
ForceReceivers = dto.AssigneeIds ?? new List<long>(),
AssigneeIds = dto.AssigneeIds ?? new List<long>(),
CreatorId = GetUserId(),
CreatorName = User.FindFirst("realName")?.Value ?? "系统"
});
return Ok(ApiResult<object>.Ok(new { id = msgId }, "消息已创建并即时推送给在线接收人"));
}
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// 发送广播通知(拥有通知广播权限的用户可发送,空接收人 = 订阅广播)
[HttpPost("broadcast")]
public async Task<IActionResult> Broadcast([FromBody] BroadcastNotificationDto dto)
{
try
{
var userId = GetUserId();
var perms = await _authService.GetUserPermissionsAsync(userId);
if (!perms.Contains("notification:broadcast"))
return StatusCode(403, ApiResult<object>.Fail("您没有发送广播通知的权限", 403));
var msgId = await _mediator.Send(new CreateNotificationCommand
{
NotificationTypeCode = "Broadcast",
BusinessType = "Broadcast",
BusinessId = Guid.NewGuid().ToString("N"),
Title = dto.Title,
Content = dto.Content,
Priority = dto.Priority ?? NotificationPriority.Normal,
RoutePath = dto.RoutePath ?? "",
ForceReceivers = new List<long>(),
CreatorId = userId,
CreatorName = User.FindFirst("realName")?.Value ?? "系统"
});
return Ok(ApiResult<object>.Ok(new { id = msgId }, "广播消息已创建"));
}
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
private long GetUserId() => long.Parse(User.FindFirst("id")!.Value);
}
public class TriggerDto
{
public string Code { get; set; } = "TeyeFound";
public string Title { get; set; }
public string Content { get; set; }
public string BusinessType { get; set; }
public string BusinessId { get; set; }
public NotificationPriority? Priority { get; set; }
public string RoutePath { get; set; }
public List<long> AssigneeIds { get; set; }
}
@@ -0,0 +1,41 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos;
using F9WebAiTest.Application.Dtos.Prod;
using F9WebAiTest.Application.Services;
using F9WebAiTest.Application.Services.Prod;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9WebAiTest.Api.Controllers.Prod;
[ApiController]
[Route("api/hbjy")]
[Authorize]
public class HbjyController : ControllerBase
{
private readonly IHbjyService _svc;
public HbjyController(IHbjyService svc) => _svc = svc;
// 黑板检验列表
[HttpGet]
public async Task<IActionResult> GetPaged(
[FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20,
[FromQuery] DateTime? date = null, [FromQuery] long? deptId = null)
=> Ok(ApiResult<PageResult<HbjyListItemDto>>.Ok(await _svc.GetPagedAsync(pageIndex, pageSize, date, deptId)));
// 获取检验编辑数据
[HttpGet("{lsjlId}")]
public async Task<IActionResult> GetEdit(long lsjlId)
{
try { return Ok(ApiResult<HbjyEditDto>.Ok(await _svc.GetEditAsync(lsjlId))); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// 保存黑板检验
[HttpPost]
public async Task<IActionResult> Save([FromBody] HbjySaveDto dto)
{
try { await _svc.SaveAsync(dto); return Ok(ApiResult<object>.Ok(null, "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
}
@@ -0,0 +1,58 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos;
using F9WebAiTest.Application.Dtos.Base;
using F9WebAiTest.Application.Dtos.Prod;
using F9WebAiTest.Application.Services;
using F9WebAiTest.Application.Services.Base;
using F9WebAiTest.Application.Services.Prod;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9WebAiTest.Api.Controllers.Prod;
[ApiController]
[Route("api/lsjl")]
[Authorize]
public class LsjlController : ControllerBase
{
private readonly ILsjlService _svc;
private readonly IJizuChetaiService _jzSvc;
public LsjlController(ILsjlService svc, IJizuChetaiService jzSvc)
{
_svc = svc;
_jzSvc = jzSvc;
}
[HttpGet]
public async Task<IActionResult> GetPaged(
[FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20,
[FromQuery] DateTime? date = null, [FromQuery] long? deptId = null, [FromQuery] string keyword = "")
=> Ok(ApiResult<PageResult<LsjlDto>>.Ok(await _svc.GetPagedAsync(pageIndex, pageSize, date, deptId, keyword)));
// 根据车间+机组查询车台号范围
[HttpGet("chetai")]
public async Task<IActionResult> GetChetai([FromQuery] long deptId, [FromQuery] int jizu)
=> Ok(ApiResult<JizuChetaiDto>.Ok(await _jzSvc.GetByDepIdAndJizuAsync(deptId, jizu)));
[HttpPost]
public async Task<IActionResult> Create([FromBody] LsjlCreateDto dto)
{
try { return Ok(ApiResult<LsjlDto>.Ok(await _svc.CreateAsync(dto), "创建成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(long id, [FromBody] LsjlCreateDto dto)
{
try { return Ok(ApiResult<LsjlDto>.Ok(await _svc.UpdateAsync(id, dto), "更新成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
try { await _svc.DeleteAsync(id); return Ok(ApiResult<object>.Ok(null, "删除成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
}
@@ -0,0 +1,41 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos;
using F9WebAiTest.Application.Dtos.Prod;
using F9WebAiTest.Application.Services;
using F9WebAiTest.Application.Services.Prod;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9WebAiTest.Api.Controllers.Prod;
[ApiController]
[Route("api/xdjy")]
[Authorize]
public class XdjyController : ControllerBase
{
private readonly IXdjyService _svc;
public XdjyController(IXdjyService svc) => _svc = svc;
// 纤度检验列表(落丝记录 + 检验汇总)
[HttpGet]
public async Task<IActionResult> GetPaged(
[FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20,
[FromQuery] DateTime? date = null, [FromQuery] long? deptId = null)
=> Ok(ApiResult<PageResult<XdjyListItemDto>>.Ok(await _svc.GetPagedAsync(pageIndex, pageSize, date, deptId)));
// 获取检验编辑数据
[HttpGet("{lsjlId}")]
public async Task<IActionResult> GetEdit(long lsjlId)
{
try { return Ok(ApiResult<XdjyEditDto>.Ok(await _svc.GetEditAsync(lsjlId))); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// 保存纤度检验
[HttpPost]
public async Task<IActionResult> Save([FromBody] XdjySaveDto dto)
{
try { await _svc.SaveAsync(dto); return Ok(ApiResult<object>.Ok(null, "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
}
@@ -0,0 +1,55 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Application.CQRS.SYS.Departments.Commands;
using F9WebAiTest.Application.CQRS.SYS.Departments.Queries;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9WebAiTest.Api.Controllers;
/// <summary>系统部门管理(DDD + CQRS)</summary>
[ApiController]
[Route("api/sys/department")]
[Authorize]
public class SysDepartmentController : ControllerBase
{
private readonly IMediator _mediator;
public SysDepartmentController(IMediator mediator) => _mediator = mediator;
[HttpGet]
public async Task<IActionResult> GetAll()
=> Ok(ApiResult<object>.Ok(await _mediator.Send(new GetDepartmentAllQuery())));
[HttpPost]
public async Task<IActionResult> Create([FromBody] Department_CreateCommand command)
{
try { return Ok(ApiResult<object>.Ok(new { id = await _mediator.Send(command) }, "创建成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(long id, [FromBody] Department_UpdateCommand command)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(command with { Id = id }), "修改成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(new Department_DelCommand(id)), "删除成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpPut("{id}/active")]
public async Task<IActionResult> ActiveOrDeactivate(long id, [FromBody] Department_ActiveOrDeactivateCommand command)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(command with { Id = id }), "操作成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
}
@@ -0,0 +1,68 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Application.CQRS.SYS.RBAC.Menus.Commands;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Menus;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9WebAiTest.Api.Controllers;
/// <summary>系统菜单管理(DDD + CQRS)</summary>
[ApiController]
[Route("api/sys/menu")]
[Authorize]
public class SysMenuController : ControllerBase
{
private readonly IMediator _mediator;
private readonly IMenuReadRepository _menuReadRepository;
public SysMenuController(IMediator mediator, IMenuReadRepository menuReadRepository)
{
_mediator = mediator;
_menuReadRepository = menuReadRepository;
}
[HttpGet("tree")]
public async Task<IActionResult> GetTree()
=> Ok(ApiResult<object>.Ok(await _menuReadRepository.GetTreeMenuAsync()));
[HttpGet]
public async Task<IActionResult> GetAll([FromQuery] string like = "", [FromQuery] long? parentId = null)
=> Ok(ApiResult<object>.Ok(await _menuReadRepository.GetAllMenuAsync(like, parentId)));
[HttpGet("{id}")]
public async Task<IActionResult> GetById(long id)
=> Ok(ApiResult<object>.Ok(await _menuReadRepository.GetMenuByIdForEditAsync(id)));
[HttpPost]
public async Task<IActionResult> Create([FromBody] Menu_CreateCommand command)
{
try { return Ok(ApiResult<object>.Ok(new { id = await _mediator.Send(command) }, "创建成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(long id, [FromBody] Menu_UpdateCommand command)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(command with { Id = id }), "修改成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(new Menu_DelCommand(id)), "删除成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpPut("{id}/active")]
public async Task<IActionResult> ActiveOrDeactivate(long id, [FromBody] Menu_ActiveOrDeactivateCommand command)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(command with { Id = id }), "操作成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
}
@@ -0,0 +1,80 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Application.CQRS.SYS.RBAC.Roles.Commands;
using F9WebAiTest.Application.CQRS.SYS.RBAC.Roles.Queries;
using F9WebAiTest.Domain.Enums.SYS.RBAC;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9WebAiTest.Api.Controllers;
/// <summary>系统角色管理(DDD + CQRS)</summary>
[ApiController]
[Route("api/sys/role")]
[Authorize]
public class SysRoleController : ControllerBase
{
private readonly IMediator _mediator;
public SysRoleController(IMediator mediator) => _mediator = mediator;
[HttpGet]
public async Task<IActionResult> GetAll()
=> Ok(ApiResult<object>.Ok(await _mediator.Send(new GetRoleAllQuery())));
[HttpGet("{id}")]
public async Task<IActionResult> GetById(long id)
=> Ok(ApiResult<object>.Ok(await _mediator.Send(new GetRoleByIdForEditQuery(id))));
[HttpGet("{id}/permissions")]
public async Task<IActionResult> GetPermissions(long id, [FromQuery] Resource_Type_Enum resourceType)
=> Ok(ApiResult<object>.Ok(await _mediator.Send(new GetRolePermissionsByResourceTypeQuery(id, resourceType))));
[HttpPost]
public async Task<IActionResult> Create([FromBody] Role_CreateCommand command)
{
try { return Ok(ApiResult<object>.Ok(new { id = await _mediator.Send(command) }, "创建成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(long id, [FromBody] Role_UpdateCommand command)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(command with { Id = id }), "修改成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(new Role_DelCommand(id)), "删除成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpPut("{id}/active")]
public async Task<IActionResult> ActiveOrDeactivate(long id, [FromBody] Role_ActiveOrDeactivateCommand command)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(command with { Id = id }), "操作成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpPost("{id}/permissions")]
public async Task<IActionResult> AssignPermission(long id, [FromBody] RolePermission_AssignCommand command)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(command with { RoleId = id }), "绑定成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpDelete("{id}/permissions/{rpId}")]
public async Task<IActionResult> RemovePermission(long id, long rpId, [FromQuery] Resource_Type_Enum resourceType)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(new RolePermission_RemoveCommand(rpId, resourceType)), "解绑成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
}
@@ -0,0 +1,87 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Commands;
using F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Queries;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9WebAiTest.Api.Controllers;
/// <summary>系统用户管理(DDD + CQRS)</summary>
[ApiController]
[Route("api/sys/user")]
[Authorize]
public class SysUserController : ControllerBase
{
private readonly IMediator _mediator;
public SysUserController(IMediator mediator) => _mediator = mediator;
[HttpGet]
public async Task<IActionResult> GetPaged([FromQuery] long departmentId = 0, [FromQuery] string like = "", [FromQuery] int page = 1, [FromQuery] int pageSize = 10)
=> Ok(ApiResult<object>.Ok(await _mediator.Send(new GetUserPagedQuery(departmentId, like, page, pageSize))));
[HttpGet("{id}")]
public async Task<IActionResult> GetById(long id)
=> Ok(ApiResult<object>.Ok(await _mediator.Send(new GetUserByIdQuery(id))));
[HttpGet("{id}/roles")]
public async Task<IActionResult> GetUserRoles(long id)
=> Ok(ApiResult<object>.Ok(await _mediator.Send(new GetUserRolesByUserIdQuery(id))));
[HttpPost]
public async Task<IActionResult> Create([FromBody] User_CreateCommand command)
{
try { return Ok(ApiResult<object>.Ok(new { id = await _mediator.Send(command) }, "创建成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(long id, [FromBody] User_UpdateCommand command)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(command with { Id = id }), "修改成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(new User_DelCommand(id)), "删除成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpPut("{id}/active")]
public async Task<IActionResult> ActiveOrDeactivate(long id, [FromBody] User_ActiveOrDeactivateCommand command)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(command with { Id = id }), "操作成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpPut("{id}/reset-password")]
public async Task<IActionResult> ResetPassword(long id)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(new User_ResetPasswordCommand(id)), "密码已重置")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpPost("{id}/roles")]
public async Task<IActionResult> AssignRole(long id, [FromBody] UserRole_AssignCommand command)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(command with { UserId = id }), "绑定成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpDelete("{id}/roles/{roleId}")]
public async Task<IActionResult> RemoveRole(long id, long roleId)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(new UserRole_RemoveCommand(id, roleId)), "解绑成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
}
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<RootNamespace>F9WebAiTest.Api</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\F9WebAiTest.Domain\F9WebAiTest.Domain.csproj" />
<ProjectReference Include="..\F9WebAiTest.Application\F9WebAiTest.Application.csproj" />
<ProjectReference Include="..\F9WebAiTest.Infrastructure\F9WebAiTest.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" />
<PackageReference Include="NSwag.AspNetCore" Version="14.1.0" />
</ItemGroup>
</Project>
@@ -0,0 +1,47 @@
using System.Collections.Concurrent;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
namespace F9WebAiTest.Api.Hubs;
/// <summary>在线用户登记表(用于判断用户是否在线,离线则走"拉取兜底")</summary>
public static class OnlineUserRegistry
{
private static readonly ConcurrentDictionary<long, byte> _users = new();
public static void Add(long userId) => _users[userId] = 0;
public static void Remove(long userId) => _users.TryRemove(userId, out _);
public static bool IsOnline(long userId) => _users.ContainsKey(userId);
}
/// <summary>通知推送 Hub(客户端连接后按 UserId 标识,服务端用 Clients.User(userId) 定向推送)</summary>
public class NotificationHub : Hub
{
public override async Task OnConnectedAsync()
{
var userId = GetUserId();
if (userId > 0) OnlineUserRegistry.Add(userId);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
var userId = GetUserId();
if (userId > 0) OnlineUserRegistry.Remove(userId);
await base.OnDisconnectedAsync(exception);
}
private long GetUserId()
{
var raw = Context.User?.FindFirst("id")?.Value;
return long.TryParse(raw, out var id) ? id : 0;
}
}
/// <summary>自定义用户标识:使用 JWT 中的 "id" 声明作为 SignalR 的 UserIdentifier</summary>
public class NotificationUserIdProvider : IUserIdProvider
{
public string GetUserId(HubConnectionContext connection)
=> connection.User?.FindFirst("id")?.Value ?? connection.ConnectionId;
}
@@ -0,0 +1,19 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace F9WebAiTest.Api.JsonConverters;
/// <summary>
/// 将 long 序列化为字符串,避免雪花 ID(超过 JS Number 安全整数 2^53)在前后端传递时丢失精度。
/// 反序列化时兼容字符串与数字两种形式。
/// </summary>
public class LongToStringJsonConverter : JsonConverter<long>
{
public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> reader.TokenType == JsonTokenType.String
? long.Parse(reader.GetString()!)
: reader.GetInt64();
public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToString());
}
+112
View File
@@ -0,0 +1,112 @@
using System.Text;
using FreeSql;
using F9WebAiTest.Api.Hubs;
using F9WebAiTest.Api.Services;
using F9WebAiTest.Application;
using F9WebAiTest.Application.Services;
using F9WebAiTest.Application.Services.Notice;
using F9WebAiTest.Infrastructure;
using F9WebAiTest.Infrastructure.Data;
using MediatR;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.SignalR;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
// 分层注册
builder.Services.AddInfrastructure(builder.Configuration);
builder.Services.AddApplication();
// JWT 认证
var jwtKey = builder.Configuration["Jwt:Key"];
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
};
// SignalR 通过 query string 传递 token,需从 access_token 读取
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(accessToken) && context.HttpContext.Request.Path.StartsWithSegments("/hubs"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
builder.Services.AddAuthorization();
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
// long 序列化为字符串,避免雪花 ID 超出 JS 安全整数范围导致精度丢失
options.JsonSerializerOptions.Converters.Add(new F9WebAiTest.Api.JsonConverters.LongToStringJsonConverter());
});
// SignalR 实时推送 + 自定义用户标识 + 即时推送器(创建消息时直接推送,无后台投递服务)
builder.Services.AddSignalR();
builder.Services.AddSingleton<IUserIdProvider, NotificationUserIdProvider>();
builder.Services.AddScoped<INotificationPusher, NotificationPusher>();
// 业务状态变更事件 → SignalR 推送(多端状态同步)
builder.Services.AddScoped<INotificationHandler<BusinessChangedEvent>, BusinessChangedEventHandler>();
// CORS
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
});
// NSwag(OpenAPI 文档 + Swagger UI + 可生成 TS/C# 客户端)
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(settings =>
{
settings.Title = "企业 RBAC 管理系统 API";
settings.Version = "v1";
settings.Description = "基于 NSwag 生成的 OpenAPI 文档,可用于生成 TypeScript/C# 客户端";
settings.AddSecurity("Bearer", new NSwag.OpenApiSecurityScheme
{
Type = NSwag.OpenApiSecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
Description = "请输入 access token(格式:Bearer {token})"
});
settings.OperationProcessors.Add(new NSwag.Generation.Processors.Security.AspNetCoreOperationSecurityScopeProcessor("Bearer"));
});
var app = builder.Build();
// 初始化数据库(FreeSql CodeFirst 建表 + 种子)
using (var scope = app.Services.CreateScope())
{
var fsql = scope.ServiceProvider.GetRequiredService<IFreeSql>();
var snowflake = scope.ServiceProvider.GetRequiredService<F9WebAiTest.Application.Interfaces.SnowflakeID.ISnowflakeIdGenerator>();
DbSeeder.Seed(fsql, snowflake);
}
// NSwag 中间件(swagger 文档 + UI)
app.UseOpenApi();
app.UseSwaggerUi();
app.UseCors("AllowAll");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapHub<NotificationHub>("/hubs/notification");
app.Run();
@@ -0,0 +1,14 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"F9WebAiTest.Api": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:3000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
@@ -0,0 +1,22 @@
using F9WebAiTest.Api.Hubs;
using F9WebAiTest.Application.Services;
using MediatR;
using Microsoft.AspNetCore.SignalR;
namespace F9WebAiTest.Api.Services;
/// <summary>
/// 业务状态变更事件的 SignalR 推送处理器。
/// 业务服务发布 BusinessChangedEvent 后,推送给所有在线客户端,
/// 各端监听 "BusinessChanged" 后刷新已打开的相关页面,实现多端实时同步。
/// </summary>
public class BusinessChangedEventHandler : INotificationHandler<BusinessChangedEvent>
{
private readonly IHubContext<NotificationHub> _hubContext;
public BusinessChangedEventHandler(IHubContext<NotificationHub> hubContext)
=> _hubContext = hubContext;
public Task Handle(BusinessChangedEvent e, CancellationToken ct)
=> _hubContext.Clients.All.SendAsync("BusinessChanged", e, ct);
}
@@ -0,0 +1,10 @@
// 已移除:消息投递不再使用后台服务(BackgroundService)与轮询。
//
// 现在的投递方式(即时推送):
// CreateNotificationCommandHandler(F9WebAiTest.Application/Services/Notice/NotificationMediatR.cs)
// 在消息创建后立即:
// 1. 确定接收人(ForceReceivers 精确送达,否则按消息类型订阅关系广播);
// 2. 写入 NotificationReceiver(MessageId + UserId 幂等);
// 3. 通过 INotificationPusher(实现在 F9WebAiTest.Api/Services/NotificationPusher.cs)经 SignalR 直接推送。
//
// 本文件已无任何代码,可直接删除。
@@ -0,0 +1,45 @@
using F9WebAiTest.Api.Hubs;
using F9WebAiTest.Application.Dtos.Notice;
using F9WebAiTest.Application.Services.Notice;
using Microsoft.AspNetCore.SignalR;
namespace F9WebAiTest.Api.Services;
/// <summary>
/// 消息实时推送器实现:基于 SignalR 的 IHubContext 直接推送。
/// 无后台队列、无轮询——消息创建时同步完成投递与推送。
///
/// 在线判定:OnlineUserRegistry(Hub 连接/断开时登记的内存表)。
/// 之所以需要显式判定而不能依赖推送异常:SignalR 对"无连接的 Clients.User 调用"是静默成功的,
/// 不判定的话离线用户会被误记为推送成功。
///
/// 用户标识:由 NotificationUserIdProvider 统一取 JWT 的 "id" 声明,
/// 因此 Clients.User(userId) 可精准命中该用户的所有在线连接(支持多端同时在线)。
/// </summary>
public class NotificationPusher : INotificationPusher
{
private readonly IHubContext<NotificationHub> _hubContext;
public NotificationPusher(IHubContext<NotificationHub> hubContext)
{
_hubContext = hubContext;
}
/// <inheritdoc />
public bool IsOnline(long userId) => OnlineUserRegistry.IsOnline(userId);
/// <inheritdoc />
public async Task<bool> PushNewMessageAsync(long userId, NotificationMessageDto message, CancellationToken ct = default)
{
// 离线用户:不推送(消息已持久化,上线后拉取兜底),返回 false 由调用方标记 IsOffline
if (!OnlineUserRegistry.IsOnline(userId)) return false;
await _hubContext.Clients.User(userId.ToString()).SendAsync("ReceiveNotification", message, ct);
return true;
}
/// <inheritdoc />
public Task PushUnreadCountAsync(long userId, int unreadCount, int todoCount, CancellationToken ct = default)
=> _hubContext.Clients.User(userId.ToString())
.SendAsync("UnreadCountChanged", new { unreadCount, todoCount }, ct);
}
+26
View File
@@ -0,0 +1,26 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://0.0.0.0:3000"
}
}
},
"Jwt": {
"Key": "f9-web-ai-test-rbac-secret-key-2026-please-change-in-production",
"Issuer": "F9WebAiTest",
"Audience": "F9WebAiTestClient",
"AccessTokenExpireMinutes": 120,
"RefreshTokenExpireDays": 7
},
"ConnectionStrings": {
"Default": "Data Source=f9_web_ai_test_rbac.db"
}
}
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,31 @@
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Domain.Interfaces.SYS.Departments;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.Departments.Commands;
/// <summary>命令定义</summary>
public record Department_ActiveOrDeactivateCommand(long Id, bool Active) : IRequest<bool>;
/// <summary>命令处理器</summary>
public class ActiveOrDeactivateDepartmentCommandHandler(
IDepartmentWriteRepository departmentWriteRepository,
IPublisher eventBus) : IRequestHandler<Department_ActiveOrDeactivateCommand, bool>
{
public async Task<bool> Handle(Department_ActiveOrDeactivateCommand command, CancellationToken cancellationToken)
{
var dep = await departmentWriteRepository.GetByIdAsync(command.Id, CancellationToken.None);
if (dep == null) throw new F9NotFoundException("部门", command.Id);
if (command.Active) dep.Activate();
else dep.Deactivate();
var result = await departmentWriteRepository.ActiveOrDeactive(command.Id, dep.IsActive);
if (result == false) throw new F9OperationException("启用/禁用", "操作");
foreach (var de in dep.DomainEvents) await eventBus.Publish(de, cancellationToken);
dep.ClearDomainEvents();
return result;
}
}
@@ -0,0 +1,35 @@
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Application.Interfaces.SnowflakeID;
using F9WebAiTest.Domain.Entities.SYS.Departments;
using F9WebAiTest.Domain.Enums.SYS.Departments;
using F9WebAiTest.Domain.Interfaces.SYS.Departments;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.Departments.Commands;
/// <summary>命令定义</summary>
public record Department_CreateCommand(string Code, string Name, long ParentId, Department_ManageType_Enum ManageType, Department_DutyType_Enum DutyType, int SortOrder, string Description) : IRequest<long>;
/// <summary>命令处理器</summary>
public class CreateDepartmentCommandHandler(
IDepartmentWriteRepository departmentRepository,
ISnowflakeIdGenerator snowflakeIdGenerator,
IPublisher eventBus) : IRequestHandler<Department_CreateCommand, long>
{
public async Task<long> Handle(Department_CreateCommand command, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(command.Code)) throw new F9OperationException("部门编码不能为空");
if (string.IsNullOrWhiteSpace(command.Name)) throw new F9OperationException("部门名称不能为空");
var dep = new Department(snowflakeIdGenerator.CreateId(), command.Code, command.Name, command.ParentId,
command.ManageType, command.DutyType, command.SortOrder, command.Description);
var result = await departmentRepository.AddAsync(dep);
if (result == false) throw new F9OperationException("部门", "新增");
foreach (var de in dep.DomainEvents) await eventBus.Publish(de, cancellationToken);
dep.ClearDomainEvents();
return dep.Id;
}
}
@@ -0,0 +1,22 @@
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Domain.Interfaces.SYS.Departments;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.Departments.Commands;
/// <summary>命令定义</summary>
public record Department_DelCommand(long Id) : IRequest<bool>;
/// <summary>命令处理器</summary>
public class DelDepartmentCommandHandler(
IDepartmentWriteRepository departmentWriteRepository) : IRequestHandler<Department_DelCommand, bool>
{
public async Task<bool> Handle(Department_DelCommand command, CancellationToken cancellationToken)
{
if (command.Id <= 0) throw new F9OperationException("部门ID不能为空");
var result = await departmentWriteRepository.SoftDeleteAsync(command.Id);
if (result == false) throw new F9OperationException("部门", "删除");
return result;
}
}
@@ -0,0 +1,29 @@
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Application.Interfaces.SnowflakeID;
using F9WebAiTest.Domain.Entities.SYS.Departments;
using F9WebAiTest.Domain.Enums.SYS.Departments;
using F9WebAiTest.Domain.Interfaces.SYS.Departments;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.Departments.Commands;
/// <summary>命令定义</summary>
public record Department_UpdateCommand(long Id, string Code, string Name, long ParentId, Department_ManageType_Enum ManageType, Department_DutyType_Enum DutyType, int SortOrder, string Description) : IRequest<bool>;
/// <summary>命令处理器</summary>
public class UpdateDepartmentCommandHandler(
IDepartmentWriteRepository departmentRepository) : IRequestHandler<Department_UpdateCommand, bool>
{
public async Task<bool> Handle(Department_UpdateCommand command, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(command.Code)) throw new F9OperationException("部门编码不能为空");
if (string.IsNullOrWhiteSpace(command.Name)) throw new F9OperationException("部门名称不能为空");
var dep = new Department(command.Id, command.Code, command.Name, command.ParentId,
command.ManageType, command.DutyType, command.SortOrder, command.Description);
var result = await departmentRepository.UpdateAsync(dep);
if (result == false) throw new F9OperationException("部门", "修改");
return result;
}
}
@@ -0,0 +1,13 @@
using F9WebAiTest.Application.Dtos.SYS.Departments;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.Departments;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.Departments.Queries;
public record GetDepartmentAllQuery() : IRequest<List<DepartmentReadDto_View>>;
public class GetDepartmentAllQueryHandler(IDepartmentReadRepository departmentReadRepository) : IRequestHandler<GetDepartmentAllQuery, List<DepartmentReadDto_View>>
{
public async Task<List<DepartmentReadDto_View>> Handle(GetDepartmentAllQuery query, CancellationToken ct)
=> await departmentReadRepository.GetAllAsync();
}
@@ -0,0 +1,31 @@
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Menus;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Menus.Commands;
/// <summary>命令定义</summary>
public record Menu_ActiveOrDeactivateCommand(long Id, bool Active) : IRequest<bool>;
/// <summary>命令处理器</summary>
public class ActiveOrDeactivateMenuCommandHandler(
IMenuWriteRepository menuWriteRepository,
IPublisher eventBus) : IRequestHandler<Menu_ActiveOrDeactivateCommand, bool>
{
public async Task<bool> Handle(Menu_ActiveOrDeactivateCommand command, CancellationToken cancellationToken)
{
var menu = await menuWriteRepository.GetByIdAsync(command.Id);
if (menu == null) throw new F9NotFoundException("菜单", command.Id);
if (command.Active) menu.Activate();
else menu.Deactivate();
var result = await menuWriteRepository.ActiveOrDeactive(command.Id, menu.IsActive);
if (result == false) throw new F9OperationException("启用/禁用", "操作");
foreach (var de in menu.DomainEvents) await eventBus.Publish(de, cancellationToken);
menu.ClearDomainEvents();
return result;
}
}
@@ -0,0 +1,34 @@
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Application.Interfaces.SnowflakeID;
using F9WebAiTest.Domain.Entities.SYS.RBAC;
using F9WebAiTest.Domain.Enums.SYS.RBAC;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Menus;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Menus.Commands;
/// <summary>命令定义</summary>
public record Menu_CreateCommand(string Name, string IconSvg, MenuType_Enum MenuType, string AssemblyName, string TypeFullName, long ParentId, int SortOrder) : IRequest<long>;
/// <summary>命令处理器</summary>
public class CreateMenuCommandHandler(
IMenuWriteRepository menuRepository,
ISnowflakeIdGenerator snowflakeIdGenerator,
IPublisher eventBus) : IRequestHandler<Menu_CreateCommand, long>
{
public async Task<long> Handle(Menu_CreateCommand command, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(command.Name)) throw new F9OperationException("菜单名称不能为空");
var menu = new Menu(snowflakeIdGenerator.CreateId(), command.Name, command.IconSvg,
command.MenuType, command.AssemblyName, command.TypeFullName, command.ParentId, command.SortOrder);
var result = await menuRepository.AddAsync(menu);
if (result == false) throw new F9OperationException("菜单", "新增");
foreach (var de in menu.DomainEvents) await eventBus.Publish(de, cancellationToken);
menu.ClearDomainEvents();
return menu.Id;
}
}
@@ -0,0 +1,22 @@
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Menus;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Menus.Commands;
/// <summary>命令定义</summary>
public record Menu_DelCommand(long Id) : IRequest<bool>;
/// <summary>命令处理器</summary>
public class DelMenuCommandHandler(
IMenuWriteRepository menuWriteRepository) : IRequestHandler<Menu_DelCommand, bool>
{
public async Task<bool> Handle(Menu_DelCommand command, CancellationToken cancellationToken)
{
if (command.Id <= 0) throw new F9OperationException("菜单ID不能为空");
var result = await menuWriteRepository.SoftDeleteAsync(command.Id);
if (result == false) throw new F9OperationException("菜单", "删除");
return result;
}
}
@@ -0,0 +1,28 @@
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Application.Interfaces.SnowflakeID;
using F9WebAiTest.Domain.Entities.SYS.RBAC;
using F9WebAiTest.Domain.Enums.SYS.RBAC;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Menus;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Menus.Commands;
/// <summary>命令定义</summary>
public record Menu_UpdateCommand(long Id, string Name, string IconSvg, MenuType_Enum MenuType, string AssemblyName, string TypeFullName, long ParentId, int SortOrder) : IRequest<bool>;
/// <summary>命令处理器</summary>
public class UpdateMenuCommandHandler(
IMenuWriteRepository menuRepository) : IRequestHandler<Menu_UpdateCommand, bool>
{
public async Task<bool> Handle(Menu_UpdateCommand command, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(command.Name)) throw new F9OperationException("菜单名称不能为空");
var menu = new Menu(command.Id, command.Name, command.IconSvg,
command.MenuType, command.AssemblyName, command.TypeFullName, command.ParentId, command.SortOrder);
var result = await menuRepository.UpdateAsync(menu);
if (result == false) throw new F9OperationException("菜单", "修改");
return result;
}
}
@@ -0,0 +1,80 @@
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Application.Interfaces.SnowflakeID;
using F9WebAiTest.Application.Interfaces.UOW;
using F9WebAiTest.Domain.Common;
using F9WebAiTest.Domain.Entities.SYS.RBAC;
using F9WebAiTest.Domain.Enums.SYS.RBAC;
using NotificationTypeEntity = F9WebAiTest.Domain.Entities.Notice.NotificationType;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Permissions;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Roles;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Roles.Commands;
/// <summary>命令定义</summary>
public record RolePermission_AssignCommand(long RoleId, Permission_Type_Enum PermissionType, Resource_Type_Enum ResourceType, string ResourceIdentity, long ResourceId) : IRequest<long>;
/// <summary>命令处理器</summary>
public class AssignRolePermissionCommandHandler(
IRoleWriteRepository roleRepository,
IUnitOfWork_F9 unitOfWork,
IRolePermissionWriteRepository rolePermissionRep,
IPermissionWriteRepository permissionWriteRepository,
IRepository<NotificationTypeEntity> notificationTypeRepository,
ISnowflakeIdGenerator snowflakeIdGenerator,
IPublisher eventBus) : IRequestHandler<RolePermission_AssignCommand, long>
{
public async Task<long> Handle(RolePermission_AssignCommand command, CancellationToken cancellationToken)
{
if (command.RoleId <= 0) throw new F9OperationException("角色ID不能为空");
if (command.ResourceId <= 0) throw new F9OperationException("资源ID不能为空");
if (string.IsNullOrWhiteSpace(command.ResourceIdentity)) throw new F9OperationException("资源标识不能为空");
Role role = null;
long rolePermissionId = 0;
var ok = await unitOfWork.ExecuteWithUow(async () =>
{
// 查或创建权限
var dbPermission = await permissionWriteRepository.GetByIdAsync(command.PermissionType, command.ResourceType, command.ResourceId);
if (dbPermission == null)
{
dbPermission = new Permission(
snowflakeIdGenerator.CreateId(),
command.PermissionType,
command.ResourceType,
command.ResourceIdentity,
command.ResourceId);
// 消息订阅权限:Code 对齐消息类型的 RequiredPermission(业务权限码),保证订阅校验一致
if (command.ResourceType == Resource_Type_Enum.消息订阅)
{
var type = await notificationTypeRepository.GetByIdAsync(command.ResourceId);
if (type != null && !string.IsNullOrEmpty(type.RequiredPermission))
dbPermission.SetCode(type.RequiredPermission);
}
await permissionWriteRepository.AddRangeAsync(new[] { dbPermission });
}
// 绑定角色权限
var rolePermission = new RolePermission(snowflakeIdGenerator.CreateId(), command.RoleId, dbPermission.Id);
var result = await rolePermissionRep.AssignAsync(rolePermission);
role = await roleRepository.GetByIdAsync(command.RoleId);
role.AddPermission(dbPermission.Id, command.ResourceType);
rolePermissionId = rolePermission.Id;
return result;
});
if (!ok) throw new F9OperationException("权限", "绑定");
// 发布领域事件
foreach (var domainEvent in role.DomainEvents)
await eventBus.Publish(domainEvent, cancellationToken);
role.ClearDomainEvents();
return rolePermissionId;
}
}
@@ -0,0 +1,37 @@
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Domain.Enums.SYS.RBAC;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Roles;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Roles.Commands;
/// <summary>命令定义</summary>
public record RolePermission_RemoveCommand(long Id, Resource_Type_Enum ResourceType) : IRequest<bool>;
/// <summary>命令处理器</summary>
public class RemoveRolePermissionCommandHandler(
IRoleWriteRepository roleRepository,
IRolePermissionWriteRepository rolePermissionWriteRepository,
IPublisher eventBus) : IRequestHandler<RolePermission_RemoveCommand, bool>
{
public async Task<bool> Handle(RolePermission_RemoveCommand command, CancellationToken cancellationToken)
{
if (command.Id <= 0) throw new F9OperationException("角色权限ID不能为空");
var rolePermission = await rolePermissionWriteRepository.GetByIdAsync(command.Id);
if (rolePermission == null) throw new F9NotFoundException("角色权限", command.Id);
var role = await roleRepository.GetByIdAsync(rolePermission.RoleId);
role.RemovePermission(rolePermission.PermissionId, command.ResourceType);
var result = await rolePermissionWriteRepository.RemoveAsync(command.Id);
if (result == false) throw new F9OperationException("权限", "解绑");
// 发布领域事件
foreach (var domainEvent in role.DomainEvents)
await eventBus.Publish(domainEvent, cancellationToken);
role.ClearDomainEvents();
return true;
}
}
@@ -0,0 +1,26 @@
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Roles;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Roles.Commands;
/// <summary>命令定义</summary>
public record Role_ActiveOrDeactivateCommand(long Id, bool Active) : IRequest<bool>;
/// <summary>命令处理器</summary>
public class ActiveOrDeactivateRoleCommandHandler(
IRoleWriteRepository roleWriteRepository) : IRequestHandler<Role_ActiveOrDeactivateCommand, bool>
{
public async Task<bool> Handle(Role_ActiveOrDeactivateCommand command, CancellationToken cancellationToken)
{
var role = await roleWriteRepository.GetByIdAsync(command.Id);
if (role == null) throw new F9NotFoundException("角色", command.Id);
if (command.Active) role.Activate();
else role.Deactivate();
var result = await roleWriteRepository.ActiveOrDeactive(command.Id, role.IsActive);
if (result == false) throw new F9OperationException("启用/禁用", "操作");
return result;
}
}
@@ -0,0 +1,27 @@
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Application.Interfaces.SnowflakeID;
using F9WebAiTest.Domain.Entities.SYS.RBAC;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Roles;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Roles.Commands;
/// <summary>命令定义</summary>
public record Role_CreateCommand(string Name, string Code, int SortOrder, string Description) : IRequest<long>;
/// <summary>命令处理器</summary>
public class CreateRoleCommandHandler(
IRoleWriteRepository roleRepository,
ISnowflakeIdGenerator snowflakeIdGenerator) : IRequestHandler<Role_CreateCommand, long>
{
public async Task<long> Handle(Role_CreateCommand command, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(command.Name)) throw new F9OperationException("角色名称不能为空");
if (string.IsNullOrWhiteSpace(command.Code)) throw new F9OperationException("角色编码不能为空");
var newEntity = new Role(snowflakeIdGenerator.CreateId(), command.Name, command.Code, command.SortOrder, command.Description);
var result = await roleRepository.AddAsync(newEntity);
if (result == false) throw new F9OperationException("角色", "新增");
return newEntity.Id;
}
}
@@ -0,0 +1,22 @@
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Roles;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Roles.Commands;
/// <summary>命令定义</summary>
public record Role_DelCommand(long Id) : IRequest<bool>;
/// <summary>命令处理器</summary>
public class DelRoleCommandHandler(
IRoleWriteRepository roleWriteRepository) : IRequestHandler<Role_DelCommand, bool>
{
public async Task<bool> Handle(Role_DelCommand command, CancellationToken cancellationToken)
{
if (command.Id <= 0) throw new F9OperationException("角色ID不能为空");
var result = await roleWriteRepository.SoftDeleteAsync(command.Id);
if (result == false) throw new F9OperationException("角色", "删除");
return result;
}
}
@@ -0,0 +1,25 @@
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Domain.Entities.SYS.RBAC;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Roles;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Roles.Commands;
/// <summary>命令定义</summary>
public record Role_UpdateCommand(long Id, string Name, string Code, int SortOrder, string Description) : IRequest<bool>;
/// <summary>命令处理器</summary>
public class UpdateRoleCommandHandler(
IRoleWriteRepository roleRepository) : IRequestHandler<Role_UpdateCommand, bool>
{
public async Task<bool> Handle(Role_UpdateCommand command, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(command.Name)) throw new F9OperationException("角色名称不能为空");
if (string.IsNullOrWhiteSpace(command.Code)) throw new F9OperationException("角色编码不能为空");
var newEntity = new Role(command.Id, command.Name, command.Code, command.SortOrder, command.Description);
var result = await roleRepository.UpdateAsync(newEntity);
if (result == false) throw new F9OperationException("角色", "修改");
return result;
}
}
@@ -0,0 +1,13 @@
using F9WebAiTest.Application.Dtos.SYS.RBAC.Roles;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Roles;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Roles.Queries;
public record GetRoleAllQuery() : IRequest<List<RoleReadDto_View>>;
public class GetRoleAllQueryHandler(IRoleReadRepository roleReadRepository) : IRequestHandler<GetRoleAllQuery, List<RoleReadDto_View>>
{
public async Task<List<RoleReadDto_View>> Handle(GetRoleAllQuery query, CancellationToken ct)
=> await roleReadRepository.GetAllAsync();
}
@@ -0,0 +1,13 @@
using F9WebAiTest.Application.Dtos.SYS.RBAC.Roles;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Roles;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Roles.Queries;
public record GetRoleByIdForEditQuery(long Id) : IRequest<RoleReadDto_Edit>;
public class GetRoleByIdForEditQueryHandler(IRoleReadRepository roleReadRepository) : IRequestHandler<GetRoleByIdForEditQuery, RoleReadDto_Edit>
{
public async Task<RoleReadDto_Edit> Handle(GetRoleByIdForEditQuery query, CancellationToken ct)
=> await roleReadRepository.GetByIdAsync(query.Id);
}
@@ -0,0 +1,14 @@
using F9WebAiTest.Application.Dtos.SYS.RBAC.Roles;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Roles;
using F9WebAiTest.Domain.Enums.SYS.RBAC;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Roles.Queries;
public record GetRolePermissionsByResourceTypeQuery(long RoleId, Resource_Type_Enum ResourceType) : IRequest<List<RolePermissionReadDto_View>>;
public class GetRolePermissionsByResourceTypeQueryHandler(IRoleReadRepository roleReadRepository) : IRequestHandler<GetRolePermissionsByResourceTypeQuery, List<RolePermissionReadDto_View>>
{
public async Task<List<RolePermissionReadDto_View>> Handle(GetRolePermissionsByResourceTypeQuery query, CancellationToken ct)
=> await roleReadRepository.GetRolePermissionsByResourceType(query.RoleId, query.ResourceType);
}
@@ -0,0 +1,40 @@
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Application.Interfaces.SnowflakeID;
using F9WebAiTest.Domain.Entities.SYS.RBAC;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Commands;
/// <summary>命令定义</summary>
public record UserRole_AssignCommand(long UserId, long RoleId) : IRequest<long>;
/// <summary>命令处理器</summary>
public class UserRole_AssignCommandHandler(
IUserRoleWriteRepository userRoleWriteRepository,
IUserWriteRepository userWriteRepository,
ISnowflakeIdGenerator snowflakeIdGenerator,
IPublisher eventBus) : IRequestHandler<UserRole_AssignCommand, long>
{
public async Task<long> Handle(UserRole_AssignCommand command, CancellationToken cancellationToken)
{
if (command.UserId <= 0) throw new F9OperationException("用户ID不能为空");
if (command.RoleId <= 0) throw new F9OperationException("角色ID不能为空");
var newEntity = new UserRole(snowflakeIdGenerator.CreateId(), command.UserId, command.RoleId);
var user = await userWriteRepository.GetByIdAsync(command.UserId);
if (user == null) throw new F9NotFoundException("用户", command.UserId);
user.AddRole(newEntity.Id);
var result = await userRoleWriteRepository.AssignAsync(newEntity);
if (result == false) throw new F9OperationException("权限", "绑定");
// 发布领域事件
foreach (var domainEvent in user.DomainEvents)
await eventBus.Publish(domainEvent, cancellationToken);
user.ClearDomainEvents();
return newEntity.Id;
}
}
@@ -0,0 +1,35 @@
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Commands;
/// <summary>命令定义</summary>
public record UserRole_RemoveCommand(long UserId, long RoleId) : IRequest<bool>;
/// <summary>命令处理器</summary>
public class UserRole_RemoveCommandHandler(
IUserWriteRepository userWriteRepository,
IUserRoleWriteRepository userRoleWriteRepository,
IPublisher eventBus) : IRequestHandler<UserRole_RemoveCommand, bool>
{
public async Task<bool> Handle(UserRole_RemoveCommand command, CancellationToken cancellationToken)
{
if (command.UserId <= 0) throw new F9OperationException("用户ID不能为空");
if (command.RoleId <= 0) throw new F9OperationException("角色ID不能为空");
var user = await userWriteRepository.GetByIdAsync(command.UserId);
if (user == null) throw new F9NotFoundException("用户", command.UserId);
user.RemoveRole(command.RoleId);
var result = await userRoleWriteRepository.RemoveAsync(command.UserId, command.RoleId);
if (result == false) throw new F9OperationException("权限", "解绑");
// 发布领域事件
foreach (var domainEvent in user.DomainEvents)
await eventBus.Publish(domainEvent, cancellationToken);
user.ClearDomainEvents();
return result;
}
}
@@ -0,0 +1,26 @@
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Commands;
/// <summary>命令定义</summary>
public record User_ActiveOrDeactivateCommand(long Id, bool Active) : IRequest<bool>;
/// <summary>命令处理器</summary>
public class ActiveOrDeactivateUserCommandHandler(
IUserWriteRepository userWriteRepository) : IRequestHandler<User_ActiveOrDeactivateCommand, bool>
{
public async Task<bool> Handle(User_ActiveOrDeactivateCommand command, CancellationToken cancellationToken)
{
var user = await userWriteRepository.GetByIdAsync(command.Id);
if (user == null) throw new F9NotFoundException("用户", command.Id);
if (command.Active) user.Activate();
else user.Deactivate();
var result = await userWriteRepository.ActiveOrDeactive(command.Id, user.IsActive);
if (result == false) throw new F9OperationException("启用/禁用", "操作");
return result;
}
}
@@ -0,0 +1,73 @@
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Application.Interfaces.Security;
using F9WebAiTest.Application.Interfaces.SnowflakeID;
using F9WebAiTest.Application.Interfaces.UOW;
using F9WebAiTest.Domain.Entities.SYS.Departments;
using F9WebAiTest.Domain.Entities.SYS.RBAC;
using F9WebAiTest.Domain.Interfaces.SYS.Departments;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Commands;
/// <summary>命令定义</summary>
public record User_CreateCommand(string UserName, string PhoneNumber, string LoginName, long DepartmentId, int SortOrder) : IRequest<long>;
/// <summary>命令处理器</summary>
public class CreateUserCommandHandler(
IUnitOfWork_F9 unitOfWork,
IUserWriteRepository userRepository,
IDepartmentWriteRepository departmentWriteRepository,
IDepartmentUserRepository departmentUserRepository,
IPasswordHasher passwordHasher,
ISnowflakeIdGenerator snowflakeIdGenerator,
IPublisher eventBus) : IRequestHandler<User_CreateCommand, long>
{
public async Task<long> Handle(User_CreateCommand command, CancellationToken cancellationToken)
{
// 手写校验
if (string.IsNullOrWhiteSpace(command.UserName)) throw new F9OperationException("用户名不能为空");
if (string.IsNullOrWhiteSpace(command.LoginName)) throw new F9OperationException("登录名不能为空");
if (command.DepartmentId <= 0) throw new F9OperationException("部门不能为空");
User createdUser = null;
var ok = await unitOfWork.ExecuteWithUow(async () =>
{
// 1. 验证部门是否存在(应用层协调)
var department = await departmentWriteRepository.GetByIdAsync(command.DepartmentId, cancellationToken);
if (department == null) throw new F9NotFoundException("部门不存在");
// 2. 创建用户聚合根
var user = new User(
id: snowflakeIdGenerator.CreateId(),
command.UserName,
command.PhoneNumber,
command.LoginName,
command.DepartmentId,
command.SortOrder);
// 设置默认密码
var defaultPasswordMd5 = passwordHasher.GetMd5Hash(user.GetDefaultPasswordText());
var passwordHash = passwordHasher.HashPassword(defaultPasswordMd5);
user.SetPassword(passwordHash);
await userRepository.AddAsync(user);
// 3. 创建部门用户关联
var depUser = new DepartmentUser(snowflakeIdGenerator.CreateId(), command.DepartmentId, user.Id);
await departmentUserRepository.AssignAsync(depUser);
createdUser = user;
return true;
});
if (!ok) throw new F9OperationException("用户", "新增");
// 4. 事务提交后发布领域事件
foreach (var domainEvent in createdUser.DomainEvents)
await eventBus.Publish(domainEvent, cancellationToken);
createdUser.ClearDomainEvents();
return createdUser.Id;
}
}
@@ -0,0 +1,22 @@
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Commands;
/// <summary>命令定义</summary>
public record User_DelCommand(long Id) : IRequest<bool>;
/// <summary>命令处理器</summary>
public class DelUserCommandHandler(
IUserWriteRepository userWriteRepository) : IRequestHandler<User_DelCommand, bool>
{
public async Task<bool> Handle(User_DelCommand command, CancellationToken cancellationToken)
{
if (command.Id <= 0) throw new F9OperationException("用户ID不能为空");
var result = await userWriteRepository.SoftDeleteAsync(command.Id);
if (result == false) throw new F9OperationException("用户", "删除");
return result;
}
}
@@ -0,0 +1,32 @@
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Application.Interfaces.Security;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Commands;
/// <summary>命令定义</summary>
public record User_ResetPasswordCommand(long Id) : IRequest<bool>;
/// <summary>命令处理器</summary>
public class ResetPasswordUserCommandHandler(
IUserWriteRepository userWriteRepository,
IPasswordHasher passwordHasher) : IRequestHandler<User_ResetPasswordCommand, bool>
{
public async Task<bool> Handle(User_ResetPasswordCommand command, CancellationToken cancellationToken)
{
if (command.Id <= 0) throw new F9OperationException("用户ID不能为空");
var dbUser = await userWriteRepository.GetByIdAsync(command.Id);
if (dbUser == null) throw new F9NotFoundException("用户", command.Id);
// 设置默认密码
var defaultPasswordMd5 = passwordHasher.GetMd5Hash(dbUser.GetDefaultPasswordText());
var passwordHash = passwordHasher.HashPassword(defaultPasswordMd5);
dbUser.SetPassword(passwordHash);
var result = await userWriteRepository.UpdatePassworkdAsync(command.Id, dbUser.PasswordHash);
if (result == false) throw new F9OperationException("用户", "重置密码");
return result;
}
}
@@ -0,0 +1,49 @@
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Application.Interfaces.SnowflakeID;
using F9WebAiTest.Application.Interfaces.UOW;
using F9WebAiTest.Domain.Entities.SYS.Departments;
using F9WebAiTest.Domain.Interfaces.SYS.Departments;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Commands;
/// <summary>命令定义</summary>
public record User_UpdateCommand(long Id, string UserName, string PhoneNumber, string LoginName, long DepartmentId, int SortOrder) : IRequest<bool>;
/// <summary>命令处理器</summary>
public class User_UpdateCommandHandler(
IUnitOfWork_F9 unitOfWork,
ISnowflakeIdGenerator snowflakeIdGenerator,
IUserWriteRepository userWriteRepository,
IDepartmentUserRepository departmentUserRepository) : IRequestHandler<User_UpdateCommand, bool>
{
public async Task<bool> Handle(User_UpdateCommand command, CancellationToken cancellationToken)
{
if (command.Id <= 0) throw new F9OperationException("用户ID不能为空");
if (string.IsNullOrWhiteSpace(command.UserName)) throw new F9OperationException("用户名不能为空");
if (command.DepartmentId <= 0) throw new F9OperationException("部门不能为空");
return await unitOfWork.ExecuteWithUow(async () =>
{
// 获取数据库用户
var dbUser = await userWriteRepository.GetByIdAsync(command.Id);
if (dbUser == null) throw new F9NotFoundException("用户", command.Id);
// 移除原部门关系
await departmentUserRepository.RemoveAsync(dbUser.DepartmentId, dbUser.Id);
// 领域更新
dbUser.Update(command.UserName, command.PhoneNumber, command.LoginName, command.DepartmentId, command.SortOrder);
// 持久化
var result = await userWriteRepository.UpdateAsync(dbUser);
// 添加新部门关系
await departmentUserRepository.AssignAsync(new DepartmentUser(
snowflakeIdGenerator.CreateId(), dbUser.DepartmentId, dbUser.Id));
return result;
});
}
}
@@ -0,0 +1,13 @@
using F9WebAiTest.Application.Dtos.SYS.RBAC.Users;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Users;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Queries;
public record GetUserByIdQuery(long id) : IRequest<UserReadDto_Edit>;
public class GetUserByIdQueryHandler(IUserReadRepository userReadRepository) : IRequestHandler<GetUserByIdQuery, UserReadDto_Edit>
{
public async Task<UserReadDto_Edit> Handle(GetUserByIdQuery query, CancellationToken ct)
=> await userReadRepository.GetByIdAsync(query.id);
}
@@ -0,0 +1,13 @@
using F9WebAiTest.Application.Dtos.SYS.RBAC.Users;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Users;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Queries;
public record GetUserByIdsQuery(List<long> Ids) : IRequest<List<UserReadDto_Edit>>;
public class GetUserByIdsQueryHandler(IUserReadRepository userReadRepository) : IRequestHandler<GetUserByIdsQuery, List<UserReadDto_Edit>>
{
public async Task<List<UserReadDto_Edit>> Handle(GetUserByIdsQuery query, CancellationToken ct)
=> await userReadRepository.GetByIdsAsync(query.Ids);
}
@@ -0,0 +1,14 @@
using F9WebAiTest.Application.Dtos.Common;
using F9WebAiTest.Application.Dtos.SYS.RBAC.Users;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Users;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Queries;
public record GetUserPagedQuery(long DepartmentId, string Like, int Page = 1, int PageSize = 10) : IRequest<PagedResult<UserReadDto_View>>;
public class GetUserPagedQueryHandler(IUserReadRepository userReadRepository) : IRequestHandler<GetUserPagedQuery, PagedResult<UserReadDto_View>>
{
public async Task<PagedResult<UserReadDto_View>> Handle(GetUserPagedQuery query, CancellationToken ct)
=> await userReadRepository.GetPagedAsync(query.DepartmentId, query.Like, query.Page, query.PageSize);
}
@@ -0,0 +1,13 @@
using F9WebAiTest.Application.Dtos.SYS.RBAC.Users;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Users;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Queries;
public record GetUserRolesByUserIdQuery(long UserId) : IRequest<List<UserRoleReadDto_Cache>>;
public class GetUserRolesByUserIdQueryHandler(IUserReadRepository userReadRepository) : IRequestHandler<GetUserRolesByUserIdQuery, List<UserRoleReadDto_Cache>>
{
public async Task<List<UserRoleReadDto_Cache>> Handle(GetUserRolesByUserIdQuery query, CancellationToken ct)
=> await userReadRepository.GetUserRolesByUserId(query.UserId);
}
@@ -0,0 +1,24 @@
namespace F9WebAiTest.Application.Common;
/// <summary>统一 API 响应</summary>
public class ApiResult<T>
{
public int Code { get; set; }
public string Message { get; set; }
public T Data { get; set; }
public static ApiResult<T> Ok(T data, string message = "success")
=> new() { Code = 200, Message = message, Data = data };
public static ApiResult<T> Fail(string message, int code = 400)
=> new() { Code = code, Message = message, Data = default };
}
/// <summary>分页结果</summary>
public class PageResult<T>
{
public List<T> Items { get; set; } = new();
public long Total { get; set; }
public int PageIndex { get; set; }
public int PageSize { get; set; }
}
@@ -0,0 +1,12 @@
namespace F9WebAiTest.Application.Common.Exceptions;
/// <summary>资源不存在异常</summary>
public class F9NotFoundException : Exception
{
public F9NotFoundException(string resourceName, object key)
: base($"{resourceName}不存在'{key}'。")
{
}
public F9NotFoundException(string message) : base(message) { }
}
@@ -0,0 +1,15 @@
namespace F9WebAiTest.Application.Common.Exceptions;
/// <summary>操作失败异常</summary>
public class F9OperationException : Exception
{
/// <summary>操作失败</summary>
/// <param name="resourceName">资源名称</param>
/// <param name="oporation">操作名称</param>
public F9OperationException(string resourceName, string oporation)
: base($"{resourceName}{oporation}失败。")
{
}
public F9OperationException(string message) : base(message) { }
}
@@ -0,0 +1,12 @@
namespace F9WebAiTest.Application.Common;
/// <summary>业务异常</summary>
public class ServiceException : Exception
{
public int Code { get; set; } = 400;
public ServiceException(string message, int code = 400) : base(message)
{
Code = code;
}
}
@@ -0,0 +1,9 @@
namespace F9WebAiTest.Application.Contracts;
/// <summary>登录用户信息</summary>
/// <param name="Id">识别码</param>
/// <param name="Name">用户名称</param>
/// <param name="Login_Account">登录名</param>
/// <param name="Department_Id">所属部门Id</param>
/// <param name="IsDeveloper">是否开发者</param>
public record LoginUserInfo(long Id, string Name, string Login_Account, long Department_Id, bool IsDeveloper);
@@ -0,0 +1,33 @@
using F9WebAiTest.Application.Services;
using F9WebAiTest.Application.Services.Attendance;
using F9WebAiTest.Application.Services.Auth;
using F9WebAiTest.Application.Services.Base;
using F9WebAiTest.Application.Services.Notice;
using F9WebAiTest.Application.Services.Prod;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
namespace F9WebAiTest.Application;
public static class DependencyInjection
{
public static IServiceCollection AddApplication(this IServiceCollection services)
{
services.AddScoped<IJwtService, JwtService>();
services.AddScoped<IAuthService, AuthService>();
services.AddScoped<IAttendanceService, AttendanceService>();
services.AddScoped<ICraftZhuangKouService, CraftZhuangKouService>();
services.AddScoped<IShengsiGuigeService, ShengsiGuigeService>();
services.AddScoped<IShengsiDingjizhibiaoService, ShengsiDingjizhibiaoService>();
services.AddScoped<IJizuChetaiService, JizuChetaiService>();
services.AddScoped<ILsjlService, LsjlService>();
services.AddScoped<IXdjyService, XdjyService>();
services.AddScoped<IHbjyService, HbjyService>();
services.AddScoped<INotificationService, NotificationService>();
// MediatR:注册命令/查询 Handler
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(NotificationService).Assembly));
return services;
}
}
@@ -0,0 +1,87 @@
namespace F9WebAiTest.Application.Dtos.Attendance;
public class AttendanceDto
{
public long Id { get; set; }
public long EmployeeId { get; set; }
public string EmployeeName { get; set; }
public string DepartmentName { get; set; }
public DateTime WorkDate { get; set; }
public DateTime? SignInTime { get; set; }
public DateTime? SignOutTime { get; set; }
public string SignInStatus { get; set; }
public string SignOutStatus { get; set; }
public string Remark { get; set; }
}
public class PunchResultDto
{
public string Type { get; set; } // signIn / signOut
public DateTime Time { get; set; }
public string Status { get; set; } // 正常/迟到/早退
public string Message { get; set; }
}
public class AttendanceQueryDto
{
public string Date { get; set; } = ""; // yyyy-MM-dd
public long? DepartmentId { get; set; }
public string Keyword { get; set; } = "";
public int PageIndex { get; set; } = 1;
public int PageSize { get; set; } = 10;
}
public class AttendanceSummaryDto
{
public int NormalDays { get; set; }
public int LateDays { get; set; }
public int EarlyLeaveDays { get; set; }
public int AbsentDays { get; set; }
public int LeaveDays { get; set; }
public int TotalDays { get; set; }
}
public class LeaveRequestDto
{
public long Id { get; set; }
public long EmployeeId { get; set; }
public string EmployeeName { get; set; }
public string DepartmentName { get; set; }
public string Type { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public string Reason { get; set; }
public string Status { get; set; }
public int CurrentOrderNo { get; set; } // 当前审批到第几步
public string ApproverName { get; set; }
public string ApproveRemark { get; set; }
public DateTime? ApprovedAt { get; set; }
public DateTime CreatedAt { get; set; }
public List<LeaveApproverDto> Approvers { get; set; } = new(); // 审批流程节点
}
public class LeaveApproverDto
{
public long Id { get; set; }
public long ApproverId { get; set; }
public string ApproverName { get; set; }
public int OrderNo { get; set; } // 序号(审批顺序)
public string Status { get; set; } // 待审批/已批准/已驳回
public string ApproveRemark { get; set; }
public DateTime? ApprovedAt { get; set; }
}
public class CreateLeaveDto
{
public string Type { get; set; } = "事假";
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public string Reason { get; set; } = "";
public List<long> ApproverIds { get; set; } = new(); // 审批人ID列表(顺序=审批顺序)
}
public class ApproveLeaveDto
{
public bool Approved { get; set; }
public string Remark { get; set; } = "";
}
@@ -0,0 +1,50 @@
namespace F9WebAiTest.Application.Dtos.Auth;
public class LoginDto
{
public string Username { get; set; }
public string Password { get; set; }
}
public class LoginResultDto
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public DateTime AccessTokenExpiresAt { get; set; }
public EmployeeInfoDto User { get; set; }
}
public class RefreshTokenDto
{
public string RefreshToken { get; set; }
}
public class EmployeeInfoDto
{
public long Id { get; set; }
public string Username { get; set; }
public string RealName { get; set; }
public string Nickname { get; set; }
public string Avatar { get; set; }
public string Phone { get; set; }
public string Email { get; set; }
public long DepartmentId { get; set; }
public string DepartmentName { get; set; }
public int Status { get; set; }
public List<string> Roles { get; set; } = new();
public List<string> Permissions { get; set; } = new();
}
public class ChangePasswordDto
{
public string OldPassword { get; set; }
public string NewPassword { get; set; }
}
public class UpdateProfileDto
{
public string Nickname { get; set; }
public string Avatar { get; set; }
public string Phone { get; set; }
public string Email { get; set; }
}
@@ -0,0 +1,86 @@
namespace F9WebAiTest.Application.Dtos.Base;
// ---- 工艺庄口 ----
public class CraftZhuangKouDto
{
public long Id { get; set; }
public int BelongingYear { get; set; }
public int BendiWaidi { get; set; }
public string BendiWaidiName { get; set; }
public string JidiWaigou { get; set; }
public string ZhuangKouCode { get; set; }
public string ZhuangKouName { get; set; }
public string ProductionDisplayName { get; set; }
public string Chandi { get; set; }
public int Sort { get; set; }
public string Memo { get; set; }
public string IsJiezhuang { get; set; }
}
public class CraftZhuangKouCreateDto
{
public int BelongingYear { get; set; }
public int BendiWaidi { get; set; }
public string JidiWaigou { get; set; } = "";
public string ZhuangKouCode { get; set; } = "";
public string ZhuangKouName { get; set; } = "";
public string ProductionDisplayName { get; set; } = "";
public string Chandi { get; set; } = "";
public int Sort { get; set; }
public string Memo { get; set; } = "";
public string IsJiezhuang { get; set; } = "0";
}
// ---- 生丝规格 ----
public class ShengsiGuigeDto
{
public long Id { get; set; }
public string Guige { get; set; }
public double CenterrangeLolimit { get; set; }
public double CenterrangeUplimit { get; set; }
public double CenterXiandu { get; set; }
public double ValidLolimit { get; set; }
public double ValidUplimit { get; set; }
public double XiaoyeLolimit { get; set; }
public double XiaoyeUplimit { get; set; }
public double DayeLolimit { get; set; }
public double DayeUplimit { get; set; }
public double TeyeLolimit { get; set; }
public double TeyeUplimit { get; set; }
public double YexianNkLolimitLeft { get; set; }
public double YexianNkLolimitRight { get; set; }
public double YexianNkUplimitLeft { get; set; }
public double YexianNkUplimitRight { get; set; }
public double LaodongJingsaiYexianLolimit { get; set; }
public double LaodongJingsaiYexianUplimit { get; set; }
public int Sort { get; set; }
public string Memo { get; set; }
}
// ---- 生丝定级指标 ----
public class ShengsiDingjizhibiaoDto
{
public long Id { get; set; }
public int Standard { get; set; }
public string StandardName { get; set; }
public string Guige { get; set; }
public string Grade { get; set; }
public double GradeScore { get; set; }
public double Piancha { get; set; }
public double Dacha { get; set; }
public double Erdu { get; set; }
public double Sandu { get; set; }
public double Qingjie { get; set; }
public double Jiejing { get; set; }
}
// ---- 机组车台 ----
public class JizuChetaiDto
{
public long Id { get; set; }
public long DepartmentId { get; set; }
public string DepartmentName { get; set; }
public int Jizu { get; set; }
public int ChetaiStart { get; set; }
public int ChetaiEnd { get; set; }
}
@@ -0,0 +1,3 @@
namespace F9WebAiTest.Application.Dtos.Common;
public record PageRequest(int Page = 1, int PageSize = 10);
@@ -0,0 +1,10 @@
namespace F9WebAiTest.Application.Dtos.Common;
public record PagedResult<T>(
List<T> Items,
int Page,
int PageSize,
long TotalCount)
{
public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
}
@@ -0,0 +1,33 @@
namespace F9WebAiTest.Application.Dtos;
public class DepartmentDto
{
public long Id { get; set; }
public string Name { get; set; }
public long? ParentId { get; set; }
public long? LeaderId { get; set; }
public string LeaderName { get; set; }
public string Phone { get; set; }
public int Sort { get; set; }
public int Status { get; set; }
public List<DepartmentDto> Children { get; set; } = new();
}
public class CreateDepartmentDto
{
public string Name { get; set; }
public long? ParentId { get; set; }
public long? LeaderId { get; set; }
public string Phone { get; set; } = "";
public int Sort { get; set; }
}
public class UpdateDepartmentDto
{
public string Name { get; set; }
public long? ParentId { get; set; }
public long? LeaderId { get; set; }
public string Phone { get; set; } = "";
public int Sort { get; set; }
public int Status { get; set; }
}
@@ -0,0 +1,61 @@
namespace F9WebAiTest.Application.Dtos;
public class EmployeeDto
{
public long Id { get; set; }
public string Username { get; set; }
public string RealName { get; set; }
public string Nickname { get; set; }
public string Avatar { get; set; }
public string Gender { get; set; }
public string Phone { get; set; }
public string Email { get; set; }
public long DepartmentId { get; set; }
public string DepartmentName { get; set; }
public int Status { get; set; }
public string Remark { get; set; }
public List<long> RoleIds { get; set; } = new();
public List<string> RoleNames { get; set; } = new();
public DateTime CreatedAt { get; set; }
}
public class CreateEmployeeDto
{
public string Username { get; set; }
public string Password { get; set; }
public string RealName { get; set; }
public string Nickname { get; set; } = "";
public string Gender { get; set; } = "男";
public string Phone { get; set; } = "";
public string Email { get; set; } = "";
public long DepartmentId { get; set; }
public string Remark { get; set; } = "";
public List<long> RoleIds { get; set; } = new();
}
public class UpdateEmployeeDto
{
public string RealName { get; set; }
public string Nickname { get; set; } = "";
public string Gender { get; set; } = "男";
public string Phone { get; set; } = "";
public string Email { get; set; } = "";
public long DepartmentId { get; set; }
public int Status { get; set; } = 1;
public string Remark { get; set; } = "";
public List<long> RoleIds { get; set; } = new();
}
public class ResetPasswordDto
{
public string NewPassword { get; set; }
}
public class EmployeeQueryDto
{
public string Keyword { get; set; } = "";
public long? DepartmentId { get; set; }
public int? Status { get; set; }
public int PageIndex { get; set; } = 1;
public int PageSize { get; set; } = 10;
}
@@ -0,0 +1,41 @@
namespace F9WebAiTest.Application.Dtos;
public class MenuDto
{
public long Id { get; set; }
public long? ParentId { get; set; }
public string Name { get; set; }
public string Type { get; set; }
public string Path { get; set; }
public string Component { get; set; }
public string Icon { get; set; }
public string Permission { get; set; }
public int Sort { get; set; }
public int Status { get; set; }
public List<MenuDto> Children { get; set; } = new();
}
public class CreateMenuDto
{
public long? ParentId { get; set; }
public string Name { get; set; }
public string Type { get; set; } = "menu";
public string Path { get; set; } = "";
public string Component { get; set; } = "";
public string Icon { get; set; } = "";
public string Permission { get; set; } = "";
public int Sort { get; set; }
}
public class UpdateMenuDto
{
public long? ParentId { get; set; }
public string Name { get; set; }
public string Type { get; set; } = "menu";
public string Path { get; set; } = "";
public string Component { get; set; } = "";
public string Icon { get; set; } = "";
public string Permission { get; set; } = "";
public int Sort { get; set; }
public int Status { get; set; } = 1;
}
@@ -0,0 +1,96 @@
using F9WebAiTest.Domain.Entities.Notice;
namespace F9WebAiTest.Application.Dtos.Notice;
// ---- 消息类型 ----
public class NotificationTypeDto
{
public long Id { get; set; }
public string Code { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string RequiredPermission { get; set; }
public bool IsActive { get; set; }
public long CreatedBy { get; set; }
public int Sort { get; set; }
public int Category { get; set; } // 消息分类:0通知 1待办 2公告
public string Icon { get; set; } = ""; // 图标
public string RouteTemplate { get; set; } = ""; // Web 端跳转路由模板
public bool Subscribed { get; set; } // 当前用户是否已订阅
public bool CanSubscribe { get; set; } = true; // 当前用户是否有权限订阅
}
public class NotificationTypeCreateDto
{
public string Code { get; set; } = "";
public string Name { get; set; } = "";
public string Description { get; set; } = "";
public string RequiredPermission { get; set; } = "";
public bool IsActive { get; set; } = true;
public int Sort { get; set; }
public int Category { get; set; } // 消息分类:0通知 1待办 2公告
public string Icon { get; set; } = "";
public string RouteTemplate { get; set; } = ""; // Web 端跳转路由模板,支持 {businessId} 占位符
}
// ---- 订阅 ----
public class SubscribeDto
{
public long NotificationTypeId { get; set; }
public bool Subscribe { get; set; } = true; // true=订阅 false=取消
}
// ---- 消息 ----
public class NotificationMessageDto
{
public long Id { get; set; }
public long NotificationTypeId { get; set; }
public string MessageTypeCode { get; set; }
public string TypeName { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int Priority { get; set; }
public string BusinessType { get; set; }
public string BusinessId { get; set; }
public int Category { get; set; } // 消息分类:0通知 1待办 2公告
public string RoutePath { get; set; } = ""; // Web 端跳转路由(点击消息时跳转)
public DateTime CreatedTime { get; set; }
public int ReadStatus { get; set; } // 当前用户阅读状态 0=未读 1=已读
public DateTime? ReadTime { get; set; }
}
public class UnreadCountDto
{
public int Count { get; set; }
}
// ---- 待办 ----
public class TodoItemDto
{
public long Id { get; set; }
public long MessageId { get; set; }
public string Title { get; set; } = "";
public string Content { get; set; } = "";
public int Priority { get; set; }
public string BusinessType { get; set; } = "";
public string BusinessId { get; set; } = "";
public string RoutePath { get; set; } = "";
public int TodoStatus { get; set; } // 0待处理 1处理中 2已完成 3已取消
public DateTime? DueTime { get; set; }
public DateTime? CompletedTime { get; set; }
public DateTime CreatedTime { get; set; }
}
public class CompleteTodoDto
{
public string HandleResult { get; set; } = ""; // 处理结果说明
}
// ---- 广播通知 ----
public class BroadcastNotificationDto
{
public string Title { get; set; } = "";
public string Content { get; set; } = "";
public NotificationPriority? Priority { get; set; }
public string RoutePath { get; set; } = "";
}
@@ -0,0 +1,86 @@
namespace F9WebAiTest.Application.Dtos.Prod;
// ---- 黑板检验 ----
/// <summary>黑板检验列表行(落丝记录 + 检验汇总)</summary>
public class HbjyListItemDto
{
public long LsjlId { get; set; }
public DateTime WorkDate { get; set; }
public long DeptId { get; set; }
public string DeptName { get; set; }
public int Jizu { get; set; }
public int Huishu { get; set; }
public string Guige { get; set; }
public string ZhuangKouName { get; set; }
public string Yanse { get; set; }
public bool Isbanche { get; set; }
public string IsbancheName { get; set; }
public string LuosiTime { get; set; }
public int Chetaishu { get; set; }
public int ChetaiStart { get; set; }
public int ChetaiEnd { get; set; }
public int YingluPianshu { get; set; } // 应录片数 = 车台数 × 20
public bool IsChecked { get; set; } // 是否已检验
public int Pianshu { get; set; } // 已录片数
public double Qingjie { get; set; } // 清洁分
public double Jiejing { get; set; } // 洁净分
public string Dengji { get; set; } // 等级
}
/// <summary>黑板检验编辑页数据</summary>
public class HbjyEditDto
{
public long LsjlId { get; set; }
public string DeptName { get; set; }
public int Jizu { get; set; }
public int Huishu { get; set; }
public string ZhuangKouName { get; set; }
public string Yanse { get; set; }
public bool Isbanche { get; set; }
public string IsbancheName { get; set; }
public string Guige { get; set; }
public int ChetaiStart { get; set; }
public int ChetaiEnd { get; set; }
public int Chetaishu { get; set; }
public int YingluPianshu { get; set; } // 应录片数
public string Dengji { get; set; } // 等级
public List<HbjyRecordDto> Records { get; set; } = new();
}
/// <summary>黑板检验车台明细 DTO</summary>
public class HbjyRecordDto
{
public int Chetaihao { get; set; }
public int YdYidu { get; set; }
public int YdErdu { get; set; }
public int YdSandu { get; set; }
public int Jj100 { get; set; }
public int Jj95 { get; set; }
public int Jj90 { get; set; }
public int Jj85 { get; set; }
public int Jj80 { get; set; }
public int Jj75 { get; set; }
public int Jj70 { get; set; }
public int Jj65 { get; set; }
public int Jj60 { get; set; }
public int QjZycdTdc { get; set; }
public int QjCycdFs { get; set; }
public int QjCycdDc { get; set; }
public int QjCycdNfc { get; set; }
public int QjCycdDcj { get; set; }
public int QjCycdZlx { get; set; }
public int QjPtcdXc { get; set; }
public int QjPtcdCj { get; set; }
public int QjPtcdLx { get; set; }
public int QjPtcdHjls { get; set; }
public int Pianshu { get; set; } // 本车台片数(计算得出)
}
/// <summary>黑板检验保存请求</summary>
public class HbjySaveDto
{
public long LsjlId { get; set; }
public string Dengji { get; set; } = "";
public List<HbjyRecordDto> Records { get; set; } = new();
}
@@ -0,0 +1,41 @@
namespace F9WebAiTest.Application.Dtos.Prod;
public class LsjlDto
{
public long Id { get; set; }
public DateTime WorkDate { get; set; }
public long DeptId { get; set; }
public string DeptName { get; set; }
public long GyzkId { get; set; }
public string ZhuangKouName { get; set; }
public int Jizu { get; set; }
public string LuosiTime { get; set; }
public string Yanse { get; set; }
public int Huishu { get; set; }
public int ChetaiStart { get; set; }
public int ChetaiEnd { get; set; }
public int Chetaishu { get; set; }
public bool Isbanche { get; set; }
public string IsbancheName { get; set; }
public double Hours { get; set; }
public string Guige { get; set; }
public double Chesu { get; set; }
}
public class LsjlCreateDto
{
public DateTime WorkDate { get; set; }
public long DeptId { get; set; }
public long GyzkId { get; set; }
public int Jizu { get; set; }
public string LuosiTime { get; set; } = "";
public string Yanse { get; set; } = "";
public int Huishu { get; set; }
public int ChetaiStart { get; set; }
public int ChetaiEnd { get; set; }
public int Chetaishu { get; set; }
public bool Isbanche { get; set; }
public double Hours { get; set; }
public string Guige { get; set; } = "";
public double Chesu { get; set; }
}
@@ -0,0 +1,93 @@
using F9WebAiTest.Application.Dtos.Base;
namespace F9WebAiTest.Application.Dtos.Prod;
// ---- 纤度检验 ----
/// <summary>纤度检验列表行(落丝记录 + 检验汇总)</summary>
public class XdjyListItemDto
{
public long LsjlId { get; set; } // 落丝记录 Id
public DateTime WorkDate { get; set; } // 日期
public long DeptId { get; set; }
public string DeptName { get; set; }
public int Jizu { get; set; } // 机组
public int Huishu { get; set; } // 回数
public string Guige { get; set; } // 规格
public string ZhuangKouName { get; set; } // 庄口
public string Yanse { get; set; } // 颜色
public bool Isbanche { get; set; } // 半车
public string IsbancheName { get; set; }
public int Chetaishu { get; set; } // 车台数
public int ChetaiStart { get; set; }
public int ChetaiEnd { get; set; }
public string LuosiTime { get; set; } // 落丝时间
public int Yingchengnum { get; set; } // 应秤数 = 车台数 × (半车?2:4)
public bool IsChecked { get; set; } // 是否已检验
public int Jiaoshu { get; set; } // 已秤数
public double Avgxiandu { get; set; } // 平均纤度
public double Junfangcha { get; set; } // 均方差
public double Zongcha { get; set; } // 总差
public double FanxianLv { get; set; } // 范纤率
public int FanweiNum { get; set; } // 范围数
public int XiaoyeNum { get; set; } // 小野数
public int DayeNum { get; set; } // 大野数
public int TeyeNum { get; set; } // 特野数
public int CenterNum { get; set; } // 中心数
public string Dengji { get; set; } // 等级
}
/// <summary>纤度检验编辑页数据</summary>
public class XdjyEditDto
{
public long LsjlId { get; set; }
public string DeptName { get; set; }
public int Jizu { get; set; }
public int Huishu { get; set; }
public string ZhuangKouName { get; set; }
public string Yanse { get; set; }
public bool Isbanche { get; set; }
public string IsbancheName { get; set; }
public string Guige { get; set; }
public int ChetaiStart { get; set; }
public int ChetaiEnd { get; set; }
public int Chetaishu { get; set; }
public int Yingchengnum { get; set; } // 应秤数
public string Dengji { get; set; } // 等级
public ShengsiGuigeDto GuigeConfig { get; set; } // 生丝规格(用于判定/颜色标识)
public List<XdjyRecordDto> Xdjy { get; set; } = new();
}
/// <summary>纤度检验车台明细 DTO</summary>
public class XdjyRecordDto
{
public int Chetaihao { get; set; } // 车台号
public double Xd1 { get; set; }
public double Xd2 { get; set; }
public double Xd3 { get; set; }
public double Xd4 { get; set; }
public int XdNum { get; set; } // 已称绞数
public double XdAvg { get; set; } // 平均纤度
public int FanweiNum { get; set; } // 范围
public int XiaoyeNum { get; set; } // 小野
public int DayeNum { get; set; } // 大野
public int TeyeNum { get; set; } // 特野
public int CenterNum { get; set; } // 中心
}
/// <summary>纤度检验保存请求</summary>
public class XdjySaveDto
{
public long LsjlId { get; set; }
public string Dengji { get; set; } = "";
public List<XdjySaveItemDto> Items { get; set; } = new();
}
public class XdjySaveItemDto
{
public int Chetaihao { get; set; }
public double Xd1 { get; set; }
public double Xd2 { get; set; }
public double Xd3 { get; set; }
public double Xd4 { get; set; }
}
@@ -0,0 +1,31 @@
namespace F9WebAiTest.Application.Dtos;
public class RoleDto
{
public long Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public string Description { get; set; }
public int Sort { get; set; }
public int Status { get; set; }
public int EmployeeCount { get; set; }
public DateTime CreatedAt { get; set; }
}
public class CreateRoleDto
{
public string Name { get; set; }
public string Code { get; set; }
public string Description { get; set; } = "";
public int Sort { get; set; }
public List<long> MenuIds { get; set; } = new();
}
public class UpdateRoleDto
{
public string Name { get; set; }
public string Description { get; set; } = "";
public int Sort { get; set; }
public int Status { get; set; } = 1;
public List<long> MenuIds { get; set; } = new();
}
@@ -0,0 +1,5 @@
using F9WebAiTest.Domain.Enums.SYS.Departments;
namespace F9WebAiTest.Application.Dtos.SYS.Departments;
public record DepartmentReadDto_View(long Id, string Code, string Name, long ParentId, Department_ManageType_Enum ManageType, Department_DutyType_Enum DutyType, string Description, bool IsActive, int SortOrder);
@@ -0,0 +1,16 @@
using F9WebAiTest.Domain.Enums.SYS.RBAC;
namespace F9WebAiTest.Application.Dtos.SYS.RBAC.Menus;
public record MenuReadDto_Menu_Edit(
long Id,
string Code,
string Name,
string IconSvg,
MenuType_Enum MenuType,
string AssemblyName,
string TypeFullName,
bool IsActive,
long ParentId,
int SortOrder
);
@@ -0,0 +1,23 @@
namespace F9WebAiTest.Application.Dtos.SYS.RBAC.Menus;
// 读模型
public record MenuReadDto_Menu_Table(
long Id,
long ParentId,
string Code,
string Name,
string IconSvg,
string MenuType,
string AssemblyName,
string TypeFullName,
string ParentName,
int Level,
string IsActive,
int SortOrder,
// 用于前端 table select
bool Selected = false,
// 用于前端 table tree
MenuReadDto_Menu_Table[]? Items = default
);
@@ -0,0 +1,5 @@
using F9WebAiTest.Domain.Enums.SYS.RBAC;
namespace F9WebAiTest.Application.Dtos.SYS.RBAC.Roles;
public record RolePermissionReadDto_View(long Id, long RoleId, long PermissionId, long ResourceId, Permission_Type_Enum PermissionType);
@@ -0,0 +1,3 @@
namespace F9WebAiTest.Application.Dtos.SYS.RBAC.Roles;
public record RoleReadDto_Cache(long Id, string Name, string Code, int SortOrder);
@@ -0,0 +1,3 @@
namespace F9WebAiTest.Application.Dtos.SYS.RBAC.Roles;
public record RoleReadDto_Edit(long Id, string Name, string Code, int SortOrder, string Description);
@@ -0,0 +1,3 @@
namespace F9WebAiTest.Application.Dtos.SYS.RBAC.Roles;
public record RoleReadDto_View(long Id, string Name, string Code, int SortOrder, int MenuPermissionCount, int ReportPermissionCount, int DepPermissionCount, bool IsActive);
@@ -0,0 +1,4 @@
namespace F9WebAiTest.Application.Dtos.SYS.RBAC.Users;
// 用户读模型
public record UserReadDto_Edit(long Id, string UserName, string PhoneNumber, string LoginName, long DepartmentId, int SortOrder);
@@ -0,0 +1,3 @@
namespace F9WebAiTest.Application.Dtos.SYS.RBAC.Users;
public record UserReadDto_Login(long Id, string UserName, string LoginName, string PasswordHash, long DepartmentId);
@@ -0,0 +1,4 @@
namespace F9WebAiTest.Application.Dtos.SYS.RBAC.Users;
// 用户读模型(列表视图)
public record UserReadDto_View(long Id, string UserName, string PhoneNumber, string LoginName, string DepartmentName, string RoleName, string LastLoginAt, int SortOrder, bool IsActive);
@@ -0,0 +1,3 @@
namespace F9WebAiTest.Application.Dtos.SYS.RBAC.Users;
public record UserRoleReadDto_Cache(long Id, long UserId, long RoleId, string RoleName);
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<RootNamespace>F9WebAiTest.Application</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\F9WebAiTest.Domain\F9WebAiTest.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.2" />
</ItemGroup>
</Project>
@@ -0,0 +1,10 @@
using F9WebAiTest.Application.Contracts;
namespace F9WebAiTest.Application.Interfaces.LoginUsers;
/// <summary>登录用户服务(从当前请求上下文获取登录用户信息)</summary>
public interface ILoginUserService
{
/// <summary>登录用户信息</summary>
LoginUserInfo LoginUserInfo { get; }
}
@@ -0,0 +1,10 @@
using F9WebAiTest.Application.Dtos.SYS.Departments;
using F9WebAiTest.Domain.Interfaces;
namespace F9WebAiTest.Application.Interfaces.Repositories.SYS.Departments;
/// <summary>部门读仓储(返回读模型 DTO)</summary>
public interface IDepartmentReadRepository : IDInterface
{
Task<List<DepartmentReadDto_View>> GetAllAsync();
}
@@ -0,0 +1,14 @@
using F9WebAiTest.Application.Dtos.SYS.RBAC.Menus;
using F9WebAiTest.Domain.Interfaces;
namespace F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Menus;
/// <summary>菜单读仓储(返回读模型 DTO)</summary>
public interface IMenuReadRepository : IDInterface
{
Task<MenuReadDto_Menu_Edit?> GetMenuByIdForEditAsync(long id);
Task<bool> ExistMenuByTypeFullnameExcludeIdAsync(string typeFullname, long? excludeId);
Task<List<MenuReadDto_Menu_Table>> GetAllMenuAsync(string like, long? parentId);
Task<List<MenuReadDto_Menu_Table>> GetTreeMenuAsync();
}
@@ -0,0 +1,21 @@
using F9WebAiTest.Application.Dtos.SYS.RBAC.Roles;
using F9WebAiTest.Domain.Enums.SYS.RBAC;
using F9WebAiTest.Domain.Interfaces;
namespace F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Roles;
/// <summary>角色读仓储(返回读模型 DTO)</summary>
public interface IRoleReadRepository : IDInterface
{
/// <summary>查询单个</summary>
Task<RoleReadDto_Edit> GetByIdAsync(long id);
/// <summary>获取所有对象集合</summary>
Task<List<RoleReadDto_View>> GetAllAsync();
/// <summary>获取所有对象集合(缓存用)</summary>
Task<List<RoleReadDto_Cache>> GetAllForCacheAsync();
/// <summary>通过资源类型获取角色权限集合</summary>
Task<List<RolePermissionReadDto_View>> GetRolePermissionsByResourceType(long roleId, Resource_Type_Enum resourceType);
}
@@ -0,0 +1,30 @@
using F9WebAiTest.Application.Dtos.Common;
using F9WebAiTest.Application.Dtos.SYS.RBAC.Users;
using F9WebAiTest.Domain.Interfaces;
namespace F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Users;
/// <summary>用户读仓储(返回读模型 DTO)</summary>
public interface IUserReadRepository : IDInterface
{
// 单个用户查询
Task<UserReadDto_Edit> GetByIdAsync(long id);
// 通过 ID 集合查询用户集合
Task<List<UserReadDto_Edit>> GetByIdsAsync(List<long> ids);
// 通过登录名查询
Task<UserReadDto_Login> GetByLoginNameAsync(string loginName);
// 通过电话查询
Task<UserReadDto_Login> GetByPhoneAsync(string phone);
// 条件分页查询
Task<PagedResult<UserReadDto_View>> GetPagedAsync(long departmentId, string like, int page = 1, int pageSize = 10);
// 获取用户绑定角色集合
Task<List<UserRoleReadDto_Cache>> GetUserRolesByUserId(long userId);
// 通过角色 ID 集合查询用户集合
Task<List<UserReadDto_Edit>> GetByRoleIdsAsync(List<long> roleIds);
}

Some files were not shown because too many files have changed in this diff Show More