添加项目文件。

master
dongbolin 1 week ago
parent 542d55a56d
commit 39e2692417

@ -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 |

@ -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

@ -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, -- 全店风格 JSONtheme/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

@ -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();
}

@ -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);
}

@ -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;
}
/// <summary>
/// 获取图形验证码。生成 4 位验证码写入 Session返回 Base64 格式的 GIF 图片。
/// </summary>
[HttpGet("CheckCodeImage")]
[NoLoginRequired]
public IActionResult CheckCodeImage()
{
var code = CaptchaImageService.MakeCode(4);
_loginToken.SetCaptcha(code);
var base64 = CaptchaImageService.ToBase64Gif(code);
return Ok(ApiResult.Success(base64));
}
/// <summary>
/// 用户登录(含验证码)。请求体为 application/x-www-form-urlencoded校验 Session 中的验证码,登录后写入 Cookie。
/// </summary>
/// <param name="UserName">用户名或工号</param>
/// <param name="Password">登录密码(明文,服务端 MD5 后与库中比对)</param>
/// <param name="code">图形验证码</param>
[HttpPost("Login")]
[NoLoginRequired]
public Task<ApiResult> Login([FromForm] string? UserName, [FromForm] string? Password, [FromForm] string? code) =>
_accountService.LoginAsync(UserName ?? "", Password ?? "", code, requireCaptcha: true);
/// <summary>
/// 用户登录(无验证码)。与 Login 相同但不校验图形验证码,常用于内部或测试场景。
/// </summary>
/// <param name="UserName">用户名或工号</param>
/// <param name="Password">登录密码</param>
[HttpPost("LoginByNoCode")]
[NoLoginRequired]
public Task<ApiResult> LoginByNoCode([FromForm] string? UserName, [FromForm] string? Password) =>
_accountService.LoginAsync(UserName ?? "", Password ?? "", null, requireCaptcha: false);
/// <summary>
/// 检查当前是否已登录。根据 Cookie 中的 token 返回当前用户信息;未登录时 status 为 401。
/// </summary>
[HttpPost("IsLogin")]
public Task<ApiResult> IsLogin() => _accountService.IsLoginAsync();
/// <summary>
/// 退出登录,清除 Cookie 与内存中的会话信息(与 hcrm4 接口命名保持一致)。
/// </summary>
///
[HttpPost("WXLogOut")]
public Task<ApiResult> WXLogOut() => _accountService.LogoutAsync();
/// <summary>
/// 修改密码。需已登录,校验原密码后更新并刷新登录 Cookie。
/// </summary>
/// <param name="EmployeeID">员工 ID 或工号</param>
/// <param name="Password">原密码(明文)</param>
/// <param name="NewPassword">新密码(明文)</param>
[HttpPost("ModifyPwd")]
public Task<ApiResult> ModifyPwd([FromForm] string? EmployeeID, [FromForm] string? Password, [FromForm] string? NewPassword) =>
_accountService.ModifyPwdAsync(EmployeeID ?? "", Password ?? "", NewPassword ?? "");
}

@ -0,0 +1,65 @@
using Hcrm.DTO;
using Hcrm.IService;
using Microsoft.AspNetCore.Mvc;
namespace Hcrm.Api.Controllers;
/// <summary>
/// 店铺装修(页面列表 / 发布 / 风格设置),接口风格对齐 hcrm4 AppletStoreDecoration。
/// </summary>
[ApiController]
[Route("ShopRenovate")]
public class ShopRenovateController : ControllerBase
{
private readonly IShopRenovateService _service;
public ShopRenovateController(IShopRenovateService service)
{
_service = service;
}
/// <summary>页面列表,支持按页面名称模糊查询。</summary>
[HttpGet("GetPageList")]
public Task<ApiResult> GetPageList([FromQuery] string? pageName) =>
_service.GetPageListAsync(pageName);
/// <summary>页面详情(含装修 JSON。</summary>
[HttpGet("GetPage")]
public Task<ApiResult> GetPage([FromQuery] string? id) =>
_service.GetPageAsync(id ?? "");
/// <summary>新增页面。</summary>
[HttpPost("CreatePage")]
public Task<ApiResult> CreatePage([FromBody] ShopRenovatePageSaveRequest request) =>
_service.CreatePageAsync(request);
/// <summary>保存页面(名称、配置等)。</summary>
[HttpPost("UpdatePage")]
public Task<ApiResult> UpdatePage([FromBody] ShopRenovatePageSaveRequest request) =>
_service.UpdatePageAsync(request);
/// <summary>删除页面(软删除)。</summary>
[HttpPost("DeletePage")]
public Task<ApiResult> DeletePage([FromForm] string? id) =>
_service.DeletePageAsync(id ?? "");
/// <summary>发布页面。</summary>
[HttpPost("PublishPage")]
public Task<ApiResult> PublishPage([FromForm] string? id) =>
_service.PublishPageAsync(id ?? "");
/// <summary>取消发布。</summary>
[HttpPost("UnpublishPage")]
public Task<ApiResult> UnpublishPage([FromForm] string? id) =>
_service.UnpublishPageAsync(id ?? "");
/// <summary>获取店铺风格配置。</summary>
[HttpGet("GetStyleConfig")]
public Task<ApiResult> GetStyleConfig() =>
_service.GetStyleConfigAsync();
/// <summary>保存店铺风格配置。</summary>
[HttpPost("SaveStyleConfig")]
public Task<ApiResult> SaveStyleConfig([FromBody] ShopRenovateStyleDto request) =>
_service.SaveStyleConfigAsync(request.StyleConfigJson);
}

@ -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<ILoginTokenService, LoginTokenService>();
services.AddScoped<ICurrentUserAccessor, CurrentUserAccessor>();
return services;
}
}

@ -0,0 +1,34 @@
using Hcrm.DTO;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace Hcrm.Api.Filters;
/// <summary>
/// 未捕获异常时返回 JSON对齐 hcrm4避免前端只看到 HTTP 500 + text/plain
/// </summary>
public sealed class ApiExceptionFilter : IExceptionFilter
{
private readonly ILogger<ApiExceptionFilter> _logger;
public ApiExceptionFilter(ILogger<ApiExceptionFilter> 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;
}
}

@ -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<NoLoginRequiredAttribute>() != null)
{
await next();
return;
}
if (_loginToken.GetCurrentUser() == null)
{
context.Result = new JsonResult(ApiResult.NotLoggedIn())
{
StatusCode = StatusCodes.Status200OK
};
return;
}
await next();
}
}

@ -0,0 +1,4 @@
namespace Hcrm.Api.Filters;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public sealed class NoLoginRequiredAttribute : Attribute;

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<!-- SDK 9 构建 net8关闭 Static Web Assets避免 d:\ 与 D:\ 路径混用导致 wwwroot 冲突 -->
<StaticWebAssetsEnabled>false</StaticWebAssetsEnabled>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SixLabors.Fonts" Version="2.0.4" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Hcrm.DTO\Hcrm.DTO.csproj" />
<ProjectReference Include="..\Hcrm.IService\Hcrm.IService.csproj" />
<ProjectReference Include="..\Hcrm.Model\Hcrm.Model.csproj" />
<ProjectReference Include="..\Hcrm.Repository\Hcrm.Repository.csproj" />
<ProjectReference Include="..\Hcrm.Service\Hcrm.Service.csproj" />
</ItemGroup>
</Project>

@ -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<LoginRequiredFilter>())
.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();

@ -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"
}
}
}
}

@ -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<Rgba32>(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());
}
}

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

@ -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"
}
}

@ -0,0 +1,112 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>HCRM 登录</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
min-height: 100vh; display: flex; align-items: center; justify-content: center;
background: linear-gradient(135deg, #1a6fb5 0%, #0d3d6b 100%);
font-family: "Microsoft YaHei", sans-serif;
}
.card {
width: 380px; background: #fff; border-radius: 8px;
padding: 36px 32px; box-shadow: 0 8px 32px rgba(0,0,0,.2);
}
h1 { font-size: 22px; color: #1a6fb5; text-align: center; margin-bottom: 28px; }
.field { margin-bottom: 16px; }
label { display: block; font-size: 13px; color: #555; margin-bottom: 6px; }
input {
width: 100%; height: 38px; border: 1px solid #ddd; border-radius: 4px;
padding: 0 10px; font-size: 14px; outline: none;
}
input:focus { border-color: #1a6fb5; }
.captcha-row { display: flex; gap: 8px; align-items: center; }
.captcha-row input { flex: 1; }
.captcha-img {
width: 100px; height: 38px; cursor: pointer; border: 1px solid #ddd;
border-radius: 4px; background: #f5f5f5;
}
button {
width: 100%; height: 40px; margin-top: 8px; border: none; border-radius: 4px;
background: #1a6fb5; color: #fff; font-size: 15px; cursor: pointer;
}
button:hover { background: #155d9a; }
button:disabled { background: #9bbad4; cursor: not-allowed; }
.msg { margin-top: 12px; font-size: 13px; text-align: center; min-height: 18px; }
.msg.error { color: #e53935; }
.msg.ok { color: #43a047; }
</style>
</head>
<body>
<div class="card">
<h1>康策 HCRM</h1>
<div class="field">
<label>用户名</label>
<input id="userName" type="text" placeholder="工号或姓名" autocomplete="username" />
</div>
<div class="field">
<label>密码</label>
<input id="password" type="password" placeholder="密码" autocomplete="current-password" />
</div>
<div class="field">
<label>验证码</label>
<div class="captcha-row">
<input id="code" type="text" placeholder="验证码" maxlength="6" />
<img id="captchaImg" class="captcha-img" title="点击刷新" alt="验证码" />
</div>
</div>
<button id="btnLogin" type="button">登 录</button>
<p id="msg" class="msg"></p>
</div>
<script>
const $ = id => document.getElementById(id);
async function loadCaptcha() {
const res = await fetch('/Account/CheckCodeImage', { credentials: 'include' });
const json = await res.json();
if (json.status === 1 && json.data) {
$('captchaImg').src = 'data:image/gif;base64,' + json.data;
}
}
async function login() {
$('btnLogin').disabled = true;
$('msg').className = 'msg';
$('msg').textContent = '登录中...';
const body = new URLSearchParams({
UserName: $('userName').value.trim(),
Password: $('password').value,
code: $('code').value.trim()
});
try {
const res = await fetch('/Account/Login', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
credentials: 'include'
});
const json = await res.json();
if (json.status === 1) {
$('msg').className = 'msg ok';
$('msg').textContent = '登录成功,正在进入店铺装修...';
setTimeout(() => { location.href = '/shop/index.html'; }, 400);
} else {
$('msg').className = 'msg error';
$('msg').textContent = json.message || '登录失败';
loadCaptcha();
}
} catch (e) {
$('msg').className = 'msg error';
$('msg').textContent = '网络错误';
} finally {
$('btnLogin').disabled = false;
}
}
$('captchaImg').onclick = loadCaptcha;
$('btnLogin').onclick = login;
$('password').onkeydown = e => { if (e.key === 'Enter') login(); };
loadCaptcha();
</script>
</body>
</html>

@ -0,0 +1,336 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>店铺装修 - HCRM</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
min-height: 100vh; background: #f0f2f5;
font-family: "Microsoft YaHei", sans-serif; color: #333;
}
.header {
background: #1a6fb5; color: #fff; padding: 14px 24px;
display: flex; align-items: center; justify-content: space-between;
}
.header h1 { font-size: 18px; font-weight: normal; }
.header .user { font-size: 13px; opacity: .9; }
.header button {
margin-left: 12px; padding: 6px 14px; border: 1px solid rgba(255,255,255,.6);
background: transparent; color: #fff; border-radius: 4px; cursor: pointer; font-size: 13px;
}
.header button:hover { background: rgba(255,255,255,.15); }
.main { max-width: 1100px; margin: 24px auto; padding: 0 16px 40px; }
.toolbar { display: flex; gap: 10px; margin-bottom: 16px; }
.toolbar button {
padding: 8px 16px; border: 1px solid #1a6fb5; background: #fff; color: #1a6fb5;
border-radius: 4px; cursor: pointer; font-size: 13px;
}
.toolbar button:hover { background: #e8f2fa; }
.panel {
background: #fff; border-radius: 8px; padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,.06);
}
.search-row {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 16px; flex-wrap: wrap; gap: 12px;
}
.search-left { display: flex; align-items: center; gap: 10px; }
.search-left input {
width: 220px; height: 34px; border: 1px solid #ddd; border-radius: 4px; padding: 0 10px;
}
.btn {
height: 34px; padding: 0 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 13px;
}
.btn-primary { background: #1a6fb5; color: #fff; }
.btn-primary:hover { background: #155d9a; }
.btn-primary:disabled { background: #9bbad4; cursor: not-allowed; }
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th, td { padding: 12px 10px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #fafafa; color: #666; font-weight: 500; }
tr:hover td { background: #f9fcff; }
.link { color: #1a6fb5; cursor: pointer; margin-right: 12px; text-decoration: none; }
.link:hover { text-decoration: underline; }
.link.danger { color: #e53935; }
.status-published { color: #43a047; }
.status-draft { color: #888; }
.msg-bar { margin-bottom: 12px; font-size: 13px; min-height: 18px; }
.msg-bar.error { color: #e53935; }
.msg-bar.ok { color: #43a047; }
.empty { text-align: center; color: #999; padding: 40px 0; }
.modal-mask {
display: none; position: fixed; inset: 0; background: rgba(0,0,0,.4);
align-items: center; justify-content: center; z-index: 100;
}
.modal-mask.show { display: flex; }
.modal {
width: 400px; background: #fff; border-radius: 8px; padding: 24px;
box-shadow: 0 8px 24px rgba(0,0,0,.15);
}
.modal h3 { margin-bottom: 16px; font-size: 16px; }
.modal .field { margin-bottom: 14px; }
.modal label { display: block; font-size: 13px; color: #555; margin-bottom: 6px; }
.modal input, .modal textarea {
width: 100%; border: 1px solid #ddd; border-radius: 4px; padding: 8px 10px; font-size: 14px;
}
.modal textarea { min-height: 80px; resize: vertical; font-family: inherit; }
.modal-actions { margin-top: 20px; text-align: right; }
.modal-actions .btn { margin-left: 8px; }
.btn-default { background: #f5f5f5; color: #333; border: 1px solid #ddd; }
#stylePanel { display: none; margin-top: 16px; }
#stylePanel.show { display: block; }
</style>
</head>
<body>
<div class="header">
<h1>店铺装修</h1>
<div>
<span class="user" id="userInfo"></span>
<button type="button" id="btnLogout">退出</button>
</div>
</div>
<div class="main">
<div class="toolbar">
<button type="button" id="btnStyle">风格设置</button>
<button type="button" id="btnPreview">店铺预览</button>
</div>
<div class="panel" id="stylePanel">
<h3 style="margin-bottom:12px;font-size:15px;">风格设置</h3>
<div class="field">
<label>StyleConfigJson</label>
<textarea id="styleJson" placeholder='{"theme":"red","tabCount":4,"tabStyle":1}'></textarea>
</div>
<button class="btn btn-primary" type="button" id="btnSaveStyle">保存风格</button>
<span id="styleMsg" class="msg-bar"></span>
</div>
<div class="panel" style="margin-top:16px;">
<div class="search-row">
<div class="search-left">
<span>查找:</span>
<input id="keyword" type="text" placeholder="请输入页面名称" />
<button class="btn btn-primary" type="button" id="btnSearch">查询</button>
</div>
<button class="btn btn-primary" type="button" id="btnAdd">+ 新增页面</button>
</div>
<p id="listMsg" class="msg-bar"></p>
<table>
<thead>
<tr>
<th width="70">序号</th>
<th>页面名称</th>
<th width="180">最近发布时间</th>
<th width="100">页面状态</th>
<th width="220">操作</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
<p id="emptyTip" class="empty" style="display:none;">暂无数据,请先新增页面或执行建表脚本</p>
</div>
</div>
<div class="modal-mask" id="createModal">
<div class="modal">
<h3>新增页面</h3>
<div class="field">
<label>页面名称</label>
<input id="newPageName" type="text" placeholder="如:首页-生活版" />
</div>
<div class="field">
<label>页面标识 PageKey</label>
<input id="newPageKey" type="text" placeholder="home / shop / user" value="home" />
</div>
<div class="modal-actions">
<button class="btn btn-default" type="button" id="btnCancelCreate">取消</button>
<button class="btn btn-primary" type="button" id="btnConfirmCreate">确定</button>
</div>
</div>
</div>
<script>
const $ = id => document.getElementById(id);
const cred = { credentials: 'include' };
async function apiGet(url) {
const res = await fetch(url, cred);
return res.json();
}
async function apiPostForm(url, data) {
const body = new URLSearchParams(data);
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
...cred
});
return res.json();
}
async function apiPostJson(url, data) {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
...cred
});
return res.json();
}
function showMsg(el, text, ok) {
el.textContent = text || '';
el.className = 'msg-bar' + (text ? (ok ? ' ok' : ' error') : '');
}
async function ensureLogin() {
const json = await apiPostForm('/Account/IsLogin', {});
if (json.status === 401 || json.status !== 1) {
location.href = '/login/index.html';
return false;
}
$('userInfo').textContent = '您好,' + (json.data?.EmployeeName || json.data?.EmployeeID || '');
return true;
}
async function loadList() {
const name = $('keyword').value.trim();
const q = name ? '?pageName=' + encodeURIComponent(name) : '';
showMsg($('listMsg'), '加载中...', true);
try {
const json = await apiGet('/ShopRenovate/GetPageList' + q);
if (json.status !== 1) {
showMsg($('listMsg'), json.message || '加载失败', false);
return;
}
renderTable(json.data || []);
showMsg($('listMsg'), '', true);
} catch (e) {
showMsg($('listMsg'), '网络错误,请确认已执行建表脚本且 API 已启动', false);
}
}
function pick(row, a, b) {
const v = row[a] ?? row[b];
return v === undefined || v === null ? '' : v;
}
function renderTable(rows) {
const tbody = $('tableBody');
tbody.innerHTML = '';
$('emptyTip').style.display = rows.length ? 'none' : 'block';
rows.forEach(row => {
const id = pick(row, 'tb_ShopRenovatePage_ID', 'tb_ShopRenovatePage_ID');
const status = Number(pick(row, 'Status', 'status'));
const tr = document.createElement('tr');
const statusCls = status === 1 ? 'status-published' : 'status-draft';
const ops = status === 0
? `<a class="link" data-act="publish" data-id="${id}">发布</a>
<a class="link danger" data-act="delete" data-id="${id}">删除</a>`
: `<a class="link" data-act="unpublish" data-id="${id}">取消发布</a>
<a class="link danger" data-act="delete" data-id="${id}">删除</a>`;
tr.innerHTML = `
<td>${pick(row, 'SortNo', 'sortNo')}</td>
<td>${escapeHtml(pick(row, 'PageName', 'pageName'))}</td>
<td>${pick(row, 'PublishTime', 'publishTime') || '-'}</td>
<td class="${statusCls}">${escapeHtml(pick(row, 'StatusName', 'statusName'))}</td>
<td>
<a class="link" data-act="edit" data-id="${id}" data-key="${escapeHtml(pick(row, 'PageKey', 'pageKey'))}">编辑</a>
${ops}
</td>`;
tbody.appendChild(tr);
});
tbody.querySelectorAll('[data-act]').forEach(el => {
el.onclick = () => onAction(el.dataset.act, el.dataset.id, el.dataset.key);
});
}
function escapeHtml(s) {
if (!s) return '';
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
async function onAction(act, id, pageKey) {
if (act === 'edit') {
alert('页面 ID: ' + id + '\nPageKey: ' + pageKey + '\n装修画布可后续接 Vue 或扩展本页)');
return;
}
if (act === 'publish') {
const json = await apiPostForm('/ShopRenovate/PublishPage', { id });
alert(json.status === 1 ? '发布成功' : (json.message || '失败'));
if (json.status === 1) loadList();
return;
}
if (act === 'unpublish') {
if (!confirm('确定取消发布?')) return;
const json = await apiPostForm('/ShopRenovate/UnpublishPage', { id });
alert(json.status === 1 ? '已取消发布' : (json.message || '失败'));
if (json.status === 1) loadList();
return;
}
if (act === 'delete') {
if (!confirm('确定删除该页面?')) return;
const json = await apiPostForm('/ShopRenovate/DeletePage', { id });
alert(json.status === 1 ? '删除成功' : (json.message || '失败'));
if (json.status === 1) loadList();
}
}
$('btnSearch').onclick = loadList;
$('btnAdd').onclick = () => $('createModal').classList.add('show');
$('btnCancelCreate').onclick = () => $('createModal').classList.remove('show');
$('btnConfirmCreate').onclick = async () => {
const json = await apiPostJson('/ShopRenovate/CreatePage', {
PageName: $('newPageName').value.trim(),
PageKey: $('newPageKey').value.trim()
});
if (json.status === 1) {
$('createModal').classList.remove('show');
loadList();
} else {
alert(json.message || '新增失败');
}
};
$('btnStyle').onclick = async () => {
const panel = $('stylePanel');
panel.classList.toggle('show');
if (panel.classList.contains('show')) {
const json = await apiGet('/ShopRenovate/GetStyleConfig');
if (json.status === 1) {
$('styleJson').value = json.data?.StyleConfigJson || '';
}
}
};
$('btnSaveStyle').onclick = async () => {
const json = await apiPostJson('/ShopRenovate/SaveStyleConfig', {
StyleConfigJson: $('styleJson').value.trim()
});
showMsg($('styleMsg'), json.status === 1 ? '保存成功' : (json.message || '失败'), json.status === 1);
};
$('btnPreview').onclick = async () => {
const json = await apiGet('/ShopRenovate/GetPageList');
if (json.status !== 1) return alert(json.message || '加载失败');
const published = (json.data || []).find(p => p.Status === 1);
if (!published) return alert('暂无已发布页面');
alert('预览:' + published.PageName + '' + published.PageKey + '');
};
$('btnLogout').onclick = async () => {
await apiPostForm('/Account/WXLogOut', {});
location.href = '/login/index.html';
};
(async () => {
if (await ensureLogin()) loadList();
})();
</script>
</body>
</html>

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

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

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

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Hcrm.Model\Hcrm.Model.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,14 @@
namespace Hcrm.DTO;
/// <summary>
/// 登录请求(兼容 form 与 JSON字段名对齐 hcrm4 / hcrm11
/// </summary>
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 ?? "";
}

@ -0,0 +1,55 @@
namespace Hcrm.DTO;
/// <summary>装修页面列表/详情返回。</summary>
public class ShopRenovatePageDto
{
/// <summary>页面 ID。</summary>
public string tb_ShopRenovatePage_ID { get; set; } = string.Empty;
/// <summary>排序号。</summary>
public int SortNo { get; set; }
/// <summary>页面名称。</summary>
public string PageName { get; set; } = string.Empty;
/// <summary>页面业务标识。</summary>
public string PageKey { get; set; } = string.Empty;
/// <summary>发布时间字符串yyyy-MM-dd HH:mm:ss未发布时为 null。</summary>
public string? PublishTime { get; set; }
/// <summary>发布状态0 未发布1 已发布。</summary>
public int Status { get; set; }
/// <summary>状态中文名(未发布 / 已发布)。</summary>
public string StatusName { get; set; } = string.Empty;
/// <summary>页面装修 JSON列表接口不返回详情接口返回。</summary>
public string? PageConfigJson { get; set; }
}
/// <summary>新增/保存页面请求。</summary>
public class ShopRenovatePageSaveRequest
{
/// <summary>页面 ID新增时可空更新时必填。</summary>
public string? tb_ShopRenovatePage_ID { get; set; }
/// <summary>页面名称。</summary>
public string? PageName { get; set; }
/// <summary>页面业务标识。</summary>
public string? PageKey { get; set; }
/// <summary>排序号,可空时新增默认为 0。</summary>
public int? SortNo { get; set; }
/// <summary>页面装修 JSON。</summary>
public string? PageConfigJson { get; set; }
}
/// <summary>全店风格配置读写。</summary>
public class ShopRenovateStyleDto
{
/// <summary>风格 JSON示例{"theme":"red","tabCount":4,"tabStyle":1}。</summary>
public string? StyleConfigJson { get; set; }
}

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Hcrm.Model\Hcrm.Model.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,17 @@
using Hcrm.Model;
namespace Hcrm.IRepository;
public interface IAccountRepository
{
Task<int?> GetLoginPromptAsync(CancellationToken ct = default);
Task<bool> EmployeeExistsAsync(string userName, CancellationToken ct = default);
Task<TbEmployee?> GetEmployeeByCredentialsAsync(string userName, string hashedPassword, CancellationToken ct = default);
Task<TbEmployee?> GetEmployeeByIdAsync(string employeeId, CancellationToken ct = default);
Task<TbEmployee?> GetEmployeeByIdOrCodeAsync(string employeeId, CancellationToken ct = default);
Task<bool> UpdateEmployeePasswordAsync(string employeeId, string hashedPassword, CancellationToken ct = default);
Task<string[]> GetDepartmentIdsAsync(string employeeId, CancellationToken ct = default);
Task<TbSysRole?> GetRoleByIdAsync(string roleId, CancellationToken ct = default);
Task<string?> GetHospitalNameAsync(string hospitalId, CancellationToken ct = default);
Task<TbDataRight?> GetDataRightByEmployeeIdAsync(string employeeId, CancellationToken ct = default);
}

@ -0,0 +1,31 @@
using Hcrm.Model;
namespace Hcrm.IRepository;
/// <summary>店铺装修数据访问tb_ShopRenovatePage / tb_ShopRenovateStyle。</summary>
public interface IShopRenovateRepository
{
/// <summary>查询未删除页面列表,可按 PageName 模糊筛选,按 SortNo、CreateTime 排序。</summary>
Task<List<TbShopRenovatePage>> GetPageListAsync(string? pageName, CancellationToken ct = default);
/// <summary>按 ID 查询单条未删除页面。</summary>
Task<TbShopRenovatePage?> GetPageByIdAsync(string id, CancellationToken ct = default);
/// <summary>插入新页面。</summary>
Task<bool> InsertPageAsync(TbShopRenovatePage entity, CancellationToken ct = default);
/// <summary>更新页面字段(需已存在且未删除)。</summary>
Task<bool> UpdatePageAsync(TbShopRenovatePage entity, CancellationToken ct = default);
/// <summary>软删除页面IsDeleted=true。</summary>
Task<bool> SoftDeletePageAsync(string id, CancellationToken ct = default);
/// <summary>更新发布状态与发布时间。</summary>
Task<bool> SetPageStatusAsync(string id, int status, DateTime? publishTime, CancellationToken ct = default);
/// <summary>获取 default 风格配置。</summary>
Task<TbShopRenovateStyle?> GetStyleAsync(CancellationToken ct = default);
/// <summary>保存 default 风格配置(不存在则插入)。</summary>
Task<bool> SaveStyleAsync(string styleConfigJson, CancellationToken ct = default);
}

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Hcrm.DTO\Hcrm.DTO.csproj" />
<ProjectReference Include="..\Hcrm.Model\Hcrm.Model.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,11 @@
using Hcrm.DTO;
namespace Hcrm.IService;
public interface IAccountService
{
Task<ApiResult> LoginAsync(string userName, string password, string? code, bool requireCaptcha, CancellationToken ct = default);
Task<ApiResult> IsLoginAsync(CancellationToken ct = default);
Task<ApiResult> LogoutAsync();
Task<ApiResult> ModifyPwdAsync(string employeeId, string password, string newPassword, CancellationToken ct = default);
}

@ -0,0 +1,8 @@
using Hcrm.DTO;
namespace Hcrm.IService;
public interface ICurrentUserAccessor
{
AccountModel? CurrentUser { get; }
}

@ -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();
}

@ -0,0 +1,34 @@
using Hcrm.DTO;
namespace Hcrm.IService;
/// <summary>店铺装修业务(页面 CRUD、发布、全店风格。</summary>
public interface IShopRenovateService
{
/// <summary>页面列表。</summary>
Task<ApiResult> GetPageListAsync(string? pageName, CancellationToken ct = default);
/// <summary>页面详情(含 PageConfigJson。</summary>
Task<ApiResult> GetPageAsync(string id, CancellationToken ct = default);
/// <summary>新增页面,默认未发布。</summary>
Task<ApiResult> CreatePageAsync(ShopRenovatePageSaveRequest request, CancellationToken ct = default);
/// <summary>保存页面名称、标识、排序、装修 JSON。</summary>
Task<ApiResult> UpdatePageAsync(ShopRenovatePageSaveRequest request, CancellationToken ct = default);
/// <summary>软删除页面。</summary>
Task<ApiResult> DeletePageAsync(string id, CancellationToken ct = default);
/// <summary>发布页面并写入 PublishTime。</summary>
Task<ApiResult> PublishPageAsync(string id, CancellationToken ct = default);
/// <summary>取消发布并清空 PublishTime。</summary>
Task<ApiResult> UnpublishPageAsync(string id, CancellationToken ct = default);
/// <summary>读取全店风格 StyleConfigJson。</summary>
Task<ApiResult> GetStyleConfigAsync(CancellationToken ct = default);
/// <summary>保存全店风格 StyleConfigJson。</summary>
Task<ApiResult> SaveStyleConfigAsync(string? styleConfigJson, CancellationToken ct = default);
}

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

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

@ -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();
}
}

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

@ -0,0 +1,7 @@
namespace Hcrm.Model;
public class TbDepartment
{
public string tb_Department_ID { get; set; } = string.Empty;
public string? DepartName { get; set; }
}

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

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

@ -0,0 +1,7 @@
namespace Hcrm.Model;
public class TbHospital
{
public string tb_Hospital_ID { get; set; } = string.Empty;
public string? HospitalName { get; set; }
}

@ -0,0 +1,58 @@
namespace Hcrm.Model;
/// <summary>店铺装修页面,对应表 tb_ShopRenovatePage。</summary>
public class TbShopRenovatePage
{
/// <summary>页面主键,新建时由 Guid 生成32 位无连字符)。</summary>
public string tb_ShopRenovatePage_ID { get; set; } = string.Empty;
/// <summary>页面显示名称,供后台列表展示与按名称模糊查询。</summary>
public string PageName { get; set; } = string.Empty;
/// <summary>页面业务标识,供前端/小程序按 key 定位页面(如 home、shop、user。</summary>
public string PageKey { get; set; } = string.Empty;
/// <summary>排序号,列表按 SortNo 升序再按 CreateTime 升序。</summary>
public int SortNo { get; set; }
/// <summary>发布状态0-未发布1-已发布。见 <see cref="ShopRenovateStatus"/>。</summary>
public int Status { get; set; }
/// <summary>发布时间;未发布或取消发布时为 null。</summary>
public DateTime? PublishTime { get; set; }
/// <summary>该页面的装修布局 JSON组件、区块等后端整段读写不拆字段。</summary>
public string? PageConfigJson { get; set; }
/// <summary>记录创建时间。</summary>
public DateTime CreateTime { get; set; }
/// <summary>最后修改时间(保存、软删、发布/取消发布时更新)。</summary>
public DateTime? UpdateTime { get; set; }
/// <summary>软删除标记false 有效true 已删除。</summary>
public bool IsDeleted { get; set; }
}
/// <summary>店铺全局风格配置,对应表 tb_ShopRenovateStyle通常仅一条 default 记录)。</summary>
public class TbShopRenovateStyle
{
/// <summary>风格配置主键,业务固定使用 default 表示全店唯一配置。</summary>
public string tb_ShopRenovateStyle_ID { get; set; } = "default";
/// <summary>全店风格 JSON如 theme、tabCount、tabStyle与单页 PageConfigJson 分离存储。</summary>
public string? StyleConfigJson { get; set; }
/// <summary>风格配置最后保存时间。</summary>
public DateTime? UpdateTime { get; set; }
}
/// <summary>tb_ShopRenovatePage.Status 取值。</summary>
public static class ShopRenovateStatus
{
/// <summary>未发布。</summary>
public const int Unpublished = 0;
/// <summary>已发布。</summary>
public const int Published = 1;
}

@ -0,0 +1,7 @@
namespace Hcrm.Model;
public class TbSysConfig
{
public string tb_SysConfig_ID { get; set; } = string.Empty;
public int? LoginPrompt { get; set; }
}

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

@ -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<ISqlSugarClient>(_ => SqlSugarClientFactory.CreateClient(configuration));
services.AddScoped<IAccountRepository, AccountRepository>();
services.AddScoped<IShopRenovateRepository, ShopRenovateRepository>();
return services;
}
}

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Include="SqlSugarCore" Version="5.1.4.157" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Hcrm.IRepository\Hcrm.IRepository.csproj" />
<ProjectReference Include="..\Hcrm.Model\Hcrm.Model.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,36 @@
using Hcrm.Model;
using SqlSugar;
namespace Hcrm.Repository.Persistence;
internal static class SugarQueries
{
public static ISugarQueryable<TbEmployee> Employees(this ISqlSugarClient db) =>
db.Queryable<TbEmployee>().AS("tb_Employee");
public static ISugarQueryable<TbSysConfig> SysConfigs(this ISqlSugarClient db) =>
db.Queryable<TbSysConfig>().AS("tb_SysConfig");
public static ISugarQueryable<TbSysRole> SysRoles(this ISqlSugarClient db) =>
db.Queryable<TbSysRole>().AS("tb_SysRole");
public static ISugarQueryable<TbHospital> Hospitals(this ISqlSugarClient db) =>
db.Queryable<TbHospital>().AS("tb_Hospital");
public static ISugarQueryable<TbDataRight> DataRights(this ISqlSugarClient db) =>
db.Queryable<TbDataRight>().AS("tb_DataRight");
public static ISugarQueryable<TbEmployeeDepartMapping> EmployeeDepartMappings(this ISqlSugarClient db) =>
db.Queryable<TbEmployeeDepartMapping>().AS("tb_EmployeeDepartMapping");
public static IUpdateable<TbEmployee> UpdateEmployees(this ISqlSugarClient db) =>
db.Updateable<TbEmployee>().AS("tb_Employee");
/// <summary>店铺装修页面表 tb_ShopRenovatePage。</summary>
public static ISugarQueryable<TbShopRenovatePage> ShopRenovatePages(this ISqlSugarClient db) =>
db.Queryable<TbShopRenovatePage>().AS("tb_ShopRenovatePage");
/// <summary>店铺全局风格表 tb_ShopRenovateStyle。</summary>
public static ISugarQueryable<TbShopRenovateStyle> ShopRenovateStyles(this ISqlSugarClient db) =>
db.Queryable<TbShopRenovateStyle>().AS("tb_ShopRenovateStyle");
}

@ -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<int?> GetLoginPromptAsync(CancellationToken ct = default) =>
_db.SysConfigs().Select(x => x.LoginPrompt).FirstAsync(ct);
public Task<bool> EmployeeExistsAsync(string userName, CancellationToken ct = default) =>
_db.Employees().Where(e => e.EmployeeID == userName || e.EmployeeName == userName).AnyAsync();
public Task<TbEmployee?> 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<TbEmployee?> GetEmployeeByIdAsync(string employeeId, CancellationToken ct = default) =>
FirstOrDefaultAsync(_db.Employees().Where(e => e.tb_Employee_ID == employeeId), ct);
public Task<TbEmployee?> GetEmployeeByIdOrCodeAsync(string employeeId, CancellationToken ct = default) =>
FirstOrDefaultAsync(
_db.Employees().Where(e => e.tb_Employee_ID == employeeId || e.EmployeeID == employeeId),
ct);
public async Task<bool> 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<string[]> 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<TbSysRole?> GetRoleByIdAsync(string roleId, CancellationToken ct = default) =>
FirstOrDefaultAsync(_db.SysRoles().Where(r => r.tb_SysRole_ID == roleId), ct);
public Task<string?> GetHospitalNameAsync(string hospitalId, CancellationToken ct = default) =>
_db.Hospitals().Where(h => h.tb_Hospital_ID == hospitalId).Select(h => h.HospitalName).FirstAsync(ct);
public Task<TbDataRight?> GetDataRightByEmployeeIdAsync(string employeeId, CancellationToken ct = default) =>
FirstOrDefaultAsync(_db.DataRights().Where(d => d.EmployeeID == employeeId), ct);
private static async Task<T?> FirstOrDefaultAsync<T>(ISugarQueryable<T> query, CancellationToken ct)
where T : class, new()
{
var list = await query.Take(1).ToListAsync(ct);
return list.Count > 0 ? list[0] : null;
}
}

@ -0,0 +1,102 @@
using Hcrm.IRepository;
using Hcrm.Model;
using Hcrm.Repository.Persistence;
using SqlSugar;
namespace Hcrm.Repository.Repositories;
/// <summary>店铺装修仓储实现。</summary>
public class ShopRenovateRepository : IShopRenovateRepository
{
private readonly ISqlSugarClient _db;
public ShopRenovateRepository(ISqlSugarClient db)
{
_db = db;
}
public Task<List<TbShopRenovatePage>> 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<TbShopRenovatePage?> 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<bool> InsertPageAsync(TbShopRenovatePage entity, CancellationToken ct = default)
{
var rows = await _db.Insertable(entity).AS("tb_ShopRenovatePage").ExecuteCommandAsync(ct);
return rows > 0;
}
public async Task<bool> 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<bool> SoftDeletePageAsync(string id, CancellationToken ct = default)
{
var rows = await _db.Updateable<TbShopRenovatePage>().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<bool> SetPageStatusAsync(string id, int status, DateTime? publishTime, CancellationToken ct = default)
{
var rows = await _db.Updateable<TbShopRenovatePage>().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<TbShopRenovateStyle?> 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<bool> 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<TbShopRenovateStyle>().AS("tb_ShopRenovateStyle")
.SetColumns(s => new TbShopRenovateStyle { StyleConfigJson = styleConfigJson, UpdateTime = DateTime.Now })
.Where(s => s.tb_ShopRenovateStyle_ID == "default")
.ExecuteCommandAsync(ct);
return updated > 0;
}
}

@ -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
});
}
}

@ -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<IAccountService, AccountService>();
services.AddScoped<IShopRenovateService, ShopRenovateService>();
return services;
}
}

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Hcrm.DTO\Hcrm.DTO.csproj" />
<ProjectReference Include="..\Hcrm.IRepository\Hcrm.IRepository.csproj" />
<ProjectReference Include="..\Hcrm.IService\Hcrm.IService.csproj" />
<ProjectReference Include="..\Hcrm.Model\Hcrm.Model.csproj" />
</ItemGroup>
</Project>

@ -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<ApiResult> 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<ApiResult> 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<ApiResult> LogoutAsync()
{
_loginToken.Logout();
return Task.FromResult(ApiResult.Success());
}
public async Task<ApiResult> 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<AccountModel> 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<EmployeeDto> 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
};
}
}

@ -0,0 +1,172 @@
using Hcrm.DTO;
using Hcrm.IRepository;
using Hcrm.IService;
using Hcrm.Model;
namespace Hcrm.Service.Services;
/// <summary>店铺装修:页面管理与全店风格配置。</summary>
public class ShopRenovateService : IShopRenovateService
{
private readonly IShopRenovateRepository _repo;
public ShopRenovateService(IShopRenovateRepository repo)
{
_repo = repo;
}
public async Task<ApiResult> 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<ApiResult> 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<ApiResult> 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<ApiResult> 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<ApiResult> 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<ApiResult> 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<ApiResult> 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<ApiResult> GetStyleConfigAsync(CancellationToken ct = default)
{
var style = await _repo.GetStyleAsync(ct);
return ApiResult.Success(new ShopRenovateStyleDto
{
StyleConfigJson = style?.StyleConfigJson
});
}
public async Task<ApiResult> 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;
}
}
Loading…
Cancel
Save