feat: ??????????????????????????
?????????????????????????????????? SQL? Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
e0216e9692
commit
206dc3ef3c
46
sql/20260730_customer_price_tier_key.sql
Normal file
46
sql/20260730_customer_price_tier_key.sql
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
-- 价区键迁到客户:与会员等级解耦
|
||||||
|
-- 新注册默认价区键/会员等级均为「普通会员」;计价时空键回落「普通客户」
|
||||||
|
|
||||||
|
-- 1) 客户表增加价区键
|
||||||
|
ALTER TABLE `t_customer`
|
||||||
|
ADD COLUMN `PriceTierKey` VARCHAR(64) NULL COMMENT '价区键(卖价 CustomerGroupValues JSON 键)' AFTER `MemberLevelId`;
|
||||||
|
|
||||||
|
-- 2) 从原会员等级价区键回填(依赖本脚本执行前 t_member_level 仍有 PriceTierKey)
|
||||||
|
UPDATE `t_customer` c
|
||||||
|
INNER JOIN `t_member_level` ml ON ml.`Id` = c.`MemberLevelId` AND ml.`IsDeleted` = 0
|
||||||
|
SET c.`PriceTierKey` = NULLIF(TRIM(ml.`PriceTierKey`), '')
|
||||||
|
WHERE c.`IsDeleted` = 0
|
||||||
|
AND (c.`PriceTierKey` IS NULL OR TRIM(c.`PriceTierKey`) = '')
|
||||||
|
AND ml.`PriceTierKey` IS NOT NULL
|
||||||
|
AND TRIM(ml.`PriceTierKey`) <> '';
|
||||||
|
|
||||||
|
-- 3) 仍为空的主客户:默认「普通会员」(与新注册一致)
|
||||||
|
UPDATE `t_customer`
|
||||||
|
SET `PriceTierKey` = '普通会员'
|
||||||
|
WHERE `IsDeleted` = 0
|
||||||
|
AND (`ParentCustomerId` IS NULL OR `ParentCustomerId` = 0)
|
||||||
|
AND (`PriceTierKey` IS NULL OR TRIM(`PriceTierKey`) = '');
|
||||||
|
|
||||||
|
-- 4) 子客户跟随主客户价区
|
||||||
|
UPDATE `t_customer` sub
|
||||||
|
INNER JOIN `t_customer` main ON main.`Id` = sub.`ParentCustomerId` AND main.`IsDeleted` = 0
|
||||||
|
SET sub.`PriceTierKey` = main.`PriceTierKey`
|
||||||
|
WHERE sub.`IsDeleted` = 0
|
||||||
|
AND sub.`ParentCustomerId` IS NOT NULL
|
||||||
|
AND sub.`ParentCustomerId` <> 0;
|
||||||
|
|
||||||
|
-- 5) 删除会员等级上的价区键列(须在插入默认等级前执行,避免 NOT NULL 约束)
|
||||||
|
ALTER TABLE `t_member_level` DROP COLUMN `PriceTierKey`;
|
||||||
|
|
||||||
|
-- 6) 确保存在启用的「普通会员」等级(供新注册默认挂靠)
|
||||||
|
INSERT INTO `t_member_level` (
|
||||||
|
`LevelCode`, `LevelName`, `MinPoints`, `ExtraDiscountRate`, `Sort`,
|
||||||
|
`Status`, `IsDeleted`, `CreateTime`
|
||||||
|
)
|
||||||
|
SELECT '普通会员', '普通会员', 0, 0, 0, 2, 0, NOW()
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `t_member_level`
|
||||||
|
WHERE `IsDeleted` = 0
|
||||||
|
AND (`LevelName` = '普通会员' OR `LevelCode` = '普通会员')
|
||||||
|
);
|
||||||
@ -106,6 +106,12 @@ public class CreateCustomerInput
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int? MemberLevelId { get; set; }
|
public int? MemberLevelId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 价区键(卖价 CustomerGroupValues JSON 键)。空则默认「普通会员」。
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(64)]
|
||||||
|
public string? PriceTierKey { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册邀请码(引荐绑定,仅新客户有效)。
|
/// 注册邀请码(引荐绑定,仅新客户有效)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -102,6 +102,11 @@ public class CustomerOutput
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? MemberLevelName { get; set; }
|
public string? MemberLevelName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 价区键。
|
||||||
|
/// </summary>
|
||||||
|
public string? PriceTierKey { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 最后登录 IP。
|
/// 最后登录 IP。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -102,6 +102,11 @@ public class CustomerPaginatedOutput
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? MemberLevelName { get; set; }
|
public string? MemberLevelName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 价区键。
|
||||||
|
/// </summary>
|
||||||
|
public string? PriceTierKey { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 最后登录 IP。
|
/// 最后登录 IP。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -105,4 +105,10 @@ public class UpdateCustomerInput
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int? MemberLevelId { get; set; }
|
public int? MemberLevelId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 价区键(卖价 CustomerGroupValues JSON 键)。空则回落「普通客户」。
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(64)]
|
||||||
|
public string? PriceTierKey { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,8 +15,6 @@ public class MemberLevelOutput
|
|||||||
|
|
||||||
public int MinPoints { get; set; }
|
public int MinPoints { get; set; }
|
||||||
|
|
||||||
public string PriceTierKey { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public decimal ExtraDiscountRate { get; set; }
|
public decimal ExtraDiscountRate { get; set; }
|
||||||
|
|
||||||
public int Sort { get; set; }
|
public int Sort { get; set; }
|
||||||
|
|||||||
@ -18,9 +18,6 @@ public class MemberLevelSaveInput
|
|||||||
|
|
||||||
public int MinPoints { get; set; }
|
public int MinPoints { get; set; }
|
||||||
|
|
||||||
[Required]
|
|
||||||
public string PriceTierKey { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public decimal ExtraDiscountRate { get; set; }
|
public decimal ExtraDiscountRate { get; set; }
|
||||||
|
|
||||||
public int Sort { get; set; }
|
public int Sort { get; set; }
|
||||||
|
|||||||
@ -226,7 +226,8 @@ public class CustomerService : ApplicationService
|
|||||||
await UserService.AssignRolesAsync(user.Id, [customerRoleId]);
|
await UserService.AssignRolesAsync(user.Id, [customerRoleId]);
|
||||||
var userId = await GetRbacUserMapIdOrThrowAsync(user.Id);
|
var userId = await GetRbacUserMapIdOrThrowAsync(user.Id);
|
||||||
var entity = BuildCustomerEntity(input, normalized, null, userId);
|
var entity = BuildCustomerEntity(input, normalized, null, userId);
|
||||||
entity.MemberLevelId = await ResolveMemberLevelIdForSaveAsync(input.MemberLevelId);
|
entity.MemberLevelId = await ResolveMemberLevelIdForCreateAsync(input.MemberLevelId);
|
||||||
|
entity.PriceTierKey = MarketingCustomerHelper.ResolveNewCustomerPriceTierKey(input.PriceTierKey);
|
||||||
await MarketingReferralBinder.BindReferrerOnCreateAsync(
|
await MarketingReferralBinder.BindReferrerOnCreateAsync(
|
||||||
entity,
|
entity,
|
||||||
input.InviteCode,
|
input.InviteCode,
|
||||||
@ -274,7 +275,11 @@ public class CustomerService : ApplicationService
|
|||||||
roleId = await CreateSubCustomerRoleAsync(userId, normalized.CustomerName, permissions);
|
roleId = await CreateSubCustomerRoleAsync(userId, normalized.CustomerName, permissions);
|
||||||
await UserService.AssignRolesAsync(user.Id, [roleId.Value]);
|
await UserService.AssignRolesAsync(user.Id, [roleId.Value]);
|
||||||
var entity = BuildCustomerEntity(input, normalized, mainCustomer.Id, userId);
|
var entity = BuildCustomerEntity(input, normalized, mainCustomer.Id, userId);
|
||||||
entity.MemberLevelId = await ResolveMemberLevelIdForSaveAsync(input.MemberLevelId);
|
// 子客户跟随主客户价区与等级展示,计价走主客户
|
||||||
|
entity.MemberLevelId = mainCustomer.MemberLevelId
|
||||||
|
?? await ResolveMemberLevelIdForCreateAsync(input.MemberLevelId);
|
||||||
|
entity.PriceTierKey = MarketingCustomerHelper.NormalizePriceTierKeyOrDefault(
|
||||||
|
mainCustomer.PriceTierKey ?? input.PriceTierKey);
|
||||||
await CustomerRepository.InsertAsync(entity);
|
await CustomerRepository.InsertAsync(entity);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@ -320,6 +325,7 @@ public class CustomerService : ApplicationService
|
|||||||
entity.CustomerService = NormalizeOptionalText(input.CustomerService);
|
entity.CustomerService = NormalizeOptionalText(input.CustomerService);
|
||||||
entity.CustomerGroup = input.CustomerGroup;
|
entity.CustomerGroup = input.CustomerGroup;
|
||||||
entity.MemberLevelId = await ResolveMemberLevelIdForSaveAsync(input.MemberLevelId);
|
entity.MemberLevelId = await ResolveMemberLevelIdForSaveAsync(input.MemberLevelId);
|
||||||
|
entity.PriceTierKey = MarketingCustomerHelper.NormalizePriceTierKeyOrDefault(input.PriceTierKey);
|
||||||
|
|
||||||
await CustomerRepository.UpdateAsync(entity);
|
await CustomerRepository.UpdateAsync(entity);
|
||||||
}
|
}
|
||||||
@ -570,7 +576,8 @@ public class CustomerService : ApplicationService
|
|||||||
Salesman = NormalizeOptionalText(input.Salesman),
|
Salesman = NormalizeOptionalText(input.Salesman),
|
||||||
CustomerService = NormalizeOptionalText(input.CustomerService),
|
CustomerService = NormalizeOptionalText(input.CustomerService),
|
||||||
CustomerGroup = input.CustomerGroup,
|
CustomerGroup = input.CustomerGroup,
|
||||||
MemberLevelId = input.MemberLevelId
|
MemberLevelId = input.MemberLevelId,
|
||||||
|
PriceTierKey = input.PriceTierKey
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -586,6 +593,17 @@ public class CustomerService : ApplicationService
|
|||||||
return levels.ToDictionary(x => x.Id, x => x.LevelName);
|
return levels.ToDictionary(x => x.Id, x => x.LevelName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>创建时:未指定则默认「普通会员」等级。</summary>
|
||||||
|
private async Task<int?> ResolveMemberLevelIdForCreateAsync(int? memberLevelId)
|
||||||
|
{
|
||||||
|
if (memberLevelId.HasValue)
|
||||||
|
{
|
||||||
|
return await ResolveMemberLevelIdForSaveAsync(memberLevelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await MarketingCustomerHelper.ResolveDefaultMemberLevelIdAsync(MemberLevelRepository);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<int?> ResolveMemberLevelIdForSaveAsync(int? memberLevelId)
|
private async Task<int?> ResolveMemberLevelIdForSaveAsync(int? memberLevelId)
|
||||||
{
|
{
|
||||||
if (!memberLevelId.HasValue)
|
if (!memberLevelId.HasValue)
|
||||||
|
|||||||
@ -115,15 +115,13 @@ public class MarketingMemberLevelService : ApplicationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
return x => x.LevelCode.Contains(keyword)
|
return x => x.LevelCode.Contains(keyword)
|
||||||
|| x.LevelName.Contains(keyword)
|
|| x.LevelName.Contains(keyword);
|
||||||
|| x.PriceTierKey.Contains(keyword);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ValidateSaveAsync(MemberLevelSaveInput input, int? excludeId)
|
private async Task ValidateSaveAsync(MemberLevelSaveInput input, int? excludeId)
|
||||||
{
|
{
|
||||||
var levelCode = input.LevelCode.Trim();
|
var levelCode = input.LevelCode.Trim();
|
||||||
var levelName = input.LevelName.Trim();
|
var levelName = input.LevelName.Trim();
|
||||||
var priceTierKey = input.PriceTierKey.Trim();
|
|
||||||
if (string.IsNullOrWhiteSpace(levelCode))
|
if (string.IsNullOrWhiteSpace(levelCode))
|
||||||
{
|
{
|
||||||
throw new BusinessException("等级编码不能为空");
|
throw new BusinessException("等级编码不能为空");
|
||||||
@ -134,11 +132,6 @@ public class MarketingMemberLevelService : ApplicationService
|
|||||||
throw new BusinessException("等级名称不能为空");
|
throw new BusinessException("等级名称不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(priceTierKey))
|
|
||||||
{
|
|
||||||
throw new BusinessException("价区键不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.ExtraDiscountRate < 0 || input.ExtraDiscountRate >= 1)
|
if (input.ExtraDiscountRate < 0 || input.ExtraDiscountRate >= 1)
|
||||||
{
|
{
|
||||||
throw new BusinessException("额外折扣率须在 0~1 之间(不含 1)");
|
throw new BusinessException("额外折扣率须在 0~1 之间(不含 1)");
|
||||||
@ -155,7 +148,6 @@ public class MarketingMemberLevelService : ApplicationService
|
|||||||
entity.LevelCode = input.LevelCode.Trim();
|
entity.LevelCode = input.LevelCode.Trim();
|
||||||
entity.LevelName = input.LevelName.Trim();
|
entity.LevelName = input.LevelName.Trim();
|
||||||
entity.MinPoints = input.MinPoints;
|
entity.MinPoints = input.MinPoints;
|
||||||
entity.PriceTierKey = input.PriceTierKey.Trim();
|
|
||||||
entity.ExtraDiscountRate = input.ExtraDiscountRate;
|
entity.ExtraDiscountRate = input.ExtraDiscountRate;
|
||||||
entity.Sort = input.Sort;
|
entity.Sort = input.Sort;
|
||||||
entity.IconUrl = string.IsNullOrWhiteSpace(input.IconUrl) ? null : input.IconUrl.Trim();
|
entity.IconUrl = string.IsNullOrWhiteSpace(input.IconUrl) ? null : input.IconUrl.Trim();
|
||||||
@ -173,7 +165,6 @@ public class MarketingMemberLevelService : ApplicationService
|
|||||||
LevelCode = entity.LevelCode,
|
LevelCode = entity.LevelCode,
|
||||||
LevelName = entity.LevelName,
|
LevelName = entity.LevelName,
|
||||||
MinPoints = entity.MinPoints,
|
MinPoints = entity.MinPoints,
|
||||||
PriceTierKey = entity.PriceTierKey,
|
|
||||||
ExtraDiscountRate = entity.ExtraDiscountRate,
|
ExtraDiscountRate = entity.ExtraDiscountRate,
|
||||||
Sort = entity.Sort,
|
Sort = entity.Sort,
|
||||||
IconUrl = entity.IconUrl,
|
IconUrl = entity.IconUrl,
|
||||||
|
|||||||
@ -57,7 +57,7 @@ public class MarketingPortalMemberService : ApplicationService
|
|||||||
{
|
{
|
||||||
MemberLevelId = currentLevel?.Id,
|
MemberLevelId = currentLevel?.Id,
|
||||||
MemberLevelName = currentLevel?.LevelName,
|
MemberLevelName = currentLevel?.LevelName,
|
||||||
PriceTierKey = currentLevel?.PriceTierKey,
|
PriceTierKey = MarketingCustomerHelper.NormalizePriceTierKeyOrDefault(mainCustomer.PriceTierKey),
|
||||||
TotalPoints = account.TotalPoints,
|
TotalPoints = account.TotalPoints,
|
||||||
AvailablePoints = account.AvailablePoints,
|
AvailablePoints = account.AvailablePoints,
|
||||||
NextLevelId = nextLevel?.Id,
|
NextLevelId = nextLevel?.Id,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ using Oms.Domain.CustomersAggregate.Enums;
|
|||||||
using Oms.Domain.CustomersAggregate.Repositories;
|
using Oms.Domain.CustomersAggregate.Repositories;
|
||||||
using Oms.Domain.MarketingAggregate.Entities;
|
using Oms.Domain.MarketingAggregate.Entities;
|
||||||
using Oms.Domain.MarketingAggregate.Repositories;
|
using Oms.Domain.MarketingAggregate.Repositories;
|
||||||
|
using Oms.Domain.Shared.Enums;
|
||||||
|
|
||||||
namespace Oms.Application.Shared;
|
namespace Oms.Application.Shared;
|
||||||
|
|
||||||
@ -13,6 +14,15 @@ namespace Oms.Application.Shared;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class MarketingCustomerHelper
|
public static class MarketingCustomerHelper
|
||||||
{
|
{
|
||||||
|
/// <summary>缺省/空价区键回落(卖价 JSON 标准键)。</summary>
|
||||||
|
public const string FallbackPriceTierKey = "普通客户";
|
||||||
|
|
||||||
|
/// <summary>新注册客户默认价区键(与会员等级名称对齐为「普通会员」)。</summary>
|
||||||
|
public const string DefaultNewCustomerPriceTierKey = "普通会员";
|
||||||
|
|
||||||
|
/// <summary>新注册客户默认会员等级编码/名称。</summary>
|
||||||
|
public const string DefaultMemberLevelName = "普通会员";
|
||||||
|
|
||||||
public static async Task<Customer> ResolveMarketingMainCustomerAsync(
|
public static async Task<Customer> ResolveMarketingMainCustomerAsync(
|
||||||
ICustomerRepository customerRepository,
|
ICustomerRepository customerRepository,
|
||||||
Customer sessionCustomer,
|
Customer sessionCustomer,
|
||||||
@ -46,7 +56,7 @@ public static class MarketingCustomerHelper
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 解析用于计价的价区键:主客户 MemberLevel → PriceTierKey;无等级时回退 CustomerGroup 映射。
|
/// 解析用于计价的价区键:主客户.PriceTierKey;空则回落「普通客户」。与会员等级无关。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static async Task<string?> ResolvePriceTierKeyAsync(
|
public static async Task<string?> ResolvePriceTierKeyAsync(
|
||||||
ICustomerRepository customerRepository,
|
ICustomerRepository customerRepository,
|
||||||
@ -54,15 +64,18 @@ public static class MarketingCustomerHelper
|
|||||||
int? customerId,
|
int? customerId,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
// memberLevelRepository 保留参数以兼容既有调用方签名;价区已与等级解耦。
|
||||||
|
_ = memberLevelRepository;
|
||||||
|
|
||||||
if (!customerId.HasValue || customerId.Value <= 0)
|
if (!customerId.HasValue || customerId.Value <= 0)
|
||||||
{
|
{
|
||||||
return null;
|
return FallbackPriceTierKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sessionCustomer = await customerRepository.FirstOrDefaultAsync(x => x.Id == customerId.Value, cancellationToken);
|
var sessionCustomer = await customerRepository.FirstOrDefaultAsync(x => x.Id == customerId.Value, cancellationToken);
|
||||||
if (sessionCustomer == null)
|
if (sessionCustomer == null)
|
||||||
{
|
{
|
||||||
return null;
|
return FallbackPriceTierKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
var mainCustomer = sessionCustomer.ParentCustomerId.HasValue
|
var mainCustomer = sessionCustomer.ParentCustomerId.HasValue
|
||||||
@ -70,39 +83,63 @@ public static class MarketingCustomerHelper
|
|||||||
: sessionCustomer;
|
: sessionCustomer;
|
||||||
if (mainCustomer == null)
|
if (mainCustomer == null)
|
||||||
{
|
{
|
||||||
return null;
|
return FallbackPriceTierKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mainCustomer.MemberLevelId.HasValue)
|
if (!string.IsNullOrWhiteSpace(mainCustomer.PriceTierKey))
|
||||||
{
|
{
|
||||||
var level = await memberLevelRepository.FirstOrDefaultAsync(
|
return mainCustomer.PriceTierKey.Trim();
|
||||||
x => x.Id == mainCustomer.MemberLevelId.Value,
|
|
||||||
cancellationToken);
|
|
||||||
if (level != null && !string.IsNullOrWhiteSpace(level.PriceTierKey))
|
|
||||||
{
|
|
||||||
return level.PriceTierKey.Trim();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return MapCustomerGroupToPriceTierKey(mainCustomer.CustomerGroup);
|
// 兼容未回填客户:短暂回退遗留 CustomerGroup,最终仍归一到标准键
|
||||||
|
return MapCustomerGroupToPriceTierKey(mainCustomer.CustomerGroup) ?? FallbackPriceTierKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NormalizePriceTierKeyOrDefault(string? priceTierKey)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(priceTierKey) ? FallbackPriceTierKey : priceTierKey.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>新客默认价区键。</summary>
|
||||||
|
public static string ResolveNewCustomerPriceTierKey(string? input) =>
|
||||||
|
string.IsNullOrWhiteSpace(input) ? DefaultNewCustomerPriceTierKey : input.Trim();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析默认「普通会员」等级 Id;不存在则取启用等级中 MinPoints 最低者。
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<int?> ResolveDefaultMemberLevelIdAsync(
|
||||||
|
IMemberLevelRepository memberLevelRepository,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var named = await memberLevelRepository.FirstOrDefaultAsync(
|
||||||
|
x => x.Status == EnableStatus.Enabled
|
||||||
|
&& (x.LevelName == DefaultMemberLevelName || x.LevelCode == DefaultMemberLevelName),
|
||||||
|
cancellationToken);
|
||||||
|
if (named != null)
|
||||||
|
{
|
||||||
|
return named.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
var levels = await memberLevelRepository.GetAllEnabledOrderedAsync(cancellationToken);
|
||||||
|
return levels.OrderBy(x => x.MinPoints).ThenBy(x => x.Sort).ThenBy(x => x.Id).FirstOrDefault()?.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string? MapCustomerGroupToPriceTierKey(CustomerGroup? customerGroup)
|
public static string? MapCustomerGroupToPriceTierKey(CustomerGroup? customerGroup)
|
||||||
{
|
{
|
||||||
if (!customerGroup.HasValue)
|
if (!customerGroup.HasValue)
|
||||||
{
|
{
|
||||||
return "普通客户";
|
return FallbackPriceTierKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
return customerGroup.Value switch
|
return customerGroup.Value switch
|
||||||
{
|
{
|
||||||
CustomerGroup.Normal => "普通客户",
|
CustomerGroup.Normal => FallbackPriceTierKey,
|
||||||
CustomerGroup.Vip1 => "客户组1",
|
CustomerGroup.Vip1 => "客户组1",
|
||||||
CustomerGroup.Vip2 => "客户组2",
|
CustomerGroup.Vip2 => "客户组2",
|
||||||
CustomerGroup.Vip3 => "客户组3",
|
CustomerGroup.Vip3 => "客户组3",
|
||||||
CustomerGroup.Vip4 => "客户组4",
|
CustomerGroup.Vip4 => "客户组4",
|
||||||
CustomerGroup.Vip5 => "客户组5",
|
CustomerGroup.Vip5 => "客户组5",
|
||||||
_ => "普通客户"
|
_ => FallbackPriceTierKey
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,8 +51,7 @@ public class MarketingOrderHelper : IScopedDependency
|
|||||||
level = await _memberLevelRepository.FirstOrDefaultAsync(x => x.Id == mainCustomer.MemberLevelId.Value, cancellationToken);
|
level = await _memberLevelRepository.FirstOrDefaultAsync(x => x.Id == mainCustomer.MemberLevelId.Value, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
var priceTierKey = level?.PriceTierKey
|
var priceTierKey = MarketingCustomerHelper.NormalizePriceTierKeyOrDefault(mainCustomer.PriceTierKey);
|
||||||
?? MarketingCustomerHelper.MapCustomerGroupToPriceTierKey(mainCustomer.CustomerGroup);
|
|
||||||
var couponInput = await _couponManager.ResolveCouponDiscountInputAsync(couponInstanceId, mainCustomerId, cancellationToken);
|
var couponInput = await _couponManager.ResolveCouponDiscountInputAsync(couponInstanceId, mainCustomerId, cancellationToken);
|
||||||
|
|
||||||
return MarketingBillingCalculator.Calculate(
|
return MarketingBillingCalculator.Calculate(
|
||||||
|
|||||||
@ -47,6 +47,12 @@ public class Customer : OmsAuditedAggregateRoot
|
|||||||
|
|
||||||
public int? MemberLevelId { get; set; }
|
public int? MemberLevelId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 价区键,对应卖价 CustomerGroupValues JSON 键(如 普通客户、VIP1)。与会员等级无关。
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(Length = 64, IsNullable = true)]
|
||||||
|
public string? PriceTierKey { get; set; }
|
||||||
|
|
||||||
public decimal CommissionBalance { get; set; }
|
public decimal CommissionBalance { get; set; }
|
||||||
|
|
||||||
public string? LastLoginIp { get; set; }
|
public string? LastLoginIp { get; set; }
|
||||||
|
|||||||
@ -13,11 +13,6 @@ public class MemberLevel : OmsAuditedAggregateRoot
|
|||||||
|
|
||||||
public int MinPoints { get; set; }
|
public int MinPoints { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 价区键,对应卖价 CustomerGroupValues JSON 键(如 普通客户、VIP1)。
|
|
||||||
/// </summary>
|
|
||||||
public string PriceTierKey { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public decimal ExtraDiscountRate { get; set; }
|
public decimal ExtraDiscountRate { get; set; }
|
||||||
|
|
||||||
public int Sort { get; set; }
|
public int Sort { get; set; }
|
||||||
|
|||||||
@ -15,6 +15,11 @@ public static class SystemOtherConfigCodes
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const string WeChatMp = "wechat_mp";
|
public const string WeChatMp = "wechat_mp";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信公众号关注跳转链接(BaseUrl=微信内可打开的公众号主页/名片/图文引导链接)。
|
||||||
|
/// </summary>
|
||||||
|
public const string WeChatMpFollow = "wechat_mp_follow";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 店铺授权公共配置(BaseUrl=前端回调根地址 FrontendBaseUrl,Remark=StateSecret 可选)。
|
/// 店铺授权公共配置(BaseUrl=前端回调根地址 FrontendBaseUrl,Remark=StateSecret 可选)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -32,6 +32,7 @@ public class WeChatRouteIntegrationTests
|
|||||||
host.RoutePatterns.Should().Contain("apiwechat/verification");
|
host.RoutePatterns.Should().Contain("apiwechat/verification");
|
||||||
host.RoutePatterns.Should().Contain("wechat/mp/{appId}");
|
host.RoutePatterns.Should().Contain("wechat/mp/{appId}");
|
||||||
host.RoutePatterns.Should().Contain("wechat/mp/login-qrcode");
|
host.RoutePatterns.Should().Contain("wechat/mp/login-qrcode");
|
||||||
|
host.RoutePatterns.Should().Contain("wechat/mp/follow-qrcode");
|
||||||
host.RoutePatterns.Should().Contain("wechat/oauth/user-info");
|
host.RoutePatterns.Should().Contain("wechat/oauth/user-info");
|
||||||
host.RoutePatterns.Should().Contain("wechat/customer/register");
|
host.RoutePatterns.Should().Contain("wechat/customer/register");
|
||||||
host.RoutePatterns.Should().Contain("wechat/customer/bind");
|
host.RoutePatterns.Should().Contain("wechat/customer/bind");
|
||||||
|
|||||||
@ -186,6 +186,30 @@ public partial class AuthService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取注册/绑定成功后跳转的公众号链接(微信内直接打开即可关注,无需长按扫码)。
|
||||||
|
/// </summary>
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpGet("/wechat/mp/follow-qrcode")]
|
||||||
|
[ApiExplorerSettings(GroupName = "WeChat")]
|
||||||
|
public async Task<WxFollowQrcodeOutput> GetWeChatFollowQrcodeAsync(
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var jumpUrl = await WeChatMpConfigProvider.GetFollowJumpUrlAsync(cancellationToken);
|
||||||
|
if (string.IsNullOrWhiteSpace(jumpUrl))
|
||||||
|
{
|
||||||
|
throw new BusinessException(
|
||||||
|
"未配置公众号关注跳转链接。请在「其他配置」新增 wechat_mp_follow,BaseUrl 填公众号主页/名片链接。");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WxFollowQrcodeOutput
|
||||||
|
{
|
||||||
|
JumpUrl = jumpUrl,
|
||||||
|
QrCodeUrl = jumpUrl,
|
||||||
|
Scene = WeChatQrSceneParser.FollowScene
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 同源代理微信 showqrcode 图片,便于微信内长按识别(避免跨域直连 mp.weixin.qq.com)。
|
/// 同源代理微信 showqrcode 图片,便于微信内长按识别(避免跨域直连 mp.weixin.qq.com)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -12,6 +12,19 @@ public class WxLoginQrcodeOutput
|
|||||||
public int ExpiresIn { get; set; } = 600;
|
public int ExpiresIn { get; set; } = 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>引导关注公众号:优先返回可直接跳转的公众号链接。</summary>
|
||||||
|
public class WxFollowQrcodeOutput
|
||||||
|
{
|
||||||
|
/// <summary>微信内 location 跳转用(公众号主页 / 名片 / 引导图文)。</summary>
|
||||||
|
public string JumpUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>兼容旧字段;与 JumpUrl 相同。</summary>
|
||||||
|
public string QrCodeUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>scene_str,固定 oms_follow(仅当走扫码兜底时有意义)。</summary>
|
||||||
|
public string Scene { get; set; } = "oms_follow";
|
||||||
|
}
|
||||||
|
|
||||||
public class WxLoginStatusOutput
|
public class WxLoginStatusOutput
|
||||||
{
|
{
|
||||||
public bool Ready { get; set; }
|
public bool Ready { get; set; }
|
||||||
|
|||||||
@ -19,4 +19,10 @@ public class WeChatMpOptions
|
|||||||
/// OAuth 回调基础域名,如 https://oms.nanafa.com
|
/// OAuth 回调基础域名,如 https://oms.nanafa.com
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string OAuthRedirectBase { get; set; } = string.Empty;
|
public string OAuthRedirectBase { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册/绑定成功后跳转的公众号主页或引导关注链接(微信内打开即可关注)。
|
||||||
|
/// 可在公众号后台复制「账号详情 / 推广」相关链接;也可在其他配置 wechat_mp_follow.BaseUrl 维护。
|
||||||
|
/// </summary>
|
||||||
|
public string FollowUrl { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,13 @@ public class WeChatEventHandler(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (WeChatQrSceneParser.IsFollowOnlyScene(message.EventKey))
|
||||||
|
{
|
||||||
|
return BuildTextReply(
|
||||||
|
message,
|
||||||
|
"感谢关注!已绑定成功。点击底部菜单「登录注册」可进入客户中心,「我要下单」可直接下单。");
|
||||||
|
}
|
||||||
|
|
||||||
var loginKey = WeChatQrSceneParser.ExtractLoginKey(message.EventKey);
|
var loginKey = WeChatQrSceneParser.ExtractLoginKey(message.EventKey);
|
||||||
if (string.IsNullOrWhiteSpace(loginKey))
|
if (string.IsNullOrWhiteSpace(loginKey))
|
||||||
{
|
{
|
||||||
|
|||||||
@ -10,6 +10,11 @@ public interface IWeChatMpApiClient
|
|||||||
{
|
{
|
||||||
Task<string> CreateLoginQrCodeUrlAsync(string loginUuid, int expireSeconds, CancellationToken cancellationToken = default);
|
Task<string> CreateLoginQrCodeUrlAsync(string loginUuid, int expireSeconds, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取引导关注公众号的永久二维码(scene=oms_follow),结果进程内缓存。
|
||||||
|
/// </summary>
|
||||||
|
Task<string> CreateFollowQrCodeUrlAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 代理下载公众号 showqrcode 图片,供 H5 同源展示以提升微信长按识别成功率。
|
/// 代理下载公众号 showqrcode 图片,供 H5 同源展示以提升微信长按识别成功率。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -45,6 +50,9 @@ internal sealed class WeChatMpApiClient(
|
|||||||
IWeChatMpAccessTokenStore accessTokenStore,
|
IWeChatMpAccessTokenStore accessTokenStore,
|
||||||
ILogger<WeChatMpApiClient> logger) : IWeChatMpApiClient
|
ILogger<WeChatMpApiClient> logger) : IWeChatMpApiClient
|
||||||
{
|
{
|
||||||
|
private readonly object _followQrLock = new();
|
||||||
|
private string? _cachedFollowQrCodeUrl;
|
||||||
|
|
||||||
public Task InvalidateAccessTokenCacheAsync(CancellationToken cancellationToken = default) =>
|
public Task InvalidateAccessTokenCacheAsync(CancellationToken cancellationToken = default) =>
|
||||||
accessTokenStore.ClearAsync(cancellationToken);
|
accessTokenStore.ClearAsync(cancellationToken);
|
||||||
|
|
||||||
@ -53,12 +61,43 @@ internal sealed class WeChatMpApiClient(
|
|||||||
int expireSeconds,
|
int expireSeconds,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await CreateLoginQrCodeUrlCoreAsync(loginUuid, expireSeconds, retried: false, cancellationToken);
|
return await CreateQrCodeUrlCoreAsync(
|
||||||
|
actionName: "QR_STR_SCENE",
|
||||||
|
sceneStr: loginUuid,
|
||||||
|
expireSeconds: expireSeconds,
|
||||||
|
retried: false,
|
||||||
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> CreateLoginQrCodeUrlCoreAsync(
|
public async Task<string> CreateFollowQrCodeUrlAsync(CancellationToken cancellationToken = default)
|
||||||
string loginUuid,
|
{
|
||||||
int expireSeconds,
|
lock (_followQrLock)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(_cachedFollowQrCodeUrl))
|
||||||
|
{
|
||||||
|
return _cachedFollowQrCodeUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = await CreateQrCodeUrlCoreAsync(
|
||||||
|
actionName: "QR_LIMIT_STR_SCENE",
|
||||||
|
sceneStr: WeChatQrSceneParser.FollowScene,
|
||||||
|
expireSeconds: null,
|
||||||
|
retried: false,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
lock (_followQrLock)
|
||||||
|
{
|
||||||
|
_cachedFollowQrCodeUrl = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> CreateQrCodeUrlCoreAsync(
|
||||||
|
string actionName,
|
||||||
|
string sceneStr,
|
||||||
|
int? expireSeconds,
|
||||||
bool retried,
|
bool retried,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@ -66,18 +105,18 @@ internal sealed class WeChatMpApiClient(
|
|||||||
var accessToken = await GetAccessTokenAsync(mp, cancellationToken);
|
var accessToken = await GetAccessTokenAsync(mp, cancellationToken);
|
||||||
var client = httpClientFactory.CreateClient(nameof(WeChatMpApiClient));
|
var client = httpClientFactory.CreateClient(nameof(WeChatMpApiClient));
|
||||||
|
|
||||||
var payload = new
|
object payload = expireSeconds is > 0
|
||||||
{
|
? new
|
||||||
expire_seconds = expireSeconds,
|
|
||||||
action_name = "QR_STR_SCENE",
|
|
||||||
action_info = new
|
|
||||||
{
|
{
|
||||||
scene = new
|
expire_seconds = expireSeconds.Value,
|
||||||
{
|
action_name = actionName,
|
||||||
scene_str = loginUuid
|
action_info = new { scene = new { scene_str = sceneStr } }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
: new
|
||||||
|
{
|
||||||
|
action_name = actionName,
|
||||||
|
action_info = new { scene = new { scene_str = sceneStr } }
|
||||||
|
};
|
||||||
|
|
||||||
// 微信服务端要求 POST 带 Content-Length;PostAsJsonAsync 可能走 chunked 导致 HTTP 412
|
// 微信服务端要求 POST 带 Content-Length;PostAsJsonAsync 可能走 chunked 导致 HTTP 412
|
||||||
var json = JsonSerializer.Serialize(payload);
|
var json = JsonSerializer.Serialize(payload);
|
||||||
@ -89,7 +128,7 @@ internal sealed class WeChatMpApiClient(
|
|||||||
|
|
||||||
var result = await ReadWeChatJsonAsync<WeChatQrTicketResponse>(
|
var result = await ReadWeChatJsonAsync<WeChatQrTicketResponse>(
|
||||||
response,
|
response,
|
||||||
"创建登录二维码",
|
"创建公众号二维码",
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
if (result == null || string.IsNullOrWhiteSpace(result.Ticket))
|
if (result == null || string.IsNullOrWhiteSpace(result.Ticket))
|
||||||
{
|
{
|
||||||
@ -102,10 +141,15 @@ internal sealed class WeChatMpApiClient(
|
|||||||
{
|
{
|
||||||
logger.LogWarning("微信 access_token 失效(errcode={Errcode}),清空库内 token 后重试创建二维码", result.Errcode);
|
logger.LogWarning("微信 access_token 失效(errcode={Errcode}),清空库内 token 后重试创建二维码", result.Errcode);
|
||||||
await accessTokenStore.ClearAsync(cancellationToken);
|
await accessTokenStore.ClearAsync(cancellationToken);
|
||||||
return await CreateLoginQrCodeUrlCoreAsync(loginUuid, expireSeconds, retried: true, cancellationToken);
|
return await CreateQrCodeUrlCoreAsync(
|
||||||
|
actionName,
|
||||||
|
sceneStr,
|
||||||
|
expireSeconds,
|
||||||
|
retried: true,
|
||||||
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new InvalidOperationException(FormatWeChatError("创建登录二维码", result.Errcode, result.Errmsg));
|
throw new InvalidOperationException(FormatWeChatError("创建公众号二维码", result.Errcode, result.Errmsg));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $"https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={Uri.EscapeDataString(result.Ticket)}";
|
return $"https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={Uri.EscapeDataString(result.Ticket)}";
|
||||||
|
|||||||
@ -12,6 +12,9 @@ namespace SimpleAdmin.Api.WeChat.Services;
|
|||||||
public interface IWeChatMpConfigProvider
|
public interface IWeChatMpConfigProvider
|
||||||
{
|
{
|
||||||
Task<WeChatMpOptions> GetAsync(CancellationToken cancellationToken = default);
|
Task<WeChatMpOptions> GetAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>注册成功后跳转的公众号链接(其他配置 wechat_mp_follow 优先,其次 Consul FollowUrl)。</summary>
|
||||||
|
Task<string?> GetFollowJumpUrlAsync(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class WeChatMpConfigProvider(
|
internal sealed class WeChatMpConfigProvider(
|
||||||
@ -33,10 +36,25 @@ internal sealed class WeChatMpConfigProvider(
|
|||||||
Secret = Coalesce(entity.AppSecret, fallback.Secret),
|
Secret = Coalesce(entity.AppSecret, fallback.Secret),
|
||||||
Token = Coalesce(entity.Remark, fallback.Token),
|
Token = Coalesce(entity.Remark, fallback.Token),
|
||||||
OAuthRedirectBase = Coalesce(entity.BaseUrl, fallback.OAuthRedirectBase),
|
OAuthRedirectBase = Coalesce(entity.BaseUrl, fallback.OAuthRedirectBase),
|
||||||
|
FollowUrl = fallback.FollowUrl,
|
||||||
EncodingAesKey = fallback.EncodingAesKey
|
EncodingAesKey = fallback.EncodingAesKey
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string?> GetFollowJumpUrlAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var followEntity = await repository.GetByConfigCodeAsync(
|
||||||
|
SystemOtherConfigCodes.WeChatMpFollow,
|
||||||
|
cancellationToken);
|
||||||
|
if (!string.IsNullOrWhiteSpace(followEntity?.BaseUrl))
|
||||||
|
{
|
||||||
|
return followEntity.BaseUrl.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
var mp = await GetAsync(cancellationToken);
|
||||||
|
return string.IsNullOrWhiteSpace(mp.FollowUrl) ? null : mp.FollowUrl.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
private static string Coalesce(string? primary, string fallback) =>
|
private static string Coalesce(string? primary, string fallback) =>
|
||||||
string.IsNullOrWhiteSpace(primary) ? fallback : primary.Trim();
|
string.IsNullOrWhiteSpace(primary) ? fallback : primary.Trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,16 @@ internal static class WeChatQrSceneParser
|
|||||||
{
|
{
|
||||||
private const string QrScenePrefix = "qrscene_";
|
private const string QrScenePrefix = "qrscene_";
|
||||||
|
|
||||||
|
/// <summary>注册/绑定成功后引导关注用的永久二维码 scene。</summary>
|
||||||
|
public const string FollowScene = "oms_follow";
|
||||||
|
|
||||||
|
/// <summary>是否为「仅关注」场景(不参与扫码登录会话)。</summary>
|
||||||
|
public static bool IsFollowOnlyScene(string? eventKey)
|
||||||
|
{
|
||||||
|
var scene = NormalizeScene(eventKey);
|
||||||
|
return string.Equals(scene, FollowScene, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 从微信 SCAN / SUBSCRIBE 事件的 EventKey 解析 PC 端轮询用的 loginUuid(key)。
|
/// 从微信 SCAN / SUBSCRIBE 事件的 EventKey 解析 PC 端轮询用的 loginUuid(key)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -17,6 +27,11 @@ internal static class WeChatQrSceneParser
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(scene, FollowScene, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (scene.StartsWith('{') && scene.EndsWith('}'))
|
if (scene.StartsWith('{') && scene.EndsWith('}'))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@ -27,6 +27,7 @@ public class WeChatCustomerService(
|
|||||||
IWeChatMpApiClient weChatMpApiClient,
|
IWeChatMpApiClient weChatMpApiClient,
|
||||||
ICustomerRepository customerRepository,
|
ICustomerRepository customerRepository,
|
||||||
ICustomerReferralCodeRepository referralCodeRepository,
|
ICustomerReferralCodeRepository referralCodeRepository,
|
||||||
|
IMemberLevelRepository memberLevelRepository,
|
||||||
MarketingCouponManager couponManager,
|
MarketingCouponManager couponManager,
|
||||||
UserService userService,
|
UserService userService,
|
||||||
RoleService roleService,
|
RoleService roleService,
|
||||||
@ -147,7 +148,11 @@ public class WeChatCustomerService(
|
|||||||
WechatOpenid = wechatOpenid,
|
WechatOpenid = wechatOpenid,
|
||||||
WechatNickname = wechatNickname,
|
WechatNickname = wechatNickname,
|
||||||
WechatAvatar = wechatAvatar,
|
WechatAvatar = wechatAvatar,
|
||||||
Status = CustomerStatus.Normal
|
Status = CustomerStatus.Normal,
|
||||||
|
MemberLevelId = await MarketingCustomerHelper.ResolveDefaultMemberLevelIdAsync(
|
||||||
|
memberLevelRepository,
|
||||||
|
cancellationToken),
|
||||||
|
PriceTierKey = MarketingCustomerHelper.DefaultNewCustomerPriceTierKey
|
||||||
};
|
};
|
||||||
await MarketingReferralBinder.BindReferrerOnCreateAsync(
|
await MarketingReferralBinder.BindReferrerOnCreateAsync(
|
||||||
entity,
|
entity,
|
||||||
|
|||||||
@ -50,7 +50,8 @@
|
|||||||
"Secret": "379f51a89419dc36fbb9167feef6f89a",
|
"Secret": "379f51a89419dc36fbb9167feef6f89a",
|
||||||
"Token": "c6ca74013df823c0e1da0feaaaf8bf66",
|
"Token": "c6ca74013df823c0e1da0feaaaf8bf66",
|
||||||
"EncodingAesKey": "",
|
"EncodingAesKey": "",
|
||||||
"OAuthRedirectBase": "https://oms.nanafa.com"
|
"OAuthRedirectBase": "https://oms.nanafa.com",
|
||||||
|
"FollowUrl": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Oms": {
|
"Oms": {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user