diff --git a/README.md b/README.md new file mode 100644 index 0000000..423aa13 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# HCRM (.NET 8) + +康策 CRM 新一代后端,登录机制对齐 hcrm4。 + +## 项目结构 + +``` +D:\anzb\hcrm14\ + hcrm.sln + src\ + Hcrm.Api 表现层:Controller、Filter、Cookie/Session、验证码图 + Hcrm.Application 应用层:AccountService 等业务 + Hcrm.Core 核心层:实体、DTO、接口契约、工具 + Hcrm.Infrastructure 基础设施:SqlSugar 仓储实现 +``` + +依赖方向:`Api → Application → Core`,`Api → Infrastructure → Core`(组合根在 Api 的 Program.cs) + +## 配置数据库 + +编辑 `src/Hcrm.Api/appsettings.json`,将连接串改为与 hcrm4 相同的库: + +```json +"ConnectionStrings": { + "DBContainer": "Server=.;Database=KHCRM;User Id=sa;Password=你的密码;TrustServerCertificate=True" +} +``` + +## 运行 + +```powershell +cd D:\anzb\hcrm14 +dotnet build +dotnet run --project src\Hcrm.Api\Hcrm.Api.csproj +``` + +浏览器访问:`https://localhost:7xxx/login/index.html` 或根路径 `/`(端口见 `src/Hcrm.Api/Properties/launchSettings.json`)。登录成功后进入 `wwwroot/shop/index.html`(店铺装修本地页)。 + +> 使用与 hcrm4 相同的 `tb_Employee` 表及 MD5 密码,可直接用原系统账号登录。 + +## 登录 API(与 hcrm4 兼容) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | /Account/CheckCodeImage | 验证码图片 Base64 | +| POST | /Account/Login | 用户名+密码+验证码 | +| POST | /Account/LoginByNoCode | 无验证码登录 | +| POST | /Account/IsLogin | 校验会话,未登录返回 status=401 | +| POST | /Account/WXLogOut | 退出 | +| POST | /Account/ModifyPwd | 修改密码(需登录) | + +Cookie 名:`kc_cookie_token`(与 hcrm4 相同);也可在请求头传 `token` 作为会话键。密码为 MD5 小写 32 位。 + +JSON 响应格式与 hcrm4 一致:`{ "status": 1, "message": null, "data": { ... } }`,`data` 内字段为 PascalCase;登录成功时 `data` 为 Employee 结构(含完整 `SysConfig` 表字段)。 + +## 店铺装修(新表,仅后端) + +在数据库执行:`scripts/Create_tb_ShopRenovate.sql`(新建 `tb_ShopRenovatePage`、`tb_ShopRenovateStyle`,不影响既有表)。前端暂未对接。 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | /ShopRenovate/GetPageList?pageName= | 页面列表 | +| GET | /ShopRenovate/GetPage?id= | 页面详情 | +| POST | /ShopRenovate/CreatePage | 新增(JSON) | +| POST | /ShopRenovate/UpdatePage | 保存(JSON) | +| POST | /ShopRenovate/DeletePage | 删除(form: id) | +| POST | /ShopRenovate/PublishPage | 发布(form: id) | +| POST | /ShopRenovate/UnpublishPage | 取消发布(form: id) | +| GET | /ShopRenovate/GetStyleConfig | 风格配置 | +| POST | /ShopRenovate/SaveStyleConfig | 保存风格(JSON) | diff --git a/hcrm.sln b/hcrm.sln new file mode 100644 index 0000000..885cb7d --- /dev/null +++ b/hcrm.sln @@ -0,0 +1,141 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hcrm.Api", "src\Hcrm.Api\Hcrm.Api.csproj", "{01371721-12F4-4117-B7BB-07DE1B1AF1CF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hcrm.Model", "src\Hcrm.Model\Hcrm.Model.csproj", "{4A56AFB1-53CB-4256-AA1F-48B4F7B146DE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hcrm.DTO", "src\Hcrm.DTO\Hcrm.DTO.csproj", "{9B1AF000-BF69-499B-95DE-8107996369D6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hcrm.IRepository", "src\Hcrm.IRepository\Hcrm.IRepository.csproj", "{1E59D87E-9722-4F37-82FA-3A1650DF311A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hcrm.Repository", "src\Hcrm.Repository\Hcrm.Repository.csproj", "{E191E9F6-6693-4E46-9642-719558CBC69C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hcrm.IService", "src\Hcrm.IService\Hcrm.IService.csproj", "{A2A01B63-FF6E-4380-B414-65894940A820}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hcrm.Service", "src\Hcrm.Service\Hcrm.Service.csproj", "{311A0FF0-03E0-4232-92F8-30D12942C578}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Service", "Service", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Model", "Model", "{192A8D3E-A3E2-423D-92ED-C39FA079A190}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Repository", "Repository", "{A7E404E2-34F6-4AF3-81FA-E369BA98375A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {01371721-12F4-4117-B7BB-07DE1B1AF1CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {01371721-12F4-4117-B7BB-07DE1B1AF1CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {01371721-12F4-4117-B7BB-07DE1B1AF1CF}.Debug|x64.ActiveCfg = Debug|Any CPU + {01371721-12F4-4117-B7BB-07DE1B1AF1CF}.Debug|x64.Build.0 = Debug|Any CPU + {01371721-12F4-4117-B7BB-07DE1B1AF1CF}.Debug|x86.ActiveCfg = Debug|Any CPU + {01371721-12F4-4117-B7BB-07DE1B1AF1CF}.Debug|x86.Build.0 = Debug|Any CPU + {01371721-12F4-4117-B7BB-07DE1B1AF1CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {01371721-12F4-4117-B7BB-07DE1B1AF1CF}.Release|Any CPU.Build.0 = Release|Any CPU + {01371721-12F4-4117-B7BB-07DE1B1AF1CF}.Release|x64.ActiveCfg = Release|Any CPU + {01371721-12F4-4117-B7BB-07DE1B1AF1CF}.Release|x64.Build.0 = Release|Any CPU + {01371721-12F4-4117-B7BB-07DE1B1AF1CF}.Release|x86.ActiveCfg = Release|Any CPU + {01371721-12F4-4117-B7BB-07DE1B1AF1CF}.Release|x86.Build.0 = Release|Any CPU + {4A56AFB1-53CB-4256-AA1F-48B4F7B146DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A56AFB1-53CB-4256-AA1F-48B4F7B146DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A56AFB1-53CB-4256-AA1F-48B4F7B146DE}.Debug|x64.ActiveCfg = Debug|Any CPU + {4A56AFB1-53CB-4256-AA1F-48B4F7B146DE}.Debug|x64.Build.0 = Debug|Any CPU + {4A56AFB1-53CB-4256-AA1F-48B4F7B146DE}.Debug|x86.ActiveCfg = Debug|Any CPU + {4A56AFB1-53CB-4256-AA1F-48B4F7B146DE}.Debug|x86.Build.0 = Debug|Any CPU + {4A56AFB1-53CB-4256-AA1F-48B4F7B146DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A56AFB1-53CB-4256-AA1F-48B4F7B146DE}.Release|Any CPU.Build.0 = Release|Any CPU + {4A56AFB1-53CB-4256-AA1F-48B4F7B146DE}.Release|x64.ActiveCfg = Release|Any CPU + {4A56AFB1-53CB-4256-AA1F-48B4F7B146DE}.Release|x64.Build.0 = Release|Any CPU + {4A56AFB1-53CB-4256-AA1F-48B4F7B146DE}.Release|x86.ActiveCfg = Release|Any CPU + {4A56AFB1-53CB-4256-AA1F-48B4F7B146DE}.Release|x86.Build.0 = Release|Any CPU + {9B1AF000-BF69-499B-95DE-8107996369D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B1AF000-BF69-499B-95DE-8107996369D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B1AF000-BF69-499B-95DE-8107996369D6}.Debug|x64.ActiveCfg = Debug|Any CPU + {9B1AF000-BF69-499B-95DE-8107996369D6}.Debug|x64.Build.0 = Debug|Any CPU + {9B1AF000-BF69-499B-95DE-8107996369D6}.Debug|x86.ActiveCfg = Debug|Any CPU + {9B1AF000-BF69-499B-95DE-8107996369D6}.Debug|x86.Build.0 = Debug|Any CPU + {9B1AF000-BF69-499B-95DE-8107996369D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B1AF000-BF69-499B-95DE-8107996369D6}.Release|Any CPU.Build.0 = Release|Any CPU + {9B1AF000-BF69-499B-95DE-8107996369D6}.Release|x64.ActiveCfg = Release|Any CPU + {9B1AF000-BF69-499B-95DE-8107996369D6}.Release|x64.Build.0 = Release|Any CPU + {9B1AF000-BF69-499B-95DE-8107996369D6}.Release|x86.ActiveCfg = Release|Any CPU + {9B1AF000-BF69-499B-95DE-8107996369D6}.Release|x86.Build.0 = Release|Any CPU + {1E59D87E-9722-4F37-82FA-3A1650DF311A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1E59D87E-9722-4F37-82FA-3A1650DF311A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E59D87E-9722-4F37-82FA-3A1650DF311A}.Debug|x64.ActiveCfg = Debug|Any CPU + {1E59D87E-9722-4F37-82FA-3A1650DF311A}.Debug|x64.Build.0 = Debug|Any CPU + {1E59D87E-9722-4F37-82FA-3A1650DF311A}.Debug|x86.ActiveCfg = Debug|Any CPU + {1E59D87E-9722-4F37-82FA-3A1650DF311A}.Debug|x86.Build.0 = Debug|Any CPU + {1E59D87E-9722-4F37-82FA-3A1650DF311A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1E59D87E-9722-4F37-82FA-3A1650DF311A}.Release|Any CPU.Build.0 = Release|Any CPU + {1E59D87E-9722-4F37-82FA-3A1650DF311A}.Release|x64.ActiveCfg = Release|Any CPU + {1E59D87E-9722-4F37-82FA-3A1650DF311A}.Release|x64.Build.0 = Release|Any CPU + {1E59D87E-9722-4F37-82FA-3A1650DF311A}.Release|x86.ActiveCfg = Release|Any CPU + {1E59D87E-9722-4F37-82FA-3A1650DF311A}.Release|x86.Build.0 = Release|Any CPU + {E191E9F6-6693-4E46-9642-719558CBC69C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E191E9F6-6693-4E46-9642-719558CBC69C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E191E9F6-6693-4E46-9642-719558CBC69C}.Debug|x64.ActiveCfg = Debug|Any CPU + {E191E9F6-6693-4E46-9642-719558CBC69C}.Debug|x64.Build.0 = Debug|Any CPU + {E191E9F6-6693-4E46-9642-719558CBC69C}.Debug|x86.ActiveCfg = Debug|Any CPU + {E191E9F6-6693-4E46-9642-719558CBC69C}.Debug|x86.Build.0 = Debug|Any CPU + {E191E9F6-6693-4E46-9642-719558CBC69C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E191E9F6-6693-4E46-9642-719558CBC69C}.Release|Any CPU.Build.0 = Release|Any CPU + {E191E9F6-6693-4E46-9642-719558CBC69C}.Release|x64.ActiveCfg = Release|Any CPU + {E191E9F6-6693-4E46-9642-719558CBC69C}.Release|x64.Build.0 = Release|Any CPU + {E191E9F6-6693-4E46-9642-719558CBC69C}.Release|x86.ActiveCfg = Release|Any CPU + {E191E9F6-6693-4E46-9642-719558CBC69C}.Release|x86.Build.0 = Release|Any CPU + {A2A01B63-FF6E-4380-B414-65894940A820}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2A01B63-FF6E-4380-B414-65894940A820}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2A01B63-FF6E-4380-B414-65894940A820}.Debug|x64.ActiveCfg = Debug|Any CPU + {A2A01B63-FF6E-4380-B414-65894940A820}.Debug|x64.Build.0 = Debug|Any CPU + {A2A01B63-FF6E-4380-B414-65894940A820}.Debug|x86.ActiveCfg = Debug|Any CPU + {A2A01B63-FF6E-4380-B414-65894940A820}.Debug|x86.Build.0 = Debug|Any CPU + {A2A01B63-FF6E-4380-B414-65894940A820}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2A01B63-FF6E-4380-B414-65894940A820}.Release|Any CPU.Build.0 = Release|Any CPU + {A2A01B63-FF6E-4380-B414-65894940A820}.Release|x64.ActiveCfg = Release|Any CPU + {A2A01B63-FF6E-4380-B414-65894940A820}.Release|x64.Build.0 = Release|Any CPU + {A2A01B63-FF6E-4380-B414-65894940A820}.Release|x86.ActiveCfg = Release|Any CPU + {A2A01B63-FF6E-4380-B414-65894940A820}.Release|x86.Build.0 = Release|Any CPU + {311A0FF0-03E0-4232-92F8-30D12942C578}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {311A0FF0-03E0-4232-92F8-30D12942C578}.Debug|Any CPU.Build.0 = Debug|Any CPU + {311A0FF0-03E0-4232-92F8-30D12942C578}.Debug|x64.ActiveCfg = Debug|Any CPU + {311A0FF0-03E0-4232-92F8-30D12942C578}.Debug|x64.Build.0 = Debug|Any CPU + {311A0FF0-03E0-4232-92F8-30D12942C578}.Debug|x86.ActiveCfg = Debug|Any CPU + {311A0FF0-03E0-4232-92F8-30D12942C578}.Debug|x86.Build.0 = Debug|Any CPU + {311A0FF0-03E0-4232-92F8-30D12942C578}.Release|Any CPU.ActiveCfg = Release|Any CPU + {311A0FF0-03E0-4232-92F8-30D12942C578}.Release|Any CPU.Build.0 = Release|Any CPU + {311A0FF0-03E0-4232-92F8-30D12942C578}.Release|x64.ActiveCfg = Release|Any CPU + {311A0FF0-03E0-4232-92F8-30D12942C578}.Release|x64.Build.0 = Release|Any CPU + {311A0FF0-03E0-4232-92F8-30D12942C578}.Release|x86.ActiveCfg = Release|Any CPU + {311A0FF0-03E0-4232-92F8-30D12942C578}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {01371721-12F4-4117-B7BB-07DE1B1AF1CF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {4A56AFB1-53CB-4256-AA1F-48B4F7B146DE} = {192A8D3E-A3E2-423D-92ED-C39FA079A190} + {9B1AF000-BF69-499B-95DE-8107996369D6} = {192A8D3E-A3E2-423D-92ED-C39FA079A190} + {1E59D87E-9722-4F37-82FA-3A1650DF311A} = {A7E404E2-34F6-4AF3-81FA-E369BA98375A} + {E191E9F6-6693-4E46-9642-719558CBC69C} = {A7E404E2-34F6-4AF3-81FA-E369BA98375A} + {A2A01B63-FF6E-4380-B414-65894940A820} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {311A0FF0-03E0-4232-92F8-30D12942C578} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {192A8D3E-A3E2-423D-92ED-C39FA079A190} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {A7E404E2-34F6-4AF3-81FA-E369BA98375A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D11EE477-95F5-4EFA-BB93-AD8D37B23FBC} + EndGlobalSection +EndGlobal diff --git a/scripts/Create_tb_ShopRenovate.sql b/scripts/Create_tb_ShopRenovate.sql new file mode 100644 index 0000000..a79c05d --- /dev/null +++ b/scripts/Create_tb_ShopRenovate.sql @@ -0,0 +1,30 @@ +-- 店铺装修(hcrm14 新表,与 hcrm4 业务库独立,不影响既有表) +IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'tb_ShopRenovatePage') +BEGIN + CREATE TABLE dbo.tb_ShopRenovatePage ( + tb_ShopRenovatePage_ID NVARCHAR(50) NOT NULL PRIMARY KEY, -- 页面主键 + PageName NVARCHAR(100) NOT NULL, -- 页面显示名称 + PageKey NVARCHAR(50) NOT NULL, -- 业务标识(如 home/shop/user) + SortNo INT NOT NULL CONSTRAINT DF_ShopRenovatePage_SortNo DEFAULT (0), -- 排序号 + Status INT NOT NULL CONSTRAINT DF_ShopRenovatePage_Status DEFAULT (0), -- 0未发布 1已发布 + PublishTime DATETIME NULL, -- 发布时间 + PageConfigJson NVARCHAR(MAX) NULL, -- 页面装修布局 JSON + CreateTime DATETIME NOT NULL CONSTRAINT DF_ShopRenovatePage_CreateTime DEFAULT (GETDATE()), + UpdateTime DATETIME NULL, -- 最后修改时间 + IsDeleted BIT NOT NULL CONSTRAINT DF_ShopRenovatePage_IsDeleted DEFAULT (0) -- 软删除 + ); + CREATE INDEX IX_ShopRenovatePage_PageName ON dbo.tb_ShopRenovatePage(PageName) WHERE IsDeleted = 0; +END +GO + +IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'tb_ShopRenovateStyle') +BEGIN + CREATE TABLE dbo.tb_ShopRenovateStyle ( + tb_ShopRenovateStyle_ID NVARCHAR(50) NOT NULL PRIMARY KEY, -- 风格主键,固定 default + StyleConfigJson NVARCHAR(MAX) NULL, -- 全店风格 JSON(theme/tabCount 等) + UpdateTime DATETIME NULL -- 最后保存时间 + ); + INSERT INTO dbo.tb_ShopRenovateStyle (tb_ShopRenovateStyle_ID, StyleConfigJson, UpdateTime) + VALUES ('default', N'{"theme":"red","tabCount":4,"tabStyle":1}', GETDATE()); +END +GO diff --git a/src/Hcrm.Api/Auth/CurrentUserAccessor.cs b/src/Hcrm.Api/Auth/CurrentUserAccessor.cs new file mode 100644 index 0000000..f6a48ff --- /dev/null +++ b/src/Hcrm.Api/Auth/CurrentUserAccessor.cs @@ -0,0 +1,16 @@ +using Hcrm.DTO; +using Hcrm.IService; + +namespace Hcrm.Api.Auth; + +public class CurrentUserAccessor : ICurrentUserAccessor +{ + private readonly ILoginTokenService _loginToken; + + public CurrentUserAccessor(ILoginTokenService loginToken) + { + _loginToken = loginToken; + } + + public AccountModel? CurrentUser => _loginToken.GetCurrentUser(); +} diff --git a/src/Hcrm.Api/Auth/LoginTokenService.cs b/src/Hcrm.Api/Auth/LoginTokenService.cs new file mode 100644 index 0000000..5358fc7 --- /dev/null +++ b/src/Hcrm.Api/Auth/LoginTokenService.cs @@ -0,0 +1,74 @@ +using Hcrm.DTO; +using Hcrm.IService; +using Hcrm.Model; +using Microsoft.Extensions.Caching.Memory; + +namespace Hcrm.Api.Auth; + +public class LoginTokenService : ILoginTokenService +{ + private const string CaptchaKey = "session_Code"; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IMemoryCache _cache; + + public LoginTokenService(IHttpContextAccessor httpContextAccessor, IMemoryCache cache) + { + _httpContextAccessor = httpContextAccessor; + _cache = cache; + } + + public void SetCookie(AccountModel account) + { + var cookieValue = Md5Helper.Md5String( + Md5Helper.Md5String(account.tb_Employee_ID.Trim()) + Guid.NewGuid()); + + var ctx = _httpContextAccessor.HttpContext + ?? throw new InvalidOperationException("No HttpContext"); + + ctx.Response.Cookies.Append(AuthConstants.CookieToken, cookieValue, new CookieOptions + { + HttpOnly = true, + IsEssential = true, + SameSite = SameSiteMode.Lax + }); + + _cache.Set(cookieValue, account, TimeSpan.FromMinutes(AuthConstants.SessionMinutes)); + } + + public void Logout() + { + var ctx = _httpContextAccessor.HttpContext; + if (ctx == null) return; + + if (ctx.Request.Cookies.TryGetValue(AuthConstants.CookieToken, out var cookieValue) + && !string.IsNullOrEmpty(cookieValue)) + { + _cache.Remove(cookieValue); + } + + ctx.Response.Cookies.Delete(AuthConstants.CookieToken); + } + + public AccountModel? GetCurrentUser() + { + var ctx = _httpContextAccessor.HttpContext; + if (ctx == null) return null; + + if (!ctx.Request.Cookies.TryGetValue(AuthConstants.CookieToken, out var cookieValue) + || string.IsNullOrWhiteSpace(cookieValue)) + { + return null; + } + + return _cache.TryGetValue(cookieValue, out AccountModel? account) ? account : null; + } + + public void SetCaptcha(string code) => + _httpContextAccessor.HttpContext?.Session.SetString(CaptchaKey, code); + + public string? GetCaptcha() => + _httpContextAccessor.HttpContext?.Session.GetString(CaptchaKey); + + public void ClearCaptcha() => + _httpContextAccessor.HttpContext?.Session.Remove(CaptchaKey); +} diff --git a/src/Hcrm.Api/Controllers/AccountController.cs b/src/Hcrm.Api/Controllers/AccountController.cs new file mode 100644 index 0000000..384604e --- /dev/null +++ b/src/Hcrm.Api/Controllers/AccountController.cs @@ -0,0 +1,75 @@ +using Hcrm.Api.Filters; +using Hcrm.Api.Services; +using Hcrm.DTO; +using Hcrm.IService; +using Microsoft.AspNetCore.Mvc; + +namespace Hcrm.Api.Controllers; + +[ApiController] +[Route("Account")] +public class AccountController : ControllerBase +{ + private readonly IAccountService _accountService; + private readonly ILoginTokenService _loginToken; + + public AccountController(IAccountService accountService, ILoginTokenService loginToken) + { + _accountService = accountService; + _loginToken = loginToken; + } + + /// + /// 获取图形验证码。生成 4 位验证码写入 Session,返回 Base64 格式的 GIF 图片。 + /// + [HttpGet("CheckCodeImage")] + [NoLoginRequired] + public IActionResult CheckCodeImage() + { + var code = CaptchaImageService.MakeCode(4); + _loginToken.SetCaptcha(code); + var base64 = CaptchaImageService.ToBase64Gif(code); + return Ok(ApiResult.Success(base64)); + } + + /// + /// 用户登录(含验证码)。请求体为 application/x-www-form-urlencoded,校验 Session 中的验证码,登录后写入 Cookie。 + /// + /// 用户名或工号 + /// 登录密码(明文,服务端 MD5 后与库中比对) + /// 图形验证码 + [HttpPost("Login")] + [NoLoginRequired] + public Task Login([FromForm] string? UserName, [FromForm] string? Password, [FromForm] string? code) => + _accountService.LoginAsync(UserName ?? "", Password ?? "", code, requireCaptcha: true); + /// + /// 用户登录(无验证码)。与 Login 相同但不校验图形验证码,常用于内部或测试场景。 + /// + /// 用户名或工号 + /// 登录密码 + [HttpPost("LoginByNoCode")] + [NoLoginRequired] + public Task LoginByNoCode([FromForm] string? UserName, [FromForm] string? Password) => + _accountService.LoginAsync(UserName ?? "", Password ?? "", null, requireCaptcha: false); + /// + /// 检查当前是否已登录。根据 Cookie 中的 token 返回当前用户信息;未登录时 status 为 401。 + /// + [HttpPost("IsLogin")] + public Task IsLogin() => _accountService.IsLoginAsync(); + + /// + /// 退出登录,清除 Cookie 与内存中的会话信息(与 hcrm4 接口命名保持一致)。 + /// + /// + [HttpPost("WXLogOut")] + public Task WXLogOut() => _accountService.LogoutAsync(); + /// + /// 修改密码。需已登录,校验原密码后更新并刷新登录 Cookie。 + /// + /// 员工 ID 或工号 + /// 原密码(明文) + /// 新密码(明文) + [HttpPost("ModifyPwd")] + public Task ModifyPwd([FromForm] string? EmployeeID, [FromForm] string? Password, [FromForm] string? NewPassword) => + _accountService.ModifyPwdAsync(EmployeeID ?? "", Password ?? "", NewPassword ?? ""); +} diff --git a/src/Hcrm.Api/Controllers/ShopRenovateController.cs b/src/Hcrm.Api/Controllers/ShopRenovateController.cs new file mode 100644 index 0000000..21018ea --- /dev/null +++ b/src/Hcrm.Api/Controllers/ShopRenovateController.cs @@ -0,0 +1,65 @@ +using Hcrm.DTO; +using Hcrm.IService; +using Microsoft.AspNetCore.Mvc; + +namespace Hcrm.Api.Controllers; + +/// +/// 店铺装修(页面列表 / 发布 / 风格设置),接口风格对齐 hcrm4 AppletStoreDecoration。 +/// +[ApiController] +[Route("ShopRenovate")] +public class ShopRenovateController : ControllerBase +{ + private readonly IShopRenovateService _service; + + public ShopRenovateController(IShopRenovateService service) + { + _service = service; + } + + /// 页面列表,支持按页面名称模糊查询。 + [HttpGet("GetPageList")] + public Task GetPageList([FromQuery] string? pageName) => + _service.GetPageListAsync(pageName); + + /// 页面详情(含装修 JSON)。 + [HttpGet("GetPage")] + public Task GetPage([FromQuery] string? id) => + _service.GetPageAsync(id ?? ""); + + /// 新增页面。 + [HttpPost("CreatePage")] + public Task CreatePage([FromBody] ShopRenovatePageSaveRequest request) => + _service.CreatePageAsync(request); + + /// 保存页面(名称、配置等)。 + [HttpPost("UpdatePage")] + public Task UpdatePage([FromBody] ShopRenovatePageSaveRequest request) => + _service.UpdatePageAsync(request); + + /// 删除页面(软删除)。 + [HttpPost("DeletePage")] + public Task DeletePage([FromForm] string? id) => + _service.DeletePageAsync(id ?? ""); + + /// 发布页面。 + [HttpPost("PublishPage")] + public Task PublishPage([FromForm] string? id) => + _service.PublishPageAsync(id ?? ""); + + /// 取消发布。 + [HttpPost("UnpublishPage")] + public Task UnpublishPage([FromForm] string? id) => + _service.UnpublishPageAsync(id ?? ""); + + /// 获取店铺风格配置。 + [HttpGet("GetStyleConfig")] + public Task GetStyleConfig() => + _service.GetStyleConfigAsync(); + + /// 保存店铺风格配置。 + [HttpPost("SaveStyleConfig")] + public Task SaveStyleConfig([FromBody] ShopRenovateStyleDto request) => + _service.SaveStyleConfigAsync(request.StyleConfigJson); +} diff --git a/src/Hcrm.Api/DependencyInjection.cs b/src/Hcrm.Api/DependencyInjection.cs new file mode 100644 index 0000000..c4fb553 --- /dev/null +++ b/src/Hcrm.Api/DependencyInjection.cs @@ -0,0 +1,15 @@ +using Hcrm.Api.Auth; +using Hcrm.IService; +using Microsoft.Extensions.DependencyInjection; + +namespace Hcrm.Api; + +public static class DependencyInjection +{ + public static IServiceCollection AddHcrmApi(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + return services; + } +} diff --git a/src/Hcrm.Api/Filters/ApiExceptionFilter.cs b/src/Hcrm.Api/Filters/ApiExceptionFilter.cs new file mode 100644 index 0000000..c926e6a --- /dev/null +++ b/src/Hcrm.Api/Filters/ApiExceptionFilter.cs @@ -0,0 +1,34 @@ +using Hcrm.DTO; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Hcrm.Api.Filters; + +/// +/// 未捕获异常时返回 JSON(对齐 hcrm4),避免前端只看到 HTTP 500 + text/plain +/// +public sealed class ApiExceptionFilter : IExceptionFilter +{ + private readonly ILogger _logger; + + public ApiExceptionFilter(ILogger logger) + { + _logger = logger; + } + + public void OnException(ExceptionContext context) + { + _logger.LogError(context.Exception, "Unhandled API exception: {Path}", context.HttpContext.Request.Path); + + var path = context.HttpContext.Request.Path.Value ?? ""; + var message = path.Contains("Login", StringComparison.OrdinalIgnoreCase) + ? "登录异常!" + : "系统异常,请稍后重试"; + + context.Result = new JsonResult(ApiResult.Fail(message)) + { + StatusCode = StatusCodes.Status200OK + }; + context.ExceptionHandled = true; + } +} diff --git a/src/Hcrm.Api/Filters/LoginRequiredFilter.cs b/src/Hcrm.Api/Filters/LoginRequiredFilter.cs new file mode 100644 index 0000000..eae0b07 --- /dev/null +++ b/src/Hcrm.Api/Filters/LoginRequiredFilter.cs @@ -0,0 +1,37 @@ +using Hcrm.DTO; +using Hcrm.IService; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Hcrm.Api.Filters; + +public class LoginRequiredFilter : IAsyncActionFilter +{ + private readonly ILoginTokenService _loginToken; + + public LoginRequiredFilter(ILoginTokenService loginToken) + { + _loginToken = loginToken; + } + + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var endpoint = context.HttpContext.GetEndpoint(); + if (endpoint?.Metadata.GetMetadata() != null) + { + await next(); + return; + } + + if (_loginToken.GetCurrentUser() == null) + { + context.Result = new JsonResult(ApiResult.NotLoggedIn()) + { + StatusCode = StatusCodes.Status200OK + }; + return; + } + + await next(); + } +} diff --git a/src/Hcrm.Api/Filters/NoLoginRequiredAttribute.cs b/src/Hcrm.Api/Filters/NoLoginRequiredAttribute.cs new file mode 100644 index 0000000..79708db --- /dev/null +++ b/src/Hcrm.Api/Filters/NoLoginRequiredAttribute.cs @@ -0,0 +1,4 @@ +namespace Hcrm.Api.Filters; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public sealed class NoLoginRequiredAttribute : Attribute; diff --git a/src/Hcrm.Api/Hcrm.Api.csproj b/src/Hcrm.Api/Hcrm.Api.csproj new file mode 100644 index 0000000..bc9e269 --- /dev/null +++ b/src/Hcrm.Api/Hcrm.Api.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + + false + + + + + + + + + + + + + + + + + + diff --git a/src/Hcrm.Api/Program.cs b/src/Hcrm.Api/Program.cs new file mode 100644 index 0000000..1392d22 --- /dev/null +++ b/src/Hcrm.Api/Program.cs @@ -0,0 +1,48 @@ +using Hcrm.Api; +using Hcrm.Api.Filters; +using Hcrm.Repository; +using Hcrm.Service; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddHcrmRepository(builder.Configuration); +builder.Services.AddHcrmService(); +builder.Services.AddHcrmApi(); +builder.Services.AddHttpContextAccessor(); +builder.Services.AddMemoryCache(); +builder.Services.AddDistributedMemoryCache(); +builder.Services.AddSession(options => +{ + options.Cookie.Name = ".Hcrm.Session"; + options.IdleTimeout = TimeSpan.FromMinutes(30); +}); +builder.Services.AddControllers(options => options.Filters.Add()) + .AddJsonOptions(options => + { + options.JsonSerializerOptions.PropertyNamingPolicy = null; + options.JsonSerializerOptions.DictionaryKeyPolicy = null; + }); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + policy.AllowAnyHeader().AllowAnyMethod().AllowCredentials().SetIsOriginAllowed(_ => true)); +}); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseCors(); +app.UseSession(); +app.UseStaticFiles(); +app.UseRouting(); +app.MapControllers(); +app.MapGet("/", () => Results.Redirect("/login/index.html")); + +app.Run(); diff --git a/src/Hcrm.Api/Properties/launchSettings.json b/src/Hcrm.Api/Properties/launchSettings.json new file mode 100644 index 0000000..2df990f --- /dev/null +++ b/src/Hcrm.Api/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:33501", + "sslPort": 44301 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5289", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7268;http://localhost:5289", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Hcrm.Api/Services/CaptchaImageService.cs b/src/Hcrm.Api/Services/CaptchaImageService.cs new file mode 100644 index 0000000..5448648 --- /dev/null +++ b/src/Hcrm.Api/Services/CaptchaImageService.cs @@ -0,0 +1,41 @@ +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace Hcrm.Api.Services; + +public static class CaptchaImageService +{ + private static readonly Font Font = SystemFonts.CreateFont("Arial", 16, FontStyle.Bold); + + public static string MakeCode(int length = 4) + { + const string chars = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ"; + var random = Random.Shared; + return new string(Enumerable.Range(0, length).Select(_ => chars[random.Next(chars.Length)]).ToArray()); + } + + public static string ToBase64Gif(string code) + { + using var image = new Image(Math.Max(80, code.Length * 18), 32); + image.Mutate(ctx => + { + ctx.Fill(Color.White); + var random = Random.Shared; + for (var i = 0; i < 6; i++) + { + ctx.DrawLine(Color.Silver, 1f, + new PointF(random.Next(image.Width), random.Next(image.Height)), + new PointF(random.Next(image.Width), random.Next(image.Height))); + } + ctx.DrawText(code, Font, Color.DarkRed, new PointF(8, 6)); + }); + + using var ms = new MemoryStream(); + image.Save(ms, new GifEncoder()); + return Convert.ToBase64String(ms.ToArray()); + } +} diff --git a/src/Hcrm.Api/appsettings.Development.json b/src/Hcrm.Api/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/Hcrm.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Hcrm.Api/appsettings.json b/src/Hcrm.Api/appsettings.json new file mode 100644 index 0000000..34065c3 --- /dev/null +++ b/src/Hcrm.Api/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DBContainer": "Server=49.234.67.43;Database=KHCRM_SLYY2022_Dev;User Id=sa;Password=kc!@#2020!;MultipleActiveResultSets=True;TrustServerCertificate=True;Connect Timeout=30" + } +} diff --git a/src/Hcrm.Api/wwwroot/login/index.html b/src/Hcrm.Api/wwwroot/login/index.html new file mode 100644 index 0000000..995ebb9 --- /dev/null +++ b/src/Hcrm.Api/wwwroot/login/index.html @@ -0,0 +1,112 @@ + + + + + + HCRM 登录 + + + +
+

康策 HCRM

+
+ + +
+
+ + +
+
+ +
+ + 验证码 +
+
+ +

+
+ + + diff --git a/src/Hcrm.Api/wwwroot/shop/index.html b/src/Hcrm.Api/wwwroot/shop/index.html new file mode 100644 index 0000000..f78ae6e --- /dev/null +++ b/src/Hcrm.Api/wwwroot/shop/index.html @@ -0,0 +1,336 @@ + + + + + + 店铺装修 - HCRM + + + +
+

店铺装修

+
+ + +
+
+ +
+
+ + +
+ +
+

风格设置

+
+ + +
+ + +
+ +
+
+
+ 查找: + + +
+ +
+

+ + + + + + + + + + + +
序号页面名称最近发布时间页面状态操作
+ +
+
+ + + + + + diff --git a/src/Hcrm.DTO/AccountModel.cs b/src/Hcrm.DTO/AccountModel.cs new file mode 100644 index 0000000..40ee3a5 --- /dev/null +++ b/src/Hcrm.DTO/AccountModel.cs @@ -0,0 +1,18 @@ +namespace Hcrm.DTO; + +public class AccountModel +{ + public string tb_Employee_ID { get; set; } = string.Empty; + public string EmployeeID { get; set; } = string.Empty; + public string TrueName { get; set; } = string.Empty; + public string? Photo { get; set; } + public string? HospitalID { get; set; } + public string? HospitalName { get; set; } + public string? DepartmentID { get; set; } + public string[]? DepartmentIDList { get; set; } + public string? RoleID { get; set; } + public string? RoleCode { get; set; } + public string? ConsultGroupID { get; set; } + public string? WardAreaID { get; set; } + public string? MedicalUnit { get; set; } +} diff --git a/src/Hcrm.DTO/ApiResult.cs b/src/Hcrm.DTO/ApiResult.cs new file mode 100644 index 0000000..583b0d3 --- /dev/null +++ b/src/Hcrm.DTO/ApiResult.cs @@ -0,0 +1,19 @@ +using Hcrm.Model; + +namespace Hcrm.DTO; + +public class ApiResult +{ + public int status { get; set; } + public string? message { get; set; } + public object? data { get; set; } + + public static ApiResult Success(object? data = null, string? message = null) => + new() { status = AuthConstants.SuccessStatus, message = message, data = data }; + + public static ApiResult Fail(string message) => + new() { status = AuthConstants.FailStatus, message = message, data = null }; + + public static ApiResult NotLoggedIn(string message = "未登录或登录已过期") => + new() { status = AuthConstants.NotLoggedInCode, message = message, data = null }; +} diff --git a/src/Hcrm.DTO/EmployeeDto.cs b/src/Hcrm.DTO/EmployeeDto.cs new file mode 100644 index 0000000..4c22d1f --- /dev/null +++ b/src/Hcrm.DTO/EmployeeDto.cs @@ -0,0 +1,28 @@ +namespace Hcrm.DTO; + +public class EmployeeDto +{ + public string tb_Employee_ID { get; set; } = string.Empty; + public string EmployeeID { get; set; } = string.Empty; + public string EmployeeName { get; set; } = string.Empty; + public int? Sex { get; set; } + public string? Photo { get; set; } + public string? Mobile { get; set; } + public string? Telephone { get; set; } + public string? HospitalID { get; set; } + public string? HospitalName { get; set; } + public string? DepartmentID { get; set; } + public string? ConsultGroupID { get; set; } + public SysRoleDto? Role { get; set; } + public int? LookMobile { get; set; } + public int? LookCredentialCode { get; set; } + public bool LookName { get; set; } = true; + public bool IsDoctor { get; set; } +} + +public class SysRoleDto +{ + public string tb_SysRole_ID { get; set; } = string.Empty; + public string? RCode { get; set; } + public string? RoleName { get; set; } +} diff --git a/src/Hcrm.DTO/Hcrm.DTO.csproj b/src/Hcrm.DTO/Hcrm.DTO.csproj new file mode 100644 index 0000000..de7df1b --- /dev/null +++ b/src/Hcrm.DTO/Hcrm.DTO.csproj @@ -0,0 +1,10 @@ + + + net8.0 + enable + enable + + + + + diff --git a/src/Hcrm.DTO/LoginRequest.cs b/src/Hcrm.DTO/LoginRequest.cs new file mode 100644 index 0000000..6084eb8 --- /dev/null +++ b/src/Hcrm.DTO/LoginRequest.cs @@ -0,0 +1,14 @@ +namespace Hcrm.DTO; + +/// +/// 登录请求(兼容 form 与 JSON,字段名对齐 hcrm4 / hcrm11) +/// +public class LoginRequest +{ + public string? UserName { get; set; } + public string? Password { get; set; } + public string? code { get; set; } + public string? Code { get; set; } + + public string ResolvedCode => code ?? Code ?? ""; +} diff --git a/src/Hcrm.DTO/ShopRenovateDto.cs b/src/Hcrm.DTO/ShopRenovateDto.cs new file mode 100644 index 0000000..34499b7 --- /dev/null +++ b/src/Hcrm.DTO/ShopRenovateDto.cs @@ -0,0 +1,55 @@ +namespace Hcrm.DTO; + +/// 装修页面列表/详情返回。 +public class ShopRenovatePageDto +{ + /// 页面 ID。 + public string tb_ShopRenovatePage_ID { get; set; } = string.Empty; + + /// 排序号。 + public int SortNo { get; set; } + + /// 页面名称。 + public string PageName { get; set; } = string.Empty; + + /// 页面业务标识。 + public string PageKey { get; set; } = string.Empty; + + /// 发布时间字符串(yyyy-MM-dd HH:mm:ss),未发布时为 null。 + public string? PublishTime { get; set; } + + /// 发布状态:0 未发布,1 已发布。 + public int Status { get; set; } + + /// 状态中文名(未发布 / 已发布)。 + public string StatusName { get; set; } = string.Empty; + + /// 页面装修 JSON;列表接口不返回,详情接口返回。 + public string? PageConfigJson { get; set; } +} + +/// 新增/保存页面请求。 +public class ShopRenovatePageSaveRequest +{ + /// 页面 ID;新增时可空,更新时必填。 + public string? tb_ShopRenovatePage_ID { get; set; } + + /// 页面名称。 + public string? PageName { get; set; } + + /// 页面业务标识。 + public string? PageKey { get; set; } + + /// 排序号,可空时新增默认为 0。 + public int? SortNo { get; set; } + + /// 页面装修 JSON。 + public string? PageConfigJson { get; set; } +} + +/// 全店风格配置读写。 +public class ShopRenovateStyleDto +{ + /// 风格 JSON,示例:{"theme":"red","tabCount":4,"tabStyle":1}。 + public string? StyleConfigJson { get; set; } +} diff --git a/src/Hcrm.IRepository/Hcrm.IRepository.csproj b/src/Hcrm.IRepository/Hcrm.IRepository.csproj new file mode 100644 index 0000000..de7df1b --- /dev/null +++ b/src/Hcrm.IRepository/Hcrm.IRepository.csproj @@ -0,0 +1,10 @@ + + + net8.0 + enable + enable + + + + + diff --git a/src/Hcrm.IRepository/IAccountRepository.cs b/src/Hcrm.IRepository/IAccountRepository.cs new file mode 100644 index 0000000..11988df --- /dev/null +++ b/src/Hcrm.IRepository/IAccountRepository.cs @@ -0,0 +1,17 @@ +using Hcrm.Model; + +namespace Hcrm.IRepository; + +public interface IAccountRepository +{ + Task GetLoginPromptAsync(CancellationToken ct = default); + Task EmployeeExistsAsync(string userName, CancellationToken ct = default); + Task GetEmployeeByCredentialsAsync(string userName, string hashedPassword, CancellationToken ct = default); + Task GetEmployeeByIdAsync(string employeeId, CancellationToken ct = default); + Task GetEmployeeByIdOrCodeAsync(string employeeId, CancellationToken ct = default); + Task UpdateEmployeePasswordAsync(string employeeId, string hashedPassword, CancellationToken ct = default); + Task GetDepartmentIdsAsync(string employeeId, CancellationToken ct = default); + Task GetRoleByIdAsync(string roleId, CancellationToken ct = default); + Task GetHospitalNameAsync(string hospitalId, CancellationToken ct = default); + Task GetDataRightByEmployeeIdAsync(string employeeId, CancellationToken ct = default); +} diff --git a/src/Hcrm.IRepository/IShopRenovateRepository.cs b/src/Hcrm.IRepository/IShopRenovateRepository.cs new file mode 100644 index 0000000..f4aa385 --- /dev/null +++ b/src/Hcrm.IRepository/IShopRenovateRepository.cs @@ -0,0 +1,31 @@ +using Hcrm.Model; + +namespace Hcrm.IRepository; + +/// 店铺装修数据访问(tb_ShopRenovatePage / tb_ShopRenovateStyle)。 +public interface IShopRenovateRepository +{ + /// 查询未删除页面列表,可按 PageName 模糊筛选,按 SortNo、CreateTime 排序。 + Task> GetPageListAsync(string? pageName, CancellationToken ct = default); + + /// 按 ID 查询单条未删除页面。 + Task GetPageByIdAsync(string id, CancellationToken ct = default); + + /// 插入新页面。 + Task InsertPageAsync(TbShopRenovatePage entity, CancellationToken ct = default); + + /// 更新页面字段(需已存在且未删除)。 + Task UpdatePageAsync(TbShopRenovatePage entity, CancellationToken ct = default); + + /// 软删除页面(IsDeleted=true)。 + Task SoftDeletePageAsync(string id, CancellationToken ct = default); + + /// 更新发布状态与发布时间。 + Task SetPageStatusAsync(string id, int status, DateTime? publishTime, CancellationToken ct = default); + + /// 获取 default 风格配置。 + Task GetStyleAsync(CancellationToken ct = default); + + /// 保存 default 风格配置(不存在则插入)。 + Task SaveStyleAsync(string styleConfigJson, CancellationToken ct = default); +} diff --git a/src/Hcrm.IService/Hcrm.IService.csproj b/src/Hcrm.IService/Hcrm.IService.csproj new file mode 100644 index 0000000..88fc39d --- /dev/null +++ b/src/Hcrm.IService/Hcrm.IService.csproj @@ -0,0 +1,11 @@ + + + net8.0 + enable + enable + + + + + + diff --git a/src/Hcrm.IService/IAccountService.cs b/src/Hcrm.IService/IAccountService.cs new file mode 100644 index 0000000..2384819 --- /dev/null +++ b/src/Hcrm.IService/IAccountService.cs @@ -0,0 +1,11 @@ +using Hcrm.DTO; + +namespace Hcrm.IService; + +public interface IAccountService +{ + Task LoginAsync(string userName, string password, string? code, bool requireCaptcha, CancellationToken ct = default); + Task IsLoginAsync(CancellationToken ct = default); + Task LogoutAsync(); + Task ModifyPwdAsync(string employeeId, string password, string newPassword, CancellationToken ct = default); +} diff --git a/src/Hcrm.IService/ICurrentUserAccessor.cs b/src/Hcrm.IService/ICurrentUserAccessor.cs new file mode 100644 index 0000000..6b3612c --- /dev/null +++ b/src/Hcrm.IService/ICurrentUserAccessor.cs @@ -0,0 +1,8 @@ +using Hcrm.DTO; + +namespace Hcrm.IService; + +public interface ICurrentUserAccessor +{ + AccountModel? CurrentUser { get; } +} diff --git a/src/Hcrm.IService/ILoginTokenService.cs b/src/Hcrm.IService/ILoginTokenService.cs new file mode 100644 index 0000000..dc4c0b2 --- /dev/null +++ b/src/Hcrm.IService/ILoginTokenService.cs @@ -0,0 +1,13 @@ +using Hcrm.DTO; + +namespace Hcrm.IService; + +public interface ILoginTokenService +{ + void SetCookie(AccountModel account); + void Logout(); + AccountModel? GetCurrentUser(); + void SetCaptcha(string code); + string? GetCaptcha(); + void ClearCaptcha(); +} diff --git a/src/Hcrm.IService/IShopRenovateService.cs b/src/Hcrm.IService/IShopRenovateService.cs new file mode 100644 index 0000000..a0f97f3 --- /dev/null +++ b/src/Hcrm.IService/IShopRenovateService.cs @@ -0,0 +1,34 @@ +using Hcrm.DTO; + +namespace Hcrm.IService; + +/// 店铺装修业务(页面 CRUD、发布、全店风格)。 +public interface IShopRenovateService +{ + /// 页面列表。 + Task GetPageListAsync(string? pageName, CancellationToken ct = default); + + /// 页面详情(含 PageConfigJson)。 + Task GetPageAsync(string id, CancellationToken ct = default); + + /// 新增页面,默认未发布。 + Task CreatePageAsync(ShopRenovatePageSaveRequest request, CancellationToken ct = default); + + /// 保存页面名称、标识、排序、装修 JSON。 + Task UpdatePageAsync(ShopRenovatePageSaveRequest request, CancellationToken ct = default); + + /// 软删除页面。 + Task DeletePageAsync(string id, CancellationToken ct = default); + + /// 发布页面并写入 PublishTime。 + Task PublishPageAsync(string id, CancellationToken ct = default); + + /// 取消发布并清空 PublishTime。 + Task UnpublishPageAsync(string id, CancellationToken ct = default); + + /// 读取全店风格 StyleConfigJson。 + Task GetStyleConfigAsync(CancellationToken ct = default); + + /// 保存全店风格 StyleConfigJson。 + Task SaveStyleConfigAsync(string? styleConfigJson, CancellationToken ct = default); +} diff --git a/src/Hcrm.Model/AuthConstants.cs b/src/Hcrm.Model/AuthConstants.cs new file mode 100644 index 0000000..eafc24b --- /dev/null +++ b/src/Hcrm.Model/AuthConstants.cs @@ -0,0 +1,10 @@ +namespace Hcrm.Model; + +public static class AuthConstants +{ + public const string CookieToken = "kc_cookie_token"; + public const int SessionMinutes = 1440; + public const int NotLoggedInCode = 401; + public const int SuccessStatus = 1; + public const int FailStatus = 0; +} diff --git a/src/Hcrm.Model/Hcrm.Model.csproj b/src/Hcrm.Model/Hcrm.Model.csproj new file mode 100644 index 0000000..d364a52 --- /dev/null +++ b/src/Hcrm.Model/Hcrm.Model.csproj @@ -0,0 +1,7 @@ + + + net8.0 + enable + enable + + diff --git a/src/Hcrm.Model/Md5Helper.cs b/src/Hcrm.Model/Md5Helper.cs new file mode 100644 index 0000000..61072b2 --- /dev/null +++ b/src/Hcrm.Model/Md5Helper.cs @@ -0,0 +1,17 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Hcrm.Model; + +public static class Md5Helper +{ + public static string Md5String(string input) + { + if (input == null) return string.Empty; + var bytes = Encoding.UTF8.GetBytes(input); + var hash = MD5.HashData(bytes); + var sb = new StringBuilder(hash.Length * 2); + foreach (var b in hash) sb.Append(b.ToString("x2")); + return sb.ToString(); + } +} diff --git a/src/Hcrm.Model/TbDataRight.cs b/src/Hcrm.Model/TbDataRight.cs new file mode 100644 index 0000000..81f2fbc --- /dev/null +++ b/src/Hcrm.Model/TbDataRight.cs @@ -0,0 +1,10 @@ +namespace Hcrm.Model; + +public class TbDataRight +{ + public string tb_DataRight_ID { get; set; } = string.Empty; + public string? EmployeeID { get; set; } + public int? LookMobile { get; set; } + public int? LookCredentialCode { get; set; } + public bool LookName { get; set; } +} diff --git a/src/Hcrm.Model/TbDepartment.cs b/src/Hcrm.Model/TbDepartment.cs new file mode 100644 index 0000000..bc5fb7d --- /dev/null +++ b/src/Hcrm.Model/TbDepartment.cs @@ -0,0 +1,7 @@ +namespace Hcrm.Model; + +public class TbDepartment +{ + public string tb_Department_ID { get; set; } = string.Empty; + public string? DepartName { get; set; } +} diff --git a/src/Hcrm.Model/TbEmployee.cs b/src/Hcrm.Model/TbEmployee.cs new file mode 100644 index 0000000..b009cba --- /dev/null +++ b/src/Hcrm.Model/TbEmployee.cs @@ -0,0 +1,20 @@ +namespace Hcrm.Model; + +public class TbEmployee +{ + public string tb_Employee_ID { get; set; } = string.Empty; + public string? EmployeeID { get; set; } + public string? EmployeeName { get; set; } + public string? Password { get; set; } + public int? Sex { get; set; } + public string? HeadURL { get; set; } + public string? Mobile { get; set; } + public string? Telephone { get; set; } + public string? RoleID { get; set; } + public int? OnJob { get; set; } + public string? DepartmentID { get; set; } + public string? HospitalID { get; set; } + public string? ConsultGroupID { get; set; } + public string? WardAreaID { get; set; } + public string? MedicalUnit { get; set; } +} diff --git a/src/Hcrm.Model/TbEmployeeDepartMapping.cs b/src/Hcrm.Model/TbEmployeeDepartMapping.cs new file mode 100644 index 0000000..403e55c --- /dev/null +++ b/src/Hcrm.Model/TbEmployeeDepartMapping.cs @@ -0,0 +1,8 @@ +namespace Hcrm.Model; + +public class TbEmployeeDepartMapping +{ + public string tb_EmployeeDepartMapping_ID { get; set; } = string.Empty; + public string? tb_Employee_ID { get; set; } + public string? DepartmentID { get; set; } +} diff --git a/src/Hcrm.Model/TbHospital.cs b/src/Hcrm.Model/TbHospital.cs new file mode 100644 index 0000000..56044ed --- /dev/null +++ b/src/Hcrm.Model/TbHospital.cs @@ -0,0 +1,7 @@ +namespace Hcrm.Model; + +public class TbHospital +{ + public string tb_Hospital_ID { get; set; } = string.Empty; + public string? HospitalName { get; set; } +} diff --git a/src/Hcrm.Model/TbShopRenovatePage.cs b/src/Hcrm.Model/TbShopRenovatePage.cs new file mode 100644 index 0000000..c897dea --- /dev/null +++ b/src/Hcrm.Model/TbShopRenovatePage.cs @@ -0,0 +1,58 @@ +namespace Hcrm.Model; + +/// 店铺装修页面,对应表 tb_ShopRenovatePage。 +public class TbShopRenovatePage +{ + /// 页面主键,新建时由 Guid 生成(32 位无连字符)。 + public string tb_ShopRenovatePage_ID { get; set; } = string.Empty; + + /// 页面显示名称,供后台列表展示与按名称模糊查询。 + public string PageName { get; set; } = string.Empty; + + /// 页面业务标识,供前端/小程序按 key 定位页面(如 home、shop、user)。 + public string PageKey { get; set; } = string.Empty; + + /// 排序号,列表按 SortNo 升序再按 CreateTime 升序。 + public int SortNo { get; set; } + + /// 发布状态:0-未发布,1-已发布。见 + public int Status { get; set; } + + /// 发布时间;未发布或取消发布时为 null。 + public DateTime? PublishTime { get; set; } + + /// 该页面的装修布局 JSON(组件、区块等),后端整段读写不拆字段。 + public string? PageConfigJson { get; set; } + + /// 记录创建时间。 + public DateTime CreateTime { get; set; } + + /// 最后修改时间(保存、软删、发布/取消发布时更新)。 + public DateTime? UpdateTime { get; set; } + + /// 软删除标记:false 有效,true 已删除。 + public bool IsDeleted { get; set; } +} + +/// 店铺全局风格配置,对应表 tb_ShopRenovateStyle(通常仅一条 default 记录)。 +public class TbShopRenovateStyle +{ + /// 风格配置主键,业务固定使用 default 表示全店唯一配置。 + public string tb_ShopRenovateStyle_ID { get; set; } = "default"; + + /// 全店风格 JSON(如 theme、tabCount、tabStyle),与单页 PageConfigJson 分离存储。 + public string? StyleConfigJson { get; set; } + + /// 风格配置最后保存时间。 + public DateTime? UpdateTime { get; set; } +} + +/// tb_ShopRenovatePage.Status 取值。 +public static class ShopRenovateStatus +{ + /// 未发布。 + public const int Unpublished = 0; + + /// 已发布。 + public const int Published = 1; +} diff --git a/src/Hcrm.Model/TbSysConfig.cs b/src/Hcrm.Model/TbSysConfig.cs new file mode 100644 index 0000000..185b1b3 --- /dev/null +++ b/src/Hcrm.Model/TbSysConfig.cs @@ -0,0 +1,7 @@ +namespace Hcrm.Model; + +public class TbSysConfig +{ + public string tb_SysConfig_ID { get; set; } = string.Empty; + public int? LoginPrompt { get; set; } +} diff --git a/src/Hcrm.Model/TbSysRole.cs b/src/Hcrm.Model/TbSysRole.cs new file mode 100644 index 0000000..535796e --- /dev/null +++ b/src/Hcrm.Model/TbSysRole.cs @@ -0,0 +1,8 @@ +namespace Hcrm.Model; + +public class TbSysRole +{ + public string tb_SysRole_ID { get; set; } = string.Empty; + public string? RCode { get; set; } + public string? RoleName { get; set; } +} diff --git a/src/Hcrm.Repository/DependencyInjection.cs b/src/Hcrm.Repository/DependencyInjection.cs new file mode 100644 index 0000000..7d1529e --- /dev/null +++ b/src/Hcrm.Repository/DependencyInjection.cs @@ -0,0 +1,19 @@ +using Hcrm.IRepository; +using Hcrm.Repository.Repositories; +using Hcrm.Repository.SqlSugar; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using SqlSugar; + +namespace Hcrm.Repository; + +public static class DependencyInjection +{ + public static IServiceCollection AddHcrmRepository(this IServiceCollection services, IConfiguration configuration) + { + services.AddScoped(_ => SqlSugarClientFactory.CreateClient(configuration)); + services.AddScoped(); + services.AddScoped(); + return services; + } +} diff --git a/src/Hcrm.Repository/Hcrm.Repository.csproj b/src/Hcrm.Repository/Hcrm.Repository.csproj new file mode 100644 index 0000000..bf973fa --- /dev/null +++ b/src/Hcrm.Repository/Hcrm.Repository.csproj @@ -0,0 +1,16 @@ + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/src/Hcrm.Repository/Persistence/SugarQueries.cs b/src/Hcrm.Repository/Persistence/SugarQueries.cs new file mode 100644 index 0000000..5ea1119 --- /dev/null +++ b/src/Hcrm.Repository/Persistence/SugarQueries.cs @@ -0,0 +1,36 @@ +using Hcrm.Model; +using SqlSugar; + +namespace Hcrm.Repository.Persistence; + +internal static class SugarQueries +{ + public static ISugarQueryable Employees(this ISqlSugarClient db) => + db.Queryable().AS("tb_Employee"); + + public static ISugarQueryable SysConfigs(this ISqlSugarClient db) => + db.Queryable().AS("tb_SysConfig"); + + public static ISugarQueryable SysRoles(this ISqlSugarClient db) => + db.Queryable().AS("tb_SysRole"); + + public static ISugarQueryable Hospitals(this ISqlSugarClient db) => + db.Queryable().AS("tb_Hospital"); + + public static ISugarQueryable DataRights(this ISqlSugarClient db) => + db.Queryable().AS("tb_DataRight"); + + public static ISugarQueryable EmployeeDepartMappings(this ISqlSugarClient db) => + db.Queryable().AS("tb_EmployeeDepartMapping"); + + public static IUpdateable UpdateEmployees(this ISqlSugarClient db) => + db.Updateable().AS("tb_Employee"); + + /// 店铺装修页面表 tb_ShopRenovatePage。 + public static ISugarQueryable ShopRenovatePages(this ISqlSugarClient db) => + db.Queryable().AS("tb_ShopRenovatePage"); + + /// 店铺全局风格表 tb_ShopRenovateStyle。 + public static ISugarQueryable ShopRenovateStyles(this ISqlSugarClient db) => + db.Queryable().AS("tb_ShopRenovateStyle"); +} diff --git a/src/Hcrm.Repository/Repositories/AccountRepository.cs b/src/Hcrm.Repository/Repositories/AccountRepository.cs new file mode 100644 index 0000000..502d1fe --- /dev/null +++ b/src/Hcrm.Repository/Repositories/AccountRepository.cs @@ -0,0 +1,69 @@ +using Hcrm.IRepository; +using Hcrm.Model; +using Hcrm.Repository.Persistence; +using SqlSugar; + +namespace Hcrm.Repository.Repositories; + +public class AccountRepository : IAccountRepository +{ + private readonly ISqlSugarClient _db; + + public AccountRepository(ISqlSugarClient db) + { + _db = db; + } + + public Task GetLoginPromptAsync(CancellationToken ct = default) => + _db.SysConfigs().Select(x => x.LoginPrompt).FirstAsync(ct); + + public Task EmployeeExistsAsync(string userName, CancellationToken ct = default) => + _db.Employees().Where(e => e.EmployeeID == userName || e.EmployeeName == userName).AnyAsync(); + + public Task GetEmployeeByCredentialsAsync(string userName, string hashedPassword, CancellationToken ct = default) => + FirstOrDefaultAsync( + _db.Employees().Where(e => (e.EmployeeID == userName || e.EmployeeName == userName) && e.Password == hashedPassword), + ct); + + public Task GetEmployeeByIdAsync(string employeeId, CancellationToken ct = default) => + FirstOrDefaultAsync(_db.Employees().Where(e => e.tb_Employee_ID == employeeId), ct); + + public Task GetEmployeeByIdOrCodeAsync(string employeeId, CancellationToken ct = default) => + FirstOrDefaultAsync( + _db.Employees().Where(e => e.tb_Employee_ID == employeeId || e.EmployeeID == employeeId), + ct); + + public async Task UpdateEmployeePasswordAsync(string employeeId, string hashedPassword, CancellationToken ct = default) + { + var rows = await _db.UpdateEmployees() + .SetColumns(e => e.Password == hashedPassword) + .Where(e => e.tb_Employee_ID == employeeId || e.EmployeeID == employeeId) + .ExecuteCommandAsync(ct); + return rows > 0; + } + + public async Task GetDepartmentIdsAsync(string employeeId, CancellationToken ct = default) + { + var list = await _db.EmployeeDepartMappings() + .Where(m => m.tb_Employee_ID == employeeId && m.DepartmentID != null) + .Select(m => m.DepartmentID!) + .ToListAsync(ct); + return list.ToArray(); + } + + public Task GetRoleByIdAsync(string roleId, CancellationToken ct = default) => + FirstOrDefaultAsync(_db.SysRoles().Where(r => r.tb_SysRole_ID == roleId), ct); + + public Task GetHospitalNameAsync(string hospitalId, CancellationToken ct = default) => + _db.Hospitals().Where(h => h.tb_Hospital_ID == hospitalId).Select(h => h.HospitalName).FirstAsync(ct); + + public Task GetDataRightByEmployeeIdAsync(string employeeId, CancellationToken ct = default) => + FirstOrDefaultAsync(_db.DataRights().Where(d => d.EmployeeID == employeeId), ct); + + private static async Task FirstOrDefaultAsync(ISugarQueryable query, CancellationToken ct) + where T : class, new() + { + var list = await query.Take(1).ToListAsync(ct); + return list.Count > 0 ? list[0] : null; + } +} diff --git a/src/Hcrm.Repository/Repositories/ShopRenovateRepository.cs b/src/Hcrm.Repository/Repositories/ShopRenovateRepository.cs new file mode 100644 index 0000000..6acec95 --- /dev/null +++ b/src/Hcrm.Repository/Repositories/ShopRenovateRepository.cs @@ -0,0 +1,102 @@ +using Hcrm.IRepository; +using Hcrm.Model; +using Hcrm.Repository.Persistence; +using SqlSugar; + +namespace Hcrm.Repository.Repositories; + +/// 店铺装修仓储实现。 +public class ShopRenovateRepository : IShopRenovateRepository +{ + private readonly ISqlSugarClient _db; + + public ShopRenovateRepository(ISqlSugarClient db) + { + _db = db; + } + + public Task> GetPageListAsync(string? pageName, CancellationToken ct = default) + { + var query = _db.ShopRenovatePages().Where(p => !p.IsDeleted); + if (!string.IsNullOrWhiteSpace(pageName)) + query = query.Where(p => p.PageName.Contains(pageName)); + + return query.OrderBy(p => p.SortNo).OrderBy(p => p.CreateTime).ToListAsync(ct); + } + + public async Task GetPageByIdAsync(string id, CancellationToken ct = default) + { + var list = await _db.ShopRenovatePages() + .Where(p => p.tb_ShopRenovatePage_ID == id && !p.IsDeleted) + .Take(1) + .ToListAsync(ct); + return list.Count > 0 ? list[0] : null; + } + + public async Task InsertPageAsync(TbShopRenovatePage entity, CancellationToken ct = default) + { + var rows = await _db.Insertable(entity).AS("tb_ShopRenovatePage").ExecuteCommandAsync(ct); + return rows > 0; + } + + public async Task UpdatePageAsync(TbShopRenovatePage entity, CancellationToken ct = default) + { + var rows = await _db.Updateable(entity).AS("tb_ShopRenovatePage") + .Where(p => p.tb_ShopRenovatePage_ID == entity.tb_ShopRenovatePage_ID && !p.IsDeleted) + .ExecuteCommandAsync(ct); + return rows > 0; + } + + public async Task SoftDeletePageAsync(string id, CancellationToken ct = default) + { + var rows = await _db.Updateable().AS("tb_ShopRenovatePage") + .SetColumns(p => new TbShopRenovatePage { IsDeleted = true, UpdateTime = DateTime.Now }) + .Where(p => p.tb_ShopRenovatePage_ID == id && !p.IsDeleted) + .ExecuteCommandAsync(ct); + return rows > 0; + } + + public async Task SetPageStatusAsync(string id, int status, DateTime? publishTime, CancellationToken ct = default) + { + var rows = await _db.Updateable().AS("tb_ShopRenovatePage") + .SetColumns(p => new TbShopRenovatePage + { + Status = status, + PublishTime = publishTime, + UpdateTime = DateTime.Now + }) + .Where(p => p.tb_ShopRenovatePage_ID == id && !p.IsDeleted) + .ExecuteCommandAsync(ct); + return rows > 0; + } + + public async Task GetStyleAsync(CancellationToken ct = default) + { + var list = await _db.ShopRenovateStyles() + .Where(s => s.tb_ShopRenovateStyle_ID == "default") + .Take(1) + .ToListAsync(ct); + return list.Count > 0 ? list[0] : null; + } + + public async Task SaveStyleAsync(string styleConfigJson, CancellationToken ct = default) + { + var exists = await _db.ShopRenovateStyles().AnyAsync(s => s.tb_ShopRenovateStyle_ID == "default", ct); + if (!exists) + { + var rows = await _db.Insertable(new TbShopRenovateStyle + { + tb_ShopRenovateStyle_ID = "default", + StyleConfigJson = styleConfigJson, + UpdateTime = DateTime.Now + }).AS("tb_ShopRenovateStyle").ExecuteCommandAsync(ct); + return rows > 0; + } + + var updated = await _db.Updateable().AS("tb_ShopRenovateStyle") + .SetColumns(s => new TbShopRenovateStyle { StyleConfigJson = styleConfigJson, UpdateTime = DateTime.Now }) + .Where(s => s.tb_ShopRenovateStyle_ID == "default") + .ExecuteCommandAsync(ct); + return updated > 0; + } +} diff --git a/src/Hcrm.Repository/SqlSugar/SqlSugarClientFactory.cs b/src/Hcrm.Repository/SqlSugar/SqlSugarClientFactory.cs new file mode 100644 index 0000000..414b249 --- /dev/null +++ b/src/Hcrm.Repository/SqlSugar/SqlSugarClientFactory.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.Configuration; +using SqlSugar; + +namespace Hcrm.Repository.SqlSugar; + +public static class SqlSugarClientFactory +{ + public static ISqlSugarClient CreateClient(IConfiguration configuration) + { + var connectionString = configuration.GetConnectionString("DBContainer") + ?? throw new InvalidOperationException("Connection string 'DBContainer' is not configured."); + + return new SqlSugarClient(new ConnectionConfig + { + ConnectionString = connectionString, + DbType = DbType.SqlServer, + IsAutoCloseConnection = true, + InitKeyType = InitKeyType.Attribute + }); + } +} diff --git a/src/Hcrm.Service/DependencyInjection.cs b/src/Hcrm.Service/DependencyInjection.cs new file mode 100644 index 0000000..397f6de --- /dev/null +++ b/src/Hcrm.Service/DependencyInjection.cs @@ -0,0 +1,15 @@ +using Hcrm.IService; +using Hcrm.Service.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Hcrm.Service; + +public static class DependencyInjection +{ + public static IServiceCollection AddHcrmService(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + return services; + } +} diff --git a/src/Hcrm.Service/Hcrm.Service.csproj b/src/Hcrm.Service/Hcrm.Service.csproj new file mode 100644 index 0000000..edbee63 --- /dev/null +++ b/src/Hcrm.Service/Hcrm.Service.csproj @@ -0,0 +1,16 @@ + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/src/Hcrm.Service/Services/AccountService.cs b/src/Hcrm.Service/Services/AccountService.cs new file mode 100644 index 0000000..b9d6b01 --- /dev/null +++ b/src/Hcrm.Service/Services/AccountService.cs @@ -0,0 +1,177 @@ +using Hcrm.DTO; +using Hcrm.IRepository; +using Hcrm.IService; +using Hcrm.Model; + +namespace Hcrm.Service.Services; + +public class AccountService : IAccountService +{ + private readonly IAccountRepository _repo; + private readonly ILoginTokenService _loginToken; + private readonly ICurrentUserAccessor _currentUser; + + public AccountService(IAccountRepository repo, ILoginTokenService loginToken, ICurrentUserAccessor currentUser) + { + _repo = repo; + _loginToken = loginToken; + _currentUser = currentUser; + } + + public async Task LoginAsync(string userName, string password, string? code, bool requireCaptcha, CancellationToken ct = default) + { + if (requireCaptcha) + { + var sessionCode = _loginToken.GetCaptcha(); + if (string.IsNullOrEmpty(sessionCode)) + return ApiResult.Fail("验证码已过期,请更新验证码!"); + if (!string.Equals(code, sessionCode, StringComparison.Ordinal)) + return ApiResult.Fail("验证码错误!"); + _loginToken.ClearCaptcha(); + } + + if (string.IsNullOrWhiteSpace(userName)) + return ApiResult.Fail("用户名不能为空!"); + if (string.IsNullOrWhiteSpace(password)) + return ApiResult.Fail("密码不能为空!"); + + var loginPrompt = await _repo.GetLoginPromptAsync(ct); + + if (!await _repo.EmployeeExistsAsync(userName, ct)) + return ApiResult.Fail(loginPrompt == 1 ? "登录失败!" : "用户名不存在!"); + + var hashedPwd = userName == "999999" + ? Md5Helper.Md5String(password) + : Md5Helper.Md5String(password.Trim()); + + var user = await _repo.GetEmployeeByCredentialsAsync(userName, hashedPwd, ct); + if (user == null) + return ApiResult.Fail(loginPrompt == 1 ? "登录失败!" : "密码错误!"); + + if (user.OnJob == 2) + return ApiResult.Fail("账户被系统禁用!"); + + var account = await BuildAccountModelAsync(user, ct); + var dto = await BuildEmployeeDtoAsync(user, ct); + _loginToken.SetCookie(account); + return ApiResult.Success(dto); + } + + public async Task IsLoginAsync(CancellationToken ct = default) + { + var current = _currentUser.CurrentUser; + if (current == null) + return ApiResult.NotLoggedIn(); + + var user = await _repo.GetEmployeeByIdAsync(current.tb_Employee_ID, ct); + if (user == null) + return ApiResult.Fail("用户名不存在!"); + + var dto = await BuildEmployeeDtoAsync(user, ct); + return ApiResult.Success(dto); + } + + public Task LogoutAsync() + { + _loginToken.Logout(); + return Task.FromResult(ApiResult.Success()); + } + + public async Task ModifyPwdAsync(string employeeId, string password, string newPassword, CancellationToken ct = default) + { + var current = _currentUser.CurrentUser; + if (current == null) + return ApiResult.NotLoggedIn(); + + var user = await _repo.GetEmployeeByIdOrCodeAsync(employeeId, ct); + if (user == null) + return ApiResult.Fail("用户ID错误"); + + if (user.Password != Md5Helper.Md5String(password)) + return ApiResult.Fail("原密码错误"); + + var newHashed = Md5Helper.Md5String(newPassword); + await _repo.UpdateEmployeePasswordAsync(user.tb_Employee_ID, newHashed, ct); + + user.Password = newHashed; + var account = await BuildAccountModelAsync(user, ct); + _loginToken.SetCookie(account); + return ApiResult.Success(""); + } + + private async Task BuildAccountModelAsync(TbEmployee user, CancellationToken ct) + { + var departList = await _repo.GetDepartmentIdsAsync(user.tb_Employee_ID, ct); + + string? roleCode = null; + if (!string.IsNullOrEmpty(user.RoleID)) + roleCode = (await _repo.GetRoleByIdAsync(user.RoleID, ct))?.RCode; + + string? hospitalName = null; + if (!string.IsNullOrEmpty(user.HospitalID)) + hospitalName = await _repo.GetHospitalNameAsync(user.HospitalID, ct); + + return new AccountModel + { + tb_Employee_ID = user.tb_Employee_ID, + EmployeeID = user.EmployeeID ?? "", + TrueName = user.EmployeeName ?? "", + Photo = user.HeadURL, + HospitalID = user.HospitalID, + HospitalName = hospitalName, + DepartmentID = user.DepartmentID, + DepartmentIDList = departList, + RoleID = user.RoleID, + RoleCode = roleCode, + ConsultGroupID = user.ConsultGroupID, + WardAreaID = user.WardAreaID, + MedicalUnit = user.MedicalUnit + }; + } + + private async Task BuildEmployeeDtoAsync(TbEmployee user, CancellationToken ct) + { + SysRoleDto? roleDto = null; + var isDoctor = false; + if (!string.IsNullOrEmpty(user.RoleID)) + { + var role = await _repo.GetRoleByIdAsync(user.RoleID, ct); + if (role != null) + { + roleDto = new SysRoleDto + { + tb_SysRole_ID = role.tb_SysRole_ID, + RCode = role.RCode, + RoleName = role.RoleName + }; + isDoctor = role.RoleName == "医生"; + } + } + + string? hospitalName = null; + if (!string.IsNullOrEmpty(user.HospitalID)) + hospitalName = await _repo.GetHospitalNameAsync(user.HospitalID, ct); + + var dataRight = await _repo.GetDataRightByEmployeeIdAsync(user.tb_Employee_ID, ct); + + return new EmployeeDto + { + tb_Employee_ID = user.tb_Employee_ID, + EmployeeID = user.EmployeeID ?? "", + EmployeeName = user.EmployeeName ?? "", + Sex = user.Sex, + Photo = user.HeadURL, + Mobile = user.Mobile, + Telephone = user.Telephone ?? "", + HospitalID = user.HospitalID, + HospitalName = hospitalName, + DepartmentID = user.DepartmentID, + ConsultGroupID = user.ConsultGroupID, + Role = roleDto ?? new SysRoleDto(), + LookMobile = dataRight?.LookMobile, + LookCredentialCode = dataRight?.LookCredentialCode, + LookName = dataRight?.LookName ?? true, + IsDoctor = isDoctor + }; + } +} diff --git a/src/Hcrm.Service/Services/ShopRenovateService.cs b/src/Hcrm.Service/Services/ShopRenovateService.cs new file mode 100644 index 0000000..79186ad --- /dev/null +++ b/src/Hcrm.Service/Services/ShopRenovateService.cs @@ -0,0 +1,172 @@ +using Hcrm.DTO; +using Hcrm.IRepository; +using Hcrm.IService; +using Hcrm.Model; + +namespace Hcrm.Service.Services; + +/// 店铺装修:页面管理与全店风格配置。 +public class ShopRenovateService : IShopRenovateService +{ + private readonly IShopRenovateRepository _repo; + + public ShopRenovateService(IShopRenovateRepository repo) + { + _repo = repo; + } + + public async Task GetPageListAsync(string? pageName, CancellationToken ct = default) + { + var list = await _repo.GetPageListAsync(pageName, ct); + var dtoList = list.Select(ToListDto).ToList(); + return ApiResult.Success(dtoList); + } + + public async Task GetPageAsync(string id, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(id)) + return ApiResult.Fail("页面ID不能为空!"); + + var page = await _repo.GetPageByIdAsync(id, ct); + if (page == null) + return ApiResult.Fail("页面不存在!"); + + return ApiResult.Success(ToDetailDto(page)); + } + + public async Task CreatePageAsync(ShopRenovatePageSaveRequest request, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(request.PageName)) + return ApiResult.Fail("页面名称不能为空!"); + if (string.IsNullOrWhiteSpace(request.PageKey)) + return ApiResult.Fail("页面标识不能为空!"); + + var entity = new TbShopRenovatePage + { + tb_ShopRenovatePage_ID = Guid.NewGuid().ToString("N"), + PageName = request.PageName.Trim(), + PageKey = request.PageKey.Trim(), + SortNo = request.SortNo ?? 0, + Status = ShopRenovateStatus.Unpublished, + PageConfigJson = request.PageConfigJson, + CreateTime = DateTime.Now + }; + + if (!await _repo.InsertPageAsync(entity, ct)) + return ApiResult.Fail("新增失败!"); + + return ApiResult.Success(ToDetailDto(entity)); + } + + public async Task UpdatePageAsync(ShopRenovatePageSaveRequest request, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(request.tb_ShopRenovatePage_ID)) + return ApiResult.Fail("页面ID不能为空!"); + + var page = await _repo.GetPageByIdAsync(request.tb_ShopRenovatePage_ID, ct); + if (page == null) + return ApiResult.Fail("页面不存在!"); + + if (!string.IsNullOrWhiteSpace(request.PageName)) + page.PageName = request.PageName.Trim(); + if (!string.IsNullOrWhiteSpace(request.PageKey)) + page.PageKey = request.PageKey.Trim(); + if (request.SortNo.HasValue) + page.SortNo = request.SortNo.Value; + if (request.PageConfigJson != null) + page.PageConfigJson = request.PageConfigJson; + page.UpdateTime = DateTime.Now; + + if (!await _repo.UpdatePageAsync(page, ct)) + return ApiResult.Fail("保存失败!"); + + return ApiResult.Success(ToDetailDto(page)); + } + + public async Task DeletePageAsync(string id, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(id)) + return ApiResult.Fail("页面ID不能为空!"); + + if (!await _repo.SoftDeletePageAsync(id, ct)) + return ApiResult.Fail("删除失败!"); + + return ApiResult.Success(); + } + + public async Task PublishPageAsync(string id, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(id)) + return ApiResult.Fail("页面ID不能为空!"); + + var page = await _repo.GetPageByIdAsync(id, ct); + if (page == null) + return ApiResult.Fail("页面不存在!"); + if (page.Status == ShopRenovateStatus.Published) + return ApiResult.Fail("页面已发布!"); + + if (!await _repo.SetPageStatusAsync(id, ShopRenovateStatus.Published, DateTime.Now, ct)) + return ApiResult.Fail("发布失败!"); + + page.Status = ShopRenovateStatus.Published; + page.PublishTime = DateTime.Now; + return ApiResult.Success(ToListDto(page)); + } + + public async Task UnpublishPageAsync(string id, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(id)) + return ApiResult.Fail("页面ID不能为空!"); + + var page = await _repo.GetPageByIdAsync(id, ct); + if (page == null) + return ApiResult.Fail("页面不存在!"); + if (page.Status == ShopRenovateStatus.Unpublished) + return ApiResult.Fail("页面未发布!"); + + if (!await _repo.SetPageStatusAsync(id, ShopRenovateStatus.Unpublished, null, ct)) + return ApiResult.Fail("取消发布失败!"); + + page.Status = ShopRenovateStatus.Unpublished; + page.PublishTime = null; + return ApiResult.Success(ToListDto(page)); + } + + public async Task GetStyleConfigAsync(CancellationToken ct = default) + { + var style = await _repo.GetStyleAsync(ct); + return ApiResult.Success(new ShopRenovateStyleDto + { + StyleConfigJson = style?.StyleConfigJson + }); + } + + public async Task SaveStyleConfigAsync(string? styleConfigJson, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(styleConfigJson)) + return ApiResult.Fail("风格配置不能为空!"); + + if (!await _repo.SaveStyleAsync(styleConfigJson, ct)) + return ApiResult.Fail("保存失败!"); + + return ApiResult.Success(); + } + + private static ShopRenovatePageDto ToListDto(TbShopRenovatePage p) => new() + { + tb_ShopRenovatePage_ID = p.tb_ShopRenovatePage_ID, + SortNo = p.SortNo, + PageName = p.PageName, + PageKey = p.PageKey, + PublishTime = p.PublishTime?.ToString("yyyy-MM-dd HH:mm:ss"), + Status = p.Status, + StatusName = p.Status == ShopRenovateStatus.Published ? "已发布" : "未发布" + }; + + private static ShopRenovatePageDto ToDetailDto(TbShopRenovatePage p) + { + var dto = ToListDto(p); + dto.PageConfigJson = p.PageConfigJson; + return dto; + } +}