diff --git a/sql/migrate_dict_employee_position.sql b/sql/migrate_dict_employee_position.sql new file mode 100644 index 0000000..c0c2846 --- /dev/null +++ b/sql/migrate_dict_employee_position.sql @@ -0,0 +1,80 @@ +-- 员工档案「岗位 / 职级」字典初始数据(幂等:已存在同 DictType+DictCode 则跳过) +-- dictType: employee_position / employee_job_level +-- MySQL 5.7+ / 8.0 + +SET NAMES utf8mb4; + +-- ========== 岗位 employee_position ========== +INSERT INTO `t_dict_type` (`DictType`, `Remark`, `TypeSort`, `IsDeleted`, `CreateUserId`) +SELECT 'employee_position', '员工岗位', 0, 0, 0 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `t_dict_type` WHERE `DictType` = 'employee_position' AND `IsDeleted` = 0 +); + +INSERT INTO `t_dict` (`DictType`, `DictSort`, `DictCode`, `DictName`, `IsDeleted`, `CreateUserId`) +SELECT v.`DictType`, v.`DictSort`, v.`DictCode`, v.`DictName`, 0, 0 +FROM ( + SELECT 'employee_position' AS DictType, 1 AS DictSort, 'generalManager' AS DictCode, '总经理' AS DictName + UNION ALL SELECT 'employee_position', 2, 'deputyGeneralManager', '副总经理' + UNION ALL SELECT 'employee_position', 3, 'logisticsAdvisor', '物流顾问' + UNION ALL SELECT 'employee_position', 4, 'csManager', '客服主管' + UNION ALL SELECT 'employee_position', 5, 'csSpecialist', '客服专员' + UNION ALL SELECT 'employee_position', 6, 'opsManager', '运营经理' + UNION ALL SELECT 'employee_position', 7, 'opsSpecialist', '运营专员' + UNION ALL SELECT 'employee_position', 8, 'financeManager', '财务经理' + UNION ALL SELECT 'employee_position', 9, 'financeSpecialist', '财务专员' + UNION ALL SELECT 'employee_position', 10, 'warehouseManager', '仓储主管' + UNION ALL SELECT 'employee_position', 11, 'warehouseClerk', '仓管员' + UNION ALL SELECT 'employee_position', 12, 'customsSpecialist', '关务专员' + UNION ALL SELECT 'employee_position', 13, 'productManager', '产品经理' + UNION ALL SELECT 'employee_position', 14, 'techEngineer', '技术工程师' + UNION ALL SELECT 'employee_position', 15, 'hrAdmin', '人事行政' + UNION ALL SELECT 'employee_position', 16, 'marketingSpecialist', '市场专员' + UNION ALL SELECT 'employee_position', 17, 'dispatcher', '调度员' +) AS v +WHERE NOT EXISTS ( + SELECT 1 + FROM `t_dict` d + WHERE d.`DictType` = v.`DictType` + AND d.`DictCode` = v.`DictCode` + AND d.`IsDeleted` = 0 +); + +UPDATE `t_dict` +SET `IsDeleted` = 1, + `UpdateTime` = NOW() +WHERE `DictType` = 'employee_position' + AND `DictCode` IN ( + 'salesDirector', + 'salesManager', + 'salesSpecialist', + 'logisticsAdvisorDirector', + 'logisticsAdvisorManager' + ) + AND `IsDeleted` = 0; + +-- ========== 职级 employee_job_level(一级~五级) ========== +INSERT INTO `t_dict_type` (`DictType`, `Remark`, `TypeSort`, `IsDeleted`, `CreateUserId`) +SELECT 'employee_job_level', '员工职级', 0, 0, 0 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `t_dict_type` WHERE `DictType` = 'employee_job_level' AND `IsDeleted` = 0 +); + +INSERT INTO `t_dict` (`DictType`, `DictSort`, `DictCode`, `DictName`, `IsDeleted`, `CreateUserId`) +SELECT v.`DictType`, v.`DictSort`, v.`DictCode`, v.`DictName`, 0, 0 +FROM ( + SELECT 'employee_job_level' AS DictType, 1 AS DictSort, '1' AS DictCode, '一级' AS DictName + UNION ALL SELECT 'employee_job_level', 2, '2', '二级' + UNION ALL SELECT 'employee_job_level', 3, '3', '三级' + UNION ALL SELECT 'employee_job_level', 4, '4', '四级' + UNION ALL SELECT 'employee_job_level', 5, '5', '五级' +) AS v +WHERE NOT EXISTS ( + SELECT 1 + FROM `t_dict` d + WHERE d.`DictType` = v.`DictType` + AND d.`DictCode` = v.`DictCode` + AND d.`IsDeleted` = 0 +); diff --git a/src/Oms.Application/Dtos/Customers/CreateCustomerInput.cs b/src/Oms.Application/Dtos/Customers/CreateCustomerInput.cs index cfbef33..1c1ac13 100644 --- a/src/Oms.Application/Dtos/Customers/CreateCustomerInput.cs +++ b/src/Oms.Application/Dtos/Customers/CreateCustomerInput.cs @@ -118,4 +118,10 @@ public class CreateCustomerInput [StringLength(16)] public string? InviteCode { get; set; } + /// + /// 顾问邀请码(所属顾问绑定,值为员工 Account;仅 CustomerService 为空时生效)。 + /// + [StringLength(64)] + public string? AdvisorCode { get; set; } + } diff --git a/src/Oms.Application/Services/Customers/CustomerService.cs b/src/Oms.Application/Services/Customers/CustomerService.cs index 07ddc94..21550cd 100644 --- a/src/Oms.Application/Services/Customers/CustomerService.cs +++ b/src/Oms.Application/Services/Customers/CustomerService.cs @@ -233,6 +233,10 @@ public class CustomerService : ApplicationService input.InviteCode, ReferralCodeRepository, CustomerRepository); + await StaffAdvisorBinder.BindAdvisorOnCreateAsync( + entity, + input.AdvisorCode, + RbacDbContext); await CustomerRepository.InsertAsync(entity); try { diff --git a/src/Oms.Application/Shared/StaffAdvisorBinder.cs b/src/Oms.Application/Shared/StaffAdvisorBinder.cs new file mode 100644 index 0000000..48c9719 --- /dev/null +++ b/src/Oms.Application/Shared/StaffAdvisorBinder.cs @@ -0,0 +1,67 @@ +using Fake.Rbac.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Oms.Domain.CustomersAggregate.Entities; + +namespace Oms.Application.Shared; + +/// +/// 顾问邀请绑定:注册时把所属顾问(CustomerService)写成员工登录账号。 +/// +public static class StaffAdvisorBinder +{ + /// + /// 仅在 CustomerService 为空时绑定;advisorCode 为员工 Account(来自 ?advisor=)。 + /// 无效账号静默跳过,不阻断注册。 + /// + public static async Task BindAdvisorOnCreateAsync( + Customer entity, + string? advisorCode, + FakeRbacDbContext rbacDbContext, + CancellationToken cancellationToken = default) + { + if (!string.IsNullOrWhiteSpace(entity.CustomerService)) + { + return; + } + + var key = advisorCode?.Trim(); + if (string.IsNullOrWhiteSpace(key)) + { + return; + } + + var account = await ResolveStaffAccountAsync(rbacDbContext, key, cancellationToken); + if (string.IsNullOrWhiteSpace(account)) + { + return; + } + + entity.CustomerService = account; + } + + private static async Task ResolveStaffAccountAsync( + FakeRbacDbContext rbacDbContext, + string key, + CancellationToken cancellationToken) + { + const string sql = """ + SELECT u.`Account` AS `Account` + FROM `user` u + WHERE u.`IsDeleted` = 0 + AND u.`Account` = {0} + ORDER BY u.`CreateTime` ASC + LIMIT 1 + """; + + var row = await rbacDbContext.Database + .SqlQueryRaw(sql, key) + .FirstOrDefaultAsync(cancellationToken); + + return string.IsNullOrWhiteSpace(row?.Account) ? null : row.Account.Trim(); + } + + private sealed class AccountRow + { + public string? Account { get; set; } + } +} diff --git a/src/SimpleAdmin.Api/WeChat/Dtos/WeChatDtos.cs b/src/SimpleAdmin.Api/WeChat/Dtos/WeChatDtos.cs index ea4808c..f5c09ba 100644 --- a/src/SimpleAdmin.Api/WeChat/Dtos/WeChatDtos.cs +++ b/src/SimpleAdmin.Api/WeChat/Dtos/WeChatDtos.cs @@ -96,6 +96,11 @@ public class WeChatCustomerRegisterInput /// public string? InviteCode { get; set; } + /// + /// 顾问邀请码(可选)。来自 ?advisor=,值为员工登录账号;注册时写入所属顾问 CustomerService。 + /// + public string? AdvisorCode { get; set; } + [Required] public WeChatProfileInput Wechat { get; set; } = new(); } diff --git a/src/SimpleAdmin.Api/WeChat/WeChatCustomerService.cs b/src/SimpleAdmin.Api/WeChat/WeChatCustomerService.cs index 21f239a..36b5ce9 100644 --- a/src/SimpleAdmin.Api/WeChat/WeChatCustomerService.cs +++ b/src/SimpleAdmin.Api/WeChat/WeChatCustomerService.cs @@ -164,6 +164,11 @@ public class WeChatCustomerService( { entity.ReferralChannel = "wechat"; } + await StaffAdvisorBinder.BindAdvisorOnCreateAsync( + entity, + input.AdvisorCode, + rbacDbContext, + cancellationToken); await customerRepository.InsertAsync(entity); try