MD5 计算 (加密辅助类, Win32, C++)

news2024/11/23 23:46:31

CCryptHelper.h

#pragma once
#include <string>
#include <tchar.h>
#include <windows.h>

#ifdef _UNICODE
using _tstring = std::wstring;
#else
using _tstring = std::string;
#endif

// 加密辅助类
// 客户端: Windows XP 及以上系统可用
// 服务器: Windows Server 2003 及以上系统可用
class CCryptHelper
{
public:
	CCryptHelper();
	~CCryptHelper();

    //
    // @brief: 初始化
    // @param: strAlgorithm     哈希算法名字符串, 可选类型如下:
    //                          常见可选 CALG_MD5, CALG_SHA1, CALG_SHA_256, CALG_SHA_384, CALG_SHA_512
    // 
    //                          全部支持可选如下:
    //                              CALG_MD2, CALG_MD4, CALG_MD5, CALG_SHA, CALG_SHA1, CALG_RSA_SIGN, CALG_DSS_SIGN
    //                          Windows XP及以上: CALG_NO_SIGN, CALG_RSA_KEYX, CALG_DES, CALG_3DES_112, CALG_3DES, 
    //                              CALG_DESX, CALG_RC2, CALG_RC4, CALG_SEAL, CALG_DH_SF, CALG_DH_EPHEM, 
    //                              CALG_AGREEDKEY_ANY, CALG_KEA_KEYX, CALG_HUGHES_MD5, CALG_SKIPJACK, CALG_TEK, 
    //                              CALG_CYLINK_MEK, CALG_SSL3_SHAMD5, CALG_SSL3_MASTER, CALG_SCHANNEL_MASTER_HASH, 
    //                              CALG_SCHANNEL_MAC_KEY, CALG_SCHANNEL_ENC_KEY, CALG_PCT1_MASTER, CALG_SSL2_MASTER, 
    //                              CALG_TLS1_MASTER, CALG_RC5, CALG_HMAC, CALG_TLS1PRF
    //                          Windows XP及以上: CALG_HASH_REPLACE_OWF, CALG_AES_128, CALG_AES_192, CALG_AES_256, CALG_AES
    //                          Windows SP2以上: CALG_SHA_256, CALG_SHA_384, CALG_SHA_512
    //                          Windows Vista及以上: CALG_ECDH, CALG_ECDH_EPHEM, CALG_ECMQV, CALG_ECDSA, CALG_NULLCIPHER
    //                          Windows 10 1607及以上: CALG_THIRDPARTY_KEY_EXCHANGE, CALG_THIRDPARTY_SIGNATURE
    //                              CALG_THIRDPARTY_CIPHER, CALG_THIRDPARTY_HASH
    // 
    // @ret: bool               操作是否成功
	bool Initialize(
        DWORD dwAlgorithm = CALG_MD5
    );

    //
    // @brief: 反初始化
    // @ret: 无
	void Uninitialize();

    //
    // @brief: 重置
    // @ret: 无
    void Reset();

    //
    // @brief: 计算哈希值
    // @param: pData            数据
    // @param: ulSize           数据长度
    // @ret: bool               操作是否成功
    bool HashData(
        const void* pData, 
        unsigned long long ullSize
    );

    //
    // @brief: 获取累积的哈希值结果
    // @param: bUpper           是否大写
    // @ret: _tstring           哈希值字符串
    _tstring FinishHash(
        bool bUpper = true
    );

    //
    // @brief: 获取文件的哈希值结果
    // @param: strPath          文件路径
    // @param: bUpper           是否大写
    // @ret: _tstring           哈希值结果字符串
    _tstring GetFileHash(
        const _tstring& strPath, 
        bool bUpper = true
    );

    //
    // @brief: 获取数据的哈希值结果
    // @param: pData            数据指针
    // @param: ulSize           数据长度
    // @param: bUpper           是否大写
    // @ret: _tstring           哈希值结果字符串
    _tstring GetDataHash(
        const void* pData, 
        unsigned long long ullSize, 
        bool bUpper = true
    );

private:

    //
    // @brief: 计算哈希
    // @param: pData            数据指针
    // @param: ulSize           数据长度
    // @ret: 无
    bool _HashData(
        const void* pData,
        unsigned long ulSize
    );

    //
    // @brief: 字节内容转字符串
    // @param: pData            数据指针
    // @param: nSize            数据长度
    // @param: bUpper           是否大写
    // @ret: _tstring           转换后的字符串
    _tstring _BytesToString(
        const void* pData,
        size_t nSize,
        bool bUpper = true
    );

    //
    // @brief: 字符串转大小
    // @param: str              字符串
    // @ret: _tstring           转换后的字符串
    _tstring _ToUpper(
        const _tstring& str
    );

    //
    // @brief: 多字符字符串转宽字符串
    // @param: CodePage         代码页
    // @param: str              字符串
    // @ret: std::wstring       转换后的宽字符串
    std::wstring _MultiStrToWStr(
        UINT CodePage,
        const std::string& str
    );

    //
    // @brief: 字符串转宽字符串
    // @param: str              字符串
    // @ret: std::wstring       转换后的宽字符串
    std::wstring _TStrToWStr(
        const _tstring& str
    );

private:
	HCRYPTPROV              m_hProv = NULL;             // CSP 句柄
	HCRYPTHASH              m_hHash = NULL;             // 哈希对象
    DWORD                   m_dwAlgorithm = 0;          // 算法类型
    std::string             m_dataBuf;                  // 数据缓冲(用于文件读取)
};

CCryptHelper.cpp

#include "CCryptHelper.h"

#define FILE_HASH_BLOCK_SIZE        (1024 * 1024 * 4)

CCryptHelper::CCryptHelper()
    :
    m_hProv(NULL),
    m_hHash(NULL),
    m_dwAlgorithm(0)
{

}

CCryptHelper::~CCryptHelper()
{
    Uninitialize();
}


bool CCryptHelper::Initialize(
    DWORD dwAlgorithm/* = CALG_MD5*/
)
{
    bool bSuccess = false;

    if (m_dwAlgorithm == dwAlgorithm)
    {
        return true;
    }

    Uninitialize();

    do
    {
        // 获取加密服务提供程序句柄
        // https://learn.microsoft.com/zh-cn/windows/win32/api/wincrypt/nf-wincrypt-cryptacquirecontextw
        if (!CryptAcquireContext(
            &m_hProv,//指向 CSP 句柄的指针
            NULL,//密钥容器名称
            NULL,//要使用的 CSP 的名称
            PROV_RSA_FULL,//要获取的提供程序的类型
            CRYPT_VERIFYCONTEXT//标记值。 此参数通常设置为零
        ))
        {
            break;
        }

        bSuccess = true;

    } while (false);

    if (bSuccess)
    {
        m_dwAlgorithm = dwAlgorithm;
    }

    return bSuccess;
}

void CCryptHelper::Uninitialize()
{
    if (m_hHash)
    {
        ::CryptDestroyHash(m_hHash);
        m_hHash = NULL;
    }

    if (m_hProv)
    {
        ::CryptReleaseContext(m_hProv, 0);
        m_hProv = NULL;
    }

    m_dwAlgorithm = 0;
}

void CCryptHelper::Reset()
{
    if (m_hHash)
    {
        ::CryptDestroyHash(m_hHash);
        m_hHash = NULL;
    }
}

bool CCryptHelper::HashData(
    const void* pData, 
    unsigned long long ullSize
)
{
    const char* pDataBegin = (const char*)pData;
    const unsigned long ulMaxBlockSize = UINT32_MAX;
    bool bSuccess = false;

    if (0 == m_dwAlgorithm)
    {
        return false;
    }

    // 小于32位最大值则直接计算哈希值
    if (ullSize <= ulMaxBlockSize)
    {
        return _HashData(pDataBegin, (unsigned long)ullSize);
    }

    // 分段计算哈希值
    while (ullSize > 0)
    {
        unsigned long ulReadSize = (ullSize > ulMaxBlockSize) ? ulMaxBlockSize : (unsigned long)ullSize;
        if (!_HashData(pDataBegin, ulReadSize))
        {
            break;
        }

        pDataBegin += ulReadSize;
        ullSize -= ulReadSize;
    }

    return bSuccess;
}

_tstring CCryptHelper::FinishHash(
    bool bUpper/* = true*/
)
{
    _tstring strResult;
    BYTE byteHash[MAX_PATH] = { 0 };
    DWORD cbHash = sizeof(byteHash);

    if (0 == m_dwAlgorithm)
    {
        return strResult;
    }

    if (NULL == m_hHash)
    {
        // 创建哈希处理对象
        // https://learn.microsoft.com/zh-cn/windows/win32/api/wincrypt/nf-wincrypt-cryptcreatehash
        ::CryptCreateHash(
            m_hProv, //通过调用 CryptAcquireContext 创建的 CSP 的句柄
            m_dwAlgorithm, //标识要使用的哈希算法 的ALG_ID 值, 此参数的有效值因使用的 CSP 而异
            0, //如果哈希算法的类型是键控哈希, 则哈希的密钥在此参数中传递
            0, //标志值
            &m_hHash//输出哈希对象的地址
        );
    }

    if (NULL == m_hHash)
    {
        return strResult;
    }

    // 检索控制哈希对象操作的数据, 获取实际哈希值
    // https://learn.microsoft.com/zh-cn/windows/win32/api/wincrypt/nf-wincrypt-cryptgethashparam
    if (::CryptGetHashParam(
        m_hHash, //要查询的哈希对象的句柄
        HP_HASHVAL, //查询类型
        byteHash, //指向接收指定值数据的缓冲区的指针
        &cbHash, //指向指定 pbData 缓冲区大小
        0 //保留供将来使用,必须为零
    ))
    {
        strResult = _BytesToString(byteHash, cbHash, bUpper);
    }

    // 销毁哈希对象
    if (m_hHash)
    {
        ::CryptDestroyHash(m_hHash);
        m_hHash = NULL;
    }

    return strResult;
}

_tstring CCryptHelper::GetFileHash(
    const _tstring& strPath, 
    bool bUpper/* = true*/
)
{
    HANDLE hFile = INVALID_HANDLE_VALUE;
    DWORD dwBlockSize = FILE_HASH_BLOCK_SIZE;
    DWORD dwBytesRead = 0;

    if (0 == m_dwAlgorithm)
    {
        return _tstring(_T(""));
    }

    do
    {
        // 打开文件
        // https://learn.microsoft.com/zh-cn/windows/win32/api/fileapi/nf-fileapi-createfilew
        hFile = CreateFile(strPath.c_str(),
            GENERIC_READ,
            FILE_SHARE_READ,
            NULL,
            OPEN_EXISTING,
            FILE_FLAG_SEQUENTIAL_SCAN,
            NULL);

        if (INVALID_HANDLE_VALUE == hFile)
        {
            break;
        }

        if (m_dataBuf.empty())
        {
            m_dataBuf.resize(dwBlockSize);
        }

        // 读取文件数据
        // https://learn.microsoft.com/zh-cn/windows/win32/api/fileapi/nf-fileapi-readfile
        while (::ReadFile(hFile, &m_dataBuf[0], dwBlockSize, &dwBytesRead, NULL))
        {
            if (0 == dwBytesRead)
            {
                break;
            }

            if (!_HashData(&m_dataBuf[0], dwBytesRead))
            {
                break;
            }
        }

    } while (false);

    if (INVALID_HANDLE_VALUE != hFile)
    {
        ::CloseHandle(hFile);
    }

    return FinishHash(bUpper);
}

_tstring CCryptHelper::GetDataHash(
    const void* pData, 
    unsigned long long ullSize,
    bool bUpper/* = true*/
)
{
    _tstring strResult;
    if (0 == m_dwAlgorithm)
    {
        return strResult;
    }

    if (HashData(pData, ullSize))
    {
        strResult = FinishHash(bUpper);
    }

    return strResult;
}

bool CCryptHelper::_HashData(
    const void* pData, 
    unsigned long ulSize
)
{
    bool bSuccess = false;

    if (NULL == m_hHash)
    {
        // 创建哈希处理对象
        // https://learn.microsoft.com/zh-cn/windows/win32/api/wincrypt/nf-wincrypt-cryptcreatehash
        ::CryptCreateHash(
            m_hProv, //通过调用 CryptAcquireContext 创建的 CSP 的句柄
            m_dwAlgorithm, //标识要使用的哈希算法 的ALG_ID 值, 此参数的有效值因使用的 CSP 而异
            0, //如果哈希算法的类型是键控哈希, 则哈希的密钥在此参数中传递
            0, //标志值
            &m_hHash//输出哈希对象的地址
        );
    }

    if (NULL == m_hHash)
    {
        return false;
    }

    // 将数据添加到指定的哈希对象
    bSuccess = ::CryptHashData(
        m_hHash,
        (const BYTE*)pData,
        ulSize,
        0
    );
    return bSuccess;
}

_tstring CCryptHelper::_BytesToString(
    const void* pData, 
    size_t nSize, 
    bool bUpper/* = true*/
)
{
    const TCHAR rgbDigitsUpper[] = _T("0123456789ABCDEF");
    const TCHAR rgbDigitsLower[] = _T("0123456789abcdef");
    _tstring strResult(nSize * 2, 0);
    LPCBYTE lpBytes = (LPCBYTE)pData;

    if (bUpper)
    {
        for (DWORD i = 0; i < nSize; i++)
        {
            strResult[i * 2] = rgbDigitsUpper[lpBytes[i] >> 4];
            strResult[i * 2 + 1] = rgbDigitsUpper[lpBytes[i] & 0x0F];
        }
    }
    else
    {
        for (DWORD i = 0; i < nSize; i++)
        {
            strResult[i * 2] = rgbDigitsLower[lpBytes[i] >> 4];
            strResult[i * 2 + 1] = rgbDigitsLower[lpBytes[i] & 0x0F];
        }
    }

    return strResult;
}

_tstring CCryptHelper::_ToUpper(
    const _tstring& str
)
{
    _tstring strResult = str;
    for (auto& item : strResult)
    {
        if (item >= _T('a') && item <= _T('z'))
        {
            item -= 0x20;
        }
    }

    return strResult;
}

std::wstring CCryptHelper::_MultiStrToWStr(
    UINT CodePage, 
    const std::string& str
)
{
    // 计算缓冲区所需的字符长度
    // https://learn.microsoft.com/zh-cn/windows/win32/api/stringapiset/nf-stringapiset-multibytetowidechar
    int cchWideChar = ::MultiByteToWideChar(CodePage, 0, str.c_str(), -1, NULL, NULL);
    std::wstring strResult(cchWideChar, 0);

    //成功则返回写入到指示的缓冲区的字符数
    size_t nConverted = ::MultiByteToWideChar(CodePage, 0, str.c_str(), (int)str.size(), &strResult[0], (int)strResult.size());

    //调整内容长度
    strResult.resize(nConverted);
    return strResult;
}

std::wstring CCryptHelper::_TStrToWStr(
    const _tstring& str
)
{
#ifdef _UNICODE
    return str;
#else
    return _MultiStrToWStr(CP_ACP, str);
#endif
}

main.cpp

#include <locale.h>
#include <tchar.h>
#include "Win32Utils/CTimeUtils.h"
#include "Win32Utils/CPathUtils.h"
#include "Win32Utils/CCNGHelper.h"
#include "Win32Utils/CCryptHelper.h"

int _tmain(int argc, LPCTSTR argv[])
{
    UNREFERENCED_PARAMETER(argc);
    UNREFERENCED_PARAMETER(argv);

    ::setlocale(LC_ALL, "");

    unsigned long long ullStartTime;
    unsigned long long ullEndTime;

    CCNGHelper cngObj;
    cngObj.Initialize(_T("md5"));
    CCryptHelper cryptObj;
    cryptObj.Initialize(CALG_MD5);

    _tstring strCngHash;
    _tstring strCryptHash;

    system("pause");

    int nCount = 1000;
    while (true)
    {
        ullStartTime = CTimeUtils::GetCurrentTickCount();
        for (int i = 0; i < nCount; i++)
        {
            strCngHash = cngObj.GetFileHash(CPathUtils::GetCurrentModulePath(), true);
        }
        ullEndTime = CTimeUtils::GetCurrentTickCount();

        _tprintf(
            _T("CCNGHelper MD5: %s Count: %d Cost time: %lld ms\n"), 
            strCngHash.c_str(), 
            nCount, 
            ullEndTime - ullStartTime
        );

        ullStartTime = CTimeUtils::GetCurrentTickCount();
        for (int i = 0; i < nCount; i++)
        {
            strCryptHash = cryptObj.GetFileHash(CPathUtils::GetCurrentModulePath(), true);
        }
        ullEndTime = CTimeUtils::GetCurrentTickCount();

        _tprintf(
            _T("CCryptHelper MD5: %s Count: %d Cost time: %lld ms\n"), 
            strCryptHash.c_str(), 
            nCount, 
            ullEndTime - ullStartTime
        );

        system("pause");
    }

    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1553394.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VESTA模拟计算XRD标准卡片

先上Crystallography Open Database网站下载标准CIF卡片&#xff08;以PbI2为例&#xff09; 1.直接进网站搜元素就行 2.点CIF直接下载 3.打开VESTA&#xff0c;导入刚刚下载的CIF 4.导入成功就是这样的 5.按照我这个操作来计算 6.点Calculation 7.已经计算出来了&#xff…

政安晨:专栏目录【TensorFlow与Keras实战演绎机器学习】

政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 收录专栏: TensorFlow与Keras实战演绎机器学习 希望政安晨的博客能够对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff01; 本篇是作者政安晨的专栏《TensorFlow与Keras…

cf937Div4E题F题

题目要找到一个长为k的子串,使得x个相同的k相连长度和s相同且对应字符中只能有一个地方对应的字符不同, 那是不是说明s也能分成x段,且最多有一段中的一个字符不同,否则就不满足要求,那我们现在要讨论这个不同的字符在哪,如果在第一段比如sabaa aaaa aaaa aaaa,如果我们取了abaa…

怎么评价小米汽车SU7?

编辑搜图 请点击输入图片描述&#xff08;最多18字&#xff09; 小米汽车SU7&#xff1a;电动智能驾驶的新篇章 随着全球汽车产业的深度变革&#xff0c;新能源汽车、智能驾驶等概念逐渐深入人心。在这场汽车产业的革新中&#xff0c;小米汽车SU7无疑是一个引人注目的焦点。这…

利用 Scapy 库编写 ARP 缓存中毒攻击脚本

一、ARP 协议基础 参考下篇文章学习 二、ARP 缓存中毒原理 ARP&#xff08;Address Resolution Protocol&#xff09;缓存中毒是一种网络攻击&#xff0c;它利用了ARP协议中的漏洞&#xff0c;通过欺骗或篡改网络中的ARP缓存来实施攻击。ARP协议是用于将IP地址映射到物理MAC…

002-基于Pytorch的手写汉字数字分类

本节将介绍一种 2.1 准备 2.1.1 数据集 &#xff08;1&#xff09;MNIST 只要学习过深度学习相关理论的人&#xff0c;都一定听说过名字叫做LeNet-5模型&#xff0c;它是深度学习三巨头只有Yann Lecun在1998年提出的一个CNN模型&#xff08;很多人认为这是第一个具有实际应用…

npm install 报错ERESOLVE unable to resolve dependency tree

描述&#xff1a;npm install 报错ERESOLVE unable to resolve dependency tree 解决方案&#xff1a; npm install --legacy-peer-deps

【Linux】网络编程套接字二

网络编程套接字二 1.TCP网络编程1.1TCP Server服务端1.2 TCP Client客户端 2.Server 多进程版本2.1普通版2.2 信号版 3.Server 多线程版4.Server 线程池版5.日志函数重新设计6.守护进程7.TCP协议通讯流程8.TCP和UDP 对比 喜欢的点赞&#xff0c;收藏&#xff0c;关注一下把&…

[Java基础揉碎]抽象类

目录 通过问题引出 介绍 关键点 细节 ​编辑 抽象类的最佳设计模式--模版设计模式 1.先用最容易想到的方法 2.分析问题&#xff0c;提出使用模板设计模式 通过问题引出 假如我们有个动物类, 动物都有eat吃的方法, 但是具体吃什么, 我们不知道, 因为是什么动物我们不知道…

绘制特征曲线-ROC(Machine Learning 研习十七)

接收者操作特征曲线&#xff08;ROC&#xff09;是二元分类器的另一个常用工具。它与精确度/召回率曲线非常相似&#xff0c;但 ROC 曲线不是绘制精确度与召回率的关系曲线&#xff0c;而是绘制真阳性率&#xff08;召回率的另一个名称&#xff09;与假阳性率&#xff08;FPR&a…

【爬虫框架pyspider】01-pyspider入门与基本使用

前言 前面我们把爬虫的流程实现一遍&#xff0c;将不同的功能定义成不同的方法&#xff0c;甚至抽象出模块的概念。如微信公众号爬虫&#xff0c;我们已经有了爬虫框架的雏形&#xff0c;如调度器、队列、请求对象等&#xff0c;但是它的架构和模块还是太简单&#xff0c;远远…

|行业洞察·碳纤维|《中国碳纤维行业现状与发展趋势-39页》

报告内容的详细解读&#xff1a; 1. 战略性新材料的重要性 碳纤维是一种轻质高强的高性能纤维材料&#xff0c;在航空航天、国防军工、高端装备制造等领域具有不可替代的作用。碳纤维的应用有助于减少能源消耗和降低碳排放&#xff0c;符合全球可持续发展的要求。 |趋势洞察…

Java增强for循环和foreach循环的误区

网上很多文章都在说增强for循环和foreach循环遍历时不能修改值&#xff0c;只能查看&#xff0c;其实是有区分条件的&#xff0c;不能修改值的是包装类&#xff0c;例如List<String>,引用类型是可以修改值的&#xff0c;例如对象集合。 使用增强for循环或者foreach循环遍…

李宏毅【生成式AI导论 2024】第6讲 大型语言模型修炼_第一阶段_ 自我学习累积实力

背景知识:机器怎么学会做文字接龙 详见:https://blog.csdn.net/qq_26557761/article/details/136986922?spm=1001.2014.3001.5501 在语言模型的修炼中,我们需要训练资料来找出数十亿个未知参数,这个过程叫做训练或学习。找到参数后,我们可以使用函数来进行文字接龙,拿…

解决“Pycharm中Matplotlib图像不弹出独立的显示窗口”问题

matplotlib的绘图的结果默认显示在SciView窗口中, 而不是弹出独立的窗口, 这样看起来就不是很舒服&#xff0c;不习惯。 通过修改设置&#xff0c;改成独立弹出的窗口。 File—>Settings—>Tools—>Python Scientific—>Show plots in toolwindow 将√去掉即可

一台日本原生ip站群服务器多少钱?

一台日本原生ip站群服务器多少钱&#xff1f;日本原生ip站群服务器的价格受到多个因素的影响。以下是一些主要的因素&#xff1a; 服务器配置&#xff1a;硬件配置越高&#xff0c;自然价格也越高。对于站群服务器来说&#xff0c;由于需要同时运行多个网站&#xff0c;因此配置…

Vue挂载全局方法

简介&#xff1a;有时候&#xff0c;频繁调用的函数&#xff0c;我们需要把它挂载在全局的vue原型上&#xff0c;方便调用&#xff0c;具体怎么操作&#xff0c;这里来记录一下。 一、这里以本地存储的方法为例 var localStorage window.localStorage; const db {/** * 更新…

学习JavaEE的日子 Day32 线程池

Day32 线程池 1.引入 一个线程完成一项任务所需时间为&#xff1a; 创建线程时间 - Time1线程中执行任务的时间 - Time2销毁线程时间 - Time3 2.为什么需要线程池(重要) 线程池技术正是关注如何缩短或调整Time1和Time3的时间&#xff0c;从而提高程序的性能。项目中可以把Time…

【tensorflow框架神经网络实现鸢尾花分类】

文章目录 1、数据获取2、数据集构建3、模型的训练验证可视化训练过程 1、数据获取 从sklearn中获取鸢尾花数据&#xff0c;并合并处理 from sklearn.datasets import load_iris import pandas as pdx_data load_iris().data y_data load_iris().targetx_data pd.DataFrame…

Flask学习(六):蓝图(Blueprint)

蓝图&#xff08;Blueprint&#xff09;&#xff1a;将各个业务进行区分&#xff0c;然后每一个业务单元可以独立维护&#xff0c;Blueprint可以单独具有自己的模板、静态文件或者其它的通用操作方法&#xff0c;它并不是必须要实现应用的视图和函数的。 Demo目录结构&#xf…