Commit 3fcf01d821cafc54c0fecde78eb3bd045ce9793d

Authored by hexiaodong
1 parent 2ce7c3cb

hxd202509112049

.DS_Store
No preview for this file type
antis-ncc-admin/.env.development
1 1 # 开发
2 2  
3 3 VUE_CLI_BABEL_TRANSPILE_MODULES = true
4   -VUE_APP_BASE_API = 'http://localhost:5000'
5   -VUE_APP_BASE_WSS = 'ws://192.168.110.45:5000/websocket'
  4 +VUE_APP_BASE_API = 'http://localhost:2011'
  5 +VUE_APP_BASE_WSS = 'ws://192.168.110.45:2011/websocket'
... ...
netcore/netcore/.DS_Store
No preview for this file type
netcore/netcore/src/.DS_Store
No preview for this file type
netcore/netcore/src/Modularity/.DS_Store
No preview for this file type
netcore/src/.DS_Store
No preview for this file type
netcore/src/Application/.DS_Store
No preview for this file type
netcore/src/Application/NCC.API/appsettings.json
... ... @@ -77,7 +77,7 @@
77 77 },
78 78 "CorsAccessorSettings": {
79 79 "PolicyName": "NCCCorsAccessor",
80   - "WithOrigins": [ "http://192.168.0.138:8080", "http://localhost:8080", "http://localhost:2015", "http://localhost:2016", "http://localhost:3000", "http://localhost:9528", "http://localhost:8200", "http://localhost:3001", "http://localhost:8080" ],
  80 + "WithOrigins": [ "http://192.168.0.138:8080", "http://localhost:8080", "http://localhost:2015", "http://localhost:2011", "http://localhost:3000", "http://localhost:9528", "http://localhost:8200", "http://localhost:3001", "http://localhost:8080" ],
81 81 "WithExposedHeaders": [ "access-token", "x-access-token", "Content-Disposition" ]
82 82 },
83 83 "PaymentSettings": {
... ...
service/LqSalaryCalculationService/Dockerfile 0 → 100644
  1 +# 使用官方.NET 6运行时作为基础镜像
  2 +FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base
  3 +WORKDIR /app
  4 +
  5 +# 安装必要的系统依赖
  6 +RUN apt-get update && apt-get install -y \
  7 + curl \
  8 + && rm -rf /var/lib/apt/lists/*
  9 +
  10 +# 创建必要的目录
  11 +RUN mkdir -p /app/logs /app/output /app/backup
  12 +
  13 +# 使用官方.NET 6 SDK作为构建镜像
  14 +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
  15 +WORKDIR /src
  16 +
  17 +# 复制项目文件
  18 +COPY ["LqSalaryCalculationService.csproj", "./"]
  19 +RUN dotnet restore "LqSalaryCalculationService.csproj"
  20 +
  21 +# 复制所有源代码
  22 +COPY . .
  23 +
  24 +# 构建应用程序
  25 +RUN dotnet build "LqSalaryCalculationService.csproj" -c Release -o /app/build
  26 +
  27 +# 发布应用程序
  28 +FROM build AS publish
  29 +RUN dotnet publish "LqSalaryCalculationService.csproj" -c Release -o /app/publish \
  30 + --self-contained true \
  31 + --runtime linux-x64
  32 +
  33 +# 最终运行镜像
  34 +FROM base AS final
  35 +WORKDIR /app
  36 +
  37 +# 复制发布的应用程序
  38 +COPY --from=publish /app/publish .
  39 +
  40 +# 设置环境变量
  41 +ENV ASPNETCORE_ENVIRONMENT=Production
  42 +ENV DOTNET_RUNNING_IN_CONTAINER=true
  43 +
  44 +# 创建非root用户
  45 +RUN groupadd -r appuser && useradd -r -g appuser appuser
  46 +RUN chown -R appuser:appuser /app
  47 +USER appuser
  48 +
  49 +# 设置入口点
  50 +ENTRYPOINT ["dotnet", "LqSalaryCalculationService.dll"]
  51 +
  52 +# 健康检查
  53 +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
  54 + CMD curl -f http://localhost:8080/health || exit 1
... ...
service/LqSalaryCalculationService/LqSalaryCalculationService.csproj 0 → 100644
  1 +<Project Sdk="Microsoft.NET.Sdk">
  2 +
  3 + <PropertyGroup>
  4 + <OutputType>Exe</OutputType>
  5 + <TargetFramework>net6.0</TargetFramework>
  6 + <ImplicitUsings>enable</ImplicitUsings>
  7 + <Nullable>enable</Nullable>
  8 + <PublishSingleFile>true</PublishSingleFile>
  9 + <SelfContained>true</SelfContained>
  10 + </PropertyGroup>
  11 +
  12 + <ItemGroup>
  13 + <PackageReference Include="SqlSugar" Version="5.1.4.156" />
  14 + <PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
  15 + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
  16 + <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
  17 + <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
  18 + <PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
  19 + <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
  20 + <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
  21 + <PackageReference Include="Serilog" Version="2.12.0" />
  22 + <PackageReference Include="Serilog.Extensions.Hosting" Version="5.0.1" />
  23 + <PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
  24 + <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
  25 + <PackageReference Include="Yitter.IdGenerator" Version="1.0.6" />
  26 + <PackageReference Include="Serilog.Settings.Configuration" Version="7.0.0" />
  27 + <PackageReference Include="System.Text.Json" Version="7.0.1" />
  28 + <PackageReference Include="MySql.Data" Version="8.0.33" />
  29 + </ItemGroup>
  30 +
  31 +</Project>
... ...
service/LqSalaryCalculationService/Models/EmployeeSalary.cs 0 → 100644
  1 +using System;
  2 +using System.ComponentModel.DataAnnotations;
  3 +
  4 +namespace LqSalaryCalculationService.Models
  5 +{
  6 + /// <summary>
  7 + /// 员工工资计算结果模型
  8 + /// </summary>
  9 + public class EmployeeSalary
  10 + {
  11 + /// <summary>
  12 + /// 主键ID
  13 + /// </summary>
  14 + public string Id { get; set; } = string.Empty;
  15 +
  16 + /// <summary>
  17 + /// 员工姓名
  18 + /// </summary>
  19 + public string EmployeeName { get; set; } = string.Empty;
  20 +
  21 + /// <summary>
  22 + /// 员工ID
  23 + /// </summary>
  24 + public string EmployeeId { get; set; } = string.Empty;
  25 +
  26 + /// <summary>
  27 + /// 门店名称
  28 + /// </summary>
  29 + public string StoreName { get; set; } = string.Empty;
  30 +
  31 + /// <summary>
  32 + /// 门店ID
  33 + /// </summary>
  34 + public string StoreId { get; set; } = string.Empty;
  35 +
  36 + /// <summary>
  37 + /// 职位
  38 + /// </summary>
  39 + public string Position { get; set; } = string.Empty;
  40 +
  41 + /// <summary>
  42 + /// 岗位分类
  43 + /// </summary>
  44 + public string PositionCategory { get; set; } = string.Empty;
  45 +
  46 + /// <summary>
  47 + /// 计算月份
  48 + /// </summary>
  49 + public string CalculationMonth { get; set; } = string.Empty;
  50 +
  51 + /// <summary>
  52 + /// 底薪
  53 + /// </summary>
  54 + public decimal BaseSalary { get; set; }
  55 +
  56 + /// <summary>
  57 + /// 总业绩
  58 + /// </summary>
  59 + public decimal TotalPerformance { get; set; }
  60 +
  61 + /// <summary>
  62 + /// 消耗业绩
  63 + /// </summary>
  64 + public decimal ConsumptionPerformance { get; set; }
  65 +
  66 + /// <summary>
  67 + /// 项目数
  68 + /// </summary>
  69 + public int ProjectCount { get; set; }
  70 +
  71 + /// <summary>
  72 + /// 人头数
  73 + /// </summary>
  74 + public int CustomerCount { get; set; }
  75 +
  76 + /// <summary>
  77 + /// 提成金额
  78 + /// </summary>
  79 + public decimal CommissionAmount { get; set; }
  80 +
  81 + /// <summary>
  82 + /// 奖金金额
  83 + /// </summary>
  84 + public decimal BonusAmount { get; set; }
  85 +
  86 + /// <summary>
  87 + /// 扣款金额
  88 + /// </summary>
  89 + public decimal DeductionAmount { get; set; }
  90 +
  91 + /// <summary>
  92 + /// 应发工资
  93 + /// </summary>
  94 + public decimal GrossSalary { get; set; }
  95 +
  96 + /// <summary>
  97 + /// 实发工资
  98 + /// </summary>
  99 + public decimal NetSalary { get; set; }
  100 +
  101 + /// <summary>
  102 + /// 金三角业绩
  103 + /// </summary>
  104 + public decimal TeamPerformance { get; set; }
  105 +
  106 + /// <summary>
  107 + /// 金三角提成
  108 + /// </summary>
  109 + public decimal TeamCommission { get; set; }
  110 +
  111 + /// <summary>
  112 + /// 是否新店
  113 + /// </summary>
  114 + public bool IsNewStore { get; set; }
  115 +
  116 + /// <summary>
  117 + /// 门店生命线
  118 + /// </summary>
  119 + public decimal StoreLifeLine { get; set; }
  120 +
  121 + /// <summary>
  122 + /// 是否达标
  123 + /// </summary>
  124 + public bool IsTargetAchieved { get; set; }
  125 +
  126 + /// <summary>
  127 + /// 创建时间
  128 + /// </summary>
  129 + public DateTime CreatedTime { get; set; } = DateTime.Now;
  130 +
  131 + /// <summary>
  132 + /// 备注
  133 + /// </summary>
  134 + public string Remarks { get; set; } = string.Empty;
  135 + }
  136 +}
... ...
service/LqSalaryCalculationService/Models/SalaryCalculationConfig.cs 0 → 100644
  1 +using System;
  2 +using System.Collections.Generic;
  3 +
  4 +namespace LqSalaryCalculationService.Models
  5 +{
  6 + /// <summary>
  7 + /// 工资核算配置模型
  8 + /// </summary>
  9 + public class SalaryCalculationConfig
  10 + {
  11 + /// <summary>
  12 + /// 计算月份
  13 + /// </summary>
  14 + public string CalculationMonth { get; set; } = string.Empty;
  15 +
  16 + /// <summary>
  17 + /// 输出路径
  18 + /// </summary>
  19 + public string OutputPath { get; set; } = "./output";
  20 +
  21 + /// <summary>
  22 + /// 备份路径
  23 + /// </summary>
  24 + public string BackupPath { get; set; } = "./backup";
  25 +
  26 + /// <summary>
  27 + /// 日志路径
  28 + /// </summary>
  29 + public string LogPath { get; set; } = "./logs";
  30 +
  31 + /// <summary>
  32 + /// 是否启用邮件通知
  33 + /// </summary>
  34 + public bool EnableEmailNotification { get; set; } = true;
  35 +
  36 + /// <summary>
  37 + /// 邮件设置
  38 + /// </summary>
  39 + public EmailSettings EmailSettings { get; set; } = new EmailSettings();
  40 + }
  41 +
  42 + /// <summary>
  43 + /// 邮件设置
  44 + /// </summary>
  45 + public class EmailSettings
  46 + {
  47 + /// <summary>
  48 + /// SMTP服务器
  49 + /// </summary>
  50 + public string SmtpServer { get; set; } = string.Empty;
  51 +
  52 + /// <summary>
  53 + /// SMTP端口
  54 + /// </summary>
  55 + public int SmtpPort { get; set; } = 587;
  56 +
  57 + /// <summary>
  58 + /// 用户名
  59 + /// </summary>
  60 + public string Username { get; set; } = string.Empty;
  61 +
  62 + /// <summary>
  63 + /// 密码
  64 + /// </summary>
  65 + public string Password { get; set; } = string.Empty;
  66 +
  67 + /// <summary>
  68 + /// 发件人邮箱
  69 + /// </summary>
  70 + public string FromEmail { get; set; } = string.Empty;
  71 +
  72 + /// <summary>
  73 + /// 收件人邮箱列表
  74 + /// </summary>
  75 + public List<string> ToEmails { get; set; } = new List<string>();
  76 + }
  77 +}
... ...
service/LqSalaryCalculationService/Program.cs 0 → 100644
  1 +using Microsoft.Extensions.Configuration;
  2 +using Microsoft.Extensions.DependencyInjection;
  3 +using Microsoft.Extensions.Hosting;
  4 +using Microsoft.Extensions.Logging;
  5 +using Serilog;
  6 +using SqlSugar;
  7 +using LqSalaryCalculationService.Services;
  8 +using LqSalaryCalculationService.Models;
  9 +
  10 +namespace LqSalaryCalculationService
  11 +{
  12 + class Program
  13 + {
  14 + static async Task Main(string[] args)
  15 + {
  16 + // 配置Serilog
  17 + var configuration = GetConfiguration();
  18 + Log.Logger = new LoggerConfiguration()
  19 + .ReadFrom.Configuration(configuration)
  20 + .CreateLogger();
  21 +
  22 + try
  23 + {
  24 + Log.Information("绿纤美业ERP工资核算服务启动中...");
  25 +
  26 + var host = CreateHostBuilder(args).Build();
  27 +
  28 + // 获取服务
  29 + var salaryService = host.Services.GetRequiredService<ISalaryCalculationService>();
  30 + var logger = host.Services.GetRequiredService<ILogger<Program>>();
  31 +
  32 + // 获取计算月份参数
  33 + var calculationMonth = GetCalculationMonth(args);
  34 + if (string.IsNullOrEmpty(calculationMonth))
  35 + {
  36 + logger.LogError("请提供计算月份参数,格式: yyyy-MM");
  37 + Console.WriteLine("使用方法: dotnet run -- 2024-09");
  38 + return;
  39 + }
  40 +
  41 + logger.LogInformation($"开始执行工资核算,计算月份: {calculationMonth}");
  42 +
  43 + // 获取用户基础信息并插入到工资表
  44 + logger.LogInformation("开始获取用户基础信息并插入到工资表...");
  45 + var userInfoResult = await salaryService.InitializeUserInfoAsync(calculationMonth);
  46 +
  47 + if (userInfoResult.Success)
  48 + {
  49 + logger.LogInformation($"✅ 成功初始化 {userInfoResult.UserCount} 名用户信息");
  50 + Console.WriteLine($"✅ 用户信息初始化完成!");
  51 + Console.WriteLine($"📊 共处理 {userInfoResult.UserCount} 名用户");
  52 + Console.WriteLine($"📅 计算月份: {calculationMonth}");
  53 + }
  54 + else
  55 + {
  56 + logger.LogError($"初始化用户信息失败: {userInfoResult.Message}");
  57 + Console.WriteLine($"❌ 初始化用户信息失败: {userInfoResult.Message}");
  58 + }
  59 + }
  60 + catch (Exception ex)
  61 + {
  62 + Log.Fatal(ex, "服务启动失败");
  63 + Console.WriteLine($"❌ 服务启动失败: {ex.Message}");
  64 + }
  65 + finally
  66 + {
  67 + Log.CloseAndFlush();
  68 + }
  69 + }
  70 +
  71 + static IHostBuilder CreateHostBuilder(string[] args) =>
  72 + Host.CreateDefaultBuilder(args)
  73 + .UseSerilog()
  74 + .ConfigureServices((context, services) =>
  75 + {
  76 + // 配置SqlSugar
  77 + services.AddSingleton<SqlSugarClient>(provider =>
  78 + {
  79 + var configuration = provider.GetRequiredService<IConfiguration>();
  80 + var connectionString = configuration.GetConnectionString("DefaultConnection");
  81 +
  82 + return new SqlSugarClient(new ConnectionConfig()
  83 + {
  84 + ConnectionString = connectionString,
  85 + DbType = DbType.MySql,
  86 + IsAutoCloseConnection = true,
  87 + InitKeyType = InitKeyType.Attribute
  88 + });
  89 + });
  90 +
  91 + // 注册服务
  92 + services.AddScoped<ISalaryCalculationService, SalaryCalculationService>();
  93 + });
  94 +
  95 + static IConfiguration GetConfiguration()
  96 + {
  97 + return new ConfigurationBuilder()
  98 + .SetBasePath(Directory.GetCurrentDirectory())
  99 + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
  100 + .AddEnvironmentVariables()
  101 + .Build();
  102 + }
  103 +
  104 + static string GetCalculationMonth(string[] args)
  105 + {
  106 + if (args.Length > 0)
  107 + {
  108 + return args[0];
  109 + }
  110 +
  111 + // 如果没有提供参数,使用当前月份
  112 + return DateTime.Now.ToString("yyyy-MM");
  113 + }
  114 + }
  115 +}
... ...
service/LqSalaryCalculationService/README.md 0 → 100644
  1 +# 绿纤美业ERP工资核算服务
  2 +
  3 +## 项目简介
  4 +
  5 +这是一个基于.NET 6开发的跨平台控制台服务,专门用于绿纤美业ERP系统的月末财务工资核算。该服务支持Windows、Linux、CentOS等操作系统,能够自动计算员工工资、生成工资报表并发送邮件通知。
  6 +
  7 +## 功能特性
  8 +
  9 +- ✅ **跨平台支持**: 支持Windows、Linux、CentOS等操作系统
  10 +- ✅ **用户信息初始化**: 自动从系统用户表获取用户信息并插入到工资表
  11 +- ✅ **自动工资核算**: 根据绿纤美业薪酬规则自动计算员工工资
  12 +- ✅ **多角色支持**: 支持健康师、店长、主任、店助等不同岗位的工资计算
  13 +- ✅ **金三角提成**: 支持金三角(战队)提成计算
  14 +- ✅ **门店生命线**: 基于门店生命线的考核机制
  15 +- ✅ **新店保护**: 支持新店特殊薪酬规则
  16 +- ✅ **报表导出**: 自动生成Excel/CSV格式的工资报表
  17 +- ✅ **邮件通知**: 支持邮件通知功能
  18 +- ✅ **日志记录**: 完整的操作日志记录
  19 +- ✅ **Docker支持**: 支持Docker容器化部署
  20 +
  21 +## 技术栈
  22 +
  23 +- **.NET 6.0**: 跨平台运行时
  24 +- **SqlSugar**: ORM框架,支持MySQL数据库
  25 +- **Serilog**: 结构化日志记录
  26 +- **Microsoft.Extensions**: 依赖注入和配置管理
  27 +- **Yitter.IdGenerator**: 分布式ID生成
  28 +
  29 +## 系统要求
  30 +
  31 +### 开发环境
  32 +- .NET 6.0 SDK
  33 +- Visual Studio 2022 或 VS Code
  34 +- MySQL 5.7+ 或 8.0+
  35 +
  36 +### 运行环境
  37 +- .NET 6.0 Runtime
  38 +- MySQL数据库连接
  39 +- 网络连接(用于数据库访问)
  40 +
  41 +## 快速开始
  42 +
  43 +### 1. 克隆项目
  44 +```bash
  45 +git clone [项目地址]
  46 +cd service/LqSalaryCalculationService
  47 +```
  48 +
  49 +### 2. 配置数据库
  50 +编辑 `appsettings.json` 文件,配置数据库连接字符串:
  51 +
  52 +```json
  53 +{
  54 + "ConnectionStrings": {
  55 + "DefaultConnection": "Database=lqerp;Data Source=your-server;Port=3306;User Id=your-username;Password=your-password;Charset=utf8;"
  56 + }
  57 +}
  58 +```
  59 +
  60 +### 3. 运行服务
  61 +
  62 +#### Windows
  63 +```cmd
  64 +# 计算当前月份工资
  65 +start.bat
  66 +
  67 +# 计算指定月份工资
  68 +start.bat 2024-09
  69 +
  70 +# 显示帮助
  71 +start.bat --help
  72 +```
  73 +
  74 +#### Linux/CentOS
  75 +```bash
  76 +# 计算当前月份工资
  77 +./start.sh
  78 +
  79 +# 计算指定月份工资
  80 +./start.sh 2024-09
  81 +
  82 +# 显示帮助
  83 +./start.sh --help
  84 +```
  85 +
  86 +#### 直接使用dotnet命令
  87 +```bash
  88 +# 还原包
  89 +dotnet restore
  90 +
  91 +# 构建项目
  92 +dotnet build -c Release
  93 +
  94 +# 运行服务
  95 +dotnet run --configuration Release -- 2024-09
  96 +```
  97 +
  98 +## Docker部署
  99 +
  100 +### 1. 构建Docker镜像
  101 +```bash
  102 +docker build -t lq-salary-calculation-service .
  103 +```
  104 +
  105 +### 2. 运行容器
  106 +```bash
  107 +# 运行容器
  108 +docker run -d \
  109 + --name salary-service \
  110 + -v $(pwd)/logs:/app/logs \
  111 + -v $(pwd)/output:/app/output \
  112 + -v $(pwd)/backup:/app/backup \
  113 + lq-salary-calculation-service 2024-09
  114 +
  115 +# 查看日志
  116 +docker logs salary-service
  117 +```
  118 +
  119 +### 3. 使用Docker Compose
  120 +```bash
  121 +# 启动服务
  122 +docker-compose up -d
  123 +
  124 +# 查看日志
  125 +docker-compose logs -f
  126 +
  127 +# 停止服务
  128 +docker-compose down
  129 +```
  130 +
  131 +## 配置说明
  132 +
  133 +### appsettings.json配置项
  134 +
  135 +```json
  136 +{
  137 + "ConnectionStrings": {
  138 + "DefaultConnection": "数据库连接字符串"
  139 + },
  140 + "SalaryCalculation": {
  141 + "CalculationMonth": "计算月份",
  142 + "OutputPath": "./output",
  143 + "BackupPath": "./backup",
  144 + "LogPath": "./logs",
  145 + "EnableEmailNotification": true,
  146 + "EmailSettings": {
  147 + "SmtpServer": "smtp.163.com",
  148 + "SmtpPort": 587,
  149 + "Username": "邮箱用户名",
  150 + "Password": "邮箱密码",
  151 + "FromEmail": "发件人邮箱",
  152 + "ToEmails": ["收件人邮箱列表"]
  153 + }
  154 + }
  155 +}
  156 +```
  157 +
  158 +## 工作流程
  159 +
  160 +### 1. 用户信息初始化
  161 +服务启动时首先执行以下步骤:
  162 +
  163 +1. **获取用户基础信息**:
  164 + - 从 `BASE_USER` 表获取 `F_Id`(用户编号)、`F_REALNAME`(姓名)、`F_GW`(岗位)
  165 + - 通过 `F_MDID`(门店编号)关联 `lq_mdxx` 表获取 `dm`(门店名称)
  166 +
  167 +2. **获取金三角信息**:
  168 + - 通过用户ID在 `lq_jinsanjiao_user` 表中查找对应月份的金三角ID
  169 + - 通过金三角ID在 `lq_ycsd_jsj` 表中获取 `jsj`(金三角名称)
  170 +
  171 +3. **插入工资表**:
  172 + - 将用户信息插入到 `lq_gz`(工资表)的对应字段:
  173 + - `userid` ← `F_Id`(用户编号)
  174 + - `xm` ← `F_REALNAME`(姓名)
  175 + - `hsgw` ← `F_GW`(岗位)
  176 + - `md` ← `dm`(门店名称)
  177 + - `jsjzd` ← `jsj`(金三角名称)
  178 +
  179 +### 2. 工资核算
  180 +完成用户信息初始化后,执行工资核算:
  181 +
  182 +## 薪酬规则
  183 +
  184 +### 健康师薪酬规则
  185 +- **一星**: 月消耗≥10000元 且 项目数≥96个 → 底薪2000元
  186 +- **二星**: 月消耗≥20000元 且 项目数≥126个 → 底薪2200元
  187 +- **三星**: 月消耗≥40000元 且 项目数≥156个 → 底薪2400元
  188 +- **0星**: 未达到最低标准 → 底薪1800元
  189 +
  190 +### 金三角提成规则
  191 +- **3人战队**: 业绩30000-150000元,提成比例3%-7%
  192 +- **2人战队**: 业绩20000-80000元,提成比例3%-6%
  193 +- **1人战队**: 业绩10000-60000元,提成比例3%-6%
  194 +
  195 +### 管理岗位薪酬规则
  196 +- **店长**: 底薪4000元,基于毛利的提成计算
  197 +- **主任**: 底薪3500元,基于毛利的提成计算
  198 +- **店助**: 底薪3000元,基于毛利的提成计算
  199 +
  200 +## 输出文件
  201 +
  202 +服务运行后会在以下目录生成文件:
  203 +
  204 +- `./output/`: 工资报表文件(Excel/CSV格式)
  205 +- `./logs/`: 日志文件
  206 +- `./backup/`: 备份文件
  207 +
  208 +## 日志说明
  209 +
  210 +日志文件位置:`./logs/salary-calculation-YYYY-MM-DD.log`
  211 +
  212 +日志级别:
  213 +- **Information**: 一般信息
  214 +- **Warning**: 警告信息
  215 +- **Error**: 错误信息
  216 +- **Fatal**: 致命错误
  217 +
  218 +## 故障排除
  219 +
  220 +### 常见问题
  221 +
  222 +1. **数据库连接失败**
  223 + - 检查数据库连接字符串是否正确
  224 + - 确认数据库服务是否运行
  225 + - 检查网络连接
  226 +
  227 +2. **权限不足**
  228 + - 确保有足够的文件系统权限
  229 + - 检查输出目录是否可写
  230 +
  231 +3. **依赖包问题**
  232 + - 运行 `dotnet restore` 还原包
  233 + - 检查网络连接
  234 +
  235 +### 调试模式
  236 +
  237 +```bash
  238 +# 启用详细日志
  239 +export ASPNETCORE_ENVIRONMENT=Development
  240 +dotnet run --configuration Debug -- 2024-09
  241 +```
  242 +
  243 +## 开发指南
  244 +
  245 +### 项目结构
  246 +```
  247 +LqSalaryCalculationService/
  248 +├── Models/ # 数据模型
  249 +│ ├── EmployeeSalary.cs # 员工工资模型
  250 +│ └── SalaryCalculationConfig.cs # 配置模型
  251 +├── Services/ # 服务层
  252 +│ ├── ISalaryCalculationService.cs # 服务接口
  253 +│ └── SalaryCalculationService.cs # 服务实现
  254 +├── Program.cs # 程序入口
  255 +├── appsettings.json # 配置文件
  256 +├── Dockerfile # Docker配置
  257 +├── docker-compose.yml # Docker Compose配置
  258 +├── start.sh # Linux启动脚本
  259 +├── start.bat # Windows启动脚本
  260 +└── README.md # 说明文档
  261 +```
  262 +
  263 +### 添加新的薪酬规则
  264 +
  265 +1. 在 `SalaryCalculationService.cs` 中添加新的计算方法
  266 +2. 在 `CalculateSalaryAsync` 方法中添加新的岗位判断
  267 +3. 更新配置文件以支持新的参数
  268 +
  269 +## 版本历史
  270 +
  271 +### v1.0.0 (2024-09-11)
  272 +- 初始版本发布
  273 +- 支持基础工资核算功能
  274 +- 支持跨平台部署
  275 +- 支持Docker容器化
  276 +
  277 +## 许可证
  278 +
  279 +本项目仅供内部使用,请勿用于商业用途。
  280 +
  281 +## 技术支持
  282 +
  283 +如有技术问题,请联系开发团队或查看项目文档。
  284 +
  285 +---
  286 +
  287 +**注意**: 请确保在生产环境中正确配置数据库连接和邮件设置。
... ...
service/LqSalaryCalculationService/Services/ISalaryCalculationService.cs 0 → 100644
  1 +using System;
  2 +using System.Collections.Generic;
  3 +using System.Threading.Tasks;
  4 +using LqSalaryCalculationService.Models;
  5 +
  6 +namespace LqSalaryCalculationService.Services
  7 +{
  8 + /// <summary>
  9 + /// 工资核算服务接口
  10 + /// </summary>
  11 + public interface ISalaryCalculationService
  12 + {
  13 + /// <summary>
  14 + /// 初始化用户信息到工资表
  15 + /// </summary>
  16 + /// <param name="calculationMonth">计算月份 (格式: yyyy-MM)</param>
  17 + /// <returns>初始化结果</returns>
  18 + Task<UserInfoInitResult> InitializeUserInfoAsync(string calculationMonth);
  19 +
  20 + /// <summary>
  21 + /// 执行工资核算
  22 + /// </summary>
  23 + /// <param name="calculationMonth">计算月份 (格式: yyyy-MM)</param>
  24 + /// <returns>核算结果</returns>
  25 + Task<SalaryCalculationResult> CalculateSalaryAsync(string calculationMonth);
  26 +
  27 + /// <summary>
  28 + /// 获取员工业绩数据
  29 + /// </summary>
  30 + /// <param name="calculationMonth">计算月份</param>
  31 + /// <returns>员工业绩列表</returns>
  32 + Task<List<EmployeePerformance>> GetEmployeePerformanceAsync(string calculationMonth);
  33 +
  34 + /// <summary>
  35 + /// 计算健康师工资
  36 + /// </summary>
  37 + /// <param name="performance">业绩数据</param>
  38 + /// <returns>工资计算结果</returns>
  39 + Task<EmployeeSalary> CalculateHealthWorkerSalaryAsync(EmployeePerformance performance);
  40 +
  41 + /// <summary>
  42 + /// 计算金三角提成
  43 + /// </summary>
  44 + /// <param name="teamPerformance">团队业绩</param>
  45 + /// <param name="memberCount">团队成员数</param>
  46 + /// <returns>提成比例</returns>
  47 + decimal CalculateTeamCommissionRate(decimal teamPerformance, int memberCount);
  48 +
  49 + /// <summary>
  50 + /// 计算门店管理员工资
  51 + /// </summary>
  52 + /// <param name="performance">业绩数据</param>
  53 + /// <returns>工资计算结果</returns>
  54 + Task<EmployeeSalary> CalculateManagerSalaryAsync(EmployeePerformance performance);
  55 +
  56 + /// <summary>
  57 + /// 导出工资报表
  58 + /// </summary>
  59 + /// <param name="salaries">工资列表</param>
  60 + /// <param name="outputPath">输出路径</param>
  61 + /// <returns>导出文件路径</returns>
  62 + Task<string> ExportSalaryReportAsync(List<EmployeeSalary> salaries, string outputPath);
  63 +
  64 + /// <summary>
  65 + /// 发送邮件通知
  66 + /// </summary>
  67 + /// <param name="result">核算结果</param>
  68 + /// <returns>发送结果</returns>
  69 + Task<bool> SendEmailNotificationAsync(SalaryCalculationResult result);
  70 + }
  71 +
  72 + /// <summary>
  73 + /// 员工业绩数据模型
  74 + /// </summary>
  75 + public class EmployeePerformance
  76 + {
  77 + public string EmployeeId { get; set; } = string.Empty;
  78 + public string EmployeeName { get; set; } = string.Empty;
  79 + public string StoreId { get; set; } = string.Empty;
  80 + public string StoreName { get; set; } = string.Empty;
  81 + public string Position { get; set; } = string.Empty;
  82 + public string PositionCategory { get; set; } = string.Empty;
  83 + public decimal TotalPerformance { get; set; }
  84 + public decimal ConsumptionPerformance { get; set; }
  85 + public int ProjectCount { get; set; }
  86 + public int CustomerCount { get; set; }
  87 + public decimal TeamPerformance { get; set; }
  88 + public bool IsNewStore { get; set; }
  89 + public decimal StoreLifeLine { get; set; }
  90 + public string TeamId { get; set; } = string.Empty;
  91 + public int TeamMemberCount { get; set; }
  92 + }
  93 +
  94 + /// <summary>
  95 + /// 工资核算结果
  96 + /// </summary>
  97 + public class SalaryCalculationResult
  98 + {
  99 + public bool Success { get; set; }
  100 + public string Message { get; set; } = string.Empty;
  101 + public List<EmployeeSalary> Salaries { get; set; } = new List<EmployeeSalary>();
  102 + public string OutputFilePath { get; set; } = string.Empty;
  103 + public DateTime CalculationTime { get; set; } = DateTime.Now;
  104 + public string CalculationMonth { get; set; } = string.Empty;
  105 + }
  106 +
  107 + /// <summary>
  108 + /// 用户信息初始化结果
  109 + /// </summary>
  110 + public class UserInfoInitResult
  111 + {
  112 + public bool Success { get; set; }
  113 + public string Message { get; set; } = string.Empty;
  114 + public int UserCount { get; set; }
  115 + public DateTime InitTime { get; set; } = DateTime.Now;
  116 + public string CalculationMonth { get; set; } = string.Empty;
  117 + }
  118 +}
... ...
service/LqSalaryCalculationService/Services/SalaryCalculationService.cs 0 → 100644
  1 +using System;
  2 +using System.Collections.Generic;
  3 +using System.Data;
  4 +using System.IO;
  5 +using System.Linq;
  6 +using System.Threading.Tasks;
  7 +using Microsoft.Extensions.Configuration;
  8 +using Microsoft.Extensions.Logging;
  9 +using SqlSugar;
  10 +using LqSalaryCalculationService.Models;
  11 +using Yitter.IdGenerator;
  12 +
  13 +namespace LqSalaryCalculationService.Services
  14 +{
  15 + /// <summary>
  16 + /// 工资核算服务实现
  17 + /// </summary>
  18 + public class SalaryCalculationService : ISalaryCalculationService
  19 + {
  20 + private readonly SqlSugarClient _db;
  21 + private readonly ILogger<SalaryCalculationService> _logger;
  22 + private readonly IConfiguration _configuration;
  23 + private readonly SalaryCalculationConfig _config;
  24 +
  25 + public SalaryCalculationService(
  26 + SqlSugarClient db,
  27 + ILogger<SalaryCalculationService> logger,
  28 + IConfiguration configuration)
  29 + {
  30 + _db = db;
  31 + _logger = logger;
  32 + _configuration = configuration;
  33 + _config = _configuration.GetSection("SalaryCalculation").Get<SalaryCalculationConfig>() ?? new SalaryCalculationConfig();
  34 + }
  35 +
  36 + /// <summary>
  37 + /// 初始化用户信息到工资表
  38 + /// </summary>
  39 + public async Task<UserInfoInitResult> InitializeUserInfoAsync(string calculationMonth)
  40 + {
  41 + try
  42 + {
  43 + _logger.LogInformation($"开始初始化用户信息,计算月份: {calculationMonth}");
  44 +
  45 + // 1. 从base_user表获取所有用户基础信息(包括没有门店信息的)
  46 + var userInfoSql = @"
  47 + SELECT
  48 + u.F_Id as UserId,
  49 + u.F_REALNAME as UserName,
  50 + u.F_GW as Position,
  51 + u.F_MDID as StoreId,
  52 + COALESCE(m.dm, '无门店') as StoreName
  53 + FROM BASE_USER u
  54 + LEFT JOIN lq_mdxx m ON u.F_MDID = m.F_Id
  55 + WHERE u.F_ENABLEDMARK = 1
  56 + AND u.F_DELETEMARK IS NULL";
  57 +
  58 + var userInfos = await _db.Ado.SqlQueryAsync<UserInfoDto>(userInfoSql);
  59 + _logger.LogInformation($"获取到 {userInfos.Count()} 名用户基础信息");
  60 +
  61 + // 2. 清理工资表数据
  62 + _logger.LogInformation("开始清理工资表数据...");
  63 + var clearSql = "DELETE FROM lq_gz";
  64 + await _db.Ado.ExecuteCommandAsync(clearSql);
  65 + _logger.LogInformation("工资表数据清理完成");
  66 +
  67 + // 3. 先插入所有用户到工资表
  68 + var insertCount = 0;
  69 + foreach (var userInfo in userInfos)
  70 + {
  71 + try
  72 + {
  73 + // 插入到lq_gz工资表 - 先插入基本字段
  74 + var insertSql = @"
  75 + INSERT INTO lq_gz (F_Id, userid, xm, hsgw, md, jsjzd)
  76 + VALUES (@fid, @userid, @xm, @hsgw, @md, @jsjzd)
  77 + ON DUPLICATE KEY UPDATE
  78 + xm = VALUES(xm),
  79 + hsgw = VALUES(hsgw),
  80 + md = VALUES(md)";
  81 +
  82 + await _db.Ado.ExecuteCommandAsync(insertSql, new
  83 + {
  84 + fid = Guid.NewGuid().ToString(),
  85 + userid = userInfo.UserId,
  86 + xm = userInfo.UserName,
  87 + hsgw = userInfo.Position,
  88 + md = userInfo.StoreName,
  89 + jsjzd = "" // 先插入空值
  90 + });
  91 +
  92 + insertCount++;
  93 + _logger.LogDebug($"已插入用户: {userInfo.UserName} - {userInfo.StoreName}");
  94 + }
  95 + catch (Exception ex)
  96 + {
  97 + _logger.LogWarning(ex, $"插入用户 {userInfo.UserName} 时出错: {ex.Message}");
  98 + }
  99 + }
  100 +
  101 + _logger.LogInformation($"第一阶段完成:已插入 {insertCount} 名用户到工资表");
  102 +
  103 + // 4. 更新门店信息(重新查询有门店信息的用户)
  104 + var updateStoreCount = 0;
  105 + var storeUpdateSql = @"
  106 + SELECT
  107 + u.F_Id as UserId,
  108 + u.F_REALNAME as UserName,
  109 + m.dm as StoreName
  110 + FROM BASE_USER u
  111 + LEFT JOIN lq_mdxx m ON u.F_MDID = m.F_Id
  112 + WHERE u.F_ENABLEDMARK = 1
  113 + AND u.F_DELETEMARK IS NULL
  114 + AND u.F_MDID IS NOT NULL
  115 + AND m.dm IS NOT NULL";
  116 +
  117 + var storeInfos = await _db.Ado.SqlQueryAsync<UserInfoDto>(storeUpdateSql);
  118 +
  119 + foreach (var storeInfo in storeInfos)
  120 + {
  121 + try
  122 + {
  123 + var updateStoreSql = @"
  124 + UPDATE lq_gz
  125 + SET md = @storeName
  126 + WHERE userid = @userId";
  127 +
  128 + await _db.Ado.ExecuteCommandAsync(updateStoreSql, new
  129 + {
  130 + storeName = storeInfo.StoreName,
  131 + userId = storeInfo.UserId
  132 + });
  133 +
  134 + updateStoreCount++;
  135 + _logger.LogDebug($"已更新门店信息: {storeInfo.UserName} - {storeInfo.StoreName}");
  136 + }
  137 + catch (Exception ex)
  138 + {
  139 + _logger.LogWarning(ex, $"更新门店信息 {storeInfo.UserName} 时出错: {ex.Message}");
  140 + }
  141 + }
  142 +
  143 + _logger.LogInformation($"第二阶段完成:已更新 {updateStoreCount} 名用户的门店信息");
  144 +
  145 + // 5. 更新金三角信息
  146 + var updateTeamCount = 0;
  147 + foreach (var userInfo in userInfos)
  148 + {
  149 + try
  150 + {
  151 + // 通过用户ID在lq_jinsanjiao_user中获取对应月份的金三角ID
  152 + var monthFormat = calculationMonth.Replace("-", "");
  153 + var teamSql = @"
  154 + SELECT jsj_user.jsj_id, jsj.jsj as TeamName
  155 + FROM lq_jinsanjiao_user jsj_user
  156 + LEFT JOIN lq_ycsd_jsj jsj ON jsj_user.jsj_id = jsj.F_Id
  157 + WHERE jsj_user.user_id = @userId
  158 + AND jsj_user.status = 'ACTIVE'
  159 + AND jsj_user.F_Month = @monthFormat
  160 + LIMIT 1";
  161 +
  162 + var teamInfo = await _db.Ado.SqlQueryAsync<TeamInfoDto>(teamSql, new { userId = userInfo.UserId, monthFormat = monthFormat });
  163 + var teamName = teamInfo.FirstOrDefault()?.TeamName ?? "";
  164 +
  165 + if (!string.IsNullOrEmpty(teamName))
  166 + {
  167 + var updateTeamSql = @"
  168 + UPDATE lq_gz
  169 + SET jsjzd = @teamName
  170 + WHERE userid = @userId";
  171 +
  172 + await _db.Ado.ExecuteCommandAsync(updateTeamSql, new
  173 + {
  174 + teamName = teamName,
  175 + userId = userInfo.UserId
  176 + });
  177 +
  178 + updateTeamCount++;
  179 + _logger.LogInformation($"已更新金三角信息: {userInfo.UserName} - {teamName}");
  180 + }
  181 + else
  182 + {
  183 + _logger.LogDebug($"用户 {userInfo.UserName} 没有找到金三角信息");
  184 + }
  185 + }
  186 + catch (Exception ex)
  187 + {
  188 + _logger.LogWarning(ex, $"更新金三角信息 {userInfo.UserName} 时出错: {ex.Message}");
  189 + }
  190 + }
  191 +
  192 + _logger.LogInformation($"第三阶段完成:已更新 {updateTeamCount} 名用户的金三角信息");
  193 + _logger.LogInformation($"用户信息初始化完成,共处理 {insertCount} 名用户");
  194 +
  195 + return new UserInfoInitResult
  196 + {
  197 + Success = true,
  198 + Message = "用户信息初始化完成",
  199 + UserCount = insertCount,
  200 + CalculationMonth = calculationMonth
  201 + };
  202 + }
  203 + catch (Exception ex)
  204 + {
  205 + _logger.LogError(ex, $"初始化用户信息失败: {ex.Message}");
  206 + return new UserInfoInitResult
  207 + {
  208 + Success = false,
  209 + Message = $"初始化用户信息失败: {ex.Message}",
  210 + CalculationMonth = calculationMonth
  211 + };
  212 + }
  213 + }
  214 +
  215 + /// <summary>
  216 + /// 执行工资核算
  217 + /// </summary>
  218 + public async Task<SalaryCalculationResult> CalculateSalaryAsync(string calculationMonth)
  219 + {
  220 + try
  221 + {
  222 + _logger.LogInformation($"开始执行工资核算,计算月份: {calculationMonth}");
  223 +
  224 + // 1. 获取员工业绩数据
  225 + var performances = await GetEmployeePerformanceAsync(calculationMonth);
  226 + _logger.LogInformation($"获取到 {performances.Count} 条员工业绩数据");
  227 +
  228 + // 2. 计算每个员工的工资
  229 + var salaries = new List<EmployeeSalary>();
  230 + foreach (var performance in performances)
  231 + {
  232 + EmployeeSalary salary;
  233 + switch (performance.PositionCategory)
  234 + {
  235 + case "健康师":
  236 + salary = await CalculateHealthWorkerSalaryAsync(performance);
  237 + break;
  238 + case "店长":
  239 + case "主任":
  240 + case "店助":
  241 + salary = await CalculateManagerSalaryAsync(performance);
  242 + break;
  243 + default:
  244 + salary = await CalculateDefaultSalaryAsync(performance);
  245 + break;
  246 + }
  247 + salaries.Add(salary);
  248 + }
  249 +
  250 + // 3. 导出工资报表
  251 + var outputPath = Path.Combine(_config.OutputPath, $"工资核算_{calculationMonth}.xlsx");
  252 + var exportPath = await ExportSalaryReportAsync(salaries, outputPath);
  253 +
  254 + // 4. 发送邮件通知
  255 + if (_config.EnableEmailNotification)
  256 + {
  257 + await SendEmailNotificationAsync(new SalaryCalculationResult
  258 + {
  259 + Success = true,
  260 + Message = "工资核算完成",
  261 + Salaries = salaries,
  262 + OutputFilePath = exportPath,
  263 + CalculationMonth = calculationMonth
  264 + });
  265 + }
  266 +
  267 + _logger.LogInformation($"工资核算完成,共计算 {salaries.Count} 名员工工资");
  268 +
  269 + return new SalaryCalculationResult
  270 + {
  271 + Success = true,
  272 + Message = "工资核算完成",
  273 + Salaries = salaries,
  274 + OutputFilePath = exportPath,
  275 + CalculationMonth = calculationMonth
  276 + };
  277 + }
  278 + catch (Exception ex)
  279 + {
  280 + _logger.LogError(ex, $"工资核算失败: {ex.Message}");
  281 + return new SalaryCalculationResult
  282 + {
  283 + Success = false,
  284 + Message = $"工资核算失败: {ex.Message}",
  285 + CalculationMonth = calculationMonth
  286 + };
  287 + }
  288 + }
  289 +
  290 + /// <summary>
  291 + /// 获取员工业绩数据
  292 + /// </summary>
  293 + public async Task<List<EmployeePerformance>> GetEmployeePerformanceAsync(string calculationMonth)
  294 + {
  295 + var startDate = DateTime.Parse($"{calculationMonth}-01");
  296 + var endDate = startDate.AddMonths(1).AddDays(-1);
  297 +
  298 + var sql = @"
  299 + SELECT
  300 + u.F_Id as EmployeeId,
  301 + u.F_REALNAME as EmployeeName,
  302 + u.F_MDID as StoreId,
  303 + m.dm as StoreName,
  304 + u.F_GW as Position,
  305 + u.F_GWFL as PositionCategory,
  306 + COALESCE(SUM(CAST(yj.ssyj as DECIMAL(18,2))), 0) as TotalPerformance,
  307 + COALESCE(SUM(CAST(xh.ssyj as DECIMAL(18,2))), 0) as ConsumptionPerformance,
  308 + COUNT(DISTINCT yj.xmbh) as ProjectCount,
  309 + COUNT(DISTINCT yj.khbh) as CustomerCount,
  310 + COALESCE(SUM(CAST(jsj.team_performance as DECIMAL(18,2))), 0) as TeamPerformance,
  311 + CASE WHEN m.kysj >= DATE_SUB(NOW(), INTERVAL 6 MONTH) THEN 1 ELSE 0 END as IsNewStore,
  312 + COALESCE(m.xsyj, 0) as StoreLifeLine,
  313 + COALESCE(jsj.team_id, '') as TeamId,
  314 + COALESCE(jsj.team_member_count, 1) as TeamMemberCount
  315 + FROM BASE_USER u
  316 + LEFT JOIN lq_mdxx m ON u.F_MDID = m.F_Id
  317 + LEFT JOIN lq_yjmxb yj ON u.F_REALNAME = yj.jks
  318 + AND DATE_FORMAT(yj.fssj, '%Y-%m') = @month
  319 + LEFT JOIN lq_xhmxb xh ON u.F_REALNAME = xh.jks
  320 + AND DATE_FORMAT(xh.fssj, '%Y-%m') = @month
  321 + LEFT JOIN (
  322 + SELECT
  323 + jsj.F_Id as team_id,
  324 + jsj.jsj,
  325 + COUNT(jsj_user.user_id) as team_member_count,
  326 + SUM(CAST(yj2.ssyj as DECIMAL(18,2))) as team_performance
  327 + FROM lq_ycsd_jsj jsj
  328 + LEFT JOIN lq_jinsanjiao_user jsj_user ON jsj.F_Id = jsj_user.jsj_id
  329 + LEFT JOIN lq_yjmxb yj2 ON jsj_user.user_name = yj2.jks
  330 + AND DATE_FORMAT(yj2.fssj, '%Y-%m') = @month
  331 + WHERE jsj.yf = @month
  332 + GROUP BY jsj.F_Id, jsj.jsj
  333 + ) jsj ON u.F_REALNAME = jsj.jsj
  334 + WHERE u.F_ENABLEDMARK = 1
  335 + AND u.F_DELETEMARK IS NULL
  336 + GROUP BY u.F_Id, u.F_REALNAME, u.F_MDID, m.dm, u.F_GW, u.F_GWFL,
  337 + m.kysj, m.xsyj, jsj.team_id, jsj.team_member_count";
  338 +
  339 + var performances = await _db.Ado.SqlQueryAsync<EmployeePerformance>(sql, new { month = calculationMonth });
  340 + return performances.ToList();
  341 + }
  342 +
  343 + /// <summary>
  344 + /// 计算健康师工资
  345 + /// </summary>
  346 + public Task<EmployeeSalary> CalculateHealthWorkerSalaryAsync(EmployeePerformance performance)
  347 + {
  348 + var salary = new EmployeeSalary
  349 + {
  350 + Id = Guid.NewGuid().ToString(),
  351 + EmployeeId = performance.EmployeeId,
  352 + EmployeeName = performance.EmployeeName,
  353 + StoreId = performance.StoreId,
  354 + StoreName = performance.StoreName,
  355 + Position = performance.Position,
  356 + PositionCategory = performance.PositionCategory,
  357 + CalculationMonth = performance.StoreId, // 这里应该传计算月份
  358 + TotalPerformance = performance.TotalPerformance,
  359 + ConsumptionPerformance = performance.ConsumptionPerformance,
  360 + ProjectCount = performance.ProjectCount,
  361 + CustomerCount = performance.CustomerCount,
  362 + TeamPerformance = performance.TeamPerformance,
  363 + IsNewStore = performance.IsNewStore,
  364 + StoreLifeLine = performance.StoreLifeLine,
  365 + CreatedTime = DateTime.Now
  366 + };
  367 +
  368 + // 根据健康师薪酬规则计算底薪
  369 + if (performance.ConsumptionPerformance >= 40000 && performance.ProjectCount >= 156)
  370 + {
  371 + salary.BaseSalary = 2400; // 三星
  372 + }
  373 + else if (performance.ConsumptionPerformance >= 20000 && performance.ProjectCount >= 126)
  374 + {
  375 + salary.BaseSalary = 2200; // 二星
  376 + }
  377 + else if (performance.ConsumptionPerformance >= 10000 && performance.ProjectCount >= 96)
  378 + {
  379 + salary.BaseSalary = 2000; // 一星
  380 + }
  381 + else
  382 + {
  383 + salary.BaseSalary = 1800; // 0星
  384 + }
  385 +
  386 + // 计算金三角提成
  387 + if (performance.TeamMemberCount >= 1)
  388 + {
  389 + var commissionRate = CalculateTeamCommissionRate(performance.TeamPerformance, performance.TeamMemberCount);
  390 + salary.TeamCommission = performance.TeamPerformance * commissionRate / 100;
  391 + }
  392 +
  393 + // 计算个人提成(基础业绩提成)
  394 + if (performance.TotalPerformance > 6000)
  395 + {
  396 + salary.CommissionAmount = performance.TotalPerformance * 0.95m * 0.03m; // 3%提成点
  397 + }
  398 +
  399 + salary.GrossSalary = salary.BaseSalary + salary.CommissionAmount + salary.TeamCommission + salary.BonusAmount - salary.DeductionAmount;
  400 + salary.NetSalary = salary.GrossSalary;
  401 +
  402 + return Task.FromResult(salary);
  403 + }
  404 +
  405 + /// <summary>
  406 + /// 计算金三角提成比例
  407 + /// </summary>
  408 + public decimal CalculateTeamCommissionRate(decimal teamPerformance, int memberCount)
  409 + {
  410 + switch (memberCount)
  411 + {
  412 + case 3: // 3人战队
  413 + if (teamPerformance >= 150000) return 7m;
  414 + if (teamPerformance >= 120000) return 6m;
  415 + if (teamPerformance >= 90000) return 5m;
  416 + if (teamPerformance >= 60000) return 4m;
  417 + if (teamPerformance >= 30000) return 3m;
  418 + break;
  419 + case 2: // 2人战队
  420 + if (teamPerformance >= 80000) return 6m;
  421 + if (teamPerformance >= 60000) return 5m;
  422 + if (teamPerformance >= 40000) return 4m;
  423 + if (teamPerformance >= 20000) return 3m;
  424 + break;
  425 + case 1: // 1人战队
  426 + if (teamPerformance >= 60000) return 6m;
  427 + if (teamPerformance >= 40000) return 5m;
  428 + if (teamPerformance >= 20000) return 4m;
  429 + if (teamPerformance >= 10000) return 3m;
  430 + break;
  431 + }
  432 + return 0m;
  433 + }
  434 +
  435 + /// <summary>
  436 + /// 计算门店管理员工资
  437 + /// </summary>
  438 + public Task<EmployeeSalary> CalculateManagerSalaryAsync(EmployeePerformance performance)
  439 + {
  440 + var salary = new EmployeeSalary
  441 + {
  442 + Id = Guid.NewGuid().ToString(),
  443 + EmployeeId = performance.EmployeeId,
  444 + EmployeeName = performance.EmployeeName,
  445 + StoreId = performance.StoreId,
  446 + StoreName = performance.StoreName,
  447 + Position = performance.Position,
  448 + PositionCategory = performance.PositionCategory,
  449 + CalculationMonth = performance.StoreId, // 这里应该传计算月份
  450 + TotalPerformance = performance.TotalPerformance,
  451 + ConsumptionPerformance = performance.ConsumptionPerformance,
  452 + ProjectCount = performance.ProjectCount,
  453 + CustomerCount = performance.CustomerCount,
  454 + TeamPerformance = performance.TeamPerformance,
  455 + IsNewStore = performance.IsNewStore,
  456 + StoreLifeLine = performance.StoreLifeLine,
  457 + CreatedTime = DateTime.Now
  458 + };
  459 +
  460 + // 根据职位设置底薪
  461 + switch (performance.Position)
  462 + {
  463 + case "店长":
  464 + salary.BaseSalary = 4000;
  465 + break;
  466 + case "主任":
  467 + salary.BaseSalary = 3500;
  468 + break;
  469 + case "店助":
  470 + salary.BaseSalary = 3000;
  471 + break;
  472 + default:
  473 + salary.BaseSalary = 3000;
  474 + break;
  475 + }
  476 +
  477 + // 计算提成(基于毛利)
  478 + var grossProfit = performance.TotalPerformance * 0.6m; // 假设毛利率60%
  479 + var commissionRate = 0.03m; // 3%提成率
  480 + salary.CommissionAmount = grossProfit * commissionRate;
  481 +
  482 + // 检查是否达标
  483 + salary.IsTargetAchieved = performance.TotalPerformance >= performance.StoreLifeLine;
  484 +
  485 + salary.GrossSalary = salary.BaseSalary + salary.CommissionAmount + salary.BonusAmount - salary.DeductionAmount;
  486 + salary.NetSalary = salary.GrossSalary;
  487 +
  488 + return Task.FromResult(salary);
  489 + }
  490 +
  491 + /// <summary>
  492 + /// 计算默认工资
  493 + /// </summary>
  494 + private Task<EmployeeSalary> CalculateDefaultSalaryAsync(EmployeePerformance performance)
  495 + {
  496 + var salary = new EmployeeSalary
  497 + {
  498 + Id = Guid.NewGuid().ToString(),
  499 + EmployeeId = performance.EmployeeId,
  500 + EmployeeName = performance.EmployeeName,
  501 + StoreId = performance.StoreId,
  502 + StoreName = performance.StoreName,
  503 + Position = performance.Position,
  504 + PositionCategory = performance.PositionCategory,
  505 + CalculationMonth = performance.StoreId,
  506 + TotalPerformance = performance.TotalPerformance,
  507 + ConsumptionPerformance = performance.ConsumptionPerformance,
  508 + ProjectCount = performance.ProjectCount,
  509 + CustomerCount = performance.CustomerCount,
  510 + TeamPerformance = performance.TeamPerformance,
  511 + IsNewStore = performance.IsNewStore,
  512 + StoreLifeLine = performance.StoreLifeLine,
  513 + CreatedTime = DateTime.Now,
  514 + BaseSalary = 2000, // 默认底薪
  515 + GrossSalary = 2000,
  516 + NetSalary = 2000
  517 + };
  518 +
  519 + return Task.FromResult(salary);
  520 + }
  521 +
  522 + /// <summary>
  523 + /// 导出工资报表
  524 + /// </summary>
  525 + public async Task<string> ExportSalaryReportAsync(List<EmployeeSalary> salaries, string outputPath)
  526 + {
  527 + try
  528 + {
  529 + // 确保输出目录存在
  530 + var directory = Path.GetDirectoryName(outputPath);
  531 + if (!Directory.Exists(directory))
  532 + {
  533 + Directory.CreateDirectory(directory!);
  534 + }
  535 +
  536 + // 这里可以使用EPPlus或其他Excel库来生成Excel文件
  537 + // 为了简化,这里生成CSV文件
  538 + var csvPath = outputPath.Replace(".xlsx", ".csv");
  539 +
  540 + using var writer = new StreamWriter(csvPath);
  541 + await writer.WriteLineAsync("员工姓名,门店名称,职位,底薪,总业绩,消耗业绩,提成金额,应发工资,实发工资,计算月份");
  542 +
  543 + foreach (var salary in salaries)
  544 + {
  545 + await writer.WriteLineAsync($"{salary.EmployeeName},{salary.StoreName},{salary.Position}," +
  546 + $"{salary.BaseSalary},{salary.TotalPerformance},{salary.ConsumptionPerformance}," +
  547 + $"{salary.CommissionAmount},{salary.GrossSalary},{salary.NetSalary},{salary.CalculationMonth}");
  548 + }
  549 +
  550 + _logger.LogInformation($"工资报表已导出到: {csvPath}");
  551 + return csvPath;
  552 + }
  553 + catch (Exception ex)
  554 + {
  555 + _logger.LogError(ex, $"导出工资报表失败: {ex.Message}");
  556 + throw;
  557 + }
  558 + }
  559 +
  560 + /// <summary>
  561 + /// 发送邮件通知
  562 + /// </summary>
  563 + public Task<bool> SendEmailNotificationAsync(SalaryCalculationResult result)
  564 + {
  565 + try
  566 + {
  567 + // 这里可以实现邮件发送逻辑
  568 + // 可以使用MailKit或其他邮件库
  569 + _logger.LogInformation($"邮件通知功能待实现,核算结果: {result.Message}");
  570 + return Task.FromResult(true);
  571 + }
  572 + catch (Exception ex)
  573 + {
  574 + _logger.LogError(ex, $"发送邮件通知失败: {ex.Message}");
  575 + return Task.FromResult(false);
  576 + }
  577 + }
  578 + }
  579 +
  580 + /// <summary>
  581 + /// 用户信息DTO
  582 + /// </summary>
  583 + public class UserInfoDto
  584 + {
  585 + public string UserId { get; set; } = string.Empty;
  586 + public string UserName { get; set; } = string.Empty;
  587 + public string Position { get; set; } = string.Empty;
  588 + public string StoreId { get; set; } = string.Empty;
  589 + public string StoreName { get; set; } = string.Empty;
  590 + }
  591 +
  592 + /// <summary>
  593 + /// 团队信息DTO
  594 + /// </summary>
  595 + public class TeamInfoDto
  596 + {
  597 + public string JsjId { get; set; } = string.Empty;
  598 + public string TeamName { get; set; } = string.Empty;
  599 + }
  600 +}
... ...
service/LqSalaryCalculationService/appsettings.json 0 → 100644
  1 +{
  2 + "ConnectionStrings": {
  3 + "DefaultConnection": "Database=lqerp;Data Source=rm-bp19ohrgc6111ynzh1o.mysql.rds.aliyuncs.com;Port=3306;User Id=netteam;Password=netteam;Charset=utf8;TreatTinyAsBoolean=true;"
  4 + },
  5 + "Logging": {
  6 + "LogLevel": {
  7 + "Default": "Information",
  8 + "Microsoft": "Warning",
  9 + "Microsoft.Hosting.Lifetime": "Information"
  10 + }
  11 + },
  12 + "SalaryCalculation": {
  13 + "CalculationMonth": "",
  14 + "OutputPath": "./output",
  15 + "BackupPath": "./backup",
  16 + "LogPath": "./logs",
  17 + "EnableEmailNotification": true,
  18 + "EmailSettings": {
  19 + "SmtpServer": "smtp.163.com",
  20 + "SmtpPort": 587,
  21 + "Username": "",
  22 + "Password": "",
  23 + "FromEmail": "",
  24 + "ToEmails": []
  25 + }
  26 + },
  27 + "Serilog": {
  28 + "MinimumLevel": {
  29 + "Default": "Information",
  30 + "Override": {
  31 + "System": "Warning",
  32 + "Microsoft": "Warning"
  33 + }
  34 + },
  35 + "WriteTo": [
  36 + {
  37 + "Name": "Console",
  38 + "Args": {
  39 + "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}"
  40 + }
  41 + },
  42 + {
  43 + "Name": "File",
  44 + "Args": {
  45 + "path": "./logs/salary-calculation-.log",
  46 + "rollingInterval": "Day",
  47 + "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}"
  48 + }
  49 + }
  50 + ]
  51 + }
  52 +}
... ...
service/LqSalaryCalculationService/docker-compose.yml 0 → 100644
  1 +version: '3.8'
  2 +
  3 +services:
  4 + salary-calculation-service:
  5 + build:
  6 + context: .
  7 + dockerfile: Dockerfile
  8 + container_name: lq-salary-calculation-service
  9 + restart: unless-stopped
  10 + environment:
  11 + - ASPNETCORE_ENVIRONMENT=Production
  12 + - ConnectionStrings__DefaultConnection=Database=lqerp;Data Source=rm-bp19ohrgc6111ynzh1o.mysql.rds.aliyuncs.com;Port=3306;User Id=netteam;Password=netteam;Charset=utf8;TreatTinyAsBoolean=true;
  13 + volumes:
  14 + - ./logs:/app/logs
  15 + - ./output:/app/output
  16 + - ./backup:/app/backup
  17 + networks:
  18 + - salary-network
  19 + command: ["2024-09"] # 默认计算2024年9月工资
  20 +
  21 +networks:
  22 + salary-network:
  23 + driver: bridge
... ...
service/LqSalaryCalculationService/start.bat 0 → 100644
  1 +@echo off
  2 +chcp 65001 >nul
  3 +setlocal enabledelayedexpansion
  4 +
  5 +REM 绿纤美业ERP工资核算服务启动脚本 (Windows)
  6 +REM 支持Windows 10/11, Windows Server 2016+
  7 +
  8 +echo.
  9 +echo ========================================
  10 +echo 绿纤美业ERP工资核算服务
  11 +echo ========================================
  12 +echo.
  13 +
  14 +REM 检查参数
  15 +if "%1"=="--help" goto :help
  16 +if "%1"=="-h" goto :help
  17 +if "%1"=="--version" goto :version
  18 +if "%1"=="-v" goto :version
  19 +if "%1"=="--build" goto :build
  20 +if "%1"=="--clean" goto :clean
  21 +
  22 +REM 检查.NET 6是否安装
  23 +echo [INFO] 检查.NET 6环境...
  24 +dotnet --version >nul 2>&1
  25 +if errorlevel 1 (
  26 + echo [ERROR] .NET 6 未安装,请先安装 .NET 6 SDK
  27 + echo [INFO] 下载地址: https://dotnet.microsoft.com/download/dotnet/6.0
  28 + pause
  29 + exit /b 1
  30 +)
  31 +
  32 +for /f "tokens=*" %%i in ('dotnet --version') do set DOTNET_VERSION=%%i
  33 +echo [INFO] 检测到 .NET 版本: %DOTNET_VERSION%
  34 +
  35 +REM 检查配置文件
  36 +if not exist "appsettings.json" (
  37 + echo [ERROR] 配置文件 appsettings.json 不存在
  38 + pause
  39 + exit /b 1
  40 +)
  41 +echo [INFO] 配置文件检查通过
  42 +
  43 +REM 创建必要目录
  44 +echo [INFO] 创建必要目录...
  45 +if not exist "logs" mkdir logs
  46 +if not exist "output" mkdir output
  47 +if not exist "backup" mkdir backup
  48 +echo [INFO] 目录创建完成
  49 +
  50 +REM 还原NuGet包
  51 +echo [INFO] 还原NuGet包...
  52 +dotnet restore
  53 +if errorlevel 1 (
  54 + echo [ERROR] NuGet包还原失败
  55 + pause
  56 + exit /b 1
  57 +)
  58 +echo [INFO] NuGet包还原成功
  59 +
  60 +REM 构建项目
  61 +echo [INFO] 构建项目...
  62 +dotnet build -c Release
  63 +if errorlevel 1 (
  64 + echo [ERROR] 项目构建失败
  65 + pause
  66 + exit /b 1
  67 +)
  68 +echo [INFO] 项目构建成功
  69 +
  70 +REM 运行服务
  71 +set MONTH=%1
  72 +if "%MONTH%"=="" (
  73 + for /f "tokens=1,2 delims=-" %%a in ('date /t') do set MONTH=%%a-%%b
  74 + echo [WARN] 未指定计算月份,使用当前月份: %MONTH%
  75 + echo [INFO] 使用方法: %0 [YYYY-MM]
  76 + echo [INFO] 示例: %0 2024-09
  77 +)
  78 +
  79 +echo [INFO] 启动工资核算服务,计算月份: %MONTH%
  80 +dotnet run --configuration Release -- %MONTH%
  81 +
  82 +goto :end
  83 +
  84 +:help
  85 +echo.
  86 +echo 绿纤美业ERP工资核算服务
  87 +echo.
  88 +echo 使用方法:
  89 +echo %0 [月份] 运行工资核算服务
  90 +echo %0 --help, -h 显示帮助信息
  91 +echo %0 --version, -v 显示版本信息
  92 +echo %0 --build 仅构建项目
  93 +echo %0 --clean 清理构建文件
  94 +echo.
  95 +echo 参数:
  96 +echo 月份 计算月份,格式: YYYY-MM (例如: 2024-09)
  97 +echo.
  98 +echo 示例:
  99 +echo %0 2024-09 计算2024年9月工资
  100 +echo %0 计算当前月份工资
  101 +echo.
  102 +echo 环境要求:
  103 +echo - .NET 6.0 SDK
  104 +echo - MySQL数据库连接
  105 +echo - 网络连接
  106 +goto :end
  107 +
  108 +:version
  109 +echo 绿纤美业ERP工资核算服务 v1.0.0
  110 +echo 构建时间: %date% %time%
  111 +for /f "tokens=*" %%i in ('dotnet --version') do echo .NET版本: %%i
  112 +goto :end
  113 +
  114 +:build
  115 +echo [INFO] 检查.NET 6环境...
  116 +dotnet --version >nul 2>&1
  117 +if errorlevel 1 (
  118 + echo [ERROR] .NET 6 未安装
  119 + pause
  120 + exit /b 1
  121 +)
  122 +
  123 +echo [INFO] 检查配置文件...
  124 +if not exist "appsettings.json" (
  125 + echo [ERROR] 配置文件不存在
  126 + pause
  127 + exit /b 1
  128 +)
  129 +
  130 +echo [INFO] 创建目录...
  131 +if not exist "logs" mkdir logs
  132 +if not exist "output" mkdir output
  133 +if not exist "backup" mkdir backup
  134 +
  135 +echo [INFO] 还原包...
  136 +dotnet restore
  137 +if errorlevel 1 (
  138 + echo [ERROR] 还原失败
  139 + pause
  140 + exit /b 1
  141 +)
  142 +
  143 +echo [INFO] 构建项目...
  144 +dotnet build -c Release
  145 +if errorlevel 1 (
  146 + echo [ERROR] 构建失败
  147 + pause
  148 + exit /b 1
  149 +)
  150 +
  151 +echo [INFO] 构建完成
  152 +goto :end
  153 +
  154 +:clean
  155 +echo [INFO] 清理构建文件...
  156 +dotnet clean
  157 +if exist "bin" rmdir /s /q "bin"
  158 +if exist "obj" rmdir /s /q "obj"
  159 +echo [INFO] 清理完成
  160 +goto :end
  161 +
  162 +:end
  163 +pause
... ...
service/LqSalaryCalculationService/start.sh 0 → 100755
  1 +#!/bin/bash
  2 +
  3 +# 绿纤美业ERP工资核算服务启动脚本
  4 +# 支持Windows、Linux、CentOS等操作系统
  5 +
  6 +set -e
  7 +
  8 +# 颜色定义
  9 +RED='\033[0;31m'
  10 +GREEN='\033[0;32m'
  11 +YELLOW='\033[1;33m'
  12 +BLUE='\033[0;34m'
  13 +NC='\033[0m' # No Color
  14 +
  15 +# 日志函数
  16 +log_info() {
  17 + echo -e "${GREEN}[INFO]${NC} $1"
  18 +}
  19 +
  20 +log_warn() {
  21 + echo -e "${YELLOW}[WARN]${NC} $1"
  22 +}
  23 +
  24 +log_error() {
  25 + echo -e "${RED}[ERROR]${NC} $1"
  26 +}
  27 +
  28 +log_debug() {
  29 + echo -e "${BLUE}[DEBUG]${NC} $1"
  30 +}
  31 +
  32 +# 检查.NET 6是否安装
  33 +check_dotnet() {
  34 + if ! command -v dotnet &> /dev/null; then
  35 + log_error ".NET 6 未安装,请先安装 .NET 6 SDK"
  36 + log_info "下载地址: https://dotnet.microsoft.com/download/dotnet/6.0"
  37 + exit 1
  38 + fi
  39 +
  40 + local dotnet_version=$(dotnet --version)
  41 + log_info "检测到 .NET 版本: $dotnet_version"
  42 +
  43 + if [[ ! "$dotnet_version" =~ ^6\. ]]; then
  44 + log_warn "建议使用 .NET 6.0 版本"
  45 + fi
  46 +}
  47 +
  48 +# 检查配置文件
  49 +check_config() {
  50 + if [ ! -f "appsettings.json" ]; then
  51 + log_error "配置文件 appsettings.json 不存在"
  52 + exit 1
  53 + fi
  54 + log_info "配置文件检查通过"
  55 +}
  56 +
  57 +# 创建必要目录
  58 +create_directories() {
  59 + log_info "创建必要目录..."
  60 + mkdir -p logs output backup
  61 + log_info "目录创建完成"
  62 +}
  63 +
  64 +# 还原NuGet包
  65 +restore_packages() {
  66 + log_info "还原NuGet包..."
  67 + dotnet restore
  68 + if [ $? -eq 0 ]; then
  69 + log_info "NuGet包还原成功"
  70 + else
  71 + log_error "NuGet包还原失败"
  72 + exit 1
  73 + fi
  74 +}
  75 +
  76 +# 构建项目
  77 +build_project() {
  78 + log_info "构建项目..."
  79 + dotnet build -c Release
  80 + if [ $? -eq 0 ]; then
  81 + log_info "项目构建成功"
  82 + else
  83 + log_error "项目构建失败"
  84 + exit 1
  85 + fi
  86 +}
  87 +
  88 +# 运行服务
  89 +run_service() {
  90 + local month=${1:-$(date +%Y-%m)}
  91 + log_info "启动工资核算服务,计算月份: $month"
  92 +
  93 + # 检查参数
  94 + if [ $# -eq 0 ]; then
  95 + log_warn "未指定计算月份,使用当前月份: $month"
  96 + log_info "使用方法: $0 [YYYY-MM]"
  97 + log_info "示例: $0 2024-09"
  98 + fi
  99 +
  100 + # 运行服务
  101 + dotnet run --configuration Release -- $month
  102 +}
  103 +
  104 +# 显示帮助信息
  105 +show_help() {
  106 + echo "绿纤美业ERP工资核算服务"
  107 + echo ""
  108 + echo "使用方法:"
  109 + echo " $0 [月份] 运行工资核算服务"
  110 + echo " $0 --help, -h 显示帮助信息"
  111 + echo " $0 --version, -v 显示版本信息"
  112 + echo " $0 --build 仅构建项目"
  113 + echo " $0 --clean 清理构建文件"
  114 + echo ""
  115 + echo "参数:"
  116 + echo " 月份 计算月份,格式: YYYY-MM (例如: 2024-09)"
  117 + echo ""
  118 + echo "示例:"
  119 + echo " $0 2024-09 计算2024年9月工资"
  120 + echo " $0 计算当前月份工资"
  121 + echo ""
  122 + echo "环境要求:"
  123 + echo " - .NET 6.0 SDK"
  124 + echo " - MySQL数据库连接"
  125 + echo " - 网络连接"
  126 +}
  127 +
  128 +# 显示版本信息
  129 +show_version() {
  130 + echo "绿纤美业ERP工资核算服务 v1.0.0"
  131 + echo "构建时间: $(date)"
  132 + echo ".NET版本: $(dotnet --version)"
  133 +}
  134 +
  135 +# 清理构建文件
  136 +clean_project() {
  137 + log_info "清理构建文件..."
  138 + dotnet clean
  139 + rm -rf bin obj
  140 + log_info "清理完成"
  141 +}
  142 +
  143 +# 主函数
  144 +main() {
  145 + case "${1:-}" in
  146 + --help|-h)
  147 + show_help
  148 + exit 0
  149 + ;;
  150 + --version|-v)
  151 + show_version
  152 + exit 0
  153 + ;;
  154 + --build)
  155 + check_dotnet
  156 + check_config
  157 + create_directories
  158 + restore_packages
  159 + build_project
  160 + log_info "构建完成"
  161 + exit 0
  162 + ;;
  163 + --clean)
  164 + clean_project
  165 + exit 0
  166 + ;;
  167 + *)
  168 + check_dotnet
  169 + check_config
  170 + create_directories
  171 + restore_packages
  172 + build_project
  173 + run_service "$@"
  174 + ;;
  175 + esac
  176 +}
  177 +
  178 +# 执行主函数
  179 +main "$@"
... ...