first commit
This commit is contained in:
+28
@@ -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
|
||||
@@ -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` - 审批请假
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 { }
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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)"
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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.
+31
@@ -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;
|
||||
}
|
||||
}
|
||||
+35
@@ -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;
|
||||
}
|
||||
}
|
||||
+22
@@ -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;
|
||||
}
|
||||
}
|
||||
+29
@@ -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();
|
||||
}
|
||||
+31
@@ -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;
|
||||
}
|
||||
}
|
||||
+80
@@ -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;
|
||||
}
|
||||
}
|
||||
+37
@@ -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;
|
||||
}
|
||||
}
|
||||
+26
@@ -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();
|
||||
}
|
||||
+13
@@ -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);
|
||||
}
|
||||
+14
@@ -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);
|
||||
}
|
||||
+40
@@ -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;
|
||||
}
|
||||
}
|
||||
+35
@@ -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;
|
||||
}
|
||||
}
|
||||
+26
@@ -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;
|
||||
}
|
||||
}
|
||||
+32
@@ -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);
|
||||
}
|
||||
+13
@@ -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; }
|
||||
}
|
||||
+10
@@ -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();
|
||||
}
|
||||
+14
@@ -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();
|
||||
}
|
||||
+21
@@ -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);
|
||||
}
|
||||
+30
@@ -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
Reference in New Issue
Block a user