基于 DevExpress 从零开始搭建多租户自洽的权限数据配置模块(一)
从零开始设计一个最简单的winform练习程序,在这个过程中会涉及到devexpress、简单数据库设计、简单分层设计、基本操作方法等一些简单知识点的演示。
1、思路设计
基础数据的维护管理,以简单基本操作的形式展开。主要是演示devexpress做基本的增删改查、加载表单、建立多表关联、用户操作动态加载数据等。
2、程序基本代码轮廓
我们先把这个简单工具的代码划分一个模糊的层次。总体上是一个这样的思路:
UI结构 | 登录界面 + 主界面 + Mdi子界面管理 + 响应交互的逻辑 |
视图 | winform窗体,主要是基本操作界面 |
后台控制 | 数据请求层 + Json转化层(为后续前后端分离做准备) |
模型 | 数据库模型 + DTO模型 |
数据库 | 数据库结构设计 以及建库脚本保留 |
对UI结构做一个基本要求:基础数据的维护操作,必须是基于单个租户作为基本范围进行配置。系统进入的时候,必须强制管理员选择要操作的租户。
这么做的原因是,我们有很多非常小巧的孤岛业务应用,如果每部署一个应用都需要跟着部署一套权限模块的话,有点浪费。多租户可以在同一个程序中进行托管运维,逻辑可以在租户约束的范围内自洽适应。
在这里,我们定义了一个XtraFormBaseTenancy。
using DevExpress.XtraEditors;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace HiAuth2021
{
public partial class XtraFormBaseTenancy : XtraForm
{
private bool isNeedAutoClose = false;
public XtraFormBaseTenancy()
{
InitializeComponent();
this.Activated += XtraFormBase_Activated;
this.Shown += XtraFormBase_Shown;
}
/// <summary>
/// 因为Activated事件订阅的优先级顺序要早于Shown事件的订阅契机,因此私有属性会在此生效
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void XtraFormBase_Shown(object sender, EventArgs e)
{
if (isNeedAutoClose) this.Close();
}
/// <summary>
/// 在基类窗体被激活时,判断是否已经进入选定的租户范围,如果没有,提醒用户进行选择,并强制退出继承该基类窗体的子窗体
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void XtraFormBase_Activated(object sender, EventArgs e)
{
if (StaticGlobal.Tenancy == null || string.IsNullOrEmpty(StaticGlobal.Tenancy.CurrentTenancy))
{
isNeedAutoClose = true;
HiAuthStaticSetting.AlertErrorMsg(this, "请先选择您要配置的租户!");
}
HiAuthStaticSetting.SetCurrentLocationText(this.Text);
}
}
}
其他的业务操作界面只需要继承上面的基类窗体,就可以实现不选择租户窗体就打不开的效果。变相等于实现了菜单权限的控制逻辑。
public partial class HiAuthUsersManage : XtraFormBaseTenancy
{
private bool isNeedPass = false;
private List<DbModels.t_auth_users> source;
public HiAuthUsersManage()
{
InitializeComponent();
this.Shown += HiAuthUsersManage_Shown;
gridView1.RowCellClick += GridView1_RowCellClick;
gridView1.FocusedRowChanged += GridView1_FocusedRowChanged;
gridView1.SelectionChanged += GridView1_SelectionChanged;
...
我们使用一个全局状态管理器,来静态地托管用户登录以后的状态数据。当然,这种方式仅仅是适用于winform形式,web的时候他其实是个会话(Session)
using HiAuth2021.DtoModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace HiAuth2021
{
/// <summary>
/// 全局静态数据
/// </summary>
public static class StaticGlobal
{
public static DtoModels.TenancyCutting Tenancy { get; set; } = new DtoModels.TenancyCutting();
public static List<DtoModels.TenancyCutting> TenancySource { get; set; }
= RestHelper<DtoModels.TenancyCutting>.GetDataSourceByRestRequest(() =>
{
return new HiAuthBasicsRestBussiness.HiAuthTenancyRestBusiness().GetTenancyCuttingSource();
});
}
/// <summary>
/// 系统登录个人Session加密后记录到缓存服务器
/// </summary>
public class LoginInfo
{
/// <summary>
/// 主界面窗体
/// </summary>
public static HiAuthManageSystem MainForm { get; set; }
/// <summary>
/// 登录窗体
/// </summary>
public static HiAuthLogin LoginForm { get; set; }
/// <summary>
/// 登录状态
/// </summary>
public static LoginState LoginState { get; set; }
/// <summary>
/// 登录参数
/// </summary>
public static LoginEntity LoginConfig { get; set; }
/// <summary>
/// 用户账号
/// </summary>
public static string UserCode { get; set; }
/// <summary>
/// 用户姓名
/// </summary>
public static string UserName { get; set; }
/// <summary>
/// 性别
/// </summary>
public static string UserSex { get; set; }
/// <summary>
/// 部门代码
/// </summary>
public static string UserDeptCode { get; set; }
/// <summary>
/// 部门名称
/// </summary>
public static string UserDeptName { get; set; }
/// <summary>
/// 岗位
/// </summary>
public static string UserDuty { get; set; }
/// <summary>
/// IP地址
/// </summary>
public static string UserLoginIp { get; set; }
/// <summary>
/// 当前设备名称
/// </summary>
public static string Device { get; set; }
/// <summary>
/// 当前登录域账户
/// </summary>
public static string WindowsUser { get; set; }
/// <summary>
/// 认证角色id
/// </summary>
public static List<int> RoleIds { get; set; }
/// <summary>
/// 可访问菜单集合
/// </summary>
public static List<PowerMenus> AccessablePowerMenus { get; set; }
/// <summary>
/// 共享文件夹根路径
/// </summary>
public static string ShareDiskRootPath { get => @"***"; }
/// <summary>
/// 连接共享文件夹的账号
/// </summary>
public static string ShareDiskUser { get => "***"; }
/// <summary>
/// 连接共享文件夹的密码
/// </summary>
public static string ShareDiskPassword { get => "***"; }
/// <summary>
/// 记住皮肤
/// </summary>
public static SkinConfig SkinRemember
{
get
{
SkinConfig skinConfig = new SkinConfig() { SkinName = "Office 2016 Colorful" };
//判断是否存在记住密码文件
if (FileOps.IsExistFile("SkinPath"))
{
SkinConfig config = FileOps.LoadObjectFromXml("SkinPath", typeof(SkinConfig)) as SkinConfig;
if (config != null)
{
return config;
}
else
{
return skinConfig;
}
}
else
{
return skinConfig;
}
}
}
}
/// <summary>
/// 认证成功返回权限菜单集合
/// </summary>
public class PowerMenus
{
/// <summary>
/// 父项号
/// </summary>
public int? ParentId { get; set; }
/// <summary>
/// 子项号
/// </summary>
public int MenuId { get; set; }
/// <summary>
/// 菜单名称
/// </summary>
public string MenuName { get; set; }
/// <summary>
/// 角色Id
/// </summary>
public int RoleId { get; set; }
/// <summary>
/// 角色名称
/// </summary>
public string RoleName { get; set; }
/// <summary>
/// 菜单路径(命名空间加类名)
/// </summary>
public string MenuPath { get; set; }
}
/// <summary>
/// 登录状态
/// </summary>
public enum LoginState
{
LoginFail = -1, //登录失败
Login = 0, //登录成功
Logout = 1, //注销
Close = 2, //关闭
Exit = 3, //退出
LostConnection = 4, //失去连接
}
/// <summary>
/// 皮肤配置
/// </summary>
public class SkinConfig
{
public string SkinName { get; set; }
}
}
同时,我还需要把一些需要能够被全局控制的控件对象,也放到全局的状态管理器里面,以便于我在程序的任何一个位置,都能够调用到他,使他配合我们显示合适的场景或内容。比如,我当前正在访问哪个页面。
using DevExpress.XtraBars;
using DevExpress.XtraBars.Alerter;
using DevExpress.XtraSplashScreen;
using DevExpress.XtraTabbedMdi;
using DevExpress.XtraTreeList;
using DevExpress.XtraTreeList.Nodes;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace HiAuth2021
{
public static class HiAuthStaticSetting
{
public static HiAuthManageSystem MainForm { get; set; }
public static XtraTabbedMdiManager Mdi { get; set; }
public static SplashScreenManager SplashManager { get; set; }
public static AlertControl Alert { get; set; }
public static BarStaticItem CurrentTenancy { get; set; }
public static BarStaticItem CurrentLogin { get; set; }
public static BarStaticItem CurrentLocation { get; set; }
public static void SetCurrentLocationText(string text)
{
HiAuthStaticSetting.CurrentLocation.Caption = $"您正在访问:{text}";
}
public static void CloseMdiPagesOpenning()
{
int count = HiAuthStaticSetting.Mdi.Pages.Count;
for (int i = 0; i < count; i++) HiAuthStaticSetting.Mdi.Pages[0].MdiChild.Close();
}
public static void SetWaittingFormOpen(Form frm)
{
SplashManager.ShowWaitForm();
}
public static void SetWaittingFormClose(Form frm)
{
SplashManager.CloseWaitForm();
}
public static void AlertMsg(Form frm,string msg)
{
Alert.Show(frm,new AlertInfo("温馨提示",msg));
}
public static void AlertErrorMsg(Form frm, string msg)
{
Alert.Show(frm, new AlertInfo("错误提示", msg, global::HiAuth2021.Properties.Resources.bug));
}
/// <summary>
/// 系统菜单点击事件处理
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void MenuTree_MouseClick(object sender, MouseEventArgs e)
{
TreeListHitInfo hitinfo = (sender as TreeList).CalcHitInfo(e.Location);
if (hitinfo.HitInfoType != HitInfoType.Cell || e.Button != MouseButtons.Left)
{
return;
}
ShowChildForm(hitinfo.Node);
}
/// <summary>
/// 系统菜单树的图标分层显示
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void MenuTree_CustomDrawNodeImages(object sender, CustomDrawNodeImagesEventArgs e)
{
if (e.Node.Nodes.Count > 0 && !e.Node.Expanded)
{
e.SelectImageIndex = 0;
}
else if (e.Node.Nodes.Count > 0 && e.Node.Expanded)
{
e.SelectImageIndex = 1;
}
else
{
e.SelectImageIndex = 2;
}
}
/// <summary>
/// 根据窗体Text属性获取打开的窗体实例
/// </summary>
/// <param name="formName"></param>
/// <returns></returns>
public static dynamic GetOpenedForm(string formName)
{
foreach (dynamic openedForm in Application.OpenForms)
{
if (openedForm.AccessibilityObject.Name == formName)
{
return openedForm;
}
}
return null;
}
/// <summary>
/// 根据窗体Text属性判断是否已经打开窗体
/// </summary>
/// <param name="formName"></param>
/// <returns></returns>
public static dynamic IsHasOpened(string formName)
{
foreach (dynamic openedForm in Application.OpenForms)
{
if (!openedForm.IsDisposed && openedForm.AccessibilityObject.Name == formName)
{
return true;
}
}
return false;
}
/// <summary>
/// 连接共享文件夹,连接上后可以像操作本地磁盘的方式操作文件夹和文件
/// </summary>
/// <param name="path">共享文件夹路径</param>
/// <param name="userName">用户名</param>
/// <param name="passWord">密码</param>
/// <returns>连接成功返回true,否则返回false</returns>
public static bool ShareDiskConnect()
{
string path = LoginInfo.ShareDiskRootPath; //共享地址
string userName = LoginInfo.ShareDiskUser; //共享用户
string passWord = LoginInfo.ShareDiskPassword; //共享密钥
bool Flag = false; //标识
Process proc = new Process();
try
{
proc.StartInfo.FileName = "cmd.exe";
proc.StartInfo.UseShellExecute = false;
proc.StartInfo.RedirectStandardInput = true;
proc.StartInfo.RedirectStandardOutput = true;
proc.StartInfo.RedirectStandardError = true;
proc.StartInfo.CreateNoWindow = true;
proc.Start();
string dosLine = @"net use " + path + " /User:" + userName + " " + passWord + " /PERSISTENT:YES";
proc.StandardInput.WriteLine(dosLine);
proc.StandardInput.WriteLine("exit");
while (!proc.HasExited)
{
proc.WaitForExit(1000);
}
string errormsg = proc.StandardError.ReadToEnd();
proc.StandardError.Close();
if (string.IsNullOrEmpty(errormsg))
{
Flag = true;
}
else
{
//判断是否已经连上
if (errormsg.Contains("发生系统错误 1219"))
{
Flag = true;
}
else
{
throw new Exception(errormsg);
}
}
}
catch (Exception ex)
{
throw ex;
}
finally
{
proc.Close();
proc.Dispose();
}
return Flag;
}
/// <summary>
/// 获取样式保存位置
/// </summary>
/// <returns></returns>
public static string GetShareStylePath()
{
string path = "\\DeepSeaAps\\ShareStyle\\" + LoginInfo.UserCode + LoginInfo.UserName;
return Path.Combine(LoginInfo.ShareDiskRootPath, path.StartsWith("\\") ? path.Substring(1) : path) + "\\";
}
/// <summary>
/// 获取固定列保存位置
/// </summary>
/// <returns></returns>
public static string GetFixedColumnConfigPath()
{
string path = "\\DeepSeaAps\\FixedColumnConfig\\" + LoginInfo.UserCode + LoginInfo.UserName;
return Path.Combine(LoginInfo.ShareDiskRootPath, path.StartsWith("\\") ? path.Substring(1) : path) + "\\";
}
/// <summary>
/// 把打开窗体作为Mdi子窗体合并到主界面中
/// </summary>
/// <param name="frm"></param>
public static void ShowAsChildForm(Form frm)
{
if (!IsHasOpened(frm.Text))
{
frm.MdiParent = MainForm;
frm.Show();
}
else
{
dynamic form = GetOpenedForm(frm.Text);
form.Dispose();
frm.MdiParent = MainForm;
frm.Show();
}
}
/// <summary>
/// 打开窗体
/// </summary>
/// <param name="node"></param>
public static void ShowChildForm(TreeListNode node)
{
if (!node.HasChildren && node.Visible == true)
{
var formtype = node.GetValue("MenuPath") == null ? null : node.GetValue("MenuPath").ToString();
var formName = node.GetValue("MenuName") == null ? null : node.GetValue("MenuName").ToString();
if (formtype == null || formName == null)
{
AlertErrorMsg(MainForm, "界面加载失败!请确认选择界面是否已经开放进入!如有疑问,请联系系统工程师!");
}
else
{
ShowChildForm(formtype, formName);
}
}
}
/// <summary>
/// 打开窗体
/// </summary>
/// <param name="formtype">窗体对象的全路径类型名称</param>
/// <param name="formName">窗体的Text 需要唯一识别</param>
public static void ShowChildForm(string formtype, string formName)
{
if (!IsHasOpened(formName))
{
try
{
dynamic frm = Type.GetType(formtype).Assembly.CreateInstance(formtype);
frm.MdiParent = MainForm;
frm.Show();
}
catch (Exception)
{
AlertErrorMsg(MainForm, "界面加载失败!请确认选择界面是否已经开放进入!如有疑问,请联系系统工程师!");
}
}
else
{
dynamic form = GetOpenedForm(formName);
form.Activate();
}
}
/// <summary>
/// 设置等待框开启
/// </summary>
public static void SetWaitFormOpen(Form frm)
{
if (SplashManager == null || SplashManager.IsSplashFormVisible != true)
{
SplashManager = new SplashScreenManager(frm, typeof(HiAuthWaitting), true, true);
SplashManager.ShowWaitForm();
}
}
/// <summary>
/// 设置等待框关闭
/// </summary>
public static void SetWaitFormClose(Form frm)
{
if (SplashManager != null || SplashManager.IsSplashFormVisible == true)
{
SplashManager.CloseWaitForm();
SplashManager.Dispose();
}
}
}
}
3、数据库结构
我们在这里仅仅展示一下表和结构的设计,随着程序的慢慢细化,我们可能会有一些调整。
下一篇开始,我们开始具体业务界面的操作介绍。