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

Fundraiser Focus - 一个 D&B 项目

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (4投票s)

2013年7月28日

CPOL

16分钟阅读

viewsIcon

19608

downloadIcon

345

帮助非营利组织精准开展募捐活动。

介绍 

你是否曾尝试向完全陌生的人为你的事业募捐?结果如何?如果你随机问100个人,你认为会有多少人捐款?1%?5%?现在,如果你问的不是随机的人,而是对你事业类型感兴趣的人,情况又会如何?你认为这个比例会更高吗?当然会。这就是 Fundraiser Focus 提供的功能。

概述

Fundraiser Focus 是一款基于网络的应用程序,用于智能地定位企业和商业领袖以获取捐款。可以指定搜索参数,识别最有可能回应特定请求的组织和领导者,从而通过提高成功几率来提高投资回报率(ROI)。准备与特定商业领袖会面的用户,还可以在会面前调出完整的公司报告,以便熟悉公司情况。 

实时站点 

您可以在 http://fundraiserfocus.azurewebsites.net 找到 Fundraiser Focus。请随意使用演示数据随意尝试。您的数据仅存储在内存中,以便为下一位用户保持清洁。   

应用程序概念

Fundraiser Focus 的概念是非营利组织在募捐方面的资源有限。他们用于募捐的钱越多,为事业留下的钱就越少。Fundraiser Focus 通过提供有针对性的组织名单来寻求捐款,从而改善这种情况。例如,如果你正在做一个地球日活动,那些获得绿色认证的公司更有可能与你的事业产生共鸣。甚至可能不止一个团体是你想要联系的。例如,一项旨在消除贫困的活动可能会引起劳动过剩地区、Hub Zones 等地区公司的兴趣。作为一个组织,你可能会根据你正在联系的组织群体采取不同的策略。

Fundraiser Focus 可以轻松处理所有这些。获取信息的主要方法是活动(campaign)。这是你的总体募捐活动。在一个活动中,你可以有一个或多个搜索。每个搜索代表一个目标群体。当你处理一个活动时,会为每个搜索生成匹配项。为列表中的每个公司生成基本的公司联系信息。但是,一旦用户选择联系特定公司,他们可能会觉得需要更多关于该公司的信息。他们可以通过请求公司研究报告来获取这些信息。这份报告包含了详细的数据,重点关注重要的注意事项,包括破产、留置权和年收入。

应用程序演练 

Fundraiser Focus 网站是一个功能齐全的网站,设置得就像一家实际公司一样。当你访问登陆页面时,你将获得该软件功能概述,就像你是一位潜在客户一样。

所有页面都已设置好,并且对于查找有关服务的更多信息都很有用。但是,关于所有代码都能做什么的最简洁信息可以在定价页面找到,该页面简要解释了每项服务。

然而,本网站(以及本次 D&B 大赛参赛作品)的目的是在演示页面上。该页面演示了 Fundraiser Focus 的客户在注册账户时将获得的体验。方便的是,这也允许我展示应用程序的功能,而无需人们创建登录。如果我要将这个变成实际服务,我会更严格地限制演示页面,因为这可能会产生巨额账单,但由于使用的是沙盒数据,所以没问题。

当你最初访问演示页面时,你会看到演示帮助文本。该页面为你提供了一些关于如何查找数据的建议。在此需要注意的是,此页面预先填充了 D&B 的数据。这些数据已被下载并存储以供以后检索。我们每次都可以访问实时数据,但我决定不为相同的测试数据每次都访问服务器。当你创建新活动或新公司研究报告时,数据将直接从 D&B 中提取。此功能还允许你测试创建新记录,而不会改变下一个用户的演示。你的更改将存储在内存中,并在你离开时被清除。

你可以深入了解的第一个地方是活动信息。选择现有活动下的某个搜索结果,你应该会看到类似这样的内容。

 

如你所见,每个搜索都有一个简短的描述,解释搜索结果及其用途。这些信息由生成活动的人员指定。接下来,你将看到用于生成搜索结果的标准。最后,你将看到一个与搜索条件匹配的组织列表。如果你点击一家公司,该条目将展开显示该组织的联系信息。在联系信息底部有一个生成特定公司研究报告的链接。当你点击它时,你将被带到报告。

公司研究报告会告知查看者公司有哪些重要信息。它提供了公司的基本概述,包括联系信息、领导者信息和其他相关细节。它还重点介绍了可能表明公司存在问题的,例如未偿留置权或破产情况。

回到活动,你可能想尝试设置自己的活动(毕竟这是演示的目的)。为此,请点击“活动”标题旁边的加号按钮,进入新活动设置屏幕: 

在这里,你可以创建一项或多项有助于你的活动目标的搜索。你会注意到“公司研究”标题旁边也有一个加号按钮。如果你想通过 DUNS 号码查找公司,而不是通过搜索结果中的链接点击,请点击加号按钮并输入。生成的报告将像任何其他结果一样列出。 

应用程序技术

Fundraiser Focus 网站主要使用以下技术构建:

  • D&B 数据(通过 OData)
  • C#
  • ASP.NET MVC
  • JavaScript
  • Knockout
  • Windows Azure
  • Twitter Bootstrap 

网站托管在 Windows Azure 上。数据也来自 Windows Azure。为了获得最佳的网站性能,处理演示页面的控制器有几个 ActionResult 返回 JSON 数据而不是视图。原因是,这样演示页面就可以利用 AJAX 调用,而不是耗时较长的完整页面刷新。这也能让网站具有桌面应用程序的感觉。Knockout 完善了这些技术,提供了双向数据绑定,以便连接 AJAX 调用返回的数据。

关键代码部分

好了,够了市场宣传和销售说辞。真正有趣的部分是代码,对吧?还是只有我觉得是这样?Fundraiser Focus 是一个相当标准的 ASP.NET MVC Web 应用程序,但与其他任何应用程序一样,它也有一些棘手的代码片段。让我们一起回顾一下,以便我们都能从我发现的东西(和我的错误)中学习。

ASP.NET MVC

每当你创建一个 ASP.NET MVC(以下简称“MVC”)应用程序时,都需要决定一些基础问题,然后再继续前进。我的第一个决定是如何处理路由。我想要一个漂亮的 URL 结构,并且我想要保持设计的灵活性。我的第一个想法是将所有操作都放在一个控制器中。这让我能够保持事情的简单性,因为我的大多数操作都只是返回适当的视图。问题是我的 URL 结构会是这样的:

  • /Home/Features
  • /Home/AboutUs
  • /Home/FAQ

这并不是一个非常直观的 URL 结构。当然,对此有一个解决方案。我在 RouteConfig.cs 文件中创建了一个新路由,并将其添加到默认路由之前。新路由如下所示:

routes.MapRoute(
   name: "CleanURLs",
   url: "{action}",
   defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
); 

这个路由的作用是查看 URL,如果网站根目录后只有一个项,它就会将该项作为操作。它将控制器设置为 Home 控制器。现在我们的路由看起来像这样:

  • /Features
  • /AboutUs
  • /FAQ

这更直观,但它使得一个控制器内容很多。当我创建演示页面时,这也带来了棘手的部分,但可以通过创建一个新的控制器专门用于演示来解决。由于我保留了默认路由,我仍然可以使用其他控制器。但最终,我决定网站的未来扩展需要更强大的设计。因此,我为每个页面创建了一个控制器,并将我的路由恢复到标准的 MVC 路由集。这使我能够保持漂亮的 URL 方案,但也为我提供了未来扩展的可能性,例如:

  • /Features
  • /Features/Campaigns
  • /Features/BusinessReports
  • /AboutUs
  • /AboutUs/Tim
  • /FAQ
  • /FAQ/General
  • /FAQ/Billing

需要克服的下一个障碍是如何最好地将加载的数据从服务器端传递到客户端。最简单的方法,也是典型的 MVC 方法,是通过 ViewModel 直接将模型传递给视图。虽然这样做很简单,但它强制进行完整的页面刷新。我决定一个更好的解决方案是将数据作为 JSON 传递给主视图。这样我就可以通过 AJAX 调用来使用它,并避免了完整的页面刷新。为了做到这一点,我创建了几个操作来返回 JSON 而不是视图,如下所示:

public ActionResult GenerateCompanyReport(string dunsNumber)
{
   DnB data = new DnB();
   Models.DashboardModel model = new Models.DashboardModel();
   return Json(data.GetCompanyByDuns(dunsNumber), JsonRequestBehavior.AllowGet);
} 

请注意,我也允许 GET 调用访问此操作。稍后在 Knockout 部分我们将回到客户端。

C#

要获取 D&B 的数据,我通过服务引用进行了连接。这使得连接到数据并查询特定条目变得容易。例如,要按 DUNS 号码获取特定公司的人口统计数据,我可以使用这一行代码:

var demographic = context.Demographics.Where(x => x.DUNSNumber == duns); 

简单吧?现在 demographic 变量包含了我需要的所有人口统计记录(在本例中只有一个)。但是,当我想查询特定数据集时,情况会变得复杂一些。例如,我有一系列复选框,用户可以选中以指示他们是否希望使用某些标准作为过滤器的一部分。只有在勾选了某个项目时,我才需要对其进行过滤,并且我不想对同一数据运行两次查询。我最终想出的代码如下: 

var veteran = from orgs in context.Veteran
                where (orgs.VietnamVeteranOwnedIndicator == "Y" || 
                    orgs.VietnamVeteranOwnedIndicator == criteria.IsVietnamVeteranOwned.ToYOrBlank())
                where (orgs.ServiceDisabledVeteranOwnedIndicator == "Y" || 
                    orgs.ServiceDisabledVeteranOwnedIndicator == criteria.IsServiceDisabledVeteranOwned.ToYOrBlank())
                where (orgs.DisadvantagedVeteranEnterpriseIndicator == "Y" || 
                    orgs.DisadvantagedVeteranEnterpriseIndicator == criteria.IsDisadvantagedVeteranEnterprise.ToYOrBlank())
                select orgs; 

请注意,我使用的是一个名为 `ToYOrBlank` 的自定义辅助方法。这是一个布尔重载,它将结果值转换为“Y”或空(这是该列的两个可能值)。 这是代码:

public static string ToYOrBlank(this bool input)
{
    string output = string.Empty;
    if (input)
    {
        output = "Y";
    }
    else
    {
        output = null;
    }
    return output;
} 

现在如果我们回头看 linq 语句,它应该会更容易理解。我从 veterans 列表中获取所有项目,并根据一系列配对值进行过滤。每个配对都是“Y”值或布尔变量的转换。这样,如果勾选了该项目,它将是“Y”或“Y”(仅那些为真的项目), whereas 如果未勾选该框,它将是“Y”或空(这将是每个项目)。这使得我可以轻松地根据复选框过滤项目,而无需在设计时进行更复杂的逻辑。由于这是在构建 linq 查询时,这似乎是最合理的。最终状态是输出此数据集到列表。

这让我们遇到了下一个问题:超过 100 条记录。如果查询返回的记录超过 100 条,则只返回前 100 条(想想一个查询,在现实世界而不是我们的沙盒中,它会返回 1,000,000 条记录 - 你不希望一次性将所有记录都通过网络传输)。那么,如何获取所有记录呢?这听起来像是个递归的任务。这是我的最终代码(where 语句已被移除以便阅读):

private List<Veteran> VeteranLookup(SearchCriteriaModel criteria, int startPosition = 0)
{
    var veteran = from orgs in context.Veteran
                    select orgs;
    List<Veteran> output = new List<Veteran>();
    output = veteran.Skip(startPosition).ToList();
    if (output.Count == 100)
    {
        output.AddRange(VeteranLookup(criteria, startPosition + 100));
    }
    return output;
} 

看到如果当前数据集的数量超过 100,该方法是否会再次被调用,并且从该调用返回的记录是否会被添加到输出结果中。此方法的结果是,无论你获得 10 条记录还是 10,000 条记录,你都可以返回所有匹配的行。我使用 skip 操作符和开始位置指示器来告诉我应该在结果中去哪里。 

我需要克服的最后一个主要障碍是如何以一种不包含重复项的方式将所有这些数据合并在一起。幸运的是,Linq 是一个强大的工具,可以使这项工作比其他方式简单得多。我首先查看每组数据(退伍军人、少数族裔等),看看是否需要按该类别过滤数据。如果需要,我将过滤数据并取回结果。然后,我查看返回的结果是否是我的第一个结果集,或者是否没有记录(因为如果我们按数据过滤并且没有返回结果,那么我们应该没有任何记录)。这看起来是这样的:

if (criteria.IsMinority)
{
    minorities = MinorityLookup(criteria);
    if (!searchedData || minorities.Count == 0)
    {
        output = (from model in minorities
                    select new BasicCompanyModel
                    {
                        DunsNumber = model.DUNSNumber,
                        CompanyName = model.Company,
                        Address = model.Address,
                        City = model.City,
                        StateAbbrv = model.StateAbbrv,
                        ZipCode = model.ZipCode,
                        Phone = model.Phone,
                        Fax = model.Fax
                    }).ToList();
    }
    searchedData = true;
} 

我对每个数据集完成之后,就开始合并。首先,我检查以确保 `searchData` 变量为 true(这意味着至少有一个过滤器成功应用),并且输出中有记录(因为如果没有,我们就无需尝试合并任何内容 - 我们无论如何都会返回一个空记录集)。在该语句中,我检查每个组是否应合并记录。如果有,我会对输出和当前组执行 Linq 查询,如下所示:

if (minorities.Count > 0)
{
    output = (from curr in output
                join adds in minorities on curr.DunsNumber equals adds.DUNSNumber
                select new BasicCompanyModel
                {
                    DunsNumber = curr.DunsNumber,
                    CompanyName = curr.CompanyName,
                    Address = curr.Address,
                    City = curr.City,
                    StateAbbrv = curr.StateAbbrv,
                    ZipCode = curr.ZipCode,
                    Phone = curr.Phone,
                    Fax = curr.Fax
                }).ToList();
} 

请注意,我正在对结果进行内连接。原因是,我需要找到出现在每个过滤分组中的公司。例如,如果我正在寻找拥有少数族裔的公司,我不仅仅想要绿色公司。它们必须同时满足这两个条件才能被选中。另请注意,我将结果输出到 `BasicCompanyModel` 列表。然后,我可以直接将最终输出变量传递回调用方法,而无需进一步转换。如果你对我是如何做到这一点的仍有疑问,我鼓励你去查看 `DnB.cs` 文件,以便查看整个 `GetSearchResults` 方法。 

Knockout

我决定使用 Knockout 来处理客户端数据绑定。这使得 Web 应用程序更具“桌面”应用程序的感觉。Knockout 帮助我解决的第一个难题是如何一次性获取 D&B 的数据,然后将其存储以备将来使用(这样我们就不必每次都下载相同的初始演示数据)。我通过将页面的 JSON 结果保存到文本文件来解决这个问题。然后,一旦页面准备好,我就使用 AJAX 从文本文件中加载数据,然后进行数据绑定,如下所示: 

$.ajax({
    type: "GET",
    url: 'data/dashboard.txt',
    cache: true,
    dataType: "json",
    contentType: "json",
    success: function (response) {
        ko.applyBindings(new demoJS.vm(response));
    },
    error: function (response) {
        alert('An error has occured while processing your request');
    }
});  

`ko.applybindings` 行是我连接 Knockout 的地方。我正在使用映射插件来让我的生活更轻松。它接收我的数据,并通过添加 `observable` 或 `observablearray` 函数来包装数据,以提供必要的功能。 

Knockout(尤其是映射插件)帮助我解决的下一个问题是当我返回一个新活动时。请记住,我的 Demo 控制器在收到活动请求时发送 JSON 数据。我遇到的问题是,由于这是一个如此嵌套的数组和项,因此很难正确地进行映射以将其推送到现有的活动数组中。我通过再次使用映射器解决了这个问题,如下所示:

$.ajax({
    type: "POST",
    url: '/Demo/ProcessNewCampaign',
    data: campData,
    cache: false,
    dataType: "json",
    contentType: "application/json; charset=utf-8",
    success: function (response) {
        var newData = {};
        ko.mapping.fromJS(response, {}, newData);
        self.Campaigns.push(newData);
        self.currentSearch(newData.Searches()[0]);
        self.makeSearchVisible();
    },
    error: function (response) {
        toastr.error('There was an error processing your campaign. Please check the data and try again.', 'Process Campaign Error');
    }
});  

正如你所看到的,我创建了一个新的数组变量,然后将响应数据映射到这个新变量中。这会在我的数据上放置适当的 `observable` 和 `observablearray` 函数,这样当我将它推送到现有数组时,它就会完美匹配。 

如果你查看上面的代码,你还会注意到成功事件中的最后两行。我将当前搜索项设置为返回数据的第一个结果,并将搜索项设置为可见项。最终用户看到的结果是,新活动数据会自动显示在屏幕上。这为我们想要的流畅、实时的应用程序增添了光彩。 

Knockout 和 JavaScript 总体上帮助我克服的下一个难题是将返回的真值(truthy values)转换为绿色复选标记,反之亦然,将假值(falsey values)转换为红色叉号。为了做到这一点,我定义了如下 HTML(我使用的是 Twitter Bootstrap 和 font-awesome):

<i data-bind="attr: { class: yesNoIcon(currentCompany().IsMinority) }"></i> 

这会将类设置为 `yesNoIcon` 函数的结果,并将 `IsMinority` 的值传递进去。所以,让我们看一下那个函数:

self.yesNoIcon = function (indicator) {
    if (typeof indicator !== "undefined") {
        if (indicator === 'Y' || indicator === 'B') {
            return 'icon-ok green';
        } else {
            return 'icon-remove red';
        }
    } else {
        return 'icon-remove red';
    }
};  

明白了?  基本上,我首先检查以确保指示器(传递的值)不是未定义的。接下来,我检查该值是 Y 还是 B。原因是数据不一致(就我的目的而言)。对于某些数据列,Y 表示肯定,但对于破产列,B 表示肯定指示符。任何其他值都会得到否定类。我说类是因为你可能会注意到实际上返回了两个类。第一个是要显示的图标类型。第二个是该图标的颜色。Knockout 可以一次添加这两个类(我有点惊讶)。 

结论 

这就是 Fundraiser Focus。我着手开发一个应用程序,以帮助非营利组织为他们的项目筹集资金。我相信,在 D&B 沙盒数据的帮助下,我已经完成了这项任务。我的代码已附在此文章中(不包括我的 CSS 文件,我无权分发)。请随意查看网站和代码。我很乐意回答你的任何问题。  

变更日志 

2013年7月29日 

  • 初始发布 
 2013年7月31日 

  • 将净资产和年收入格式化为货币
  • 在研究新公司时将焦点设置在输入框
  • 将警告格式化为红色框,并在不适用时隐藏
  • 显示新结果时滚动到页面顶部
  • 在搜索结果中添加了“下载结果”按钮(IE 支持有限 - 目前在 Chrome 中效果最佳)
  • 允许单击“添加搜索条件”部分旁边的复选框标签
  • 在新的公司研究报告框中输入 DUNS 号码后按 Enter 键现在可以正确提交表单 

© . All rights reserved.