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

Asp.net 开源市场框架 - BeYourMarket

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (34投票s)

2015 年 6 月 22 日

CPOL

7分钟阅读

viewsIcon

111877

用 BeYourMarket 打造您自己的市场。

引言

许多人对新的协作/共享经济感到兴奋,人们利用技术从彼此那里获取所需。这给一些行业带来了颠覆。

如果您想创建一个 Airbnb for X 或 TaskRabbit for Y 呢?在 .net 中,并没有太多框架/工具可以让您轻松地构建一个市场。本文将介绍一个开源框架 BeYourMarket,让您在几分钟内构建和定制自己的市场!

在本文结束时,您将拥有一个功能齐全的市场,可以找到您所在社区的美容和水疗服务提供商!

在线演示
http://demo.beyourmarket.com

此框架基于 ASP.NET MVC 5,因此您需要 MS Visual Studio 2013。此外,还需要 SQL Compact Server/SQL Server 2014 数据库。建议安装 Web Essentials 2013 和 Entity Framework Power Tools。

使用 Web Platform 运行演示

您可以在 5 分钟内通过 Web Platform 启动一个市场,BeYourMarket 已在 Web App Gallery 中列出:http://www.microsoft.com/web/gallery/beyourmarket.aspx

要安装 BeYourMarket,只需点击上述链接上的“安装”按钮并按照说明操作。

使用代码

代码可以从 github release 下载:https://github.com/beyourmarket/beyourmarket/releases

或者,也可以从 github 克隆解决方案。

在命令行中,

git clone https://github.com/beyourmarket/beyourmarket.git

该解决方案可以使用 Visual Studio 2013 打开,支持 .net 4.5。

编译解决方案并运行项目 **BeYourMarket.Web**。首次运行时,它将启动一个安装向导,您可以在其中指定管理员用户名/密码和数据库(SQL CE Compact 或 MS SQL Server)。

请记住勾选“安装示例数据”,这将创建一个示例市场——美容和水疗服务市场!

术语

市场是双边市场。一些用户会提供供应,一些用户会在特定类别和地点产生需求。在此示例中,它是您本地的美容和水疗服务。

此外,市场还将提供一种方式让服务接收者付款,以及让服务提供者收款。

市场本身需要一个社区经理来推广和维护社区。它确保服务质量并解决任何争议。它通常会在服务被预订时收取预订费。例如,Airbnb 在有人预订房间时会收取预订费。交易越多,利润就越多。

1. 服务提供商 - 提供服务/产品的用户;当有人使用/购买他们的服务时,他们将获得报酬。

2. 服务接收者 - 消费服务/产品的用户;他们将为服务/产品付费。

3. 社区经理 - 创建和管理市场的管理员,包括列表/订单/交易/用户。他/她还负责解决服务提供商和服务接收者之间的纠纷。

创建市场

要创建一个市场,请在管理员面板中配置设置。

BeYourMarket 提供一个管理员面板,社区经理可以在其中管理用户/订单/交易。

数据库结构

数据库结构非常简单明了。它使用 Entity Framework (EF) 和 Code-First,但也支持 Database-First。模型文件结构在项目 **BeYourMarket.Model** 中。

URF - Unit of Work & (可扩展/通用) Repositories Framework(参见参考)用于轻松扩展域(例如,新表/列)和数据库与应用程序之间的映射。

创建列表 (供应)

用户注册后,他们可以为他们的服务/产品创建列表。在此示例中,它是美容和水疗服务。

通常,一个列表包含 4 种信息:

1. 商品信息(名称/描述/价格...)
2. 地理位置信息(纬度/经度)
3. 照片
4. 自定义/额外信息

*自定义信息的格式可以在管理员面板中定义。

在 ListingController.cs 中,它负责获取/更新/删除列表。

ListingUpdate 方法处理新的或更新列表的请求。

[HttpPost]
public async Task<ActionResult> ListingUpdate(Item item, FormCollection form, IEnumerable<HttpPostedFileBase> files)
{
    bool updateCount = false;

    int nextPictureOrderId = 0;

    // Add new listing
    if (item.ID == 0)
    {
        item.ObjectState = Repository.Pattern.Infrastructure.ObjectState.Added;
        item.IP = Request.GetVisitorIP();
        item.Expiration = DateTime.MaxValue.AddDays(-1);
        item.UserID = User.Identity.GetUserId();

        updateCount = true;
        _itemService.Insert(item);
    }
    else
    {
        // Update listing
        var itemExisting = await _itemService.FindAsync(item.ID);

        itemExisting.Title = item.Title;
        itemExisting.Description = item.Description;
        itemExisting.CategoryID = item.CategoryID;

        itemExisting.Enabled = item.Enabled;
        itemExisting.Active = item.Active;
        itemExisting.Premium = item.Premium;

        itemExisting.ContactEmail = item.ContactEmail;
        itemExisting.ContactName = item.ContactName;
        itemExisting.ContactPhone = item.ContactPhone;

        itemExisting.Latitude = item.Latitude;
        itemExisting.Longitude = item.Longitude;
        itemExisting.Location = item.Location;

        itemExisting.ShowPhone = item.ShowPhone;
        itemExisting.ShowEmail = item.ShowEmail;

        itemExisting.UserID = item.UserID;

        itemExisting.Price = item.Price;
        itemExisting.Currency = item.Currency;

        itemExisting.ObjectState = Repository.Pattern.Infrastructure.ObjectState.Modified;

        _itemService.Update(itemExisting);
    }

    // Delete existing fields on item
    var customFieldItemQuery = await _customFieldItemService.Query(x => x.ItemID == item.ID).SelectAsync();
    var customFieldIds = customFieldItemQuery.Select(x => x.ID).ToList();
    foreach (var customFieldId in customFieldIds)
    {
        await _customFieldItemService.DeleteAsync(customFieldId);
    }

    // Get custom fields
    var customFieldCategoryQuery = await _customFieldCategoryService.Query(x => x.CategoryID == item.CategoryID).Include(x => x.MetaField.ItemMetas).SelectAsync();
    var customFieldCategories = customFieldCategoryQuery.ToList();

    // Update custom fields
    foreach (var metaCategory in customFieldCategories)
    {
        var field = metaCategory.MetaField;
        var controlType = (BeYourMarket.Model.Enum.Enum_MetaFieldControlType)field.ControlTypeID;

        string controlId = string.Format("customfield_{0}_{1}_{2}", metaCategory.ID, metaCategory.CategoryID, metaCategory.FieldID);

        var formValue = form[controlId];

        if (string.IsNullOrEmpty(formValue))
            continue;

        formValue = formValue.ToString();

        var itemMeta = new ItemMeta()
        {
            ItemID = item.ID,
            Value = formValue,
            FieldID = field.ID,
            ObjectState = Repository.Pattern.Infrastructure.ObjectState.Added
        };

        _customFieldItemService.Insert(itemMeta);
    }

    await _unitOfWorkAsync.SaveChangesAsync();

    // Update photos
    if (Request.Files.Count > 0)
    {
        var itemPictureQuery = _itemPictureService.Queryable().Where(x => x.ItemID == item.ID);
        if (itemPictureQuery.Count() > 0)
            nextPictureOrderId = itemPictureQuery.Max(x => x.Ordering);
    }

    foreach (HttpPostedFileBase file in files)
    {
        if ((file != null) && (file.ContentLength > 0) && !string.IsNullOrEmpty(file.FileName))
        {
            // Picture picture and get id
            var picture = new Picture();
            picture.MimeType = "image/jpeg";
            _pictureService.Insert(picture);
            await _unitOfWorkAsync.SaveChangesAsync();

            // Format is automatically detected though can be changed.
            ISupportedImageFormat format = new JpegFormat { Quality = 90 };
            Size size = new Size(500, 0);

            //https://naimhamadi.wordpress.com/2014/06/25/processing-images-in-c-easily-using-imageprocessor/
            // Initialize the ImageFactory using the overload to preserve EXIF metadata.
            using (ImageFactory imageFactory = new ImageFactory(preserveExifData: true))
            {
                var path = Path.Combine(Server.MapPath("~/images/item"), string.Format("{0}.{1}", picture.ID.ToString("00000000"), "jpg"));

                // Load, resize, set the format and quality and save an image.
                imageFactory.Load(file.InputStream)
                            .Resize(size)
                            .Format(format)
                            .Save(path);
            }

            var itemPicture = new ItemPicture();
            itemPicture.ItemID = item.ID;
            itemPicture.PictureID = picture.ID;
            itemPicture.Ordering = nextPictureOrderId;

            _itemPictureService.Insert(itemPicture);

            nextPictureOrderId++;
        }
    }

    await _unitOfWorkAsync.SaveChangesAsync();

    // Update statistics count
    if (updateCount)
    {
        _sqlDbService.UpdateCategoryItemCount(item.CategoryID);
        _dataCacheService.RemoveCachedItem(CacheKeys.Statistics);
    }

    return RedirectToAction("Listings");
}

服务预订 (需求)

一旦有了一些用户可以预订和支付的列表(服务/产品),我们就需要为这些请求创建订单。

PaymentController.cs 处理所有订单和支付请求。

Order 方法根据用户的请求创建订单;成功后,它会将用户重定向到支付页面。

public async Task<ActionResult> Order(Order order)
        {
            var item = await _itemService.FindAsync(order.ItemID);

            if (item == null)
                return new HttpNotFoundResult();

            // Check if payment method is setup on user or the platform
            var descriptors = _pluginFinder.GetPluginDescriptors<IHookPlugin>(LoadPluginsMode.InstalledOnly, "Payment").Where(x => x.Enabled);
            if (descriptors.Count() == 0)
            {
                TempData[TempDataKeys.UserMessageAlertState] = "bg-danger";
                TempData[TempDataKeys.UserMessage] = "[[[The provider has not setup the payment option yet, please contact the provider.]]]";

                return RedirectToAction("Listing", "Listing", new { id = order.ItemID });
            }

            foreach (var descriptor in descriptors)
            {
                var controllerType = descriptor.Instance<IHookPlugin>().GetControllerType();
                var controller = ContainerManager.GetConfiguredContainer().Resolve(controllerType) as IPaymentController;

                if (!controller.HasPaymentMethod(item.UserID))
                {
                    TempData[TempDataKeys.UserMessageAlertState] = "bg-danger";
                    TempData[TempDataKeys.UserMessage] = string.Format("[[[The provider has not setup the payment option for {0} yet, please contact the provider.]]]", descriptor.FriendlyName);

                    return RedirectToAction("Listing", "Listing", new { id = order.ItemID });
                }   
            }            

            if (order.ID == 0)
            {
                order.ObjectState = Repository.Pattern.Infrastructure.ObjectState.Added;
                order.Created = DateTime.Now;
                order.Modified = DateTime.Now;
                order.Status = (int)Enum_OrderStatus.Created;
                order.UserProvider = item.UserID;
                order.UserReceiver = User.Identity.GetUserId();

                if (order.UserProvider == order.UserReceiver)
                {
                    TempData[TempDataKeys.UserMessageAlertState] = "bg-danger";
                    TempData[TempDataKeys.UserMessage] = "[[[You cannot book the item from yourself!]]]";

                    return RedirectToAction("Listing", "Listing", new { id = order.ItemID });
                }

                if (order.ToDate.HasValue && order.FromDate.HasValue)
                {
                    order.Description = string.Format("{0} #{1} ([[[From]]] {2} [[[To]]] {3})",
                        item.Title, item.ID,
                        order.FromDate.Value.ToShortDateString(), order.ToDate.Value.ToShortDateString());

                    order.Quantity = order.ToDate.Value.Date.AddDays(1).Subtract(order.FromDate.Value.Date).Days;
                    order.Price = order.Quantity * item.Price;
                }
                else
                {
                    order.Description = string.Format("{0} #{1}", item.Title, item.ID);
                    order.Quantity = 1;
                    order.Price = item.Price;
                }

                _orderService.Insert(order);
            }

            await _unitOfWorkAsync.SaveChangesAsync();

            ClearCache();

            return RedirectToAction("Payment", new { id = order.ID });
        }

与 Stripe 的支付集成

市场中的服务/产品需要接收用户付款。 Stripe 提供了一个接收付款的协议。BeYourMarket 已集成 Stripe Connect API,因此用户可以轻松付款和收款(甚至使用比特币!)。如果需要,还可以通过在 BeYourMarket 中编写插件来集成其他支付 API,如 Braintree/PayPal Checkout Express。

在 Payment.cshtml 中,它嵌入了带有结账的 Stripe 表单。使用以下 HTML 代码,它将负责构建表单、验证输入以及保护用户的卡片数据。

需要注意的关键是 data-key 属性,它在与 Stripe 通信时标识您的网站。可发布 API 密钥可以在管理员面板中配置。

 <div class="form-group">
    <form class="form" action="@Url.Action("Payment")" method="post" role="form">
        <script src="https://checkout.stripe.com/checkout.js" class="stripe-button"
                data-key="@CacheHelper.GetSettingDictionary(Plugin.Payment.Stripe.StripePlugin.SettingStripePublishableKey).Value"
                data-image="https://stripe.com/img/documentation/checkout/marketplace.png"
                data-name="@CacheHelper.Settings.Name"
                data-description="@Model.Description"
                data-currency="@CacheHelper.Settings.Currency"
                data-amount="@Model.PriceInCents">

        </script>
    </form>
</div>

一旦用户用信用卡付款,Stripe 将生成一个安全令牌并返回,该令牌可用于向他们收费。在市场中,服务提供商通常需要在交易完成前确认其可用性并接受报价。因此,我们可以保存令牌并创建一个稍后捕获的交易。

在 PaymentController.cs 中,Payment 方法处理用户输入有效卡号并点击付款后 Stripe 的回调。它包含一个安全令牌以及 orderid。

[HttpPost]

public async Task<ActionResult> Payment(int id, string stripeToken, string stripeEmail)
        {
            var selectQuery = await _orderService.Query(x => x.ID == id).Include(x => x.Item).SelectAsync();

            // Check if order exists
            var order = selectQuery.FirstOrDefault();
            if (order == null)
                return new HttpNotFoundResult();

            var stripeConnectQuery = await _stripConnectService.Query(x => x.UserID == order.UserProvider).SelectAsync();
            var stripeConnect = stripeConnectQuery.FirstOrDefault();

            if (stripeConnect == null)
                return new HttpNotFoundResult();

            //https://stripe.com/docs/checkout
            var charge = new StripeChargeCreateOptions();

            // always set these properties
            charge.Amount = order.PriceInCents;
            charge.Currency = CacheHelper.Settings.Currency;
            charge.Source = new StripeSourceOptions()
            {
                TokenId = stripeToken
            };

            // set booking fee
            var bookingFee = (int)Math.Round(CacheHelper.Settings.TransactionFeePercent * order.PriceInCents);
            if (bookingFee < CacheHelper.Settings.TransactionMinimumFee * 100)
                bookingFee = (int)(CacheHelper.Settings.TransactionMinimumFee * 100);

            charge.ApplicationFee = bookingFee;
            charge.Capture = false;
            charge.Description = order.Description;
            charge.Destination = stripeConnect.stripe_user_id;
            var chargeService = new StripeChargeService(CacheHelper.GetSettingDictionary("StripeApiKey").Value);
            StripeCharge stripeCharge = chargeService.Create(charge);

            // Update order status
            order.Status = (int)Enum_OrderStatus.Pending;
            order.PaymentPlugin = StripePlugin.PluginName;
            _orderService.Update(order);

            // Save transaction
            var transaction = new StripeTransaction()
            {
                OrderID = id,
                ChargeID = stripeCharge.Id,
                StripeEmail = stripeEmail,
                StripeToken = stripeToken,
                Created = DateTime.Now,
                LastUpdated = DateTime.Now,
                FailureCode = stripeCharge.FailureCode,
                FailureMessage = stripeCharge.FailureMessage,
                ObjectState = Repository.Pattern.Infrastructure.ObjectState.Added
            };

            _transactionService.Insert(transaction);

            await _unitOfWorkAsync.SaveChangesAsync();
            await _unitOfWorkAsyncStripe.SaveChangesAsync();

            ClearCache();

            // Payment succeeded
            if (string.IsNullOrEmpty(stripeCharge.FailureCode))
            {
                TempData[TempDataKeys.UserMessage] = "[[[Thanks for your order! You payment will not be charged until the provider accepted your request.]]]";
                return RedirectToAction("Orders", "Payment");
            }
            else
            {
                TempData[TempDataKeys.UserMessageAlertState] = "bg-danger";
                TempData[TempDataKeys.UserMessage] = stripeCharge.FailureMessage;

                return RedirectToAction("Payment");
            }
        }

接受付款并收取交易费

一旦服务提供商通过仪表板接受了用户的请求。交易就需要被捕获。资金将从用户的信用卡中扣除,服务提供商将获得报酬并提供服务,社区经理将获得预订费。皆大欢喜!

在 PaymentController.cs 中,OrderAction 处理服务提供商接受或拒绝请求的订单操作。如果他/她接受,它将使用 Stripe API 创建一个收费并捕获付款。

[HttpPost]

public async Task<ActionResult> OrderAction(int id, int status)
        {
            var order = await _orderService.FindAsync(id);

            if (order == null)
                return new HttpNotFoundResult();

            var descriptor = _pluginFinder.GetPluginDescriptorBySystemName<IHookPlugin>(order.PaymentPlugin);
            if (descriptor == null)
                return new HttpNotFoundResult("Not found");

            var controllerType = descriptor.Instance<IHookPlugin>().GetControllerType();
            var controller = ContainerManager.GetConfiguredContainer().Resolve(controllerType) as IPaymentController;

            string message = string.Empty;
            var orderResult = controller.OrderAction(id, status, out message);

            var result = new
            {
                Success = orderResult,
                Message = message
            };

            return Json(result, JsonRequestBehavior.AllowGet);
        }

沟通

BeYourMarket 具有消息框系统,允许服务提供商和接收者相互通信。

当用户首次发起对话时,将创建一个消息线程。如果用户之前有过对话(MessageParticipanets),将使用相同的消息线程。每个消息线程包含一条消息列表。每条消息都有一个已读或未读的状态。

    public partial class MessageThread : Repository.Pattern.Ef6.Entity
    {
        public MessageThread()
        {
            this.Messages = new List<Message>();
            this.MessageParticipants = new List<MessageParticipant>();
        }

        public int ID { get; set; }
        public string Subject { get; set; }
        public Nullable<int> ListingID { get; set; }
        public System.DateTime Created { get; set; }
        public System.DateTime LastUpdated { get; set; }
        public virtual Listing Listing { get; set; }
        public virtual ICollection<Message> Messages { get; set; }
        public virtual ICollection<MessageParticipant> MessageParticipants { get; set; }
    }

每个消息线程都与每个服务提供商和服务接收者相关联,也可以与特定列表(服务或产品)相关联。如果消息未读,顶部导航栏将显示一个通知图标。

评论和评分

服务完成后,服务提供商和服务接收者都有机会互相提供反馈和评分。这样,其他用户就可以查看有关服务或产品的评论,以及它们是否好。

国际化

该平台的设计支持多语言,并易于进行 i18n。使用了基于 GetText / PO 生态系统的 ASP.NET 智能国际化(参见参考)。您唯一需要做的就是将其翻译成您自己的语言。

要本地化应用程序中的文本,请将字符串用 [[[ 和 ]]] 标记字符括起来,以将其标记为可翻译。

以下是在 Razor 视图中本地化文本“创建账户”和“登录”的示例:

<ul class="nav navbar-nav">
    <li class="dropdown messages-menu hidden-xs">
        @Html.ActionLink("[[[Create an account]]]", "Register", "Account", new { area = string.Empty }, htmlAttributes: new { id = "registerLink" })
    </li>
    <li class="dropdown messages-menu hidden-xs">
        @Html.ActionLink("[[[Log in]]]", "Login", "Account", new { area = string.Empty }, htmlAttributes: new { id = "loginLink" })
    </li>
</ul>

模板文件位于解决方案构建后的 locale/messages.pot。

结论

最后,我们将看到如何使用 asp.net mvc 构建一个美容和水疗市场。协作消费是一种趋势,越来越多的传统企业将转变为市场业务。

技术需要易于定制和扩展,以缩短产品上市时间。BeYourMarket 的初衷是创建一个开源框架,让开发人员能够轻松帮助企业构建和定制市场。它也应该易于扩展其他功能和组件。

许可证

BeYourMarket 已获得 MIT 许可。

参考

BeYourMarket - 一个开源 asp.net 市场 - http://beyourmarket.com

BeYourMarket 文档 - https://beyourmarket.atlassian.net/wiki/display/BYM/BeYourMarket

ASP.NET 智能国际化 - https://github.com/turquoiseowl/i18n

URF - Unit of Work & (可扩展/通用) Repositories Framework - https://genericunitofworkandrepositories.codeplex.com/

历史

2015 年 9 月 3 日 - 更新了数据库,添加了通信和评论/评分部分

2015 年 7 月 21 日 - 更新了插件架构支持和数据库图

2015 年 6 月 20 日 - 介绍开源 asp.net 市场框架

© . All rights reserved.