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;
+ }
+}