Asp.net 开源市场框架 - BeYourMarket






4.97/5 (34投票s)
用 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 市场框架