2023年12月

上次给大家介绍了
支付宝 v3 自签名如何实现
,这次顺便再把验签也写一下。

为什么要验签

说起为什么要验签,如果要详细一点解释的话,可以写很多很多......

我们就简单一点来解释:
验签可以证明接收到的信息是支付宝给我的,不是被人中途拦截篡改数据之后再发给我的。

支付宝的通知分为 「同步通知 」和 「异步通知 」:

  • 「同步通知 」就是我们请求支付宝之后,支付宝返回的数据。
  • 「异步通知 」是到达某些条件之后,支付宝主动发的;更详细内容可以参考之前我写的 [
    手把手|支付宝异步通知如何使用
    ]。

对于这两种通知我们
都需要进行验签处理
,才能保证数据的准确性!(
⚠️
很重要!!)

其实支付宝给的 SDK 里面也封装了验签的方法,并且对同步通知都经过了验签的处理,同步验签不过的话,接口会直接抛出异常 [
sign check fail: check Sign and Data Fail
]。

另外 v3 SDK 里面提供的验签方法名跟 v2 版本是一样的,大家不想麻烦的话可以直接查看 [
SDK如何实现验签
]。



(大家凑合看,v3 版本好像还没有完整的验签示例代码,也可能是我没找到

iamge

Entity FrameWork(简称 EF)以面向对象的方式操作数据库给开发人员带来了很大的便利性,但其性能问题从面世以来就一直就被广大的 .NET 生态开发技术人员所吐槽,然而,它真的那么不堪使用吗?试试下面这 6 个小技巧,瞬间极大提升 EF Core 性能:

  1. AsNoTracking
    在项目开发的时候,如果查询出来的对象不会被修改、删除等,那么在查询的时候,可以启用AsNoTracking,这样就能降低EF Core的资源占用

    Book[] books = ctx.Books.AsNoTracking().Take(3).ToArray();
    Book b1 = books[0];
    b1.Title = "abc";
    EntityEntry entry1 = ctx.Entry(b1);
    Console.WriteLine(entry1.State);
    
  2. 通过一条SQL语句高效更新实体类

    Book b1 = new Book {Id=10};
    b1.Title = "yzk";
    var entry1 = ctx.Entry(b1);		//直接创建实体对象
    entry1.Property("Title").IsModified = true;		//标记 Title 字段被修改
    Console.WriteLine(entry1.DebugView.LongView);
    ctx.SaveChanges();	//只生成这个字段的更新的SQL语句
    
  3. 用一条SQL语句高效删除数据

    Book b1 = new Book { Id = 28 };
    ctx.Entry(b1).State = EntityState.Deleted;
    ctx.SaveChanges();
    
  4. 关于 Find 和 FindAsync 方法
    Find 或者 FindAsync 方法(以下简称为Find)会先在上下文查找这个对象是否已经被跟踪,如果对象已经被跟踪,就直接返回被跟踪的对象,只有在本地没有找到这个对象时,EF Core 才去数据库查询,而 Single 方法则一直都是执行一次数据库查询。因此用 Find 方法有可能减少一次数据库查询,性能更好。但是如果在对象被跟踪之后,数据库中对应的数据已经被其他程序修改了,则 Find 方法可能会返回旧数据。

  5. 如何在 EF Core 中高效地删除、更新数据?


    • 使用原生 SQL,但不太符合模型驱动、分层隔离等思想
    • 使用其它开源库如 Zack.EFCore.Batch
  6. 全局查询筛选器
    EF Core 支持在配置实体类的时候,为实体类设置全局查询筛选器,EF Core 会自动将全局查询筛选器应用于涉及这个实体类型的所有 LINQ 查询。这个功能常见的应用场景有 "软删除" 和 "多租户"

    Fluent API配置全局查询筛选器:
    builder.HasQueryFilter(b=>b.IsDeleted==false)
    
    忽略全局查询筛选器
    ct x.Books.IgnoreQueryFilters().Where(b => b.Title.Contains("o")).ToArray();

目录✨

1、前言

2、效果

3、具体实现

​ 页面设计

​ 全部代码

​ FileSystemWatcher的介绍

​ FileSystemWatcher的构造函数

​ FileSystemWatcher的属性

​ FileSystemWatcher的事件

4、总结

前言✨

有时候我们会有监控电脑上某一个文件夹中文件变动情况的需求,在本文中,我也会以一个具体的例子,说明在C#中如何使用FileSystemWatcher类来实现上述需求。

效果✨

文件监控效果

image-20231227100851391

具体实现✨

如果你对C#如何监控选定文件夹中文件的变动情况感兴趣,可以继续往下阅读。

界面设计

为了更好的演示效果,我这里winform的界面设计如下:

image-20231227101050127

很简单,只有一个button与一个richtextbox,button用来指定被监控的文件,richtextbox用来输出一些信息。

全部代码

namespace FileSystemWatcherDemo
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {

            // 创建一个 FolderBrowserDialog 实例
            FolderBrowserDialog folderBrowserDialog = new FolderBrowserDialog();

            // 设置对话框的标题
            folderBrowserDialog.Description = "选择文件夹";

            // 如果用户点击了“确定”按钮
            if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
            {
                richTextBox1.Text = "";
                // 获取用户选择的文件夹路径
                string selectedFolder = folderBrowserDialog.SelectedPath;

                // 提示被监控文件夹路径
                richTextBox1.Text += $"被监控的文件夹为:{selectedFolder}\r\n";

                var watcher = new FileSystemWatcher($"{selectedFolder}");
               
                watcher.NotifyFilter = NotifyFilters.Attributes
                                | NotifyFilters.CreationTime
                                | NotifyFilters.DirectoryName
                                | NotifyFilters.FileName
                                | NotifyFilters.LastAccess
                                | NotifyFilters.LastWrite
                                | NotifyFilters.Security
                                | NotifyFilters.Size;

                watcher.Changed += OnChanged;
                watcher.Created += OnCreated;
                watcher.Deleted += OnDeleted;
                watcher.Renamed += OnRenamed;
                
                watcher.Filter = "*.txt";
                watcher.IncludeSubdirectories = true;
                watcher.EnableRaisingEvents = true;
            }
            else
            {
                MessageBox.Show("您本次没有选择文件夹!!!");
            }
          

           
   
        }

        private void AppendMessageToRichTextBox(string message)
        {
            // 在RichTextBox中添加提示信息        
            richTextBox1.Invoke(new Action(() =>
            {
                richTextBox1.AppendText(message + Environment.NewLine);
            }));
        }

        private void OnChanged(object sender, FileSystemEventArgs e)
        {
            if (e.ChangeType != WatcherChangeTypes.Changed)
            {
                return;
            }
            AppendMessageToRichTextBox($"Changed: {e.FullPath}");
        }

        private void OnCreated(object sender, FileSystemEventArgs e)
        {
            string value = $"Created: {e.FullPath}";
            AppendMessageToRichTextBox($"Created: {e.FullPath}");
        }

        private  void OnDeleted(object sender, FileSystemEventArgs e)
        {
            AppendMessageToRichTextBox($"Deleted: {e.FullPath}");
        }

        private  void OnRenamed(object sender, RenamedEventArgs e)
        {       
            AppendMessageToRichTextBox($"Renamed:");
            AppendMessageToRichTextBox($"    Old: {e.OldFullPath}");
            AppendMessageToRichTextBox($"    New: {e.FullPath} ");           
        }
     
    }
}

FileSystemWatcher的介绍

看过以上代码,会发现核心就是FileSystemWatcher的使用。接下来我将介绍一下C#中的FileSystemWatcher类。

FileSystemWatcher是C#中的一个类,该类可以侦听文件系统更改通知,并在目录或目录中的文件发生更改时引发事件。

FileSystemWatcher的构造函数

该类有三种构造函数,如下所示:

形式 含义
FileSystemWatcher() 初始化 FileSystemWatcher 类的新实例。
FileSystemWatcher(String) 初始化 FileSystemWatcher 类的新实例,给定要监视的目录。
FileSystemWatcher(String, String) 初始化 FileSystemWatcher类的新实例,给定要监视的目录和文件类型。
 var watcher = new FileSystemWatcher($"{selectedFolder}");

本文中我选择的就是第二种构造函数,指定要监视的目录。

FileSystemWatcher的属性

现在介绍一下在本示例中用到的FileSystemWatcher的属性,如下所示:

名称 类型 含义
EnableRaisingEvents bool 设置FileSystemWatcher是否有效
Filter string 设置一个要监控的文件的格式
Filters Collection
设置多个要监控的文件的格式
IncludeSubdirectories bool 获取或设置一个值,该值指示是否应监视指定路径中的子目录
NotifyFilter NotifyFilters 获取或设置要监视的更改的类型
Path string 获取或设置要监视的目录的路径

现在来解释下所用到的代码的含义:

watcher.Filter = "*.txt";

表示要监控的文件为.txt格式。

 watcher.IncludeSubdirectories = true;

表示指定路径中的子目录也要监视。

 watcher.EnableRaisingEvents = true;

表示该对象可以触发事件,也就是还有效。

 watcher.NotifyFilter = NotifyFilters.Attributes
                                | NotifyFilters.CreationTime
                                | NotifyFilters.DirectoryName
                                | NotifyFilters.FileName
                                | NotifyFilters.LastAccess
                                | NotifyFilters.LastWrite
                                | NotifyFilters.Security
                                | NotifyFilters.Size;

设置要监视的更改的类型。NotifyFilter属性的类型为NotifyFilters枚举类型。

NotifyFilters枚举类型:

[System.Flags]
public enum NotifyFilters

指定要在文件或文件夹中监视的更改。

此枚举支持其成员值的按位组合。

该枚举类型包含的值与含义如下所示:

名称 含义
Attributes 文件或文件夹的属性
CreationTime 文件或文件夹的创建时间
DirectoryName 目录名
FileName 文件的名称
LastAccess 文件或文件夹上一次打开的日期
LastWrite 上一次向文件或文件夹写入内容的日期
Security 文件或文件夹的安全设置
Size 文件或文件夹的大小

在这里使用了该枚举类型的按位组合表示这几种更改的类型要受到监视。

FileSystemWatcher的事件

FileSystemWatcher中的事件如下:

名称 含义
Changed 当更改指定 Path 中的文件和目录时发生
Created 当在指定Path 中创建文件和目录时发生
Deleted 删除指定Path中的文件或目录时发生
Renamed 重命名指定 Path中的文件或目录时发生
Error 当 FileSystemWatcher 的实例无法继续监视更改或内部缓冲区溢出时发生
                watcher.Changed += OnChanged;
                watcher.Created += OnCreated;
                watcher.Deleted += OnDeleted;
                watcher.Renamed += OnRenamed;

在这里我使用到了Changed、Created、Deleted和Renamed事件。

我将以Changed 事件为例,详细解释一下:

 watcher.Changed += OnChanged;

这行代码的含义。

我们查看FileSystemWatcher的源代码,Changed事件的代码如下所示:

/// <devdoc>
///    Occurs when a file or directory in the specified <see cref='System.IO.FileSystemWatcher.Path'/> is changed.
/// </devdoc>
public event FileSystemEventHandler? Changed
{
    add
    {
        _onChangedHandler += value;
    }
    remove
    {
        _onChangedHandler -= value;
    }
}

可知将值赋给了
_onChangedHandler
,我们再来查看
_onChangedHandler
的定义:

 // Event handlers
 private FileSystemEventHandler? _onChangedHandler;

类型为FileSystemEventHandler?与Changed事件一致,再来看看FileSystemEventHandler?的定义:

 public delegate void FileSystemEventHandler(object sender, FileSystemEventArgs e);

发现是一个参数类型分别为object、FileSystemEventArgs返回值类型为空的委托类型。

object我们知道,那么FileSystemEventArgs又是什么呢?

查看它的源码,截取一部分,如下所示:

public class FileSystemEventArgs : EventArgs
{
     private readonly WatcherChangeTypes _changeType;
     private readonly string? _name;
     private readonly string _fullPath;
     /// <devdoc>
///    Gets one of the <see cref='System.IO.WatcherChangeTypes'/> values.
/// </devdoc>
public WatcherChangeTypes ChangeType
{
    get
    {
        return _changeType;
    }
}

/// <devdoc>
///    Gets the fully qualified path of the affected file or directory.
/// </devdoc>
public string FullPath
{
    get
    {
        return _fullPath;
    }
}


/// <devdoc>
///       Gets the name of the affected file or directory.
/// </devdoc>
public string? Name
{
    get
    {
        return _name;
    }
}
 }

发现FileSystemEventArgs继承自EventArgs,而EventArgs表示包含事件数据的类的基类,因此可以明白FileSystemEventArgs表示为目录事件:Changed, Created, Deleted提供数据的类。

FileSystemEventArgs提供三个数据分别为ChangeType、FullPath、Name。

那ChangeType是什么呢?

查看ChangeType的定义:

 //
 // 摘要:
 //     Changes that might occur to a file or directory.
 [Flags]
 public enum WatcherChangeTypes
 {
     //
     // 摘要:
     //     The creation of a file or folder.
     Created = 1,
     //
     // 摘要:
     //     The deletion of a file or folder.
     Deleted = 2,
     //
     // 摘要:
     //     The change of a file or folder. The types of changes include: changes to size,
     //     attributes, security settings, last write, and last access time.
     Changed = 4,
     //
     // 摘要:
     //     The renaming of a file or folder.
     Renamed = 8,
     //
     // 摘要:
     //     The creation, deletion, change, or renaming of a file or folder.
     All = 15
 }

是一个枚举类型,表示更改的类型。

现在回过头来看:

watcher.Changed += OnChanged;

OnChanged方法如下:

  private void OnChanged(object sender, FileSystemEventArgs e)
  {
      if (e.ChangeType != WatcherChangeTypes.Changed)
      {
          return;
      }
      AppendMessageToRichTextBox($"Changed: {e.FullPath}");
  }

为什么可以将OnChanged方法订阅到watcher.Changed事件上呢?

因为OnChanged方法与watcher.Changed事件中的委托类型FileSystemEventHandler的返回类型和签名是相同的。

OnChanged方法的返回类型与签名如下:

 private void OnChanged(object sender, FileSystemEventArgs e)

FileSystemEventHandler委托类型的定义如下:

 public delegate void FileSystemEventHandler(object sender, FileSystemEventArgs e);

现在已经理解了订阅事件,那么什么时候触发事件呢?

查看FileSystemWatcher的部分源码:

 /// <devdoc>
 ///    Raises the <see cref='System.IO.FileSystemWatcher.Changed'/> event.
 /// </devdoc>
 protected void OnChanged(FileSystemEventArgs e)
 {
     InvokeOn(e, _onChangedHandler);
 }
 private void InvokeOn(FileSystemEventArgs e, FileSystemEventHandler? handler)
 {
     if (handler != null)
     {
         ISynchronizeInvoke? syncObj = SynchronizingObject;
         if (syncObj != null && syncObj.InvokeRequired)
             syncObj.BeginInvoke(handler, new object[] { this, e });
         else
             handler(this, e);
     }
 }

当发生相应的改变时,就会调用FileSystemWatcher类的OnChanged方法,从而触发事件。

总结✨

本文通过一个实例,介绍了如何通过C#中的FileSystemWatcher类实现监控选定的文件夹,希望对你有所帮助。

Qt 是一个跨平台C++图形界面开发库,利用Qt可以快速开发跨平台窗体应用程序,在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置,实现图形化开发极大的方便了开发效率,本章将重点介绍
SqlTableModule
组件的常用方法及灵活运用。

在多数情况下我们需要使用SQL的方法来维护数据库,但此方式相对较为繁琐对于表格等数据的编辑非常不友好,在
Qt
中提供了
QSqlTableModel
模型类,它为开发者提供了一种直观的方式来与数据库表格进行交互。通过使用该组件可以将数据库与特定的组件进行关联,一旦关联被建立那么用户的所有操作均可以使用函数的方式而无需使用
SQL
语句,该特性有点类似于
ORM
对象关系映射机制。

在接下来的章节中,我们将学习如何配置
QSqlTableModel
、与数据库进行交互、实现数据的动态显示和编辑,首先读者应绘制好UI界面,本次案例界面稍显复杂,读者可自行完成如下案例的绘制;

以下是
QSqlTableModel
类的一些常用方法,包括方法名、参数以及简要说明。这里列举的方法并非全部,而是一些常见的方法,更详细的信息可以参考官方文档。

方法 描述
QSqlTableModel(QObject *parent = nullptr, QSqlDatabase db = QSqlDatabase()) 构造函数,创建
QSqlTableModel
对象。
setTable(const QString &tableName) 设置要操作的数据库表名。
select() 执行查询操作,从数据库中获取数据。
rowCount(const QModelIndex &parent = QModelIndex()) const 返回模型中的行数。
columnCount(const QModelIndex &parent = QModelIndex()) const 返回模型中的列数。
record(int row) 返回指定行的记录。
setFilter(const QString &filter) 设置用于过滤数据的条件。
setSort(int column, Qt::SortOrder order) 设置排序的列和排序规则。
setEditStrategy(QSqlTableModel::EditStrategy strategy) 设置编辑策略,决定何时将修改提交到数据库。
setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) 设置模型中指定索引的数据。
data(const QModelIndex &index, int role = Qt::DisplayRole) const 返回模型中指定索引的数据。
addRecord(const QSqlRecord &values) 添加一条记录到模型中。
removeRow(int row) 从模型中删除指定行。
insertRecord(int row, const QSqlRecord &record) 在指定位置插入一条记录。
submitAll() 提交所有对模型的修改到数据库。
revertAll() 撤销对模型的所有修改。
setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role = Qt::EditRole) 设置表头数据。
indexInQuery(const QSqlQuery &query) const 返回查询中模型的索引。

这些方法提供了对
QSqlTableModel
进行数据操作、过滤、排序以及提交修改的基本手段。通过这些方法,可以在应用程序中方便地操作数据库表格的数据。

1.1 初始化组件

首先我们来看一下
MainWindow
初始化部分是如何工作的,主要实现了以下功能:

打开数据库

首先使用
SQLite
数据库驱动连接名为
"database.db"
的数据库文件。如果数据库连接失败,函数直接返回。接着通过新建一个
QSqlTableModel
类,并调用
setTable
来打开一个数据表,设置编辑策略为
OnManualSubmit
,即手动提交修改。并通过
setSort
函数来设置排序方式为根据
ID
字段升序
Qt::AscendingOrder
排列。

DB = QSqlDatabase::addDatabase("QSQLITE");
DB.setDatabaseName("./database.db");
if (!DB.open())
{
    return;
}

tabModel = new QSqlTableModel(this, DB);
tabModel->setTable("Student");
tabModel->setEditStrategy(QSqlTableModel::OnManualSubmit);
tabModel->setSort(tabModel->fieldIndex("id"), Qt::AscendingOrder);
if (!(tabModel->select()))
{
    return;
}

设置字段名称

此处我们数据库中有6个字段,也就需要设置数据库字段与表格关联,如下则是对字段的动态关联。

tabModel->setHeaderData(tabModel->fieldIndex("id"),Qt::Horizontal,"Uid");
tabModel->setHeaderData(tabModel->fieldIndex("name"),Qt::Horizontal,"Uname");
tabModel->setHeaderData(tabModel->fieldIndex("sex"),Qt::Horizontal,"Usex");
tabModel->setHeaderData(tabModel->fieldIndex("age"),Qt::Horizontal,"Uage");
tabModel->setHeaderData(tabModel->fieldIndex("mobile"),Qt::Horizontal,"Umobile");
tabModel->setHeaderData(tabModel->fieldIndex("city"),Qt::Horizontal,"Ucity");

关联选择模型和数据模型

通过创建
QItemSelectionModel
对象
theSelection
并关联到
tabModel
模型,将数据模型和选择模型关联到
ui->tableView
,并设置选择模式为行选择模式。

theSelection = new QItemSelectionModel(tabModel);
ui->tableView->setModel(tabModel);
ui->tableView->setSelectionModel(theSelection);
ui->tableView->setSelectionBehavior(QAbstractItemView::SelectRows);

创建数据映射

创建
QDataWidgetMapper
对象
dataMapper
,将数据模型设置为
tabModel
,设置提交策略为
AutoSubmit
,即自动提交修改。并将 "name" 字段映射到
ui->lineEdit_name
,默认选中第一条映射记录。

dataMapper = new QDataWidgetMapper();
dataMapper->setModel(tabModel);
dataMapper->setSubmitPolicy(QDataWidgetMapper::AutoSubmit);
dataMapper->addMapping(ui->lineEdit_name, tabModel->fieldIndex("name"));
dataMapper->toFirst();

信号和槽连接

当选择模型中的当前行改变时,连接到槽函数
on_currentRowChanged
,用于在右侧编辑框中输出当前选择的记录。

connect(theSelection, SIGNAL(currentRowChanged(QModelIndex, QModelIndex)), this, SLOT(on_currentRowChanged(QModelIndex, QModelIndex)));

这个槽函数的实现如下所示,当行被点击后执行获取
name/mobile
字段,并放入映射数据集中的
lineEdit
编辑框中,使其能够动态的显示数据列表。

void MainWindow::on_currentRowChanged(const QModelIndex &current, const QModelIndex &previous)
{
    Q_UNUSED(previous);

    dataMapper->setCurrentIndex(current.row());      // 更细数据映射的行号
    int curRecNo=current.row();                      // 获取行号
    QSqlRecord  curRec=tabModel->record(curRecNo);   // 获取当前记录

    QString uname = curRec.value("name").toString();     // 取出数据
    QString mobile = curRec.value("mobile").toString();

    ui->lineEdit_name->setText(uname);                   // 设置到编辑框
    ui->lineEdit_mobile->setText(mobile);
}

最后在UI文件的底部有一个
comboBox
组件,我们通过动态的查询记录,并将其赋值为第一个字段元素,其代码如下所示;

QSqlRecord emptyRec=tabModel->record();           //获取空记录,只有字段名
for (int i=0;i<emptyRec.count();i++)
{
    ui->comboBox->addItem(emptyRec.fieldName(i));
}

这段代码实现了一个简单的数据库浏览和编辑界面,用户可以通过表格展示的方式查看和编辑 "Student" 表格中的数据。当程序运行后则可以看到如下图所示的初始化部分;

1.2 数据处理

1.2.1 新增一条记录

当用户按下
on_pushButton_add_clicked
按钮时,则会在表格中新增一条记录,并设置默认值的功能。下面是代码的详细解释:

插入新行

在表格模型
tabModel
的末尾插入一行新记录。
QModelIndex()
是一个空的索引,表示插入到末尾。

tabModel->insertRow(tabModel->rowCount(), QModelIndex());

获取最后一行的索引

获取刚刚插入的行的索引,这里假设 "name" 字段对应的列索引是 1。

QModelIndex curIndex = tabModel->index(tabModel->rowCount() - 1, 1);

清空选择项并设置新行为当前选择行

清空当前选择项,然后将刚刚插入的行设为当前选择行,并选择该行。

theSelection->clearSelection();
theSelection->setCurrentIndex(curIndex, QItemSelectionModel::Select);

获取当前行号

获取当前行的行号。

int currow = curIndex.row();

设置自动生成的编号和默认值

这段代码的作用是在表格模型中插入一行新记录,然后设置该行的默认值,其中 "Uid" 字段会自动生成一个编号,"Usex" 字段默认为 "M","Uage" 字段默认为 "0"。

  • 自动生成编号,假设 "Uid" 字段对应的列索引是 0。
  • 将 "Usex" 字段设置为 "M"。
  • 将 "Uage" 字段设置为 "0"。
tabModel->setData(tabModel->index(currow, 0), 1000 + tabModel->rowCount());
tabModel->setData(tabModel->index(currow, 2), "M");
tabModel->setData(tabModel->index(currow, 3), "0");

运行代码,读者可自行点击增加记录按钮,每次点击均会在表格中提供新行,当读者点击
on_pushButton_save_clicked
保存按钮是则会调用
submitAll()
该函数用于将数据提交到数据库中存储,如下图所示;

1.2.4 插入一条记录


TableView
中当前选择行的上方插入一行新记录,并自动生成编号。下面是代码的详细解释:

获取当前选择行的索引和行号

获取当前选择的单元格的索引和行号。

QModelIndex curIndex = ui->tableView->currentIndex();
int currow = curIndex.row();

在当前行上方插入一行新记录

在表格模型
tabModel
的当前选择行(
curIndex.row()
)的上方插入一行新记录。
QModelIndex()
是一个空的索引,表示插入到指定行的上方。

tabModel->insertRow(curIndex.row(), QModelIndex());

设置自动生成的编号

自动生成编号,假设 "Uid" 字段对应的列索引是 0。

tabModel->setData(tabModel->index(currow, 0), 1000 + tabModel->rowCount());

清除已有选择并将当前选择行设为新插入的行

清空已有选择项,然后将当前选择行设为新插入的行,并选择该行。

theSelection->clearSelection();
theSelection->setCurrentIndex(curIndex, QItemSelectionModel::Select);

当上述代码运行后则可以实现在指定行的上方插入一行新纪录,并为新插入的行生成一个自增的编号,其效果如下图所示;

对于删除一条记录来说则可以通过调用
tabModel->removeRow(curIndex.row())
来实现删除所选行,因为其实现起来很简单此处就不再演示,具体实现细节可以参考附件。

1.2.5 修改表中记录

如下所示代码,用于批量修改表格中所有记录的 "Uage" 字段值为某个固定的年龄。下面是代码的详细解释:

检查是否有记录

如果表格中没有记录,则直接返回,不执行后续的批量修改操作。

if (tabModel->rowCount() == 0)
    return;

循环遍历每一行记录并修改年龄

首先使用
tabModel->record(i)
获取表格模型中的第
i
行记录,接着使用
ui->lineEdit->text()
获取用户在
QLineEdit
中输入的文本,作为新的年龄值,并通过
aRec.setValue("age", ...)
设置 "age" 字段的新值,最后使用
tabModel->setRecord(i, aRec)
将修改后的记录设置回表格模型中的相应行。

for (int i = 0; i < tabModel->rowCount(); i++)
{
    QSqlRecord aRec = tabModel->record(i);                // 获取当前记录
    aRec.setValue("age", ui->lineEdit->text());           // 设置数据,使用 QLineEdit 中的文本作为新的年龄值
    tabModel->setRecord(i, aRec);                         // 将修改后的记录设置回表格模型中的相应行
}

提交修改

使用
tabModel->submitAll()
提交对表格模型的所有修改,将修改保存到数据库中。

tabModel->submitAll();

上述代码实现了一个简单的批量修改操作,将表格中所有记录的 "Uage" 字段值设置为用户在
QLineEdit
中输入的年龄值。请注意,这里没有对输入的年龄值进行验证,确保输入的是合法的数字。在实际应用中,可能需要添加一些输入验证和错误处理的逻辑。

1.2.6 表记录的排序

升序与降序排列

对表中记录的排序可以使用模型提供的
setSort
函数来实现,通过对该字段第二个参数设置为
Qt::AscendingOrder
则是升序排序,反之如果设置为
Qt::DescendingOrder
则为降序排序。

如下所示代码用于根据用户选择的字段对表格进行排序,并重新执行查询以更新表格数据。下面是代码的详细解释:

  • ui->comboBox->currentIndex()
    获取用户在
    QComboBox
    中选择的字段的索引。
  • Qt::AscendingOrder
    表示升序排序。
  • tabModel->select()
    执行对数据库的查询操作,重新获取数据并应用排序。
// 升序排序
tabModel->setSort(ui->comboBox->currentIndex(), Qt::AscendingOrder);
// 降序排序
tabModel->setSort(ui->comboBox->currentIndex(),Qt::DescendingOrder);
// 刷新查询
tabModel->select();

上述代码的作用是根据用户在下拉框中选择的字段进行升序或降序排序,并将排序后的结果重新加载到表格中。在使用这段代码之前,用户需要在
QComboBox
中选择一个字段,作为排序的依据。以升序排序为例,输出效果如下图所示;

1、业务背景

由于业务流程的复杂性,用户往往只具备部分功能的权限,导致在操作自开发程序时出现权限问题。例如前台限制了用户对销售订单的修改,而自开发功能中又涉及单据修改,此时一味限制权限,则无法正常使用功能。

2、使用RFC跳过权限

2.1、实现原理

将被权限限制的逻辑,封装成RFC函数,在调用时,使用拥有大权限的账号,远程调用目标系统(实际还是本系统)的RFC,则实际上在代码运行时,是使用了大权限的账号,执行了该函数。最终在凭证中的更改日志,也是记录了大权限账号的修改记录。

2.2、封装RFC

例如将销售订单修改逻辑封装到RFC中

复制BAPI_SALESORDER_CHANGE到ZBAPI_SALESORDER_CHANGE,确保远程模式

原参数保持不变,多增加一个返回结构E_RESULT,将调用BAPI的最终结果处理后返回,这样外部调用时,可以避免重复编写处理BAPI消息的代码

将commit和rollback写在函数中,待凭证处理完之后,执行事务,防止事务一直未提交,出现锁单情况

"--------------------@斌将军--------------------
FUNCTIONzbapi_salesorder_change.*"----------------------------------------------------------------------*"*"本地接口:*"  IMPORTING*"     VALUE(SALESDOCUMENT) LIKE  BAPIVBELN-VBELN*"     VALUE(ORDER_HEADER_IN) LIKE  BAPISDH1 STRUCTURE  BAPISDH1*"       OPTIONAL*"     VALUE(ORDER_HEADER_INX) LIKE  BAPISDH1X STRUCTURE  BAPISDH1X*"     VALUE(SIMULATION) LIKE  BAPIFLAG-BAPIFLAG OPTIONAL*"     VALUE(BEHAVE_WHEN_ERROR) LIKE  BAPIFLAG-BAPIFLAG DEFAULT SPACE*"     VALUE(INT_NUMBER_ASSIGNMENT) LIKE  BAPIFLAG-BAPIFLAG DEFAULT*"       SPACE*"     VALUE(LOGIC_SWITCH) LIKE  BAPISDLS STRUCTURE  BAPISDLS OPTIONAL*"     VALUE(NO_STATUS_BUF_INIT) LIKE  BAPIFLAG-BAPIFLAG DEFAULT SPACE*"  EXPORTING*"     VALUE(E_RESULT) TYPE  BAPIRETURN*"  TABLES*"      RETURN STRUCTURE  BAPIRET2*"      ORDER_ITEM_IN STRUCTURE  BAPISDITM OPTIONAL*"      ORDER_ITEM_INX STRUCTURE  BAPISDITMX OPTIONAL*"      PARTNERS STRUCTURE  BAPIPARNR OPTIONAL*"      PARTNERCHANGES STRUCTURE  BAPIPARNRC OPTIONAL*"      PARTNERADDRESSES STRUCTURE  BAPIADDR1 OPTIONAL*"      ORDER_CFGS_REF STRUCTURE  BAPICUCFG OPTIONAL*"      ORDER_CFGS_INST STRUCTURE  BAPICUINS OPTIONAL*"      ORDER_CFGS_PART_OF STRUCTURE  BAPICUPRT OPTIONAL*"      ORDER_CFGS_VALUE STRUCTURE  BAPICUVAL OPTIONAL*"      ORDER_CFGS_BLOB STRUCTURE  BAPICUBLB OPTIONAL*"      ORDER_CFGS_VK STRUCTURE  BAPICUVK OPTIONAL*"      ORDER_CFGS_REFINST STRUCTURE  BAPICUREF OPTIONAL*"      SCHEDULE_LINES STRUCTURE  BAPISCHDL OPTIONAL*"      SCHEDULE_LINESX STRUCTURE  BAPISCHDLX OPTIONAL*"      ORDER_TEXT STRUCTURE  BAPISDTEXT OPTIONAL*"      ORDER_KEYS STRUCTURE  BAPISDKEY OPTIONAL*"      CONDITIONS_IN STRUCTURE  BAPICOND OPTIONAL*"      CONDITIONS_INX STRUCTURE  BAPICONDX OPTIONAL*"      EXTENSIONIN STRUCTURE  BAPIPAREX OPTIONAL*"      EXTENSIONEX STRUCTURE  BAPIPAREX OPTIONAL*"----------------------------------------------------------------------

  DATA:lv_check   TYPEchar1,
lv_message
TYPEchar255."修改销售订单 CALL FUNCTION 'BAPI_SALESORDER_CHANGE' EXPORTINGsalesdocument=salesdocument*order_header_in = order_header_in order_header_inx =order_header_inx
logic_switch
=logic_switchTABLESreturn=return
order_item_in
=order_item_in
order_item_inx
=order_item_inx
schedule_lines
=schedule_lines
schedule_linesx
=schedule_linesx
extensionin
=extensionin.CLEAR:lv_check,lv_message,e_result.LOOP AT return INTO DATA(ls_return) WHERE type CA 'AEX'.
lv_message
= lv_message && ls_return-message.
lv_check
= 'E'.CLEAR:ls_return.ENDLOOP.IF lv_check = 'E'.CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.

e_result
-type = 'E'.
e_result
-message =lv_message.ELSE.CALL FUNCTION 'BAPI_TRANSACTION_COMMIT' EXPORTINGwait=abap_true.

e_result
-type = 'S'.
e_result
-message = '修改成功'.ENDIF.ENDFUNCTION."--------------------@斌将军--------------------

2.3、配置目标系统

创建ABAP连接

目标系统

拥有大权限的账密

Unicode

连接测试成功。点击远程登录,则能直接用配置的账号登录SAPGUI

2.4、调用RFC

"--------------------@斌将军--------------------
DATA:gv_system          TYPE rfcdes-rfcdest."目标系统
gv_system = sy-sysid && '_' && sy-mandt."S4D_300

"修改销售订单
PERFORMfrm_salesorder_change.IF ls_result-type = 'E'."失败
ELSE."成功
ENDIF.FORMfrm_salesorder_change."修改销售订单
  CALL FUNCTION 'ZBAPI_SALESORDER_CHANGE' DESTINATIONgv_systemEXPORTINGsalesdocument=lv_salesdocument*order_header_in  = ls_order_header_in
      order_header_inx =ls_order_header_inx
logic_switch
=ls_logic_switchIMPORTINGe_result=ls_resultTABLESreturn=lt_so_return
order_item_in
=lt_order_item_in
order_item_inx
=lt_order_item_inx
schedule_lines
=lt_schedule_lines
schedule_linesx
=lt_schedule_linesx
extensionin
=lt_extensionin.ENDFORM."--------------------@斌将军-------------------

2.5、关于通讯类型Unicode

Unicode:
表示目标系统能够处理Unicode字符集。Unicode是一种全球通用的字符编码标准,用于表示世界上几乎所有语言的字符。使用Unicode通讯类型,可以在SAP系统中处理并传输多种语言的数据,包括特殊字符和非拉丁字母字符。

非Unicode:
表示目标系统只能处理非Unicode字符集。即系统仅支持传输和处理特定的字符集,如ANSI和ASCII字符集。非Unicode通讯类型适用于处理少数语言或特定的本地化需求。

区别:
在于系统对字符集的支持和处理能力。Unicode通讯类型允许SAP系统处理更广泛的字符集,使其具有更好的国际化和本地化能力。而非Unicode通讯类型的系统则限制了字符集的范围,可能导致在处理和传输多语言数据时出现问题。在选择目标系统的通讯类型时,需要根据具体的业务需求和语言环境来考虑。如果需要处理多语言数据或具有全球化的需求,建议选择Unicode通讯类型。而如果业务仅涉及特定的语言或本地化需求,非Unicode通讯类型可能更加适合。

定期更文,
欢迎关注