65.9K
CodeProject 正在变化。 阅读更多。
Home

ERP 基础设施

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (23投票s)

2015年12月15日

CPOL

11分钟阅读

viewsIcon

31620

downloadIcon

126

一个示例基础设施项目,例如 ERP、MRP、CRM、管理面板。

引言

在本文中,将展示一个基础设施示例,用于 ERP、MRP 和 CRM 等项目。考虑到此类项目中用户请求的挑战性和多样性,必然需要执行符合软件标准的动态且直接的后端和前端工作。鉴于工作的复杂性,创建这样的工作环境是合适的。高级软件开发人员设定软件标准,初级软件开发人员也通过使用这些标准从分析师那里获取用户流程。在考虑软件标准时,它包括创建数据库模型、实现 n 层架构和应用各种显示示例。在高级软件开发人员设定这些标准后,只剩下最后一件事要做:复制并粘贴这些屏幕和代码块;更改标签、数据库链接和相关页面信息,并将屏幕呈现给最终用户。因此,序列化标准集至关重要。假设通过此类工作,按照特定标准序列化了数百个屏幕,然后高级软件开发人员希望更改他们设定的某个标准。高级软件开发人员必须借助正则表达式捕获为所有屏幕准备的标准代码块(当然,这只能是相关的代码块),并将其替换为新的代码块。

背景

首先,让我们看看表示层应该执行哪些操作。使用屏幕分割器将其分为四个部分。左侧应有一个根据用户授权创建的导航菜单,顶部应有带有项目徽标和名称以及登录的用户活动,底部应有页脚区域,中间是显示我们页面的内容区域。左侧导航菜单的设计应接近数据库构建。因此,数据库设计、导航菜单、将在内容区域填充的网格、树形图像和滚动树形图像应兼容。例如,导航菜单中的“模板处理 > 主模板”显示一个网格。单击网格详细信息,将看到“详细模板 1”和“详细模板 2”网格。单击导航菜单中的“详细模板 1”,将打开一个包含“详细模板 1”所有记录的完整列表。

如果要在内容页面打开的屏幕由网格组成,则此网格的特征应如下:

DevExpress 控件为我们提供了过滤、排序、添加和删除列、替换列、编写自定义查询、导出等功能。但最重要的是,需要从调用它们的地方设置所有这些功能。例如,当在以下屏幕上打开“主模板网格”的详细信息时,我应该能够修改“详细模板 1”网格的所需属性。当单击详细信息选项卡时,我希望以特定的宽度和高度打开相关的选项卡,并在详细信息弹出时隐藏主表中的列。

网格列可能因相关数据类型而异。例如,这是 int 列的模板。

Settings.Columns.Add(column =>
{
    column.FieldName = "IntColumn";
    column.Caption = Translator.TemplateIntColumn;
    column.ColumnType = MVCxGridViewColumnType.SpinEdit;
    ((SpinEditProperties)column.PropertiesEdit).NumberFormat = SpinEditNumberFormat.Number;
    ((SpinEditProperties)column.PropertiesEdit).NumberType = SpinEditNumberType.Integer;
});

 

应如下编辑屏幕:

能够调整编辑表单的大小,并在调整大小时为相关选项卡中的某些对象设置百分比高度。例如,当通过调整大小增加屏幕高度时,多行文本列和 HTML 列的高度会按一定百分比增加。

通过组合框旁边的菜单,可以添加新记录、更改或删除选定记录,或者转到显示网格的组合框相关页面。通过菜单组合框打开的页面显示在弹出菜单中,允许无限嵌套地打开页面。例如,当我们编辑屏幕上的组合框“person”时,将弹出以下屏幕:

如果对于记录 Fatih Doğanay,在该重新打开的窗口中没有父记录,则能够单击“添加”按钮。单击“添加”后,再次打开“添加新人员”页面并输入新记录。添加新记录后,将对“父”组合框进行说明。当最终用户进行此类工作时,他/她无需遵守数据录入顺序。换句话说,他/她无需首先输入此记录即可选择“父”记录。

如何实现这种无限打开且不混淆屏幕上相似对象的 ID 和名称?每个编辑表单都包含一个代码,此代码将在打开新屏幕时使用。不混淆其 ID 和名称,这与 .NET 平台中的命名空间逻辑完全相同,所有对象都通过从这些命名空间派生来获取其名称。

@Html.Action("ActionPartial", "Popup", new ActionPopupModel
{
    Name = Model.Name + "Edit"
})

例如,上面屏幕最顶部显示的“父”组合框的名称是 cmbFatherAccountPersonGrid1Edit。也就是说,这意味着 AccountPerson 网格的 ID 号 1 正在被编辑,并且屏幕添加也是为此打开的。

地址操作与 Google Maps 兼容。当最终用户开始输入数据时,国家、省份、地区、地址等记录不需要存在于系统中。

单击 Google Maps 时,将打开以下屏幕:

如果单击“保存到地址”,则会将现有的国家、省份、地区和地址记录添加到数据库或更新(如果已存在)。在 Google Maps 调用时,将在地图菜单中指示单击“保存到地址”时会发生什么。要求将示例中的这些值输入到相应的控件(组合框、spinedit 等)中。

 

为文件和图像处理提供了 FilePath 和 ImagePath 的示例列。单击编辑控件旁边的 ... 按钮时,将弹出以下屏幕:

可以作为参数输入允许的文件扩展名、最大上传大小、要记录的位置以及最小宽度、最大宽度、最小高度和最大高度的值。

GridLookup 用于进行多选的流程。

存在 AccountPermission 表用于权限,其记录如下:Read TemplateMaster, Insert TemplateMaster, Update TemplateMaster, Delete TemplateMaster, Export TemplateMaster, Read TemplateDetail1, Insert TemplateDetail1 ...

 

数据库中存在 AccountRole 表用于角色操作。现有记录有:完全授权、总经理、编辑。存在 AccountRolePermission 表用于角色权限。

每个用户都有一个角色,并且存在 AccountUserPermission 表用于用户特定授权。可以向用户的角色授权添加或删除额外的角色。添加角色是正常的,但角色应该如何删除?此过程根据异或逻辑运行。

AccountRolePermission

AccountUserPermission

授权结果

无记录

无记录

不允许

无记录

记录可用

可用额外权限。

记录可用

无记录

角色提供的权限。

记录可用

记录可用

不允许。角色已覆盖

角色权限屏幕上完整列表的角色必须位于树的根部分。

用户权限屏幕上完整列表的用户必须位于树的根部分。打开详细信息时,应删除根。

可以管理权限。例如,如果用户没有“详细信息 1”的“插入”权限,则需要从网格菜单中删除“插入”按钮。例如,如果用户没有“详细信息 1”的监控权限,则需要从导航菜单中删除相关链接,并在访问此页面的位置提示警告消息“您无权访问详细模板 1”。通过派生自 AuthorizeAttribute 的 PermissionAuthorize,可以将此消息添加到此预防措施和相关的 ViewContext 中。

[PermissionAuthorize("TemplateDetail1", Permission.ReadTemplateDetail1)]
public ActionResult GridInit(TemplateDetail1GridModel model)

 

Permission.ReadTemplateDetail1 中显示的权限应为枚举。枚举源自数据库中 AccountPermission 表中的数据,并使用脚本生成,其值对应于 ID。

public enum Permission : int
{
    None = 0,
    ReadTemplateMaster = 1,
    InsertTemplateMaster = 2,
    UpdateTemplateMaster = 3
    ...

此项目使用 Microsoft Membership Provider。用户角色通过 CustomRoleProvider > GetRolesForUser 进行异或检索,如前所述。使用 IsUserInRole,它检查用户是否具有某个角色。Microsoft Membership 在此处使用了“Rol”作为名称,但在我们的系统中,它被用作权限。

数据库应如何管理?

让数据库中的所有表,即使包括 n:n 关系表,也必须包含 Id、InsertDate、InsertedBy、UpdateDate、UpdatedBy 和 IsDeleted 列。在我们需要更改记录排序的表中,特别是类型表中,应包含 RowNumber 字段。这不是强制性的,但为了速度,这些 RowNumber 字段可以具有聚集索引。

 

唯一字段应如下管理:例如,RoleName 在角色表中应是唯一的。假设记录“General manager”已从该表中删除,然后又添加了记录“General manager”。此时,应进行恢复过程。

同样,当尝试再次添加现有记录时,请发出警告。

命名必须基于一定的逻辑,以便我们能够通过从数据库中捕获唯一操作来实际处理它们,就像 IX_AccountRole_RoleName_Unique 一样。

MVC 端有针对标题、消息等的语言支持。但我们可能希望数据库中的数据具有语言支持。例如,如果我们希望 AccountRole 表中的 RoleName 和 RoleDescription 字段具有语言支持,我们必须创建 AccountRoleLanguage 表。就像 .NET 端资源文件的操作一样,如果 AccountRoleLanguage 表中有任何记录,我们可以说“Get”,或者加载 AccountRole 中的默认值。实际上,上面屏幕上的“角色语言”选项卡对应于 AccountRoleLanguage 表。

现在我正在向 AccountRole 添加一条新记录。当我单击“角色名称”选项卡时,我收到以下警告消息:Secretary,Description:null。角色语言。

当我单击“确定”按钮时,我记录数据并重新打开它进行编辑,现在可以输入“角色语言”所需的记录。

 

实现 N 层架构

Core 层包含涵盖整个项目和 DalSqlServer SQL Server 的数据访问对象,Service 层包含处理操作列表的对象。我们的研究是数据库优先设计的。但 T4 模板中添加了必要的代码来实现我们想要的模型。为了应用依赖注入,VillageSocietyModel.tt,Entity Framework 默认在 DalSqlServer > VillageSocietyModel.edmx 下创建,从中提取并移动到 Core 层。因此,EntityFramework 使实体能够在 Core 层中创建。

 

现在我们以 AccountPerson 为例,看看创建了什么样的基础设施。

在 DalSqlServer 层

public partial class VillageSocietyEntities : DbContext
{
    public virtual DbSet<Core.AccountPerson> AccountPerson { get; set; }
    ...

在 Core 层

public partial class AccountPerson : BaseEntity
{
public int VillageId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public bool IsMale { get; set; }
public string PicturePath { get; set; }
public int? Father { get; set; }
public int? Mother { get; set; }
	...


public class BaseEntity
{
    public int Id { get; set; }
    public DateTime InsertDate { get; set; }
    public int InsertedBy { get; set; }
    public DateTime? UpdateDate { get; set; }
    public int? UpdatedBy { get; set; }
    public DateTime? DeleteDate { get; set; }
    public int? DeletedBy { get; set; }
    public bool IsDeleted { get; set; }
}

public interface IAccountPersonService : IBaseService<AccountPerson>
{
    IQueryable<AccountPersonEx> GetEx(Expression<Func<AccountPerson, bool>> filter = null,
        Expression<Func<SystemVillage, bool>> filterVillage = null,
        Expression<Func<AccountPerson, bool>> filterFather = null,
        Expression<Func<AccountPerson, bool>> filterMother = null);
}

public partial class AccountPersonEx : AccountPerson
{
    public string VillageName { get; set; }

    public string FatherFirstName { get; set; }
    public string FatherLastName { get; set; }
        
    public string MotherFirstName { get; set; }
    public string MotherLastName { get; set; }
}

public interface IBaseService<TEntity> where TEntity : BaseEntity
{
    IQueryable<TEntity> Get(Expression<Func<TEntity, bool>> filter = null);
    TEntity First(Expression<Func<TEntity, bool>> filter);
    TEntity FirstOrDefault(Expression<Func<TEntity, bool>> filter);
    TEntity Single(Expression<Func<TEntity, bool>> filter);
    TEntity SingleOrDefault(Expression<Func<TEntity, bool>> filter);
    TEntity GetById(int id);
    void Insert(TEntity item);
    void Update(TEntity item);
    void Edit(TEntity item, params string[] ignoreButSomeProperties);
    void Delete(TEntity item);
    void Restore(TEntity item, bool ignoreProperties);
    void Save();

    UniqueKeyException ParseUniqueKeyException(Exception exception, TEntity item);
    JsonResultModel ExceptionToJsonResult(Exception exception, TEntity item, bool isNewRecord);
}

在 Service 层

public class AccountPersonService : BaseService<AccountPerson>, IAccountPersonService
{
    public AccountPersonService()
        : base(IoC.Resolve<IUnitOfWork>())
    {
    }
    public AccountPersonService(IUnitOfWork uow)
        : base(uow)
    {
    }

    public IQueryable<AccountPersonEx> GetEx(Expression<Func<AccountPerson, bool>> filter = null,
        Expression<Func<SystemVillage, bool>> filterVillage = null,
        Expression<Func<AccountPerson, bool>> filterFather = null,
        Expression<Func<AccountPerson, bool>> filterMother = null)
    {
        filter = filter ?? (p => true);
        filterVillage = filterVillage ?? (p => true);
        filterFather = filterFather ?? (p => true);
        filterMother = filterMother ?? (p => true);

        var items = repository.Get();
        var villages = uow.Repository<SystemVillage>().Get();
        var persons = uow.Repository<AccountPerson>().Get();

        var query = from item in items.Where(filter)
                    join village in villages.Where(filterVillage) on item.VillageId equals village.Id
                    from father in persons.Where(p => p.Id == item.Father).DefaultIfEmpty().Where(filterFather)
                    from mother in persons.Where(p => p.Id == item.Mother).DefaultIfEmpty().Where(filterMother)
                    select new AccountPersonEx
                    {
                        Id = item.Id,
                        VillageId = item.VillageId,
                        FirstName = item.FirstName,
                        LastName = item.LastName,
                        IsMale = item.IsMale,
                        PicturePath = item.PicturePath,
                        Father = item.Father,
                        Mother = item.Mother,
                        InsertDate = item.InsertDate,
                        InsertedBy = item.InsertedBy,
                        UpdateDate = item.UpdateDate,
                        UpdatedBy = item.UpdatedBy,
                        DeleteDate = item.DeleteDate,
                        DeletedBy = item.DeletedBy,
                        IsDeleted = item.IsDeleted,
                            
                        VillageName = village.VillageName,
                            
                        FatherFirstName = father.FirstName,
                        FatherLastName = father.LastName,
                            
                        MotherFirstName = mother.FirstName,
                        MotherLastName = mother.LastName
                    };
        return query;

    }
}



public class BaseService<TEntity> : IBaseService<TEntity>, IDisposable where TEntity : BaseEntity
{
    internal IUnitOfWork uow;
    internal readonly IRepository<TEntity> repository;

    public BaseService(IUnitOfWork uow)
    {
        this.uow = uow;
        this.repository = uow.Repository<TEntity>();
    }

    public virtual IQueryable<TEntity> Get(Expression<Func<TEntity, bool>> filter = null)
    {
        return repository.Get(filter);
    }

    public virtual TEntity First(Expression<Func<TEntity, bool>> filter)
    {
        return repository.First(filter);
    }

    public virtual TEntity FirstOrDefault(Expression<Func<TEntity, bool>> filter)
    {
        return repository.FirstOrDefault(filter);
    }
    public virtual TEntity Single(Expression<Func<TEntity, bool>> filter)
    {
        return repository.Single(filter);
    }
    public virtual TEntity SingleOrDefault(Expression<Func<TEntity, bool>> filter)
    {
        return repository.SingleOrDefault(filter);
    }
    public virtual TEntity GetById(int id)
    {
        return repository.GetById(id);
    }
    public virtual void Insert(TEntity item)
    {
        repository.Insert(item);
    }
    public virtual void Update(TEntity item)
    {
        repository.Update(item);
    }
    public virtual void Edit(TEntity item, params string[] ignoreButSomeProperties)
    {
        repository.Edit(item, ignoreButSomeProperties);
    }
    public virtual void Delete(TEntity item)
    {
        repository.Delete(item);
    }
    public virtual void Restore(TEntity item, bool ignoreProperties)
    {
        repository.Restore(item, ignoreProperties);
    }

    public void Save()
    {
        uow.Save();
    }

    private bool disposed = false;

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                uow.Dispose();
            }
        }
        this.disposed = true;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    public UniqueKeyException ParseUniqueKeyException(Exception exception, TEntity item)
    {
        return repository.ParseUniqueKeyException(exception, item);
    }

    public JsonResultModel ExceptionToJsonResult(Exception exception, TEntity item, bool isNewRecord)
    {
        return repository.ExceptionToJsonResult(exception, item, isNewRecord);
    }
}

 

事实上,所有数据库操作都通过存储库执行。有一个 UnitofWork 使用所有存储库,UnitOfWork 也作为参数传递到 Service 层。在这种用法中,同一个 dbcontext 用于一系列工作,并且可以在 Service 层中执行事务。例如,AddressService 类中有一个 InsertOrUpdate 方法。

public AddressSetStruct InsertOrUpdate(AddressCountry country, AddressCity city, AddressDistrict district, Address address)
{
    var countryRepository = uow.Repository<AddressCountry>();
    var countryDb = countryRepository.Get(p => p.CountryCode == country.CountryCode).SingleOrDefault();

    var cityRepository = uow.Repository<AddressCity>();
    var cityDb = cityRepository.Get(p => p.CityName == city.CityName).SingleOrDefault();

    var districtRepository = uow.Repository<AddressDistrict>();
    var districtDb = districtRepository.Get(p => p.DistrictName == district.DistrictName).SingleOrDefault();

    var addressRepository = uow.Repository<Address>();
    var addressDb = addressRepository.Get(p => p.FullAddress == address.FullAddress).SingleOrDefault();
...

在此方法中,来自 GoogleMaps 的国家、城市、地区和地址值作为参数传入,如果交易成功通过 context.save,则更新相关记录或创建新记录。

与 AccountPerson 一样,并非所有类都必须派生自 BaseEntity。AccountRole 表中存在 RowNumber 字段,我们想对记录进行排序。在这种情况下,AccountRole 的相关代码应构建如下:

由于它具有 T4 模板和 AccountRole RowNumber 字段,因此它应该派生自 RowNumberedEntity。

public partial class AccountRole : RowNumberedEntity
{
    public string RoleName { get; set; }
    public string RoleDescription { get; set; }
...


public class RowNumberedEntity : BaseEntity
{
    public double RowNumber { get; set; }
}

public interface IAccountRoleService : IRowNumberedService<AccountRole>
{
    IQueryable<AccountRoleEx> GetWithLanguage(Expression<Func<AccountRole, bool>> filter, int languageId);
}

public class AccountRoleService : RowNumberedService<AccountRole>, IAccountRoleService
{
    public AccountRoleService()
        : base(IoC.Resolve<IUnitOfWork>())
    {
    }
    public AccountRoleService(IUnitOfWork uow)
        : base(uow)
    {
    }
...


public class RowNumberedService<TEntity> : BaseService<TEntity>, IRowNumberedService<TEntity>, IDisposable where TEntity : RowNumberedEntity
{
    public RowNumberedService(IUnitOfWork uow)
        : base(uow)
    {
    }

    public bool Up(int id1, int? id2)
    {
	...
    }

    public bool Down(int id1, int? id2)
    {
       ...
    }
}

当选择一条记录时,“向上”按钮将其移到上面。选择两条记录后单击“向上”按钮,下面的记录将移到上面的记录之上。对于“向下”按钮也是如此。

未来...

  1. 当前工作系统特别适用于网格和树状列表,其布局(列顺序、列、可见列、过滤器等)写入数据库中的 SystemLayout 表并从中读取。因此,如果最终用户以某种方式离开屏幕,他/她就能保证以同样的方式恢复。然而,最终用户应该能够创建许多情况并能够选择他/她想要查看的情况。
  2. HTML 编辑器可用作报告工具。例如,为 Person 网格设计了一个模板,如下所示。在选择所需记录后发出“创建报告”命令时,将按顺序生成相关记录的模板。因此,它允许最终用户通过记录获得设计好的输出。创建的模板和 PDF 输出可以保存到数据库。

  3. 最终用户可以将 SQL 结果(有人看到可以工作)保存到系统中,并可以在网格中动态查看这些记录。有人可以决定导航菜单上的链接将位于何处,并通过此授权连接到相关角色和用户。因此,第二项中提到的报告工具将通过此项得到更有效的改进。

 

希望传达您的意见和建议

http://bestprogramming@hotmail.com

 

© . All rights reserved.