diff --git a/antis-ncc-admin/.env.development b/antis-ncc-admin/.env.development index 198955a..6462393 100644 --- a/antis-ncc-admin/.env.development +++ b/antis-ncc-admin/.env.development @@ -2,8 +2,8 @@ VUE_CLI_BABEL_TRANSPILE_MODULES = true # VUE_APP_BASE_API = 'https://erp.lvqianmeiye.com' -VUE_APP_BASE_API = 'http://erp_test.lvqianmeiye.com' -# VUE_APP_BASE_API = 'http://localhost:2011' +# VUE_APP_BASE_API = 'http://erp_test.lvqianmeiye.com' +VUE_APP_BASE_API = 'http://localhost:2011' # VUE_APP_BASE_API = 'http://localhost:2011' VUE_APP_IMG_API = '' VUE_APP_BASE_WSS = 'ws://192.168.110.45:2011/websocket' diff --git a/antis-ncc-admin/src/api/extend/annualSummary.js b/antis-ncc-admin/src/api/extend/annualSummary.js new file mode 100644 index 0000000..ff58089 --- /dev/null +++ b/antis-ncc-admin/src/api/extend/annualSummary.js @@ -0,0 +1,129 @@ +import request from '@/utils/request' + +// 获取年度汇总列表 +export function getList(params) { + return request({ + url: '/api/Extend/LqAnnualSummary/list', + method: 'get', + params + }) +} + +// 保存(新增或更新) +export function save(data) { + return request({ + url: '/api/Extend/LqAnnualSummary/save', + method: 'post', + data + }) +} + +// 删除 +export function del(data) { + return request({ + url: '/api/Extend/LqAnnualSummary/delete', + method: 'post', + data + }) +} + +// 导入 +export function importData(data) { + return request({ + url: '/api/Extend/LqAnnualSummary/import', + method: 'post', + data + }) +} + +// ========== 统计接口 ========== + +// 4.1 全年门店业绩表 +export function getTotalPerformanceStat(data) { + return request({ + url: '/api/Extend/LqAnnualSummary/GetTotalPerformanceStat', + method: 'post', + data + }) +} + +// 4.2 全年门店消耗表 +export function getTotalConsumeStat(data) { + return request({ + url: '/api/Extend/LqAnnualSummary/GetTotalConsumeStat', + method: 'post', + data + }) +} + +// 4.3 年度门店人头表 +export function getHeadCountStat(data) { + return request({ + url: '/api/Extend/LqAnnualSummary/GetHeadCountStat', + method: 'post', + data + }) +} + +// 4.4 年度门店项目数表 +export function getProjectCountStat(data) { + return request({ + url: '/api/Extend/LqAnnualSummary/GetProjectCountStat', + method: 'post', + data + }) +} + +// 4.5 年度门店人次表 +export function getPersonTimeStat(params) { + return request({ + url: '/api/Extend/LqAnnualSummary/GetPersonTimeStat', + method: 'get', + params + }) +} + +// 通用月度趋势统计 +export function getMonthlyTrend(params) { + return request({ + url: '/api/Extend/LqAnnualSummary/GetMonthlyTrend', + method: 'get', + params + }) +} + +// 4.6 门店五项指标统计图 +export function getStoreIndicatorsStat(data) { + return request({ + url: '/api/Extend/LqAnnualSummary/GetStoreIndicatorsStat', + method: 'post', + data + }) +} + +// 获取门店指标详情 +export function getStoreIndicatorDetail(params) { + return request({ + url: '/api/Extend/LqAnnualSummary/GetStoreIndicatorDetail', + method: 'get', + params + }) +} + +// 4.7 事业部五项指标总计图 +export function getBusinessUnitIndicatorsStat(params) { + return request({ + url: '/api/Extend/LqAnnualSummary/GetBusinessUnitIndicatorsStat', + method: 'get', + params + }) +} + +// 4.8 事业部内部汇总 (宽表) +export function getBusinessUnitSummaryStat(data) { + return request({ + url: '/api/Extend/LqAnnualSummary/GetBusinessUnitSummaryStat', + method: 'post', + data + }) +} diff --git a/antis-ncc-admin/src/utils/request.js b/antis-ncc-admin/src/utils/request.js index 7bba7f3..9f6fd62 100644 --- a/antis-ncc-admin/src/utils/request.js +++ b/antis-ncc-admin/src/utils/request.js @@ -29,7 +29,9 @@ service.interceptors.request.use( if (store.getters.token) { config.headers['Authorization'] = getToken() } - if (config.method == 'get') { + // GET 请求时,如果传入了 data 但没有 params,则将其转换为 params + // 如果已经有 params,则保留 params,不覆盖 + if (config.method == 'get' && config.data && !config.params) { config.params = config.data } let timestamp = Date.parse(new Date()) / 1000 diff --git a/antis-ncc-admin/src/views/extend/annualSummary/dashboard/index.vue b/antis-ncc-admin/src/views/extend/annualSummary/dashboard/index.vue new file mode 100644 index 0000000..8258192 --- /dev/null +++ b/antis-ncc-admin/src/views/extend/annualSummary/dashboard/index.vue @@ -0,0 +1,1906 @@ + + + + + + + 年度经营统计分析 + + + + + + + + + + + + + 查询 + 重置 + + + + + + + + + + + + 统计目录 + + + + + 月度趋势分析 + + + + 全年门店业绩表 + + + + 全年门店消耗表 + + + + 年度门店人头表 + + + + 年度门店人次表 + + + + 年度门店项目数表 + + + + 门店五项指标统计 + + + + 事业部五项指标统计 + + + + 事业部内部汇总 + + + + + + + + + + + 月度趋势分析 + + + + + + + + 业绩走势对比 + + + + + + + + + 消耗走势对比 + + + + + + + + + + + 客头数走势 + + + + + + + + + 客次数走势 + + + + + + + + + 项目数走势 + + + + + + + + + + 月度趋势数据列表 + + + + + + {{ scope.row[col.prop] || '0' }} + + + + + + + + + + + + + 全年门店业绩表 + + + + + + 业绩走势图 + + + + + + + + 业绩数据列表 + + + + + + {{ scope.row[col.prop] || '0' }} + + + + + + + + + + + + + 全年门店消耗表 + + + + + + 消耗走势图 + + + + + + + + 消耗数据列表 + + + + + + {{ scope.row[col.prop] || '0' }} + + + + + + + + + + + + + 年度门店人头表 + + + + + + 人头数走势图 + + + + + + + + 人头数据列表 + + + + + + {{ scope.row[col.prop] || '0' }} + + + + + + + + + + + + + 年度门店人次表 + + + + + + 人次走势图 + + + + + + + + 人次数据列表 + + + + + + {{ scope.row[col.prop] || '0' }} + + + + + + + + + + + + + 年度门店项目数表 + + + + + + 项目数走势图 + + + + + + + + 项目数据列表 + + + + + + {{ scope.row[col.prop] || '0' }} + + + + + + + + + + + + + 门店五项指标统计 + + + + + + + 总业绩 + + + + 总消耗 + + + + 客头数 + + + + 客次数 + + + + 项目数 + + + + + + + + + 各门店指标对比 + + + + + + + + + 门店占比分析 + + + + + + + + + + 门店指标数据列表 + + + + + + {{ scope.row[col.prop] || '0' }} + + + + + + + + + + + + + 事业部五项指标统计 + + + + + + + 总业绩 + + + + 总消耗 + + + + 客头数 + + + + 客次数 + + + + 项目数 + + + + + + + + + 事业部贡献占比 + + + + + + + + + 事业部增长率分析 + + + + + + + + + + 事业部指标数据列表 + + + + + + {{ scope.row[col.prop] || '0' }} + + + + + + + + + + + + + 事业部内部汇总 + + + + + + 事业部汇总数据列表 + + + + + + {{ scope.row[col.prop] || '0' }} + + + + + + + + + + + + + + diff --git a/antis-ncc-admin/src/views/extend/annualSummary/dataManage/index.vue b/antis-ncc-admin/src/views/extend/annualSummary/dataManage/index.vue new file mode 100644 index 0000000..20b9a64 --- /dev/null +++ b/antis-ncc-admin/src/views/extend/annualSummary/dataManage/index.vue @@ -0,0 +1,437 @@ + + + + + + + 年度经营数据管理 + + + + + + 导入年度经营数据 + + + + + + + + + + + + + + + + + + + + + + + + + 查询 + 重置 + + + + + + + + + + {{ scope.row[col.prop] || '-' }} + + + + + + + 编辑 + + + + 删除 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/antis-ncc-admin/src/views/lqTkjlb/Report.vue b/antis-ncc-admin/src/views/lqTkjlb/Report.vue index 1f28eb5..3e415e7 100644 --- a/antis-ncc-admin/src/views/lqTkjlb/Report.vue +++ b/antis-ncc-admin/src/views/lqTkjlb/Report.vue @@ -652,6 +652,7 @@ + diff --git a/antis-ncc-admin/src/views/personalPerformanceStatistics/index.vue b/antis-ncc-admin/src/views/personalPerformanceStatistics/index.vue index aa584a5..a8f7f56 100644 --- a/antis-ncc-admin/src/views/personalPerformanceStatistics/index.vue +++ b/antis-ncc-admin/src/views/personalPerformanceStatistics/index.vue @@ -41,13 +41,10 @@ - - 健康师个人开单业绩列表 - - + @@ -79,7 +76,7 @@ {{ formatMoney(scope.row.ActualPerformance) - }} + }} diff --git a/antis-ncc-admin/src/views/salaryCalculation/index.vue b/antis-ncc-admin/src/views/salaryCalculation/index.vue index 07cbe1f..0fcc55b 100644 --- a/antis-ncc-admin/src/views/salaryCalculation/index.vue +++ b/antis-ncc-admin/src/views/salaryCalculation/index.vue @@ -58,7 +58,7 @@ - + 健康师个人开单业绩 @@ -127,7 +127,7 @@ - + 门店总业绩 @@ -144,7 +144,7 @@ - + 工资统计 @@ -161,7 +161,7 @@ - + 更多统计方法 diff --git a/antis-ncc-admin/src/views/statisticsList/form9_backup.vue b/antis-ncc-admin/src/views/statisticsList/form9_backup.vue new file mode 100644 index 0000000..c2b1033 --- /dev/null +++ b/antis-ncc-admin/src/views/statisticsList/form9_backup.vue @@ -0,0 +1,2041 @@ + + + + + + + + + - + + + + + + + + + + 查 询 + 重 置 + + + + + + + + + + 业务统计 + + + + + + + + + + {{ item.displayValue }} + {{ item.title }} + + + + + + + + + + + + + + 客户类型统计 + + + + + + + + + + 拓客总人数 + {{ customerData.TotalInviteCount || 0 }} + + + + + + + + + + 新客数量 + {{ customerData.NewCustomerCount || 0 }} + + + + + + + + + + 散客数 + {{ customerData.CasualCustomerCount || 0 }} + + + + + + + + + + 会员数 + {{ customerData.MemberCount || 0 }} + + + + + + + + + 会员转化率 + {{ formatPercent((customerData.MemberCount / customerData.TotalInviteCount) * 100) }}% + + + + + + + + + + + + + + + + + 门店业绩对比 + + + + + + + + + + + + 品项统计对比 + + + + + + + 开单金额 + + + + 消耗金额 + + + + + + + + + {{ item.BillingAmount || 0 }} + + + + + + + {{ item.ItemName || item.ItemId }} + + + + + + + {{ item.ConsumeAmount || 0 }} + + + + + + + + + + + + + + + + + 客户到店次数分布 + + + + + 暂无数据 + + + + + + + + + + 拓客转化漏斗图 + + + + + + + + + 拓客邀约率 + {{ formatPercent(getTkInviteRate()) }}% + + + + 邀约到店率 + {{ formatPercent(getInviteStoreRate()) }}% + + + + 预约转化率 + {{ formatPercent(getStoreConversionRate()) }}% + + + + + + 暂无数据 + + + + + + + + + + + 门店项目统计 + + + 字段配置 + + + + + + + + + + ¥{{ formatMoney(scope.row.BillingAmount) }} + + + + + + + + + {{ formatMoney(scope.row.AvgProjectPerConsume) }} + + + + + ¥{{ formatMoney(scope.row.ConsumeAmount) }} + + + + + ¥{{ formatMoney(scope.row.AvgAmountPerConsume) }} + + + + + {{ formatPercent((scope.row.ConsumeRate || 0) ) }}% + + + + + + 暂无数据 + + + + + + + 全选 + 全不选 + + + + {{ field.label }} + + + + + 取消 + 确定 + + + + + + + + + + 健康师开单排名 + + + + + + + + ¥{{ formatMoney(scope.row.BillingPerformance) }} + + + + + ¥{{ formatMoney(scope.row.ConsumePerformance) }} + + + + + ¥{{ formatMoney(scope.row.RefundPerformance) }} + + + + + + + + + 暂无数据 + + + + + + + + 门店剩余权益统计 + + + + + + + ¥{{ formatMoney(scope.row.RemainingRightsAmount) }} + + + + + + + 暂无数据 + + + + + + + + \ No newline at end of file diff --git a/excel/历史数据归档.xlsx b/excel/历史数据归档.xlsx new file mode 100644 index 0000000..db7578a --- /dev/null +++ b/excel/历史数据归档.xlsx diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqAnnualSummary/AnnualStatDto.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqAnnualSummary/AnnualStatDto.cs new file mode 100644 index 0000000..4797327 --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqAnnualSummary/AnnualStatDto.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; + +namespace NCC.Extend.Entitys.Dto.LqAnnualSummary +{ + /// + /// 全年门店月度数据统计输出 (用于报表 4.1 - 4.5) + /// + public class AnnualMonthlyStatOutput + { + /// + /// 1-12月数据列定义 + /// + public List MonthColumns { get; set; } = new List + { + "Month1", "Month2", "Month3", "Month4", "Month5", "Month6", + "Month7", "Month8", "Month9", "Month10", "Month11", "Month12" + }; + + /// + /// 数据行 + /// + public List Rows { get; set; } = new List(); + } + + public class MonthlyDataRow + { + public string BusinessUnitName { get; set; } + public string StoreName { get; set; } + + // 1-12月数值 + public decimal Month1 { get; set; } + public decimal Month2 { get; set; } + public decimal Month3 { get; set; } + public decimal Month4 { get; set; } + public decimal Month5 { get; set; } + public decimal Month6 { get; set; } + public decimal Month7 { get; set; } + public decimal Month8 { get; set; } + public decimal Month9 { get; set; } + public decimal Month10 { get; set; } + public decimal Month11 { get; set; } + public decimal Month12 { get; set; } + + // 上年度1-12月数值 (用于同比走势图) + public decimal LastMonth1 { get; set; } + public decimal LastMonth2 { get; set; } + public decimal LastMonth3 { get; set; } + public decimal LastMonth4 { get; set; } + public decimal LastMonth5 { get; set; } + public decimal LastMonth6 { get; set; } + public decimal LastMonth7 { get; set; } + public decimal LastMonth8 { get; set; } + public decimal LastMonth9 { get; set; } + public decimal LastMonth10 { get; set; } + public decimal LastMonth11 { get; set; } + public decimal LastMonth12 { get; set; } + + public decimal TotalCurrentYear { get; set; } + public decimal AvgCurrentYear { get; set; } + public decimal TotalLastYear { get; set; } + public decimal AvgLastYear { get; set; } + public string GrowthRate { get; set; } // 百分比字符串 + } + + /// + /// 指标统计输出 (用于报表 4.6, 4.7) + /// + public class IndicatorStatOutput + { + public List Rows { get; set; } = new List(); + } + + public class IndicatorDataRow + { + public string BusinessUnitName { get; set; } + public string StoreName { get; set; } + public decimal LastYearValue { get; set; } + public decimal CurrentYearValue { get; set; } + public string GrowthRate { get; set; } + } + + /// + /// 事业部内部汇总输出 (用于报表 4.8) + /// + public class BusinessUnitSummaryOutput + { + public List Rows { get; set; } = new List(); + } + + public class BusinessUnitSummaryRow + { + public string BusinessUnitName { get; set; } + public string StoreName { get; set; } + + // 业绩 + public decimal LastYearPerformance { get; set; } + public decimal CurrentYearPerformance { get; set; } + public string PerformanceGrowthRate { get; set; } + + // 消耗 + public decimal LastYearConsume { get; set; } + public decimal CurrentYearConsume { get; set; } + public string ConsumeGrowthRate { get; set; } + + // 人头 + public decimal LastYearHeadCount { get; set; } + public decimal CurrentYearHeadCount { get; set; } + public string HeadCountGrowthRate { get; set; } + + // 人次 + public decimal LastYearPersonTime { get; set; } + public decimal CurrentYearPersonTime { get; set; } + public string PersonTimeGrowthRate { get; set; } + + // 项目数 + public decimal LastYearProjectCount { get; set; } + public decimal CurrentYearProjectCount { get; set; } + public string ProjectCountGrowthRate { get; set; } + } +} diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqAnnualSummary/AnnualSummaryDto.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqAnnualSummary/AnnualSummaryDto.cs new file mode 100644 index 0000000..3761a4e --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqAnnualSummary/AnnualSummaryDto.cs @@ -0,0 +1,46 @@ +using System; +using System.ComponentModel.DataAnnotations; +using NCC.Common.Filter; + +namespace NCC.Extend.Entitys.Dto.LqAnnualSummary +{ + public class AnnualSummaryQueryInput : NCC.Common.Filter.PageInputBase + { + public string StoreName { get; set; } + public string StoreId { get; set; } + public int? Year { get; set; } + public int? Month { get; set; } + } + + public class AnnualSummaryInput + { + public string Id { get; set; } + [Required(ErrorMessage = "门店不能为空")] + public string StoreId { get; set; } + public string StoreName { get; set; } + public string BusinessUnitId { get; set; } + public string BusinessUnitName { get; set; } + [Required(ErrorMessage = "年份不能为空")] + public int Year { get; set; } + [Required(ErrorMessage = "月份不能为空")] + public int Month { get; set; } + public decimal TotalPerformance { get; set; } + public decimal TotalConsume { get; set; } + public decimal HeadCount { get; set; } + public decimal PersonTime { get; set; } + public decimal ProjectCount { get; set; } + } + + public class AnnualSummaryImportDto + { + public string StoreName { get; set; } + public string BusinessUnitName { get; set; } + public int Year { get; set; } + public int Month { get; set; } + public decimal TotalPerformance { get; set; } + public decimal TotalConsume { get; set; } + public decimal HeadCount { get; set; } + public decimal PersonTime { get; set; } + public decimal ProjectCount { get; set; } + } +} diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqBusinessUnitManagerSalary/BusinessUnitManagerSalaryOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqBusinessUnitManagerSalary/BusinessUnitManagerSalaryOutput.cs index c6e1ee8..89fec7a 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqBusinessUnitManagerSalary/BusinessUnitManagerSalaryOutput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqBusinessUnitManagerSalary/BusinessUnitManagerSalaryOutput.cs @@ -53,6 +53,36 @@ namespace NCC.Extend.Entitys.Dto.LqBusinessUnitManagerSalary public string StorePerformanceDetail { get; set; } /// + /// 销售业绩(开单业绩-退款业绩) + /// + public decimal SalesPerformance { get; set; } + + /// + /// 产品物料(仓库领用金额) + /// + public decimal ProductMaterial { get; set; } + + /// + /// 合作项目成本 + /// + public decimal CooperationCost { get; set; } + + /// + /// 店内支出 + /// + public decimal StoreExpense { get; set; } + + /// + /// 洗毛巾费用 + /// + public decimal LaundryCost { get; set; } + + /// + /// 毛利(销售业绩-产品物料-合作项目成本-店内支出-洗毛巾) + /// + public decimal GrossProfit { get; set; } + + /// /// 底薪 /// public decimal BaseSalary { get; set; } diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdDeductinfo/LqKdDeductinfoListQueryInput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdDeductinfo/LqKdDeductinfoListQueryInput.cs index 75dd9ba..6b5e867 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdDeductinfo/LqKdDeductinfoListQueryInput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdDeductinfo/LqKdDeductinfoListQueryInput.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using NCC.Common.Filter; namespace NCC.Extend.Entitys.Dto.LqKdDeductinfo @@ -67,5 +68,30 @@ namespace NCC.Extend.Entitys.Dto.LqKdDeductinfo /// 结束创建时间 /// public DateTime? EndCreateTime { get; set; } + + /// + /// 门店ID(筛选) + /// + public string StoreId { get; set; } + + /// + /// 门店ID列表(支持多门店筛选) + /// + public List StoreIds { get; set; } + + /// + /// 开始时间(开单时间筛选) + /// + public DateTime? StartTime { get; set; } + + /// + /// 结束时间(开单时间筛选) + /// + public DateTime? EndTime { get; set; } + + /// + /// 品项分类(科美、医美、生美、产品等) + /// + public string ItemCategory { get; set; } } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/HealthCoachStatisticsOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/HealthCoachStatisticsOutput.cs index 9afc45a..f85796b 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/HealthCoachStatisticsOutput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/HealthCoachStatisticsOutput.cs @@ -109,5 +109,15 @@ namespace NCC.Extend.Entitys.Dto.LqKdKdjlb /// 加班手工费 - 统计该健康师在指定时间周期内消耗时的加班手工费总金额 /// public decimal overtimeLaborCost { get; set; } + + /// + /// 金三角名称 - 该健康师所在的金三角战队名称 + /// + public string goldTriangleName { get; set; } + + /// + /// 队伍业绩占比 - 该健康师所在金三角的业绩占门店总业绩的比例(百分比,0-100) + /// + public decimal teamPerformanceRatio { get; set; } } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxInfoOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxInfoOutput.cs index 17e3ab7..b5b570c 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxInfoOutput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxInfoOutput.cs @@ -136,6 +136,11 @@ namespace NCC.Extend.Entitys.Dto.LqKhxx public string subHealthUserName { get; set; } /// + /// 最后消费时间 + /// + public DateTime? lastConsumeTime { get; set; } + + /// /// 是否生美会员(0-否,1-是) /// public int isBeautyMember { get; set; } diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxListOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxListOutput.cs index 5108881..5b32796 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxListOutput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxListOutput.cs @@ -163,6 +163,11 @@ namespace NCC.Extend.Entitys.Dto.LqKhxx public string subHealthUserName { get; set; } /// + /// 最后消费时间 + /// + public DateTime? lastConsumeTime { get; set; } + + /// /// 是否生美会员(0-否,1-是) /// public int isBeautyMember { get; set; } diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXmzl/ItemStoreStatisticsOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXmzl/ItemStoreStatisticsOutput.cs index 2f23bfc..ac3827d 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXmzl/ItemStoreStatisticsOutput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXmzl/ItemStoreStatisticsOutput.cs @@ -24,11 +24,31 @@ namespace NCC.Extend.Entitys.Dto.LqXmzl public int BillingCount { get; set; } /// - /// 项目数(项目次数总和) + /// 项目数(项目次数总和,已废弃,使用TotalProjectCount代替) /// public decimal ProjectCount { get; set; } /// + /// 总项目数(项目次数总和) + /// + public decimal TotalProjectCount { get; set; } + + /// + /// 购买项目数(来源类型为"购买"的项目次数总和) + /// + public decimal PurchaseProjectCount { get; set; } + + /// + /// 体验项目数(来源类型为"体验"的项目次数总和) + /// + public decimal ExperienceProjectCount { get; set; } + + /// + /// 赠送项目数(来源类型为"赠送"的项目次数总和) + /// + public decimal GiftProjectCount { get; set; } + + /// /// 实付金额(实付金额总和) /// public decimal ActualAmount { get; set; } diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXmzl/LqXmzlStatisticsInput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXmzl/LqXmzlStatisticsInput.cs index d939edb..ed1f5fa 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXmzl/LqXmzlStatisticsInput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXmzl/LqXmzlStatisticsInput.cs @@ -23,11 +23,16 @@ namespace NCC.Extend.Entitys.Dto.LqXmzl public string StoreId { get; set; } /// - /// 品项分类 + /// 品项分类(已废弃,使用ItemCategory代替) /// public string Category { get; set; } /// + /// 品项分类筛选(科美、医美、生美、产品等,对应lq_xmzl表的qt2字段) + /// + public string ItemCategory { get; set; } + + /// /// 品项ID(单个品项统计) /// public string ItemId { get; set; } diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXmzl/LqXmzlStatisticsOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXmzl/LqXmzlStatisticsOutput.cs index 86cfd82..9660ad2 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXmzl/LqXmzlStatisticsOutput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXmzl/LqXmzlStatisticsOutput.cs @@ -86,5 +86,20 @@ namespace NCC.Extend.Entitys.Dto.LqXmzl /// 退卡次数 /// public int RefundCount { get; set; } + + /// + /// 储扣金额 + /// + public decimal DeductAmount { get; set; } + + /// + /// 储扣次数 + /// + public int DeductCount { get; set; } + + /// + /// 品项分类(科美、医美、生美、产品等,对应lq_xmzl表的qt2字段) + /// + public string ItemCategory { get; set; } } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_annual_summary/LqAnnualSummaryEntity.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_annual_summary/LqAnnualSummaryEntity.cs new file mode 100644 index 0000000..f8c2050 --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_annual_summary/LqAnnualSummaryEntity.cs @@ -0,0 +1,116 @@ +using SqlSugar; +using System; +using NCC.Common.Const; + +namespace NCC.Extend.Entitys.lq_annual_summary +{ + /// + /// 年度汇总表 + /// + [SugarTable("lq_annual_summary")] + [Tenant(ClaimConst.TENANT_ID)] + public class LqAnnualSummaryEntity + { + /// + /// 主键ID + /// + [SugarColumn(ColumnName = "F_Id", IsPrimaryKey = true)] + public string Id { get; set; } + + /// + /// 门店ID + /// + [SugarColumn(ColumnName = "F_StoreId")] + public string StoreId { get; set; } + + /// + /// 门店名称 + /// + [SugarColumn(ColumnName = "F_StoreName")] + public string StoreName { get; set; } + + /// + /// 归属事业部ID + /// + [SugarColumn(ColumnName = "F_BusinessUnitId")] + public string BusinessUnitId { get; set; } + + /// + /// 归属事业部名称 + /// + [SugarColumn(ColumnName = "F_BusinessUnitName")] + public string BusinessUnitName { get; set; } + + /// + /// 年份 + /// + [SugarColumn(ColumnName = "F_Year")] + public int Year { get; set; } + + /// + /// 月份 + /// + [SugarColumn(ColumnName = "F_Month")] + public int Month { get; set; } + + /// + /// 总业绩 + /// + [SugarColumn(ColumnName = "F_TotalPerformance")] + public decimal TotalPerformance { get; set; } + + /// + /// 总消耗 + /// + [SugarColumn(ColumnName = "F_TotalConsume")] + public decimal TotalConsume { get; set; } + + /// + /// 人头数 + /// + [SugarColumn(ColumnName = "F_HeadCount")] + public decimal HeadCount { get; set; } + + /// + /// 人次数 + /// + [SugarColumn(ColumnName = "F_PersonTime")] + public decimal PersonTime { get; set; } + + /// + /// 项目数 + /// + [SugarColumn(ColumnName = "F_ProjectCount")] + public decimal ProjectCount { get; set; } + + /// + /// 是否有效 0无效 1有效 + /// + [SugarColumn(ColumnName = "F_IsEffective")] + public int IsEffective { get; set; } = 1; + + /// + /// 创建时间 + /// + [SugarColumn(ColumnName = "F_CreateTime")] + public DateTime? CreateTime { get; set; } + + /// + /// 创建人 + /// + [SugarColumn(ColumnName = "F_CreateUser")] + public string CreateUser { get; set; } + + /// + /// 更新时间 + /// + [SugarColumn(ColumnName = "F_UpdateTime")] + public DateTime? UpdateTime { get; set; } + + /// + /// 更新人 + /// + [SugarColumn(ColumnName = "F_UpdateUser")] + public string UpdateUser { get; set; } + } +} diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_business_unit_manager_salary_statistics/LqBusinessUnitManagerSalaryStatisticsEntity.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_business_unit_manager_salary_statistics/LqBusinessUnitManagerSalaryStatisticsEntity.cs index d13f728..b9ff865 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_business_unit_manager_salary_statistics/LqBusinessUnitManagerSalaryStatisticsEntity.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_business_unit_manager_salary_statistics/LqBusinessUnitManagerSalaryStatisticsEntity.cs @@ -72,6 +72,42 @@ namespace NCC.Extend.Entitys.lq_business_unit_manager_salary_statistics public decimal BaseSalary { get; set; } /// + /// 销售业绩(开单业绩-退款业绩) + /// + [SugarColumn(ColumnName = "F_SalesPerformance")] + public decimal SalesPerformance { get; set; } + + /// + /// 产品物料(仓库领用金额,注意11月特殊规则:11月工资算10月数据) + /// + [SugarColumn(ColumnName = "F_ProductMaterial")] + public decimal ProductMaterial { get; set; } + + /// + /// 合作项目成本 + /// + [SugarColumn(ColumnName = "F_CooperationCost")] + public decimal CooperationCost { get; set; } + + /// + /// 店内支出 + /// + [SugarColumn(ColumnName = "F_StoreExpense")] + public decimal StoreExpense { get; set; } + + /// + /// 洗毛巾费用(只统计送出的记录,F_FlowType = 0) + /// + [SugarColumn(ColumnName = "F_LaundryCost")] + public decimal LaundryCost { get; set; } + + /// + /// 毛利(销售业绩-产品物料-合作项目成本-店内支出-洗毛巾) + /// + [SugarColumn(ColumnName = "F_GrossProfit")] + public decimal GrossProfit { get; set; } + + /// /// 提成合计(所有门店提成金额汇总) /// [SugarColumn(ColumnName = "F_TotalCommission")] diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Mapper/LqKhxxMapper.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Mapper/LqKhxxMapper.cs index ac43793..47d34ed 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Mapper/LqKhxxMapper.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Mapper/LqKhxxMapper.cs @@ -23,6 +23,7 @@ namespace NCC.Extend.Entitys.Mapper.LqKhxx .Map(dest => dest.techMemberTime, src => src.TechMemberTime) .Map(dest => dest.firstVisitTime, src => src.FirstVisitTime) .Map(dest => dest.lastVisitTime, src => src.LastVisitTime) + .Map(dest => dest.lastConsumeTime, src => src.LastConsumeTime) .Map(dest => dest.visitDays, src => src.VisitDays) .Map(dest => dest.sleepStartTime, src => src.SleepStartTime) .Map(dest => dest.sleepDays, src => src.SleepDays) diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqAnnualSummaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqAnnualSummaryService.cs new file mode 100644 index 0000000..e7945ab --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqAnnualSummaryService.cs @@ -0,0 +1,1358 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using NCC.Common.Filter; +using NCC.Common.Helper; +using NCC.Dependency; +using NCC.DynamicApiController; +using NCC.Extend.Entitys.Dto.LqAnnualSummary; +using NCC.Extend.Entitys.lq_annual_summary; +using NCC.Extend.Entitys.lq_mdxx; +using NCC.System.Entitys.Permission; +using Mapster; +using NPOI.HSSF.UserModel; +using NPOI.SS.UserModel; +using NPOI.XSSF.UserModel; +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Yitter.IdGenerator; + +namespace NCC.Extend +{ + /// + /// 年度汇总表服务 + /// + [ApiDescriptionSettings(Tag = "年度经营数据汇总", Name = "LqAnnualSummary", Order = 500)] + [Route("api/Extend/[controller]")] + public class LqAnnualSummaryService : IDynamicApiController, ITransient + { + private readonly ISqlSugarClient _db; + + /// + /// 初始化一个类型的新实例 + /// + /// 数据库客户端 + public LqAnnualSummaryService(ISqlSugarClient db) + { + _db = db; + } + + #region 基础 CRUD + + /// + /// 分页查询 + /// + [HttpGet("list")] + public async Task GetList([FromQuery] AnnualSummaryQueryInput input) + { + var query = _db.Queryable() + .Where(x => x.IsEffective == 1); + + if (!string.IsNullOrEmpty(input.StoreName)) + { + query = query.Where(x => x.StoreName.Contains(input.StoreName)); + } + if (!string.IsNullOrEmpty(input.StoreId)) + { + query = query.Where(x => x.StoreId == input.StoreId); + } + if (input.Year.HasValue) + { + query = query.Where(x => x.Year == input.Year.Value); + } + if (input.Month.HasValue) + { + query = query.Where(x => x.Month == input.Month.Value); + } + + var list = await query.OrderBy(x => x.Year, OrderByType.Desc) + .OrderBy(x => x.Month, OrderByType.Desc) + .OrderBy(x => x.StoreId) + .ToPagedListAsync(input.currentPage, input.pageSize); + + // 获取事业部名称映射 + var buNameMap = await GetBusinessUnitNameMapAsync(); + + // 获取所有门店信息,用于获取正确的事业部信息 + var storeIds = list.list.Select(x => x.StoreId).Distinct().ToList(); + var stores = await _db.Queryable() + .Where(x => storeIds.Contains(x.Id)) + .Select(x => new { x.Id, x.Syb, x.Kjb }) + .ToListAsync(); + var storeDict = stores.ToDictionary(x => x.Id, x => x); + + var result = new + { + pagination = list.pagination, + list = list.list.Select(x => + { + // 确定正确的事业部ID + string correctBuId = null; + string correctBuName = null; + + // 1. 如果BusinessUnitId不为空,检查是否是科技部 + if (!string.IsNullOrEmpty(x.BusinessUnitId)) + { + var buName = GetBusinessUnitDisplayName(x.BusinessUnitId, buNameMap); + // 如果名称包含"科技",说明是科技部,需要从门店表获取事业部 + if (buName.Contains("科技")) + { + // 从门店表获取事业部ID + if (storeDict.ContainsKey(x.StoreId)) + { + correctBuId = storeDict[x.StoreId].Syb; + if (!string.IsNullOrEmpty(correctBuId)) + { + correctBuName = GetBusinessUnitDisplayName(correctBuId, buNameMap); + } + } + } + else + { + // 不是科技部,使用原有的BusinessUnitId + correctBuId = x.BusinessUnitId; + correctBuName = buName; + } + } + else if (!string.IsNullOrEmpty(x.BusinessUnitName)) + { + // 2. 如果BusinessUnitId为空,但BusinessUnitName不为空 + var buName = GetBusinessUnitDisplayName(x.BusinessUnitName, buNameMap); + if (buName.Contains("科技")) + { + // 从门店表获取事业部ID + if (storeDict.ContainsKey(x.StoreId)) + { + correctBuId = storeDict[x.StoreId].Syb; + if (!string.IsNullOrEmpty(correctBuId)) + { + correctBuName = GetBusinessUnitDisplayName(correctBuId, buNameMap); + } + } + } + else + { + // 尝试从BusinessUnitName中解析ID + if (buNameMap.ContainsKey(x.BusinessUnitName)) + { + correctBuName = buNameMap[x.BusinessUnitName]; + } + else + { + correctBuName = buName; + } + } + } + else + { + // 3. 如果都为空,从门店表获取事业部 + if (storeDict.ContainsKey(x.StoreId)) + { + correctBuId = storeDict[x.StoreId].Syb; + if (!string.IsNullOrEmpty(correctBuId)) + { + correctBuName = GetBusinessUnitDisplayName(correctBuId, buNameMap); + } + } + } + + // 如果还是没有找到,返回"未知" + if (string.IsNullOrEmpty(correctBuName)) + { + correctBuName = "未知"; + } + + return new + { + id = x.Id, + storeId = x.StoreId, + storeName = x.StoreName, + businessUnitId = correctBuId, + businessUnitName = correctBuName, + year = x.Year, + month = x.Month, + totalPerformance = x.TotalPerformance, + totalConsume = x.TotalConsume, + headCount = x.HeadCount, + personTime = x.PersonTime, + projectCount = x.ProjectCount + }; + }).ToList() + }; + + return result; + } + + /// + /// 保存(新增或更新) + /// + [HttpPost("save")] + public async Task Save([FromBody] AnnualSummaryInput input) + { + if (string.IsNullOrEmpty(input.Id)) + { + // Uniqueness check + var exist = await _db.Queryable() + .AnyAsync(x => x.StoreId == input.StoreId && x.Year == input.Year && x.Month == input.Month && x.IsEffective == 1); + if (exist) throw new Exception($"该门店 {input.Year}年{input.Month}月 数据已存在"); + + var entity = input.Adapt(); + entity.Id = YitIdHelper.NextId().ToString(); + entity.CreateTime = DateTime.Now; + entity.IsEffective = 1; + await _db.Insertable(entity).ExecuteCommandAsync(); + } + else + { + var entity = await _db.Queryable().FirstAsync(x => x.Id == input.Id); + if (entity == null) throw new Exception("数据不存在"); + + // Update fields + entity.TotalPerformance = input.TotalPerformance; + entity.TotalConsume = input.TotalConsume; + entity.HeadCount = input.HeadCount; + entity.PersonTime = input.PersonTime; + entity.ProjectCount = input.ProjectCount; + + entity.UpdateTime = DateTime.Now; + await _db.Updateable(entity).ExecuteCommandAsync(); + } + } + + /// + /// 删除 + /// + [HttpPost("delete")] + public async Task Delete([FromBody] List ids) + { + await _db.Updateable() + .SetColumns(x => x.IsEffective == 0) + .Where(x => ids.Contains(x.Id)) + .ExecuteCommandAsync(); + } + + #endregion + + #region Excel 导入导出 + + /// + /// 导入数据 + /// + [HttpPost("import")] + public async Task Import(IFormFile file) + { + if (file == null || file.Length == 0) throw new Exception("请上传文件"); + + // 检查文件格式 + var allowedExtensions = new[] { ".xlsx", ".xls" }; + var fileExtension = Path.GetExtension(file.FileName).ToLowerInvariant(); + if (!allowedExtensions.Contains(fileExtension)) + { + throw new Exception("只支持.xlsx和.xls格式的Excel文件"); + } + + // 保存临时文件 + var tempFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + Path.GetExtension(file.FileName)); + try + { + using (var stream = new FileStream(tempFilePath, FileMode.Create)) + { + await file.CopyToAsync(stream); + } + + // 使用NPOI直接读取Excel文件,支持读取公式的计算值 + var list = new List(); + var errorMessages = new List(); + + using (var fileStream = new FileStream(tempFilePath, FileMode.Open, FileAccess.Read)) + { + IWorkbook workbook = null; + if (fileExtension == ".xlsx") + { + workbook = new XSSFWorkbook(fileStream); + } + else + { + workbook = new HSSFWorkbook(fileStream); + } + + ISheet sheet = workbook.GetSheetAt(0); // 第一个工作表 + if (sheet == null || sheet.LastRowNum < 1) + { + throw new Exception("Excel文件中没有数据行(至少需要标题行和一行数据)"); + } + + // 读取标题行,确定列索引 + var headerRow = sheet.GetRow(0); + if (headerRow == null) + { + throw new Exception("Excel文件第一行(标题行)为空"); + } + + // 根据列名查找列索引(支持多种可能的列名) + int GetColumnIndex(string[] possibleNames) + { + for (int col = 0; col < headerRow.LastCellNum; col++) + { + var cell = headerRow.GetCell(col); + if (cell == null) continue; + var columnName = GetCellValue(cell)?.Trim() ?? ""; + foreach (var name in possibleNames) + { + if (columnName == name || columnName.Contains(name) || name.Contains(columnName)) + { + return col; + } + } + } + return -1; + } + + // 查找各列的索引(根据实际Excel格式) + var businessUnitColIndex = GetColumnIndex(new[] { "事业部", "归属事业部", "BusinessUnit" }); + var storeNameColIndex = GetColumnIndex(new[] { "门店", "门店名称", "店名", "店铺名称" }); + var yearColIndex = GetColumnIndex(new[] { "年份", "年", "Year" }); + var monthColIndex = GetColumnIndex(new[] { "月份", "月", "Month" }); + var perfColIndex = GetColumnIndex(new[] { "总业绩", "业绩", "总营业额", "TotalPerformance" }); + var consumeColIndex = GetColumnIndex(new[] { "总消耗", "消耗", "TotalConsume" }); + var headColIndex = GetColumnIndex(new[] { "人头数", "客头数", "HeadCount" }); + var personColIndex = GetColumnIndex(new[] { "人次数", "客次数", "PersonTime" }); + var projectColIndex = GetColumnIndex(new[] { "项目数", "总项目数", "ProjectCount" }); + + // 验证必填列 + if (storeNameColIndex == -1) + { + throw new Exception("Excel文件中未找到门店名称列,请确保第一行包含'门店名称'列"); + } + if (yearColIndex == -1) + { + throw new Exception("Excel文件中未找到年份列,请确保第一行包含'年份'列"); + } + if (monthColIndex == -1) + { + throw new Exception("Excel文件中未找到月份列,请确保第一行包含'月份'列"); + } + + // 从第1行开始读取数据(跳过标题行,索引0是标题行) + for (int i = 1; i <= sheet.LastRowNum; i++) + { + var row = sheet.GetRow(i); + if (row == null) continue; + + try + { + // 读取门店名称,支持公式计算值 + var storeNameCell = row.GetCell(storeNameColIndex); + if (storeNameCell == null) continue; + + string storeName = GetCellValue(storeNameCell); + if (string.IsNullOrWhiteSpace(storeName)) continue; // 跳过空行 + + // 检测是否为公式文本(如果GetCellValue返回的是公式文本,说明计算失败) + if (storeName.StartsWith("_xlfn.") || storeName.StartsWith("=") || + storeName.Contains("XLOOKUP") || storeName.Contains("VLOOKUP")) + { + errorMessages.Add($"第{i + 1}行:门店名称包含无法计算的公式({storeName}),已跳过。请确保门店名称列是实际值而不是公式。"); + continue; + } + + // 读取事业部名称(从Excel中读取) + string businessUnitName = null; + if (businessUnitColIndex >= 0) + { + var businessUnitCell = row.GetCell(businessUnitColIndex); + if (businessUnitCell != null) + { + businessUnitName = GetCellValue(businessUnitCell); + if (!string.IsNullOrWhiteSpace(businessUnitName)) + { + businessUnitName = businessUnitName.Trim(); + } + } + } + + // 读取其他字段 + var yearCell = row.GetCell(yearColIndex); + var monthCell = row.GetCell(monthColIndex); + var perfCell = perfColIndex >= 0 ? row.GetCell(perfColIndex) : null; + var consumeCell = consumeColIndex >= 0 ? row.GetCell(consumeColIndex) : null; + var headCell = headColIndex >= 0 ? row.GetCell(headColIndex) : null; + var personCell = personColIndex >= 0 ? row.GetCell(personColIndex) : null; + var projectCell = projectColIndex >= 0 ? row.GetCell(projectColIndex) : null; + + // 解析年份(支持"2024年"格式) + int year = 0; + var yearStr = GetCellValue(yearCell); + if (!string.IsNullOrWhiteSpace(yearStr)) + { + // 提取数字部分(如"2024年" -> "2024") + var yearMatch = Regex.Match(yearStr, @"(\d{4})"); + if (yearMatch.Success) + { + int.TryParse(yearMatch.Groups[1].Value, out year); + } + else + { + int.TryParse(yearStr, out year); + } + } + + // 解析月份(支持"1月"、"01月"格式) + int month = 0; + var monthStr = GetCellValue(monthCell); + if (!string.IsNullOrWhiteSpace(monthStr)) + { + // 提取数字部分(如"1月" -> "1") + var monthMatch = Regex.Match(monthStr, @"(\d{1,2})"); + if (monthMatch.Success) + { + int.TryParse(monthMatch.Groups[1].Value, out month); + } + else + { + int.TryParse(monthStr, out month); + } + } + + var item = new AnnualSummaryImportDto + { + StoreName = storeName.Trim(), + BusinessUnitName = businessUnitName, + Year = year, + Month = month, + TotalPerformance = perfCell != null && decimal.TryParse(GetCellValue(perfCell), out var perf) ? perf : 0m, + TotalConsume = consumeCell != null && decimal.TryParse(GetCellValue(consumeCell), out var consume) ? consume : 0m, + HeadCount = headCell != null && decimal.TryParse(GetCellValue(headCell), out var head) ? head : 0m, + PersonTime = personCell != null && decimal.TryParse(GetCellValue(personCell), out var person) ? person : 0m, + ProjectCount = projectCell != null && decimal.TryParse(GetCellValue(projectCell), out var project) ? project : 0m + }; + list.Add(item); + } + catch (Exception ex) + { + errorMessages.Add($"第{i + 1}行:读取数据时出错 - {ex.Message}"); + continue; + } + } + + workbook.Close(); + } + + if (!list.Any()) + { + if (errorMessages.Any()) + { + throw new Exception($"导入失败:没有有效数据。\n{string.Join("\n", errorMessages)}"); + } + throw new Exception("导入失败:Excel文件中没有有效数据行"); + } + + // 预加载所有门店信息,用于匹配 StoreId 和 BusinessUnit + var allStores = await _db.Queryable().ToListAsync(); + + // 预加载所有组织机构信息,用于匹配事业部 + var allOrgs = await _db.Queryable() + .Where(x => x.DeleteMark == null) + .Select(x => new { x.Id, x.FullName }) + .ToListAsync(); + + var entities = new List(); + var successCount = 0; + var failCount = 0; + + foreach (var item in list) + { + if (string.IsNullOrEmpty(item.StoreName)) + { + failCount++; + continue; + } + + try + { + // 增强匹配逻辑:支持模糊匹配和简称 + // Excel中的门店名称可能是简化名称(如"紫荆"),需要匹配完整名称(如"绿纤紫荆店") + var store = allStores.FirstOrDefault(x => x.Dm == item.StoreName); + if (store == null) + { + // 精确匹配(包含关系) + store = allStores.FirstOrDefault(x => x.Dm.Contains(item.StoreName)); + } + if (store == null) + { + // 去除 绿纤 和 店 之后再比 + var cleanExcelName = item.StoreName.Replace("绿纤", "").Replace("店", "").Trim(); + if (!string.IsNullOrEmpty(cleanExcelName)) + { + store = allStores.FirstOrDefault(x => + x.Dm.Replace("绿纤", "").Replace("店", "").Trim() == cleanExcelName); + } + } + if (store == null) + { + // 反向匹配:Excel名称可能是门店名称的一部分(如"紫荆"匹配"绿纤紫荆店") + store = allStores.FirstOrDefault(x => + x.Dm.Replace("绿纤", "").Replace("店", "").Trim().Contains(item.StoreName.Trim())); + } + if (store == null) + { + // 最后尝试:Excel名称包含门店名称的一部分(如"静居寺"匹配"绿纤静居寺店") + store = allStores.FirstOrDefault(x => + item.StoreName.Trim().Contains(x.Dm.Replace("绿纤", "").Replace("店", "").Trim()) || + x.Dm.Replace("绿纤", "").Replace("店", "").Trim().Contains(item.StoreName.Trim())); + } + + if (store == null) + { + failCount++; + errorMessages.Add($"门店【{item.StoreName}】未找到匹配的门店。请确保 Excel 中的门店名称在系统中存在(系统示例:{allStores.FirstOrDefault()?.Dm ?? "无"})"); + continue; + } + + var entity = new LqAnnualSummaryEntity + { + StoreId = store.Id, + StoreName = store.Dm, + Year = item.Year, + Month = item.Month, + TotalPerformance = item.TotalPerformance, + TotalConsume = item.TotalConsume, + HeadCount = item.HeadCount, + PersonTime = item.PersonTime, + ProjectCount = item.ProjectCount, + IsEffective = 1, + CreateTime = DateTime.Now, + CreateUser = "Import" + }; + + // 处理事业部信息 + // 1. 优先从Excel中读取的事业部名称匹配组织表(只匹配事业部,不匹配科技部) + if (!string.IsNullOrEmpty(item.BusinessUnitName)) + { + // 先尝试精确匹配 + var org = allOrgs.FirstOrDefault(x => x.FullName == item.BusinessUnitName); + + // 如果精确匹配失败,尝试模糊匹配,但只匹配包含"事业"的组织(排除科技部) + if (org == null) + { + org = allOrgs.FirstOrDefault(x => + x.FullName.Contains("事业") && // 确保是事业部 + (x.FullName.Contains(item.BusinessUnitName) || + item.BusinessUnitName.Contains(x.FullName))); + } + + if (org != null) + { + // 验证匹配到的组织确实是事业部(不是科技部) + if (org.FullName.Contains("事业") && !org.FullName.Contains("科技")) + { + entity.BusinessUnitId = org.Id; + entity.BusinessUnitName = org.FullName; + } + else + { + // 如果匹配到的是科技部,记录错误,从门店表获取事业部 + errorMessages.Add($"门店【{item.StoreName}】的事业部【{item.BusinessUnitName}】匹配到了科技部,已从门店表获取正确的事业部信息。"); + // 从门店表获取事业部 + if (!string.IsNullOrEmpty(store.Syb)) + { + // 设置事业部ID + entity.BusinessUnitId = store.Syb; + + var sybOrg = allOrgs.FirstOrDefault(x => x.Id == store.Syb); + if (sybOrg != null) + { + // 设置事业部名称 + entity.BusinessUnitName = sybOrg.FullName; + } + else + { + // 如果找不到组织,记录警告 + errorMessages.Add($"门店【{item.StoreName}】的事业部ID【{store.Syb}】未找到对应的组织名称。"); + entity.BusinessUnitName = null; + } + } + else + { + // 如果门店表中也没有事业部信息 + entity.BusinessUnitId = null; + entity.BusinessUnitName = null; + } + } + } + else + { + // 如果找不到匹配的组织,从门店表获取事业部 + errorMessages.Add($"门店【{item.StoreName}】的事业部【{item.BusinessUnitName}】未找到匹配的组织,已从门店表获取事业部信息。"); + if (!string.IsNullOrEmpty(store.Syb)) + { + // 设置事业部ID + entity.BusinessUnitId = store.Syb; + + var sybOrg = allOrgs.FirstOrDefault(x => x.Id == store.Syb); + if (sybOrg != null) + { + // 设置事业部名称 + entity.BusinessUnitName = sybOrg.FullName; + } + else + { + // 如果找不到组织,记录警告 + errorMessages.Add($"门店【{item.StoreName}】的事业部ID【{store.Syb}】未找到对应的组织名称。"); + entity.BusinessUnitName = null; + } + } + else + { + // 如果门店表中也没有事业部信息 + entity.BusinessUnitId = null; + entity.BusinessUnitName = null; + } + } + } + else + { + // 2. 如果Excel中没有事业部信息,从门店信息中获取 Syb (事业部) + // 注意:只使用Syb,不使用Kjb(科技部) + string buId = store.Syb; + + if (!string.IsNullOrEmpty(buId)) + { + // 设置事业部ID + entity.BusinessUnitId = buId; + + // 从组织表获取事业部名称 + var org = allOrgs.FirstOrDefault(x => x.Id == buId); + if (org != null) + { + // 设置事业部名称 + entity.BusinessUnitName = org.FullName; + } + else + { + // 如果找不到组织,记录警告,但不设置名称(保持为空) + errorMessages.Add($"门店【{item.StoreName}】的事业部ID【{buId}】未找到对应的组织名称。"); + entity.BusinessUnitName = null; // 明确设置为null,而不是ID + } + } + else + { + // 如果门店表中也没有事业部信息,记录警告 + errorMessages.Add($"门店【{item.StoreName}】没有事业部信息,请检查门店配置。"); + // 明确设置为null + entity.BusinessUnitId = null; + entity.BusinessUnitName = null; + } + } + + entities.Add(entity); + } + catch (Exception ex) + { + failCount++; + errorMessages.Add($"处理门店【{item.StoreName}】时出错:{ex.Message}"); + continue; + } + } + + // 批量处理:覆盖逻辑 + // 先删除已存在的 (StoreId + Year + Month) + foreach (var group in entities.GroupBy(x => new { x.StoreId, x.Year, x.Month })) + { + // 删除旧数据 + await _db.Deleteable() + .Where(x => x.StoreId == group.Key.StoreId && x.Year == group.Key.Year && x.Month == group.Key.Month) + .ExecuteCommandAsync(); + + // 插入新数据 (取Excel中最后一条,防止Excel自身重复) + var toInsert = group.Last(); + toInsert.Id = YitIdHelper.NextId().ToString(); + toInsert.CreateTime = DateTime.Now; + toInsert.CreateUser = "Import"; + await _db.Insertable(toInsert).ExecuteCommandAsync(); + successCount++; + } + + // 返回导入结果 + var resultMessage = $"导入完成:成功 {successCount} 条"; + if (failCount > 0 || errorMessages.Any()) + { + resultMessage += $",失败 {failCount} 条"; + if (errorMessages.Any()) + { + resultMessage += $"\n错误详情:\n{string.Join("\n", errorMessages)}"; + } + } + + // 如果有错误但部分成功,返回警告信息(通过异常返回,但包含成功信息) + if (successCount > 0 && (failCount > 0 || errorMessages.Any())) + { + // 部分成功,抛出包含成功和失败信息的异常 + throw new Exception(resultMessage); + } + else if (successCount == 0) + { + // 全部失败,抛出异常 + throw new Exception(resultMessage); + } + } + finally + { + // 删除临时文件 + if (File.Exists(tempFilePath)) + { + File.Delete(tempFilePath); + } + } + } + + #endregion + + #region 统计报表 + + /// + /// 4.1 全年门店业绩表 + /// + [HttpPost("GetTotalPerformanceStat")] + public async Task GetTotalPerformanceStat([FromBody] AnnualSummaryQueryInput input) + { + return await GetMonthlyStat(input, x => x.TotalPerformance); + } + + /// + /// 4.2 全年门店消耗表 + /// + [HttpPost("GetTotalConsumeStat")] + public async Task GetTotalConsumeStat([FromBody] AnnualSummaryQueryInput input) + { + return await GetMonthlyStat(input, x => x.TotalConsume); + } + + /// + /// 4.3 年度门店人头表 + /// + [HttpPost("GetHeadCountStat")] + public async Task GetHeadCountStat([FromBody] AnnualSummaryQueryInput input) + { + return await GetMonthlyStat(input, x => x.HeadCount); + } + + /// + /// 4.4 年度门店项目数表 + /// + [HttpPost("GetProjectCountStat")] + public async Task GetProjectCountStat([FromBody] AnnualSummaryQueryInput input) + { + return await GetMonthlyStat(input, x => x.ProjectCount); + } + + /// + /// 4.5 年度门店人次表 + /// + [HttpGet("GetPersonTimeStat")] + public async Task GetPersonTimeStat([FromQuery] AnnualSummaryQueryInput input) + { + return await GetMonthlyStat(input, x => x.PersonTime); + } + + /// + /// 通用月度趋势统计 + /// + [HttpGet("GetMonthlyTrend")] + public async Task GetMonthlyTrend([FromQuery] AnnualSummaryQueryInput input, [FromQuery] string type) + { + // 确保 type 参数正确绑定 + if (string.IsNullOrEmpty(type)) + { + type = "totalperformance"; // 默认值 + } + + // 根据 type 参数选择正确的字段选择器 + Func fieldSelector; + switch (type.ToLower()) + { + case "totalperformance": + fieldSelector = x => x.TotalPerformance; + break; + case "totalconsume": + fieldSelector = x => x.TotalConsume; + break; + case "headcount": + fieldSelector = x => x.HeadCount; + break; + case "persontime": + fieldSelector = x => x.PersonTime; + break; + case "projectcount": + fieldSelector = x => x.ProjectCount; + break; + default: + fieldSelector = x => x.TotalPerformance; + break; + } + + return await GetMonthlyStat(input, fieldSelector); + } + + /// + /// 4.6 门店五项指标统计图 + /// + [HttpPost("GetStoreIndicatorsStat")] + public async Task GetStoreIndicatorsStat([FromBody] AnnualSummaryQueryInput input) + { + return await GetIndicatorStat(input, x => x.TotalPerformance); + } + + /// + /// 获取门店指标详情 + /// + /// 查询参数 + /// 指标类型 + /// 指标统计输出 + [HttpGet("GetStoreIndicatorDetail")] + public async Task GetStoreIndicatorDetail([FromQuery] AnnualSummaryQueryInput input, string type) + { + switch (type?.ToLower()) + { + case "totalperformance": return await GetIndicatorStat(input, x => x.TotalPerformance); + case "totalconsume": return await GetIndicatorStat(input, x => x.TotalConsume); + case "headcount": return await GetIndicatorStat(input, x => x.HeadCount); + case "persontime": return await GetIndicatorStat(input, x => x.PersonTime); + case "projectcount": return await GetIndicatorStat(input, x => x.ProjectCount); + default: return await GetIndicatorStat(input, x => x.TotalPerformance); + } + } + + /// + /// 4.7 事业部五项指标总计图 + /// + [HttpGet("GetBusinessUnitIndicatorsStat")] + public async Task GetBusinessUnitIndicatorsStat([FromQuery] AnnualSummaryQueryInput input, string type) + { + // 类似 4.6,但是按 BusinessUnitName 分组 + switch (type?.ToLower()) + { + case "totalperformance": return await GetBuIndicatorStat(input, x => x.TotalPerformance); + case "totalconsume": return await GetBuIndicatorStat(input, x => x.TotalConsume); + case "headcount": return await GetBuIndicatorStat(input, x => x.HeadCount); + case "persontime": return await GetBuIndicatorStat(input, x => x.PersonTime); + case "projectcount": return await GetBuIndicatorStat(input, x => x.ProjectCount); + default: return await GetBuIndicatorStat(input, x => x.TotalPerformance); + } + } + + /// + /// 4.8 事业部内部汇总 (宽表) + /// + [HttpPost("GetBusinessUnitSummaryStat")] + public async Task GetBusinessUnitSummaryStat([FromBody] AnnualSummaryQueryInput input) + { + int year = input.Year ?? DateTime.Now.Year; + + // 获取事业部名称映射 + var buNameMap = await GetBusinessUnitNameMapAsync(); + + var currentData = await _db.Queryable() + .Where(x => x.IsEffective == 1 && x.Year == year) + .WhereIF(!string.IsNullOrEmpty(input.StoreName), x => x.StoreName.Contains(input.StoreName)) + .ToListAsync(); + + var lastYearData = await _db.Queryable() + .Where(x => x.IsEffective == 1 && x.Year == year - 1) + .WhereIF(!string.IsNullOrEmpty(input.StoreName), x => x.StoreName.Contains(input.StoreName)) + .ToListAsync(); + + // 聚合数据:按 (BusinessUnitName, StoreName) - 其实就是按 StoreName,因为 Store 属于 BU + // 先获取所有涉及的门店 + var allStores = currentData.Select(x => new { x.StoreId, x.StoreName, x.BusinessUnitName, x.BusinessUnitId }) + .Union(lastYearData.Select(x => new { x.StoreId, x.StoreName, x.BusinessUnitName, x.BusinessUnitId })) + .Distinct() + .OrderBy(x => x.BusinessUnitName) + .ThenBy(x => x.StoreName) + .ToList(); + + var output = new BusinessUnitSummaryOutput(); + + foreach (var store in allStores) + { + // Current Year Sums + var curr = currentData.Where(x => x.StoreId == store.StoreId).ToList(); + var last = lastYearData.Where(x => x.StoreId == store.StoreId).ToList(); + + // 优先使用BusinessUnitId,如果没有则使用BusinessUnitName + var buId = store.BusinessUnitId ?? store.BusinessUnitName; + var buDisplayName = GetBusinessUnitDisplayName(buId, buNameMap); + + var row = new BusinessUnitSummaryRow + { + BusinessUnitName = buDisplayName, + StoreName = store.StoreName, + + CurrentYearPerformance = curr.Sum(x => x.TotalPerformance), + LastYearPerformance = last.Sum(x => x.TotalPerformance), + + CurrentYearConsume = curr.Sum(x => x.TotalConsume), + LastYearConsume = last.Sum(x => x.TotalConsume), + + CurrentYearHeadCount = curr.Sum(x => x.HeadCount), + LastYearHeadCount = last.Sum(x => x.HeadCount), + + CurrentYearPersonTime = curr.Sum(x => x.PersonTime), + LastYearPersonTime = last.Sum(x => x.PersonTime), + + CurrentYearProjectCount = curr.Sum(x => x.ProjectCount), + LastYearProjectCount = last.Sum(x => x.ProjectCount), + }; + + row.PerformanceGrowthRate = CalculateGrowthRate(row.CurrentYearPerformance, row.LastYearPerformance); + row.ConsumeGrowthRate = CalculateGrowthRate(row.CurrentYearConsume, row.LastYearConsume); + row.HeadCountGrowthRate = CalculateGrowthRate(row.CurrentYearHeadCount, row.LastYearHeadCount); + row.PersonTimeGrowthRate = CalculateGrowthRate(row.CurrentYearPersonTime, row.LastYearPersonTime); + row.ProjectCountGrowthRate = CalculateGrowthRate(row.CurrentYearProjectCount, row.LastYearProjectCount); + + output.Rows.Add(row); + } + + return new + { + list = output.Rows.Select(x => new + { + businessUnitName = x.BusinessUnitName, + storeName = x.StoreName, + currentPerformance = x.CurrentYearPerformance, + lastPerformance = x.LastYearPerformance, + performanceGrowthRate = x.PerformanceGrowthRate, + currentConsume = x.CurrentYearConsume, + lastConsume = x.LastYearConsume, + consumeGrowthRate = x.ConsumeGrowthRate, + currentHeadCount = x.CurrentYearHeadCount, + lastHeadCount = x.LastYearHeadCount, + headCountGrowthRate = x.HeadCountGrowthRate, + currentPersonTime = x.CurrentYearPersonTime, + lastPersonTime = x.LastYearPersonTime, + personTimeGrowthRate = x.PersonTimeGrowthRate, + currentProjectCount = x.CurrentYearProjectCount, + lastProjectCount = x.LastYearProjectCount, + projectCountGrowthRate = x.ProjectCountGrowthRate + }).ToList() + }; + } + + #endregion + + #region 私有辅助方法 + + private async Task GetMonthlyStat(AnnualSummaryQueryInput input, Func fieldSelector) + { + int year = input.Year ?? DateTime.Now.Year; + + // 获取事业部名称映射 + var buNameMap = await GetBusinessUnitNameMapAsync(); + + // 获取本年度数据 + var currentYearData = await _db.Queryable() + .Where(x => x.IsEffective == 1 && x.Year == year) + .WhereIF(!string.IsNullOrEmpty(input.StoreName), x => x.StoreName.Contains(input.StoreName)) + .ToListAsync(); + + // 获取上年度数据 (用于计算同比/增长率) + var lastYearData = await _db.Queryable() + .Where(x => x.IsEffective == 1 && x.Year == year - 1) + .WhereIF(!string.IsNullOrEmpty(input.StoreName), x => x.StoreName.Contains(input.StoreName)) + .ToListAsync(); + + // 整理所有门店 (包括今年有数据和去年有数据的) + var allStores = currentYearData.Select(x => new { x.StoreId, x.StoreName, x.BusinessUnitName, x.BusinessUnitId }) + .Distinct() + .OrderBy(x => x.BusinessUnitName) + .ThenBy(x => x.StoreName) + .ToList(); + + var output = new AnnualMonthlyStatOutput(); + + foreach (var store in allStores) + { + // 优先使用BusinessUnitId,如果没有则使用BusinessUnitName + var buId = store.BusinessUnitId ?? store.BusinessUnitName; + var buDisplayName = GetBusinessUnitDisplayName(buId, buNameMap); + + var row = new MonthlyDataRow + { + BusinessUnitName = buDisplayName, + StoreName = store.StoreName + }; + + var storeCurrData = currentYearData.Where(x => x.StoreId == store.StoreId).ToList(); + var storeLastData = lastYearData.Where(x => x.StoreId == store.StoreId).ToList(); + + // Fill months (Current Year) - 优化:避免重复调用 FirstOrDefault + for (int month = 1; month <= 12; month++) + { + var currMonthData = storeCurrData.FirstOrDefault(x => x.Month == month); + var lastMonthData = storeLastData.FirstOrDefault(x => x.Month == month); + + switch (month) + { + case 1: row.Month1 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth1 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break; + case 2: row.Month2 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth2 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break; + case 3: row.Month3 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth3 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break; + case 4: row.Month4 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth4 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break; + case 5: row.Month5 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth5 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break; + case 6: row.Month6 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth6 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break; + case 7: row.Month7 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth7 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break; + case 8: row.Month8 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth8 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break; + case 9: row.Month9 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth9 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break; + case 10: row.Month10 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth10 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break; + case 11: row.Month11 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth11 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break; + case 12: row.Month12 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth12 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break; + } + } + + row.TotalCurrentYear = storeCurrData.Sum(x => fieldSelector(x)); + row.AvgCurrentYear = storeCurrData.Any() ? row.TotalCurrentYear / 12 : 0; + + row.TotalLastYear = storeLastData.Sum(x => fieldSelector(x)); + row.AvgLastYear = storeLastData.Any() ? row.TotalLastYear / 12 : 0; + + row.GrowthRate = CalculateGrowthRate(row.TotalCurrentYear, row.TotalLastYear); + + output.Rows.Add(row); + } + + return new + { + monthColumns = output.MonthColumns.Select(c => c.Substring(0, 1).ToLower() + c.Substring(1)).ToList(), + rows = output.Rows.Select(x => new + { + businessUnitName = x.BusinessUnitName, + storeName = x.StoreName, + month1 = x.Month1, + month2 = x.Month2, + month3 = x.Month3, + month4 = x.Month4, + month5 = x.Month5, + month6 = x.Month6, + month7 = x.Month7, + month8 = x.Month8, + month9 = x.Month9, + month10 = x.Month10, + month11 = x.Month11, + month12 = x.Month12, + lastMonth1 = x.LastMonth1, + lastMonth2 = x.LastMonth2, + lastMonth3 = x.LastMonth3, + lastMonth4 = x.LastMonth4, + lastMonth5 = x.LastMonth5, + lastMonth6 = x.LastMonth6, + lastMonth7 = x.LastMonth7, + lastMonth8 = x.LastMonth8, + lastMonth9 = x.LastMonth9, + lastMonth10 = x.LastMonth10, + lastMonth11 = x.LastMonth11, + lastMonth12 = x.LastMonth12, + totalCurrentYear = x.TotalCurrentYear, + avgCurrentYear = x.AvgCurrentYear, + totalLastYear = x.TotalLastYear, + avgLastYear = x.AvgLastYear, + growthRate = x.GrowthRate + }).ToList() + }; + } + + private async Task GetIndicatorStat(AnnualSummaryQueryInput input, Func fieldSelector) + { + int year = input.Year ?? DateTime.Now.Year; + + // 获取事业部名称映射 + var buNameMap = await GetBusinessUnitNameMapAsync(); + + var currentData = await _db.Queryable() + .Where(x => x.IsEffective == 1 && x.Year == year) + .WhereIF(!string.IsNullOrEmpty(input.StoreName), x => x.StoreName.Contains(input.StoreName)) + .ToListAsync(); + + var lastData = await _db.Queryable() + .Where(x => x.IsEffective == 1 && x.Year == year - 1) + .WhereIF(!string.IsNullOrEmpty(input.StoreName), x => x.StoreName.Contains(input.StoreName)) + .ToListAsync(); + + // 按门店ID去重,确保每个门店只出现一次 + // 如果同一门店有多个事业部,取第一个(通常应该只有一个) + var allStores = currentData + .GroupBy(x => x.StoreId) + .Select(g => new + { + StoreId = g.Key, + StoreName = g.First().StoreName, + BusinessUnitName = g.First().BusinessUnitName, + BusinessUnitId = g.First().BusinessUnitId + }) + .OrderBy(x => x.BusinessUnitName) + .ThenBy(x => x.StoreName) + .ToList(); + + var output = new IndicatorStatOutput(); + foreach (var store in allStores) + { + // 按门店ID汇总所有月份的数据 + var curVal = currentData.Where(x => x.StoreId == store.StoreId).Sum(x => fieldSelector(x)); + var lastVal = lastData.Where(x => x.StoreId == store.StoreId).Sum(x => fieldSelector(x)); + + // 优先使用BusinessUnitId,如果没有则使用BusinessUnitName + var buId = store.BusinessUnitId ?? store.BusinessUnitName; + var buDisplayName = GetBusinessUnitDisplayName(buId, buNameMap); + + output.Rows.Add(new IndicatorDataRow + { + BusinessUnitName = buDisplayName, + StoreName = store.StoreName, + CurrentYearValue = curVal, + LastYearValue = lastVal, + GrowthRate = CalculateGrowthRate(curVal, lastVal) + }); + } + return new + { + rows = output.Rows.Select(x => new + { + businessUnitName = x.BusinessUnitName, + storeName = x.StoreName, + currentYearValue = x.CurrentYearValue, + lastYearValue = x.LastYearValue, + growthRate = x.GrowthRate + }).ToList() + }; + } + + private async Task GetBuIndicatorStat(AnnualSummaryQueryInput input, Func fieldSelector) + { + int year = input.Year ?? DateTime.Now.Year; + + // 获取所有事业部(事业一部到事业六部) + var allBusinessUnits = await _db.Queryable() + .Where(x => x.Category == "department" && (x.DeleteMark == null || x.DeleteMark != 1)) + .Where(x => x.FullName.Contains("事业") && x.FullName != "事业部") + .OrderBy(x => x.FullName) + .ToListAsync(); + + // 获取本年度数据 + var currentData = await _db.Queryable() + .Where(x => x.IsEffective == 1 && x.Year == year) + .WhereIF(!string.IsNullOrEmpty(input.StoreName), x => x.StoreName.Contains(input.StoreName)) + .ToListAsync(); + + // 获取上年度数据 + var lastData = await _db.Queryable() + .Where(x => x.IsEffective == 1 && x.Year == year - 1) + .WhereIF(!string.IsNullOrEmpty(input.StoreName), x => x.StoreName.Contains(input.StoreName)) + .ToListAsync(); + + // 创建事业部ID到名称的映射 + var buIdToNameMap = allBusinessUnits.ToDictionary(x => x.Id, x => x.FullName); + + // 获取年度汇总数据中有数据的事业部ID(从BusinessUnitId或BusinessUnitName中获取) + var dataBuIds = currentData + .Where(x => !string.IsNullOrEmpty(x.BusinessUnitId)) + .Select(x => x.BusinessUnitId) + .Distinct() + .ToList(); + + // 如果BusinessUnitId为空,尝试从BusinessUnitName中解析(可能是ID) + var dataBuNames = currentData + .Where(x => string.IsNullOrEmpty(x.BusinessUnitId) && !string.IsNullOrEmpty(x.BusinessUnitName)) + .Select(x => x.BusinessUnitName) + .Distinct() + .ToList(); + + // 合并所有有数据的事业部ID + var allDataBuIds = dataBuIds.Union(dataBuNames).Distinct().ToList(); + + var output = new IndicatorStatOutput(); + + // 遍历所有事业部(确保显示所有6个事业部) + foreach (var bu in allBusinessUnits) + { + // 查找该事业部的数据(通过BusinessUnitId或BusinessUnitName匹配) + var curVal = currentData + .Where(x => (x.BusinessUnitId == bu.Id || x.BusinessUnitName == bu.Id || x.BusinessUnitName == bu.FullName)) + .Sum(x => fieldSelector(x)); + + var lastVal = lastData + .Where(x => (x.BusinessUnitId == bu.Id || x.BusinessUnitName == bu.Id || x.BusinessUnitName == bu.FullName)) + .Sum(x => fieldSelector(x)); + + output.Rows.Add(new IndicatorDataRow + { + BusinessUnitName = bu.FullName, // 使用事业部名称而不是ID + StoreName = "汇总", + CurrentYearValue = curVal, + LastYearValue = lastVal, + GrowthRate = CalculateGrowthRate(curVal, lastVal) + }); + } + + return new + { + rows = output.Rows.Select(x => new + { + businessUnitName = x.BusinessUnitName, + storeName = x.StoreName, + currentYearValue = x.CurrentYearValue, + lastYearValue = x.LastYearValue, + growthRate = x.GrowthRate + }).ToList() + }; + } + + private string CalculateGrowthRate(decimal current, decimal last) + { + if (last == 0) return current > 0 ? "100%" : "0%"; + var rate = (current - last) / last; + return (rate * 100).ToString("F2") + "%"; + } + + /// + /// 获取事业部ID到名称的映射 + /// + private async Task> GetBusinessUnitNameMapAsync() + { + var orgs = await _db.Queryable() + .Where(x => x.Category == "department" && (x.DeleteMark == null || x.DeleteMark != 1)) + .Select(x => new { x.Id, x.FullName }) + .ToListAsync(); + + var map = new Dictionary(); + foreach (var org in orgs) + { + map[org.Id] = org.FullName; + // 如果名称本身也是ID格式,也添加映射 + if (!map.ContainsKey(org.FullName)) + { + map[org.FullName] = org.FullName; + } + } + return map; + } + + /// + /// 将事业部ID或名称转换为显示名称 + /// + private string GetBusinessUnitDisplayName(string buIdOrName, Dictionary buNameMap) + { + if (string.IsNullOrEmpty(buIdOrName)) + return "未知"; + + // 如果映射中存在,返回名称 + if (buNameMap.ContainsKey(buIdOrName)) + return buNameMap[buIdOrName]; + + // 如果本身就是名称(包含"事业"或"科技"等),直接返回 + if (buIdOrName.Contains("事业") || buIdOrName.Contains("科技") || buIdOrName.Contains("教育") || buIdOrName.Contains("大项目")) + return buIdOrName; + + // 否则返回原值(可能是ID) + return buIdOrName; + } + + /// + /// 获取Excel单元格的值(支持公式计算值) + /// + /// 单元格 + /// 单元格的值(字符串形式) + private string GetCellValue(ICell cell) + { + if (cell == null) return string.Empty; + + try + { + // 如果是公式类型,先尝试获取计算后的值 + if (cell.CellType == CellType.Formula) + { + try + { + var formulaEvaluator = cell.Sheet.Workbook.GetCreationHelper().CreateFormulaEvaluator(); + var cellValue = formulaEvaluator.Evaluate(cell); + + // 根据计算结果类型返回相应的值 + switch (cellValue.CellType) + { + case CellType.String: + return cellValue.StringValue?.Trim() ?? string.Empty; + case CellType.Numeric: + if (DateUtil.IsCellDateFormatted(cell)) + { + return cellValue.NumberValue.ToString("yyyy/MM/dd"); + } + var numValue = cellValue.NumberValue; + if (numValue == Math.Floor(numValue)) + { + return ((long)numValue).ToString(); + } + return numValue.ToString(); + case CellType.Boolean: + return cellValue.BooleanValue.ToString(); + case CellType.Error: + // 公式计算错误,返回空字符串 + return string.Empty; + default: + return string.Empty; + } + } + catch + { + // 如果公式计算失败,尝试读取公式文本 + // 如果包含公式特征,返回空字符串(会被后续逻辑跳过) + var formulaText = cell.CellFormula; + if (!string.IsNullOrEmpty(formulaText) && + (formulaText.Contains("XLOOKUP") || formulaText.Contains("VLOOKUP") || + formulaText.Contains("_xlfn.") || formulaText.StartsWith("="))) + { + return string.Empty; + } + return string.Empty; + } + } + + // 非公式类型,直接读取值 + switch (cell.CellType) + { + case CellType.String: + return cell.StringCellValue?.Trim() ?? string.Empty; + + case CellType.Numeric: + if (DateUtil.IsCellDateFormatted(cell)) + { + return cell.DateCellValue.ToString("yyyy/MM/dd"); + } + // 处理数字,避免科学计数法 + var numericValue = cell.NumericCellValue; + if (numericValue == Math.Floor(numericValue)) + { + return ((long)numericValue).ToString(); + } + return numericValue.ToString(); + + case CellType.Boolean: + return cell.BooleanCellValue.ToString(); + + case CellType.Blank: + return string.Empty; + + default: + return cell.ToString()?.Trim() ?? string.Empty; + } + } + catch + { + return string.Empty; + } + } + + #endregion + } +} diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqAssistantSalaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqAssistantSalaryService.cs index a586b3c..86bd13d 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqAssistantSalaryService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqAssistantSalaryService.cs @@ -334,9 +334,28 @@ namespace NCC.Extend decimal performanceRatio = salary.StoreTotalPerformance / salary.StoreLifeline; // 根据岗位类型确定提成比例 - // 店助和店助主任使用相同的阶梯提成规则 - // 先计算总提成金额(阶梯计算) - decimal totalCommission = CalculateAssistantCommission(salary.StoreTotalPerformance, salary.StoreLifeline); + // 店助和店助主任使用相同的分段提成规则,但100%以上部分比例不同 + decimal totalCommission; + if (isDirector) + { + // 店助主任提成规则(分段式): + // 1. 前提条件:门店业绩必须达到门店生命线的70%,否则无提成 + // 2. 70% ≤ 业绩 < 100%:整个业绩按0.4% + // 3. 业绩 ≥ 100%:分段式 + // - 0-100%部分(整个生命线):0.4% + // - 100%以上部分:1.6%(与店助的0.6%不同) + totalCommission = CalculateDirectorCommission(salary.StoreTotalPerformance, salary.StoreLifeline); + } + else + { + // 店助提成规则(分段式): + // 1. 前提条件:门店业绩必须达到门店生命线的70%,否则无提成 + // 2. 70% ≤ 业绩 < 100%:整个业绩按0.4% + // 3. 业绩 ≥ 100%:分段式 + // - 0-100%部分(整个生命线):0.4% + // - 100%以上部分:0.6% + totalCommission = CalculateAssistantCommission(salary.StoreTotalPerformance, salary.StoreLifeline); + } // 计算平均提成比例(用于显示) if (salary.StoreTotalPerformance > 0) @@ -436,8 +455,16 @@ namespace NCC.Extend // 阶段奖励 = 门店总奖励 / 当月天数 × 在店天数 if (daysInMonth > 0 && workingDays > 0) { - // 先计算门店总提成(阶梯计算)- 店助和店助主任使用相同的规则 - decimal storeTotalCommission = CalculateAssistantCommission(salary.StoreTotalPerformance, salary.StoreLifeline); + // 先计算门店总提成(阶梯计算)- 根据岗位类型使用不同规则 + decimal storeTotalCommission; + if (isDirector) + { + storeTotalCommission = CalculateDirectorCommission(salary.StoreTotalPerformance, salary.StoreLifeline); + } + else + { + storeTotalCommission = CalculateAssistantCommission(salary.StoreTotalPerformance, salary.StoreLifeline); + } // 按比例计算提成:门店总提成 / 当月天数 × 在店天数 salary.CommissionAmount = storeTotalCommission / daysInMonth * workingDays; @@ -573,27 +600,24 @@ namespace NCC.Extend } /// - /// 计算底薪(店助) - /// - /// 门店分类(1=A类,2=B类,3=C类) - /// 底薪金额 - private decimal CalculateBaseSalary(int storeCategory) - { - return storeCategory switch - { - 1 => 3000m, // A类门店 - 2 => 3100m, // B类门店 - 3 => 3200m, // C类门店 - _ => throw new Exception($"门店分类值无效:{storeCategory},有效值为1(A类)、2(B类)、3(C类)") - }; - } - - /// - /// 计算店助主任提成(阶梯提成模式) + /// 计算店助主任提成(分段提成模式) /// /// 门店业绩 /// 门店生命线 /// 提成金额 + /// + /// 店助主任提成规则(分段式,与店助相同,但100%以上部分比例不同): + /// 1. 前提条件:门店业绩必须达到门店生命线的70%,否则无提成 + /// 2. 70% ≤ 业绩 < 100%:整个业绩按0.4%计算 + /// 3. 业绩 ≥ 100%:分段式提成 + /// - 0-100%部分(整个生命线):按0.4%计算 + /// - 100%以上部分:按1.6%计算(与店助的0.6%不同) + /// + /// 计算公式: + /// - 如果业绩 < 70%:提成 = 0 + /// - 如果 70% ≤ 业绩 < 100%:提成 = 业绩 × 0.4% + /// - 如果业绩 ≥ 100%:提成 = 生命线 × 0.4% + (业绩 - 生命线) × 1.6% + /// private decimal CalculateDirectorCommission(decimal storePerformance, decimal storeLifeline) { if (storeLifeline <= 0) @@ -603,37 +627,46 @@ namespace NCC.Extend decimal ratio = storePerformance / storeLifeline; + // 前提条件:必须达到70%才有提成 if (ratio < 0.7m) { - // 门店业绩 < 门店生命线 × 70% → 0% + // 门店业绩 < 门店生命线 × 70% → 0%(无提成) return 0; } else if (ratio < 1.0m) { - // 门店生命线 × 70% ≤ 门店业绩 < 门店生命线 × 100% → 0.4%(阶梯) - // 70%以下部分:0% - // 70%-100%部分:0.4% - decimal stage70 = storeLifeline * 0.7m; - decimal performanceInRange = storePerformance - stage70; - return performanceInRange * 0.004m; + // 门店生命线 × 70% ≤ 门店业绩 < 门店生命线 × 100% → 整个业绩按0.4%计算 + return storePerformance * 0.004m; } else { - // 门店业绩 ≥ 门店生命线 × 100% → 阶梯提成 - // ≤生命线部分(整个生命线):0.6% - // >生命线部分:1% - decimal stage100 = storeLifeline; - decimal performanceUpToLifeline = stage100; // ≤生命线部分(整个生命线) - decimal performanceAbove100 = storePerformance - stage100; // >生命线部分 - - // ≤生命线部分:0.6% - decimal commissionUpToLifeline = performanceUpToLifeline * 0.006m; - // >生命线部分:1% - decimal commissionAbove100 = performanceAbove100 * 0.01m; // 1% - - return commissionUpToLifeline + commissionAbove100; + // 门店业绩 ≥ 门店生命线 × 100% → 分段式提成 + // 0-100%部分(整个生命线):按0.4%计算 + // 100%以上部分:按1.6%计算(店助主任与店助的区别) + decimal commissionBelow100 = storeLifeline * 0.004m; // 0-100%部分(整个生命线)按0.4% + decimal commissionAbove100 = (storePerformance - storeLifeline) * 0.016m; // 100%以上部分按1.6% + + return commissionBelow100 + commissionAbove100; } } + + /// + /// 计算底薪(店助) + /// + /// 门店分类(1=A类,2=B类,3=C类) + /// 底薪金额 + private decimal CalculateBaseSalary(int storeCategory) + { + return storeCategory switch + { + 1 => 3000m, // A类门店 + 2 => 3100m, // B类门店 + 3 => 3200m, // C类门店 + _ => throw new Exception($"门店分类值无效:{storeCategory},有效值为1(A类)、2(B类)、3(C类)") + }; + } + } } + diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqBusinessUnitManagerSalaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqBusinessUnitManagerSalaryService.cs index 83a8deb..1dafebc 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqBusinessUnitManagerSalaryService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqBusinessUnitManagerSalaryService.cs @@ -1,17 +1,22 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using NCC.Common.Enum; using NCC.Common.Filter; using NCC.Common.Helper; using NCC.Dependency; using NCC.DynamicApiController; using NCC.Extend.Entitys.Dto.LqBusinessUnitManagerSalary; +using NCC.Extend.Entitys.Enum; using NCC.Extend.Entitys.lq_attendance_summary; +using NCC.Extend.Entitys.lq_cooperation_cost; using NCC.Extend.Entitys.lq_hytk_hytk; using NCC.Extend.Entitys.lq_kd_kdjlb; +using NCC.Extend.Entitys.lq_laundry_flow; using NCC.Extend.Entitys.lq_md_general_manager_lifeline; using NCC.Extend.Entitys.lq_md_target; using NCC.Extend.Entitys.lq_mdxx; using NCC.Extend.Entitys.lq_business_unit_manager_salary_statistics; +using NCC.Extend.Entitys.lq_store_expense; using NCC.System.Entitys.Permission; using SqlSugar; using System; @@ -84,6 +89,12 @@ namespace NCC.Extend ManagerType = x.ManagerType, IsTerminated = x.IsTerminated, StorePerformanceDetail = x.StorePerformanceDetail, + SalesPerformance = x.SalesPerformance, + ProductMaterial = x.ProductMaterial, + CooperationCost = x.CooperationCost, + StoreExpense = x.StoreExpense, + LaundryCost = x.LaundryCost, + GrossProfit = x.GrossProfit, BaseSalary = x.BaseSalary, TotalCommission = x.TotalCommission, WorkingDays = x.WorkingDays, @@ -172,7 +183,7 @@ namespace NCC.Extend .Where(x => !string.IsNullOrEmpty(x.StoreId)) .ToDictionary(x => x.StoreId, x => x.StoreLifeline); - // 1.6 门店总业绩计算 (开单实付 - 退卡金额) + // 1.6 门店销售业绩计算 (开单实付 - 退卡金额) // 开单实付(从lq_kd_kdjlb表统计sfyj字段) var storeBillingList = await _db.Queryable() .Where(x => x.Kdrq >= startDate && x.Kdrq <= endDate.AddDays(1) && x.IsEffective == 1) @@ -193,13 +204,78 @@ namespace NCC.Extend .GroupBy(x => x.Md) .ToDictionary(g => g.Key, g => g.Sum(x => x.ActualRefundAmount ?? x.Tkje ?? 0)); - // 1.7 考勤数据 (lq_attendance_summary) + // 1.7 产品物料统计(仓库领用金额,注意11月特殊规则) + var queryMonth = monthStr; + if (month == 11) + { + // 11月工资算10月数据 + queryMonth = $"{year}10"; + } + var productMaterialSql = $@" + SELECT + F_StoreId as StoreId, + COALESCE(SUM(F_TotalAmount), 0) as MaterialAmount + FROM lq_inventory_usage + WHERE F_IsEffective = 1 + AND DATE_FORMAT(F_UsageTime, '%Y%m') = @queryMonth + GROUP BY F_StoreId"; + + var productMaterialData = await _db.Ado.SqlQueryAsync(productMaterialSql, new { queryMonth }); + var productMaterialDict = productMaterialData + .Where(x => x.StoreId != null) + .ToDictionary(x => x.StoreId.ToString(), x => Convert.ToDecimal(x.MaterialAmount ?? 0)); + + // 1.8 合作项目成本统计 + // Month字段格式为"11"(月份数字),不是"202511"(YYYYMM格式) + var cooperationCostMonth = $"{month:D2}"; // 格式化为"11" + var cooperationCostList = await _db.Queryable() + .Where(x => x.Year == year && x.Month == cooperationCostMonth && x.IsEffective == StatusEnum.有效.GetHashCode()) + .Select(x => new { x.StoreId, x.TotalAmount }) + .ToListAsync(); + var cooperationCostDict = cooperationCostList + .Where(x => !string.IsNullOrEmpty(x.StoreId)) + .GroupBy(x => x.StoreId) + .ToDictionary(g => g.Key, g => g.Sum(x => x.TotalAmount)); + + // 1.9 店内支出统计 + var storeExpenseSql = $@" + SELECT + F_StoreId as StoreId, + COALESCE(SUM(F_Amount), 0) as ExpenseAmount + FROM lq_store_expense + WHERE F_IsEffective = 1 + AND DATE_FORMAT(F_ExpenseDate, '%Y%m') = @monthStr + GROUP BY F_StoreId"; + + var storeExpenseData = await _db.Ado.SqlQueryAsync(storeExpenseSql, new { monthStr }); + var storeExpenseDict = storeExpenseData + .Where(x => x.StoreId != null) + .ToDictionary(x => x.StoreId.ToString(), x => Convert.ToDecimal(x.ExpenseAmount ?? 0)); + + // 1.10 洗毛巾费用统计(只统计送出的记录,F_FlowType = 0) + // 优先使用送出时间(F_SendTime),如果为空则使用创建时间(F_CreateTime) + var laundryCostSql = $@" + SELECT + F_StoreId as StoreId, + COALESCE(SUM(F_TotalPrice), 0) as LaundryAmount + FROM lq_laundry_flow + WHERE F_IsEffective = 1 + AND F_FlowType = 0 + AND DATE_FORMAT(COALESCE(F_SendTime, F_CreateTime), '%Y%m') = @monthStr + GROUP BY F_StoreId"; + + var laundryCostData = await _db.Ado.SqlQueryAsync(laundryCostSql, new { monthStr }); + var laundryCostDict = laundryCostData + .Where(x => x.StoreId != null) + .ToDictionary(x => x.StoreId.ToString(), x => Convert.ToDecimal(x.LaundryAmount ?? 0)); + + // 1.11 考勤数据 (lq_attendance_summary) var attendanceList = await _db.Queryable() .Where(x => x.Year == year && x.Month == month && x.IsEffective == 1) .ToListAsync(); var attendanceDict = attendanceList.ToDictionary(x => x.UserId, x => x); - // 1.8 获取员工信息 (BASE_USER) + // 1.12 获取员工信息 (BASE_USER) var userList = await _db.Queryable() .Where(x => allManagerIds.Contains(x.Id)) .Select(x => new { x.Id, x.RealName, x.Account, x.IsOnJob }) @@ -257,6 +333,12 @@ namespace NCC.Extend // 2.5 遍历该总经理/经理管理的每个门店,计算提成 var storePerformanceDetails = new List(); decimal totalCommission = 0m; + decimal totalSalesPerformance = 0m; + decimal totalProductMaterial = 0m; + decimal totalCooperationCost = 0m; + decimal totalStoreExpense = 0m; + decimal totalLaundryCost = 0m; + decimal totalGrossProfit = 0m; // 获取该总经理/经理管理的门店列表(如果没有管理的门店,则为空列表) var managedStores = managerStoreDict.ContainsKey(managerId) ? managerStoreDict[managerId] : new List(); @@ -277,69 +359,38 @@ namespace NCC.Extend // 获取门店信息 var storeName = storeDict.ContainsKey(storeId) ? storeDict[storeId].Dm ?? "" : ""; - // 获取门店生命线(提成门槛) - if (!storeLifelineDict.ContainsKey(storeId)) - { - // 门店生命线未设置,跳过该门店 - storePerformanceDetails.Add(new StorePerformanceDetail - { - StoreId = storeId, - StoreName = storeName, - StoreLifeline = 0, - BillingPerformance = 0, - RefundPerformance = 0, - StorePerformance = 0, - ReachedLifeline = false, - CommissionAmount = 0, - CalculationDetail = "门店生命线未设置,无法计算提成" - }); - continue; - } - - var storeLifeline = storeLifelineDict[storeId]; - if (storeLifeline <= 0) - { - // 门店生命线未设置或为0,跳过该门店 - storePerformanceDetails.Add(new StorePerformanceDetail - { - StoreId = storeId, - StoreName = storeName, - StoreLifeline = 0, - BillingPerformance = 0, - RefundPerformance = 0, - StorePerformance = 0, - ReachedLifeline = false, - CommissionAmount = 0, - CalculationDetail = "门店生命线未设置或为0,无法计算提成" - }); - continue; - } + // 获取门店生命线(仅用于记录) + var storeLifeline = storeLifelineDict.ContainsKey(storeId) ? storeLifelineDict[storeId] : 0; - // 获取门店业绩 + // 计算销售业绩(开单业绩-退款业绩) var billing = storeBillingDict.ContainsKey(storeId) ? storeBillingDict[storeId] : 0; var refund = storeRefundDict.ContainsKey(storeId) ? storeRefundDict[storeId] : 0; - var storePerformance = billing - refund; + var salesPerformance = billing - refund; - // 判断是否达到门店生命线 - var reachedLifeline = storePerformance >= storeLifeline; + // 统计各项成本 + var productMaterial = productMaterialDict.ContainsKey(storeId) ? productMaterialDict[storeId] : 0; + var cooperationCost = cooperationCostDict.ContainsKey(storeId) ? cooperationCostDict[storeId] : 0; + var storeExpense = storeExpenseDict.ContainsKey(storeId) ? storeExpenseDict[storeId] : 0; + var laundryCost = laundryCostDict.ContainsKey(storeId) ? laundryCostDict[storeId] : 0; - // 计算提成 - decimal commissionAmount = 0m; - string calculationDetail = ""; + // 计算毛利 + // 毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾 + var grossProfit = salesPerformance - productMaterial - cooperationCost - storeExpense - laundryCost; - if (reachedLifeline) - { - // 达到门店生命线,使用提成阶梯计算提成(分段累进) - var commissionResult = CalculateStoreCommission(storePerformance, storeLifelineSetting); - commissionAmount = commissionResult.Amount; - calculationDetail = commissionResult.Detail; + // 累加各项数据 + totalSalesPerformance += salesPerformance; + totalProductMaterial += productMaterial; + totalCooperationCost += cooperationCost; + totalStoreExpense += storeExpense; + totalLaundryCost += laundryCost; + totalGrossProfit += grossProfit; - totalCommission += commissionAmount; - } - else - { - calculationDetail = $"业绩{storePerformance:N2}元,未达到门店生命线{storeLifeline:N2}元,无提成"; - } + // 计算提成(必须满足提成阶梯1才能有提成资格,使用毛利计算) + var commissionResult = CalculateStoreCommission(grossProfit, storeLifelineSetting); + var commissionAmount = commissionResult.Amount; + var calculationDetail = commissionResult.Detail; + + totalCommission += commissionAmount; // 添加到门店业绩明细 storePerformanceDetails.Add(new StorePerformanceDetail @@ -349,8 +400,14 @@ namespace NCC.Extend StoreLifeline = storeLifeline, BillingPerformance = billing, RefundPerformance = refund, - StorePerformance = storePerformance, - ReachedLifeline = reachedLifeline, + SalesPerformance = salesPerformance, + ProductMaterial = productMaterial, + CooperationCost = cooperationCost, + StoreExpense = storeExpense, + LaundryCost = laundryCost, + GrossProfit = grossProfit, + StorePerformance = grossProfit, // 用于提成计算的业绩是毛利 + ReachedLifeline1 = grossProfit >= storeLifelineSetting.Lifeline1, // 是否达到提成阶梯1 Lifeline1 = storeLifelineSetting.Lifeline1, CommissionRate1 = storeLifelineSetting.CommissionRate1, Lifeline2 = storeLifelineSetting.Lifeline2, @@ -365,10 +422,18 @@ namespace NCC.Extend // 2.6 保存门店业绩明细(JSON格式) salary.StorePerformanceDetail = storePerformanceDetails.ToJson(); - // 2.7 提成合计 + // 2.7 保存毛利相关数据 + salary.SalesPerformance = totalSalesPerformance; + salary.ProductMaterial = totalProductMaterial; + salary.CooperationCost = totalCooperationCost; + salary.StoreExpense = totalStoreExpense; + salary.LaundryCost = totalLaundryCost; + salary.GrossProfit = totalGrossProfit; + + // 2.8 提成合计 salary.TotalCommission = totalCommission; - // 2.8 计算应发工资 + // 2.9 计算应发工资 salary.CalculatedGrossSalary = salary.BaseSalary + salary.TotalCommission; salary.FinalGrossSalary = salary.CalculatedGrossSalary; @@ -413,12 +478,25 @@ namespace NCC.Extend } /// - /// 计算门店提成(分段累进) + /// 计算门店提成(分段累进式) /// - /// 门店业绩 + /// 门店毛利 /// 提成阶梯设置 /// 提成金额和计算说明 - private (decimal Amount, string Detail) CalculateStoreCommission(decimal storePerformance, LqMdGeneralManagerLifelineEntity lifelineSetting) + /// + /// 提成规则(分段累进式): + /// 1. 前提条件:必须满足提成阶梯1才能有提成资格 + /// 2. 提成基数:使用毛利计算,而不是开单业绩 + /// 3. 分段累进式计算:不同区间按不同比例分别计算后累加 + /// - 毛利 < 提成阶梯1:无提成 + /// - 提成阶梯1 ≤ 毛利 < 提成阶梯2:提成阶梯1 × 提成比例1 + (毛利 - 提成阶梯1) × 提成比例2 + /// - 提成阶梯2 ≤ 毛利 < 提成阶梯3:提成阶梯1 × 提成比例1 + (提成阶梯2 - 提成阶梯1) × 提成比例2 + (毛利 - 提成阶梯2) × 提成比例3 + /// - 毛利 ≥ 提成阶梯3:提成阶梯1 × 提成比例1 + (提成阶梯2 - 提成阶梯1) × 提成比例2 + (提成阶梯3 - 提成阶梯2) × 提成比例3 + (毛利 - 提成阶梯3) × 提成比例3 + /// + /// 示例:毛利 = 100,000元,提成阶梯1 = 150,000元,提成比例1 = 1% + /// 计算:100,000 < 150,000,未达到提成阶梯1,提成 = 0元 + /// + private (decimal Amount, string Detail) CalculateStoreCommission(decimal grossProfit, LqMdGeneralManagerLifelineEntity lifelineSetting) { // 验证提成阶梯1和提成比例1必须设置 if (lifelineSetting.Lifeline1 <= 0 || lifelineSetting.CommissionRate1 <= 0) @@ -426,6 +504,12 @@ namespace NCC.Extend return (0m, "提成阶梯1或提成比例1未设置,无法计算提成"); } + // 必须满足提成阶梯1才能有提成资格 + if (grossProfit < lifelineSetting.Lifeline1) + { + return (0m, $"毛利{grossProfit:N2}元,< 提成阶梯1({lifelineSetting.Lifeline1:N2}元),未达到提成资格,提成 = 0元"); + } + decimal commissionAmount = 0m; string detail = ""; @@ -436,53 +520,49 @@ namespace NCC.Extend var lifeline3 = lifelineSetting.Lifeline3 ?? 0; var rate3 = lifelineSetting.CommissionRate3 ?? 0; - // 分段累进计算 - if (storePerformance <= lifeline1) - { - // 业绩 ≤ 提成阶梯1 - commissionAmount = storePerformance * (rate1 / 100m); - detail = $"业绩{storePerformance:N2}元,≤ 提成阶梯1({lifeline1:N2}元),提成 = {storePerformance:N2} × {rate1}% = {commissionAmount:N2}元"; - } - else if (lifeline2 > 0 && storePerformance <= lifeline2) + // 分段累进式计算(已通过提成阶梯1检查) + if (lifeline2 > 0 && grossProfit < lifeline2) { - // 提成阶梯1 < 业绩 ≤ 提成阶梯2 + // 提成阶梯1 ≤ 毛利 < 提成阶梯2:分段累进计算 var part1 = lifeline1 * (rate1 / 100m); - var part2 = (storePerformance - lifeline1) * (rate2 / 100m); + var part2 = (grossProfit - lifeline1) * (rate2 / 100m); commissionAmount = part1 + part2; - detail = $"业绩{storePerformance:N2}元,> 提成阶梯1({lifeline1:N2}元) 且 ≤ 提成阶梯2({lifeline2:N2}元),提成 = {lifeline1:N2} × {rate1}% + ({storePerformance:N2} - {lifeline1:N2}) × {rate2}% = {part1:N2} + {part2:N2} = {commissionAmount:N2}元"; + detail = $"毛利{grossProfit:N2}元,≥ 提成阶梯1({lifeline1:N2}元) 且 < 提成阶梯2({lifeline2:N2}元),提成 = {lifeline1:N2} × {rate1}% + ({grossProfit:N2} - {lifeline1:N2}) × {rate2}% = {part1:N2} + {part2:N2} = {commissionAmount:N2}元"; } - else if (lifeline3 > 0 && storePerformance <= lifeline3) + else if (lifeline3 > 0 && grossProfit < lifeline3) { - // 提成阶梯2 < 业绩 ≤ 提成阶梯3 + // 提成阶梯2 ≤ 毛利 < 提成阶梯3:分段累进计算 var part1 = lifeline1 * (rate1 / 100m); var part2 = (lifeline2 - lifeline1) * (rate2 / 100m); - var part3 = (storePerformance - lifeline2) * (rate3 / 100m); + var part3 = (grossProfit - lifeline2) * (rate3 / 100m); commissionAmount = part1 + part2 + part3; - detail = $"业绩{storePerformance:N2}元,> 提成阶梯2({lifeline2:N2}元) 且 ≤ 提成阶梯3({lifeline3:N2}元),提成 = {lifeline1:N2} × {rate1}% + ({lifeline2:N2} - {lifeline1:N2}) × {rate2}% + ({storePerformance:N2} - {lifeline2:N2}) × {rate3}% = {part1:N2} + {part2:N2} + {part3:N2} = {commissionAmount:N2}元"; + detail = $"毛利{grossProfit:N2}元,≥ 提成阶梯2({lifeline2:N2}元) 且 < 提成阶梯3({lifeline3:N2}元),提成 = {lifeline1:N2} × {rate1}% + ({lifeline2:N2} - {lifeline1:N2}) × {rate2}% + ({grossProfit:N2} - {lifeline2:N2}) × {rate3}% = {part1:N2} + {part2:N2} + {part3:N2} = {commissionAmount:N2}元"; } else if (lifeline3 > 0) { - // 业绩 > 提成阶梯3 + // 毛利 ≥ 提成阶梯3:分段累进计算 var part1 = lifeline1 * (rate1 / 100m); var part2 = (lifeline2 - lifeline1) * (rate2 / 100m); var part3 = (lifeline3 - lifeline2) * (rate3 / 100m); - var part4 = (storePerformance - lifeline3) * (rate3 / 100m); + var part4 = (grossProfit - lifeline3) * (rate3 / 100m); commissionAmount = part1 + part2 + part3 + part4; - detail = $"业绩{storePerformance:N2}元,> 提成阶梯3({lifeline3:N2}元),提成 = {lifeline1:N2} × {rate1}% + ({lifeline2:N2} - {lifeline1:N2}) × {rate2}% + ({lifeline3:N2} - {lifeline2:N2}) × {rate3}% + ({storePerformance:N2} - {lifeline3:N2}) × {rate3}% = {part1:N2} + {part2:N2} + {part3:N2} + {part4:N2} = {commissionAmount:N2}元"; + detail = $"毛利{grossProfit:N2}元,≥ 提成阶梯3({lifeline3:N2}元),提成 = {lifeline1:N2} × {rate1}% + ({lifeline2:N2} - {lifeline1:N2}) × {rate2}% + ({lifeline3:N2} - {lifeline2:N2}) × {rate3}% + ({grossProfit:N2} - {lifeline3:N2}) × {rate3}% = {part1:N2} + {part2:N2} + {part3:N2} + {part4:N2} = {commissionAmount:N2}元"; } else if (lifeline2 > 0) { - // 提成阶梯3未设置,业绩 > 提成阶梯2,按提成比例2计算超出部分 + // 提成阶梯3未设置,毛利 ≥ 提成阶梯2:分段累进计算 var part1 = lifeline1 * (rate1 / 100m); - var part2 = (storePerformance - lifeline1) * (rate2 / 100m); + var part2 = (grossProfit - lifeline1) * (rate2 / 100m); commissionAmount = part1 + part2; - detail = $"业绩{storePerformance:N2}元,> 提成阶梯2({lifeline2:N2}元),提成阶梯3未设置,提成 = {lifeline1:N2} × {rate1}% + ({storePerformance:N2} - {lifeline1:N2}) × {rate2}% = {part1:N2} + {part2:N2} = {commissionAmount:N2}元"; + detail = $"毛利{grossProfit:N2}元,≥ 提成阶梯2({lifeline2:N2}元),提成阶梯3未设置,提成 = {lifeline1:N2} × {rate1}% + ({grossProfit:N2} - {lifeline1:N2}) × {rate2}% = {part1:N2} + {part2:N2} = {commissionAmount:N2}元"; } else { - // 只有提成阶梯1,业绩 > 提成阶梯1,按提成比例1计算 - commissionAmount = storePerformance * (rate1 / 100m); - detail = $"业绩{storePerformance:N2}元,> 提成阶梯1({lifeline1:N2}元),提成阶梯2未设置,提成 = {storePerformance:N2} × {rate1}% = {commissionAmount:N2}元"; + // 只有提成阶梯1,毛利 ≥ 提成阶梯1:分段累进计算(提成阶梯1部分 × 提成比例1 + 超出部分 × 提成比例1) + var part1 = lifeline1 * (rate1 / 100m); + var part2 = (grossProfit - lifeline1) * (rate1 / 100m); + commissionAmount = part1 + part2; + detail = $"毛利{grossProfit:N2}元,≥ 提成阶梯1({lifeline1:N2}元),提成阶梯2未设置,提成 = {lifeline1:N2} × {rate1}% + ({grossProfit:N2} - {lifeline1:N2}) × {rate1}% = {part1:N2} + {part2:N2} = {commissionAmount:N2}元"; } return (commissionAmount, detail); @@ -498,8 +578,14 @@ namespace NCC.Extend public decimal StoreLifeline { get; set; } public decimal BillingPerformance { get; set; } public decimal RefundPerformance { get; set; } - public decimal StorePerformance { get; set; } - public bool ReachedLifeline { get; set; } + public decimal SalesPerformance { get; set; } + public decimal ProductMaterial { get; set; } + public decimal CooperationCost { get; set; } + public decimal StoreExpense { get; set; } + public decimal LaundryCost { get; set; } + public decimal GrossProfit { get; set; } + public decimal StorePerformance { get; set; } // 用于提成计算的业绩(等于毛利) + public bool ReachedLifeline1 { get; set; } // 是否达到提成阶梯1 public decimal Lifeline1 { get; set; } public decimal CommissionRate1 { get; set; } public decimal? Lifeline2 { get; set; } diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs index dc9196a..4f07244 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs @@ -3328,6 +3328,17 @@ namespace NCC.Extend.LqKdKdjlb var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); var endTime = input.EndTime ?? DateTime.Now; + // 确保endTime包含当天的结束时间 + if (input.EndTime.HasValue) + { + endTime = input.EndTime.Value.Date.AddHours(23).AddMinutes(59).AddSeconds(59); + } + + // 计算月份(用于匹配金三角设定,使用startTime的月份) + var statisticsMonth = startTime.ToString("yyyyMM"); + + _logger.LogInformation($"健康师统计查询 - StartTime: {startTime:yyyy-MM-dd HH:mm:ss}, EndTime: {endTime:yyyy-MM-dd HH:mm:ss}, Month: {statisticsMonth}"); + // 构建SQL查询 var sql = $@" SELECT @@ -3365,37 +3376,97 @@ namespace NCC.Extend.LqKdKdjlb -- 手工费相关统计 COALESCE(consume_stats.LaborCost, 0) as LaborCost, COALESCE(consume_stats.OriginalLaborCost, 0) as OriginalLaborCost, - COALESCE(consume_stats.OvertimeLaborCost, 0) as OvertimeLaborCost + COALESCE(consume_stats.OvertimeLaborCost, 0) as OvertimeLaborCost, + + -- 金三角名称 + COALESCE(jsj_info.GoldTriangleName, '') as GoldTriangleName, + + -- 队伍业绩占比(该健康师所在金三角的业绩占门店总业绩的比例) + CASE + WHEN store_total_stats.StoreTotalAmount > 0 THEN + CAST(COALESCE(team_performance_stats.TeamPerformance, 0) * 100.0 / store_total_stats.StoreTotalAmount AS DECIMAL(18,2)) + ELSE 0 + END as TeamPerformanceRatio FROM BASE_USER u LEFT JOIN lq_mdxx md ON u.F_MDID = md.F_Id LEFT JOIN base_organize dept ON md.syb = dept.F_Id - - -- 邀约统计子查询 + + -- 金三角信息子查询(获取健康师所在的金三角名称) + LEFT JOIN ( + SELECT + jsjUser.user_id as EmployeeId, + jsj.jsj as GoldTriangleName + FROM lq_jinsanjiao_user jsjUser + INNER JOIN lq_ycsd_jsj jsj ON jsjUser.jsj_id COLLATE utf8mb4_general_ci = jsj.F_Id COLLATE utf8mb4_general_ci + WHERE jsjUser.status = 'ACTIVE' + AND jsjUser.F_DeleteMark = 0 + AND jsj.yf = @statisticsMonth + GROUP BY jsjUser.user_id, jsj.jsj + ) jsj_info ON u.F_Id = jsj_info.EmployeeId + + -- 门店总业绩统计(用于计算占比) + LEFT JOIN ( + SELECT + kd.Djmd as StoreId, + SUM(CAST(kd.sfyj AS DECIMAL(18,2))) as StoreTotalAmount + FROM lq_kd_kdjlb kd + WHERE kd.F_IsEffective = 1 + AND kd.kdrq >= @startTime + AND kd.kdrq <= @endTime + GROUP BY kd.Djmd + ) store_total_stats ON u.F_MDID = store_total_stats.StoreId + + -- 队伍业绩统计(该健康师所在金三角的所有成员业绩总和) + LEFT JOIN ( + SELECT + jsjUser.user_id as EmployeeId, + SUM(CAST(jksyj.jksyj AS DECIMAL(18,2))) as TeamPerformance + FROM lq_jinsanjiao_user jsjUser + INNER JOIN lq_ycsd_jsj jsj ON jsjUser.jsj_id COLLATE utf8mb4_general_ci = jsj.F_Id COLLATE utf8mb4_general_ci + INNER JOIN lq_jinsanjiao_user jsu_team ON jsu_team.jsj_id = jsj.F_Id + AND jsu_team.status = 'ACTIVE' + AND jsu_team.F_DeleteMark = 0 + INNER JOIN lq_kd_jksyj jksyj ON jksyj.jkszh = jsu_team.user_id + INNER JOIN lq_kd_kdjlb kd ON jksyj.glkdbh = kd.F_Id + WHERE jsjUser.status = 'ACTIVE' + AND jsjUser.F_DeleteMark = 0 + AND jsj.yf = @statisticsMonth + AND jksyj.F_IsEffective = 1 + AND kd.F_IsEffective = 1 + AND kd.Djmd = jsj.md + AND jksyj.yjsj >= @startTime + AND jksyj.yjsj <= @endTime + AND kd.kdrq >= @startTime + AND kd.kdrq <= @endTime + GROUP BY jsjUser.user_id + ) team_performance_stats ON u.F_Id = team_performance_stats.EmployeeId + + -- 邀约统计子查询(使用邀约时间yysj筛选,而不是创建时间) LEFT JOIN ( SELECT yyr as EmployeeId, COUNT(DISTINCT yykh) as InviteCount FROM lq_yaoyjl WHERE yyr IS NOT NULL - AND F_CreateTime >= @startTime - AND F_CreateTime <= @endTime + AND yysj >= @startTime + AND yysj <= @endTime GROUP BY yyr ) invite_stats ON u.F_Id = invite_stats.EmployeeId - -- 预约统计子查询 + -- 预约统计子查询(使用预约时间yysj筛选,而不是创建时间) LEFT JOIN ( SELECT yyr as EmployeeId, COUNT(DISTINCT gk) as AppointmentCount FROM lq_yyjl WHERE yyr IS NOT NULL - AND F_CreateTime >= @startTime - AND F_CreateTime <= @endTime + AND yysj >= @startTime + AND yysj <= @endTime GROUP BY yyr ) appointment_stats ON u.F_Id = appointment_stats.EmployeeId - -- 到店统计子查询 + -- 到店统计子查询(使用预约时间yysj筛选,而不是创建时间) LEFT JOIN ( SELECT yyr as EmployeeId, @@ -3403,8 +3474,8 @@ namespace NCC.Extend.LqKdKdjlb FROM lq_yyjl WHERE yyr IS NOT NULL AND F_Status = '已确认' - AND F_CreateTime >= @startTime - AND F_CreateTime <= @endTime + AND yysj >= @startTime + AND yysj <= @endTime GROUP BY yyr ) visit_stats ON u.F_Id = visit_stats.EmployeeId @@ -3554,7 +3625,8 @@ namespace NCC.Extend.LqKdKdjlb var parameters = new List { new SugarParameter("@startTime", startTime), - new SugarParameter("@endTime", endTime) + new SugarParameter("@endTime", endTime), + new SugarParameter("@statisticsMonth", statisticsMonth) }; if (!string.IsNullOrEmpty(input.DepartmentId)) @@ -3582,9 +3654,15 @@ namespace NCC.Extend.LqKdKdjlb sql += " ORDER BY u.F_REALNAME"; + // 记录SQL和参数用于调试 + _logger.LogInformation($"健康师统计SQL执行 - 参数: startTime={startTime:yyyy-MM-dd HH:mm:ss}, endTime={endTime:yyyy-MM-dd HH:mm:ss}"); + // 执行查询 var allData = await _db.Ado.SqlQueryAsync(sql, parameters); + // 记录查询结果数量 + _logger.LogInformation($"健康师统计查询结果 - 总记录数: {allData.Count}, 有到店人数的记录数: {allData.Count(x => x.visitCount > 0)}"); + // 手动分页 var totalCount = allData.Count; var pagedData = allData @@ -3891,12 +3969,32 @@ namespace NCC.Extend.LqKdKdjlb /// "DeductType": "储值", /// "BillingId": "开单ID", /// "ItemName": "品项名称", + /// "StoreId": "门店ID", + /// "StoreIds": ["门店ID1", "门店ID2"], + /// "StartTime": "2025-01-01T00:00:00", + /// "EndTime": "2025-12-31T23:59:59", + /// "ItemCategory": "科美", /// "MinAmount": 100, /// "MaxAmount": 1000 /// } /// ``` /// + /// 参数说明: + /// - StoreId: 门店ID(单个门店筛选) + /// - StoreIds: 门店ID列表(支持多门店筛选) + /// - StartTime: 开始时间(开单时间筛选) + /// - EndTime: 结束时间(开单时间筛选) + /// - ItemCategory: 品项分类(科美、医美、生美、产品等) + /// /// 返回字段说明: + /// - list: 分页数据列表 + /// - pagination: 分页信息 + /// - statistics: 统计信息(针对所有符合条件的数据) + /// - totalCount: 总记录数 + /// - totalAmount: 总金额 + /// - totalProjectNumber: 总项目数 + /// + /// 列表字段说明: /// - Id: 储扣记录ID /// - DeductType: 扣减类型 /// - DeductTypeName: 扣减类型名称 @@ -3920,24 +4018,30 @@ namespace NCC.Extend.LqKdKdjlb { var sidx = string.IsNullOrEmpty(input.sidx) ? "CreateTime" : input.sidx; - // 构建基础查询:储扣信息 JOIN 开单记录 JOIN 客户信息 JOIN 门店信息 + // 构建基础查询:储扣信息 LEFT JOIN 开单记录 LEFT JOIN 客户信息 LEFT JOIN 门店信息 var baseQuery = _db.Queryable( - (deduct, billing, member, store) => - deduct.BillingId == billing.Id && - billing.Kdhy == member.Id && - billing.Djmd == store.Id) + (deduct, billing, member, store) => new JoinQueryInfos( + JoinType.Left, deduct.BillingId == billing.Id, + JoinType.Left, billing.Kdhy == member.Id, + JoinType.Left, billing.Djmd == store.Id + )) + .WhereIF(input.IsEffective.HasValue, (deduct, billing, member, store) => deduct.IsEffective == input.IsEffective.Value) .WhereIF(!string.IsNullOrEmpty(input.DeductType), (deduct, billing, member, store) => deduct.DeductType == input.DeductType) .WhereIF(!string.IsNullOrEmpty(input.DeductId), (deduct, billing, member, store) => deduct.DeductId == input.DeductId) .WhereIF(!string.IsNullOrEmpty(input.BillingId), (deduct, billing, member, store) => deduct.BillingId == input.BillingId) .WhereIF(input.MinAmount.HasValue, (deduct, billing, member, store) => deduct.Amount >= input.MinAmount.Value) .WhereIF(input.MaxAmount.HasValue, (deduct, billing, member, store) => deduct.Amount <= input.MaxAmount.Value) - .WhereIF(input.IsEffective.HasValue, (deduct, billing, member, store) => deduct.IsEffective == input.IsEffective.Value) .WhereIF(!string.IsNullOrEmpty(input.ItemName), (deduct, billing, member, store) => deduct.ItemName != null && deduct.ItemName.Contains(input.ItemName)) .WhereIF(!string.IsNullOrEmpty(input.ItemId), (deduct, billing, member, store) => deduct.ItemId == input.ItemId) .WhereIF(input.MinUnitPrice.HasValue, (deduct, billing, member, store) => deduct.UnitPrice >= input.MinUnitPrice.Value) .WhereIF(input.MaxUnitPrice.HasValue, (deduct, billing, member, store) => deduct.UnitPrice <= input.MaxUnitPrice.Value) .WhereIF(input.StartCreateTime.HasValue, (deduct, billing, member, store) => deduct.CreateTime >= input.StartCreateTime.Value) .WhereIF(input.EndCreateTime.HasValue, (deduct, billing, member, store) => deduct.CreateTime <= input.EndCreateTime.Value) + .WhereIF(!string.IsNullOrEmpty(input.StoreId), (deduct, billing, member, store) => billing.Djmd == input.StoreId) + .WhereIF(input.StoreIds != null && input.StoreIds.Any(), (deduct, billing, member, store) => input.StoreIds.Contains(billing.Djmd)) + .WhereIF(input.StartTime.HasValue, (deduct, billing, member, store) => (deduct.BillingTime ?? billing.Kdrq) >= input.StartTime.Value) + .WhereIF(input.EndTime.HasValue, (deduct, billing, member, store) => (deduct.BillingTime ?? billing.Kdrq) <= input.EndTime.Value) + .WhereIF(!string.IsNullOrEmpty(input.ItemCategory), (deduct, billing, member, store) => deduct.ItemCategory == input.ItemCategory) .WhereIF(!string.IsNullOrEmpty(input.keyword), (deduct, billing, member, store) => (deduct.ItemName != null && deduct.ItemName.Contains(input.keyword)) || (member.Khmc != null && member.Khmc.Contains(input.keyword)) || @@ -3961,20 +4065,104 @@ namespace NCC.Extend.LqKdKdjlb CreateTime = deduct.CreateTime, ProjectNumber = deduct.ProjectNumber, ItemCategory = deduct.ItemCategory ?? "", - BillingDate = deduct.BillingTime ?? billing.Kdrq, // 优先使用储扣记录表中的开单时间 + BillingDate = deduct.BillingTime ?? billing.Kdrq, MemberId = billing.Kdhy ?? "", MemberName = member.Khmc ?? "", MemberPhone = member.Sjh ?? "", StoreId = billing.Djmd ?? "", StoreName = store.Dm ?? "", - TimePeriod = deduct.BillingTime ?? billing.Kdrq, // 优先使用储扣记录表中的开单时间 + TimePeriod = deduct.BillingTime ?? billing.Kdrq, BillingType = SqlFunc.Subqueryable() .Where(pxmx => pxmx.Id == deduct.DeductId && pxmx.Px == deduct.ItemId) .Select(pxmx => pxmx.SourceType), CooperationInstitution = billing.Hgjg ?? "" }).MergeTable().OrderBy(sidx + " " + input.sort).ToPagedListAsync(input.currentPage, input.pageSize); - return PageResult.SqlSugarPageResult(data); + // 构建返回结果 + var result = PageResult.SqlSugarPageResult(data); + + // 单独查询统计数据,避免复杂JOIN导致的问题 + try + { + // 先获取符合条件的开单记录ID列表(用于门店、时间、关键词筛选) + var billingIds = new List(); + if (!string.IsNullOrEmpty(input.StoreId) || (input.StoreIds != null && input.StoreIds.Any()) || + input.StartTime.HasValue || input.EndTime.HasValue || !string.IsNullOrEmpty(input.keyword)) + { + var billingQuery = _db.Queryable() + .WhereIF(!string.IsNullOrEmpty(input.StoreId), billing => billing.Djmd == input.StoreId) + .WhereIF(input.StoreIds != null && input.StoreIds.Any(), billing => input.StoreIds.Contains(billing.Djmd)) + .WhereIF(input.StartTime.HasValue, billing => billing.Kdrq >= input.StartTime.Value) + .WhereIF(input.EndTime.HasValue, billing => billing.Kdrq <= input.EndTime.Value) + .WhereIF(!string.IsNullOrEmpty(input.keyword), billing => + billing.Id != null && billing.Id.Contains(input.keyword) || + SqlFunc.Subqueryable() + .Where(member => member.Id == billing.Kdhy) + .Where(member => (member.Khmc != null && member.Khmc.Contains(input.keyword)) || + (member.Sjh != null && member.Sjh.Contains(input.keyword))) + .Any()); + billingIds = await billingQuery.Select(billing => billing.Id).ToListAsync(); + } + + // 构建统计查询(只查询储扣表) + var statisticsQuery = _db.Queryable() + .WhereIF(input.IsEffective.HasValue, deduct => deduct.IsEffective == input.IsEffective.Value) + .WhereIF(!string.IsNullOrEmpty(input.DeductType), deduct => deduct.DeductType == input.DeductType) + .WhereIF(!string.IsNullOrEmpty(input.DeductId), deduct => deduct.DeductId == input.DeductId) + .WhereIF(!string.IsNullOrEmpty(input.BillingId), deduct => deduct.BillingId == input.BillingId) + .WhereIF(billingIds.Any(), deduct => billingIds.Contains(deduct.BillingId)) + .WhereIF(input.MinAmount.HasValue, deduct => deduct.Amount >= input.MinAmount.Value) + .WhereIF(input.MaxAmount.HasValue, deduct => deduct.Amount <= input.MaxAmount.Value) + .WhereIF(!string.IsNullOrEmpty(input.ItemName), deduct => deduct.ItemName != null && deduct.ItemName.Contains(input.ItemName)) + .WhereIF(!string.IsNullOrEmpty(input.ItemId), deduct => deduct.ItemId == input.ItemId) + .WhereIF(input.MinUnitPrice.HasValue, deduct => deduct.UnitPrice >= input.MinUnitPrice.Value) + .WhereIF(input.MaxUnitPrice.HasValue, deduct => deduct.UnitPrice <= input.MaxUnitPrice.Value) + .WhereIF(input.StartCreateTime.HasValue, deduct => deduct.CreateTime >= input.StartCreateTime.Value) + .WhereIF(input.EndCreateTime.HasValue, deduct => deduct.CreateTime <= input.EndCreateTime.Value) + .WhereIF(!string.IsNullOrEmpty(input.ItemCategory), deduct => deduct.ItemCategory == input.ItemCategory) + // 关键词筛选:检查品项名称 + .WhereIF(!string.IsNullOrEmpty(input.keyword) && !billingIds.Any(), deduct => + deduct.ItemName != null && deduct.ItemName.Contains(input.keyword)) + // 时间筛选:如果储扣记录有开单时间,也需要检查 + .WhereIF(input.StartTime.HasValue && !billingIds.Any(), deduct => + deduct.BillingTime.HasValue && deduct.BillingTime >= input.StartTime.Value) + .WhereIF(input.EndTime.HasValue && !billingIds.Any(), deduct => + deduct.BillingTime.HasValue && deduct.BillingTime <= input.EndTime.Value); + + // 统计总记录数 + var totalCount = await statisticsQuery.CountAsync(); + + // 统计总金额和总项目数 + var statisticsList = await statisticsQuery + .Select(deduct => new + { + Amount = deduct.Amount ?? 0m, + ProjectNumber = deduct.ProjectNumber ?? 0m + }) + .ToListAsync(); + + var totalAmount = statisticsList?.Sum(x => x.Amount) ?? 0m; + var totalProjectNumber = statisticsList?.Sum(x => x.ProjectNumber) ?? 0m; + + // 拼接统计信息到返回结果 + return new + { + list = result.list, + pagination = result.pagination, + statistics = new + { + totalCount = totalCount, + totalAmount = totalAmount, + totalProjectNumber = totalProjectNumber + } + }; + } + catch (Exception statEx) + { + _logger.LogError(statEx, $"统计查询失败: {statEx.Message}, StackTrace: {statEx.StackTrace}"); + // 如果统计查询失败,只返回基础查询结果 + return result; + } } catch (Exception ex) { diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs index 841589c..3491f72 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs @@ -162,6 +162,7 @@ namespace NCC.Extend.LqKhxx mainHealthUserName = SqlFunc.Subqueryable().Where(u => u.Id == it.MainHealthUser).Select(u => u.RealName), subHealthUserName = SqlFunc.Subqueryable().Where(u => u.Id == it.SubHealthUser).Select(u => u.RealName), tjrName = SqlFunc.Subqueryable().Where(u => u.Id == it.Tjr).Select(u => u.Khmc), + lastConsumeTime = it.LastConsumeTime, isBeautyMember = it.IsBeautyMember, isMedicalMember = it.IsMedicalMember, isTechMember = it.IsTechMember, diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectDirectorSalaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectDirectorSalaryService.cs index 3caeb68..d6e74df 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectDirectorSalaryService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectDirectorSalaryService.cs @@ -372,10 +372,27 @@ namespace NCC.Extend } /// - /// 计算提成(分段方式) + /// 计算提成(分段累进式) /// /// 总业绩 /// 提成金额和比例 + /// + /// 提成规则(分段累进式): + /// 1. 前提条件:总业绩必须大于50万才有提成资格 + /// 2. 如果有提成资格后,分段计算: + /// - 0-70万部分:1%(整个0-70万部分都按1%计算) + /// - 70万以上部分:1.5% + /// + /// 计算公式(分段累进): + /// - 如果总业绩 ≤ 50万:提成 = 0(无提成资格) + /// - 如果 50万 < 总业绩 ≤ 70万:提成 = 总业绩 × 1% + /// - 如果总业绩 > 70万:提成 = 70万 × 1% + (总业绩 - 70万) × 1.5% + /// + /// 示例: + /// - 总业绩 = 40万 → 提成 = 0(无提成资格) + /// - 总业绩 = 60万 → 提成 = 60万 × 1% = 6,000元 + /// - 总业绩 = 80万 → 提成 = 70万 × 1% + (80万 - 70万) × 1.5% = 7,000 + 1,500 = 8,500元 + /// private (decimal Amount, decimal? Rate) CalculateCommission(decimal totalPerformance) { if (totalPerformance <= 0) @@ -388,21 +405,27 @@ namespace NCC.Extend if (totalPerformance <= 500000m) { - // ≤ 50万:无提成 + // ≤ 50万:无提成资格 commissionAmount = 0m; rate = null; } else if (totalPerformance <= 700000m) { - // 50万 < 总业绩 ≤ 70万:1%提成 + // 50万 < 总业绩 ≤ 70万:整个业绩按1%计算 commissionAmount = totalPerformance * 0.01m; - rate = 1.00m; + // 计算平均提成比例(用于显示) + rate = 1m; // 1% } else { - // > 70万:1.5%提成 - commissionAmount = totalPerformance * 0.015m; - rate = 1.50m; + // 总业绩 > 70万:分段累进计算 + // 0-70万部分:1% + decimal part1 = 700000m * 0.01m; // 70万 × 1% = 7,000元 + // 70万以上部分:1.5% + decimal part2 = (totalPerformance - 700000m) * 0.015m; + commissionAmount = part1 + part2; + // 计算平均提成比例(用于显示) + rate = commissionAmount > 0 && totalPerformance > 0 ? (commissionAmount / totalPerformance) * 100m : null; } return (commissionAmount, rate); diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs index 62a415c..61fc067 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs @@ -1391,8 +1391,8 @@ namespace NCC.Extend.LqStatistics AND jksyj.F_kdpxid IS NOT NULL AND jksyj.F_kdpxid != '' AND jksyj.F_IsEffective = 1 - AND YEAR(jksyj.yjsj) = @year - AND MONTH(jksyj.yjsj) = @month + AND jksyj.yjsj >= @startDate + AND jksyj.yjsj <= @endDate AND xmzl.fl3 = '合作业绩' GROUP BY jksyj.jkszh ) coop_stats ON order_stats.EmployeeId = coop_stats.EmployeeId @@ -1411,8 +1411,8 @@ namespace NCC.Extend.LqStatistics AND jksyj.F_kdpxid IS NOT NULL AND jksyj.F_kdpxid != '' AND jksyj.F_IsEffective = 1 - AND YEAR(jksyj.yjsj) = @year - AND MONTH(jksyj.yjsj) = @month + AND jksyj.yjsj >= @startDate + AND jksyj.yjsj <= @endDate AND (xmzl.fl3 IS NULL OR xmzl.fl3 != '合作业绩') GROUP BY jksyj.jkszh ) base_stats ON order_stats.EmployeeId = base_stats.EmployeeId @@ -1429,8 +1429,8 @@ namespace NCC.Extend.LqStatistics AND hytk_jksyj.jksyj != '0' AND hytk_jksyj.F_IsEffective = 1 AND hytk.F_IsEffective = 1 - AND YEAR(hytk_jksyj.tksj) = @year - AND MONTH(hytk_jksyj.tksj) = @month + AND hytk_jksyj.tksj >= @startDate + AND hytk_jksyj.tksj <= @endDate GROUP BY hytk_jksyj.jks ) refund_stats ON order_stats.EmployeeId = refund_stats.EmployeeId ORDER BY order_stats.TotalPerformance DESC"; @@ -1560,10 +1560,10 @@ namespace NCC.Extend.LqStatistics } /// - /// 分页查询个人开单业绩统计数据(在用) + /// 分页查询个人开单业绩统计数据(实时查询) /// /// - /// 分页查询个人业绩统计数据,支持多条件筛选 + /// 实时查询个人业绩统计数据,支持多条件筛选,直接从开单记录表统计 /// /// 示例请求: /// ```json @@ -1572,7 +1572,7 @@ namespace NCC.Extend.LqStatistics /// "statisticsMonth": "202401", /// "storeId": "store123", /// "employeeName": "张三", - /// "pageIndex": 1, + /// "currentPage": 1, /// "pageSize": 20 /// } /// ``` @@ -1586,56 +1586,259 @@ namespace NCC.Extend.LqStatistics { try { - var query = _db.Queryable(); + // 验证统计月份必填 + if (string.IsNullOrEmpty(input.StatisticsMonth) || input.StatisticsMonth.Length != 6) + { + throw NCCException.Oh("统计月份不能为空,格式为YYYYMM"); + } - // 添加查询条件 - query = query.WhereIF(!string.IsNullOrEmpty(input.StatisticsMonth), x => x.StatisticsMonth == input.StatisticsMonth); - query = query.WhereIF(!string.IsNullOrEmpty(input.StoreId), x => x.StoreId == input.StoreId); - query = query.WhereIF(!string.IsNullOrEmpty(input.StoreName), x => x.StoreName.Contains(input.StoreName)); - query = query.WhereIF(!string.IsNullOrEmpty(input.EmployeeId), x => x.EmployeeId == input.EmployeeId); - query = query.WhereIF(!string.IsNullOrEmpty(input.EmployeeName), x => x.EmployeeName.Contains(input.EmployeeName)); - query = query.WhereIF(!string.IsNullOrEmpty(input.GoldTriangleId), x => x.GoldTriangleId == input.GoldTriangleId); - query = query.WhereIF(!string.IsNullOrEmpty(input.Position), x => x.Position == input.Position); + var statisticsMonth = input.StatisticsMonth; + var year = int.Parse(statisticsMonth.Substring(0, 4)); + var month = int.Parse(statisticsMonth.Substring(4, 2)); - // 按总业绩降序排序 - query = query.OrderBy(x => x.TotalPerformance, OrderByType.Desc); + // 计算日期范围(使用日期范围查询替代YEAR/MONTH函数,提升性能) + var startDate = new DateTime(year, month, 1); + var endDate = startDate.AddMonths(1).AddDays(-1).Date.AddHours(23).AddMinutes(59).AddSeconds(59); - // 分页查询并映射到DTO - var result = await query.Select(it => new LqStatisticsPersonalPerformanceListOutput + // 构建筛选条件 + var innerWhereConditions = new List(); // 子查询中的筛选条件 + var outerWhereConditions = new List(); // 外层查询的筛选条件 + var parameters = new Dictionary { - Id = it.Id, - StatisticsMonth = it.StatisticsMonth, - StoreId = it.StoreId, - StoreName = it.StoreName, - GoldTriangleId = it.GoldTriangleId, - GoldTriangleName = it.GoldTriangleName, - Position = it.Position, - EmployeeId = it.EmployeeId, - EmployeeName = it.EmployeeName, - TotalPerformance = it.TotalPerformance, - BasePerformance = it.BasePerformance, - CooperationPerformance = it.CooperationPerformance, - OrderCount = it.OrderCount, - FirstOrderCount = it.FirstOrderCount, - UpgradeOrderCount = it.UpgradeOrderCount, - FirstOrderPerformance = it.FirstOrderPerformance, - UpgradeOrderPerformance = it.UpgradeOrderPerformance, - LastOrderDate = it.LastOrderDate, - FirstOrderDate = it.FirstOrderDate, - CreateTime = it.CreateTime, - RefundPerformance = it.RefundPerformance, - RefundCount = it.RefundCount, - ActualPerformance = it.ActualPerformance - }).ToPagedListAsync(input.currentPage, input.pageSize); + { "@statisticsMonth", statisticsMonth }, + { "@startDate", startDate }, + { "@endDate", endDate } + }; + + // 员工ID筛选(在子查询中) + if (!string.IsNullOrEmpty(input.EmployeeId)) + { + innerWhereConditions.Add("jksyj.jkszh = @EmployeeId"); + parameters.Add("@EmployeeId", input.EmployeeId); + } + + // 门店ID筛选(在JOIN后) + if (!string.IsNullOrEmpty(input.StoreId)) + { + outerWhereConditions.Add("u.F_MDID = @StoreId"); + parameters.Add("@StoreId", input.StoreId); + } + + // 门店名称筛选(在JOIN后) + if (!string.IsNullOrEmpty(input.StoreName)) + { + outerWhereConditions.Add("md.dm LIKE @StoreName"); + parameters.Add("@StoreName", $"%{input.StoreName}%"); + } + + // 员工姓名筛选(在JOIN后) + if (!string.IsNullOrEmpty(input.EmployeeName)) + { + outerWhereConditions.Add("u.F_REALNAME LIKE @EmployeeName"); + parameters.Add("@EmployeeName", $"%{input.EmployeeName}%"); + } + + // 金三角ID筛选(在JOIN后) + if (!string.IsNullOrEmpty(input.GoldTriangleId)) + { + outerWhereConditions.Add("jsjUser.F_Id = @GoldTriangleId"); + parameters.Add("@GoldTriangleId", input.GoldTriangleId); + } + + var innerWhereClause = innerWhereConditions.Any() ? " AND " + string.Join(" AND ", innerWhereConditions) : ""; + var outerWhereClause = outerWhereConditions.Any() ? "WHERE " + string.Join(" AND ", outerWhereConditions) : ""; + + // 构建优化的主查询SQL - 合并查询减少扫描次数 + var sql = $@" + SELECT + order_stats.EmployeeId, + order_stats.EmployeeName, + order_stats.StoreId, + order_stats.StoreName, + order_stats.GoldTriangleId, + order_stats.GoldTriangleName, + order_stats.Position, + order_stats.OrderCount, + order_stats.FirstOrderCount, + order_stats.UpgradeOrderCount, + order_stats.FirstOrderPerformance, + order_stats.UpgradeOrderPerformance, + order_stats.LastOrderDate, + order_stats.FirstOrderDate, + COALESCE(coop_stats.CooperationPerformance, 0) AS CooperationPerformance, + COALESCE(order_stats.TotalPerformance, 0) - COALESCE(coop_stats.CooperationPerformance, 0) AS BasePerformance, + COALESCE(refund_stats.RefundPerformance, 0) AS RefundPerformance, + COALESCE(refund_stats.RefundCount, 0) AS RefundCount, + order_stats.TotalPerformance + FROM ( + SELECT + order_base.jkszh AS EmployeeId, + u.F_REALNAME AS EmployeeName, + u.F_MDID AS StoreId, + COALESCE(md.dm, '') AS StoreName, + COALESCE(jsjUser.F_Id, '') AS GoldTriangleId, + COALESCE(jsjUser.jsj, '') AS GoldTriangleName, + CASE + WHEN jsjUser.is_leader = 1 THEN '顾问' + ELSE COALESCE(u.F_GW, '') + END AS Position, + COUNT(*) AS OrderCount, + COUNT(CASE WHEN order_base.sfskdd = '是' THEN 1 END) AS FirstOrderCount, + COUNT(CASE WHEN order_base.sfskdd = '否' THEN 1 END) AS UpgradeOrderCount, + SUM(CASE WHEN order_base.sfskdd = '是' THEN order_base.order_performance ELSE 0 END) AS FirstOrderPerformance, + SUM(CASE WHEN order_base.sfskdd = '否' THEN order_base.order_performance ELSE 0 END) AS UpgradeOrderPerformance, + MAX(order_base.yjsj) AS LastOrderDate, + MIN(order_base.yjsj) AS FirstOrderDate, + SUM(order_base.order_performance) AS TotalPerformance, + 0 AS CooperationPerformance, + 0 AS BasePerformance + FROM ( + -- 基础开单数据汇总(不包含合作业绩分类,提升性能) + SELECT + jksyj.jkszh, + jksyj.glkdbh, + kd.sfskdd, + MAX(jksyj.yjsj) as yjsj, + SUM(CAST(jksyj.jksyj AS DECIMAL(18,2))) as order_performance + FROM lq_kd_jksyj jksyj + INNER JOIN lq_kd_pxmx pxmx ON jksyj.F_kdpxid = pxmx.F_Id AND pxmx.F_IsEffective = 1 + INNER JOIN lq_kd_kdjlb kd ON jksyj.glkdbh = CONVERT(kd.F_Id USING utf8mb4) + WHERE jksyj.yjsj IS NOT NULL + AND jksyj.jksyj IS NOT NULL + AND jksyj.jksyj != '' + AND jksyj.jksyj != '0' + AND jksyj.F_kdpxid IS NOT NULL + AND jksyj.F_kdpxid != '' + AND jksyj.F_IsEffective = 1 + AND jksyj.yjsj >= @startDate + AND jksyj.yjsj <= @endDate + {innerWhereClause} + GROUP BY jksyj.jkszh, jksyj.glkdbh, kd.sfskdd + ) order_base + INNER JOIN BASE_USER u ON order_base.jkszh = u.F_Id + LEFT JOIN lq_mdxx md ON u.F_MDID = md.F_Id + LEFT JOIN ( + SELECT + jsjUser.user_id, + MIN(jsjUser.jsj_id) as F_Id, + MIN(jsj.jsj) as jsj, + MIN(jsjUser.is_leader) as is_leader + FROM lq_jinsanjiao_user jsjUser + INNER JOIN lq_ycsd_jsj jsj ON jsjUser.jsj_id COLLATE utf8mb4_general_ci = jsj.F_Id COLLATE utf8mb4_general_ci AND jsj.yf = @statisticsMonth + WHERE jsjUser.F_Month = @statisticsMonth + AND jsjUser.status = 'ACTIVE' + AND jsjUser.F_DeleteMark = 0 + GROUP BY jsjUser.user_id + ) jsjUser ON order_base.jkszh = jsjUser.user_id + {outerWhereClause} + GROUP BY + order_base.jkszh, + u.F_REALNAME, + u.F_MDID, + md.dm, + jsjUser.F_Id, + jsjUser.jsj, + jsjUser.is_leader, + u.F_GW + ) order_stats + LEFT JOIN ( + -- 合作业绩统计(单独查询,提升性能) + SELECT + jksyj.jkszh AS EmployeeId, + SUM(CAST(jksyj.jksyj AS DECIMAL(18,2))) AS CooperationPerformance + FROM lq_kd_jksyj jksyj + INNER JOIN lq_kd_pxmx pxmx ON jksyj.F_kdpxid = pxmx.F_Id AND pxmx.F_IsEffective = 1 + INNER JOIN lq_xmzl xmzl ON pxmx.px = xmzl.F_Id AND xmzl.fl3 = '合作业绩' + WHERE jksyj.yjsj IS NOT NULL + AND jksyj.jksyj IS NOT NULL + AND jksyj.jksyj != '' + AND jksyj.jksyj != '0' + AND jksyj.F_kdpxid IS NOT NULL + AND jksyj.F_kdpxid != '' + AND jksyj.F_IsEffective = 1 + AND jksyj.yjsj >= @startDate + AND jksyj.yjsj <= @endDate + GROUP BY jksyj.jkszh + ) coop_stats ON order_stats.EmployeeId = coop_stats.EmployeeId + LEFT JOIN ( + -- 退单业绩统计 + SELECT + hytk_jksyj.jkszh AS EmployeeId, + SUM(CAST(hytk_jksyj.jksyj AS DECIMAL(18,2))) AS RefundPerformance, + COUNT(*) AS RefundCount + FROM lq_hytk_jksyj hytk_jksyj + INNER JOIN lq_hytk_hytk hytk ON hytk_jksyj.gltkbh = hytk.F_Id + WHERE hytk_jksyj.jksyj IS NOT NULL + AND hytk_jksyj.jksyj != '' + AND hytk_jksyj.jksyj != '0' + AND hytk_jksyj.F_IsEffective = 1 + AND hytk.F_IsEffective = 1 + AND hytk_jksyj.tksj >= @startDate + AND hytk_jksyj.tksj <= @endDate + GROUP BY hytk_jksyj.jkszh + ) refund_stats ON order_stats.EmployeeId = refund_stats.EmployeeId"; + + // 岗位筛选(需要在最外层,因为岗位是通过CASE计算的) + var finalWhereConditions = new List(); + if (!string.IsNullOrEmpty(input.Position)) + { + finalWhereConditions.Add("order_stats.Position = @Position"); + parameters.Add("@Position", input.Position); + } + + var finalWhereClause = finalWhereConditions.Any() ? " WHERE " + string.Join(" AND ", finalWhereConditions) : ""; + var finalSql = $@"SELECT * FROM ({sql}) AS order_stats{finalWhereClause} ORDER BY order_stats.TotalPerformance DESC"; + + // 查询总数 + var countSql = $"SELECT COUNT(*) FROM ({finalSql}) AS total_count"; + var totalCount = await _db.Ado.GetIntAsync(countSql, parameters); + + // 分页查询 + var pageIndex = input.currentPage > 0 ? input.currentPage : 1; + var pageSize = input.pageSize > 0 ? input.pageSize : 20; + var offset = (pageIndex - 1) * pageSize; + var pagedSql = $"{finalSql} LIMIT {pageSize} OFFSET {offset}"; + + _logger.LogInformation($"执行个人业绩统计实时查询SQL - 月份: {statisticsMonth}, 页码: {pageIndex}, 每页: {pageSize}"); + + var statisticsData = await _db.Ado.SqlQueryAsync(pagedSql, parameters); + + // 映射到输出DTO + var outputList = statisticsData.Select(stats => new LqStatisticsPersonalPerformanceListOutput + { + Id = YitIdHelper.NextId().ToString(), // 实时查询没有ID,生成临时ID + StatisticsMonth = statisticsMonth, + StoreId = stats.StoreId?.ToString() ?? "", + StoreName = stats.StoreName?.ToString() ?? "", + GoldTriangleId = stats.GoldTriangleId?.ToString() ?? "", + GoldTriangleName = stats.GoldTriangleName?.ToString() ?? "", + Position = stats.Position?.ToString() ?? "", + EmployeeId = stats.EmployeeId?.ToString() ?? "", + EmployeeName = stats.EmployeeName?.ToString() ?? "", + TotalPerformance = Convert.ToDecimal(stats.TotalPerformance ?? 0) - Convert.ToDecimal(stats.RefundPerformance ?? 0), + BasePerformance = Convert.ToDecimal(stats.BasePerformance ?? 0), + CooperationPerformance = Convert.ToDecimal(stats.CooperationPerformance ?? 0), + RefundPerformance = Convert.ToDecimal(stats.RefundPerformance ?? 0), + RefundCount = Convert.ToInt32(stats.RefundCount ?? 0), + ActualPerformance = Convert.ToDecimal(stats.TotalPerformance ?? 0) - Convert.ToDecimal(stats.RefundPerformance ?? 0), + OrderCount = Convert.ToInt32(stats.OrderCount ?? 0), + FirstOrderCount = Convert.ToInt32(stats.FirstOrderCount ?? 0), + UpgradeOrderCount = Convert.ToInt32(stats.UpgradeOrderCount ?? 0), + FirstOrderPerformance = Convert.ToDecimal(stats.FirstOrderPerformance ?? 0), + UpgradeOrderPerformance = Convert.ToDecimal(stats.UpgradeOrderPerformance ?? 0), + LastOrderDate = stats.LastOrderDate as DateTime?, + FirstOrderDate = stats.FirstOrderDate as DateTime?, + CreateTime = DateTime.Now + }).ToList(); return new { - list = result.list, + list = outputList, pagination = new { - pageIndex = input.currentPage, - pageSize = input.pageSize, - total = result.pagination.Total + pageIndex = pageIndex, + pageSize = pageSize, + total = totalCount } }; } @@ -3195,44 +3398,156 @@ namespace NCC.Extend.LqStatistics } /// - /// 获取门店总业绩统计列表 + /// 获取门店总业绩统计列表(实时查询) /// + /// + /// 实时查询门店总业绩统计数据,支持多条件筛选,直接从开单记录表统计 + /// + /// 示例请求: + /// ```json + /// POST /api/Extend/LqStatistics/get-store-total-performance-statistics-list + /// { + /// "statisticsMonth": "202401", + /// "storeName": "门店名称", + /// "pageIndex": 1, + /// "pageSize": 20 + /// } + /// ``` + /// /// 查询参数 /// 分页结果 + /// 成功返回分页数据 + /// 参数错误 + /// 服务器错误 [HttpPost("get-store-total-performance-statistics-list")] public async Task GetStoreTotalPerformanceStatisticsList([FromBody] LqStoreTotalPerformanceStatisticsListQueryInput input) { try { - var query = _db.Queryable(); + // 验证统计月份必填 + if (string.IsNullOrEmpty(input.StatisticsMonth) || input.StatisticsMonth.Length != 6) + { + throw NCCException.Oh("统计月份不能为空,格式为YYYYMM"); + } - // 添加查询条件 - query = query.WhereIF(!string.IsNullOrEmpty(input.StatisticsMonth), x => x.StatisticsMonth == input.StatisticsMonth); - query = query.WhereIF(!string.IsNullOrEmpty(input.StoreName), x => x.StoreName.Contains(input.StoreName)); + var statisticsMonth = input.StatisticsMonth; + var year = int.Parse(statisticsMonth.Substring(0, 4)); + var month = int.Parse(statisticsMonth.Substring(4, 2)); - // 按创建时间降序排序 - query = query.OrderBy(x => x.CreateTime, OrderByType.Desc); + // 计算日期范围(使用日期范围查询替代DATE_FORMAT函数,提升性能) + var startDate = new DateTime(year, month, 1); + var endDate = startDate.AddMonths(1).AddDays(-1).Date.AddHours(23).AddMinutes(59).AddSeconds(59); - // 分页查询并映射到DTO - var pagedResult = await query.ToPagedListAsync(input.PageIndex, input.PageSize); + // 构建筛选条件 + var whereConditions = new List(); + var parameters = new Dictionary + { + { "@statisticsMonth", statisticsMonth }, + { "@startDate", startDate }, + { "@endDate", endDate } + }; - var outputList = pagedResult.list.Select(it => new LqStoreTotalPerformanceStatisticsListOutput + // 门店名称筛选 + if (!string.IsNullOrEmpty(input.StoreName)) { - Id = it.Id, - StatisticsMonth = it.StatisticsMonth, - StoreId = it.StoreId, - StoreName = it.StoreName, - TotalPerformance = it.TotalPerformance, - DebtAmount = it.DebtAmount, - TotalOrderPerformance = it.TotalOrderPerformance, - StorageDeductionAmount = it.StorageDeductionAmount, - ItemQuantity = it.ItemQuantity, - FirstOrderCount = it.FirstOrderCount, - UpgradeOrderCount = it.UpgradeOrderCount, - RefundAmount = it.RefundAmount, - RefundCount = it.RefundCount, - CreateTime = it.CreateTime.HasValue ? it.CreateTime.Value : DateTime.Now, - ActualPerformance = it.ActualPerformance + whereConditions.Add("md.dm LIKE @StoreName"); + parameters.Add("@StoreName", $"%{input.StoreName}%"); + } + + var whereClause = whereConditions.Any() ? "WHERE " + string.Join(" AND ", whereConditions) : ""; + + // 构建实时查询SQL - 参考SaveStoreTotalPerformanceStatistics的逻辑 + var sql = $@" + SELECT + store_data.F_StoreId, + store_data.F_StoreName, + @statisticsMonth as F_StatisticsMonth, + store_data.F_TotalPerformance, + store_data.F_DebtAmount, + store_data.F_TotalOrderPerformance, + store_data.F_StorageDeductionAmount, + COALESCE(item_data.F_ItemQuantity, 0) as F_ItemQuantity, + store_data.F_FirstOrderCount, + store_data.F_UpgradeOrderCount, + store_data.F_FirstOrderPerformance, + store_data.F_UpgradeOrderPerformance, + COALESCE(refund_data.F_RefundAmount, 0) as F_RefundAmount, + COALESCE(refund_data.F_RefundCount, 0) as F_RefundCount + FROM ( + SELECT + kd.djmd as F_StoreId, + md.dm as F_StoreName, + COALESCE(SUM(kd.zdyj), 0) as F_TotalPerformance, + COALESCE(SUM(kd.qk), 0) as F_DebtAmount, + COALESCE(SUM(kd.sfyj), 0) as F_TotalOrderPerformance, + COALESCE(SUM(kd.F_DeductAmount), 0) as F_StorageDeductionAmount, + COUNT(DISTINCT CASE WHEN kd.sfskdd = '是' THEN kd.F_Id END) as F_FirstOrderCount, + COUNT(DISTINCT CASE WHEN kd.sfskdd = '否' THEN kd.F_Id END) as F_UpgradeOrderCount, + SUM(CASE WHEN kd.sfskdd = '是' THEN COALESCE(kd.zdyj, 0) ELSE 0 END) as F_FirstOrderPerformance, + SUM(CASE WHEN kd.sfskdd = '否' THEN COALESCE(kd.zdyj, 0) ELSE 0 END) as F_UpgradeOrderPerformance + FROM lq_kd_kdjlb kd + LEFT JOIN lq_mdxx md ON CONVERT(kd.djmd USING utf8mb4) = md.F_Id + WHERE kd.F_IsEffective = 1 + AND kd.kdrq >= @startDate + AND kd.kdrq <= @endDate + GROUP BY kd.djmd, md.dm + ) store_data + LEFT JOIN ( + SELECT + kd.djmd as F_StoreId, + COUNT(pxmx.F_ProjectNumber) as F_ItemQuantity + FROM lq_kd_kdjlb kd + LEFT JOIN lq_kd_pxmx pxmx ON CONVERT(kd.F_Id USING utf8mb4) = pxmx.glkdbh AND pxmx.F_IsEffective = 1 + WHERE kd.F_IsEffective = 1 + AND kd.kdrq >= @startDate + AND kd.kdrq <= @endDate + GROUP BY kd.djmd + ) item_data ON store_data.F_StoreId = item_data.F_StoreId + LEFT JOIN ( + SELECT + hytk.md as F_StoreId, + COALESCE(SUM(hytk.F_ActualRefundAmount), 0) as F_RefundAmount, + COUNT(DISTINCT hytk.F_Id) as F_RefundCount + FROM lq_hytk_hytk hytk + WHERE hytk.F_IsEffective = 1 + AND hytk.tksj >= @startDate + AND hytk.tksj <= @endDate + GROUP BY hytk.md + ) refund_data ON store_data.F_StoreId = refund_data.F_StoreId + {whereClause}"; + + // 查询总数 + var countSql = $"SELECT COUNT(*) FROM ({sql}) AS total_count"; + var totalCount = await _db.Ado.GetIntAsync(countSql, parameters); + + // 分页查询 + var pageIndex = input.PageIndex > 0 ? input.PageIndex : 1; + var pageSize = input.PageSize > 0 ? input.PageSize : 20; + var offset = (pageIndex - 1) * pageSize; + var pagedSql = $"{sql} ORDER BY store_data.F_TotalPerformance DESC LIMIT {pageSize} OFFSET {offset}"; + + _logger.LogInformation($"执行门店总业绩统计实时查询SQL - 月份: {statisticsMonth}, 页码: {pageIndex}, 每页: {pageSize}"); + + var statisticsData = await _db.Ado.SqlQueryAsync(pagedSql, parameters); + + // 映射到输出DTO + var outputList = statisticsData.Select(data => new LqStoreTotalPerformanceStatisticsListOutput + { + Id = YitIdHelper.NextId().ToString(), // 实时查询没有ID,生成临时ID + StatisticsMonth = statisticsMonth, + StoreId = data.F_StoreId?.ToString() ?? "", + StoreName = data.F_StoreName?.ToString() ?? "", + TotalPerformance = Convert.ToDecimal(data.F_TotalPerformance ?? 0), + DebtAmount = Convert.ToDecimal(data.F_DebtAmount ?? 0), + TotalOrderPerformance = Convert.ToDecimal(data.F_TotalOrderPerformance ?? 0), + StorageDeductionAmount = Convert.ToDecimal(data.F_StorageDeductionAmount ?? 0), + ItemQuantity = Convert.ToInt32(data.F_ItemQuantity ?? 0), + FirstOrderCount = Convert.ToInt32(data.F_FirstOrderCount ?? 0), + UpgradeOrderCount = Convert.ToInt32(data.F_UpgradeOrderCount ?? 0), + RefundAmount = Convert.ToDecimal(data.F_RefundAmount ?? 0), + RefundCount = Convert.ToInt32(data.F_RefundCount ?? 0), + ActualPerformance = Convert.ToDecimal(data.F_TotalOrderPerformance ?? 0) - Convert.ToDecimal(data.F_RefundAmount ?? 0), + CreateTime = DateTime.Now }).ToList(); return new @@ -3240,9 +3555,9 @@ namespace NCC.Extend.LqStatistics list = outputList, pagination = new { - pageIndex = input.PageIndex, - pageSize = input.PageSize, - total = pagedResult.pagination.Total + pageIndex = pageIndex, + pageSize = pageSize, + total = totalCount } }; } diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqTechGeneralManagerSalaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqTechGeneralManagerSalaryService.cs index 1cc7903..561f7af 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqTechGeneralManagerSalaryService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqTechGeneralManagerSalaryService.cs @@ -7,7 +7,9 @@ using NCC.DynamicApiController; using NCC.Extend.Entitys.Dto.LqTechGeneralManagerSalary; using NCC.Extend.Entitys.lq_attendance_summary; using NCC.Extend.Entitys.lq_hytk_hytk; +using NCC.Extend.Entitys.lq_hytk_jksyj; using NCC.Extend.Entitys.lq_hytk_mx; +using NCC.Extend.Entitys.lq_kd_jksyj; using NCC.Extend.Entitys.lq_kd_kdjlb; using NCC.Extend.Entitys.lq_kd_pxmx; using NCC.Extend.Entitys.lq_md_general_manager_lifeline; @@ -236,57 +238,47 @@ namespace NCC.Extend { foreach (var storeId in allManagedStoreIds) { - // 该门店的开单溯源金额 - var storeTraceabilityBilling = await _db.Queryable( - (pxmx, billing, item) => pxmx.Glkdbh == billing.Id && pxmx.Px == item.Id) - .Where((pxmx, billing, item) => - pxmx.IsEffective == 1 - && billing.IsEffective == 1 - && item.IsEffective == 1 - && (pxmx.BeautyType == "溯源系统" || pxmx.BeautyType == "溯源" - || item.BeautyType == "溯源系统" || item.BeautyType == "溯源") - && billing.Djmd == storeId - && billing.Kdrq >= startDate && billing.Kdrq <= endDate.AddDays(1)) - .SumAsync((pxmx, billing, item) => (decimal?)pxmx.ActualPrice) ?? 0m; - - // 该门店的退卡溯源金额 - var storeTraceabilityRefund = await _db.Queryable( - (tkmx, refund, item) => tkmx.RefundInfoId == refund.Id && tkmx.Px == item.Id) - .Where((tkmx, refund, item) => - tkmx.IsEffective == 1 - && refund.IsEffective == 1 - && item.IsEffective == 1 - && (tkmx.BeautyType == "溯源系统" || tkmx.BeautyType == "溯源" - || item.BeautyType == "溯源系统" || item.BeautyType == "溯源") - && refund.Md == storeId - && refund.Tksj >= startDate && refund.Tksj <= endDate.AddDays(1)) - .SumAsync((tkmx, refund, item) => (decimal?)tkmx.Tkje) ?? 0m; - - // 该门店的开单Cell金额 - var storeCellBilling = await _db.Queryable( - (pxmx, billing, item) => pxmx.Glkdbh == billing.Id && pxmx.Px == item.Id) - .Where((pxmx, billing, item) => - pxmx.IsEffective == 1 - && billing.IsEffective == 1 - && item.IsEffective == 1 - && (pxmx.BeautyType == "cell" || pxmx.BeautyType == "Cell" - || item.BeautyType == "cell" || item.BeautyType == "Cell") - && billing.Djmd == storeId - && billing.Kdrq >= startDate && billing.Kdrq <= endDate.AddDays(1)) - .SumAsync((pxmx, billing, item) => (decimal?)pxmx.ActualPrice) ?? 0m; - - // 该门店的退卡Cell金额 - var storeCellRefund = await _db.Queryable( - (tkmx, refund, item) => tkmx.RefundInfoId == refund.Id && tkmx.Px == item.Id) - .Where((tkmx, refund, item) => - tkmx.IsEffective == 1 - && refund.IsEffective == 1 - && item.IsEffective == 1 - && (tkmx.BeautyType == "cell" || tkmx.BeautyType == "Cell" - || item.BeautyType == "cell" || item.BeautyType == "Cell") - && refund.Md == storeId - && refund.Tksj >= startDate && refund.Tksj <= endDate.AddDays(1)) - .SumAsync((tkmx, refund, item) => (decimal?)tkmx.Tkje) ?? 0m; + // 该门店的开单溯源金额(从健康师业绩表统计) + var storeTraceabilityBillingList = await _db.Queryable() + .Where(x => x.IsEffective == 1 + && x.StoreId == storeId + && (x.BeautyType == "溯源系统" || x.BeautyType == "溯源") + && x.Yjsj >= startDate && x.Yjsj <= endDate.AddDays(1)) + .Select(x => x.Jksyj) + .ToListAsync(); + + var storeTraceabilityBilling = storeTraceabilityBillingList + .Where(x => !string.IsNullOrEmpty(x)) + .Sum(x => decimal.TryParse(x, out var val) ? val : 0m); + + // 该门店的退卡溯源金额(从退卡健康师业绩表统计) + var storeTraceabilityRefund = await _db.Queryable() + .Where(x => x.IsEffective == 1 + && x.StoreId == storeId + && (x.BeautyType == "溯源系统" || x.BeautyType == "溯源") + && x.Tksj >= startDate && x.Tksj <= endDate.AddDays(1)) + .SumAsync(x => (decimal?)x.Jksyj) ?? 0m; + + // 该门店的开单Cell金额(从健康师业绩表统计) + var storeCellBillingList = await _db.Queryable() + .Where(x => x.IsEffective == 1 + && x.StoreId == storeId + && (x.BeautyType == "cell" || x.BeautyType == "Cell") + && x.Yjsj >= startDate && x.Yjsj <= endDate.AddDays(1)) + .Select(x => x.Jksyj) + .ToListAsync(); + + var storeCellBilling = storeCellBillingList + .Where(x => !string.IsNullOrEmpty(x)) + .Sum(x => decimal.TryParse(x, out var val) ? val : 0m); + + // 该门店的退卡Cell金额(从退卡健康师业绩表统计) + var storeCellRefund = await _db.Queryable() + .Where(x => x.IsEffective == 1 + && x.StoreId == storeId + && (x.BeautyType == "cell" || x.BeautyType == "Cell") + && x.Tksj >= startDate && x.Tksj <= endDate.AddDays(1)) + .SumAsync(x => (decimal?)x.Jksyj) ?? 0m; // 获取该门店属于哪些科技部总经理 // 通过门店的kjb字段确定:如果门店的kjb等于科技一部的组织ID,则该门店属于科技一部总经理 diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqTechTeacherSalaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqTechTeacherSalaryService.cs index f19ab56..30bcf4c 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqTechTeacherSalaryService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqTechTeacherSalaryService.cs @@ -453,43 +453,72 @@ namespace NCC.Extend } /// - /// 计算业绩提成(阶梯式) + /// 计算业绩提成(分段累进式) /// /// 总业绩 /// 提成比例和金额 + /// + /// 提成规则(分段累进式): + /// 1. 前提条件:业绩必须大于1万才能进行提成 + /// 2. 如果有提成资格后,分段计算: + /// - 0-7万部分:2%(整个0-7万部分都按2%计算) + /// - 7万-15万部分:2.5% + /// - 15万以上部分:3% + /// + /// 计算公式(分段累进): + /// - 如果业绩 ≤ 1万:提成 = 0(无提成资格) + /// - 如果 1万 < 业绩 ≤ 7万:提成 = 业绩 × 2% + /// - 如果 7万 < 业绩 ≤ 15万:提成 = 7万 × 2% + (业绩 - 7万) × 2.5% + /// - 如果业绩 > 15万:提成 = 7万 × 2% + (15万 - 7万) × 2.5% + (业绩 - 15万) × 3% + /// + /// 示例: + /// - 总业绩 = 5,000元 → 提成 = 0(无提成资格) + /// - 总业绩 = 50,000元 → 提成 = 50,000 × 2% = 1,000元 + /// - 总业绩 = 100,000元 → 提成 = 70,000 × 2% + (100,000 - 70,000) × 2.5% = 1,400 + 750 = 2,150元 + /// - 总业绩 = 200,000元 → 提成 = 70,000 × 2% + (150,000 - 70,000) × 2.5% + (200,000 - 150,000) × 3% = 1,400 + 2,000 + 1,500 = 4,900元 + /// private (decimal Rate, decimal Amount) CalculatePerformanceCommission(decimal totalPerformance) { // 提成前提:业绩必须大于1万才能进行提成 if (totalPerformance <= 10000m) { - // ≤ 10,000元 → 0%(无提成) + // ≤ 10,000元 → 0%(无提成资格) return (0m, 0m); } - decimal rate; - decimal amount; + decimal totalCommission = 0m; - // 阶梯式提成计算(整个业绩按对应比例) + // 分段累进式提成计算(已通过提成资格检查) if (totalPerformance > 150000m) { - // > 15万 → 3% - rate = 3m; - amount = totalPerformance * 0.03m; + // 业绩 > 15万:分段计算 + // 0-7万部分:2% + decimal part1 = 70000m * 0.02m; // 7万 × 2% = 1,400元 + // 7万-15万部分:2.5% + decimal part2 = (150000m - 70000m) * 0.025m; // 8万 × 2.5% = 2,000元 + // 15万以上部分:3% + decimal part3 = (totalPerformance - 150000m) * 0.03m; + totalCommission = part1 + part2 + part3; } else if (totalPerformance > 70000m) { - // > 7万 且 ≤ 15万 → 2.5% - rate = 2.5m; - amount = totalPerformance * 0.025m; + // 业绩 > 7万 且 ≤ 15万:分段计算 + // 0-7万部分:2% + decimal part1 = 70000m * 0.02m; // 7万 × 2% = 1,400元 + // 7万以上部分:2.5% + decimal part2 = (totalPerformance - 70000m) * 0.025m; + totalCommission = part1 + part2; } else { - // > 1万 且 ≤ 7万 → 2% - rate = 2m; - amount = totalPerformance * 0.02m; + // 业绩 > 1万 且 ≤ 7万:整个业绩按2%计算 + totalCommission = totalPerformance * 0.02m; } - return (rate, amount); + // 计算平均提成比例(用于显示) + decimal averageRate = totalCommission > 0 && totalPerformance > 0 ? (totalCommission / totalPerformance) * 100m : 0m; + + return (averageRate, totalCommission); } /// diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqTkjlbService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqTkjlbService.cs index 1a79ae3..1277ce4 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqTkjlbService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqTkjlbService.cs @@ -898,6 +898,8 @@ namespace NCC.Extend.LqTkjlb tk.F_MemberId as member_id, -- 会员ID tk.F_CustomerName as customer_name, -- 顾客姓名 tk.F_CreateTime as tk_time, -- 拓客时间 + tk.F_ExpansionUserId as expansion_user_id, -- 拓客人员ID + COALESCE(expansion_user.F_REALNAME, '') as expansion_user_name, -- 拓客人员姓名 -- 邀约信息 yaoy.F_Id as yaoy_id, -- 邀约ID yaoy.F_CreateTime as yaoy_time, -- 邀约时间 @@ -936,6 +938,7 @@ namespace NCC.Extend.LqTkjlb ELSE '未开卡' END as billing_status -- 开卡状态描述 FROM lq_tkjlb tk + LEFT JOIN BASE_USER expansion_user ON tk.F_ExpansionUserId = expansion_user.F_Id LEFT JOIN lq_yaoyjl yaoy ON tk.F_MemberId = yaoy.yykh AND yaoy.F_StoreId = tk.F_StoreId LEFT JOIN lq_yyjl yy ON tk.F_MemberId = yy.gk diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqXmzlService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqXmzlService.cs index 10427a9..8b261e9 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqXmzlService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqXmzlService.cs @@ -22,6 +22,7 @@ using NCC.Extend.Entitys.lq_hytk_mx; using NCC.Extend.Entitys.lq_kd_kdjlb; using NCC.Extend.Entitys.lq_xh_hyhk; using NCC.Extend.Entitys.lq_hytk_hytk; +using NCC.Extend.Entitys.lq_kd_deductinfo; using Yitter.IdGenerator; using NCC.Common.Helper; using NCC.JsonSerialization; @@ -421,8 +422,8 @@ namespace NCC.Extend.LqXmzl /// 品项维度统计 /// /// - /// 按品项维度统计开卡、消耗、退卡等数据 - /// 包括业绩、人数、占比、复购率等指标 + /// 按品项维度统计开卡、消耗、退卡、储扣等数据 + /// 包括业绩、人数、占比、复购率、储扣次数及金额等指标 /// /// 示例请求: /// ```json @@ -430,7 +431,7 @@ namespace NCC.Extend.LqXmzl /// "startTime": "2024-01-01", /// "endTime": "2024-12-31", /// "storeId": "store001", - /// "category": "美容", + /// "itemCategory": "科美", /// "itemId": "item001" /// } /// ``` @@ -439,8 +440,14 @@ namespace NCC.Extend.LqXmzl /// - startTime: 开始时间(可选) /// - endTime: 结束时间(可选) /// - storeId: 门店ID(可选) - /// - category: 品项分类(可选) + /// - category: 品项分类(可选,已废弃,建议使用itemCategory) + /// - itemCategory: 品项分类筛选(可选,支持:科美、医美、生美、产品等,对应lq_xmzl表的qt2字段) /// - itemId: 品项ID(可选,单个品项统计) + /// + /// 返回字段说明: + /// - ItemCategory: 品项分类(科美、医美、生美、产品等) + /// - DeductAmount: 储扣金额 + /// - DeductCount: 储扣次数 /// /// 统计输入参数 /// 品项维度统计数据 @@ -452,7 +459,7 @@ namespace NCC.Extend.LqXmzl { try { - // 第一步:获取品项基础信息 + // 第一步:获取品项基础信息(优化:一次性查询,包含分类字段) var itemsQuery = _db.Queryable() .Where(x => x.IsEffective == 1); @@ -461,12 +468,19 @@ namespace NCC.Extend.LqXmzl itemsQuery = itemsQuery.Where(x => x.Id == input.ItemId); } + // 兼容旧的Category字段 if (!string.IsNullOrEmpty(input.Category)) { itemsQuery = itemsQuery.Where(x => x.Fl1 == input.Category || x.Fl2 == input.Category || x.Fl == input.Category); } - var items = await itemsQuery.ToListAsync(); + // 新增:按品项分类筛选(qt2字段:科美、医美、生美、产品等) + if (!string.IsNullOrEmpty(input.ItemCategory)) + { + itemsQuery = itemsQuery.Where(x => x.Qt2 == input.ItemCategory); + } + + var items = await itemsQuery.Select(x => new { x.Id, x.Xmmc, x.Xmbh, x.Qt2 }).ToListAsync(); if (!items.Any()) { @@ -480,6 +494,9 @@ namespace NCC.Extend.LqXmzl var itemIds = items.Select(x => x.Id).ToList(); + // 创建品项信息字典,便于后续查找 + var itemInfoDict = items.ToDictionary(x => x.Id, x => new { x.Xmmc, x.Xmbh, x.Qt2 }); + // 第二步:开卡数据统计 var billingStats = await GetBillingStatistics(itemIds, input); @@ -489,25 +506,37 @@ namespace NCC.Extend.LqXmzl // 第四步:退卡数据统计 var refundStats = await GetRefundStatistics(itemIds, input); - // 第五步:计算总数据用于占比计算 + // 第五步:储扣数据统计(新增) + var deductStats = await GetDeductStatistics(itemIds, input); + + // 第六步:计算总数据用于占比计算 var totalBillingAmount = billingStats.Sum(x => x.BillingAmount); var totalConsumeAmount = consumeStats.Sum(x => x.ConsumeAmount); var totalBuyers = billingStats.Sum(x => x.TotalBuyers); - // 第六步:合并数据并计算占比 + // 第七步:合并数据并计算占比(优化:使用字典提升查找效率) + var billingDict = billingStats.ToDictionary(x => x.ItemId); + var consumeDict = consumeStats.ToDictionary(x => x.ItemId); + var refundDict = refundStats.ToDictionary(x => x.ItemId); + var deductDict = deductStats.ToDictionary(x => x.ItemId); + var result = new List(); foreach (var item in items) { - var billingData = billingStats.FirstOrDefault(x => x.ItemId == item.Id); - var consumeData = consumeStats.FirstOrDefault(x => x.ItemId == item.Id); - var refundData = refundStats.FirstOrDefault(x => x.ItemId == item.Id); + billingDict.TryGetValue(item.Id, out var billingData); + consumeDict.TryGetValue(item.Id, out var consumeData); + refundDict.TryGetValue(item.Id, out var refundData); + deductDict.TryGetValue(item.Id, out var deductData); + + var itemInfo = itemInfoDict[item.Id]; var output = new LqXmzlStatisticsOutput { ItemId = item.Id, - ItemName = item.Xmmc, - ItemNumber = item.Xmbh, + ItemName = itemInfo.Xmmc, + ItemNumber = itemInfo.Xmbh, + ItemCategory = itemInfo.Qt2 ?? "", // 显示品项分类 BillingAmount = billingData?.BillingAmount ?? 0, BillingAmountRatio = totalBillingAmount > 0 ? (billingData?.BillingAmount ?? 0) / totalBillingAmount : 0, TotalBuyers = billingData?.TotalBuyers ?? 0, @@ -520,7 +549,9 @@ namespace NCC.Extend.LqXmzl ConsumeGiftCount = consumeData?.ConsumeGiftCount ?? 0, ConsumeExperienceCount = consumeData?.ConsumeExperienceCount ?? 0, RefundAmount = refundData?.RefundAmount ?? 0, - RefundCount = refundData?.RefundCount ?? 0 + RefundCount = refundData?.RefundCount ?? 0, + DeductAmount = deductData?.DeductAmount ?? 0, // 新增:储扣金额 + DeductCount = deductData?.DeductCount ?? 0 // 新增:储扣次数 }; result.Add(output); } @@ -575,34 +606,47 @@ namespace NCC.Extend.LqXmzl }) .ToListAsync(); - // 单独计算复购人数 - foreach (var item in result) + // 优化:批量计算复购人数,避免循环查询 + if (result.Any()) { - var memberCountQuery = _db.Queryable((px, kd) => new JoinQueryInfos( + var resultItemIds = result.Select(x => x.ItemId).ToList(); + + // 一次性查询所有品项的复购人数 + var repeatBuyerQuery = _db.Queryable((px, kd) => new JoinQueryInfos( JoinType.Inner, px.Glkdbh == kd.Id)) - .Where((px, kd) => px.Px == item.ItemId && px.IsEffective == 1 && kd.IsEffective == 1); + .Where((px, kd) => resultItemIds.Contains(px.Px) && px.IsEffective == 1 && kd.IsEffective == 1); if (input.StartTime.HasValue) { - memberCountQuery = memberCountQuery.Where((px, kd) => kd.Kdrq >= input.StartTime.Value); + repeatBuyerQuery = repeatBuyerQuery.Where((px, kd) => kd.Kdrq >= input.StartTime.Value); } if (input.EndTime.HasValue) { - memberCountQuery = memberCountQuery.Where((px, kd) => kd.Kdrq <= input.EndTime.Value); + repeatBuyerQuery = repeatBuyerQuery.Where((px, kd) => kd.Kdrq <= input.EndTime.Value); } if (!string.IsNullOrEmpty(input.StoreId)) { - memberCountQuery = memberCountQuery.Where((px, kd) => kd.Djmd == input.StoreId); + repeatBuyerQuery = repeatBuyerQuery.Where((px, kd) => kd.Djmd == input.StoreId); } - var memberStats = await memberCountQuery - .GroupBy((px, kd) => px.MemberId) - .Having((px, kd) => SqlFunc.AggregateCount(px.MemberId) > 1) - .Select((px, kd) => SqlFunc.AggregateCount(px.MemberId)) + // 按品项和会员分组,统计每个会员购买次数 + var memberPurchaseStats = await repeatBuyerQuery + .GroupBy((px, kd) => new { px.Px, px.MemberId }) + .Select((px, kd) => new { px.Px, px.MemberId, PurchaseCount = SqlFunc.AggregateCount(px.MemberId) }) .ToListAsync(); - item.RepeatBuyers = memberStats.Count; + // 统计每个品项的复购人数(购买次数>1的会员数) + var repeatBuyerDict = memberPurchaseStats + .Where(x => x.PurchaseCount > 1) + .GroupBy(x => x.Px) + .ToDictionary(g => g.Key, g => g.Count()); + + // 填充复购人数 + foreach (var item in result) + { + item.RepeatBuyers = repeatBuyerDict.ContainsKey(item.ItemId) ? repeatBuyerDict[item.ItemId] : 0; + } } return result; @@ -689,6 +733,45 @@ namespace NCC.Extend.LqXmzl return result; } + + /// + /// 获取储扣统计数据(新增) + /// + private async Task> GetDeductStatistics(List itemIds, LqXmzlStatisticsInput input) + { + // 使用JOIN关联开单记录表,以便使用开单时间进行过滤 + var query = _db.Queryable((deduct, kd) => new JoinQueryInfos( + JoinType.Inner, deduct.BillingId == kd.Id)) + .Where((deduct, kd) => itemIds.Contains(deduct.ItemId) && deduct.IsEffective == 1 && kd.IsEffective == 1); + + // 时间过滤(使用开单时间) + if (input.StartTime.HasValue) + { + query = query.Where((deduct, kd) => (deduct.BillingTime ?? kd.Kdrq) >= input.StartTime.Value); + } + if (input.EndTime.HasValue) + { + query = query.Where((deduct, kd) => (deduct.BillingTime ?? kd.Kdrq) <= input.EndTime.Value); + } + + // 门店过滤 + if (!string.IsNullOrEmpty(input.StoreId)) + { + query = query.Where((deduct, kd) => kd.Djmd == input.StoreId); + } + + var result = await query + .GroupBy((deduct, kd) => deduct.ItemId) + .Select((deduct, kd) => new ItemDeductStatisticsData + { + ItemId = deduct.ItemId, + DeductAmount = SqlFunc.AggregateSum(deduct.Amount ?? 0), + DeductCount = SqlFunc.AggregateCount(deduct.Id) + }) + .ToListAsync(); + + return result; + } #endregion #region 获取品项门店统计 @@ -716,8 +799,12 @@ namespace NCC.Extend.LqXmzl /// - StoreId: 门店ID /// - StoreName: 门店名称 /// - BillingCount: 开单数(去重后的开单编号数量) - /// - ProjectCount: 项目数(项目次数总和) - /// - ActualAmount: 实付金额(实付金额总和) + /// - ProjectCount: 项目数(项目次数总和,已废弃,使用TotalProjectCount代替) + /// - TotalProjectCount: 总项目数(项目次数总和) + /// - PurchaseProjectCount: 购买项目数(来源类型为"购买"的项目次数总和) + /// - ExperienceProjectCount: 体验项目数(来源类型为"体验"的项目次数总和) + /// - GiftProjectCount: 赠送项目数(来源类型为"赠送"的项目次数总和) + /// - ActualAmount: 实付金额(实付金额总和,使用开单记录的sfyj字段) /// - RefundAmount: 退款金额(退款金额总和) /// /// 查询参数 @@ -734,13 +821,18 @@ namespace NCC.Extend.LqXmzl } // 查询开单统计数据(按门店分组) + // 修改:ActualAmount使用开单记录的实付金额(sfyj),而不是品项明细的实际价格 + // 注意:需要先按开单去重,避免一个开单包含多个该品项时重复计算sfyj + // 新增:按来源类型(SourceType)拆分项目数统计 var billingSql = $@" SELECT store.F_Id as StoreId, store.dm as StoreName, COUNT(DISTINCT billing.F_Id) as BillingCount, - COALESCE(SUM(pxmx.F_ProjectNumber), 0) as ProjectCount, - COALESCE(SUM(pxmx.F_ActualPrice), 0) as ActualAmount + COALESCE(SUM(pxmx.F_ProjectNumber), 0) as TotalProjectCount, + COALESCE(SUM(CASE WHEN pxmx.F_SourceType = '购买' THEN pxmx.F_ProjectNumber ELSE 0 END), 0) as PurchaseProjectCount, + COALESCE(SUM(CASE WHEN pxmx.F_SourceType = '体验' THEN pxmx.F_ProjectNumber ELSE 0 END), 0) as ExperienceProjectCount, + COALESCE(SUM(CASE WHEN pxmx.F_SourceType = '赠送' THEN pxmx.F_ProjectNumber ELSE 0 END), 0) as GiftProjectCount FROM lq_kd_pxmx pxmx INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id INNER JOIN lq_mdxx store ON billing.djmd = store.F_Id @@ -751,6 +843,33 @@ namespace NCC.Extend.LqXmzl AND billing.kdrq < '{input.EndTime.AddDays(1):yyyy-MM-dd} 00:00:00' GROUP BY store.F_Id, store.dm"; + // 查询实付金额(按门店分组,使用开单的sfyj字段,去重开单ID) + var actualAmountSql = $@" + SELECT + billing.djmd as StoreId, + COALESCE(SUM(billing.sfyj), 0) as ActualAmount + FROM ( + SELECT DISTINCT billing2.F_Id, billing2.djmd, billing2.sfyj + FROM lq_kd_kdjlb billing2 + INNER JOIN lq_kd_pxmx pxmx2 ON billing2.F_Id = pxmx2.glkdbh + WHERE pxmx2.px = '{input.ItemId}' + AND pxmx2.F_IsEffective = 1 + AND billing2.F_IsEffective = 1 + AND billing2.kdrq >= '{input.StartTime:yyyy-MM-dd} 00:00:00' + AND billing2.kdrq < '{input.EndTime.AddDays(1):yyyy-MM-dd} 00:00:00' + ) billing + GROUP BY billing.djmd"; + + var actualAmountData = await _db.Ado.SqlQueryAsync(actualAmountSql); + + // 创建实付金额字典 + var actualAmountDict = actualAmountData + .Where(x => x.StoreId != null) + .ToDictionary( + x => x.StoreId.ToString(), + x => Convert.ToDecimal(x.ActualAmount ?? 0) + ); + var billingData = await _db.Ado.SqlQueryAsync(billingSql); // 查询退款统计数据(按门店分组) @@ -778,13 +897,19 @@ namespace NCC.Extend.LqXmzl foreach (var item in billingData ?? Enumerable.Empty()) { var storeId = item.StoreId.ToString(); + // 从实付金额字典中获取该门店的实付金额 + var actualAmount = actualAmountDict.ContainsKey(storeId) ? actualAmountDict[storeId] : 0; resultDict[storeId] = new ItemStoreStatisticsOutput { StoreId = storeId, StoreName = item.StoreName.ToString(), BillingCount = Convert.ToInt32(item.BillingCount), - ProjectCount = Convert.ToDecimal(item.ProjectCount), - ActualAmount = Convert.ToDecimal(item.ActualAmount), + ProjectCount = Convert.ToDecimal(item.TotalProjectCount ?? item.ProjectCount ?? 0), // 保持兼容性 + TotalProjectCount = Convert.ToDecimal(item.TotalProjectCount ?? 0), + PurchaseProjectCount = Convert.ToDecimal(item.PurchaseProjectCount ?? 0), + ExperienceProjectCount = Convert.ToDecimal(item.ExperienceProjectCount ?? 0), + GiftProjectCount = Convert.ToDecimal(item.GiftProjectCount ?? 0), + ActualAmount = actualAmount, // 使用开单记录的实付金额(sfyj) RefundAmount = 0 }; } @@ -806,6 +931,10 @@ namespace NCC.Extend.LqXmzl StoreName = item.StoreName.ToString(), BillingCount = 0, ProjectCount = 0, + TotalProjectCount = 0, + PurchaseProjectCount = 0, + ExperienceProjectCount = 0, + GiftProjectCount = 0, ActualAmount = 0, RefundAmount = Convert.ToDecimal(item.RefundAmount) }; @@ -851,4 +980,14 @@ namespace NCC.Extend.LqXmzl public decimal RefundAmount { get; set; } public int RefundCount { get; set; } } + + /// + /// 品项储扣统计数据(内部类) + /// + public class ItemDeductStatisticsData + { + public string ItemId { get; set; } + public decimal DeductAmount { get; set; } + public int DeductCount { get; set; } + } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend/NCC.Extend.csproj b/netcore/src/Modularity/Extend/NCC.Extend/NCC.Extend.csproj index 3b939c9..4351943 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/NCC.Extend.csproj +++ b/netcore/src/Modularity/Extend/NCC.Extend/NCC.Extend.csproj @@ -15,5 +15,6 @@ + diff --git a/sql/事业部总经理经理工资表新增毛利相关字段.sql b/sql/事业部总经理经理工资表新增毛利相关字段.sql new file mode 100644 index 0000000..8389f32 --- /dev/null +++ b/sql/事业部总经理经理工资表新增毛利相关字段.sql @@ -0,0 +1,46 @@ +-- ============================================ +-- 事业部总经理/经理工资表新增毛利相关字段 +-- 表名:lq_business_unit_manager_salary_statistics +-- 说明:为事业部总经理/经理工资计算添加毛利相关字段,用于计算基于毛利的提成 +-- 执行时间:2025年 +-- ============================================ + +-- 1. 销售业绩(开单业绩-退款业绩) +ALTER TABLE lq_business_unit_manager_salary_statistics +ADD COLUMN F_SalesPerformance DECIMAL(18,2) DEFAULT 0.00 COMMENT '销售业绩(开单业绩-退款业绩)' AFTER F_StorePerformanceDetail; + +-- 2. 产品物料(仓库领用金额) +ALTER TABLE lq_business_unit_manager_salary_statistics +ADD COLUMN F_ProductMaterial DECIMAL(18,2) DEFAULT 0.00 COMMENT '产品物料(仓库领用金额,注意11月特殊规则:11月工资算10月数据)' AFTER F_SalesPerformance; + +-- 3. 合作项目成本 +ALTER TABLE lq_business_unit_manager_salary_statistics +ADD COLUMN F_CooperationCost DECIMAL(18,2) DEFAULT 0.00 COMMENT '合作项目成本' AFTER F_ProductMaterial; + +-- 4. 店内支出 +ALTER TABLE lq_business_unit_manager_salary_statistics +ADD COLUMN F_StoreExpense DECIMAL(18,2) DEFAULT 0.00 COMMENT '店内支出' AFTER F_CooperationCost; + +-- 5. 洗毛巾费用 +ALTER TABLE lq_business_unit_manager_salary_statistics +ADD COLUMN F_LaundryCost DECIMAL(18,2) DEFAULT 0.00 COMMENT '洗毛巾费用(只统计送出的记录,F_FlowType = 0)' AFTER F_StoreExpense; + +-- 6. 毛利(销售业绩-产品物料-合作项目成本-店内支出-洗毛巾) +ALTER TABLE lq_business_unit_manager_salary_statistics +ADD COLUMN F_GrossProfit DECIMAL(18,2) DEFAULT 0.00 COMMENT '毛利(销售业绩-产品物料-合作项目成本-店内支出-洗毛巾)' AFTER F_LaundryCost; + +-- ============================================ +-- 字段说明 +-- ============================================ +-- F_SalesPerformance: 销售业绩 = 开单业绩 - 退款业绩 +-- F_ProductMaterial: 产品物料 = 仓库领用金额合计(注意11月特殊规则:11月工资算10月数据) +-- F_CooperationCost: 合作项目成本 = 合作成本表合计金额 +-- F_StoreExpense: 店内支出 = 店内支出表合计金额 +-- F_LaundryCost: 洗毛巾费用 = 送洗记录总费用(只统计送出的记录,F_FlowType = 0) +-- F_GrossProfit: 毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾 +-- +-- 重要说明: +-- 1. 提成计算基于毛利,而不是开单业绩 +-- 2. 必须满足提成阶梯1才能有提成资格 +-- 3. 提成计算方式:分段累进式 + diff --git a/sql/年度经营数据菜单配置.sql b/sql/年度经营数据菜单配置.sql new file mode 100644 index 0000000..5190847 --- /dev/null +++ b/sql/年度经营数据菜单配置.sql @@ -0,0 +1,30 @@ +-- 年度汇总表菜单配置脚本 (增强版:包含字段补全与管理员授权) + +SET @AdminRoleId = '94e3a9bb0fce4547886972998fddba1c'; -- 系统管理员角色ID + +-- 1. 清理旧数据 (防止重复执行报错) +DELETE FROM BASE_MODULE WHERE F_Id IN ('annual-summary-catalog', 'annual-summary-data', 'annual-summary-dashboard'); +DELETE FROM BASE_AUTHORIZE WHERE F_ItemId IN ('annual-summary-catalog', 'annual-summary-data', 'annual-summary-dashboard'); + +-- 2. 创建目录: 年度经营数据 (父级: 报表中心 725873504657868037) +INSERT INTO BASE_MODULE (F_Id, F_ParentId, F_Type, F_FullName, F_EnCode, F_UrlAddress, F_Icon, F_SortCode, F_EnabledMark, F_Category, F_DeleteMark, F_LinkTarget, F_PropertyJson, F_IsButtonAuthorize, F_IsColumnAuthorize, F_IsDataAuthorize, F_IsFormAuthorize, F_CreatorTime) +VALUES +('annual-summary-catalog', '725873504657868037', 1, '年度经营数据', 'annualSummary', '', 'icon-ym icon-ym-report', 10, 1, 'Web', NULL, '_self', '{"moduleId":"","iconBackgroundColor":"","isTree":0}', 0, 0, 0, 0, NOW()); + +-- 3. 创建菜单: 汇总数据列表 (父级: 年度经营数据) +INSERT INTO BASE_MODULE (F_Id, F_ParentId, F_Type, F_FullName, F_EnCode, F_UrlAddress, F_Icon, F_SortCode, F_EnabledMark, F_Category, F_DeleteMark, F_LinkTarget, F_PropertyJson, F_IsButtonAuthorize, F_IsColumnAuthorize, F_IsDataAuthorize, F_IsFormAuthorize, F_CreatorTime) +VALUES +('annual-summary-data', 'annual-summary-catalog', 2, '汇总数据列表', 'annualSummaryData', 'extend/annualSummary/dataManage', 'icon-ym icon-ym-extended', 1, 1, 'Web', NULL, '_self', '{"moduleId":"","iconBackgroundColor":"","isTree":0}', 1, 1, 1, 1, NOW()); + +-- 4. 创建菜单: 经营统计分析 (父级: 年度经营数据) +INSERT INTO BASE_MODULE (F_Id, F_ParentId, F_Type, F_FullName, F_EnCode, F_UrlAddress, F_Icon, F_SortCode, F_EnabledMark, F_Category, F_DeleteMark, F_LinkTarget, F_PropertyJson, F_IsButtonAuthorize, F_IsColumnAuthorize, F_IsDataAuthorize, F_IsFormAuthorize, F_CreatorTime) +VALUES +('annual-summary-dashboard', 'annual-summary-catalog', 2, '经营统计分析', 'annualSummaryDashboard', 'extend/annualSummary/dashboard', 'icon-ym icon-ym-report-columnar', 2, 1, 'Web', NULL, '_self', '{"moduleId":"","iconBackgroundColor":"","isTree":0}', 1, 1, 1, 1, NOW()); + +-- 5. 授权给 系统管理员 角色 +INSERT INTO BASE_AUTHORIZE (F_Id, F_ItemType, F_ItemId, F_ObjectType, F_ObjectId, F_SortCode, F_CreatorTime, F_CreatorUserId) +VALUES +(REPLACE(UUID(), '-', ''), 'module', 'annual-summary-catalog', 'Role', @AdminRoleId, 1, NOW(), 'admin'), +(REPLACE(UUID(), '-', ''), 'module', 'annual-summary-data', 'Role', @AdminRoleId, 2, NOW(), 'admin'), +(REPLACE(UUID(), '-', ''), 'module', 'annual-summary-dashboard', 'Role', @AdminRoleId, 3, NOW(), 'admin'); + diff --git a/sql/排查生美业绩统计差异-简化版.sql b/sql/排查生美业绩统计差异-简化版.sql index 4857479..c117101 100644 --- a/sql/排查生美业绩统计差异-简化版.sql +++ b/sql/排查生美业绩统计差异-简化版.sql @@ -128,3 +128,6 @@ ORDER BY 生美业绩 DESC; + + + diff --git a/sql/排查生美业绩统计差异详细.sql b/sql/排查生美业绩统计差异详细.sql index 57417b0..de0263b 100644 --- a/sql/排查生美业绩统计差异详细.sql +++ b/sql/排查生美业绩统计差异详细.sql @@ -180,3 +180,6 @@ HAVING COUNT(*) > 1; + + + diff --git a/sql/检查生美业绩统计差异.sql b/sql/检查生美业绩统计差异.sql index 8024f26..f377371 100644 --- a/sql/检查生美业绩统计差异.sql +++ b/sql/检查生美业绩统计差异.sql @@ -150,3 +150,6 @@ ORDER BY 生美业绩 DESC; + + + diff --git a/test_deduct_list.sh b/test_deduct_list.sh new file mode 100755 index 0000000..72f2819 --- /dev/null +++ b/test_deduct_list.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# 测试储扣列表接口 + +echo "=== 测试储扣列表接口 ===" + +# 获取Token +TOKEN=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e" | python3 -c "import sys, json; print(json.load(sys.stdin)['data']['token'])" 2>/dev/null) + +if [ -z "$TOKEN" ]; then + echo "❌ 获取Token失败" + exit 1 +fi + +echo "✅ Token获取成功" +echo "" + +# 测试1: 基础查询 +echo "--- 测试1: 基础查询(第1页,每页5条) ---" +RESPONSE1=$(curl -s -w "\nTIME:%{time_total}" -X GET "http://localhost:2011/api/Extend/lqkdkdjlb/deduct-list?currentPage=1&pageSize=5" \ + -H "Authorization: $TOKEN") +TIME1=$(echo "$RESPONSE1" | grep "TIME:" | cut -d: -f2) +RESPONSE_BODY1=$(echo "$RESPONSE1" | sed '/TIME:/d') +echo "响应时间: ${TIME1}秒" +echo "$RESPONSE_BODY1" | python3 -c " +import sys, json +try: + data = json.load(sys.stdin) + if data.get('code') == 200: + result = data.get('data', {}) + if 'list' in result: + print(f'✅ 接口调用成功') + print(f'当前页记录数: {len(result.get(\"list\", []))}') + print(f'总记录数: {result.get(\"pagination\", {}).get(\"total\", 0)}') + if 'statistics' in result: + stats = result['statistics'] + print(f'统计信息:') + print(f' - 总记录数: {stats.get(\"totalCount\", 0)}') + print(f' - 总金额: {stats.get(\"totalAmount\", 0):,.2f}') + print(f' - 总项目数: {stats.get(\"totalProjectNumber\", 0)}') + else: + print('❌ 缺少统计信息') + else: + print('❌ 返回结构不正确') + print('返回的keys:', list(result.keys()) if isinstance(result, dict) else 'N/A') + else: + print(f'❌ 接口返回错误: {data.get(\"msg\", \"未知错误\")}') +except Exception as e: + print(f'❌ 解析响应失败: {e}') + import traceback + traceback.print_exc() +" 2>&1 +echo "" + +echo "=== 测试完成 ===" diff --git a/test_gold_triangle.sh b/test_gold_triangle.sh new file mode 100755 index 0000000..639af93 --- /dev/null +++ b/test_gold_triangle.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# 测试金三角名称和队伍业绩占比 + +echo "=== 测试金三角名称和队伍业绩占比 ===" +echo "" + +# 1. 获取token +TOKEN_RESPONSE=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e") + +TOKEN=$(echo "$TOKEN_RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data['data']['token'])" 2>/dev/null) + +if [ -z "$TOKEN" ]; then + echo "❌ Token获取失败" + exit 1 +fi + +echo "✅ Token获取成功" +echo "" + +# 2. 查询有金三角的健康师 +echo "=== 查询有金三角的健康师(冷忠翠)===" +RESPONSE=$(curl -s -X GET "http://localhost:2011/api/Extend/lqkdkdjlb/get-health-coach-statistics?startTime=2025-12-01&endTime=2025-12-31&employeeName=冷忠翠¤tPage=1&pageSize=5" \ + -H "Authorization: $TOKEN") + +echo "$RESPONSE" | python3 -c " +import sys, json +try: + data = json.load(sys.stdin) + items = data.get('data', {}).get('list', []) + print(f'查询到 {len(items)} 条记录') + print('') + for i, item in enumerate(items, 1): + print(f'{i}. 健康师: {item.get(\"employeeName\", \"N/A\")}') + print(f' 门店: {item.get(\"storeName\", \"N/A\")}') + print(f' 金三角名称: {item.get(\"goldTriangleName\", \"无\")}') + print(f' 队伍业绩占比: {item.get(\"teamPerformanceRatio\", 0)}%') + print(f' 开单金额: {item.get(\"billingAmount\", 0)}') + print('') +except Exception as e: + print(f'解析错误: {e}') + print('原始响应:') + print(sys.stdin.read()) +" 2>/dev/null + diff --git a/test_health_coach_statistics.sh b/test_health_coach_statistics.sh new file mode 100755 index 0000000..08fb020 --- /dev/null +++ b/test_health_coach_statistics.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +# 测试健康师统计接口 + +echo "=== 测试健康师统计接口 ===" +echo "" + +# 1. 获取token +echo "=== 1. 获取Token ===" +TOKEN_RESPONSE=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e") + +TOKEN=$(echo "$TOKEN_RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data['data']['token'])" 2>/dev/null) + +if [ -z "$TOKEN" ]; then + echo "❌ Token获取失败" + echo "$TOKEN_RESPONSE" + exit 1 +fi + +echo "✅ Token获取成功" +echo "" + +# 2. 测试接口 - 查询2025年12月的数据 +echo "=== 2. 测试接口 - 查询2025年12月的数据 ===" +START_TIME=$(date +%s%N) +RESPONSE=$(curl -s -w "\n%{http_code}\n%{time_total}" -X GET "http://localhost:2011/api/Extend/lqkdkdjlb/get-health-coach-statistics?startTime=2025-12-01&endTime=2025-12-31¤tPage=1&pageSize=10" \ + -H "Authorization: $TOKEN") +END_TIME=$(date +%s%N) + +HTTP_CODE=$(echo "$RESPONSE" | tail -2 | head -1) +TIME_TOTAL=$(echo "$RESPONSE" | tail -1) +RESPONSE_BODY=$(echo "$RESPONSE" | sed '$d' | sed '$d') + +echo "HTTP状态码: $HTTP_CODE" +echo "响应时间: ${TIME_TOTAL}秒" +ELAPSED_MS=$((($END_TIME - $START_TIME) / 1000000)) +echo "总耗时: ${ELAPSED_MS}毫秒" +echo "" + +# 检查返回结果 +if [ "$HTTP_CODE" = "200" ]; then + CODE=$(echo "$RESPONSE_BODY" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('code', ''))" 2>/dev/null) + if [ "$CODE" = "200" ]; then + echo "✅ 接口调用成功" + LIST_COUNT=$(echo "$RESPONSE_BODY" | python3 -c "import sys, json; data=json.load(sys.stdin); print(len(data.get('data', {}).get('list', [])))" 2>/dev/null) + TOTAL=$(echo "$RESPONSE_BODY" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('data', {}).get('pagination', {}).get('total', 0))" 2>/dev/null) + echo "返回数据条数: $LIST_COUNT" + echo "总记录数: $TOTAL" + + # 显示前3条数据的详细信息 + echo "" + echo "前3条数据的详细信息:" + echo "$RESPONSE_BODY" | python3 -c " +import sys, json +try: + data = json.load(sys.stdin) + items = data.get('data', {}).get('list', [])[:3] + for i, item in enumerate(items, 1): + print(f\"{i}. {item.get('employeeName', 'N/A')}: 到店人数={item.get('visitCount', 0)}, 预约人数={item.get('appointmentCount', 0)}, 邀约人数={item.get('inviteCount', 0)}\") + print(f\" 金三角名称: {item.get('goldTriangleName', '无')}, 队伍业绩占比: {item.get('teamPerformanceRatio', 0)}%\") + print(f\" 开单金额: {item.get('billingAmount', 0)}\") +except Exception as e: + print(f'解析错误: {e}') +" 2>/dev/null + + # 检查是否有到店人数大于0的记录 + HAS_VISIT=$(echo "$RESPONSE_BODY" | python3 -c " +import sys, json +try: + data = json.load(sys.stdin) + items = data.get('data', {}).get('list', []) + has_visit = any(item.get('visitCount', 0) > 0 for item in items) + print('1' if has_visit else '0') +except: + print('0') +" 2>/dev/null) + + if [ "$HAS_VISIT" = "1" ]; then + echo "" + echo "✅ 有到店人数大于0的记录" + else + echo "" + echo "⚠️ 所有记录的到店人数都是0,可能存在问题" + fi + else + echo "❌ 接口返回错误" + echo "$RESPONSE_BODY" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE_BODY" + fi +else + echo "❌ HTTP请求失败" + echo "$RESPONSE_BODY" +fi + diff --git a/test_item_statistics.sh b/test_item_statistics.sh new file mode 100755 index 0000000..6407d00 --- /dev/null +++ b/test_item_statistics.sh @@ -0,0 +1,107 @@ +#!/bin/bash + +# 测试品项维度统计接口 + +echo "=== 测试品项维度统计接口 ===" + +# 获取Token +TOKEN=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e" | python3 -c "import sys, json; print(json.load(sys.stdin)['data']['token'])" 2>/dev/null) + +if [ -z "$TOKEN" ]; then + echo "❌ 获取Token失败" + exit 1 +fi + +echo "✅ Token获取成功" +echo "" + +# 测试1: 基础查询(2025年12月) +echo "--- 测试1: 基础查询(2025年12月,前5条) ---" +RESPONSE1=$(curl -s -w "\nTIME:%{time_total}" -X POST "http://localhost:2011/api/Extend/lqxmzl/GetItemStatistics" \ + -H "Authorization: $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "startTime": "2025-12-01T00:00:00", + "endTime": "2025-12-31T23:59:59" + }') +TIME1=$(echo "$RESPONSE1" | grep "TIME:" | cut -d: -f2) +RESPONSE_BODY1=$(echo "$RESPONSE1" | sed '/TIME:/d') +COUNT1=$(echo "$RESPONSE_BODY1" | python3 -c "import sys, json; data=json.load(sys.stdin); print(len(data.get('data', [])))" 2>/dev/null) +echo "响应时间: ${TIME1}秒" +echo "返回记录数: $COUNT1" +echo "前3条记录:" +echo "$RESPONSE_BODY1" | python3 -c " +import sys, json +data = json.load(sys.stdin) +items = data.get('data', [])[:3] +for i, item in enumerate(items, 1): + print(f'{i}. {item.get(\"itemName\", \"无\")} - 分类: {item.get(\"itemCategory\", \"无\")}, 开卡业绩: {item.get(\"billingAmount\", 0):,.2f}, 储扣金额: {item.get(\"deductAmount\", 0):,.2f}, 储扣次数: {item.get(\"deductCount\", 0)}') +" 2>/dev/null +echo "" + +# 测试2: 按分类筛选(科美) +echo "--- 测试2: 按分类筛选(科美) ---" +RESPONSE2=$(curl -s -w "\nTIME:%{time_total}" -X POST "http://localhost:2011/api/Extend/lqxmzl/GetItemStatistics" \ + -H "Authorization: $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "startTime": "2025-12-01T00:00:00", + "endTime": "2025-12-31T23:59:59", + "itemCategory": "科美" + }') +TIME2=$(echo "$RESPONSE2" | grep "TIME:" | cut -d: -f2) +RESPONSE_BODY2=$(echo "$RESPONSE2" | sed '/TIME:/d') +COUNT2=$(echo "$RESPONSE_BODY2" | python3 -c "import sys, json; data=json.load(sys.stdin); print(len(data.get('data', [])))" 2>/dev/null) +echo "响应时间: ${TIME2}秒" +echo "返回记录数: $COUNT2" +echo "前3条记录:" +echo "$RESPONSE_BODY2" | python3 -c " +import sys, json +data = json.load(sys.stdin) +items = data.get('data', [])[:3] +for i, item in enumerate(items, 1): + print(f'{i}. {item.get(\"itemName\", \"无\")} - 分类: {item.get(\"itemCategory\", \"无\")}, 储扣金额: {item.get(\"deductAmount\", 0):,.2f}, 储扣次数: {item.get(\"deductCount\", 0)}') +" 2>/dev/null +echo "" + +# 测试3: 按分类筛选(医美) +echo "--- 测试3: 按分类筛选(医美) ---" +RESPONSE3=$(curl -s -w "\nTIME:%{time_total}" -X POST "http://localhost:2011/api/Extend/lqxmzl/GetItemStatistics" \ + -H "Authorization: $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "startTime": "2025-12-01T00:00:00", + "endTime": "2025-12-31T23:59:59", + "itemCategory": "医美" + }') +TIME3=$(echo "$RESPONSE3" | grep "TIME:" | cut -d: -f2) +RESPONSE_BODY3=$(echo "$RESPONSE3" | sed '/TIME:/d') +COUNT3=$(echo "$RESPONSE_BODY3" | python3 -c "import sys, json; data=json.load(sys.stdin); print(len(data.get('data', [])))" 2>/dev/null) +echo "响应时间: ${TIME3}秒" +echo "返回记录数: $COUNT3" +echo "" + +# 测试4: 验证数据完整性 +echo "--- 测试4: 验证数据完整性 ---" +echo "$RESPONSE_BODY1" | python3 -c " +import sys, json +data = json.load(sys.stdin) +items = data.get('data', [])[:5] +print('数据完整性检查:') +for item in items: + has_category = 'itemCategory' in item and item['itemCategory'] is not None + has_deduct_amount = 'deductAmount' in item + has_deduct_count = 'deductCount' in item + status = '✅' if (has_category and has_deduct_amount and has_deduct_count) else '❌' + print(f'{status} {item.get(\"itemName\", \"无\")}: 分类={has_category}, 储扣金额={has_deduct_amount}, 储扣次数={has_deduct_count}') +" 2>/dev/null + +echo "" +echo "=== 测试完成 ===" +echo "性能总结:" +echo "- 基础查询: ${TIME1}秒" +echo "- 科美筛选: ${TIME2}秒" +echo "- 医美筛选: ${TIME3}秒" + diff --git a/test_personal_performance_api.sh b/test_personal_performance_api.sh new file mode 100755 index 0000000..672340f --- /dev/null +++ b/test_personal_performance_api.sh @@ -0,0 +1,127 @@ +#!/bin/bash + +# 测试个人业绩统计接口性能和返回数据 + +echo "=== 测试个人业绩统计接口 ===" +echo "" + +# 1. 获取token +echo "=== 1. 获取Token ===" +TOKEN_RESPONSE=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e") + +TOKEN=$(echo "$TOKEN_RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data['data']['token'])" 2>/dev/null) + +if [ -z "$TOKEN" ]; then + echo "❌ Token获取失败" + echo "$TOKEN_RESPONSE" + exit 1 +fi + +echo "✅ Token获取成功" +echo "" + +# 2. 测试接口 - 无筛选条件,第一页 +echo "=== 2. 测试接口 - 无筛选条件,第一页(20条)===" +START_TIME=$(date +%s%N) +RESPONSE=$(curl -s -w "\n%{http_code}\n%{time_total}" -X POST "http://localhost:2011/api/Extend/LqStatistics/get-personal-performance-statistics-list" \ + -H "Authorization: $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "statisticsMonth": "202512", + "currentPage": 1, + "pageSize": 20 + }') +END_TIME=$(date +%s%N) + +HTTP_CODE=$(echo "$RESPONSE" | tail -2 | head -1) +TIME_TOTAL=$(echo "$RESPONSE" | tail -1) +RESPONSE_BODY=$(echo "$RESPONSE" | sed '$d' | sed '$d') + +echo "HTTP状态码: $HTTP_CODE" +echo "响应时间: ${TIME_TOTAL}秒" +ELAPSED_MS=$((($END_TIME - $START_TIME) / 1000000)) +echo "总耗时: ${ELAPSED_MS}毫秒" +echo "" + +# 检查返回结果 +if [ "$HTTP_CODE" = "200" ]; then + CODE=$(echo "$RESPONSE_BODY" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('code', ''))" 2>/dev/null) + if [ "$CODE" = "200" ]; then + echo "✅ 接口调用成功" + LIST_COUNT=$(echo "$RESPONSE_BODY" | python3 -c "import sys, json; data=json.load(sys.stdin); print(len(data.get('data', {}).get('list', [])))" 2>/dev/null) + TOTAL=$(echo "$RESPONSE_BODY" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('data', {}).get('pagination', {}).get('total', 0))" 2>/dev/null) + echo "返回数据条数: $LIST_COUNT" + echo "总记录数: $TOTAL" + + # 显示第一条数据示例 + echo "" + echo "第一条数据示例:" + echo "$RESPONSE_BODY" | python3 -c "import sys, json; data=json.load(sys.stdin); item=data.get('data', {}).get('list', [{}])[0] if data.get('data', {}).get('list') else {}; print(json.dumps(item, indent=2, ensure_ascii=False))" 2>/dev/null + else + echo "❌ 接口返回错误" + echo "$RESPONSE_BODY" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE_BODY" + fi +else + echo "❌ HTTP请求失败" + echo "$RESPONSE_BODY" +fi + +echo "" +echo "" + +# 3. 测试接口 - 带门店筛选 +echo "=== 3. 测试接口 - 带门店筛选 ===" +START_TIME=$(date +%s%N) +RESPONSE2=$(curl -s -w "\n%{http_code}\n%{time_total}" -X POST "http://localhost:2011/api/Extend/LqStatistics/get-personal-performance-statistics-list" \ + -H "Authorization: $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "statisticsMonth": "202512", + "storeName": "测试", + "currentPage": 1, + "pageSize": 20 + }') +END_TIME=$(date +%s%N) + +HTTP_CODE2=$(echo "$RESPONSE2" | tail -2 | head -1) +TIME_TOTAL2=$(echo "$RESPONSE2" | tail -1) +RESPONSE_BODY2=$(echo "$RESPONSE2" | sed '$d' | sed '$d') + +echo "HTTP状态码: $HTTP_CODE2" +echo "响应时间: ${TIME_TOTAL2}秒" +ELAPSED_MS2=$((($END_TIME - $START_TIME) / 1000000)) +echo "总耗时: ${ELAPSED_MS2}毫秒" + +if [ "$HTTP_CODE2" = "200" ]; then + CODE2=$(echo "$RESPONSE_BODY2" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('code', ''))" 2>/dev/null) + if [ "$CODE2" = "200" ]; then + echo "✅ 接口调用成功" + LIST_COUNT2=$(echo "$RESPONSE_BODY2" | python3 -c "import sys, json; data=json.load(sys.stdin); print(len(data.get('data', {}).get('list', [])))" 2>/dev/null) + TOTAL2=$(echo "$RESPONSE_BODY2" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('data', {}).get('pagination', {}).get('total', 0))" 2>/dev/null) + echo "返回数据条数: $LIST_COUNT2" + echo "总记录数: $TOTAL2" + else + echo "❌ 接口返回错误" + echo "$RESPONSE_BODY2" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE_BODY2" + fi +fi + +echo "" +echo "" + +# 4. 性能评估 +echo "=== 4. 性能评估 ===" +if [ -n "$ELAPSED_MS" ]; then + if [ "$ELAPSED_MS" -lt 1000 ]; then + echo "✅ 性能优秀 (< 1秒)" + elif [ "$ELAPSED_MS" -lt 3000 ]; then + echo "⚠️ 性能良好 (1-3秒)" + elif [ "$ELAPSED_MS" -lt 5000 ]; then + echo "⚠️ 性能一般 (3-5秒)" + else + echo "❌ 性能较差 (> 5秒),需要优化" + fi +fi + diff --git a/test_store_customer_details.sh b/test_store_customer_details.sh new file mode 100644 index 0000000..fb3abf4 --- /dev/null +++ b/test_store_customer_details.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# 测试门店客户详情接口,验证拓客人员姓名 + +echo "=== 测试门店客户详情接口 ===" +echo "" + +# 1. 获取token +TOKEN_RESPONSE=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e") + +TOKEN=$(echo "$TOKEN_RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data['data']['token'])" 2>/dev/null) + +if [ -z "$TOKEN" ]; then + echo "❌ Token获取失败" + exit 1 +fi + +echo "✅ Token获取成功" +echo "" + +# 2. 查询门店客户详情(需要先获取一个有效的eventId和storeId) +echo "=== 查询门店客户详情 ===" +echo "提示: 需要提供有效的eventId和storeId参数" +echo "" + +# 先查询一个活动ID和门店ID +EVENT_RESPONSE=$(curl -s -X GET "http://localhost:2011/api/Extend/lqtkjlb" \ + -H "Authorization: $TOKEN") + +echo "查询活动列表..." +echo "$EVENT_RESPONSE" | python3 -c " +import sys, json +try: + data = json.load(sys.stdin) + events = data.get('data', {}).get('list', [])[:3] + if events: + print('前3个活动:') + for i, event in enumerate(events, 1): + print(f'{i}. 活动ID: {event.get(\"id\", \"N/A\")}, 活动名称: {event.get(\"eventName\", \"N/A\")}') + else: + print('未找到活动数据') +except Exception as e: + print(f'解析错误: {e}') +" 2>/dev/null + +echo "" +echo "请手动测试接口,使用以下格式:" +echo "curl -X GET \"http://localhost:2011/api/Extend/lqtkjlb/GetStoreCustomerDetailsPaged/{eventId}/{storeId}?pageIndex=1&pageSize=10\" -H \"Authorization: \$TOKEN\"" + diff --git a/test_store_total_performance_performance.sh b/test_store_total_performance_performance.sh new file mode 100755 index 0000000..4496812 --- /dev/null +++ b/test_store_total_performance_performance.sh @@ -0,0 +1,114 @@ +#!/bin/bash + +# 门店总业绩统计接口性能测试 + +echo "=== 门店总业绩统计接口性能测试 ===" + +# 获取Token +TOKEN=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e" | python3 -c "import sys, json; print(json.load(sys.stdin)['data']['token'])" 2>/dev/null) + +if [ -z "$TOKEN" ]; then + echo "❌ 获取Token失败" + exit 1 +fi + +echo "✅ Token获取成功" +echo "" + +# 测试1: 分页查询(每页10条) +echo "--- 测试1: 分页查询(每页10条) ---" +START=$(date +%s.%3N) +RESPONSE1=$(curl -s -w "\nTIME:%{time_total}" -X POST "http://localhost:2011/api/Extend/LqStatistics/get-store-total-performance-statistics-list" \ + -H "Authorization: $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"statisticsMonth": "202512", "pageIndex": 1, "pageSize": 10}') +END=$(date +%s.%3N) +TIME1=$(echo "$RESPONSE1" | grep "TIME:" | cut -d: -f2) +RESPONSE_BODY1=$(echo "$RESPONSE1" | sed '/TIME:/d') +TOTAL1=$(echo "$RESPONSE_BODY1" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('data', {}).get('pagination', {}).get('total', 0))" 2>/dev/null) +echo "响应时间: ${TIME1}秒" +echo "总记录数: $TOTAL1" +echo "" + +# 测试2: 分页查询(每页50条) +echo "--- 测试2: 分页查询(每页50条) ---" +START=$(date +%s.%3N) +RESPONSE2=$(curl -s -w "\nTIME:%{time_total}" -X POST "http://localhost:2011/api/Extend/LqStatistics/get-store-total-performance-statistics-list" \ + -H "Authorization: $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"statisticsMonth": "202512", "pageIndex": 1, "pageSize": 50}') +END=$(date +%s.%3N) +TIME2=$(echo "$RESPONSE2" | grep "TIME:" | cut -d: -f2) +RESPONSE_BODY2=$(echo "$RESPONSE2" | sed '/TIME:/d') +TOTAL2=$(echo "$RESPONSE_BODY2" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('data', {}).get('pagination', {}).get('total', 0))" 2>/dev/null) +echo "响应时间: ${TIME2}秒" +echo "总记录数: $TOTAL2" +echo "" + +# 测试3: 门店名称筛选 +echo "--- 测试3: 门店名称筛选(搜索'紫荆') ---" +START=$(date +%s.%3N) +RESPONSE3=$(curl -s -w "\nTIME:%{time_total}" -X POST "http://localhost:2011/api/Extend/LqStatistics/get-store-total-performance-statistics-list" \ + -H "Authorization: $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"statisticsMonth": "202512", "storeName": "紫荆", "pageIndex": 1, "pageSize": 10}') +END=$(date +%s.%3N) +TIME3=$(echo "$RESPONSE3" | grep "TIME:" | cut -d: -f2) +RESPONSE_BODY3=$(echo "$RESPONSE3" | sed '/TIME:/d') +TOTAL3=$(echo "$RESPONSE_BODY3" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('data', {}).get('pagination', {}).get('total', 0))" 2>/dev/null) +LIST_COUNT3=$(echo "$RESPONSE_BODY3" | python3 -c "import sys, json; data=json.load(sys.stdin); print(len(data.get('data', {}).get('list', [])))" 2>/dev/null) +echo "响应时间: ${TIME3}秒" +echo "总记录数: $TOTAL3" +echo "当前页记录数: $LIST_COUNT3" +echo "" + +# 测试4: 连续5次请求,测试稳定性 +echo "--- 测试4: 连续5次请求,测试稳定性 ---" +TIMES=() +for i in {1..5}; do + START=$(date +%s.%3N) + RESPONSE=$(curl -s -w "\nTIME:%{time_total}" -X POST "http://localhost:2011/api/Extend/LqStatistics/get-store-total-performance-statistics-list" \ + -H "Authorization: $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"statisticsMonth": "202512", "pageIndex": 1, "pageSize": 10}') + END=$(date +%s.%3N) + TIME=$(echo "$RESPONSE" | grep "TIME:" | cut -d: -f2) + TIMES+=($TIME) + echo "第${i}次请求: ${TIME}秒" +done + +# 计算平均时间 +SUM=0 +for t in "${TIMES[@]}"; do + SUM=$(echo "$SUM + $t" | bc) +done +AVG=$(echo "scale=3; $SUM / ${#TIMES[@]}" | bc) +echo "平均响应时间: ${AVG}秒" +echo "" + +# 测试5: 查询不同月份(2025年11月) +echo "--- 测试5: 查询2025年11月数据 ---" +START=$(date +%s.%3N) +RESPONSE5=$(curl -s -w "\nTIME:%{time_total}" -X POST "http://localhost:2011/api/Extend/LqStatistics/get-store-total-performance-statistics-list" \ + -H "Authorization: $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"statisticsMonth": "202511", "pageIndex": 1, "pageSize": 10}') +END=$(date +%s.%3N) +TIME5=$(echo "$RESPONSE5" | grep "TIME:" | cut -d: -f2) +RESPONSE_BODY5=$(echo "$RESPONSE5" | sed '/TIME:/d') +TOTAL5=$(echo "$RESPONSE_BODY5" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('data', {}).get('pagination', {}).get('total', 0))" 2>/dev/null) +echo "响应时间: ${TIME5}秒" +echo "总记录数: $TOTAL5" +echo "" + +echo "=== 性能测试总结 ===" +echo "1. 分页查询(10条): ${TIME1}秒" +echo "2. 分页查询(50条): ${TIME2}秒" +echo "3. 门店名称筛选: ${TIME3}秒" +echo "4. 连续5次平均: ${AVG}秒" +echo "5. 不同月份查询: ${TIME5}秒" +echo "" +echo "✅ 所有测试完成" + diff --git a/test_store_total_performance_statistics.sh b/test_store_total_performance_statistics.sh new file mode 100755 index 0000000..b80b224 --- /dev/null +++ b/test_store_total_performance_statistics.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +# 测试门店总业绩统计接口(实时查询) + +echo "=== 测试门店总业绩统计接口(实时查询) ===" + +# 获取Token +TOKEN=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e" | python3 -c "import sys, json; print(json.load(sys.stdin)['data']['token'])" 2>/dev/null) + +if [ -z "$TOKEN" ]; then + echo "❌ 获取Token失败" + exit 1 +fi + +echo "✅ Token获取成功" + +# 测试查询2025年12月的门店总业绩统计(带性能测试) +echo "" +echo "--- 测试查询2025年12月的门店总业绩统计(第1页,每页10条) ---" +echo "开始时间: $(date +%s.%3N)" +START_TIME=$(date +%s.%3N) +RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}\nTIME_TOTAL:%{time_total}\n" -X POST "http://localhost:2011/api/Extend/LqStatistics/get-store-total-performance-statistics-list" \ + -H "Authorization: $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "statisticsMonth": "202512", + "pageIndex": 1, + "pageSize": 10 + }') +END_TIME=$(date +%s.%3N) + +# 提取HTTP状态码和响应时间 +HTTP_CODE=$(echo "$RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2) +TIME_TOTAL=$(echo "$RESPONSE" | grep "TIME_TOTAL:" | cut -d: -f2) +RESPONSE_BODY=$(echo "$RESPONSE" | sed '/HTTP_CODE:/d' | sed '/TIME_TOTAL:/d') + +echo "响应时间: ${TIME_TOTAL}秒" +echo "HTTP状态码: $HTTP_CODE" +echo "" +echo "响应内容(前500字符):" +echo "$RESPONSE_BODY" | head -c 500 +echo "" +echo "..." + +# 使用RESPONSE_BODY进行后续处理 +RESPONSE="$RESPONSE_BODY" + +# 解析响应 +CODE=$(echo "$RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('code', 0))" 2>/dev/null) +HAS_DATA=$(echo "$RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print('yes' if data.get('data', {}).get('list') else 'no')" 2>/dev/null) + +if [ "$CODE" = "200" ] && [ "$HAS_DATA" = "yes" ]; then + echo "" + echo "✅ 接口调用成功" + + # 显示统计信息 + TOTAL=$(echo "$RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('data', {}).get('pagination', {}).get('total', 0))" 2>/dev/null) + LIST_COUNT=$(echo "$RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(len(data.get('data', {}).get('list', [])))" 2>/dev/null) + + echo "总记录数: $TOTAL" + echo "当前页记录数: $LIST_COUNT" + + # 显示前3条记录的关键信息 + echo "" + echo "前3条记录详情:" + echo "$RESPONSE" | python3 -c " +import sys, json +try: + data = json.load(sys.stdin) + items = data.get('data', {}).get('list', [])[:3] + if items: + for i, item in enumerate(items, 1): + store_name = item.get('StoreName', item.get('storeName', '无')) + total_perf = item.get('TotalPerformance', item.get('totalPerformance', 0)) + actual_perf = item.get('ActualPerformance', item.get('actualPerformance', 0)) + first_count = item.get('FirstOrderCount', item.get('firstOrderCount', 0)) + upgrade_count = item.get('UpgradeOrderCount', item.get('upgradeOrderCount', 0)) + print(f'{i}. 门店: {store_name}, 总业绩: {total_perf:,.2f}, 实际业绩: {actual_perf:,.2f}, 首开单: {first_count}, 升单: {upgrade_count}') + else: + print('未找到数据') +except Exception as e: + print(f'解析错误: {e}') +" 2>/dev/null +else + echo "" + echo "❌ 接口调用失败,返回码: $CODE" +fi + +echo "" +echo "=== 测试完成 ===" + diff --git a/test_tianwang_api.py b/test_tianwang_api.py index ef95298..751945e 100644 --- a/test_tianwang_api.py +++ b/test_tianwang_api.py @@ -81,3 +81,6 @@ print(f"\n教育一部+教育二部合计 BillingPerformance: {total2}") + + + diff --git a/verify_store_total_performance_data.sh b/verify_store_total_performance_data.sh new file mode 100755 index 0000000..9f0f9ca --- /dev/null +++ b/verify_store_total_performance_data.sh @@ -0,0 +1,94 @@ +#!/bin/bash + +# 验证门店总业绩统计数据准确性 + +echo "=== 验证门店总业绩统计数据准确性 ===" + +# 获取Token +TOKEN=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e" | python3 -c "import sys, json; print(json.load(sys.stdin)['data']['token'])" 2>/dev/null) + +if [ -z "$TOKEN" ]; then + echo "❌ 获取Token失败" + exit 1 +fi + +echo "✅ Token获取成功" +echo "" + +# 查询实时数据 +echo "--- 查询实时数据(2025年12月,前5条) ---" +RESPONSE=$(curl -s -X POST "http://localhost:2011/api/Extend/LqStatistics/get-store-total-performance-statistics-list" \ + -H "Authorization: $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"statisticsMonth": "202512", "pageIndex": 1, "pageSize": 5}') + +echo "$RESPONSE" | python3 -c " +import sys, json +data = json.load(sys.stdin) +items = data.get('data', {}).get('list', [])[:5] +print('实时查询结果(前5条):') +print('=' * 100) +for i, item in enumerate(items, 1): + print(f'{i}. {item.get(\"StoreName\", \"无\")}') + print(f' 总业绩: {item.get(\"TotalPerformance\", 0):,.2f}') + print(f' 总单业绩: {item.get(\"TotalOrderPerformance\", 0):,.2f}') + print(f' 实际业绩: {item.get(\"ActualPerformance\", 0):,.2f}') + print(f' 首开单: {item.get(\"FirstOrderCount\", 0)}, 升单: {item.get(\"UpgradeOrderCount\", 0)}') + print(f' 品项数量: {item.get(\"ItemQuantity\", 0)}') + print(f' 退款金额: {item.get(\"RefundAmount\", 0):,.2f}, 退款次数: {item.get(\"RefundCount\", 0)}') + print('') +" 2>/dev/null + +# 验证数据完整性 +echo "--- 数据完整性验证 ---" +TOTAL=$(echo "$RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('data', {}).get('pagination', {}).get('total', 0))" 2>/dev/null) +LIST_COUNT=$(echo "$RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(len(data.get('data', {}).get('list', [])))" 2>/dev/null) + +echo "总记录数: $TOTAL" +echo "当前页记录数: $LIST_COUNT" + +# 检查数据字段完整性 +echo "" +echo "--- 字段完整性检查 ---" +echo "$RESPONSE" | python3 -c " +import sys, json +data = json.load(sys.stdin) +items = data.get('data', {}).get('list', []) +if items: + first_item = items[0] + required_fields = ['StoreId', 'StoreName', 'StatisticsMonth', 'TotalPerformance', + 'TotalOrderPerformance', 'ActualPerformance', 'FirstOrderCount', + 'UpgradeOrderCount', 'ItemQuantity', 'RefundAmount', 'RefundCount'] + missing_fields = [] + for field in required_fields: + if field not in first_item: + missing_fields.append(field) + if missing_fields: + print(f'❌ 缺少字段: {missing_fields}') + else: + print('✅ 所有必需字段都存在') + + # 验证数据逻辑 + print('') + print('--- 数据逻辑验证 ---') + for i, item in enumerate(items[:3], 1): + total_perf = item.get('TotalPerformance', 0) + total_order = item.get('TotalOrderPerformance', 0) + actual = item.get('ActualPerformance', 0) + refund = item.get('RefundAmount', 0) + + # 验证:实际业绩 = 总单业绩 - 退款金额 + expected_actual = total_order - refund + if abs(actual - expected_actual) < 0.01: + print(f'{i}. {item.get(\"StoreName\", \"无\")}: ✅ 实际业绩计算正确') + else: + print(f'{i}. {item.get(\"StoreName\", \"无\")}: ❌ 实际业绩计算错误 (期望: {expected_actual:.2f}, 实际: {actual:.2f})') +else: + print('❌ 未找到数据') +" 2>/dev/null + +echo "" +echo "=== 验证完成 ===" + diff --git a/事业部总经理经理工资计算规则梳理.md b/事业部总经理经理工资计算规则梳理.md index 56de4d7..c863339 100644 --- a/事业部总经理经理工资计算规则梳理.md +++ b/事业部总经理经理工资计算规则梳理.md @@ -13,13 +13,14 @@ 事业部总经理/经理工资由以下几个部分组成: 1. **底薪**:固定4000元 -2. **提成**:根据管理的门店业绩,使用阶梯式提成计算(基于门店总业绩) +2. **提成**:根据管理的门店毛利,使用分段累进式提成计算(基于门店毛利) **重要说明**: - 每个总经理/经理都会管理多个门店 -- 提成计算基于门店总业绩(开单业绩 - 退卡业绩) +- **提成计算基于门店毛利**,而不是开单业绩 +- **必须满足提成阶梯1才能有提成资格** - 总经理和经理的计算规则相同 -- 必须先达到门店生命线才能计算提成 +- 提成计算方式:分段累进式 --- @@ -35,22 +36,17 @@ --- -### 2. 提成规则(阶梯式) +### 2. 提成规则(分段累进式) -**提成计算方式**:根据管理的门店业绩,使用阶梯式提成计算 +**提成计算方式**:根据管理的门店毛利,使用分段累进式提成计算 -#### 2.1 提成门槛:门店生命线 +#### 2.1 提成门槛 -**重要概念**:门店生命线是提成的**门槛条件**,不是提成阶梯 +**重要规则**:**必须满足提成阶梯1才能有提成资格** -- **数据来源**:`lq_md_target` 表的 `F_StoreLifeline` 字段 -- **判断条件**: - - 如果门店业绩 < 门店生命线 → **无提成** - - 如果门店业绩 ≥ 门店生命线 → **可以计算提成** - -**重要说明**: -- 门店生命线是**必须设置的**,未设置应报错 -- 门店生命线是门店级别的指标,用于判断是否达到提成门槛 +- 如果门店毛利 < 提成阶梯1,则无提成(提成 = 0) +- 如果门店毛利 ≥ 提成阶梯1,则可以计算提成 +- 提成阶梯1是提成资格的门槛,不是提成计算的起点 #### 2.2 提成阶梯设置 @@ -74,43 +70,62 @@ - **门店生命线**:判断是否达到提成门槛 - **提成阶梯**:计算提成金额的阶梯 -#### 2.3 阶梯式提成计算规则 +#### 2.3 毛利计算 + +**核心公式**: +``` +毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾 +``` + +其中: +- **销售业绩** = 开单业绩 - 退款业绩 +- **产品物料** = 仓库领用金额合计(注意11月特殊规则:11月工资算10月数据) +- **合作项目成本** = 合作成本表合计金额 +- **店内支出** = 店内支出表合计金额 +- **洗毛巾** = 送洗记录总费用(只统计送出的记录,F_FlowType = 0) + +**重要说明**: +- **提成计算基于毛利**,而不是开单业绩 +- 所有门店的毛利分别计算,然后汇总 + +#### 2.4 分段累进式提成计算规则 -**前提条件**:门店业绩必须 ≥ 门店生命线,否则无提成 +**前提条件**:**必须满足提成阶梯1才能有提成资格** -**计算逻辑**:根据门店业绩落在哪个提成阶梯区间,使用对应的提成比例计算(分段累进) +- 如果门店毛利 < 提成阶梯1,则无提成(提成 = 0) +- 如果门店毛利 ≥ 提成阶梯1,则可以计算提成 + +**计算逻辑**:根据门店毛利落在哪个提成阶梯区间,使用分段累进方式计算(不同区间按不同比例分别计算后累加) **示例**(假设某门店的设置): -- 门店生命线(`lq_md_target.F_StoreLifeline`)= 300,000元 -- 提成阶梯1 = 350,000元,提成比例1 = 1.0% +- 提成阶梯1 = 150,000元,提成比例1 = 1.0% - 提成阶梯2 = 400,000元,提成比例2 = 1.5% - 提成阶梯3 = 450,000元,提成比例3 = 2.0% -**计算规则**(分段累进): - -1. **业绩 < 门店生命线**: - - 提成 = 0(无提成) - - 示例:业绩 = 280,000元 → 提成 = 0 +**计算规则**(分段累进式): -2. **门店生命线 ≤ 业绩 ≤ 提成阶梯1**: - - 提成 = 业绩 × 提成比例1 - - 示例:业绩 = 320,000元 → 提成 = 320,000 × 1.0% = 3,200元 +1. **毛利 < 提成阶梯1**: + - 提成 = 0元(未达到提成资格) + - 示例:毛利 = 100,000元 → 提成 = 0元 -3. **提成阶梯1 < 业绩 ≤ 提成阶梯2**: - - 提成 = 提成阶梯1 × 提成比例1 + (业绩 - 提成阶梯1) × 提成比例2 - - 示例:业绩 = 380,000元 → 提成 = 350,000 × 1.0% + (380,000 - 350,000) × 1.5% = 3,500 + 450 = 3,950元 +2. **提成阶梯1 ≤ 毛利 < 提成阶梯2**: + - 提成 = 提成阶梯1 × 提成比例1 + (毛利 - 提成阶梯1) × 提成比例2 + - 示例:毛利 = 380,000元 → 提成 = 150,000 × 1.0% + (380,000 - 150,000) × 1.5% = 1,500 + 3,450 = 4,950元 -4. **提成阶梯2 < 业绩 ≤ 提成阶梯3**: - - 提成 = 提成阶梯1 × 提成比例1 + (提成阶梯2 - 提成阶梯1) × 提成比例2 + (业绩 - 提成阶梯2) × 提成比例3 - - 示例:业绩 = 420,000元 → 提成 = 350,000 × 1.0% + (400,000 - 350,000) × 1.5% + (420,000 - 400,000) × 2.0% = 3,500 + 750 + 400 = 4,650元 +3. **提成阶梯2 ≤ 毛利 < 提成阶梯3**: + - 提成 = 提成阶梯1 × 提成比例1 + (提成阶梯2 - 提成阶梯1) × 提成比例2 + (毛利 - 提成阶梯2) × 提成比例3 + - 示例:毛利 = 420,000元 → 提成 = 150,000 × 1.0% + (400,000 - 150,000) × 1.5% + (420,000 - 400,000) × 2.0% = 1,500 + 3,750 + 400 = 5,650元 -5. **业绩 > 提成阶梯3**: - - 提成 = 提成阶梯1 × 提成比例1 + (提成阶梯2 - 提成阶梯1) × 提成比例2 + (提成阶梯3 - 提成阶梯2) × 提成比例3 + (业绩 - 提成阶梯3) × 提成比例3 - - 示例:业绩 = 500,000元 → 提成 = 350,000 × 1.0% + (400,000 - 350,000) × 1.5% + (450,000 - 400,000) × 2.0% + (500,000 - 450,000) × 2.0% = 3,500 + 750 + 1,000 + 1,000 = 6,250元 +4. **毛利 ≥ 提成阶梯3**: + - 提成 = 提成阶梯1 × 提成比例1 + (提成阶梯2 - 提成阶梯1) × 提成比例2 + (提成阶梯3 - 提成阶梯2) × 提成比例3 + (毛利 - 提成阶梯3) × 提成比例3 + - 示例:毛利 = 500,000元 → 提成 = 150,000 × 1.0% + (400,000 - 150,000) × 1.5% + (450,000 - 400,000) × 2.0% + (500,000 - 450,000) × 2.0% = 1,500 + 3,750 + 1,000 + 1,000 = 7,250元 -**注意**: +**重要说明**: +- 采用**分段累进式**计算,不同区间按不同比例分别计算后累加 +- **必须满足提成阶梯1才能有提成资格** +- **提成计算基于毛利**,而不是开单业绩 - 如果提成阶梯2或提成阶梯3未设置(为NULL或0),则只使用提成阶梯1计算 -- 如果提成阶梯2设置但提成阶梯3未设置,则业绩超过提成阶梯2的部分按提成比例2计算 +- 如果提成阶梯2设置但提成阶梯3未设置,则毛利 ≥ 提成阶梯2时,超出提成阶梯1的部分按提成比例2计算 #### 2.4 多门店提成汇总 @@ -194,9 +209,13 @@ - 按门店ID、月份、总经理/经理ID查询 - 如果未找到提成阶梯设置,则无法计算提成(应报错或跳过该门店) -### 4. 门店总业绩 +### 4. 门店毛利计算 + +**定义**:门店毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾 -**定义**:门店总业绩 = 开单业绩 - 退卡业绩 +#### 4.1 销售业绩 + +**定义**:销售业绩 = 开单业绩 - 退卡业绩 **数据来源表及字段**: @@ -207,16 +226,57 @@ **计算方式**: ```sql -门店总业绩 = SUM(门店开单实付金额) - SUM(门店退卡金额) +销售业绩 = SUM(门店开单实付金额) - SUM(门店退卡金额) ``` -**过滤条件**: -- 所有表记录必须满足:`F_IsEffective = 1`(有效记录) -- 按统计月份(YYYYMM格式)过滤时间范围 -- 按门店ID(`djmd` 或 `md`)过滤 +#### 4.2 产品物料 + +**数据来源**: +- 表:`lq_inventory_usage`(库存使用记录表) +- 字段:`F_TotalAmount`(合计金额) +- 条件: + - `F_IsEffective = 1`(有效记录) + - `F_StoreId = @StoreId`(门店ID) + - **特殊规则**:11月工资算10月数据 + - 如果计算月份是11月,则查询10月的数据 + - 其他月份正常查询当月数据 + +#### 4.3 合作项目成本 + +**数据来源**: +- 表:`lq_cooperation_cost`(合作成本表) +- 字段:`F_TotalAmount`(合计金额) +- 条件: + - `F_Year = @Year`(年份) + - `F_Month = @MonthStr`(月份,格式为"11",不是"202511") + - `F_StoreId = @StoreId`(门店ID) + - `F_IsEffective = 1`(有效记录) + +#### 4.4 店内支出 + +**数据来源**: +- 表:`lq_store_expense`(店内支出表) +- 字段:`F_Amount`(金额) +- 条件: + - `F_IsEffective = 1`(有效记录) + - `F_StoreId = @StoreId`(门店ID) + - `DATE_FORMAT(F_ExpenseDate, '%Y%m') = @MonthStr`(月份,YYYYMM格式) + +#### 4.5 洗毛巾费用 + +**数据来源**: +- 表:`lq_laundry_flow`(清洗流水表) +- 字段:`F_TotalPrice`(总费用) +- 条件: + - `F_IsEffective = 1`(有效记录) + - `F_FlowType = 0`(只统计送出的记录) + - `F_StoreId = @StoreId`(门店ID) + - 优先使用 `F_SendTime`,如果为空则使用 `F_CreateTime` + - `DATE_FORMAT(COALESCE(F_SendTime, F_CreateTime), '%Y%m') = @MonthStr`(月份,YYYYMM格式) **重要说明**: -- **确认**:提成计算基于门店总业绩(开单 - 退卡) +- **提成计算基于门店毛利**,而不是开单业绩 +- 所有门店的毛利分别计算,然后汇总 --- @@ -269,10 +329,15 @@ - 从 `lq_md_general_manager_lifeline` 表获取每个门店的提成阶梯设置 - 提成阶梯1和提成比例1是必填项,未设置应报错 -4. **获取门店总业绩**: +4. **获取门店毛利**: - 从 `lq_kd_kdjlb` 表统计每个门店的开单业绩(`sfyj`) - 从 `lq_hytk_hytk` 表统计每个门店的退卡业绩(`F_ActualRefundAmount` 或 `tkje`) - - 计算每个门店的总业绩 = 开单业绩 - 退卡业绩 + - 计算销售业绩 = 开单业绩 - 退卡业绩 + - 从 `lq_inventory_usage` 表统计产品物料(注意11月特殊规则) + - 从 `lq_cooperation_cost` 表统计合作项目成本 + - 从 `lq_store_expense` 表统计店内支出 + - 从 `lq_laundry_flow` 表统计洗毛巾费用 + - 计算每个门店的毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾 ### 2. 工资计算 @@ -284,15 +349,22 @@ 2. **遍历该总经理/经理管理的每个门店**: - a. **判断是否达到提成门槛**: - - 获取该门店的生命线(`lq_md_target.F_StoreLifeline`) - - 获取该门店的总业绩 - - 如果门店业绩 < 门店生命线 → 该门店提成 = 0,跳过 - - 如果门店业绩 ≥ 门店生命线 → 继续计算提成 + a. **计算门店毛利**: + - 计算销售业绩 = 开单业绩 - 退卡业绩 + - 统计产品物料(注意11月特殊规则) + - 统计合作项目成本 + - 统计店内支出 + - 统计洗毛巾费用 + - 计算毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾 - b. **计算该门店的提成**(如果达到门槛): + b. **判断是否达到提成资格**: - 获取该门店的提成阶梯设置(`lq_md_general_manager_lifeline`) - - 根据门店业绩和提成阶梯,使用分段累进方式计算提成金额 + - 获取提成阶梯1(`F_Lifeline1`) + - 如果门店毛利 < 提成阶梯1 → 该门店提成 = 0,跳过 + - 如果门店毛利 ≥ 提成阶梯1 → 继续计算提成 + + c. **计算该门店的提成**(如果达到资格): + - 根据门店毛利和提成阶梯,使用分段累进方式计算提成金额 - 累加到总提成 3. **计算最终工资**: @@ -318,10 +390,11 @@ - 如果门店未在 `lq_md_general_manager_lifeline` 表中设置,则无法计算该门店的提成 3. **边界情况**: - - 如果门店没有业绩数据,业绩为0,未达到门店生命线,提成为0 - - 如果门店业绩 < 门店生命线,提成为0 + - 如果门店没有业绩数据,毛利为0,未达到提成阶梯1,提成为0 + - 如果门店毛利 < 提成阶梯1,提成为0(未达到提成资格) - 如果总经理/经理没有管理的门店,总提成为0,应发工资 = 底薪(4000元) - 如果提成阶梯2或提成阶梯3未设置,则只使用提成阶梯1计算 + - 如果门店毛利为负数(成本大于销售业绩),仍按负数计算,但不会产生提成(因为负数 < 提成阶梯1) 4. **计算精度**: - 涉及金额计算时,建议保留2位小数 @@ -329,8 +402,9 @@ 5. **总经理和经理**: - 总经理和经理的计算规则相同 - - 都使用门店生命线来判断是否达到提成门槛 + - 都使用提成阶梯1来判断是否达到提成资格 - 都使用 `lq_md_general_manager_lifeline` 表的提成阶梯来计算提成 + - 都使用毛利作为提成计算的基数 6. **保底工资**: - 暂时不考虑保底工资规则 diff --git a/大项目主管工资计算规则梳理.md b/大项目主管工资计算规则梳理.md index 8b7d337..267aa2a 100644 --- a/大项目主管工资计算规则梳理.md +++ b/大项目主管工资计算规则梳理.md @@ -21,7 +21,8 @@ - 大项目主管从 `BASE_USER` 表获取,岗位字段(`F_GW`)为"主管",组织ID为大项目一部或大项目二部 - 每个大项目主管管理的门店归属在 `lq_md_target` 表中(通过 `F_MajorProjectDepartment` 字段) - 需要统计该大项目主管管理的**所有门店**的总业绩(开单-退卡) -- 提成采用分段方式计算(不是分段累进) +- **只统计医美类型的业绩**(从 `lq_kd_jksyj` 表统计医美类型的健康师业绩,从 `lq_hytk_mx` 表统计医美类型的退卡金额) +- 提成采用分段累进式计算 --- @@ -39,23 +40,31 @@ ### 2. 业绩提成规则 -**提成计算方式**:根据管理的所有门店的总业绩分段计算 +**提成计算方式**:根据管理的所有门店的总业绩**分段累进式**计算 -| 总业绩范围 | 提成比例 | 说明 | -|-----------|---------|------| -| ≤ 50万 | 0% | 无提成 | -| > 50万 且 ≤ 70万 | 1% | 按1%计算提成 | -| > 70万 | 1.5% | 按1.5%计算提成 | +| 业绩区间 | 提成比例 | 说明 | +|---------|---------|------| +| ≤ 50万 | 0% | 无提成资格 | +| > 50万 | 分段计算 | 有提成资格后,0-70万部分按1%,70万以上部分按1.5% | **计算说明**: -- 提成金额 = 总业绩 × 对应提成比例 -- 采用分段方式计算,不同区间按不同比例计算 -- **注意**:不是分段累进,而是整个总业绩按对应比例计算 +- 采用**分段累进式**计算,不同区间按不同比例分别计算后累加 +- **必须大于50万才有提成资格** +- 如果有提成资格后: + - 0-70万部分:1%(整个0-70万部分都按1%计算) + - 70万以上部分:1.5% + +**计算公式(分段累进)**: +``` +如果 总业绩 ≤ 50万:提成 = 0(无提成资格) +如果 50万 < 总业绩 ≤ 70万:提成 = 总业绩 × 1% +如果 总业绩 > 70万:提成 = 70万 × 1% + (总业绩 - 70万) × 1.5% +``` **示例**: -- 总业绩 = 40万 → 提成 = 0(无提成) -- 总业绩 = 60万 → 提成 = 60万 × 1% = 6000元 -- 总业绩 = 80万 → 提成 = 80万 × 1.5% = 12000元 +- 总业绩 = 40万 → 提成 = 0(无提成资格,未达到50万门槛) +- 总业绩 = 60万 → 提成 = 60万 × 1% = 6,000元 +- 总业绩 = 80万 → 提成 = 70万 × 1% + (80万 - 70万) × 1.5% = 7,000 + 1,500 = 8,500元 --- @@ -105,34 +114,28 @@ ### 总业绩统计 -**定义**:该大项目主管管理的所有门店中,开单金额总和减去退卡金额总和 +**定义**:该大项目主管管理的所有门店中,**医美类型**的开单金额总和减去退卡金额总和 + +**重要说明**:**只统计医美类型的业绩** **数据来源表及字段**: | 业绩类型 | 数据表 | 字段 | 说明 | |---------|--------|------|------| -| **开单金额** | `lq_kd_kdjlb` | `sfyj` | 门店开单实付金额 | -| **退卡金额** | `lq_hytk_hytk` | `F_ActualRefundAmount` 或 `tkje` | 门店退卡金额 | +| **开单金额** | `lq_kd_jksyj` | `Jksyj` | 医美类型的健康师业绩(`ItemCategory == "医美"`) | +| **退卡金额** | `lq_hytk_mx` | `Tkje` | 医美类型的退卡金额(`ItemCategory == "医美"`) | **统计逻辑**: -1. **统计开单金额**(按管理的门店筛选): - ```sql - SELECT COALESCE(SUM(billing.sfyj), 0) as BillingAmount - FROM lq_kd_kdjlb billing - WHERE billing.F_IsEffective = 1 - AND billing.djmd IN (@管理的门店ID列表) - AND DATE_FORMAT(billing.kdrq, '%Y%m') = @统计月份 - ``` +1. **统计开单金额**(只统计医美类型): + - 从 `lq_kd_jksyj` 表关联 `lq_kd_kdjlb` 表 + - 条件:`ItemCategory == "医美"`,`F_IsEffective = 1`,`djmd IN (@管理的门店ID列表)`,`Kdrq` 在统计月份范围内 + - 统计 `Jksyj` 字段(健康师业绩)的总和 -2. **统计退卡金额**(按管理的门店筛选): - ```sql - SELECT COALESCE(SUM(refund.F_ActualRefundAmount), 0) as RefundAmount - FROM lq_hytk_hytk refund - WHERE refund.F_IsEffective = 1 - AND refund.djmd IN (@管理的门店ID列表) - AND DATE_FORMAT(refund.tkrq, '%Y%m') = @统计月份 - ``` +2. **统计退卡金额**(只统计医美类型): + - 从 `lq_hytk_mx` 表关联 `lq_hytk_hytk` 表 + - 条件:`ItemCategory == "医美"`,`F_IsEffective = 1`,`md IN (@管理的门店ID列表)`,`Tksj` 在统计月份范围内 + - 统计 `Tkje` 字段(退卡金额)的总和 3. **计算净总业绩**: - 净总业绩 = 开单金额 - 退卡金额 @@ -213,11 +216,11 @@ ### 步骤4:计算提成 -根据总业绩分段计算提成: +根据总业绩分段累进计算提成: -- 如果总业绩 ≤ 50万:提成 = 0(无提成) +- 如果总业绩 ≤ 50万:提成 = 0(无提成资格) - 如果 50万 < 总业绩 ≤ 70万:提成 = 总业绩 × 1% -- 如果总业绩 > 70万:提成 = 总业绩 × 1.5% +- 如果总业绩 > 70万:提成 = 70万 × 1% + (总业绩 - 70万) × 1.5% --- @@ -246,5 +249,5 @@ - 必须从 `lq_md_target` 表获取管理的门店(通过 `F_MajorProjectDepartment` 字段) - 必须正确统计总业绩(开单金额 - 退卡金额) - 必须按管理的门店筛选,只统计该大项目主管管理的门店 -- 采用分段方式计算提成(不是分段累进),整个总业绩按对应比例计算 +- 采用**分段累进式**计算提成,不同区间按不同比例分别计算后累加 diff --git a/年度汇总表建表.sql b/年度汇总表建表.sql new file mode 100644 index 0000000..2af61fd --- /dev/null +++ b/年度汇总表建表.sql @@ -0,0 +1,21 @@ +CREATE TABLE `lq_annual_summary` ( + `F_Id` varchar(50) NOT NULL COMMENT '主键', + `F_StoreId` varchar(50) NOT NULL COMMENT '门店ID', + `F_StoreName` varchar(100) DEFAULT NULL COMMENT '门店名称', + `F_BusinessUnitId` varchar(50) DEFAULT NULL COMMENT '归属事业部ID', + `F_BusinessUnitName` varchar(100) DEFAULT NULL COMMENT '归属事业部名称', + `F_Year` int(11) NOT NULL COMMENT '年份', + `F_Month` int(11) NOT NULL COMMENT '月份', + `F_TotalPerformance` decimal(18,2) DEFAULT '0.00' COMMENT '总业绩', + `F_TotalConsume` decimal(18,2) DEFAULT '0.00' COMMENT '总消耗', + `F_HeadCount` decimal(18,2) DEFAULT '0.00' COMMENT '人头数', + `F_PersonTime` decimal(18,2) DEFAULT '0.00' COMMENT '人次数', + `F_ProjectCount` decimal(18,2) DEFAULT '0.00' COMMENT '项目数', + `F_CreateTime` datetime DEFAULT NULL COMMENT '创建时间', + `F_CreateUser` varchar(50) DEFAULT NULL COMMENT '创建人', + `F_UpdateTime` datetime DEFAULT NULL COMMENT '更新时间', + `F_UpdateUser` varchar(50) DEFAULT NULL COMMENT '更新人', + `F_IsEffective` int(11) DEFAULT '1' COMMENT '是否有效 0无效 1有效', + PRIMARY KEY (`F_Id`), + UNIQUE KEY `UK_Store_Year_Month` (`F_StoreId`,`F_Year`,`F_Month`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='年度汇总表'; diff --git a/店助主任工资计算规则梳理.md b/店助主任工资计算规则梳理.md index 56ae0b9..f5a3b7a 100644 --- a/店助主任工资计算规则梳理.md +++ b/店助主任工资计算规则梳理.md @@ -26,43 +26,63 @@ ### 2. 提成规则 -**计算公式**:根据门店业绩与门店生命线的比例确定提成比例,使用阶梯提成模式计算 +**计算公式**:分段式提成模式,根据门店业绩与门店生命线的比例分段计算提成 -#### 提成比例规则 +#### 提成前提条件 -| 门店业绩范围 | 提成计算方式 | -|------------|------------| -| 门店业绩 < 门店生命线 × 70% | 0%(无提成) | -| 门店生命线 × 70% ≤ 门店业绩 < 门店生命线 × 100% | 0.4%(全部业绩按0.4%计算) | -| 门店业绩 ≥ 门店生命线 × 100% | **阶梯提成**:- 超过生命线部分:1%- 剩余部分(≤生命线):0.6% | +- **必须达到70%**:门店业绩必须达到门店生命线的70%,否则无提成 -#### 提成计算示例 +#### 提成比例规则(分段式) -**示例1:业绩未达到70%** -- 门店生命线 = 100,000元 -- 门店业绩 = 60,000元 -- 计算:60,000 < 100,000 × 70% = 70,000 -- 提成金额 = 0元(无提成) - -**示例2:业绩在70%-100%之间** -- 门店生命线 = 100,000元 -- 门店业绩 = 85,000元 -- 计算:70,000 ≤ 85,000 < 100,000 -- 提成金额 = 85,000 × 0.4% = 340元 - -**示例3:业绩超过100%(阶梯提成)** -- 门店生命线 = 100,000元 -- 门店业绩 = 150,000元 -- 计算过程: - 1. 业绩(150,000)> 生命线(100,000) - 2. ≤生命线部分:100,000 × 0.6% = 600元 - 3. >生命线部分:(150,000 - 100,000) × 1% = 50,000 × 1% = 500元 - 4. 总提成 = 600 + 500 = 1,100元 +| 业绩区间 | 提成比例 | 计算说明 | +|---------|---------|---------| +| 门店业绩 < 门店生命线 × 70% | 0% | 无提成 | +| 门店生命线 × 70% ≤ 门店业绩 < 门店生命线 × 100% | 0.4% | 整个业绩按0.4%计算 | +| 门店业绩 ≥ 门店生命线 × 100% | 0.4% + 1.6% | **分段式**:- 0-100%部分(整个生命线):0.4%- 100%以上部分:1.6% | **重要说明**: -- 门店生命线必须在 `lq_md_target` 表中进行设置,如果未设置,系统应报错提示 -- 当业绩超过生命线时,必须使用阶梯提成模式,不能使用单一提成比例 -- 提成按门店业绩的百分比计算 +- 店助主任和店助使用**相同的分段式提成规则**,但100%以上部分比例不同 +- 店助:100%以上部分按0.6% +- 店助主任:100%以上部分按1.6% +- 提成采用**分段式**计算方式,不同区间按不同比例分别计算后累加 +- 前提条件:必须达到70%才有提成资格 +- **分段式**:业绩超过100%时,0-100%部分和100%以上部分分别按不同比例计算 + +#### 提成计算示例 + +**示例1:业绩在70%-100%之间** + +假设: +- 门店生命线 = 200,000元 +- 门店业绩 = 147,685.20元 + +计算过程: +1. 判断比例:147,685.20 / 200,000 = 73.84%(≥ 70%,满足前提条件) +2. 判断区间:70% ≤ 73.84% < 100% +3. 计算:整个业绩按0.4%计算 + - 提成金额:147,685.20元 × 0.4% = 590.74元 +4. 总提成金额:590.74元 + +**示例2:业绩超过100%(分段式)** + +假设: +- 门店生命线 = 320,000元 +- 门店业绩 = 408,593.42元 + +计算过程: +1. 判断比例:408,593.42 / 320,000 = 127.69%(≥ 70%,满足前提条件) +2. 判断区间:≥ 100% +3. **分段式计算**: + - 0-100%部分(整个生命线):320,000元 × 0.4% = 1,280元 + - 100%以上部分:(408,593.42 - 320,000)元 × 1.6% = 88,593.42元 × 1.6% = 1,417.49元 +4. 总提成金额:1,280元 + 1,417.49元 = 2,697.49元 + +**计算公式**: +``` +如果 业绩 < 70%:提成 = 0 +如果 70% ≤ 业绩 < 100%:提成 = 业绩 × 0.4% +如果 业绩 ≥ 100%:提成 = 生命线 × 0.4% + (业绩 - 生命线) × 1.6% +``` --- @@ -140,7 +160,11 @@ 4. **提成比例判断**: - 严格按照门店业绩与门店生命线的比例判断 - 注意边界值:70% 和 100% - - **重要**:当业绩超过生命线时,必须使用阶梯提成模式(超过部分1%,剩余部分0.6%) + - **重要**:店助主任和店助使用**相同的分段式提成规则**,但100%以上部分比例不同 + - **重要**:当业绩超过100%时,使用**分段式**提成模式: + - 0-100%部分(整个生命线)按0.4%计算 + - 100%以上部分:店助按0.6%计算,店助主任按1.6%计算 + - **分段式说明**:不同区间按不同比例分别计算后累加 5. **数据一致性**: - 门店业绩的计算逻辑必须与门店总业绩统计保持一致 @@ -423,20 +447,24 @@ LIMIT 1 | 岗位 | 业绩 ≥ 100%时的提成计算 | |-----|---------------------| -| 店助 | 全部业绩按 0.6% 计算 | -| 店助主任 | **阶梯提成**:- 超过生命线部分:1%- 剩余部分(≤生命线):0.6% | +| 店助 | 分段式:- 0-100%部分(整个生命线):0.4%- 100%以上部分:0.6% | +| 店助主任 | **分段式**:- 0-100%部分(整个生命线):0.4%- 100%以上部分:1.6% | -**示例对比**: -- 门店生命线 = 100,000元 -- 门店业绩 = 150,000元 - -**店助提成**: -- 150,000 × 0.6% = 900元 +**重要说明**:店助主任和店助使用**相同的分段式提成规则**,但100%以上部分比例不同 -**店助主任提成**: -- ≤生命线部分:100,000 × 0.6% = 600元 -- >生命线部分:(150,000 - 100,000) × 1% = 500元 -- 总提成 = 600 + 500 = 1,100元 +**示例对比**: +- 门店生命线 = 320,000元 +- 门店业绩 = 408,593.42元 + +**店助提成(分段式)**: +- 0-100%部分:320,000 × 0.4% = 1,280元 +- 100%以上部分:(408,593.42 - 320,000) × 0.6% = 531.56元 +- 总提成 = 1,280 + 531.56 = 1,811.56元 + +**店助主任提成(分段式)**: +- 0-100%部分:320,000 × 0.4% = 1,280元 +- 100%以上部分:(408,593.42 - 320,000) × 1.6% = 1,417.49元 +- 总提成 = 1,280 + 1,417.49 = 2,697.49元 ### 3. 阶段奖励规则 diff --git a/科技部总经理工资计算规则梳理.md b/科技部总经理工资计算规则梳理.md index f598d1c..d7cb330 100644 --- a/科技部总经理工资计算规则梳理.md +++ b/科技部总经理工资计算规则梳理.md @@ -121,51 +121,46 @@ ### 溯源金额统计 -**定义**:该科技部总经理管理的所有门店中,品项的 `F_BeautyType` 为 "溯源系统" 或 "溯源" 的品项明细的实付金额总和(开单金额 - 退卡金额) +**定义**:该科技部总经理管理的所有门店中,品项的 `F_BeautyType` 为 "溯源系统" 或 "溯源" 的健康师业绩总和(开单金额 - 退卡金额) + +**重要说明**: +- **数据来源**:从健康师业绩表(`lq_kd_jksyj`)和退卡健康师业绩表(`lq_hytk_jksyj`)统计 +- **统计依据**:使用健康师业绩表中的业绩金额,而不是开单品项明细表的实付金额 +- **原因**:健康师业绩表中的金额是经过分配和计算的准确业绩,更符合业务逻辑 **数据来源表及字段**: | 数据表 | 字段 | 说明 | |--------|------|------| -| `lq_kd_pxmx` | `F_ActualPrice` | 品项明细实付金额 | -| `lq_kd_pxmx` | `F_BeautyType` | 科美类型(用于区分溯源和Cell) | -| `lq_kd_kdjlb` | `kdrq` | 开单日期(用于按月统计) | -| `lq_kd_kdjlb` | `djmd` | 单据门店ID(用于筛选管理的门店) | -| `lq_xmzl` | `F_BeautyType` | 品项的科美类型(如果明细表没有,从品项表获取) | -| `lq_hytk_mx` | `tkje` | 退卡明细退款金额 | -| `lq_hytk_hytk` | `tkrq` | 退卡日期(用于按月统计) | -| `lq_hytk_hytk` | `djmd` | 单据门店ID(用于筛选管理的门店) | +| `lq_kd_jksyj` | `jksyj` | 健康师业绩(开单业绩,字符串类型,需转换为decimal) | +| `lq_kd_jksyj` | `F_BeautyType` | 科美类型(用于区分溯源和Cell) | +| `lq_kd_jksyj` | `F_StoreId` | 门店ID(用于筛选管理的门店) | +| `lq_kd_jksyj` | `yjsj` | 业绩时间(用于按月统计) | +| `lq_hytk_jksyj` | `jksyj` | 健康师业绩(退卡业绩,decimal类型) | +| `lq_hytk_jksyj` | `F_BeautyType` | 科美类型(用于区分溯源和Cell) | +| `lq_hytk_jksyj` | `F_StoreId` | 门店ID(用于筛选管理的门店) | +| `lq_hytk_jksyj` | `tksj` | 退卡时间(用于按月统计) | **统计逻辑**: 1. **统计开单溯源金额**(按管理的门店筛选): ```sql - SELECT COALESCE(SUM(pxmx.F_ActualPrice), 0) as TraceabilityAmount - FROM lq_kd_pxmx pxmx - INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id - INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id - WHERE pxmx.F_IsEffective = 1 - AND billing.F_IsEffective = 1 - AND item.F_IsEffective = 1 - AND (pxmx.F_BeautyType = '溯源系统' OR pxmx.F_BeautyType = '溯源' - OR item.F_BeautyType = '溯源系统' OR item.F_BeautyType = '溯源') - AND billing.djmd IN (@管理的门店ID列表) - AND DATE_FORMAT(billing.kdrq, '%Y%m') = @统计月份 + SELECT COALESCE(SUM(CAST(jksyj AS DECIMAL(18,2))), 0) as TraceabilityAmount + FROM lq_kd_jksyj + WHERE F_IsEffective = 1 + AND F_StoreId IN (@管理的门店ID列表) + AND (F_BeautyType = '溯源系统' OR F_BeautyType = '溯源') + AND DATE_FORMAT(yjsj, '%Y%m') = @统计月份 ``` 2. **统计退卡溯源金额**(按管理的门店筛选): ```sql - SELECT COALESCE(SUM(tkmx.tkje), 0) as RefundTraceabilityAmount - FROM lq_hytk_mx tkmx - INNER JOIN lq_hytk_hytk refund ON tkmx.glhytkbh = refund.F_Id - INNER JOIN lq_xmzl item ON tkmx.px = item.F_Id - WHERE tkmx.F_IsEffective = 1 - AND refund.F_IsEffective = 1 - AND item.F_IsEffective = 1 - AND (tkmx.F_BeautyType = '溯源系统' OR tkmx.F_BeautyType = '溯源' - OR item.F_BeautyType = '溯源系统' OR item.F_BeautyType = '溯源') - AND refund.djmd IN (@管理的门店ID列表) - AND DATE_FORMAT(refund.tkrq, '%Y%m') = @统计月份 + SELECT COALESCE(SUM(jksyj), 0) as RefundTraceabilityAmount + FROM lq_hytk_jksyj + WHERE F_IsEffective = 1 + AND F_StoreId IN (@管理的门店ID列表) + AND (F_BeautyType = '溯源系统' OR F_BeautyType = '溯源') + AND DATE_FORMAT(tksj, '%Y%m') = @统计月份 ``` 3. **计算净溯源金额**: @@ -175,7 +170,12 @@ ### Cell金额统计 -**定义**:该科技部总经理管理的所有门店中,品项的 `F_BeautyType` 为 "cell" 或 "Cell" 的品项明细的实付金额总和(开单金额 - 退卡金额) +**定义**:该科技部总经理管理的所有门店中,品项的 `F_BeautyType` 为 "cell" 或 "Cell" 的健康师业绩总和(开单金额 - 退卡金额) + +**重要说明**: +- **数据来源**:从健康师业绩表(`lq_kd_jksyj`)和退卡健康师业绩表(`lq_hytk_jksyj`)统计 +- **统计依据**:使用健康师业绩表中的业绩金额,而不是开单品项明细表的实付金额 +- **原因**:健康师业绩表中的金额是经过分配和计算的准确业绩,更符合业务逻辑 **数据来源表及字段**:同溯源金额统计 @@ -183,32 +183,22 @@ 1. **统计开单Cell金额**(按管理的门店筛选): ```sql - SELECT COALESCE(SUM(pxmx.F_ActualPrice), 0) as CellAmount - FROM lq_kd_pxmx pxmx - INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id - INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id - WHERE pxmx.F_IsEffective = 1 - AND billing.F_IsEffective = 1 - AND item.F_IsEffective = 1 - AND (pxmx.F_BeautyType = 'cell' OR pxmx.F_BeautyType = 'Cell' - OR item.F_BeautyType = 'cell' OR item.F_BeautyType = 'Cell') - AND billing.djmd IN (@管理的门店ID列表) - AND DATE_FORMAT(billing.kdrq, '%Y%m') = @统计月份 + SELECT COALESCE(SUM(CAST(jksyj AS DECIMAL(18,2))), 0) as CellAmount + FROM lq_kd_jksyj + WHERE F_IsEffective = 1 + AND F_StoreId IN (@管理的门店ID列表) + AND (F_BeautyType = 'cell' OR F_BeautyType = 'Cell') + AND DATE_FORMAT(yjsj, '%Y%m') = @统计月份 ``` 2. **统计退卡Cell金额**(按管理的门店筛选): ```sql - SELECT COALESCE(SUM(tkmx.tkje), 0) as RefundCellAmount - FROM lq_hytk_mx tkmx - INNER JOIN lq_hytk_hytk refund ON tkmx.glhytkbh = refund.F_Id - INNER JOIN lq_xmzl item ON tkmx.px = item.F_Id - WHERE tkmx.F_IsEffective = 1 - AND refund.F_IsEffective = 1 - AND item.F_IsEffective = 1 - AND (tkmx.F_BeautyType = 'cell' OR tkmx.F_BeautyType = 'Cell' - OR item.F_BeautyType = 'cell' OR item.F_BeautyType = 'Cell') - AND refund.djmd IN (@管理的门店ID列表) - AND DATE_FORMAT(refund.tkrq, '%Y%m') = @统计月份 + SELECT COALESCE(SUM(jksyj), 0) as RefundCellAmount + FROM lq_hytk_jksyj + WHERE F_IsEffective = 1 + AND F_StoreId IN (@管理的门店ID列表) + AND (F_BeautyType = 'cell' OR F_BeautyType = 'Cell') + AND DATE_FORMAT(tksj, '%Y%m') = @统计月份 ``` 3. **计算净Cell金额**: @@ -310,30 +300,22 @@ ### 步骤4:统计Cell金额(所有管理的门店总和) 1. **统计开单Cell金额**(按管理的门店筛选): - - 从 `lq_kd_pxmx` 表统计 - - 关联 `lq_kd_kdjlb` 表获取开单日期和门店ID - - 关联 `lq_xmzl` 表获取品项的 `F_BeautyType` + - 从 `lq_kd_jksyj` 表(健康师业绩表)统计 - 筛选条件: - - `lq_kd_pxmx.F_IsEffective = 1`(有效记录) - - `lq_kd_kdjlb.F_IsEffective = 1`(有效开单) - - `lq_xmzl.F_IsEffective = 1`(有效品项) + - `F_IsEffective = 1`(有效记录) + - `F_StoreId IN (@管理的门店ID列表)` - `F_BeautyType = 'cell'` 或 `'Cell'` - - `lq_kd_kdjlb.djmd IN (@管理的门店ID列表)` - - 开单日期在统计月份范围内 - - 汇总:`SUM(lq_kd_pxmx.F_ActualPrice)` + - 业绩时间(`yjsj`)在统计月份范围内 + - 汇总:`SUM(CAST(jksyj AS DECIMAL(18,2)))`(注意:`jksyj`字段是字符串类型,需要转换为decimal) 2. **统计退卡Cell金额**(按管理的门店筛选): - - 从 `lq_hytk_mx` 表统计 - - 关联 `lq_hytk_hytk` 表获取退卡日期和门店ID - - 关联 `lq_xmzl` 表获取品项的 `F_BeautyType` + - 从 `lq_hytk_jksyj` 表(退卡健康师业绩表)统计 - 筛选条件: - - `lq_hytk_mx.F_IsEffective = 1`(有效记录) - - `lq_hytk_hytk.F_IsEffective = 1`(有效退卡) - - `lq_xmzl.F_IsEffective = 1`(有效品项) + - `F_IsEffective = 1`(有效记录) + - `F_StoreId IN (@管理的门店ID列表)` - `F_BeautyType = 'cell'` 或 `'Cell'` - - `lq_hytk_hytk.djmd IN (@管理的门店ID列表)` - - 退卡日期在统计月份范围内 - - 汇总:`SUM(lq_hytk_mx.tkje)` + - 退卡时间(`tksj`)在统计月份范围内 + - 汇总:`SUM(jksyj)`(注意:`jksyj`字段是decimal类型) 3. **计算净Cell金额**: - 净Cell金额 = 开单Cell金额 - 退卡Cell金额 diff --git a/科技部老师工资计算规则.md b/科技部老师工资计算规则.md index 6530051..0e9ce44 100644 --- a/科技部老师工资计算规则.md +++ b/科技部老师工资计算规则.md @@ -29,27 +29,36 @@ ### 2. 业绩提成规则 -业绩提成基于**总业绩**计算,采用阶梯式方式: +业绩提成基于**总业绩**计算,采用**分段累进式**方式: **提成前提**:业绩必须大于1万才能进行提成 -| 总业绩范围 | 提成比例 | 说明 | -|-----------|---------|------| -| ≤ 10,000元 | 0% | 无提成 | -| > 10,000元 且 ≤ 70,000元 | 2% | 整个业绩按2%计算 | -| > 70,000元 且 ≤ 150,000元 | 2.5% | 整个业绩按2.5%计算 | -| > 150,000元 | 3% | 整个业绩按3%计算 | +| 业绩区间 | 提成比例 | 说明 | +|---------|---------|------| +| ≤ 10,000元 | 0% | 无提成资格 | +| > 10,000元 | 分段计算 | 有提成资格后,0-7万部分按2%,7万-15万部分按2.5%,15万以上部分按3% | **计算说明**: -- 提成金额 = 总业绩 × 对应提成比例 -- 采用阶梯式计算,整个业绩按对应区间的比例计算(不是分段累进) -- 业绩必须大于1万才有提成资格 +- 采用**分段累进式**计算,不同区间按不同比例分别计算后累加 +- **必须大于1万才有提成资格** +- 如果有提成资格后: + - 0-7万部分:2%(整个0-7万部分都按2%计算) + - 7万-15万部分:2.5% + - 15万以上部分:3% + +**计算公式(分段累进)**: +``` +如果 业绩 ≤ 1万:提成 = 0(无提成资格) +如果 1万 < 业绩 ≤ 7万:提成 = 业绩 × 2% +如果 7万 < 业绩 ≤ 15万:提成 = 7万 × 2% + (业绩 - 7万) × 2.5% +如果 业绩 > 15万:提成 = 7万 × 2% + (15万 - 7万) × 2.5% + (业绩 - 15万) × 3% +``` **示例**: -- 总业绩 = 5,000元 → 提成 = 0(无提成,未达到1万门槛) +- 总业绩 = 5,000元 → 提成 = 0(无提成资格,未达到1万门槛) - 总业绩 = 50,000元 → 提成 = 50,000 × 2% = 1,000元 -- 总业绩 = 100,000元 → 提成 = 100,000 × 2.5% = 2,500元 -- 总业绩 = 200,000元 → 提成 = 200,000 × 3% = 6,000元 +- 总业绩 = 100,000元 → 提成 = 70,000 × 2% + (100,000 - 70,000) × 2.5% = 1,400 + 750 = 2,150元 +- 总业绩 = 200,000元 → 提成 = 70,000 × 2% + (150,000 - 70,000) × 2.5% + (200,000 - 150,000) × 3% = 1,400 + 2,000 + 1,500 = 4,900元 --- @@ -198,12 +207,12 @@ WHERE lq_xh_kjbsyj.F_IsEffective = 1 - 都不满足 → 默认第一档 2,500元 #### 4.2 计算业绩提成 -- 根据总业绩范围确定提成比例: - - < 10,000元 → 0% - - 10,000-70,000元 → 2% - - 70,000-150,000元 → 2.5% - - > 150,000元 → 3% -- 业绩提成金额 = 总业绩 × 提成比例 +- 前提条件:业绩必须大于1万才有提成资格 +- 如果有提成资格后,分段计算: + - 0-7万部分:2%(整个0-7万部分都按2%计算) + - 7万-15万部分:2.5% + - 15万以上部分:3% +- 业绩提成金额按分段累进式计算 #### 4.3 计算消耗提成 - 根据消耗业绩范围确定提成规则: