Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions SimplCommerce.sln
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimplCommerce.Module.Paymen
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimplCommerce.Module.PaymentCashfree", "src\Modules\SimplCommerce.Module.PaymentCashfree\SimplCommerce.Module.PaymentCashfree.csproj", "{E30CF10F-FABF-4917-8BEB-CB81E4CE2C92}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimplCommerce.Module.ProductsRecommendation", "src\Modules\SimplCommerce.Module.ProductsRecommendation\SimplCommerce.Module.ProductsRecommendation.csproj", "{4396E1FF-C5A8-4880-8140-F0F079406D75}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -738,6 +740,18 @@ Global
{E30CF10F-FABF-4917-8BEB-CB81E4CE2C92}.Release|x64.Build.0 = Release|Any CPU
{E30CF10F-FABF-4917-8BEB-CB81E4CE2C92}.Release|x86.ActiveCfg = Release|Any CPU
{E30CF10F-FABF-4917-8BEB-CB81E4CE2C92}.Release|x86.Build.0 = Release|Any CPU
{4396E1FF-C5A8-4880-8140-F0F079406D75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4396E1FF-C5A8-4880-8140-F0F079406D75}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4396E1FF-C5A8-4880-8140-F0F079406D75}.Debug|x64.ActiveCfg = Debug|Any CPU
{4396E1FF-C5A8-4880-8140-F0F079406D75}.Debug|x64.Build.0 = Debug|Any CPU
{4396E1FF-C5A8-4880-8140-F0F079406D75}.Debug|x86.ActiveCfg = Debug|Any CPU
{4396E1FF-C5A8-4880-8140-F0F079406D75}.Debug|x86.Build.0 = Debug|Any CPU
{4396E1FF-C5A8-4880-8140-F0F079406D75}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4396E1FF-C5A8-4880-8140-F0F079406D75}.Release|Any CPU.Build.0 = Release|Any CPU
{4396E1FF-C5A8-4880-8140-F0F079406D75}.Release|x64.ActiveCfg = Release|Any CPU
{4396E1FF-C5A8-4880-8140-F0F079406D75}.Release|x64.Build.0 = Release|Any CPU
{4396E1FF-C5A8-4880-8140-F0F079406D75}.Release|x86.ActiveCfg = Release|Any CPU
{4396E1FF-C5A8-4880-8140-F0F079406D75}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -795,6 +809,7 @@ Global
{1A8B6FA0-8341-4D27-9B71-57F70AB37571} = {0A27C140-4CCB-40DD-BE48-F5DE16D1177B}
{14586564-62CC-4117-AC1B-858ED53C2D6C} = {7EFA2FA7-32DD-4047-B021-50E77A83D714}
{E30CF10F-FABF-4917-8BEB-CB81E4CE2C92} = {7EFA2FA7-32DD-4047-B021-50E77A83D714}
{4396E1FF-C5A8-4880-8140-F0F079406D75} = {7EFA2FA7-32DD-4047-B021-50E77A83D714}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B9D0D8F0-1AB9-44DD-839F-ED8CEE7DDB10}
Expand Down
1 change: 1 addition & 0 deletions src/Database/StaticData-DefaultLocalization.sql
Original file line number Diff line number Diff line change
Expand Up @@ -380,5 +380,6 @@ INSERT [dbo].[Localization_Resource] ([CultureId], [Key], [Value]) VALUES ('en-U
INSERT [dbo].[Localization_Resource] ([CultureId], [Key], [Value]) VALUES ('en-US', N'Your review will be showed within the next 24h.', N'Your review will be showed within the next 24h.');
INSERT [dbo].[Localization_Resource] ([CultureId], [Key], [Value]) VALUES ('en-US', N'Your review will be shown within the next 24h.', N'Your review will be shown within the next 24h.');
INSERT [dbo].[Localization_Resource] ([CultureId], [Key], [Value]) VALUES ('en-US', N'Zip Code', N'Zip Code');
INSERT [dbo].[Localization_Resource] ([CultureId], [Key], [Value]) VALUES ('en-US', N'Recommend Products', N'Recommend Products');

GO
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ private void MapProductOptionToProductVm(Product product, ProductDetail model)

private void MapProductVariantToProductVm(Product product, ProductDetail model)
{
if(!product.ProductLinks.Any(x => x.LinkType == ProductLinkType.Super))
if (!product.ProductLinks.Any(x => x.LinkType == ProductLinkType.Super))
{
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ into g

public IList<ProductThumbnail> RelatedProducts { get; set; } = new List<ProductThumbnail>();

public IList<ProductThumbnail> RecommendProducts { get; set; } = new List<ProductThumbnail>();

public IList<ProductThumbnail> CrossSellProducts { get; set; } = new List<ProductThumbnail>();

public Brand Brand { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
}
</div>
</div>
@foreach(var variant in Model.Variations)
@foreach (var variant in Model.Variations)
{
if (variant.Images.Any())
{
Expand All @@ -57,7 +57,7 @@
}
</div>
</div>
}
}
}
</div>
<div class="col-md-6">
Expand Down Expand Up @@ -295,6 +295,8 @@

@await Component.InvokeAsync("ProductRecentlyViewed", new { productId = Model.Id })

@await Component.InvokeAsync("RecommendedProducts", new { productId = Model.Id })

<div>
<h3>@Localizer["Customer reviews"]</h3>
@await Component.InvokeAsync("Review", new { entityId = Model.Id, entityTypeId = "Product" })
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SimplCommerce.Infrastructure.Data;
using SimplCommerce.Infrastructure.Web;
using SimplCommerce.Module.Catalog.Areas.Catalog.ViewModels;
using SimplCommerce.Module.Catalog.Models;
using SimplCommerce.Module.Catalog.Services;
using SimplCommerce.Module.Core.Extensions;
using SimplCommerce.Module.Core.Services;
using SimplCommerce.Module.ProductsRecommendation.Models;

namespace SimplCommerce.Module.ProductsRecommendation.Areas.ProductsRecommendation.Components
{
public class RecommendedProductsViewComponent : ViewComponent
{
private readonly IRepository<Product> _productRepository;
private readonly IMediaService _mediaService;
private readonly IProductPricingService _productPricingService;
private readonly IWorkContext _workContext;
private readonly IContentLocalizationService _contentLocalizationService;
private readonly IRecommendationService _recommendationService;

public RecommendedProductsViewComponent(
IRepository<Product> productRepository,
IMediaService mediaService,
IProductPricingService productPricingService,
IContentLocalizationService contentLocalizationService,
IWorkContext workContext,
IRecommendationService recommendationService)
{
_productRepository = productRepository;
_mediaService = mediaService;
_productPricingService = productPricingService;
_contentLocalizationService = contentLocalizationService;
_workContext = workContext;
_recommendationService = recommendationService;
}

// TODO Number of items to config
public async Task<IViewComponentResult> InvokeAsync(long? productId, int itemCount = 4)
{
IList<ProductThumbnail> RecommendProducts = new List<ProductThumbnail>();

var topItems = (from m in _productRepository.Query().Where(x => x.IsPublished && x.IsVisibleIndividually && x.Id != productId)
.Include(x => x.OptionValues)
.Include(x => x.ThumbnailImage)
.Include(x => x.Medias).ThenInclude(m => m.Media)
.AsEnumerable()
let p = _recommendationService.Predict(
new ProductInfo()
{
ProductID = (float)productId,
CombinedProductID = (uint)m.Id
})
orderby p.Score descending
select (Product: m, Score: p.Score)).Take(itemCount);

foreach (var item in topItems)
{
var productThumbnail = ProductThumbnail.FromProduct(item.Product);
productThumbnail.Name = _contentLocalizationService.GetLocalizedProperty(nameof(Product), productThumbnail.Id, nameof(productThumbnail.Name), productThumbnail.Name);
productThumbnail.ThumbnailUrl = _mediaService.GetThumbnailUrl(productThumbnail.ThumbnailImage);
productThumbnail.CalculatedProductPrice = _productPricingService.CalculateProductPrice(productThumbnail);
RecommendProducts.Add(productThumbnail);
}
return View(this.GetViewPath(), RecommendProducts);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@model IList<SimplCommerce.Module.Catalog.Areas.Catalog.ViewModels.ProductThumbnail>

@if (Model.Any())
{
<h2 class="page-header">@Localizer["Recommend Products"]</h2>
<div class="row product-list">
@foreach (var product in Model)
{
<div class="col-xs-6 col-md-3">
<partial name="/Areas/Catalog/Views/Shared/_ProductThumbnail.cshtml" model="product" />
</div>
}
</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.Extensions.Configuration
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper "*, SimplCommerce.Module.Core"
@addTagHelper "*, cloudscribe.Web.Pagination"

@inject IViewLocalizer Localizer
@inject IConfiguration AppSetting
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace SimplCommerce.Module.ProductsRecommendation.Common
{
public static class Helper
{
private static IEnumerable<int[]> CombinationsRosettaWoRecursion(int m, int n)
{
int[] result = new int[m];
Stack<int> stack = new Stack<int>(m);
stack.Push(0);
while (stack.Count > 0)
{
int index = stack.Count - 1;
int value = stack.Pop();
while (value < n)
{
result[index++] = value++;
stack.Push(value);

if (index != m) continue;
yield return (int[])result.Clone();

break;
}
}
}

public static IEnumerable<T[]> CombinationsRosettaWoRecursion<T>(T[] array, int m)
{
T[] result = new T[m];
foreach (int[] j in CombinationsRosettaWoRecursion(m, array.Length))
{
for (int i = 0; i < m; i++)
{
result[i] = array[j[i]];
}
yield return result;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Microsoft.ML;
using SimplCommerce.Module.ProductsRecommendation.Models;

namespace SimplCommerce.Module.ProductsRecommendation
{
public interface IRecommendationService
{
void BuildRecommendationModel();
ProductPrediction Predict(ProductInfo product);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Microsoft.ML.Data;

namespace SimplCommerce.Module.ProductsRecommendation.Models
{
public class ProductInfo
{
[LoadColumn(0)] public float ProductID { get; set; }
[LoadColumn(1)] public float CombinedProductID { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace SimplCommerce.Module.ProductsRecommendation.Models
{
public class ProductPrediction
{
public float Score { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using SimplCommerce.Infrastructure.Modules;

namespace SimplCommerce.Module.ProductsRecommendation
{
public class ModuleInitializer : IModuleInitializer
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IRecommendationService, RecommendationService>();
services.AddHostedService<RecommendationTrainingBackgroundService>();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{

}
}
}
Loading