基于MFC与Access的电子名片管理系统设计与实现
别急着说“MFC 都老掉牙了” 😅 —— 技术选型从来不是追新,而是看是否合适。对于中小企业或个人开发者而言,要快速搭建一个稳定、本地化运行的信息管理工具,MFC(Microsoft Foundation Classes)依然是极具性价比的选择:它深度集成于 Visual Studio,开箱即用;对 Win32 API 的封装极大降低了窗口创建、消息处理等底层复杂度;支持原生性能表现,无运行时依
简介:电子名片管理系统是面向现代商务环境的信息管理工具,结合MFC框架与Microsoft Access数据库,构建了一个高效、用户友好的联系人信息存储与检索平台。系统具备登录验证、快速搜索和对话框交互等核心功能,支持名片信息的增删改查及复杂查询,提升了信息管理效率。本系统不仅满足实际应用需求,还可作为开发其他管理系统的可复用模板,具有良好的扩展性与实践价值。
电子名片管理系统:从 MFC 界面到数据库安全的全栈实践
在当今这个信息爆炸的时代,你有没有试过翻遍口袋、公文包甚至沙发缝隙,只为找到一张关键联系人的纸质名片?🤯 而对方可能早就换了号码,那张印着“高级经理”的纸片也早已过时。这不仅是尴尬,更是效率的流失。
于是我们开始思考:为什么不把这一切数字化?让每一次交换都像发送一条微信那样简单,而所有信息都能自动更新、智能分类、安全存储——这就是 电子名片管理系统 诞生的初衷。它不只是一个“电子版纸质名片”,更是一套完整的个人/企业联络关系管理解决方案。今天,我们就来深入剖析这样一个系统的技术实现全过程,从 MFC 桌面界面设计,到底层 Access 数据库集成,再到登录认证与数据安全机制,带你走完一次真实的 Windows 原生应用开发之旅。
架构选择的艺术:为什么是 MFC + Access?
别急着说“MFC 都老掉牙了” 😅 —— 技术选型从来不是追新,而是看是否 合适 。
对于中小企业或个人开发者而言,要快速搭建一个稳定、本地化运行的信息管理工具,MFC(Microsoft Foundation Classes)依然是极具性价比的选择:
- 它深度集成于 Visual Studio,开箱即用;
- 对 Win32 API 的封装极大降低了窗口创建、消息处理等底层复杂度;
- 支持原生性能表现,无运行时依赖问题;
- UI 控件丰富,资源编辑器可视化拖拽,开发效率高。
再搭配 Microsoft Access 作为后端数据库,简直是“轻量级神器组合”:无需安装 SQL Server 或 MySQL 服务, .mdb 文件随身携带,双击即可打开使用。虽然它不适合高并发场景,但对于单机使用的名片管理系统来说,完全够用且部署成本几乎为零。
所以我们的架构最终定为:
[用户交互层] ←→ [业务逻辑层] ←→ [数据持久层]
↑ ↑ ↑
MFC GUI C++ 类封装 Access (.mdb) + DAO
三层结构清晰解耦,既保证可维护性,又便于后期扩展升级。比如未来想换 SQLite 或迁移到云端,只需替换最底层的数据访问模块即可,上层代码基本不动。
图形界面的灵魂:MFC 是如何“活”起来的?
该选文档/视图,还是对话框模式?
这是每个 MFC 新手都会遇到的第一个十字路口 🚦。
| 特性 | 文档/视图架构 | 对话框应用 |
|---|---|---|
| 多文档支持 | ✅ (MDI/SDI) | ❌ |
| 数据绑定 | 手动同步 | DDX/DDV 自动绑定 |
| 消息路由 | 复杂(文档→视图→框架) | 直接响应 |
| 开发效率 | 较低 | 高 |
| 典型用途 | Word、CAD 类软件 | 设置向导、管理工具 |
显然,对于我们这种以表单输入为主、不涉及文件序列化的应用场景, 基于对话框的应用模式才是最优解 。
主窗口继承自 CDialogEx ,整个程序就像一个“超级对话框”,所有的按钮点击、文本变化都被封装成清晰的消息映射函数:
class CTestDlg : public CDialogEx
{
DECLARE_DYNAMIC(CTestDlg)
public:
CTestDlg(CWnd* pParent = nullptr);
virtual ~CTestDlg();
enum { IDD = IDD_TEST_DIALOG };
protected:
virtual void DoDataExchange(CDataExchange* pDX);
DECLARE_MESSAGE_MAP()
private:
afx_msg void OnBnClickedButtonSave();
afx_msg void OnEnChangeEditName();
afx_msg void OnClose();
};
看到那个 DECLARE_MESSAGE_MAP() 没?它是 MFC 实现事件驱动的核心魔法之一。
消息映射:Windows 的心跳节律
Windows 是典型的消息驱动操作系统。你点一下鼠标,系统就往目标窗口投递一条 WM_LBUTTONDOWN 消息;你按下回车键,就是 WM_KEYDOWN ……这些消息堆积在队列里,等待被处理。
但直接写 WindowProc 回调函数太原始了!MFC 用宏包装了一切:
BEGIN_MESSAGE_MAP(CTestDlg, CDialogEx)
ON_BN_CLICKED(IDC_BUTTON_SAVE, &CTestDlg::OnBnClickedButtonSave)
ON_EN_CHANGE(IDC_EDIT_NAME, &CTestDlg::OnEnChangeEditName)
ON_WM_CLOSE()
END_MESSAGE_MAP()
这几行宏背后,其实是编译器生成的一个静态数组,记录着“什么消息 → 调哪个函数”。当按钮被点击时,MFC 内部会查找这张表,然后调用对应的成员函数——整个过程对开发者透明,写起来就像在响应网页上的 onclick 事件一样自然!
举个实际例子:保存名片按钮的响应逻辑:
void CTestDlg::OnBnClickedButtonSave()
{
UpdateData(TRUE); // 将界面上的内容“刷”进成员变量
if (m_strName.IsEmpty()) {
AfxMessageBox(_T("姓名不能为空!"));
GetDlgItem(IDC_EDIT_NAME)->SetFocus();
return;
}
SaveToDatabase(); // 真正的保存操作
AfxMessageBox(_T("名片保存成功!"));
}
注意这里的 UpdateData(TRUE) ,它是 DDX(Dialog Data Exchange)机制的关键入口。你在 .h 文件中定义的 CString m_strName; ,通过 Class Wizard 已经和 IDC_EDIT_NAME 编辑框绑定了。只要一调用 UpdateData(TRUE) ,控件里的内容就会自动填入变量;反之 UpdateData(FALSE) 则是把变量值反向写回界面。
💡 小贴士:很多人不知道,DDX 不仅支持 CString,还支持 int、float、COleDateTime 等多种类型,甚至连复选框、单选组都可以一键绑定!
可视化布局:资源编辑器的隐藏技巧
Visual Studio 的资源编辑器是 MFC 开发者的瑞士军刀 🔧。你可以像做 PPT 一样拖动按钮、调整字体、设置颜色。但真正考验功力的是——如何让界面“适应不同分辨率”?
锚定(Anchoring)怎么做?
MFC 原生不支持现代意义上的锚点系统(如 WPF 的 HorizontalAlignment),但我们可以通过重载 OnSize 来手动实现动态布局:
void CTestDlg::OnSize(UINT nType, int cx, int cy)
{
CDialogEx::OnSize(nType, cx, cy);
if (!m_bInitialized) return;
CRect rect;
GetClientRect(&rect);
// OK 按钮右下角固定
CWnd* pBtnOK = GetDlgItem(IDOK);
if (pBtnOK) {
pBtnOK->SetWindowPos(nullptr,
rect.right - 90, rect.bottom - 40,
0, 0, SWP_NOSIZE | SWP_NOZORDER);
}
// 名片列表拉伸填充中间区域
CWnd* pList = GetDlgItem(IDC_LIST_CARDS);
if (pList) {
pList->SetWindowPos(nullptr, 10, 10,
rect.Width() - 20, rect.Height() - 80,
SWP_NOZORDER);
}
}
这样无论窗口怎么缩放,按钮始终贴在右下角,列表控件则占据中央空白区,用户体验瞬间提升一大截!
DPI 自适应也不能少!
现在谁不用 2K/4K 屏啊?如果你还在用固定像素大小的字体,那你的界面在高分屏上简直就是“迷你玩具”。
正确的做法是根据系统 DPI 动态计算字体高度:
LOGFONT lf = {0};
CFont* pDefault = GetFont();
pDefault->GetLogFont(&lf);
_tcscpy_s(lf.lfFaceName, _T("Segoe UI")); // 使用现代字体
lf.lfHeight = -MulDiv(9, GetDeviceCaps(GetDC()->m_hDC, LOGPIXELSY), 72); // 根据 DPI 计算
m_fontTitle.CreateFontIndirect(&lf);
GetDlgItem(IDC_STATIC_TITLE)->SetFont(&m_fontTitle);
这里的关键在于 MulDiv(9, LOGPIXELSY, 72) ,意思是“将 9pt 字体按当前屏幕 DPI 映射为逻辑像素”。比如在 150% 缩放下,实际渲染为约 13.5px,完美适配。
跨窗口通信:模态 vs 非模态的权衡
系统里不止一个窗口。除了主界面,还有添加名片的弹窗、搜索面板、设置页等等。它们之间怎么传数据?
方法一:模态对话框参数传递(推荐新手)
最简单的就是“打开子窗 → 获取结果 → 关闭”。例如添加名片时:
void CTestDlg::OnAddNewCard()
{
CMyDialog dlg(this);
dlg.m_strName = ""; // 清空上次输入
dlg.m_strCompany = "";
if (dlg.DoModal() == IDOK) {
// 用户点了“确定”
AddContactToList(dlg.m_strName, dlg.m_strCompany);
}
// 否则取消了,啥也不干
}
这里的 DoModal() 是阻塞式的,意味着父窗口不能操作,直到子窗口关闭。适合一次性输入任务,逻辑清晰不易出错。
方法二:非模态 + 消息通知(适合高级交互)
如果希望多个窗口同时可用(比如一边编辑一边预览),就得用 Create() 创建非模态对话框,并通过自定义消息通信:
// 定义私有消息
#define WM_USER_CARD_UPDATED (WM_USER + 100)
// 子窗口修改完成后通知主窗口刷新
GetParent()->SendMessage(WM_USER_CARD_UPDATED, 0, 0);
// 主窗口消息映射
ON_MESSAGE(WM_USER_CARD_UPDATED, &CTestDlg::OnCardUpdated)
LRESULT CTestDlg::OnCardUpdated(WPARAM wParam, LPARAM lParam)
{
RefreshContactList(); // 重新加载数据
return 0;
}
这种方式更灵活,但也容易造成内存泄漏(忘记销毁窗口)或消息风暴(频繁触发)。建议只在必要时使用。
数据之根:Access 数据库的设计哲学
表结构设计:不只是字段堆砌
一个好的数据库模型,应该能反映现实世界的逻辑关系。我们先列出核心字段:
| 字段名 | 类型 | 是否为空 | 说明 |
|---|---|---|---|
| ID | AutoNumber | 否 | 自增主键 |
| Name | Text(50) | 否 | 姓名 |
| Position | Text(50) | 是 | 职位 |
| Company | Text(100) | 是 | 公司 |
| Phone | Text(20) | 是 | 手机号 |
| Text(100) | 是 | 邮箱 | |
| Address | Text(255) | 是 | 地址 |
| Notes | Memo | 是 | 备注 |
| CreateTime | DateTime | 否 | 创建时间 |
其中几个细节值得深思:
-
Phone 和 Email 为什么要设为 Text 而不是 Number?
因为手机号可能包含国家前缀(+86)、带横杠(138-XXXX-XXXX),甚至有些用户留的是座机分机号。强行转数字反而丢失信息。 -
Email 是否唯一?
当然!你不希望重复录入同一个客户吧?所以我们给 Email 加 唯一索引 :
sql CREATE UNIQUE INDEX idx_email ON CardInfo (Email);
- CreateTime 默认值设为 Now() ,确保每条记录都有准确的时间戳,方便后续按时间排序或归档。
DAO 接口连接数据库:稳准狠的三步走
MFC 提供了两种主流方式连接 Access:ODBC 和 DAO。
| 对比项 | ODBC | DAO |
|---|---|---|
| 性能 | 一般 | 更快(Jet 引擎优化) |
| 易用性 | 需配置 DSN | 直接打开 .mdb 文件 |
| 平台依赖 | 跨平台潜力 | Windows Only |
| 功能完整性 | 完整 | 支持 Access 特有功能 |
既然我们明确是 Windows 单机应用,那就果断选 DAO !
封装一个全局数据库管理类是最明智的做法:
// DbManager.h
class CDbManager : public CObject
{
public:
CDaoDatabase m_db;
BOOL OpenDatabase(LPCTSTR lpszPath);
void CloseDatabase();
};
// DbManager.cpp
BOOL CDbManager::OpenDatabase(LPCTSTR lpszPath)
{
try {
if (m_db.IsOpen())
m_db.Close();
m_db.Open(lpszPath, FALSE, FALSE, _T(""));
return TRUE;
}
catch (CDaoException* e) {
e->ReportError();
e->Delete();
return FALSE;
}
}
⚠️ 注意陷阱:第三个参数
FALSE表示“非独占模式”,允许多进程读取;但如果多人同时写入 MDB 文件,仍可能出现锁冲突。建议系统提示“请勿多开实例”。
业务逻辑实战:增删改查背后的工程智慧
添加名片:前端验证 + 后端防护双保险
你以为 AddNew 就完了?Too young.
真实世界中的数据千奇百怪,必须层层设防:
BOOL CTest::AddNewCard(const CString& name, const CString& phone, const CString& email)
{
// 第一道防线:前端校验
if (name.IsEmpty()) {
AfxMessageBox(_T("姓名不能为空!"));
return FALSE;
}
if (!IsValidPhone(phone)) {
AfxMessageBox(_T("请输入有效的手机号码!"));
return FALSE;
}
if (!email.IsEmpty() && !IsValidEmail(email)) {
AfxMessageBox(_T("邮箱格式不正确!"));
return FALSE;
}
// 第二道防线:数据库层约束
try {
CDaoRecordset rs(&g_DbManager.m_db);
rs.Open(dbOpenDynaset, _T("SELECT * FROM CardInfo WHERE 1=0"));
rs.AddNew();
rs.SetFieldValue(_T("Name"), name);
rs.SetFieldValue(_T("Phone"), phone);
rs.SetFieldValue(_T("Email"), email);
rs.SetFieldValue(_T("CreateTime"), COleDateTime::GetCurrentTime());
rs.Update(); // 提交事务
rs.Close();
return TRUE;
}
catch (CDaoException* e) {
if (e->m_nAffects == adErrDuplicateKey) {
AfxMessageBox(_T("该邮箱已存在,请勿重复添加!"));
} else {
AfxMessageBox(_T("数据库写入失败,请检查权限或磁盘空间。"));
}
e->Delete();
return FALSE;
}
}
看到没?即使前端绕过了验证(比如有人直接调 API),后端唯一索引也能拦住重复插入。这才是健壮系统的模样!
正则验证:别再手写“土味判断”
很多人验证手机号还写 if (len==11 && startsWith("1")) ,早该淘汰了!
正确的姿势是用正则表达式:
BOOL CTest::IsValidPhone(const CString& phone)
{
static const TCHAR* pattern = _T("^1[3-9]\\d{9}$"); // 匹配中国大陆手机号
CRegEx re(pattern);
return re.IsMatch(phone);
}
BOOL CTest::IsValidEmail(const CString& email)
{
static const TCHAR* pattern =
_T("^[a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");
CRegEx re(pattern);
return re.IsMatch(email);
}
🛠 提示:
CRegEx是一个轻量级封装类,可以基于 ATL 的CAtlRegExp实现,几行代码就能搞定。
删除操作也要讲“事务精神”
虽然 Access 的 DAO 不支持完整的事务控制(没有 BeginTrans/CommitTrans ),但我们依然可以模拟原子性行为:
BOOL CTest::DeleteCardsByIds(const CUIntArray& ids)
{
int successCount = 0;
for (int i = 0; i < ids.GetSize(); ++i) {
try {
CDaoRecordset rs(&g_DbManager.m_db);
CString sql;
sql.Format(_T("SELECT * FROM CardInfo WHERE ID=%ld"), ids[i]);
rs.Open(dbOpenSnapshot, sql);
if (!rs.IsEOF()) {
rs.Delete();
successCount++;
}
rs.Close();
}
catch (...) {
AfxMessageBox(_T("部分记录删除失败,请重试。"));
break; // 出错立即中断,防止状态混乱
}
}
return successCount == ids.GetSize();
}
虽然不能回滚,但至少做到了“要么全部成功,要么尽早失败”,避免出现“删了一半突然崩了”的灾难性后果。
搜索模块:让用户秒找十万联系人
随着数据积累,搜索成了生死线。没人愿意一页页翻找。
多条件组合查询怎么拼 SQL?
不能图省事直接字符串拼接,否则轻则出错,重则被注入攻击!
CString BuildSearchSQL(const SearchCriteria& criteria)
{
CString sql = _T("SELECT * FROM CardInfo WHERE 1=1");
if (!criteria.name.IsEmpty())
sql += _T(" AND Name LIKE '%") + EscapeLike(criteria.name) + _T("%'");
if (!criteria.company.IsEmpty())
sql += _T(" AND Company LIKE '%") + EscapeLike(criteria.company) + _T("%'");
if (!criteria.phone.Contains('*') && !criteria.phone.Contains('?'))
sql += _T(" AND Phone LIKE '%") + EscapeLike(criteria.phone) + _T("%'");
return sql;
}
其中 EscapeLike() 是关键:
CString EscapeLike(const CString& input)
{
CString result = input;
result.Replace(_T("'"), _T("''")); // 防止 SQL 注入
result.Replace(_T("["), _T("[[]"));
result.Replace(_T("%"), _T("[%]"));
result.Replace(_T("_"), _T("[_]"));
return result;
}
这样即使用户搜 "A_B" ,也不会误匹配 “AxB”,而是精确查找下划线本身。
性能调优:延迟加载 + 分页显示
如果你敢在 OnChange 事件里每次打字都查数据库,恭喜你,卡顿警告来了 🚨!
正确做法是引入“去抖(debounce)”机制:
void CTestDlg::OnEnChangeEditSearch()
{
KillTimer(SEARCH_DEBOUNCE_TIMER); // 取消之前的定时器
SetTimer(SEARCH_DEBOUNCE_TIMER, 300, NULL); // 延迟300ms再执行
}
void CTestDlg::OnTimer(UINT_PTR nIDEvent)
{
if (nIDEvent == SEARCH_DEBOUNCE_TIMER) {
DoSearch(); // 执行真正的查询
KillTimer(SEARCH_DEBOUNCE_TIMER);
}
CDialogEx::OnTimer(nIDEvent);
}
此外,一次性加载上千条数据会耗尽内存。建议加入分页:
CString GetPagedSQL(int pageIdx, int pageSize)
{
int offset = pageIdx * pageSize;
return Format(_T("SELECT TOP %d * FROM "
"(SELECT TOP %d * FROM CardInfo ORDER BY ID DESC) "
"ORDER BY ID ASC"), pageSize, offset + pageSize);
}
利用嵌套查询模拟 OFFSET-FETCH ,虽不够优雅但在 Access 中可行。
安全防线:从登录到加密的全方位守护
登录验证:不只是用户名密码
你以为登录就是比对数据库字段?Too simple!
我们加了几层保护:
BOOL CLoginDlg::OnLogin()
{
CString strUsername, strPassword;
GetDlgItemText(IDC_EDIT_USERNAME, strUsername);
GetDlgItemText(IDC_EDIT_PASSWORD, strPassword);
if (strUsername.IsEmpty() || strPassword.IsEmpty()) {
MessageBox(_T("请输入完整凭据!"));
return FALSE;
}
CString strHashedPwd = MD5Hash(strPassword); // 单向哈希
CDatabase db;
if (!db.Open(_T("DSN=CardDB;"))) {
return FALSE;
}
CString sql = _T("SELECT * FROM Users WHERE Username=? AND Password=?");
CRecordset rs(&db);
rs.m_strFilter = Format(_T("Username='%s' AND Password='%s'"),
strUsername, strHashedPwd);
if (!rs.Open()) return FALSE;
BOOL bValid = !rs.IsEOF();
rs.Close(); db.Close();
if (bValid) {
RecordLoginSuccess(strUsername);
return TRUE;
} else {
HandleFailedAttempt(strUsername);
return FALSE;
}
}
亮点在哪?
- 密码 绝不明文存储 ,一律用 MD5(或更好是 SHA256)哈希;
- 登录失败次数累计,超过 5 次自动锁定账户 5 分钟;
- 成功登录后写入注册表记录最后用户,下次自动填充;
- 所有操作记入审计日志,方便追溯。
敏感数据加密:本地文件也不是绝对安全
别忘了, .mdb 文件是可以被别人复制走的!一旦落入他人之手,所有联系方式一览无余。
解决办法: 关键字段加密存储
| 字段 | 加密算法 | 存储形式 |
|---|---|---|
| 手机号 | AES-128 | Base64 密文 |
| 邮箱 | AES-128 | Base64 密文 |
| 地址 | AES-128 | Base64 密文 |
读取时自动解密:
CString DecryptField(const CString& encrypted)
{
if (encrypted.IsEmpty()) return "";
return AES_Decrypt(FromBase64(encrypted), g_strAppKey);
}
CString EncryptField(const CString& plain)
{
if (plain.IsEmpty()) return "";
return ToBase64(AES_Encrypt(plain, g_strAppKey));
}
🔑 密钥可以从硬件特征(如硬盘序列号)派生,也可以由用户设置主密码解锁。
这样一来,即便数据库被盗,没有密钥也无法还原真实信息。
防逆向工程:让黑客知难而退
MFC 程序是 native code,很容易被 IDA Pro 反编译。怎么办?
我们可以采取“软对抗”策略:
- 启用
/GL和/LTCG全程序优化,打乱函数边界; - 使用 Code Virtualizer 等工具将核心函数转为虚拟机字节码;
- 字符串常量异或加密,运行时解密;
- 插入 anti-debug 代码:
BOOL IsBeingDebugged()
{
BOOL bIsDebugged = FALSE;
CheckRemoteDebuggerPresent(GetCurrentProcess(), &bIsDebugged);
return bIsDebugged;
}
虽然不能百分百防住专业破解,但足以劝退大多数脚本小子。
操作日志:系统的“黑匣子”
出了问题怎么办?查日志!
我们建立了一个独立的日志系统,记录每一项敏感操作:
flowchart TD
A[用户执行操作] --> B{是否需记录?}
B -->|是| C[生成日志条目]
C --> D[填充时间/用户/IP等信息]
D --> E[持久化到 Log 表或独立文件]
E --> F[异步写入磁盘]
B -->|否| G[继续正常流程]
日志字段包括:
| 字段 | 示例 |
|---|---|
| 时间戳 | 2025-04-01 09:15:23 |
| 用户名 | admin |
| 操作类型 | 添加名片 / 删除 / 登录 |
| 目标对象 | 张伟 (ABC公司) |
| IP地址 | 192.168.1.102 |
| 成功状态 | 是 / 否 |
并通过专用界面供管理员查看、筛选、导出 CSV。一旦发现异常行为(如夜间批量导出),立刻警觉。
结语:传统技术也能焕发新生
你看,这套电子名片管理系统并没有用什么炫酷的新框架,也没有上云、AI、区块链,但它实实在在解决了用户的痛点: 高效、安全、易用地管理人际关系网络 。
MFC + Access 的组合或许“老旧”,但在特定场景下依然散发着独特的光芒。它的价值不在于多么先进,而在于 稳定可靠、低成本交付、易于维护 。
更重要的是,通过这样一个项目,你能深刻理解:
- 消息循环的本质是什么?
- 如何设计松耦合的三层架构?
- 数据库索引为何影响性能?
- 安全防护为何要层层设防?
这些经验,才是真正跨越时代的技术财富 💎。
所以别瞧不起“老古董”,有时候,正是它们教会我们什么是扎实的工程能力。🛠️
下一步你可以尝试:
- 把 Access 换成 SQLite,看看迁移难度;
- 增加二维码分享功能,实现“扫码加名片”;
- 添加 Outlook 同步接口,打通办公生态;
- 尝试用 WTL 替代 MFC,体验更轻量的 C++ GUI。
技术之路,永无止境。一起加油吧!💪✨
简介:电子名片管理系统是面向现代商务环境的信息管理工具,结合MFC框架与Microsoft Access数据库,构建了一个高效、用户友好的联系人信息存储与检索平台。系统具备登录验证、快速搜索和对话框交互等核心功能,支持名片信息的增删改查及复杂查询,提升了信息管理效率。本系统不仅满足实际应用需求,还可作为开发其他管理系统的可复用模板,具有良好的扩展性与实践价值。
更多推荐




所有评论(0)