You need to sign in before continuing.

Commit 1fffa8765e003b2ef73d283fcc1570e1e97dca5a

Authored by “wangming”
1 parent 0a403c72

优化储扣列表接口:增加门店、时间、品项分类筛选,并添加统计数据查询

- 储扣列表接口增加门店筛选、时间筛选、品项分类筛选功能
- 统计查询改为单独查询,避免复杂JOIN导致的问题
- 返回结果包含总记录数、总金额、总项目数统计信息
- 优化查询性能,先获取符合条件的开单记录ID,再筛选储扣记录
Showing 56 changed files with 8689 additions and 582 deletions
antis-ncc-admin/.env.development
... ... @@ -2,8 +2,8 @@
2 2  
3 3 VUE_CLI_BABEL_TRANSPILE_MODULES = true
4 4 # VUE_APP_BASE_API = 'https://erp.lvqianmeiye.com'
5   -VUE_APP_BASE_API = 'http://erp_test.lvqianmeiye.com'
6   -# VUE_APP_BASE_API = 'http://localhost:2011'
  5 +# VUE_APP_BASE_API = 'http://erp_test.lvqianmeiye.com'
  6 +VUE_APP_BASE_API = 'http://localhost:2011'
7 7 # VUE_APP_BASE_API = 'http://localhost:2011'
8 8 VUE_APP_IMG_API = ''
9 9 VUE_APP_BASE_WSS = 'ws://192.168.110.45:2011/websocket'
... ...
antis-ncc-admin/src/api/extend/annualSummary.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +
  3 +// 获取年度汇总列表
  4 +export function getList(params) {
  5 + return request({
  6 + url: '/api/Extend/LqAnnualSummary/list',
  7 + method: 'get',
  8 + params
  9 + })
  10 +}
  11 +
  12 +// 保存(新增或更新)
  13 +export function save(data) {
  14 + return request({
  15 + url: '/api/Extend/LqAnnualSummary/save',
  16 + method: 'post',
  17 + data
  18 + })
  19 +}
  20 +
  21 +// 删除
  22 +export function del(data) {
  23 + return request({
  24 + url: '/api/Extend/LqAnnualSummary/delete',
  25 + method: 'post',
  26 + data
  27 + })
  28 +}
  29 +
  30 +// 导入
  31 +export function importData(data) {
  32 + return request({
  33 + url: '/api/Extend/LqAnnualSummary/import',
  34 + method: 'post',
  35 + data
  36 + })
  37 +}
  38 +
  39 +// ========== 统计接口 ==========
  40 +
  41 +// 4.1 全年门店业绩表
  42 +export function getTotalPerformanceStat(data) {
  43 + return request({
  44 + url: '/api/Extend/LqAnnualSummary/GetTotalPerformanceStat',
  45 + method: 'post',
  46 + data
  47 + })
  48 +}
  49 +
  50 +// 4.2 全年门店消耗表
  51 +export function getTotalConsumeStat(data) {
  52 + return request({
  53 + url: '/api/Extend/LqAnnualSummary/GetTotalConsumeStat',
  54 + method: 'post',
  55 + data
  56 + })
  57 +}
  58 +
  59 +// 4.3 年度门店人头表
  60 +export function getHeadCountStat(data) {
  61 + return request({
  62 + url: '/api/Extend/LqAnnualSummary/GetHeadCountStat',
  63 + method: 'post',
  64 + data
  65 + })
  66 +}
  67 +
  68 +// 4.4 年度门店项目数表
  69 +export function getProjectCountStat(data) {
  70 + return request({
  71 + url: '/api/Extend/LqAnnualSummary/GetProjectCountStat',
  72 + method: 'post',
  73 + data
  74 + })
  75 +}
  76 +
  77 +// 4.5 年度门店人次表
  78 +export function getPersonTimeStat(params) {
  79 + return request({
  80 + url: '/api/Extend/LqAnnualSummary/GetPersonTimeStat',
  81 + method: 'get',
  82 + params
  83 + })
  84 +}
  85 +
  86 +// 通用月度趋势统计
  87 +export function getMonthlyTrend(params) {
  88 + return request({
  89 + url: '/api/Extend/LqAnnualSummary/GetMonthlyTrend',
  90 + method: 'get',
  91 + params
  92 + })
  93 +}
  94 +
  95 +// 4.6 门店五项指标统计图
  96 +export function getStoreIndicatorsStat(data) {
  97 + return request({
  98 + url: '/api/Extend/LqAnnualSummary/GetStoreIndicatorsStat',
  99 + method: 'post',
  100 + data
  101 + })
  102 +}
  103 +
  104 +// 获取门店指标详情
  105 +export function getStoreIndicatorDetail(params) {
  106 + return request({
  107 + url: '/api/Extend/LqAnnualSummary/GetStoreIndicatorDetail',
  108 + method: 'get',
  109 + params
  110 + })
  111 +}
  112 +
  113 +// 4.7 事业部五项指标总计图
  114 +export function getBusinessUnitIndicatorsStat(params) {
  115 + return request({
  116 + url: '/api/Extend/LqAnnualSummary/GetBusinessUnitIndicatorsStat',
  117 + method: 'get',
  118 + params
  119 + })
  120 +}
  121 +
  122 +// 4.8 事业部内部汇总 (宽表)
  123 +export function getBusinessUnitSummaryStat(data) {
  124 + return request({
  125 + url: '/api/Extend/LqAnnualSummary/GetBusinessUnitSummaryStat',
  126 + method: 'post',
  127 + data
  128 + })
  129 +}
... ...
antis-ncc-admin/src/utils/request.js
... ... @@ -29,7 +29,9 @@ service.interceptors.request.use(
29 29 if (store.getters.token) {
30 30 config.headers['Authorization'] = getToken()
31 31 }
32   - if (config.method == 'get') {
  32 + // GET 请求时,如果传入了 data 但没有 params,则将其转换为 params
  33 + // 如果已经有 params,则保留 params,不覆盖
  34 + if (config.method == 'get' && config.data && !config.params) {
33 35 config.params = config.data
34 36 }
35 37 let timestamp = Date.parse(new Date()) / 1000
... ...
antis-ncc-admin/src/views/extend/annualSummary/dashboard/index.vue 0 → 100644
  1 +<template>
  2 + <div class="annual-summary-dashboard">
  3 + <div class="page-header">
  4 + <div class="header-content">
  5 + <div class="header-title">
  6 + <i class="el-icon-data-analysis"></i>
  7 + <span>年度经营统计分析</span>
  8 + </div>
  9 + <div class="header-filters">
  10 + <el-form :inline="true" :model="query" class="filter-form">
  11 + <el-form-item label="年度">
  12 + <el-date-picker
  13 + v-model="query.year"
  14 + type="year"
  15 + value-format="yyyy"
  16 + placeholder="选择年度"
  17 + clearable
  18 + @change="handleQueryChange"
  19 + style="width: 150px"
  20 + />
  21 + </el-form-item>
  22 + <el-form-item label="门店名称">
  23 + <el-select
  24 + v-model="query.storeName"
  25 + placeholder="请选择门店"
  26 + clearable
  27 + filterable
  28 + @change="handleQueryChange"
  29 + style="width: 200px"
  30 + >
  31 + <el-option
  32 + v-for="store in storeOptions"
  33 + :key="store.id"
  34 + :label="store.fullName"
  35 + :value="store.fullName"
  36 + />
  37 + </el-select>
  38 + </el-form-item>
  39 + <el-form-item>
  40 + <el-button type="primary" icon="el-icon-search" @click="handleQueryChange">查询</el-button>
  41 + <el-button icon="el-icon-refresh-right" @click="handleReset">重置</el-button>
  42 + </el-form-item>
  43 + </el-form>
  44 + </div>
  45 + </div>
  46 + </div>
  47 +
  48 + <div class="dashboard-content">
  49 + <!-- 左侧目录导航 -->
  50 + <div class="sidebar-nav">
  51 + <div class="nav-title">
  52 + <i class="el-icon-menu"></i>
  53 + <span>统计目录</span>
  54 + </div>
  55 + <el-menu
  56 + :default-active="activeMenu"
  57 + class="nav-menu"
  58 + @select="handleMenuSelect"
  59 + :collapse="false"
  60 + >
  61 + <el-menu-item index="monthly-trend">
  62 + <i class="el-icon-data-line"></i>
  63 + <span slot="title">月度趋势分析</span>
  64 + </el-menu-item>
  65 + <el-menu-item index="performance-stat">
  66 + <i class="el-icon-trophy"></i>
  67 + <span slot="title">全年门店业绩表</span>
  68 + </el-menu-item>
  69 + <el-menu-item index="consume-stat">
  70 + <i class="el-icon-shopping-cart-2"></i>
  71 + <span slot="title">全年门店消耗表</span>
  72 + </el-menu-item>
  73 + <el-menu-item index="headcount-stat">
  74 + <i class="el-icon-user"></i>
  75 + <span slot="title">年度门店人头表</span>
  76 + </el-menu-item>
  77 + <el-menu-item index="persontime-stat">
  78 + <i class="el-icon-user-solid"></i>
  79 + <span slot="title">年度门店人次表</span>
  80 + </el-menu-item>
  81 + <el-menu-item index="project-stat">
  82 + <i class="el-icon-goods"></i>
  83 + <span slot="title">年度门店项目数表</span>
  84 + </el-menu-item>
  85 + <el-menu-item index="store-indicators">
  86 + <i class="el-icon-pie-chart"></i>
  87 + <span slot="title">门店五项指标统计</span>
  88 + </el-menu-item>
  89 + <el-menu-item index="bu-indicators">
  90 + <i class="el-icon-s-data"></i>
  91 + <span slot="title">事业部五项指标统计</span>
  92 + </el-menu-item>
  93 + <el-menu-item index="bu-summary">
  94 + <i class="el-icon-tickets"></i>
  95 + <span slot="title">事业部内部汇总</span>
  96 + </el-menu-item>
  97 + </el-menu>
  98 + </div>
  99 +
  100 + <!-- 右侧内容区域 -->
  101 + <div class="main-content">
  102 + <!-- 月度趋势分析 -->
  103 + <div v-show="activeMenu === 'monthly-trend'" class="content-panel">
  104 + <div class="panel-header">
  105 + <i class="el-icon-data-line"></i>
  106 + <span>月度趋势分析</span>
  107 + </div>
  108 + <div class="panel-body">
  109 + <el-row :gutter="20">
  110 + <el-col :span="12">
  111 + <el-card shadow="never" class="chart-card">
  112 + <div slot="header" class="card-header">
  113 + <i class="el-icon-trophy"></i>
  114 + <span>业绩走势对比</span>
  115 + </div>
  116 + <div id="perfTrend" class="chart-box"></div>
  117 + </el-card>
  118 + </el-col>
  119 + <el-col :span="12">
  120 + <el-card shadow="never" class="chart-card">
  121 + <div slot="header" class="card-header">
  122 + <i class="el-icon-shopping-cart-2"></i>
  123 + <span>消耗走势对比</span>
  124 + </div>
  125 + <div id="consumeTrend" class="chart-box"></div>
  126 + </el-card>
  127 + </el-col>
  128 + </el-row>
  129 + <el-row :gutter="20" style="margin-top: 20px">
  130 + <el-col :span="8">
  131 + <el-card shadow="never" class="chart-card">
  132 + <div slot="header" class="card-header">
  133 + <i class="el-icon-user"></i>
  134 + <span>客头数走势</span>
  135 + </div>
  136 + <div id="headCountTrend" class="chart-box"></div>
  137 + </el-card>
  138 + </el-col>
  139 + <el-col :span="8">
  140 + <el-card shadow="never" class="chart-card">
  141 + <div slot="header" class="card-header">
  142 + <i class="el-icon-user-solid"></i>
  143 + <span>客次数走势</span>
  144 + </div>
  145 + <div id="personTimeTrend" class="chart-box"></div>
  146 + </el-card>
  147 + </el-col>
  148 + <el-col :span="8">
  149 + <el-card shadow="never" class="chart-card">
  150 + <div slot="header" class="card-header">
  151 + <i class="el-icon-goods"></i>
  152 + <span>项目数走势</span>
  153 + </div>
  154 + <div id="projectCountTrend" class="chart-box"></div>
  155 + </el-card>
  156 + </el-col>
  157 + </el-row>
  158 + <div class="table-section" style="margin-top: 20px">
  159 + <el-card shadow="never" class="table-card">
  160 + <div slot="header" class="card-header">
  161 + <i class="el-icon-s-grid"></i>
  162 + <span>月度趋势数据列表</span>
  163 + </div>
  164 + <NCC-table
  165 + v-loading="trendTableLoading"
  166 + :data="trendTableData"
  167 + border
  168 + stripe
  169 + style="width: 100%"
  170 + >
  171 + <el-table-column
  172 + v-for="col in trendTableColumns"
  173 + :key="col.prop"
  174 + :prop="col.prop"
  175 + :label="col.label"
  176 + :width="col.width"
  177 + :align="col.align || 'center'"
  178 + >
  179 + <template slot-scope="scope">
  180 + <span v-if="col.formatter" v-html="col.formatter(scope.row)"></span>
  181 + <span v-else>{{ scope.row[col.prop] || '0' }}</span>
  182 + </template>
  183 + </el-table-column>
  184 + </NCC-table>
  185 + </el-card>
  186 + </div>
  187 + </div>
  188 + </div>
  189 +
  190 + <!-- 全年门店业绩表 -->
  191 + <div v-show="activeMenu === 'performance-stat'" class="content-panel">
  192 + <div class="panel-header">
  193 + <i class="el-icon-trophy"></i>
  194 + <span>全年门店业绩表</span>
  195 + </div>
  196 + <div class="panel-body">
  197 + <el-card shadow="never" class="chart-card">
  198 + <div slot="header" class="card-header">
  199 + <i class="el-icon-data-line"></i>
  200 + <span>业绩走势图</span>
  201 + </div>
  202 + <div id="performanceChart" class="chart-box-large"></div>
  203 + </el-card>
  204 + <div class="table-section" style="margin-top: 20px">
  205 + <el-card shadow="never" class="table-card">
  206 + <div slot="header" class="card-header">
  207 + <i class="el-icon-s-grid"></i>
  208 + <span>业绩数据列表</span>
  209 + </div>
  210 + <NCC-table
  211 + v-loading="performanceTableLoading"
  212 + :data="performanceTableData"
  213 + border
  214 + stripe
  215 + style="width: 100%"
  216 + >
  217 + <el-table-column
  218 + v-for="col in performanceTableColumns"
  219 + :key="col.prop"
  220 + :prop="col.prop"
  221 + :label="col.label"
  222 + :width="col.width"
  223 + :align="col.align || 'center'"
  224 + >
  225 + <template slot-scope="scope">
  226 + <span v-if="col.formatter" v-html="col.formatter(scope.row)"></span>
  227 + <span v-else>{{ scope.row[col.prop] || '0' }}</span>
  228 + </template>
  229 + </el-table-column>
  230 + </NCC-table>
  231 + </el-card>
  232 + </div>
  233 + </div>
  234 + </div>
  235 +
  236 + <!-- 全年门店消耗表 -->
  237 + <div v-show="activeMenu === 'consume-stat'" class="content-panel">
  238 + <div class="panel-header">
  239 + <i class="el-icon-shopping-cart-2"></i>
  240 + <span>全年门店消耗表</span>
  241 + </div>
  242 + <div class="panel-body">
  243 + <el-card shadow="never" class="chart-card">
  244 + <div slot="header" class="card-header">
  245 + <i class="el-icon-data-line"></i>
  246 + <span>消耗走势图</span>
  247 + </div>
  248 + <div id="consumeChart" class="chart-box-large"></div>
  249 + </el-card>
  250 + <div class="table-section" style="margin-top: 20px">
  251 + <el-card shadow="never" class="table-card">
  252 + <div slot="header" class="card-header">
  253 + <i class="el-icon-s-grid"></i>
  254 + <span>消耗数据列表</span>
  255 + </div>
  256 + <NCC-table
  257 + v-loading="consumeTableLoading"
  258 + :data="consumeTableData"
  259 + border
  260 + stripe
  261 + style="width: 100%"
  262 + >
  263 + <el-table-column
  264 + v-for="col in consumeTableColumns"
  265 + :key="col.prop"
  266 + :prop="col.prop"
  267 + :label="col.label"
  268 + :width="col.width"
  269 + :align="col.align || 'center'"
  270 + >
  271 + <template slot-scope="scope">
  272 + <span v-if="col.formatter" v-html="col.formatter(scope.row)"></span>
  273 + <span v-else>{{ scope.row[col.prop] || '0' }}</span>
  274 + </template>
  275 + </el-table-column>
  276 + </NCC-table>
  277 + </el-card>
  278 + </div>
  279 + </div>
  280 + </div>
  281 +
  282 + <!-- 年度门店人头表 -->
  283 + <div v-show="activeMenu === 'headcount-stat'" class="content-panel">
  284 + <div class="panel-header">
  285 + <i class="el-icon-user"></i>
  286 + <span>年度门店人头表</span>
  287 + </div>
  288 + <div class="panel-body">
  289 + <el-card shadow="never" class="chart-card">
  290 + <div slot="header" class="card-header">
  291 + <i class="el-icon-data-line"></i>
  292 + <span>人头数走势图</span>
  293 + </div>
  294 + <div id="headCountChart" class="chart-box-large"></div>
  295 + </el-card>
  296 + <div class="table-section" style="margin-top: 20px">
  297 + <el-card shadow="never" class="table-card">
  298 + <div slot="header" class="card-header">
  299 + <i class="el-icon-s-grid"></i>
  300 + <span>人头数据列表</span>
  301 + </div>
  302 + <NCC-table
  303 + v-loading="headCountTableLoading"
  304 + :data="headCountTableData"
  305 + border
  306 + stripe
  307 + style="width: 100%"
  308 + >
  309 + <el-table-column
  310 + v-for="col in headCountTableColumns"
  311 + :key="col.prop"
  312 + :prop="col.prop"
  313 + :label="col.label"
  314 + :width="col.width"
  315 + :align="col.align || 'center'"
  316 + >
  317 + <template slot-scope="scope">
  318 + <span v-if="col.formatter" v-html="col.formatter(scope.row)"></span>
  319 + <span v-else>{{ scope.row[col.prop] || '0' }}</span>
  320 + </template>
  321 + </el-table-column>
  322 + </NCC-table>
  323 + </el-card>
  324 + </div>
  325 + </div>
  326 + </div>
  327 +
  328 + <!-- 年度门店人次表 -->
  329 + <div v-show="activeMenu === 'persontime-stat'" class="content-panel">
  330 + <div class="panel-header">
  331 + <i class="el-icon-user-solid"></i>
  332 + <span>年度门店人次表</span>
  333 + </div>
  334 + <div class="panel-body">
  335 + <el-card shadow="never" class="chart-card">
  336 + <div slot="header" class="card-header">
  337 + <i class="el-icon-data-line"></i>
  338 + <span>人次走势图</span>
  339 + </div>
  340 + <div id="personTimeChart" class="chart-box-large"></div>
  341 + </el-card>
  342 + <div class="table-section" style="margin-top: 20px">
  343 + <el-card shadow="never" class="table-card">
  344 + <div slot="header" class="card-header">
  345 + <i class="el-icon-s-grid"></i>
  346 + <span>人次数据列表</span>
  347 + </div>
  348 + <NCC-table
  349 + v-loading="personTimeTableLoading"
  350 + :data="personTimeTableData"
  351 + border
  352 + stripe
  353 + style="width: 100%"
  354 + >
  355 + <el-table-column
  356 + v-for="col in personTimeTableColumns"
  357 + :key="col.prop"
  358 + :prop="col.prop"
  359 + :label="col.label"
  360 + :width="col.width"
  361 + :align="col.align || 'center'"
  362 + >
  363 + <template slot-scope="scope">
  364 + <span v-if="col.formatter" v-html="col.formatter(scope.row)"></span>
  365 + <span v-else>{{ scope.row[col.prop] || '0' }}</span>
  366 + </template>
  367 + </el-table-column>
  368 + </NCC-table>
  369 + </el-card>
  370 + </div>
  371 + </div>
  372 + </div>
  373 +
  374 + <!-- 年度门店项目数表 -->
  375 + <div v-show="activeMenu === 'project-stat'" class="content-panel">
  376 + <div class="panel-header">
  377 + <i class="el-icon-goods"></i>
  378 + <span>年度门店项目数表</span>
  379 + </div>
  380 + <div class="panel-body">
  381 + <el-card shadow="never" class="chart-card">
  382 + <div slot="header" class="card-header">
  383 + <i class="el-icon-data-line"></i>
  384 + <span>项目数走势图</span>
  385 + </div>
  386 + <div id="projectCountChart" class="chart-box-large"></div>
  387 + </el-card>
  388 + <div class="table-section" style="margin-top: 20px">
  389 + <el-card shadow="never" class="table-card">
  390 + <div slot="header" class="card-header">
  391 + <i class="el-icon-s-grid"></i>
  392 + <span>项目数据列表</span>
  393 + </div>
  394 + <NCC-table
  395 + v-loading="projectCountTableLoading"
  396 + :data="projectCountTableData"
  397 + border
  398 + stripe
  399 + style="width: 100%"
  400 + >
  401 + <el-table-column
  402 + v-for="col in projectCountTableColumns"
  403 + :key="col.prop"
  404 + :prop="col.prop"
  405 + :label="col.label"
  406 + :width="col.width"
  407 + :align="col.align || 'center'"
  408 + >
  409 + <template slot-scope="scope">
  410 + <span v-if="col.formatter" v-html="col.formatter(scope.row)"></span>
  411 + <span v-else>{{ scope.row[col.prop] || '0' }}</span>
  412 + </template>
  413 + </el-table-column>
  414 + </NCC-table>
  415 + </el-card>
  416 + </div>
  417 + </div>
  418 + </div>
  419 +
  420 + <!-- 门店五项指标统计 -->
  421 + <div v-show="activeMenu === 'store-indicators'" class="content-panel">
  422 + <div class="panel-header">
  423 + <i class="el-icon-pie-chart"></i>
  424 + <span>门店五项指标统计</span>
  425 + </div>
  426 + <div class="panel-body">
  427 + <div class="indicator-controls">
  428 + <el-radio-group v-model="indicatorField" size="medium" @change="loadStoreIndicators">
  429 + <el-radio-button label="totalperformance">
  430 + <i class="el-icon-trophy"></i>
  431 + 总业绩
  432 + </el-radio-button>
  433 + <el-radio-button label="totalconsume">
  434 + <i class="el-icon-shopping-cart-2"></i>
  435 + 总消耗
  436 + </el-radio-button>
  437 + <el-radio-button label="headcount">
  438 + <i class="el-icon-user"></i>
  439 + 客头数
  440 + </el-radio-button>
  441 + <el-radio-button label="persontime">
  442 + <i class="el-icon-user-solid"></i>
  443 + 客次数
  444 + </el-radio-button>
  445 + <el-radio-button label="projectcount">
  446 + <i class="el-icon-goods"></i>
  447 + 项目数
  448 + </el-radio-button>
  449 + </el-radio-group>
  450 + </div>
  451 + <el-row :gutter="20" style="margin-top: 20px">
  452 + <el-col :span="16">
  453 + <el-card shadow="never" class="chart-card">
  454 + <div slot="header" class="card-header">
  455 + <i class="el-icon-s-data"></i>
  456 + <span>各门店指标对比</span>
  457 + </div>
  458 + <div id="storeIndicatorChart" class="chart-box-large"></div>
  459 + </el-card>
  460 + </el-col>
  461 + <el-col :span="8">
  462 + <el-card shadow="never" class="chart-card">
  463 + <div slot="header" class="card-header">
  464 + <i class="el-icon-pie-chart"></i>
  465 + <span>门店占比分析</span>
  466 + </div>
  467 + <div id="storePieChart" class="chart-box"></div>
  468 + </el-card>
  469 + </el-col>
  470 + </el-row>
  471 + <div class="table-section" style="margin-top: 20px">
  472 + <el-card shadow="never" class="table-card">
  473 + <div slot="header" class="card-header">
  474 + <i class="el-icon-s-grid"></i>
  475 + <span>门店指标数据列表</span>
  476 + </div>
  477 + <NCC-table
  478 + v-loading="storeIndicatorTableLoading"
  479 + :data="storeIndicatorTableData"
  480 + border
  481 + stripe
  482 + style="width: 100%"
  483 + >
  484 + <el-table-column
  485 + v-for="col in storeIndicatorTableColumns"
  486 + :key="col.prop"
  487 + :prop="col.prop"
  488 + :label="col.label"
  489 + :width="col.width"
  490 + :align="col.align || 'center'"
  491 + >
  492 + <template slot-scope="scope">
  493 + <span v-if="col.formatter" v-html="col.formatter(scope.row)"></span>
  494 + <span v-else>{{ scope.row[col.prop] || '0' }}</span>
  495 + </template>
  496 + </el-table-column>
  497 + </NCC-table>
  498 + </el-card>
  499 + </div>
  500 + </div>
  501 + </div>
  502 +
  503 + <!-- 事业部五项指标统计 -->
  504 + <div v-show="activeMenu === 'bu-indicators'" class="content-panel">
  505 + <div class="panel-header">
  506 + <i class="el-icon-s-data"></i>
  507 + <span>事业部五项指标统计</span>
  508 + </div>
  509 + <div class="panel-body">
  510 + <div class="indicator-controls">
  511 + <el-radio-group v-model="buIndicatorField" size="medium" @change="loadBuIndicators">
  512 + <el-radio-button label="totalperformance">
  513 + <i class="el-icon-trophy"></i>
  514 + 总业绩
  515 + </el-radio-button>
  516 + <el-radio-button label="totalconsume">
  517 + <i class="el-icon-shopping-cart-2"></i>
  518 + 总消耗
  519 + </el-radio-button>
  520 + <el-radio-button label="headcount">
  521 + <i class="el-icon-user"></i>
  522 + 客头数
  523 + </el-radio-button>
  524 + <el-radio-button label="persontime">
  525 + <i class="el-icon-user-solid"></i>
  526 + 客次数
  527 + </el-radio-button>
  528 + <el-radio-button label="projectcount">
  529 + <i class="el-icon-goods"></i>
  530 + 项目数
  531 + </el-radio-button>
  532 + </el-radio-group>
  533 + </div>
  534 + <el-row :gutter="20" style="margin-top: 20px">
  535 + <el-col :span="12">
  536 + <el-card shadow="never" class="chart-card">
  537 + <div slot="header" class="card-header">
  538 + <i class="el-icon-pie-chart"></i>
  539 + <span>事业部贡献占比</span>
  540 + </div>
  541 + <div id="buPieChart" class="chart-box"></div>
  542 + </el-card>
  543 + </el-col>
  544 + <el-col :span="12">
  545 + <el-card shadow="never" class="chart-card">
  546 + <div slot="header" class="card-header">
  547 + <i class="el-icon-data-line"></i>
  548 + <span>事业部增长率分析</span>
  549 + </div>
  550 + <div id="buGrowthChart" class="chart-box"></div>
  551 + </el-card>
  552 + </el-col>
  553 + </el-row>
  554 + <div class="table-section" style="margin-top: 20px">
  555 + <el-card shadow="never" class="table-card">
  556 + <div slot="header" class="card-header">
  557 + <i class="el-icon-s-grid"></i>
  558 + <span>事业部指标数据列表</span>
  559 + </div>
  560 + <NCC-table
  561 + v-loading="buIndicatorTableLoading"
  562 + :data="buIndicatorTableData"
  563 + border
  564 + stripe
  565 + style="width: 100%"
  566 + >
  567 + <el-table-column
  568 + v-for="col in buIndicatorTableColumns"
  569 + :key="col.prop"
  570 + :prop="col.prop"
  571 + :label="col.label"
  572 + :width="col.width"
  573 + :align="col.align || 'center'"
  574 + >
  575 + <template slot-scope="scope">
  576 + <span v-if="col.formatter" v-html="col.formatter(scope.row)"></span>
  577 + <span v-else>{{ scope.row[col.prop] || '0' }}</span>
  578 + </template>
  579 + </el-table-column>
  580 + </NCC-table>
  581 + </el-card>
  582 + </div>
  583 + </div>
  584 + </div>
  585 +
  586 + <!-- 事业部内部汇总 -->
  587 + <div v-show="activeMenu === 'bu-summary'" class="content-panel">
  588 + <div class="panel-header">
  589 + <i class="el-icon-tickets"></i>
  590 + <span>事业部内部汇总</span>
  591 + </div>
  592 + <div class="panel-body">
  593 + <el-card shadow="never" class="table-card">
  594 + <div slot="header" class="card-header">
  595 + <i class="el-icon-s-grid"></i>
  596 + <span>事业部汇总数据列表</span>
  597 + </div>
  598 + <NCC-table
  599 + v-loading="buSummaryTableLoading"
  600 + :data="buSummaryTableData"
  601 + border
  602 + stripe
  603 + style="width: 100%"
  604 + >
  605 + <el-table-column
  606 + v-for="col in buSummaryTableColumns"
  607 + :key="col.prop"
  608 + :prop="col.prop"
  609 + :label="col.label"
  610 + :width="col.width"
  611 + :align="col.align || 'center'"
  612 + >
  613 + <template slot-scope="scope">
  614 + <span v-if="col.formatter" v-html="col.formatter(scope.row)"></span>
  615 + <span v-else>{{ scope.row[col.prop] || '0' }}</span>
  616 + </template>
  617 + </el-table-column>
  618 + </NCC-table>
  619 + </el-card>
  620 + </div>
  621 + </div>
  622 + </div>
  623 + </div>
  624 + </div>
  625 +</template>
  626 +
  627 +<script>
  628 +import * as echarts from 'echarts'
  629 +import {
  630 + getMonthlyTrend,
  631 + getTotalPerformanceStat,
  632 + getTotalConsumeStat,
  633 + getHeadCountStat,
  634 + getPersonTimeStat,
  635 + getProjectCountStat,
  636 + getStoreIndicatorDetail,
  637 + getBusinessUnitIndicatorsStat,
  638 + getBusinessUnitSummaryStat
  639 +} from '@/api/extend/annualSummary'
  640 +import { getStoreSelector } from '@/api/extend/store'
  641 +
  642 +export default {
  643 + name: 'annualSummaryDashboard',
  644 + data() {
  645 + return {
  646 + activeMenu: 'monthly-trend',
  647 + query: {
  648 + year: new Date().getFullYear().toString(),
  649 + storeName: ''
  650 + },
  651 + indicatorField: 'totalperformance',
  652 + buIndicatorField: 'totalperformance',
  653 + trendField: 'totalperformance', // 月度趋势列表显示的类型
  654 + // 月度趋势
  655 + trendTableLoading: false,
  656 + trendTableData: [],
  657 + trendTableColumns: [],
  658 + // 业绩
  659 + performanceTableLoading: false,
  660 + performanceTableData: [],
  661 + performanceTableColumns: [],
  662 + // 消耗
  663 + consumeTableLoading: false,
  664 + consumeTableData: [],
  665 + consumeTableColumns: [],
  666 + // 人头
  667 + headCountTableLoading: false,
  668 + headCountTableData: [],
  669 + headCountTableColumns: [],
  670 + // 人次
  671 + personTimeTableLoading: false,
  672 + personTimeTableData: [],
  673 + personTimeTableColumns: [],
  674 + // 项目
  675 + projectCountTableLoading: false,
  676 + projectCountTableData: [],
  677 + projectCountTableColumns: [],
  678 + // 门店指标
  679 + storeIndicatorTableLoading: false,
  680 + storeIndicatorTableData: [],
  681 + storeIndicatorTableColumns: [],
  682 + // 事业部指标
  683 + buIndicatorTableLoading: false,
  684 + buIndicatorTableData: [],
  685 + buIndicatorTableColumns: [],
  686 + // 事业部汇总
  687 + buSummaryTableLoading: false,
  688 + buSummaryTableData: [],
  689 + buSummaryTableColumns: [],
  690 + // 门店下拉选项
  691 + storeOptions: []
  692 + }
  693 + },
  694 + mounted() {
  695 + this.loadStoreOptions()
  696 + this.initData()
  697 + window.addEventListener('resize', this.handleResize)
  698 + },
  699 + beforeDestroy() {
  700 + window.removeEventListener('resize', this.handleResize)
  701 + // 销毁所有图表实例
  702 + this.destroyAllCharts()
  703 + },
  704 + methods: {
  705 + // 加载门店下拉选项
  706 + async loadStoreOptions() {
  707 + try {
  708 + const res = await getStoreSelector()
  709 + if (res.data && res.data.list) {
  710 + this.storeOptions = res.data.list
  711 + }
  712 + } catch (error) {
  713 + console.error('加载门店列表失败:', error)
  714 + this.storeOptions = []
  715 + }
  716 + },
  717 + initData() {
  718 + this.loadCurrentMenuData()
  719 + },
  720 + handleQueryChange() {
  721 + this.loadCurrentMenuData()
  722 + },
  723 + handleReset() {
  724 + this.query = {
  725 + year: new Date().getFullYear().toString(),
  726 + storeName: ''
  727 + }
  728 + this.loadCurrentMenuData()
  729 + },
  730 + handleMenuSelect(key) {
  731 + this.activeMenu = key
  732 + this.$nextTick(() => {
  733 + this.loadCurrentMenuData()
  734 + })
  735 + },
  736 + loadCurrentMenuData() {
  737 + switch (this.activeMenu) {
  738 + case 'monthly-trend':
  739 + this.loadMonthlyTrend()
  740 + break
  741 + case 'performance-stat':
  742 + this.loadPerformanceStat()
  743 + break
  744 + case 'consume-stat':
  745 + this.loadConsumeStat()
  746 + break
  747 + case 'headcount-stat':
  748 + this.loadHeadCountStat()
  749 + break
  750 + case 'persontime-stat':
  751 + this.loadPersonTimeStat()
  752 + break
  753 + case 'project-stat':
  754 + this.loadProjectCountStat()
  755 + break
  756 + case 'store-indicators':
  757 + this.loadStoreIndicators()
  758 + break
  759 + case 'bu-indicators':
  760 + this.loadBuIndicators()
  761 + break
  762 + case 'bu-summary':
  763 + this.loadBuSummary()
  764 + break
  765 + }
  766 + },
  767 + // 月度趋势分析
  768 + async loadMonthlyTrend() {
  769 + const fields = [
  770 + { field: 'totalperformance', chartId: 'perfTrend', name: '业绩' },
  771 + { field: 'totalconsume', chartId: 'consumeTrend', name: '消耗' },
  772 + { field: 'headcount', chartId: 'headCountTrend', name: '客头数' },
  773 + { field: 'persontime', chartId: 'personTimeTrend', name: '客次数' },
  774 + { field: 'projectcount', chartId: 'projectCountTrend', name: '项目数' }
  775 + ]
  776 +
  777 + // 加载每个图表的数据 - 确保每个图表都使用正确的 type 参数
  778 + for (const item of fields) {
  779 + try {
  780 + // 明确传递 type 参数,确保后端能正确接收
  781 + const params = {
  782 + year: this.query.year || new Date().getFullYear().toString(),
  783 + storeName: this.query.storeName || '',
  784 + type: item.field // type 作为单独的查询参数传递
  785 + }
  786 +
  787 + const res = await getMonthlyTrend(params)
  788 + if (res && res.data) {
  789 + // 立即渲染,避免数据被覆盖
  790 + this.renderTrendChart(item.chartId, res.data, item.name)
  791 + }
  792 + } catch (error) {
  793 + console.error(`加载${item.name}趋势失败:`, error)
  794 + }
  795 + }
  796 +
  797 + // 加载列表数据(使用当前选中的趋势类型对应的数据)
  798 + try {
  799 + const res = await getMonthlyTrend({
  800 + year: this.query.year,
  801 + storeName: this.query.storeName,
  802 + type: this.trendField || 'totalperformance'
  803 + })
  804 + if (res && res.data && res.data.rows) {
  805 + this.trendTableData = res.data.rows
  806 + this.buildTrendTableColumns(res.data)
  807 + }
  808 + } catch (error) {
  809 + console.error('加载趋势列表失败:', error)
  810 + }
  811 + },
  812 + // 全年门店业绩表
  813 + async loadPerformanceStat() {
  814 + this.performanceTableLoading = true
  815 + try {
  816 + const res = await getTotalPerformanceStat(this.query)
  817 + if (res.data) {
  818 + this.renderMonthlyStatChart('performanceChart', res.data, '业绩')
  819 + this.performanceTableData = res.data.rows || []
  820 + this.buildMonthlyStatTableColumns(res.data, '业绩')
  821 + }
  822 + } catch (error) {
  823 + console.error('加载业绩统计失败:', error)
  824 + } finally {
  825 + this.performanceTableLoading = false
  826 + }
  827 + },
  828 + // 全年门店消耗表
  829 + async loadConsumeStat() {
  830 + this.consumeTableLoading = true
  831 + try {
  832 + const res = await getTotalConsumeStat(this.query)
  833 + if (res.data) {
  834 + this.renderMonthlyStatChart('consumeChart', res.data, '消耗')
  835 + this.consumeTableData = res.data.rows || []
  836 + this.buildMonthlyStatTableColumns(res.data, '消耗')
  837 + }
  838 + } catch (error) {
  839 + console.error('加载消耗统计失败:', error)
  840 + } finally {
  841 + this.consumeTableLoading = false
  842 + }
  843 + },
  844 + // 年度门店人头表
  845 + async loadHeadCountStat() {
  846 + this.headCountTableLoading = true
  847 + try {
  848 + const res = await getHeadCountStat(this.query)
  849 + if (res.data) {
  850 + this.renderMonthlyStatChart('headCountChart', res.data, '人头数')
  851 + this.headCountTableData = res.data.rows || []
  852 + this.buildMonthlyStatTableColumns(res.data, '人头数')
  853 + }
  854 + } catch (error) {
  855 + console.error('加载人头统计失败:', error)
  856 + } finally {
  857 + this.headCountTableLoading = false
  858 + }
  859 + },
  860 + // 年度门店人次表
  861 + async loadPersonTimeStat() {
  862 + this.personTimeTableLoading = true
  863 + try {
  864 + const res = await getPersonTimeStat(this.query)
  865 + if (res.data) {
  866 + this.renderMonthlyStatChart('personTimeChart', res.data, '人次')
  867 + this.personTimeTableData = res.data.rows || []
  868 + this.buildMonthlyStatTableColumns(res.data, '人次')
  869 + }
  870 + } catch (error) {
  871 + console.error('加载人次统计失败:', error)
  872 + } finally {
  873 + this.personTimeTableLoading = false
  874 + }
  875 + },
  876 + // 年度门店项目数表
  877 + async loadProjectCountStat() {
  878 + this.projectCountTableLoading = true
  879 + try {
  880 + const res = await getProjectCountStat(this.query)
  881 + if (res.data) {
  882 + this.renderMonthlyStatChart('projectCountChart', res.data, '项目数')
  883 + this.projectCountTableData = res.data.rows || []
  884 + this.buildMonthlyStatTableColumns(res.data, '项目数')
  885 + }
  886 + } catch (error) {
  887 + console.error('加载项目统计失败:', error)
  888 + } finally {
  889 + this.projectCountTableLoading = false
  890 + }
  891 + },
  892 + // 门店五项指标统计
  893 + async loadStoreIndicators() {
  894 + this.storeIndicatorTableLoading = true
  895 + try {
  896 + const res = await getStoreIndicatorDetail({
  897 + ...this.query,
  898 + type: this.indicatorField
  899 + })
  900 + if (res.data && res.data.rows) {
  901 + this.renderStoreIndicatorChart(res.data)
  902 + this.storeIndicatorTableData = res.data.rows
  903 + this.buildStoreIndicatorTableColumns()
  904 + }
  905 + } catch (error) {
  906 + console.error('加载门店指标失败:', error)
  907 + } finally {
  908 + this.storeIndicatorTableLoading = false
  909 + }
  910 + },
  911 + // 事业部五项指标统计
  912 + async loadBuIndicators() {
  913 + this.buIndicatorTableLoading = true
  914 + try {
  915 + const res = await getBusinessUnitIndicatorsStat({
  916 + ...this.query,
  917 + type: this.buIndicatorField
  918 + })
  919 + if (res.data && res.data.rows) {
  920 + this.renderBuIndicatorCharts(res.data)
  921 + this.buIndicatorTableData = res.data.rows
  922 + this.buildBuIndicatorTableColumns()
  923 + }
  924 + } catch (error) {
  925 + console.error('加载事业部指标失败:', error)
  926 + } finally {
  927 + this.buIndicatorTableLoading = false
  928 + }
  929 + },
  930 + // 事业部内部汇总
  931 + async loadBuSummary() {
  932 + this.buSummaryTableLoading = true
  933 + try {
  934 + const res = await getBusinessUnitSummaryStat(this.query)
  935 + if (res.data && res.data.list) {
  936 + this.buSummaryTableData = res.data.list
  937 + this.buildBuSummaryTableColumns()
  938 + }
  939 + } catch (error) {
  940 + console.error('加载事业部汇总失败:', error)
  941 + } finally {
  942 + this.buSummaryTableLoading = false
  943 + }
  944 + },
  945 + // 渲染趋势图
  946 + renderTrendChart(chartId, data, name) {
  947 + this.$nextTick(() => {
  948 + const chartDom = document.getElementById(chartId)
  949 + if (!chartDom) return
  950 +
  951 + // 先销毁旧的图表实例,确保使用新数据
  952 + const oldChart = echarts.getInstanceByDom(chartDom)
  953 + if (oldChart) {
  954 + oldChart.dispose()
  955 + }
  956 +
  957 + const chart = echarts.init(chartDom)
  958 + const months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
  959 +
  960 + // 确保使用传入的 data,而不是共享的数据
  961 + const rows = data && data.rows ? data.rows : []
  962 +
  963 + const currentYearValues = months.map((m) => {
  964 + return rows.reduce((sum, row) => {
  965 + const value = row['month' + m] || 0
  966 + return sum + (typeof value === 'number' ? value : 0)
  967 + }, 0)
  968 + })
  969 +
  970 + const lastYearValues = months.map((m) => {
  971 + return rows.reduce((sum, row) => {
  972 + const value = row['lastMonth' + m] || 0
  973 + return sum + (typeof value === 'number' ? value : 0)
  974 + }, 0)
  975 + })
  976 +
  977 + const option = {
  978 + tooltip: {
  979 + trigger: 'axis',
  980 + formatter: function (params) {
  981 + let res = params[0].name + '<br/>'
  982 + params.forEach((item) => {
  983 + res += item.marker + item.seriesName + ': ' + item.value.toLocaleString() + '<br/>'
  984 + })
  985 + return res
  986 + }
  987 + },
  988 + legend: {
  989 + data: ['本年走势', '上年走势'],
  990 + top: 10
  991 + },
  992 + grid: {
  993 + left: '3%',
  994 + right: '4%',
  995 + bottom: '3%',
  996 + top: '15%',
  997 + containLabel: true
  998 + },
  999 + xAxis: {
  1000 + type: 'category',
  1001 + boundaryGap: false,
  1002 + data: months.map((m) => m + '月')
  1003 + },
  1004 + yAxis: {
  1005 + type: 'value'
  1006 + },
  1007 + series: [
  1008 + {
  1009 + name: '本年走势',
  1010 + type: 'line',
  1011 + smooth: true,
  1012 + data: currentYearValues,
  1013 + itemStyle: { color: '#409EFF' },
  1014 + areaStyle: { color: 'rgba(64, 158, 255, 0.1)' }
  1015 + },
  1016 + {
  1017 + name: '上年走势',
  1018 + type: 'line',
  1019 + smooth: true,
  1020 + data: lastYearValues,
  1021 + itemStyle: { color: '#909399' },
  1022 + lineStyle: { type: 'dashed' }
  1023 + }
  1024 + ]
  1025 + }
  1026 + chart.setOption(option)
  1027 + })
  1028 + },
  1029 + // 渲染月度统计图表
  1030 + renderMonthlyStatChart(chartId, data, name) {
  1031 + this.$nextTick(() => {
  1032 + const chartDom = document.getElementById(chartId)
  1033 + if (!chartDom) return
  1034 +
  1035 + const chart = echarts.getInstanceByDom(chartDom) || echarts.init(chartDom)
  1036 + const months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
  1037 +
  1038 + const currentYearValues = months.map((m) => {
  1039 + return (data.rows || []).reduce((sum, row) => sum + (row['month' + m] || 0), 0)
  1040 + })
  1041 +
  1042 + const lastYearValues = months.map((m) => {
  1043 + return (data.rows || []).reduce((sum, row) => sum + (row['lastMonth' + m] || 0), 0)
  1044 + })
  1045 +
  1046 + const option = {
  1047 + tooltip: {
  1048 + trigger: 'axis',
  1049 + formatter: function (params) {
  1050 + let res = params[0].name + '<br/>'
  1051 + params.forEach((item) => {
  1052 + res += item.marker + item.seriesName + ': ' + item.value.toLocaleString() + '<br/>'
  1053 + })
  1054 + return res
  1055 + }
  1056 + },
  1057 + legend: {
  1058 + data: ['本年走势', '上年走势'],
  1059 + top: 10
  1060 + },
  1061 + grid: {
  1062 + left: '3%',
  1063 + right: '4%',
  1064 + bottom: '3%',
  1065 + top: '15%',
  1066 + containLabel: true
  1067 + },
  1068 + xAxis: {
  1069 + type: 'category',
  1070 + boundaryGap: false,
  1071 + data: months.map((m) => m + '月')
  1072 + },
  1073 + yAxis: {
  1074 + type: 'value'
  1075 + },
  1076 + series: [
  1077 + {
  1078 + name: '本年走势',
  1079 + type: 'line',
  1080 + smooth: true,
  1081 + data: currentYearValues,
  1082 + itemStyle: { color: '#409EFF' },
  1083 + areaStyle: { color: 'rgba(64, 158, 255, 0.1)' }
  1084 + },
  1085 + {
  1086 + name: '上年走势',
  1087 + type: 'line',
  1088 + smooth: true,
  1089 + data: lastYearValues,
  1090 + itemStyle: { color: '#909399' },
  1091 + lineStyle: { type: 'dashed' }
  1092 + }
  1093 + ]
  1094 + }
  1095 + chart.setOption(option)
  1096 + })
  1097 + },
  1098 + // 渲染门店指标图表
  1099 + renderStoreIndicatorChart(data) {
  1100 + this.$nextTick(() => {
  1101 + // 柱状图
  1102 + const barChartDom = document.getElementById('storeIndicatorChart')
  1103 + if (barChartDom) {
  1104 + const barChart = echarts.getInstanceByDom(barChartDom) || echarts.init(barChartDom)
  1105 + const sortedResult = [...(data.rows || [])]
  1106 + .sort((a, b) => b.currentYearValue - a.currentYearValue)
  1107 +
  1108 + const barOption = {
  1109 + tooltip: {
  1110 + trigger: 'axis',
  1111 + formatter: function (params) {
  1112 + return params[0].name + '<br/>' + params[0].seriesName + ': ' + params[0].value.toLocaleString()
  1113 + }
  1114 + },
  1115 + grid: {
  1116 + left: '3%',
  1117 + right: '4%',
  1118 + bottom: '15%',
  1119 + top: '10%',
  1120 + containLabel: true
  1121 + },
  1122 + xAxis: {
  1123 + type: 'category',
  1124 + data: sortedResult.map((o) => o.storeName),
  1125 + axisLabel: {
  1126 + interval: 0,
  1127 + rotate: 30
  1128 + }
  1129 + },
  1130 + yAxis: {
  1131 + type: 'value'
  1132 + },
  1133 + series: [
  1134 + {
  1135 + name: '本年数值',
  1136 + type: 'bar',
  1137 + data: sortedResult.map((o) => o.currentYearValue),
  1138 + itemStyle: {
  1139 + color: '#409EFF'
  1140 + }
  1141 + }
  1142 + ]
  1143 + }
  1144 + barChart.setOption(barOption)
  1145 + }
  1146 +
  1147 + // 饼图
  1148 + const pieChartDom = document.getElementById('storePieChart')
  1149 + if (pieChartDom) {
  1150 + const pieChart = echarts.getInstanceByDom(pieChartDom) || echarts.init(pieChartDom)
  1151 + const allStores = [...(data.rows || [])]
  1152 + .sort((a, b) => b.currentYearValue - a.currentYearValue)
  1153 +
  1154 + const pieOption = {
  1155 + tooltip: {
  1156 + trigger: 'item',
  1157 + formatter: '{a} <br/>{b}: {c} ({d}%)'
  1158 + },
  1159 + legend: {
  1160 + show: false // 隐藏图例
  1161 + },
  1162 + series: [
  1163 + {
  1164 + name: '门店占比',
  1165 + type: 'pie',
  1166 + radius: ['40%', '70%'],
  1167 + center: ['60%', '50%'],
  1168 + avoidLabelOverlap: false,
  1169 + data: allStores.map((o) => ({
  1170 + name: o.storeName,
  1171 + value: o.currentYearValue
  1172 + })),
  1173 + emphasis: {
  1174 + itemStyle: {
  1175 + shadowBlur: 10,
  1176 + shadowOffsetX: 0,
  1177 + shadowColor: 'rgba(0, 0, 0, 0.5)'
  1178 + }
  1179 + }
  1180 + }
  1181 + ]
  1182 + }
  1183 + pieChart.setOption(pieOption)
  1184 + }
  1185 + })
  1186 + },
  1187 + // 渲染事业部指标图表
  1188 + renderBuIndicatorCharts(data) {
  1189 + this.$nextTick(() => {
  1190 + // 饼图
  1191 + const pieChartDom = document.getElementById('buPieChart')
  1192 + if (pieChartDom) {
  1193 + const pieChart = echarts.getInstanceByDom(pieChartDom) || echarts.init(pieChartDom)
  1194 + const pieOption = {
  1195 + tooltip: {
  1196 + trigger: 'item',
  1197 + formatter: '{a} <br/>{b}: {c} ({d}%)'
  1198 + },
  1199 + legend: {
  1200 + orient: 'vertical',
  1201 + left: 'left',
  1202 + top: 'middle',
  1203 + data: (data.rows || []).map((o) => o.businessUnitName)
  1204 + },
  1205 + series: [
  1206 + {
  1207 + name: '事业部占比',
  1208 + type: 'pie',
  1209 + radius: '55%',
  1210 + center: ['60%', '50%'],
  1211 + data: (data.rows || []).map((o) => ({
  1212 + name: o.businessUnitName,
  1213 + value: o.currentYearValue
  1214 + })),
  1215 + emphasis: {
  1216 + itemStyle: {
  1217 + shadowBlur: 10,
  1218 + shadowOffsetX: 0,
  1219 + shadowColor: 'rgba(0, 0, 0, 0.5)'
  1220 + }
  1221 + }
  1222 + }
  1223 + ]
  1224 + }
  1225 + pieChart.setOption(pieOption)
  1226 + }
  1227 +
  1228 + // 增长率图
  1229 + const growthChartDom = document.getElementById('buGrowthChart')
  1230 + if (growthChartDom) {
  1231 + const growthChart = echarts.getInstanceByDom(growthChartDom) || echarts.init(growthChartDom)
  1232 + const growthOption = {
  1233 + tooltip: {
  1234 + trigger: 'axis',
  1235 + formatter: function (params) {
  1236 + return params[0].name + '<br/>增长率: ' + params[0].value + '%'
  1237 + }
  1238 + },
  1239 + grid: {
  1240 + left: '3%',
  1241 + right: '4%',
  1242 + bottom: '3%',
  1243 + top: '10%',
  1244 + containLabel: true
  1245 + },
  1246 + xAxis: {
  1247 + type: 'category',
  1248 + data: (data.rows || []).map((o) => o.businessUnitName),
  1249 + axisLabel: {
  1250 + rotate: 30,
  1251 + interval: 0
  1252 + }
  1253 + },
  1254 + yAxis: {
  1255 + type: 'value',
  1256 + axisLabel: {
  1257 + formatter: '{value}%'
  1258 + }
  1259 + },
  1260 + series: [
  1261 + {
  1262 + name: '增长率',
  1263 + type: 'bar',
  1264 + data: (data.rows || []).map((o) => parseFloat(o.growthRate) || 0),
  1265 + itemStyle: {
  1266 + color: function (params) {
  1267 + return params.value >= 0 ? '#67C23A' : '#F56C6C'
  1268 + }
  1269 + },
  1270 + markPoint: {
  1271 + data: [
  1272 + { type: 'max', name: '最大值' },
  1273 + { type: 'min', name: '最小值' }
  1274 + ]
  1275 + }
  1276 + }
  1277 + ]
  1278 + }
  1279 + growthChart.setOption(growthOption)
  1280 + }
  1281 + })
  1282 + },
  1283 + // 构建趋势表格列
  1284 + buildTrendTableColumns(data) {
  1285 + const columns = [
  1286 + { label: '事业部', prop: 'businessUnitName', width: 120, align: 'left' },
  1287 + { label: '门店', prop: 'storeName', width: 150, align: 'left' }
  1288 + ]
  1289 +
  1290 + for (let i = 1; i <= 12; i++) {
  1291 + columns.push({
  1292 + label: `${i}月`,
  1293 + prop: `month${i}`,
  1294 + width: 100,
  1295 + align: 'right',
  1296 + formatter: (row) => {
  1297 + return row[`month${i}`] ? Number(row[`month${i}`]).toLocaleString() : '0'
  1298 + }
  1299 + })
  1300 + }
  1301 +
  1302 + columns.push(
  1303 + {
  1304 + label: '本年合计',
  1305 + prop: 'totalCurrentYear',
  1306 + width: 120,
  1307 + align: 'right',
  1308 + formatter: (row) => {
  1309 + return row.totalCurrentYear ? Number(row.totalCurrentYear).toLocaleString() : '0'
  1310 + }
  1311 + },
  1312 + {
  1313 + label: '上年合计',
  1314 + prop: 'totalLastYear',
  1315 + width: 120,
  1316 + align: 'right',
  1317 + formatter: (row) => {
  1318 + return row.totalLastYear ? Number(row.totalLastYear).toLocaleString() : '0'
  1319 + }
  1320 + },
  1321 + {
  1322 + label: '增长率',
  1323 + prop: 'growthRate',
  1324 + width: 100,
  1325 + align: 'right',
  1326 + formatter: (row) => {
  1327 + const rate = parseFloat(row.growthRate) || 0
  1328 + const color = rate >= 0 ? '#67C23A' : '#F56C6C'
  1329 + return `<span style="color: ${color}">${rate.toFixed(2)}%</span>`
  1330 + }
  1331 + }
  1332 + )
  1333 +
  1334 + this.trendTableColumns = columns
  1335 + },
  1336 + // 构建月度统计表格列
  1337 + buildMonthlyStatTableColumns(data, name) {
  1338 + const columns = [
  1339 + { label: '事业部', prop: 'businessUnitName', width: 120, align: 'left' },
  1340 + { label: '门店', prop: 'storeName', width: 150, align: 'left' }
  1341 + ]
  1342 +
  1343 + for (let i = 1; i <= 12; i++) {
  1344 + columns.push({
  1345 + label: `${i}月`,
  1346 + prop: `month${i}`,
  1347 + width: 100,
  1348 + align: 'right',
  1349 + formatter: (row) => {
  1350 + const value = row[`month${i}`] || 0
  1351 + if (name === '业绩' || name === '消耗') {
  1352 + return `¥${Number(value).toLocaleString()}`
  1353 + }
  1354 + return Number(value).toLocaleString()
  1355 + }
  1356 + })
  1357 + }
  1358 +
  1359 + columns.push(
  1360 + {
  1361 + label: '本年合计',
  1362 + prop: 'totalCurrentYear',
  1363 + width: 120,
  1364 + align: 'right',
  1365 + formatter: (row) => {
  1366 + const value = row.totalCurrentYear || 0
  1367 + if (name === '业绩' || name === '消耗') {
  1368 + return `¥${Number(value).toLocaleString()}`
  1369 + }
  1370 + return Number(value).toLocaleString()
  1371 + }
  1372 + },
  1373 + {
  1374 + label: '上年合计',
  1375 + prop: 'totalLastYear',
  1376 + width: 120,
  1377 + align: 'right',
  1378 + formatter: (row) => {
  1379 + const value = row.totalLastYear || 0
  1380 + if (name === '业绩' || name === '消耗') {
  1381 + return `¥${Number(value).toLocaleString()}`
  1382 + }
  1383 + return Number(value).toLocaleString()
  1384 + }
  1385 + },
  1386 + {
  1387 + label: '增长率',
  1388 + prop: 'growthRate',
  1389 + width: 100,
  1390 + align: 'right',
  1391 + formatter: (row) => {
  1392 + const rate = parseFloat(row.growthRate) || 0
  1393 + const color = rate >= 0 ? '#67C23A' : '#F56C6C'
  1394 + return `<span style="color: ${color}">${rate.toFixed(2)}%</span>`
  1395 + }
  1396 + }
  1397 + )
  1398 +
  1399 + switch (name) {
  1400 + case '业绩':
  1401 + this.performanceTableColumns = columns
  1402 + break
  1403 + case '消耗':
  1404 + this.consumeTableColumns = columns
  1405 + break
  1406 + case '人头数':
  1407 + this.headCountTableColumns = columns
  1408 + break
  1409 + case '人次':
  1410 + this.personTimeTableColumns = columns
  1411 + break
  1412 + case '项目数':
  1413 + this.projectCountTableColumns = columns
  1414 + break
  1415 + }
  1416 + },
  1417 + // 构建门店指标表格列
  1418 + buildStoreIndicatorTableColumns() {
  1419 + this.storeIndicatorTableColumns = [
  1420 + { label: '事业部', prop: 'businessUnitName', width: 120, align: 'left' },
  1421 + { label: '门店', prop: 'storeName', width: 150, align: 'left' },
  1422 + {
  1423 + label: '本年数值',
  1424 + prop: 'currentYearValue',
  1425 + width: 120,
  1426 + align: 'right',
  1427 + formatter: (row) => {
  1428 + return Number(row.currentYearValue || 0).toLocaleString()
  1429 + }
  1430 + },
  1431 + {
  1432 + label: '上年数值',
  1433 + prop: 'lastYearValue',
  1434 + width: 120,
  1435 + align: 'right',
  1436 + formatter: (row) => {
  1437 + return Number(row.lastYearValue || 0).toLocaleString()
  1438 + }
  1439 + },
  1440 + {
  1441 + label: '增长率',
  1442 + prop: 'growthRate',
  1443 + width: 100,
  1444 + align: 'right',
  1445 + formatter: (row) => {
  1446 + const rate = parseFloat(row.growthRate) || 0
  1447 + const color = rate >= 0 ? '#67C23A' : '#F56C6C'
  1448 + return `<span style="color: ${color}">${rate.toFixed(2)}%</span>`
  1449 + }
  1450 + }
  1451 + ]
  1452 + },
  1453 + // 构建事业部指标表格列
  1454 + buildBuIndicatorTableColumns() {
  1455 + this.buIndicatorTableColumns = [
  1456 + { label: '事业部', prop: 'businessUnitName', width: 200, align: 'left' },
  1457 + {
  1458 + label: '本年数值',
  1459 + prop: 'currentYearValue',
  1460 + width: 150,
  1461 + align: 'right',
  1462 + formatter: (row) => {
  1463 + return Number(row.currentYearValue || 0).toLocaleString()
  1464 + }
  1465 + },
  1466 + {
  1467 + label: '上年数值',
  1468 + prop: 'lastYearValue',
  1469 + width: 150,
  1470 + align: 'right',
  1471 + formatter: (row) => {
  1472 + return Number(row.lastYearValue || 0).toLocaleString()
  1473 + }
  1474 + },
  1475 + {
  1476 + label: '增长率',
  1477 + prop: 'growthRate',
  1478 + width: 120,
  1479 + align: 'right',
  1480 + formatter: (row) => {
  1481 + const rate = parseFloat(row.growthRate) || 0
  1482 + const color = rate >= 0 ? '#67C23A' : '#F56C6C'
  1483 + return `<span style="color: ${color}">${rate.toFixed(2)}%</span>`
  1484 + }
  1485 + }
  1486 + ]
  1487 + },
  1488 + // 构建事业部汇总表格列
  1489 + buildBuSummaryTableColumns() {
  1490 + this.buSummaryTableColumns = [
  1491 + { label: '事业部', prop: 'businessUnitName', width: 120, align: 'left' },
  1492 + { label: '门店', prop: 'storeName', width: 150, align: 'left' },
  1493 + {
  1494 + label: '本年业绩',
  1495 + prop: 'currentPerformance',
  1496 + width: 120,
  1497 + align: 'right',
  1498 + formatter: (row) => {
  1499 + return `¥${Number(row.currentPerformance || 0).toLocaleString()}`
  1500 + }
  1501 + },
  1502 + {
  1503 + label: '上年业绩',
  1504 + prop: 'lastPerformance',
  1505 + width: 120,
  1506 + align: 'right',
  1507 + formatter: (row) => {
  1508 + return `¥${Number(row.lastPerformance || 0).toLocaleString()}`
  1509 + }
  1510 + },
  1511 + {
  1512 + label: '业绩增长率',
  1513 + prop: 'performanceGrowthRate',
  1514 + width: 110,
  1515 + align: 'right',
  1516 + formatter: (row) => {
  1517 + const rate = row.performanceGrowthRate || '0%'
  1518 + const numRate = parseFloat(rate) || 0
  1519 + const color = numRate >= 0 ? '#67C23A' : '#F56C6C'
  1520 + return `<span style="color: ${color}">${rate}</span>`
  1521 + }
  1522 + },
  1523 + {
  1524 + label: '本年消耗',
  1525 + prop: 'currentConsume',
  1526 + width: 120,
  1527 + align: 'right',
  1528 + formatter: (row) => {
  1529 + return `¥${Number(row.currentConsume || 0).toLocaleString()}`
  1530 + }
  1531 + },
  1532 + {
  1533 + label: '上年消耗',
  1534 + prop: 'lastConsume',
  1535 + width: 120,
  1536 + align: 'right',
  1537 + formatter: (row) => {
  1538 + return `¥${Number(row.lastConsume || 0).toLocaleString()}`
  1539 + }
  1540 + },
  1541 + {
  1542 + label: '消耗增长率',
  1543 + prop: 'consumeGrowthRate',
  1544 + width: 110,
  1545 + align: 'right',
  1546 + formatter: (row) => {
  1547 + const rate = row.consumeGrowthRate || '0%'
  1548 + const numRate = parseFloat(rate) || 0
  1549 + const color = numRate >= 0 ? '#67C23A' : '#F56C6C'
  1550 + return `<span style="color: ${color}">${rate}</span>`
  1551 + }
  1552 + },
  1553 + {
  1554 + label: '本年客头',
  1555 + prop: 'currentHeadCount',
  1556 + width: 100,
  1557 + align: 'right',
  1558 + formatter: (row) => {
  1559 + return Number(row.currentHeadCount || 0).toLocaleString()
  1560 + }
  1561 + },
  1562 + {
  1563 + label: '上年客头',
  1564 + prop: 'lastHeadCount',
  1565 + width: 100,
  1566 + align: 'right',
  1567 + formatter: (row) => {
  1568 + return Number(row.lastHeadCount || 0).toLocaleString()
  1569 + }
  1570 + },
  1571 + {
  1572 + label: '客头增长率',
  1573 + prop: 'headCountGrowthRate',
  1574 + width: 110,
  1575 + align: 'right',
  1576 + formatter: (row) => {
  1577 + const rate = row.headCountGrowthRate || '0%'
  1578 + const numRate = parseFloat(rate) || 0
  1579 + const color = numRate >= 0 ? '#67C23A' : '#F56C6C'
  1580 + return `<span style="color: ${color}">${rate}</span>`
  1581 + }
  1582 + },
  1583 + {
  1584 + label: '本年客次',
  1585 + prop: 'currentPersonTime',
  1586 + width: 100,
  1587 + align: 'right',
  1588 + formatter: (row) => {
  1589 + return Number(row.currentPersonTime || 0).toLocaleString()
  1590 + }
  1591 + },
  1592 + {
  1593 + label: '上年客次',
  1594 + prop: 'lastPersonTime',
  1595 + width: 100,
  1596 + align: 'right',
  1597 + formatter: (row) => {
  1598 + return Number(row.lastPersonTime || 0).toLocaleString()
  1599 + }
  1600 + },
  1601 + {
  1602 + label: '客次增长率',
  1603 + prop: 'personTimeGrowthRate',
  1604 + width: 110,
  1605 + align: 'right',
  1606 + formatter: (row) => {
  1607 + const rate = row.personTimeGrowthRate || '0%'
  1608 + const numRate = parseFloat(rate) || 0
  1609 + const color = numRate >= 0 ? '#67C23A' : '#F56C6C'
  1610 + return `<span style="color: ${color}">${rate}</span>`
  1611 + }
  1612 + },
  1613 + {
  1614 + label: '本年项目',
  1615 + prop: 'currentProjectCount',
  1616 + width: 100,
  1617 + align: 'right',
  1618 + formatter: (row) => {
  1619 + return Number(row.currentProjectCount || 0).toLocaleString()
  1620 + }
  1621 + },
  1622 + {
  1623 + label: '上年项目',
  1624 + prop: 'lastProjectCount',
  1625 + width: 100,
  1626 + align: 'right',
  1627 + formatter: (row) => {
  1628 + return Number(row.lastProjectCount || 0).toLocaleString()
  1629 + }
  1630 + },
  1631 + {
  1632 + label: '项目增长率',
  1633 + prop: 'projectCountGrowthRate',
  1634 + width: 110,
  1635 + align: 'right',
  1636 + formatter: (row) => {
  1637 + const rate = row.projectCountGrowthRate || '0%'
  1638 + const numRate = parseFloat(rate) || 0
  1639 + const color = numRate >= 0 ? '#67C23A' : '#F56C6C'
  1640 + return `<span style="color: ${color}">${rate}</span>`
  1641 + }
  1642 + }
  1643 + ]
  1644 + },
  1645 + handleResize() {
  1646 + const chartIds = [
  1647 + 'perfTrend',
  1648 + 'consumeTrend',
  1649 + 'headCountTrend',
  1650 + 'personTimeTrend',
  1651 + 'projectCountTrend',
  1652 + 'performanceChart',
  1653 + 'consumeChart',
  1654 + 'headCountChart',
  1655 + 'personTimeChart',
  1656 + 'projectCountChart',
  1657 + 'storeIndicatorChart',
  1658 + 'storePieChart',
  1659 + 'buPieChart',
  1660 + 'buGrowthChart'
  1661 + ]
  1662 + chartIds.forEach((id) => {
  1663 + const chart = echarts.getInstanceByDom(document.getElementById(id))
  1664 + if (chart) chart.resize()
  1665 + })
  1666 + },
  1667 + destroyAllCharts() {
  1668 + const chartIds = [
  1669 + 'perfTrend',
  1670 + 'consumeTrend',
  1671 + 'headCountTrend',
  1672 + 'personTimeTrend',
  1673 + 'projectCountTrend',
  1674 + 'performanceChart',
  1675 + 'consumeChart',
  1676 + 'headCountChart',
  1677 + 'personTimeChart',
  1678 + 'projectCountChart',
  1679 + 'storeIndicatorChart',
  1680 + 'storePieChart',
  1681 + 'buPieChart',
  1682 + 'buGrowthChart'
  1683 + ]
  1684 + chartIds.forEach((id) => {
  1685 + const chart = echarts.getInstanceByDom(document.getElementById(id))
  1686 + if (chart) chart.dispose()
  1687 + })
  1688 + }
  1689 + }
  1690 +}
  1691 +</script>
  1692 +
  1693 +<style lang="scss" scoped>
  1694 +.annual-summary-dashboard {
  1695 + padding: 20px;
  1696 + background: #f5f7fa;
  1697 + min-height: calc(100vh - 84px);
  1698 +
  1699 + .page-header {
  1700 + background: #fff;
  1701 + border-radius: 8px;
  1702 + padding: 20px 24px;
  1703 + margin-bottom: 20px;
  1704 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  1705 +
  1706 + .header-content {
  1707 + display: flex;
  1708 + justify-content: space-between;
  1709 + align-items: center;
  1710 +
  1711 + .header-title {
  1712 + display: flex;
  1713 + align-items: center;
  1714 + font-size: 18px;
  1715 + font-weight: 600;
  1716 + color: #303133;
  1717 +
  1718 + i {
  1719 + font-size: 24px;
  1720 + color: #409EFF;
  1721 + margin-right: 12px;
  1722 + }
  1723 + }
  1724 +
  1725 + .header-filters {
  1726 + .filter-form {
  1727 + ::v-deep .el-form-item {
  1728 + margin-bottom: 0;
  1729 + }
  1730 + }
  1731 + }
  1732 + }
  1733 + }
  1734 +
  1735 + .dashboard-content {
  1736 + display: flex;
  1737 + gap: 20px;
  1738 +
  1739 + .sidebar-nav {
  1740 + width: 220px;
  1741 + background: #fff;
  1742 + border-radius: 8px;
  1743 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  1744 + padding: 16px 0;
  1745 + height: fit-content;
  1746 + position: sticky;
  1747 + top: 20px;
  1748 +
  1749 + .nav-title {
  1750 + padding: 0 20px 16px;
  1751 + font-size: 16px;
  1752 + font-weight: 600;
  1753 + color: #303133;
  1754 + border-bottom: 1px solid #ebeef5;
  1755 + margin-bottom: 8px;
  1756 + display: flex;
  1757 + align-items: center;
  1758 +
  1759 + i {
  1760 + font-size: 20px;
  1761 + color: #409EFF;
  1762 + margin-right: 8px;
  1763 + }
  1764 + }
  1765 +
  1766 + .nav-menu {
  1767 + border: none;
  1768 +
  1769 + ::v-deep .el-menu-item {
  1770 + height: 48px;
  1771 + line-height: 48px;
  1772 + padding-left: 20px !important;
  1773 +
  1774 + i {
  1775 + margin-right: 8px;
  1776 + font-size: 18px;
  1777 + color: #606266;
  1778 + }
  1779 +
  1780 + &.is-active {
  1781 + background-color: #ecf5ff;
  1782 + color: #409EFF;
  1783 +
  1784 + i {
  1785 + color: #409EFF;
  1786 + }
  1787 + }
  1788 +
  1789 + &:hover {
  1790 + background-color: #f5f7fa;
  1791 + }
  1792 + }
  1793 + }
  1794 + }
  1795 +
  1796 + .main-content {
  1797 + flex: 1;
  1798 + background: #fff;
  1799 + border-radius: 8px;
  1800 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  1801 + padding: 24px;
  1802 +
  1803 + .content-panel {
  1804 + .panel-header {
  1805 + display: flex;
  1806 + align-items: center;
  1807 + font-size: 18px;
  1808 + font-weight: 600;
  1809 + color: #303133;
  1810 + margin-bottom: 24px;
  1811 + padding-bottom: 16px;
  1812 + border-bottom: 2px solid #ebeef5;
  1813 +
  1814 + i {
  1815 + font-size: 24px;
  1816 + color: #409EFF;
  1817 + margin-right: 12px;
  1818 + }
  1819 + }
  1820 +
  1821 + .panel-body {
  1822 + .indicator-controls {
  1823 + padding: 16px 0;
  1824 + border-bottom: 1px solid #ebeef5;
  1825 + margin-bottom: 20px;
  1826 +
  1827 + ::v-deep .el-radio-button__inner {
  1828 + padding: 10px 16px;
  1829 +
  1830 + i {
  1831 + margin-right: 4px;
  1832 + }
  1833 + }
  1834 + }
  1835 +
  1836 +.chart-card {
  1837 + border: 1px solid #ebeef5;
  1838 + border-radius: 8px;
  1839 +
  1840 + .card-header {
  1841 + display: flex;
  1842 + align-items: center;
  1843 + font-size: 16px;
  1844 + font-weight: 600;
  1845 + color: #303133;
  1846 + padding: 16px 20px;
  1847 + border-bottom: 1px solid #ebeef5;
  1848 +
  1849 + i {
  1850 + font-size: 20px;
  1851 + color: #409EFF;
  1852 + margin-right: 8px;
  1853 + }
  1854 + }
  1855 +
  1856 +.chart-box {
  1857 + height: 300px;
  1858 + padding: 20px;
  1859 +}
  1860 +
  1861 +.chart-box-large {
  1862 + height: 400px;
  1863 + padding: 20px;
  1864 + }
  1865 + }
  1866 +
  1867 + .table-card {
  1868 + border: 1px solid #ebeef5;
  1869 + border-radius: 8px;
  1870 +
  1871 + .card-header {
  1872 + display: flex;
  1873 + align-items: center;
  1874 + font-size: 16px;
  1875 + font-weight: 600;
  1876 + color: #303133;
  1877 + padding: 16px 20px;
  1878 + border-bottom: 1px solid #ebeef5;
  1879 +
  1880 + i {
  1881 + font-size: 20px;
  1882 + color: #409EFF;
  1883 + margin-right: 8px;
  1884 + }
  1885 + }
  1886 + }
  1887 +
  1888 + .table-section {
  1889 + ::v-deep .el-table {
  1890 + .el-table__header th {
  1891 + background-color: #f5f7fa;
  1892 + color: #606266;
  1893 + font-weight: 600;
  1894 + }
  1895 +
  1896 + .el-table__body tr:hover > td {
  1897 + background-color: #f5f7fa;
  1898 + }
  1899 + }
  1900 + }
  1901 + }
  1902 + }
  1903 + }
  1904 + }
  1905 +}
  1906 +</style>
... ...
antis-ncc-admin/src/views/extend/annualSummary/dataManage/index.vue 0 → 100644
  1 +<template>
  2 + <div class="annual-summary-data-manage">
  3 + <div class="page-header">
  4 + <div class="header-content">
  5 + <div class="header-title">
  6 + <i class="el-icon-document"></i>
  7 + <span>年度经营数据管理</span>
  8 + </div>
  9 + <div class="header-actions">
  10 + <el-upload
  11 + :action="importUrl"
  12 + :headers="uploadHeaders"
  13 + :show-file-list="false"
  14 + :on-success="handleImportSuccess"
  15 + :before-upload="beforeUpload"
  16 + :on-error="handleImportError"
  17 + accept=".xlsx, .xls"
  18 + >
  19 + <el-button type="primary" icon="el-icon-upload2" size="medium">
  20 + <i class="el-icon-upload2"></i>
  21 + 导入年度经营数据
  22 + </el-button>
  23 + </el-upload>
  24 + <el-tooltip effect="dark" content="刷新" placement="top">
  25 + <el-button icon="el-icon-refresh" circle @click="initData()"></el-button>
  26 + </el-tooltip>
  27 + </div>
  28 + </div>
  29 + </div>
  30 +
  31 + <div class="page-content">
  32 + <div class="search-section">
  33 + <el-form :inline="true" :model="listQuery" class="search-form">
  34 + <el-form-item label="年度">
  35 + <el-date-picker
  36 + v-model="listQuery.year"
  37 + type="year"
  38 + value-format="yyyy"
  39 + placeholder="选择年度"
  40 + clearable
  41 + @change="search()"
  42 + />
  43 + </el-form-item>
  44 + <el-form-item label="月份">
  45 + <el-select
  46 + v-model="listQuery.month"
  47 + placeholder="选择月份"
  48 + clearable
  49 + @change="search()"
  50 + style="width: 150px"
  51 + >
  52 + <el-option v-for="i in 12" :key="i" :label="i + '月'" :value="i" />
  53 + </el-select>
  54 + </el-form-item>
  55 + <el-form-item label="门店名称">
  56 + <el-input
  57 + v-model="listQuery.storeName"
  58 + placeholder="请输入门店名称"
  59 + clearable
  60 + @keyup.enter.native="search()"
  61 + style="width: 200px"
  62 + />
  63 + </el-form-item>
  64 + <el-form-item>
  65 + <el-button type="primary" icon="el-icon-search" @click="search()">查询</el-button>
  66 + <el-button icon="el-icon-refresh-right" @click="reset()">重置</el-button>
  67 + </el-form-item>
  68 + </el-form>
  69 + </div>
  70 +
  71 + <div class="table-section">
  72 + <NCC-table
  73 + v-loading="listLoading"
  74 + :data="list"
  75 + border
  76 + stripe
  77 + :header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: '600' }"
  78 + >
  79 + <el-table-column
  80 + v-for="col in columnOptions"
  81 + :key="col.prop"
  82 + :prop="col.prop"
  83 + :label="col.label"
  84 + :width="col.width"
  85 + :align="col.align || 'center'"
  86 + >
  87 + <template slot-scope="scope">
  88 + <span v-if="col.formatter" v-html="col.formatter(scope.row)"></span>
  89 + <span v-else>{{ scope.row[col.prop] || '-' }}</span>
  90 + </template>
  91 + </el-table-column>
  92 + <el-table-column label="操作" width="120" fixed="right" align="center">
  93 + <template slot-scope="scope">
  94 + <el-button size="mini" type="text" @click="handleEdit(scope.row)">
  95 + <i class="el-icon-edit"></i>
  96 + 编辑
  97 + </el-button>
  98 + <el-button
  99 + size="mini"
  100 + type="text"
  101 + class="NCC-table-delBtn"
  102 + @click="handleDel(scope.row.id)"
  103 + >
  104 + <i class="el-icon-delete"></i>
  105 + 删除
  106 + </el-button>
  107 + </template>
  108 + </el-table-column>
  109 + </NCC-table>
  110 + <pagination
  111 + :total="total"
  112 + :page.sync="listQuery.currentPage"
  113 + :limit.sync="listQuery.pageSize"
  114 + @pagination="initData"
  115 + />
  116 + </div>
  117 + </div>
  118 +
  119 + <!-- 编辑弹窗 -->
  120 + <el-dialog
  121 + :title="form.id ? '编辑数据' : '新增数据'"
  122 + :visible.sync="dialogVisible"
  123 + width="700px"
  124 + :close-on-click-modal="false"
  125 + >
  126 + <el-form ref="dataForm" :model="form" label-width="120px" label-position="right">
  127 + <el-row :gutter="20">
  128 + <el-col :span="12">
  129 + <el-form-item label="门店名称">
  130 + <el-input v-model="form.storeName" disabled />
  131 + </el-form-item>
  132 + </el-col>
  133 + <el-col :span="12">
  134 + <el-form-item label="年度" required>
  135 + <el-input-number v-model="form.year" disabled style="width: 100%" />
  136 + </el-form-item>
  137 + </el-col>
  138 + </el-row>
  139 + <el-row :gutter="20">
  140 + <el-col :span="12">
  141 + <el-form-item label="月份" required>
  142 + <el-input-number v-model="form.month" disabled style="width: 100%" />
  143 + </el-form-item>
  144 + </el-col>
  145 + <el-col :span="12">
  146 + <el-form-item label="事业部">
  147 + <el-input v-model="form.businessUnitName" disabled />
  148 + </el-form-item>
  149 + </el-col>
  150 + </el-row>
  151 + <el-row :gutter="20">
  152 + <el-col :span="12">
  153 + <el-form-item label="本月总业绩">
  154 + <el-input-number
  155 + v-model="form.totalPerformance"
  156 + :precision="2"
  157 + :step="100"
  158 + style="width: 100%"
  159 + />
  160 + </el-form-item>
  161 + </el-col>
  162 + <el-col :span="12">
  163 + <el-form-item label="本月总消耗">
  164 + <el-input-number
  165 + v-model="form.totalConsume"
  166 + :precision="2"
  167 + :step="100"
  168 + style="width: 100%"
  169 + />
  170 + </el-form-item>
  171 + </el-col>
  172 + </el-row>
  173 + <el-row :gutter="20">
  174 + <el-col :span="12">
  175 + <el-form-item label="本月客头数">
  176 + <el-input-number v-model="form.headCount" :step="1" style="width: 100%" />
  177 + </el-form-item>
  178 + </el-col>
  179 + <el-col :span="12">
  180 + <el-form-item label="本月客次数">
  181 + <el-input-number v-model="form.personTime" :step="1" style="width: 100%" />
  182 + </el-form-item>
  183 + </el-col>
  184 + </el-row>
  185 + <el-row :gutter="20">
  186 + <el-col :span="12">
  187 + <el-form-item label="本月总项目数">
  188 + <el-input-number v-model="form.projectCount" :step="1" style="width: 100%" />
  189 + </el-form-item>
  190 + </el-col>
  191 + </el-row>
  192 + </el-form>
  193 + <div slot="footer" class="dialog-footer">
  194 + <el-button @click="dialogVisible = false">取消</el-button>
  195 + <el-button type="primary" @click="handleSave">确定</el-button>
  196 + </div>
  197 + </el-dialog>
  198 + </div>
  199 +</template>
  200 +
  201 +<script>
  202 +import { getList, save, del } from '@/api/extend/annualSummary'
  203 +import { getToken } from '@/utils/auth'
  204 +
  205 +export default {
  206 + name: 'annualSummaryData',
  207 + data() {
  208 + return {
  209 + listLoading: false,
  210 + list: [],
  211 + total: 0,
  212 + listQuery: {
  213 + currentPage: 1,
  214 + pageSize: 20,
  215 + year: null,
  216 + month: null,
  217 + storeName: ''
  218 + },
  219 + columnOptions: [
  220 + { label: '年度', prop: 'year', width: 80, align: 'center' },
  221 + { label: '月份', prop: 'month', width: 80, align: 'center' },
  222 + { label: '门店名称', prop: 'storeName', width: 180, align: 'left' },
  223 + { label: '事业部', prop: 'businessUnitName', width: 150, align: 'left' },
  224 + {
  225 + label: '总业绩',
  226 + prop: 'totalPerformance',
  227 + width: 120,
  228 + align: 'right',
  229 + formatter: (row) => {
  230 + return row.totalPerformance ? `¥${Number(row.totalPerformance).toLocaleString()}` : '¥0'
  231 + }
  232 + },
  233 + {
  234 + label: '总消耗',
  235 + prop: 'totalConsume',
  236 + width: 120,
  237 + align: 'right',
  238 + formatter: (row) => {
  239 + return row.totalConsume ? `¥${Number(row.totalConsume).toLocaleString()}` : '¥0'
  240 + }
  241 + },
  242 + {
  243 + label: '客头数',
  244 + prop: 'headCount',
  245 + width: 100,
  246 + align: 'right',
  247 + formatter: (row) => {
  248 + return row.headCount ? Number(row.headCount).toLocaleString() : '0'
  249 + }
  250 + },
  251 + {
  252 + label: '客次数',
  253 + prop: 'personTime',
  254 + width: 100,
  255 + align: 'right',
  256 + formatter: (row) => {
  257 + return row.personTime ? Number(row.personTime).toLocaleString() : '0'
  258 + }
  259 + },
  260 + {
  261 + label: '总项目数',
  262 + prop: 'projectCount',
  263 + width: 100,
  264 + align: 'right',
  265 + formatter: (row) => {
  266 + return row.projectCount ? Number(row.projectCount).toLocaleString() : '0'
  267 + }
  268 + }
  269 + ],
  270 + dialogVisible: false,
  271 + form: {},
  272 + importUrl: process.env.VUE_APP_BASE_API + '/api/Extend/LqAnnualSummary/import',
  273 + uploadHeaders: { Authorization: getToken() }
  274 + }
  275 + },
  276 + created() {
  277 + this.initData()
  278 + },
  279 + methods: {
  280 + initData() {
  281 + this.listLoading = true
  282 + getList(this.listQuery)
  283 + .then((res) => {
  284 + this.list = res.data.list
  285 + this.total = res.data.pagination.total
  286 + this.listLoading = false
  287 + })
  288 + .catch(() => {
  289 + this.listLoading = false
  290 + })
  291 + },
  292 + search() {
  293 + this.listQuery.currentPage = 1
  294 + this.initData()
  295 + },
  296 + reset() {
  297 + this.listQuery = {
  298 + currentPage: 1,
  299 + pageSize: 20,
  300 + year: null,
  301 + month: null,
  302 + storeName: ''
  303 + }
  304 + this.initData()
  305 + },
  306 + handleDel(id) {
  307 + this.$confirm('确定要删除选中的数据吗?', '提示', {
  308 + type: 'warning'
  309 + })
  310 + .then(() => {
  311 + del([id]).then((res) => {
  312 + this.$message.success('删除成功')
  313 + this.initData()
  314 + })
  315 + })
  316 + .catch(() => {})
  317 + },
  318 + handleEdit(row) {
  319 + this.form = { ...row }
  320 + this.dialogVisible = true
  321 + },
  322 + handleSave() {
  323 + save(this.form).then((res) => {
  324 + this.$message.success('保存成功')
  325 + this.dialogVisible = false
  326 + this.initData()
  327 + })
  328 + },
  329 + beforeUpload(file) {
  330 + const isExcel = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
  331 + file.type === 'application/vnd.ms-excel'
  332 + if (!isExcel) {
  333 + this.$message.error('只能上传 Excel 文件!')
  334 + return false
  335 + }
  336 + this.listLoading = true
  337 + return true
  338 + },
  339 + handleImportSuccess(res) {
  340 + this.listLoading = false
  341 + if (res.code === 200) {
  342 + const data = res.data || {}
  343 + const successCount = data.successCount || 0
  344 + const failCount = data.failCount || 0
  345 + if (failCount > 0) {
  346 + const errorMsg = data.errorMessages ? '。' + data.errorMessages : ''
  347 + this.$message.warning(`导入完成:成功 ${successCount} 条,失败 ${failCount} 条${errorMsg}`)
  348 + } else {
  349 + this.$message.success(`导入成功,共导入 ${successCount} 条数据`)
  350 + }
  351 + this.initData()
  352 + } else {
  353 + this.$message.error(res.msg || '导入失败')
  354 + }
  355 + },
  356 + handleImportError(err) {
  357 + this.listLoading = false
  358 + this.$message.error('导入失败,请检查文件格式')
  359 + }
  360 + }
  361 +}
  362 +</script>
  363 +
  364 +<style lang="scss" scoped>
  365 +.annual-summary-data-manage {
  366 + padding: 20px;
  367 + background: #f5f7fa;
  368 + min-height: calc(100vh - 84px);
  369 +
  370 + .page-header {
  371 + background: #fff;
  372 + border-radius: 8px;
  373 + padding: 20px 24px;
  374 + margin-bottom: 20px;
  375 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  376 +
  377 + .header-content {
  378 + display: flex;
  379 + justify-content: space-between;
  380 + align-items: center;
  381 +
  382 + .header-title {
  383 + display: flex;
  384 + align-items: center;
  385 + font-size: 18px;
  386 + font-weight: 600;
  387 + color: #303133;
  388 +
  389 + i {
  390 + font-size: 24px;
  391 + color: #409EFF;
  392 + margin-right: 12px;
  393 + }
  394 + }
  395 +
  396 + .header-actions {
  397 + display: flex;
  398 + align-items: center;
  399 + gap: 12px;
  400 + }
  401 + }
  402 + }
  403 +
  404 + .page-content {
  405 + background: #fff;
  406 + border-radius: 8px;
  407 + padding: 24px;
  408 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  409 +
  410 + .search-section {
  411 + margin-bottom: 20px;
  412 + padding-bottom: 20px;
  413 + border-bottom: 1px solid #ebeef5;
  414 +
  415 + .search-form {
  416 + ::v-deep .el-form-item {
  417 + margin-bottom: 0;
  418 + }
  419 + }
  420 + }
  421 +
  422 + .table-section {
  423 + ::v-deep .el-table {
  424 + .el-table__header th {
  425 + background-color: #f5f7fa;
  426 + color: #606266;
  427 + font-weight: 600;
  428 + }
  429 +
  430 + .el-table__body tr:hover > td {
  431 + background-color: #f5f7fa;
  432 + }
  433 + }
  434 + }
  435 + }
  436 +}
  437 +</style>
... ...
antis-ncc-admin/src/views/lqTkjlb/Report.vue
... ... @@ -652,6 +652,7 @@
652 652 <el-table :data="storeCustomerDetails" stripe style="width: 100%">
653 653 <el-table-column prop="customer_name" label="客户姓名" align="center" />
654 654 <el-table-column prop="customer_phone" label="客户电话" align="center" width="120"/>
  655 + <el-table-column prop="expansion_user_name" label="拓客人员" align="center" width="120"/>
655 656 <el-table-column prop="invitation_status" label="邀约状态" align="center" />
656 657 <el-table-column prop="appointment_status" label="预约状态" align="center" />
657 658 <el-table-column prop="consume_status" label="耗卡状态" align="center" />
... ...
antis-ncc-admin/src/views/personalPerformanceStatistics/index.vue
... ... @@ -41,13 +41,10 @@
41 41  
42 42 <!-- 表格卡片 -->
43 43 <el-card class="table-card">
44   - <div slot="header" class="clearfix">
45   - <span><i class="el-icon-s-grid"></i> 健康师个人开单业绩列表</span>
46   - </div>
47 44 <div class="table-container">
48 45 <el-table :data="tableData" v-loading="loading" element-loading-text="加载中..." :height="tableHeight"
49 46 border stripe style="width: 100%">
50   - <el-table-column prop="EmployeeName" label="员工姓名" width="120" fixed="left"></el-table-column>
  47 + <el-table-column prop="EmployeeName" label="员工姓名" fixed="left"></el-table-column>
51 48 <el-table-column prop="StoreName" label="门店名称" width="150" fixed="left"></el-table-column>
52 49 <el-table-column prop="Position" label="岗位" width="100" fixed="left"></el-table-column>
53 50 <el-table-column prop="GoldTriangleName" label="金三角战队" width="120" fixed="left"></el-table-column>
... ... @@ -79,7 +76,7 @@
79 76 <el-table-column prop="ActualPerformance" label="实际业绩" width="100" align="right">
80 77 <template slot-scope="scope">
81 78 <span style="color: #67C23A; font-weight: bold;">{{ formatMoney(scope.row.ActualPerformance)
82   - }}</span>
  79 + }}</span>
83 80 </template>
84 81 </el-table-column>
85 82 <el-table-column prop="FirstOrderCount" label="首开单数量" width="100" align="right"></el-table-column>
... ...
antis-ncc-admin/src/views/salaryCalculation/index.vue
... ... @@ -58,7 +58,7 @@
58 58 </div>
59 59 </div>
60 60  
61   - <div class="stat-item green-item">
  61 + <div class="stat-item green-item" style="display: none;">
62 62 <div class="stat-header">
63 63 <i class="el-icon-user"></i>
64 64 <span>健康师个人开单业绩</span>
... ... @@ -127,7 +127,7 @@
127 127 </div>
128 128 </div>
129 129  
130   - <div class="stat-item red-item">
  130 + <div class="stat-item red-item" style="display: none;">
131 131 <div class="stat-header">
132 132 <i class="el-icon-s-finance"></i>
133 133 <span>门店总业绩</span>
... ... @@ -144,7 +144,7 @@
144 144 </div>
145 145 </div>
146 146  
147   - <div class="stat-item gold-item">
  147 + <div class="stat-item gold-item" style="display: none;">
148 148 <div class="stat-header">
149 149 <i class="el-icon-money"></i>
150 150 <span>工资统计</span>
... ... @@ -161,7 +161,7 @@
161 161 </div>
162 162  
163 163 <!-- 预留位置,用于未来添加更多统计方法 -->
164   - <div class="stat-item placeholder-item">
  164 + <div class="stat-item placeholder-item" style="display: none;">
165 165 <div class="stat-header">
166 166 <i class="el-icon-plus"></i>
167 167 <span>更多统计方法</span>
... ...
antis-ncc-admin/src/views/statisticsList/form9_backup.vue 0 → 100644
  1 +<template>
  2 + <div class="business-report-page">
  3 + <!-- 筛选条件 -->
  4 + <div class="search-box-container">
  5 + <el-card class="report-card" shadow="never">
  6 + <el-form @submit.native.prevent :inline="true" class="search-form">
  7 + <el-form-item label="时间范围">
  8 + <el-date-picker
  9 + v-model="query.startTime"
  10 + type="datetime"
  11 + value-format="yyyy-MM-dd HH:mm:ss"
  12 + format="yyyy-MM-dd HH:mm:ss"
  13 + placeholder="开始时间"
  14 + style="width: 200px;"
  15 + />
  16 + <span style="margin: 0 8px; color: #DCDFE6;">-</span>
  17 + <el-date-picker
  18 + v-model="query.endTime"
  19 + type="datetime"
  20 + value-format="yyyy-MM-dd HH:mm:ss"
  21 + format="yyyy-MM-dd HH:mm:ss"
  22 + placeholder="结束时间"
  23 + style="width: 200px;"
  24 + />
  25 + </el-form-item>
  26 + <el-form-item label="门店">
  27 + <el-select
  28 + v-model="query.storeIds"
  29 + multiple
  30 + collapse-tags
  31 + placeholder="全部门店"
  32 + filterable
  33 + clearable
  34 + style="width: 260px;">
  35 + <el-option
  36 + v-for="store in storeOptions"
  37 + :key="store.id"
  38 + :label="store.dm"
  39 + :value="store.id">
  40 + </el-option>
  41 + </el-select>
  42 + </el-form-item>
  43 + <el-form-item>
  44 + <el-button type="primary" icon="el-icon-search" @click="search()">查 询</el-button>
  45 + <el-button icon="el-icon-refresh-right" @click="reset()">重 置</el-button>
  46 + </el-form-item>
  47 + </el-form>
  48 + </el-card>
  49 + </div>
  50 +
  51 + <!-- 第一个报表:业务统计仪表盘 -->
  52 + <el-card class="report-card" shadow="never" v-loading="loading1">
  53 + <div slot="header" class="card-header">
  54 + <i class="el-icon-data-analysis"></i>
  55 + <span>业务统计</span>
  56 + </div>
  57 + <div class="gauge-dashboard" v-if="businessData">
  58 + <el-row :gutter="24">
  59 + <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" v-for="(item, index) in gaugeItems" :key="index">
  60 + <div class="gauge-item-wrapper">
  61 + <div class="gauge-item">
  62 + <div class="gauge-chart-container">
  63 + <div :id="`gaugeChart${index + 1}`" class="gauge-chart"></div>
  64 + <div class="gauge-value-overlay">
  65 + <div class="gauge-value-large">{{ item.displayValue }}</div>
  66 + <div class="gauge-label-text">{{ item.title }}</div>
  67 + </div>
  68 + </div>
  69 + </div>
  70 + </div>
  71 + </el-col>
  72 + </el-row>
  73 + </div>
  74 + </el-card>
  75 +
  76 + <!-- 第二个报表:客户类型统计 -->
  77 + <el-card class="report-card" shadow="never" v-loading="loading2">
  78 + <div slot="header" class="card-header">
  79 + <i class="el-icon-user"></i>
  80 + <span>客户类型统计</span>
  81 + </div>
  82 + <div class="customer-stats-container" v-if="customerData">
  83 + <el-row :gutter="24">
  84 + <el-col :xs="24" :sm="12" :md="8" :lg="4" :xl="4">
  85 + <div class="stat-card stat-card-purple">
  86 + <div class="stat-icon">
  87 + <i class="el-icon-star-on"></i>
  88 + </div>
  89 + <div class="stat-content">
  90 + <div class="stat-label">拓客总人数</div>
  91 + <div class="stat-value">{{ customerData.TotalInviteCount || 0 }}</div>
  92 + </div>
  93 + </div>
  94 + </el-col>
  95 + <el-col :xs="24" :sm="12" :md="8" :lg="4" :xl="4" style="margin-bottom: 24px;">
  96 + <div class="stat-card stat-card-blue">
  97 + <div class="stat-icon">
  98 + <i class="el-icon-s-custom"></i>
  99 + </div>
  100 + <div class="stat-content">
  101 + <div class="stat-label">新客数量</div>
  102 + <div class="stat-value">{{ customerData.NewCustomerCount || 0 }}</div>
  103 + </div>
  104 + </div>
  105 + </el-col>
  106 + <el-col :xs="24" :sm="12" :md="8" :lg="4" :xl="4" style="margin-bottom: 24px;">
  107 + <div class="stat-card stat-card-cyan">
  108 + <div class="stat-icon">
  109 + <i class="el-icon-user-solid"></i>
  110 + </div>
  111 + <div class="stat-content">
  112 + <div class="stat-label">散客数</div>
  113 + <div class="stat-value">{{ customerData.CasualCustomerCount || 0 }}</div>
  114 + </div>
  115 + </div>
  116 + </el-col>
  117 + <el-col :xs="24" :sm="12" :md="8" :lg="4" :xl="4" style="margin-bottom: 24px;">
  118 + <div class="stat-card stat-card-green">
  119 + <div class="stat-icon">
  120 + <i class="el-icon-user"></i>
  121 + </div>
  122 + <div class="stat-content">
  123 + <div class="stat-label">会员数</div>
  124 + <div class="stat-value">{{ customerData.MemberCount || 0 }}</div>
  125 + </div>
  126 + </div>
  127 + </el-col>
  128 + <el-col :xs="24" :sm="12" :md="8" :lg="4" :xl="4" style="margin-bottom: 24px;">
  129 + <div class="conversion-rate-card">
  130 + <div class="donut-wrapper">
  131 + <div class="donut-chart" :style="getDonutStyle(customerData.MemberCount / customerData.TotalInviteCount)">
  132 + <div class="donut-center">
  133 + <div class="conversion-title">会员转化率</div>
  134 + <div class="percentage">{{ formatPercent((customerData.MemberCount / customerData.TotalInviteCount) * 100) }}%</div>
  135 + </div>
  136 + </div>
  137 + </div>
  138 + </div>
  139 + </el-col>
  140 + </el-row>
  141 + </div>
  142 + </el-card>
  143 +
  144 + <!-- 第三个和第四个报表:并排显示 -->
  145 + <el-row :gutter="24">
  146 + <!-- 第三个报表:门店业绩对比(柱状图) -->
  147 + <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
  148 + <el-card class="report-card" shadow="never" v-loading="loading3">
  149 + <div slot="header" class="card-header">
  150 + <i class="el-icon-trophy"></i>
  151 + <span>门店业绩对比</span>
  152 + </div>
  153 + <div
  154 + id="storePerformanceChart"
  155 + class="chart-container-half"
  156 + v-if="performanceData && performanceData.length > 0">
  157 + </div>
  158 + </el-card>
  159 + </el-col>
  160 +
  161 + <!-- 第四个报表:品项统计对比(蝴蝶图) -->
  162 + <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
  163 + <el-card class="report-card" shadow="never" v-loading="loading4">
  164 + <div slot="header" class="card-header">
  165 + <i class="el-icon-goods"></i>
  166 + <span>品项统计对比</span>
  167 + </div>
  168 + <div class="butterfly-chart-container" v-if="itemStatisticsData && itemStatisticsData.length > 0">
  169 + <!-- 图例 -->
  170 + <div class="chart-legend">
  171 + <div class="legend-item">
  172 + <span class="legend-color legend-purple"></span>
  173 + <span class="legend-text">开单金额</span>
  174 + </div>
  175 + <div class="legend-item">
  176 + <span class="legend-color legend-orange"></span>
  177 + <span class="legend-text">消耗金额</span>
  178 + </div>
  179 + </div>
  180 +
  181 + <!-- 图表内容 -->
  182 + <div class="butterfly-chart-content">
  183 + <div class="chart-left">
  184 + <div
  185 + v-for="(item, index) in sortedItemData"
  186 + :key="index"
  187 + class="chart-row">
  188 + <div
  189 + class="bar-left"
  190 + :style="{width: getButterflyBarWidth(item.BillingAmount, maxValue) + '%'}">
  191 + <span class="bar-value">{{ item.BillingAmount || 0 }}</span>
  192 + </div>
  193 + </div>
  194 + </div>
  195 +
  196 + <div class="chart-center">
  197 + <div
  198 + v-for="(item, index) in sortedItemData"
  199 + :key="index"
  200 + class="category-name">
  201 + {{ item.ItemName || item.ItemId }}
  202 + </div>
  203 + </div>
  204 +
  205 + <div class="chart-right">
  206 + <div
  207 + v-for="(item, index) in sortedItemData"
  208 + :key="index"
  209 + class="chart-row">
  210 + <div
  211 + class="bar-right"
  212 + :style="{width: getButterflyBarWidth(item.ConsumeAmount, maxValue) + '%'}">
  213 + <span class="bar-value">{{ item.ConsumeAmount || 0 }}</span>
  214 + </div>
  215 + </div>
  216 + </div>
  217 + </div>
  218 + </div>
  219 + </el-card>
  220 + </el-col>
  221 + </el-row>
  222 +
  223 + <!-- 第六个和第九个报表:并排显示 -->
  224 + <el-row :gutter="24">
  225 + <!-- 第六个报表:客户到店次数分布 -->
  226 + <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
  227 + <el-card class="report-card" shadow="never" v-loading="loading6">
  228 + <div slot="header" class="card-header">
  229 + <i class="el-icon-s-marketing"></i>
  230 + <span>客户到店次数分布</span>
  231 + </div>
  232 + <div
  233 + id="customerVisitFrequencyChart"
  234 + class="chart-container-half"
  235 + v-if="customerVisitFrequencyData && customerVisitFrequencyData.length > 0">
  236 + </div>
  237 + <div v-else style="text-align: center; padding: 40px; color: #909399;">
  238 + 暂无数据
  239 + </div>
  240 + </el-card>
  241 + </el-col>
  242 +
  243 + <!-- 第九个报表:拓客转化漏斗图 -->
  244 + <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
  245 + <el-card class="report-card" shadow="never" v-loading="loading9">
  246 + <div slot="header" class="card-header">
  247 + <i class="el-icon-data-line"></i>
  248 + <span>拓客转化漏斗图</span>
  249 + </div>
  250 + <div v-if="tkStatisticsData" class="funnel-container">
  251 + <!-- 漏斗图 -->
  252 + <div id="tkFunnelChart" class="funnel-chart-container-half"></div>
  253 +
  254 + <!-- 转化率指标卡片 -->
  255 + <div class="conversion-rate-cards">
  256 + <div class="conversion-card">
  257 + <div class="conversion-label">拓客邀约率</div>
  258 + <div class="conversion-value">{{ formatPercent(getTkInviteRate()) }}%</div>
  259 + <!-- <div class="conversion-detail">{{ tkStatisticsData.YaoyCount || 0 }} / {{ tkStatisticsData.TkCount || 0 }}</div> -->
  260 + </div>
  261 + <div class="conversion-card">
  262 + <div class="conversion-label">邀约到店率</div>
  263 + <div class="conversion-value">{{ formatPercent(getInviteStoreRate()) }}%</div>
  264 + <!-- <div class="conversion-detail">{{ tkStatisticsData.DdCount || 0 }} / {{ tkStatisticsData.YaoyCount || 0 }}</div> -->
  265 + </div>
  266 + <div class="conversion-card">
  267 + <div class="conversion-label">预约转化率</div>
  268 + <div class="conversion-value">{{ formatPercent(getStoreConversionRate()) }}%</div>
  269 + <!-- <div class="conversion-detail">{{ tkStatisticsData.XfCount || 0 }} / {{ tkStatisticsData.YyCount || 0 }}</div> -->
  270 + </div>
  271 + </div>
  272 + </div>
  273 + <div v-else style="text-align: center; padding: 40px; color: #909399;">
  274 + 暂无数据
  275 + </div>
  276 + </el-card>
  277 + </el-col>
  278 + </el-row>
  279 + <!-- 第五个报表:门店项目统计 -->
  280 + <el-card class="report-card" shadow="never" v-loading="loading5">
  281 + <div slot="header" class="card-header">
  282 + <div style="display: flex; align-items: center; justify-content: space-between; width: 100%;">
  283 + <div style="display: flex; align-items: center;">
  284 + <i class="el-icon-s-data"></i>
  285 + <span>门店项目统计</span>
  286 + </div>
  287 + <el-button
  288 + type="text"
  289 + icon="el-icon-setting"
  290 + style="padding: 0; margin-left: 20px;"
  291 + @click="showFieldConfigDialog = true">
  292 + 字段配置
  293 + </el-button>
  294 + </div>
  295 + </div>
  296 + <div v-if="storeItemStatisticsData && storeItemStatisticsData.length > 0">
  297 + <el-table
  298 + :data="storeItemStatisticsData"
  299 + border
  300 + stripe
  301 + style="width: 100%"
  302 + :header-cell-style="{ background: '#f5f7fa', color: '#303133', fontWeight: '600' }"
  303 + @sort-change="handleSortChange">
  304 + <el-table-column
  305 + v-if="isFieldVisible('StoreName')"
  306 + prop="StoreName"
  307 + label="门店名称"
  308 + min-width="150"
  309 + align="center">
  310 + </el-table-column>
  311 + <el-table-column
  312 + v-if="isFieldVisible('BillingAmount')"
  313 + prop="BillingAmount"
  314 + label="开单金额"
  315 + min-width="120"
  316 + align="center"
  317 + sortable="custom">
  318 + <template slot-scope="scope">
  319 + <span>¥{{ formatMoney(scope.row.BillingAmount) }}</span>
  320 + </template>
  321 + </el-table-column>
  322 + <el-table-column
  323 + v-if="isFieldVisible('ConsumePersonCount')"
  324 + prop="ConsumePersonCount"
  325 + label="消耗人次"
  326 + min-width="120"
  327 + align="center"
  328 + sortable="custom">
  329 + </el-table-column>
  330 + <el-table-column
  331 + v-if="isFieldVisible('ConsumeProjectCount')"
  332 + prop="ConsumeProjectCount"
  333 + label="消耗项目数"
  334 + min-width="120"
  335 + align="center"
  336 + sortable="custom">
  337 + </el-table-column>
  338 + <el-table-column
  339 + v-if="isFieldVisible('AvgProjectPerConsume')"
  340 + prop="AvgProjectPerConsume"
  341 + label="客单项目数"
  342 + min-width="120"
  343 + align="center"
  344 + sortable="custom">
  345 + <template slot-scope="scope">
  346 + <span>{{ formatMoney(scope.row.AvgProjectPerConsume) }}</span>
  347 + </template>
  348 + </el-table-column>
  349 + <el-table-column
  350 + v-if="isFieldVisible('ConsumeAmount')"
  351 + prop="ConsumeAmount"
  352 + label="消耗金额"
  353 + min-width="120"
  354 + align="center"
  355 + sortable="custom">
  356 + <template slot-scope="scope">
  357 + <span>¥{{ formatMoney(scope.row.ConsumeAmount) }}</span>
  358 + </template>
  359 + </el-table-column>
  360 + <el-table-column
  361 + v-if="isFieldVisible('AvgAmountPerConsume')"
  362 + prop="AvgAmountPerConsume"
  363 + label="消耗客单价"
  364 + min-width="120"
  365 + align="center"
  366 + sortable="custom">
  367 + <template slot-scope="scope">
  368 + <span>¥{{ formatMoney(scope.row.AvgAmountPerConsume) }}</span>
  369 + </template>
  370 + </el-table-column>
  371 + <el-table-column
  372 + v-if="isFieldVisible('ConsumeRate')"
  373 + prop="ConsumeRate"
  374 + label="消耗率"
  375 + min-width="120"
  376 + align="center"
  377 + sortable="custom">
  378 + <template slot-scope="scope">
  379 + <span>{{ formatPercent((scope.row.ConsumeRate || 0) ) }}%</span>
  380 + </template>
  381 + </el-table-column>
  382 + </el-table>
  383 + </div>
  384 + <div v-else style="text-align: center; padding: 40px; color: #909399;">
  385 + 暂无数据
  386 + </div>
  387 +
  388 + <!-- 字段配置对话框 -->
  389 + <el-dialog
  390 + title="字段配置"
  391 + :visible.sync="showFieldConfigDialog"
  392 + width="500px"
  393 + :close-on-click-modal="false">
  394 + <div class="field-config-content">
  395 + <div class="field-config-actions" style="margin-bottom: 15px;">
  396 + <el-button type="text" size="small" @click="selectAllFields">全选</el-button>
  397 + <el-button type="text" size="small" @click="unselectAllFields">全不选</el-button>
  398 + </div>
  399 + <el-checkbox-group v-model="selectedFields">
  400 + <div
  401 + v-for="field in availableFields"
  402 + :key="field.prop"
  403 + class="field-config-item">
  404 + <el-checkbox :label="field.prop">{{ field.label }}</el-checkbox>
  405 + </div>
  406 + </el-checkbox-group>
  407 + </div>
  408 + <span slot="footer" class="dialog-footer">
  409 + <el-button @click="showFieldConfigDialog = false">取消</el-button>
  410 + <el-button type="primary" @click="confirmFieldConfig">确定</el-button>
  411 + </span>
  412 + </el-dialog>
  413 + </el-card>
  414 +
  415 +
  416 + <!-- 第七个报表:健康师开单排名 -->
  417 + <el-card shadow="never" class="report-card" v-loading="loading7">
  418 + <div slot="header" class="card-header">
  419 + <i class="el-icon-trophy"></i>
  420 + <span>健康师开单排名</span>
  421 + </div>
  422 + <div v-if="healthCoachBillingRankingData && healthCoachBillingRankingData.length > 0">
  423 + <el-table
  424 + :data="healthCoachBillingRankingData"
  425 + border
  426 + stripe
  427 + style="width: 100%"
  428 + :header-cell-style="{ background: '#f5f7fa', color: '#303133', fontWeight: '600' }">
  429 + <el-table-column type="index" label="排名" width="80" align="center" :index="getRankIndex"></el-table-column>
  430 + <el-table-column prop="HealthCoachName" label="健康师姓名" min-width="150" align="center"></el-table-column>
  431 + <el-table-column prop="BillingPerformance" label="开单业绩" min-width="120" align="center">
  432 + <template slot-scope="scope">
  433 + <span>¥{{ formatMoney(scope.row.BillingPerformance) }}</span>
  434 + </template>
  435 + </el-table-column>
  436 + <el-table-column prop="ConsumePerformance" label="消耗业绩" min-width="120" align="center">
  437 + <template slot-scope="scope">
  438 + <span>¥{{ formatMoney(scope.row.ConsumePerformance) }}</span>
  439 + </template>
  440 + </el-table-column>
  441 + <el-table-column prop="RefundPerformance" label="退款业绩" min-width="120" align="center">
  442 + <template slot-scope="scope">
  443 + <span>¥{{ formatMoney(scope.row.RefundPerformance) }}</span>
  444 + </template>
  445 + </el-table-column>
  446 + <el-table-column prop="BillingProjectCount" label="开单项目数" min-width="120" align="center"></el-table-column>
  447 + <el-table-column prop="ConsumeProjectCount" label="消耗项目数" min-width="120" align="center"></el-table-column>
  448 + <el-table-column prop="RefundProjectCount" label="退款项目数" min-width="120" align="center"></el-table-column>
  449 + </el-table>
  450 + </div>
  451 + <div v-else style="text-align: center; padding: 40px; color: #909399;">
  452 + 暂无数据
  453 + </div>
  454 + </el-card>
  455 +
  456 + <!-- 第八个报表:门店剩余权益统计 -->
  457 + <el-card class="report-card" shadow="never" v-loading="loading8">
  458 + <div slot="header" class="card-header">
  459 + <i class="el-icon-wallet"></i>
  460 + <span>门店剩余权益统计</span>
  461 + </div>
  462 + <div v-if="storeRemainingRightsData && storeRemainingRightsData.length > 0">
  463 + <el-table
  464 + :data="storeRemainingRightsData"
  465 + border
  466 + stripe
  467 + style="width: 100%"
  468 + :header-cell-style="{ background: '#f5f7fa', color: '#303133', fontWeight: '600' }">
  469 + <el-table-column prop="StoreName" label="门店名称" min-width="200" align="center"></el-table-column>
  470 + <el-table-column prop="RemainingRightsAmount" label="剩余权益累计金额" min-width="180" align="center">
  471 + <template slot-scope="scope">
  472 + <span>¥{{ formatMoney(scope.row.RemainingRightsAmount) }}</span>
  473 + </template>
  474 + </el-table-column>
  475 + <el-table-column prop="RemainingRightsPersonCount" label="剩余权益累计人数" min-width="180" align="center"></el-table-column>
  476 + </el-table>
  477 + </div>
  478 + <div v-else style="text-align: center; padding: 40px; color: #909399;">
  479 + 暂无数据
  480 + </div>
  481 + </el-card>
  482 + </div>
  483 +</template>
  484 +
  485 +<script>
  486 +import request from '@/utils/request'
  487 +import * as echarts from 'echarts'
  488 +
  489 +export default {
  490 + name: 'BusinessReport',
  491 + data() {
  492 + return {
  493 + query: {
  494 + startTime: undefined,
  495 + endTime: undefined,
  496 + storeIds: []
  497 + },
  498 + storeOptions: [],
  499 + businessData: null,
  500 + customerData: null,
  501 + performanceData: [],
  502 + itemStatisticsData: [],
  503 + storeItemStatisticsData: [],
  504 + customerVisitFrequencyData: [],
  505 + healthCoachBillingRankingData: [],
  506 + storeRemainingRightsData: [],
  507 + tkStatisticsData: null,
  508 + loading1: false,
  509 + loading2: false,
  510 + loading3: false,
  511 + loading4: false,
  512 + loading5: false,
  513 + loading6: false,
  514 + loading7: false,
  515 + loading8: false,
  516 + loading9: false,
  517 + gaugeItems: [],
  518 + // 门店项目统计字段配置
  519 + showFieldConfigDialog: false,
  520 + availableFields: [
  521 + { prop: 'StoreName', label: '门店名称' },
  522 + { prop: 'BillingAmount', label: '开单金额' },
  523 + { prop: 'ConsumePersonCount', label: '消耗人次' },
  524 + { prop: 'ConsumeProjectCount', label: '消耗项目数' },
  525 + { prop: 'AvgProjectPerConsume', label: '客单项目数' },
  526 + { prop: 'ConsumeAmount', label: '消耗金额' },
  527 + { prop: 'AvgAmountPerConsume', label: '消耗客单价' },
  528 + { prop: 'ConsumeRate', label: '消耗率' }
  529 + ],
  530 + selectedFields: ['StoreName', 'BillingAmount', 'ConsumePersonCount', 'ConsumeProjectCount', 'AvgProjectPerConsume', 'ConsumeAmount', 'AvgAmountPerConsume', 'ConsumeRate'],
  531 + sortParams: {
  532 + prop: null,
  533 + order: null
  534 + },
  535 + originalStoreItemStatisticsData: [] // 保存原始数据,用于取消排序时恢复
  536 + }
  537 + },
  538 + computed: {
  539 + sortedItemData() {
  540 + if (!this.itemStatisticsData || this.itemStatisticsData.length === 0) return []
  541 + return [...this.itemStatisticsData]
  542 + .sort((a, b) => (b.BillingAmount || 0) - (a.BillingAmount || 0))
  543 + .slice(0, 10)
  544 + },
  545 + maxValue() {
  546 + if (!this.itemStatisticsData || this.itemStatisticsData.length === 0) return 1
  547 + const maxBilling = Math.max(...this.itemStatisticsData.map(item => item.BillingAmount || 0))
  548 + const maxConsume = Math.max(...this.itemStatisticsData.map(item => item.ConsumeAmount || 0))
  549 + return Math.max(maxBilling, maxConsume)
  550 + }
  551 + },
  552 + created() {
  553 + this.setDefaultTimeRange()
  554 + this.initStoreOptions()
  555 + this.$nextTick(() => {
  556 + this.search()
  557 + })
  558 + },
  559 + methods: {
  560 + // 设置默认时间范围(本月1号到现在)
  561 + setDefaultTimeRange() {
  562 + const now = new Date()
  563 + const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
  564 +
  565 + this.query.startTime = this.formatDateTime(firstDayOfMonth.getTime())
  566 + this.query.endTime = this.formatDateTime(now.getTime())
  567 + },
  568 +
  569 + // 初始化门店选项
  570 + initStoreOptions() {
  571 + return request({
  572 + url: '/api/Extend/LqMdxx',
  573 + method: 'GET',
  574 + data: {
  575 + currentPage: 1,
  576 + pageSize: 1000
  577 + }
  578 + }).then(res => {
  579 + this.storeOptions = res.data.list || []
  580 + return this.storeOptions
  581 + }).catch(err => {
  582 + console.error('获取门店列表失败:', err)
  583 + this.storeOptions = []
  584 + return []
  585 + })
  586 + },
  587 +
  588 + // 查询所有数据
  589 + async search() {
  590 + await this.loadHealthCoachBillingRanking()
  591 + await this.loadBusinessStatistics()
  592 + await this.loadCustomerTypeStatistics()
  593 + await this.loadStorePerformanceComparison()
  594 + await this.loadStoreItemStatistics()
  595 + await this.loadCustomerVisitFrequency()
  596 + await this.loadTkStatistics()
  597 + //await this.loadStoreRemainingRights()
  598 + },
  599 +
  600 + // 获取业务统计数据
  601 + loadBusinessStatistics() {
  602 + this.loading1 = true
  603 + const params = {
  604 + startTime: this.query.startTime,
  605 + endTime: this.query.endTime,
  606 + storeIds: this.query.storeIds || []
  607 + }
  608 +
  609 + request({
  610 + url: '/api/Extend/lqreport/get-business-statistics',
  611 + method: 'POST',
  612 + data: params
  613 + }).then(res => {
  614 + this.businessData = res.data || null
  615 + if (this.businessData) {
  616 + this.initGaugeItems()
  617 + this.$nextTick(() => {
  618 + this.initGaugeCharts()
  619 + })
  620 + }
  621 + this.loading1 = false
  622 + }).catch(err => {
  623 + console.error('获取业务统计数据失败:', err)
  624 + this.$message.error('获取业务统计数据失败')
  625 + this.loading1 = false
  626 + })
  627 + },
  628 +
  629 + // 初始化仪表盘数据
  630 + initGaugeItems() {
  631 + const billingValue = this.businessData.TotalBillingAmount - this.businessData.TotalRefundAmount || 0
  632 + const countValue = this.businessData.BillingCount || 0
  633 + const consumeValue = this.businessData.TotalConsumeAmount || 0
  634 + const targetValue = this.businessData.TargetConsumeAmount || 0
  635 + const completionRate = this.businessData.BillingCompletionRate || 0
  636 +
  637 + this.gaugeItems = [
  638 + {
  639 + title: '现金业绩',
  640 + icon: 'el-icon-wallet',
  641 + displayValue: `¥${this.formatMoney(billingValue)}`,
  642 + value: billingValue,
  643 + max: billingValue > 0 ? Math.floor(billingValue * 0.87) : 100000,
  644 + isAmount: true
  645 + },
  646 + {
  647 + title: '人头',
  648 + icon: 'el-icon-s-custom',
  649 + displayValue: countValue || 0,
  650 + value: countValue,
  651 + max: countValue > 0 ? Math.floor(countValue * 0.84) : 100,
  652 + isAmount: false
  653 + },
  654 + {
  655 + title: '消耗业绩',
  656 + icon: 'el-icon-coin',
  657 + displayValue: `¥${this.formatMoney(consumeValue)}`,
  658 + value: consumeValue,
  659 + max: consumeValue > 0 ? Math.floor(consumeValue * 0.85) : 50000,
  660 + isAmount: true
  661 + },
  662 + {
  663 + title: '目标值',
  664 + icon: 'el-icon-coin',
  665 + displayValue: `¥${this.formatMoney(targetValue)}`,
  666 + value: targetValue,
  667 + max: targetValue > 0 ? Math.floor(targetValue * 0.85) : 50000,
  668 + isAmount: true
  669 + },
  670 + {
  671 + title: '完成率',
  672 + icon: 'el-icon-coin',
  673 + displayValue: `${this.formatMoney(completionRate)}%`,
  674 + value: completionRate,
  675 + max: completionRate > 0 ? Math.floor(completionRate * 0.85) : 50000,
  676 + isAmount: true
  677 + }
  678 + ]
  679 + },
  680 +
  681 + // 初始化仪表盘图表
  682 + initGaugeCharts() {
  683 + if (!this.businessData || this.gaugeItems.length === 0) return
  684 +
  685 + this.gaugeItems.forEach((item, index) => {
  686 + this.initGaugeChart(`gaugeChart${index + 1}`, item, `range${index + 1}`)
  687 + })
  688 + },
  689 +
  690 + // 初始化单个仪表盘
  691 + initGaugeChart(chartId, item, rangeId) {
  692 + const chartDom = document.getElementById(chartId)
  693 + if (!chartDom) return
  694 +
  695 + const myChart = echarts.init(chartDom)
  696 +
  697 + const percentage = item.max > 0 ? (item.value / item.max) : 1
  698 + const displayPercentage = percentage > 1 ? 1 : percentage
  699 +
  700 + const option = {
  701 + series: [
  702 + {
  703 + name: item.title,
  704 + type: 'gauge',
  705 + startAngle: 180,
  706 + endAngle: 0,
  707 + radius: '88%',
  708 + min: 0,
  709 + max: item.max || 1,
  710 + splitNumber: 0,
  711 + center: ['50%', '60%'],
  712 + axisLine: {
  713 + lineStyle: {
  714 + width: 22,
  715 + color: [[displayPercentage, '#409EFF'], [1, '#f0f2f5']]
  716 + }
  717 + },
  718 + pointer: {
  719 + show: false
  720 + },
  721 + axisLabel: {
  722 + show: false
  723 + },
  724 + splitLine: {
  725 + show: false
  726 + },
  727 + axisTick: {
  728 + show: false
  729 + },
  730 + title: {
  731 + show: false
  732 + },
  733 + detail: {
  734 + show: false
  735 + }
  736 + }
  737 + ]
  738 + }
  739 +
  740 + myChart.setOption(option)
  741 +
  742 + // 设置范围显示
  743 + const rangeElement = document.getElementById(rangeId)
  744 + if (rangeElement) {
  745 + const displayMax = item.isAmount ? `¥${this.formatMoney(item.max)}` : item.max
  746 + rangeElement.textContent = displayMax
  747 + }
  748 +
  749 + window.addEventListener('resize', () => {
  750 + myChart.resize()
  751 + })
  752 + },
  753 +
  754 + // 获取客户类型统计数据
  755 + loadCustomerTypeStatistics() {
  756 + this.loading2 = true
  757 + const params = {
  758 + startTime: this.query.startTime,
  759 + endTime: this.query.endTime,
  760 + storeIds: this.query.storeIds || []
  761 + }
  762 +
  763 + request({
  764 + url: '/api/Extend/lqreport/get-customer-type-statistics',
  765 + method: 'POST',
  766 + data: params
  767 + }).then(res => {
  768 + this.customerData = res.data || null
  769 + this.loading2 = false
  770 + }).catch(err => {
  771 + console.error('获取客户类型统计数据失败:', err)
  772 + this.$message.error('获取客户类型统计数据失败')
  773 + this.loading2 = false
  774 + })
  775 + },
  776 +
  777 + // 获取门店业绩对比数据
  778 + loadStorePerformanceComparison() {
  779 + this.loading3 = true
  780 + this.loading4 = true
  781 + const params = {
  782 + startTime: this.query.startTime,
  783 + endTime: this.query.endTime,
  784 + storeIds: this.query.storeIds || []
  785 + }
  786 +
  787 + // 获取门店业绩对比数据
  788 + request({
  789 + url: '/api/Extend/lqreport/get-store-performance-comparison',
  790 + method: 'POST',
  791 + data: params
  792 + }).then(res => {
  793 + // 只取前10条数据
  794 + this.performanceData = (res.data || []).slice(0, 10)
  795 + this.$nextTick(() => {
  796 + this.initStorePerformanceChart()
  797 + })
  798 + this.loading3 = false
  799 + }).catch(err => {
  800 + console.error('获取门店业绩对比数据失败:', err)
  801 + this.$message.error('获取门店业绩对比数据失败')
  802 + this.loading3 = false
  803 + })
  804 +
  805 + // 获取品项统计数据
  806 + request({
  807 + url: '/api/Extend/lqreport/get-item-statistics',
  808 + method: 'POST',
  809 + data: params
  810 + }).then(res => {
  811 + this.itemStatisticsData = res.data || []
  812 + this.loading4 = false
  813 + }).catch(err => {
  814 + console.error('获取品项统计数据失败:', err)
  815 + this.$message.error('获取品项统计数据失败')
  816 + this.loading4 = false
  817 + })
  818 + },
  819 +
  820 + // 获取门店项目统计数据
  821 + loadStoreItemStatistics() {
  822 + this.loading5 = true
  823 + const params = {
  824 + startTime: this.query.startTime,
  825 + endTime: this.query.endTime,
  826 + storeIds: this.query.storeIds || []
  827 + }
  828 +
  829 + request({
  830 + url: '/api/Extend/lqreport/get-store-item-statistics',
  831 + method: 'POST',
  832 + data: params
  833 + }).then(res => {
  834 + const data = res.data || []
  835 + // 保存原始数据
  836 + this.originalStoreItemStatisticsData = JSON.parse(JSON.stringify(data))
  837 + this.storeItemStatisticsData = data
  838 + // 如果有排序参数,重新应用排序
  839 + if (this.sortParams.prop && this.sortParams.order) {
  840 + this.sortTableData()
  841 + }
  842 + this.loading5 = false
  843 + }).catch(err => {
  844 + console.error('获取门店项目统计数据失败:', err)
  845 + this.$message.error('获取门店项目统计数据失败')
  846 + this.loading5 = false
  847 + })
  848 + },
  849 +
  850 + // 获取客户到店次数分布数据
  851 + loadCustomerVisitFrequency() {
  852 + this.loading6 = true
  853 + const params = {
  854 + startTime: this.query.startTime,
  855 + endTime: this.query.endTime,
  856 + storeIds: this.query.storeIds || []
  857 + }
  858 +
  859 + request({
  860 + url: '/api/Extend/lqreport/get-customer-visit-frequency',
  861 + method: 'POST',
  862 + data: params
  863 + }).then(res => {
  864 + this.customerVisitFrequencyData = res.data || []
  865 + this.$nextTick(() => {
  866 + this.initCustomerVisitFrequencyChart()
  867 + })
  868 + this.loading6 = false
  869 + }).catch(err => {
  870 + console.error('获取客户到店次数分布数据失败:', err)
  871 + this.$message.error('获取客户到店次数分布数据失败')
  872 + this.loading6 = false
  873 + })
  874 + },
  875 +
  876 + // 获取健康师开单排名数据
  877 + loadHealthCoachBillingRanking() {
  878 + this.loading7 = true
  879 + const params = {
  880 + startTime: this.query.startTime,
  881 + endTime: this.query.endTime,
  882 + storeIds: this.query.storeIds || []
  883 + }
  884 +
  885 + request({
  886 + url: '/api/Extend/lqreport/get-health-coach-billing-ranking',
  887 + method: 'POST',
  888 + data: params
  889 + }).then(res => {
  890 + // 按开单业绩降序排序
  891 + const data = res.data || []
  892 + this.healthCoachBillingRankingData = data.sort((a, b) => (b.BillingPerformance || 0) - (a.BillingPerformance || 0))
  893 + this.loading7 = false
  894 + }).catch(err => {
  895 + console.error('获取健康师开单排名数据失败:', err)
  896 + this.$message.error('获取健康师开单排名数据失败')
  897 + this.loading7 = false
  898 + })
  899 + },
  900 +
  901 + // 获取门店剩余权益统计数据(按门店循环查询)
  902 + loadStoreRemainingRights() {
  903 + this.loading8 = true
  904 + this.storeRemainingRightsData = []
  905 +
  906 + // 如果门店列表为空,先获取门店列表
  907 + const getStoresToQuery = () => {
  908 + if (this.query.storeIds && this.query.storeIds.length > 0) {
  909 + // 如果选择了门店,只查询选中的门店
  910 + return this.storeOptions.filter(store => this.query.storeIds.includes(store.id))
  911 + } else {
  912 + // 如果没有选择门店,查询所有门店
  913 + return this.storeOptions || []
  914 + }
  915 + }
  916 +
  917 + // 获取要查询的门店列表
  918 + let storesToQuery = getStoresToQuery()
  919 +
  920 + // 如果门店列表为空,先加载门店列表
  921 + if (storesToQuery.length === 0 && (!this.storeOptions || this.storeOptions.length === 0)) {
  922 + this.initStoreOptions().then(() => {
  923 + storesToQuery = getStoresToQuery()
  924 + if (storesToQuery.length === 0) {
  925 + this.loading8 = false
  926 + return
  927 + }
  928 + this.queryStoresRemainingRights(storesToQuery)
  929 + }).catch(() => {
  930 + this.loading8 = false
  931 + })
  932 + return
  933 + }
  934 +
  935 + if (storesToQuery.length === 0) {
  936 + this.loading8 = false
  937 + return
  938 + }
  939 +
  940 + this.queryStoresRemainingRights(storesToQuery)
  941 + },
  942 +
  943 + // 查询门店剩余权益数据
  944 + queryStoresRemainingRights(storesToQuery) {
  945 + // 使用 Promise.all 并发查询所有门店
  946 + const requests = storesToQuery.map(store => {
  947 + const params = {
  948 + startTime: this.query.startTime,
  949 + endTime: this.query.endTime,
  950 + storeIds: [store.id]
  951 + }
  952 +
  953 + return request({
  954 + url: '/api/Extend/lqreport/get-store-remaining-rights',
  955 + method: 'POST',
  956 + data: params
  957 + }).then(res => {
  958 + const data = res.data || []
  959 + return data
  960 + }).catch(err => {
  961 + console.error(`获取门店 ${store.dm || store.id} 剩余权益统计数据失败:`, err)
  962 + return []
  963 + })
  964 + })
  965 +
  966 + Promise.all(requests).then(results => {
  967 + // 合并所有门店的查询结果
  968 + const allData = []
  969 + results.forEach(data => {
  970 + if (data && data.length > 0) {
  971 + allData.push(...data)
  972 + }
  973 + })
  974 + this.storeRemainingRightsData = allData
  975 + this.loading8 = false
  976 + }).catch(err => {
  977 + console.error('获取门店剩余权益统计数据失败:', err)
  978 + this.$message.error('获取门店剩余权益统计数据失败')
  979 + this.loading8 = false
  980 + })
  981 + },
  982 +
  983 + // 获取拓客统计数据
  984 + loadTkStatistics() {
  985 + this.loading9 = true
  986 + const params = {
  987 + startTime: this.query.startTime,
  988 + endTime: this.query.endTime,
  989 + storeIds: this.query.storeIds || []
  990 + }
  991 +
  992 + return request({
  993 + url: '/api/Extend/lqtkjlb/get-tk-statistics',
  994 + method: 'POST',
  995 + data: params
  996 + }).then(res => {
  997 + this.tkStatisticsData = res.data || null
  998 + if (this.tkStatisticsData) {
  999 + this.$nextTick(() => {
  1000 + this.initTkFunnelChart()
  1001 + })
  1002 + }
  1003 + this.loading9 = false
  1004 + }).catch(err => {
  1005 + console.error('获取拓客统计数据失败:', err)
  1006 + this.$message.error('获取拓客统计数据失败')
  1007 + this.loading9 = false
  1008 + })
  1009 + },
  1010 +
  1011 + // 初始化拓客转化漏斗图
  1012 + initTkFunnelChart() {
  1013 + if (!this.tkStatisticsData) return
  1014 +
  1015 + const chartDom = document.getElementById('tkFunnelChart')
  1016 + if (!chartDom) return
  1017 +
  1018 + const myChart = echarts.init(chartDom)
  1019 +
  1020 + const tkCount = this.tkStatisticsData.TkCount || 0
  1021 + const yaoyCount = this.tkStatisticsData.YaoyCount || 0
  1022 + const yyCount = this.tkStatisticsData.YyCount || 0
  1023 + const ddCount = this.tkStatisticsData.DdCount || 0
  1024 + const kdCount = this.tkStatisticsData.KdCount || 0
  1025 + const xfCount = this.tkStatisticsData.XfCount || 0
  1026 +
  1027 + // 计算百分比(以拓客人数为基准100%)
  1028 + const maxValue = tkCount || 1
  1029 + const getPercent = (value) => {
  1030 + return tkCount > 0 ? ((value / tkCount) * 100).toFixed(1) : '0.0'
  1031 + }
  1032 +
  1033 + const funnelData = [
  1034 + { value: tkCount, name: '拓客人数', percent: '100.0%' },
  1035 + { value: yaoyCount, name: '邀约人头', percent: getPercent(yaoyCount) + '%' },
  1036 + { value: yyCount, name: '预约人头', percent: getPercent(yyCount) + '%' },
  1037 + { value: ddCount, name: '到店人头', percent: getPercent(ddCount) + '%' },
  1038 + { value: kdCount, name: '开单人头', percent: getPercent(kdCount) + '%' },
  1039 + { value: xfCount, name: '消费人头', percent: getPercent(xfCount) + '%' }
  1040 + ]
  1041 +
  1042 + const option = {
  1043 + tooltip: {
  1044 + trigger: 'item',
  1045 + formatter: '{b} : {c}人 ({d}%)'
  1046 + },
  1047 + legend: {
  1048 + bottom: '5%',
  1049 + left: 'center',
  1050 + icon: 'circle',
  1051 + itemWidth: 10,
  1052 + itemHeight: 10
  1053 + },
  1054 + series: [
  1055 + {
  1056 + name: '拓客转化漏斗',
  1057 + type: 'funnel',
  1058 + left: '10%',
  1059 + top: '10%',
  1060 + bottom: '20%',
  1061 + width: '80%',
  1062 + min: 0,
  1063 + max: maxValue,
  1064 + minSize: '0%',
  1065 + maxSize: '100%',
  1066 + sort: 'descending',
  1067 + gap: 2,
  1068 + label: {
  1069 + show: true,
  1070 + position: 'inside',
  1071 + formatter: '{c}人',
  1072 + fontSize: 12,
  1073 + color: '#fff'
  1074 + },
  1075 + itemStyle: {
  1076 + borderColor: '#fff',
  1077 + borderWidth: 1
  1078 + },
  1079 + data: funnelData,
  1080 + color: ['#409EFF', '#64B5FF', '#8CC5FF', '#B3D8FF', '#D9ECFF', '#EBF5FF']
  1081 + }
  1082 + ]
  1083 + }
  1084 +
  1085 + myChart.setOption(option)
  1086 +
  1087 + window.addEventListener('resize', () => {
  1088 + myChart.resize()
  1089 + })
  1090 + },
  1091 +
  1092 + // 计算拓客邀约率
  1093 + getTkInviteRate() {
  1094 + if (!this.tkStatisticsData || !this.tkStatisticsData.TkCount || this.tkStatisticsData.TkCount === 0) return 0
  1095 + const rate = (this.tkStatisticsData.YaoyCount || 0) / this.tkStatisticsData.TkCount * 100
  1096 + return rate
  1097 + },
  1098 +
  1099 + // 计算邀约到店率
  1100 + getInviteStoreRate() {
  1101 + if (!this.tkStatisticsData || !this.tkStatisticsData.YaoyCount || this.tkStatisticsData.YaoyCount === 0) return 0
  1102 + const rate = (this.tkStatisticsData.DdCount || 0) / this.tkStatisticsData.YaoyCount * 100
  1103 + return rate
  1104 + },
  1105 +
  1106 + // 计算到店转化率
  1107 + getStoreConversionRate() {
  1108 + if (!this.tkStatisticsData || !this.tkStatisticsData.YyCount || this.tkStatisticsData.YyCount === 0) return 0
  1109 + const rate = (this.tkStatisticsData.XfCount || 0) / this.tkStatisticsData.YyCount * 100
  1110 + return rate
  1111 + },
  1112 +
  1113 + // 初始化门店业绩对比柱状图
  1114 + initStorePerformanceChart() {
  1115 + if (!this.performanceData || this.performanceData.length === 0) return
  1116 +
  1117 + const chartDom = document.getElementById('storePerformanceChart')
  1118 + if (!chartDom) return
  1119 +
  1120 + const myChart = echarts.init(chartDom)
  1121 + console.error(this.performanceData)
  1122 + const storeNames = this.performanceData.map(item => item.StoreName)
  1123 + const targetPerformance = this.performanceData.map(item => item.ActualPerformance || 0)
  1124 + const actualPerformance = this.performanceData.map(item => item.ActualConsumePerformance || 0)
  1125 + console.error(actualPerformance)
  1126 + const option = {
  1127 + tooltip: {
  1128 + trigger: 'axis',
  1129 + axisPointer: {
  1130 + type: 'shadow'
  1131 + },
  1132 + formatter: function(params) {
  1133 + let result = params[0].name + '<br/>'
  1134 + params.forEach(item => {
  1135 + result += item.seriesName + ': ¥' + item.value + '<br/>'
  1136 + })
  1137 + return result
  1138 + }
  1139 + },
  1140 + legend: {
  1141 + data: ['现金业绩', '消耗业绩'],
  1142 + bottom: 10,
  1143 + textStyle: {
  1144 + fontSize: 14
  1145 + }
  1146 + },
  1147 + grid: {
  1148 + left: '3%',
  1149 + right: '4%',
  1150 + bottom: '15%',
  1151 + top: '5%',
  1152 + containLabel: true
  1153 + },
  1154 + xAxis: {
  1155 + type: 'category',
  1156 + data: storeNames,
  1157 + axisLabel: {
  1158 + rotate: 45,
  1159 + interval: 0,
  1160 + fontSize: 12
  1161 + }
  1162 + },
  1163 + yAxis: {
  1164 + type: 'value',
  1165 + axisLabel: {
  1166 + formatter: function(value) {
  1167 + return '¥' + value
  1168 + }
  1169 + }
  1170 + },
  1171 + series: [
  1172 + {
  1173 + name: '现金业绩',
  1174 + type: 'bar',
  1175 + data: targetPerformance,
  1176 + itemStyle: {
  1177 + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  1178 + { offset: 0, color: '#409EFF' },
  1179 + { offset: 1, color: '#79bbff' }
  1180 + ])
  1181 + },
  1182 + barWidth: '25%',
  1183 + label: {
  1184 + show: false,
  1185 + position: 'inside',
  1186 + formatter: (params) => {
  1187 + // 将数字转换为纵向显示
  1188 + const value = params.value;
  1189 + return String(value).split('').join('\n');
  1190 + },
  1191 + fontSize: 10,
  1192 + color: '#efefef',
  1193 + fontWeight: 'bold'
  1194 + }
  1195 + },
  1196 + {
  1197 + name: '消耗业绩',
  1198 + type: 'bar',
  1199 + data: actualPerformance,
  1200 + itemStyle: {
  1201 + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  1202 + { offset: 0, color: '#67C23A' },
  1203 + { offset: 1, color: '#95d475' }
  1204 + ])
  1205 + },
  1206 + barWidth: '25%',
  1207 + label: {
  1208 + show: false,
  1209 + position: 'inside',
  1210 + formatter: (params) => {
  1211 + // 将数字转换为纵向显示
  1212 + const value = params.value;
  1213 + return String(value).split('').join('\n');
  1214 + },
  1215 + fontSize: 10,
  1216 + color: '#efefef',
  1217 + fontWeight: 'bold'
  1218 + }
  1219 + }
  1220 + ]
  1221 + }
  1222 +
  1223 + myChart.setOption(option)
  1224 +
  1225 + window.addEventListener('resize', () => {
  1226 + myChart.resize()
  1227 + })
  1228 + },
  1229 +
  1230 + // 初始化客户到店次数分布柱状图
  1231 + initCustomerVisitFrequencyChart() {
  1232 + if (!this.customerVisitFrequencyData || this.customerVisitFrequencyData.length === 0) return
  1233 +
  1234 + const chartDom = document.getElementById('customerVisitFrequencyChart')
  1235 + if (!chartDom) return
  1236 +
  1237 + // 按 VisitCount 降序排序(从高到低)
  1238 + const sortedData = [...this.customerVisitFrequencyData].sort((a, b) => (b.VisitCount || 0) - (a.VisitCount || 0))
  1239 +
  1240 + const visitCounts = sortedData.map(item => `${item.VisitCount || 0}次`)
  1241 + const customerCounts = sortedData.map(item => item.CustomerCount || 0)
  1242 +
  1243 + const myChart = echarts.init(chartDom)
  1244 +
  1245 + const option = {
  1246 + tooltip: {
  1247 + trigger: 'axis',
  1248 + axisPointer: {
  1249 + type: 'shadow'
  1250 + },
  1251 + formatter: function(params) {
  1252 + const param = params[0]
  1253 + return `${param.name}<br/>人数: ${param.value}`
  1254 + }
  1255 + },
  1256 + grid: {
  1257 + left: '3%',
  1258 + right: '4%',
  1259 + bottom: '10%',
  1260 + top: '7%',
  1261 + containLabel: true
  1262 + },
  1263 + xAxis: {
  1264 + type: 'category',
  1265 + data: visitCounts,
  1266 + axisLabel: {
  1267 + fontSize: 12,
  1268 + color: '#606266'
  1269 + },
  1270 + axisLine: {
  1271 + lineStyle: {
  1272 + color: '#DCDFE6'
  1273 + }
  1274 + }
  1275 + },
  1276 + yAxis: {
  1277 + type: 'value',
  1278 + name: '人数',
  1279 + nameTextStyle: {
  1280 + color: '#606266',
  1281 + fontSize: 12
  1282 + },
  1283 + axisLabel: {
  1284 + fontSize: 12,
  1285 + color: '#606266'
  1286 + },
  1287 + axisLine: {
  1288 + lineStyle: {
  1289 + color: '#DCDFE6'
  1290 + }
  1291 + },
  1292 + splitLine: {
  1293 + lineStyle: {
  1294 + color: '#EBEEF5',
  1295 + type: 'dashed'
  1296 + }
  1297 + }
  1298 + },
  1299 + series: [
  1300 + {
  1301 + name: '人数',
  1302 + type: 'bar',
  1303 + data: customerCounts,
  1304 + itemStyle: {
  1305 + color: '#409EFF'
  1306 + },
  1307 + barWidth: '50%',
  1308 + label: {
  1309 + show: true,
  1310 + position: 'top',
  1311 + fontSize: 12,
  1312 + color: '#303133',
  1313 + fontWeight: '500'
  1314 + }
  1315 + }
  1316 + ]
  1317 + }
  1318 +
  1319 + myChart.setOption(option)
  1320 +
  1321 + window.addEventListener('resize', () => {
  1322 + myChart.resize()
  1323 + })
  1324 + },
  1325 +
  1326 + // 获取环形图样式
  1327 + getDonutStyle(rate) {
  1328 + // rate 是小数形式(如 0.75 表示 75%),需要转换为百分比
  1329 + const percentage = (rate || 0) * 100
  1330 + return {
  1331 + background: `conic-gradient(#EC4899 0% ${percentage}%, #E5E7EB ${percentage}% 100%)`
  1332 + }
  1333 + },
  1334 +
  1335 + // 计算进度条宽度
  1336 + getProgressWidth(value) {
  1337 + if (!value && value !== 0) return '0%'
  1338 + // 假设最大值为500
  1339 + const maxValue = 500
  1340 + const percentage = (value / maxValue) * 100
  1341 + return Math.min(percentage, 100) + '%'
  1342 + },
  1343 +
  1344 + // 计算柱状图宽度(用于蝴蝶图)
  1345 + getButterflyBarWidth(value, maxValue) {
  1346 + if (!value && value !== 0 || !maxValue || maxValue === 0) return 0
  1347 + const percentage = (value / maxValue) * 100
  1348 + return Math.min(percentage, 100)
  1349 + },
  1350 +
  1351 + // 重置查询条件
  1352 + reset() {
  1353 + this.query = {
  1354 + startTime: undefined,
  1355 + endTime: undefined,
  1356 + storeIds: []
  1357 + }
  1358 + this.setDefaultTimeRange()
  1359 + this.businessData = null
  1360 + this.customerData = null
  1361 + this.performanceData = []
  1362 + this.itemStatisticsData = []
  1363 + this.storeItemStatisticsData = []
  1364 + this.customerVisitFrequencyData = []
  1365 + this.healthCoachBillingRankingData = []
  1366 + this.storeRemainingRightsData = []
  1367 + this.tkStatisticsData = null
  1368 + this.gaugeItems = []
  1369 + },
  1370 +
  1371 + // 获取排名索引(从1开始)
  1372 + getRankIndex(index) {
  1373 + return index + 1
  1374 + },
  1375 +
  1376 + // 格式化金额
  1377 + formatMoney(amount) {
  1378 + if (!amount && amount !== 0) return '0.00'
  1379 + return Number(amount).toFixed(2)
  1380 + },
  1381 +
  1382 + // 格式化百分比
  1383 + formatPercent(ratio) {
  1384 + if (!ratio && ratio !== 0) return '0.00'
  1385 + return Number(ratio).toFixed(2)
  1386 + },
  1387 +
  1388 + // 格式化日期时间(用于API传参)
  1389 + formatDateTime(timestamp) {
  1390 + if (!timestamp) return ''
  1391 + const date = new Date(timestamp)
  1392 + const year = date.getFullYear()
  1393 + const month = String(date.getMonth() + 1).padStart(2, '0')
  1394 + const day = String(date.getDate()).padStart(2, '0')
  1395 + const hours = String(date.getHours()).padStart(2, '0')
  1396 + const minutes = String(date.getMinutes()).padStart(2, '0')
  1397 + const seconds = String(date.getSeconds()).padStart(2, '0')
  1398 + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
  1399 + },
  1400 +
  1401 + // 字段配置相关方法
  1402 + // 判断字段是否可见
  1403 + isFieldVisible(fieldProp) {
  1404 + return this.selectedFields.includes(fieldProp)
  1405 + },
  1406 +
  1407 + // 全选字段
  1408 + selectAllFields() {
  1409 + this.selectedFields = this.availableFields.map(field => field.prop)
  1410 + },
  1411 +
  1412 + // 全不选字段
  1413 + unselectAllFields() {
  1414 + this.selectedFields = []
  1415 + },
  1416 +
  1417 + // 确认字段配置
  1418 + confirmFieldConfig() {
  1419 + if (this.selectedFields.length === 0) {
  1420 + this.$message.warning('至少需要选择一个字段')
  1421 + return
  1422 + }
  1423 + this.showFieldConfigDialog = false
  1424 + },
  1425 +
  1426 + // 处理排序变化
  1427 + handleSortChange({ column, prop, order }) {
  1428 + if (!order) {
  1429 + // 取消排序,恢复到原始数据顺序
  1430 + this.sortParams = { prop: null, order: null }
  1431 + this.storeItemStatisticsData = JSON.parse(JSON.stringify(this.originalStoreItemStatisticsData))
  1432 + return
  1433 + }
  1434 + this.sortParams = { prop, order }
  1435 + this.sortTableData()
  1436 + },
  1437 +
  1438 + // 对表格数据进行排序
  1439 + sortTableData() {
  1440 + if (!this.sortParams.prop || !this.sortParams.order) {
  1441 + return
  1442 + }
  1443 +
  1444 + const { prop, order } = this.sortParams
  1445 + const data = [...this.storeItemStatisticsData]
  1446 +
  1447 + data.sort((a, b) => {
  1448 + let valA = a[prop]
  1449 + let valB = b[prop]
  1450 +
  1451 + // 处理空值
  1452 + if (valA == null) valA = 0
  1453 + if (valB == null) valB = 0
  1454 +
  1455 + // 确保是数字类型
  1456 + valA = Number(valA) || 0
  1457 + valB = Number(valB) || 0
  1458 +
  1459 + if (order === 'ascending') {
  1460 + return valA - valB
  1461 + } else {
  1462 + return valB - valA
  1463 + }
  1464 + })
  1465 +
  1466 + this.storeItemStatisticsData = data
  1467 + }
  1468 + }
  1469 +}
  1470 +</script>
  1471 +
  1472 +<style lang="scss" scoped>
  1473 +.business-report-page {
  1474 + padding: 16px;
  1475 + background-color: #f0f2f5;
  1476 + min-height: 100vh;
  1477 + font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
  1478 +}
  1479 +
  1480 +.search-box-container {
  1481 + margin-bottom: 24px;
  1482 +}
  1483 +
  1484 +.search-form {
  1485 + display: flex;
  1486 + align-items: center;
  1487 +}
  1488 +
  1489 +.report-card {
  1490 + margin-bottom: 24px;
  1491 + border-radius: 4px;
  1492 + border: none;
  1493 + box-shadow: none !important;
  1494 +
  1495 + &::v-deep .el-card__header {
  1496 + background-color: #fff;
  1497 + border-bottom: 1px solid #ebeef5;
  1498 + padding: 14px 20px;
  1499 + }
  1500 +
  1501 + .card-header {
  1502 + display: flex;
  1503 + align-items: center;
  1504 + font-size: 15px;
  1505 + font-weight: 600;
  1506 + color: #303133;
  1507 +
  1508 + i {
  1509 + margin-right: 8px;
  1510 + font-size: 16px;
  1511 + color: #409EFF;
  1512 + }
  1513 + }
  1514 +}
  1515 +
  1516 +/* 业务统计仪表盘 */
  1517 +.gauge-dashboard {
  1518 + padding: 16px 0;
  1519 +}
  1520 +
  1521 +.gauge-item-wrapper {
  1522 + position: relative;
  1523 + height: 100%;
  1524 +}
  1525 +
  1526 +.gauge-item {
  1527 + background: linear-gradient(135deg, #fff 0%, #f9fafb 100%);
  1528 + border-radius: 8px;
  1529 + padding: 0 10px;
  1530 + text-align: center;
  1531 + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  1532 + transition: all 0.3s ease;
  1533 + height: 100%;
  1534 + display: flex;
  1535 + flex-direction: column;
  1536 + justify-content: center;
  1537 +
  1538 + &:hover {
  1539 + transform: translateY(-5px);
  1540 + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
  1541 + }
  1542 +}
  1543 +
  1544 +.gauge-chart-container {
  1545 + position: relative;
  1546 + margin: 16px 0;
  1547 + min-height: 200px;
  1548 +}
  1549 +
  1550 +.gauge-chart {
  1551 + height: 200px;
  1552 +}
  1553 +
  1554 +.gauge-value-overlay {
  1555 + position: absolute;
  1556 + top: 0;
  1557 + left: 0;
  1558 + right: 0;
  1559 + bottom: 0;
  1560 + display: flex;
  1561 + flex-direction: column;
  1562 + align-items: center;
  1563 + justify-content: center;
  1564 + pointer-events: none;
  1565 + padding-top: 30px;
  1566 +
  1567 + .gauge-icon-wrapper {
  1568 + width: 48px;
  1569 + height: 48px;
  1570 + border-radius: 50%;
  1571 + background: linear-gradient(135deg, #409EFF, #67C23A);
  1572 + display: flex;
  1573 + align-items: center;
  1574 + justify-content: center;
  1575 + margin-bottom: 12px;
  1576 + box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
  1577 +
  1578 + i {
  1579 + font-size: 24px;
  1580 + color: #fff;
  1581 + }
  1582 + }
  1583 +
  1584 + .gauge-value-large {
  1585 + font-size: 20px;
  1586 + font-weight: bold;
  1587 + color: #303133;
  1588 + margin-bottom: 2px;
  1589 + letter-spacing: 0;
  1590 + text-align: center;
  1591 + line-height: 1.2;
  1592 + max-width: 100%;
  1593 + word-wrap: break-word;
  1594 + overflow-wrap: break-word;
  1595 + }
  1596 +
  1597 + .gauge-label-text {
  1598 + font-size: 13px;
  1599 + color: #606266;
  1600 + font-weight: 400;
  1601 + }
  1602 +}
  1603 +
  1604 +.gauge-range-info {
  1605 + display: flex;
  1606 + align-items: center;
  1607 + justify-content: center;
  1608 + margin-top: 10px;
  1609 +
  1610 + .range-label,
  1611 + .range-value {
  1612 + font-size: 12px;
  1613 + color: #909399;
  1614 + }
  1615 +
  1616 + .range-divider {
  1617 + flex: 1;
  1618 + height: 1px;
  1619 + background: #DCDFE6;
  1620 + margin: 0 10px;
  1621 + }
  1622 +}
  1623 +
  1624 +/* 客户类型统计 */
  1625 +.customer-stats-container {
  1626 + padding: 16px 0;
  1627 +}
  1628 +
  1629 +.stats-grid {
  1630 + /* Removed grid layout in favor of el-row/el-col */
  1631 +}
  1632 +
  1633 +.stat-card {
  1634 + background: linear-gradient(135deg, #fff 0%, #f9fafb 100%);
  1635 + border-radius: 8px;
  1636 + padding: 16px;
  1637 + display: flex;
  1638 + align-items: center;
  1639 + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  1640 + transition: all 0.3s ease;
  1641 + min-height: 90px;
  1642 + height: 100%;
  1643 +
  1644 + &:hover {
  1645 + transform: translateY(-3px);
  1646 + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
  1647 + }
  1648 +
  1649 + &.stat-card-purple {
  1650 + .stat-icon {
  1651 + background: rgba(138, 43, 226, 0.1);
  1652 + i { color: #8A2BE2; }
  1653 + }
  1654 + }
  1655 +
  1656 + &.stat-card-blue {
  1657 + .stat-icon {
  1658 + background: rgba(64, 158, 255, 0.1);
  1659 + i { color: #409EFF; }
  1660 + }
  1661 + }
  1662 +
  1663 + &.stat-card-green {
  1664 + .stat-icon {
  1665 + background: rgba(103, 194, 58, 0.1);
  1666 + i { color: #67C23A; }
  1667 + }
  1668 + }
  1669 +
  1670 + &.stat-card-cyan {
  1671 + .stat-icon {
  1672 + background: rgba(0, 206, 209, 0.1);
  1673 + i { color: #00CED1; }
  1674 + }
  1675 + }
  1676 +}
  1677 +
  1678 +.stat-icon {
  1679 + width: 48px;
  1680 + height: 48px;
  1681 + border-radius: 50%;
  1682 + display: flex;
  1683 + align-items: center;
  1684 + justify-content: center;
  1685 + margin-right: 16px;
  1686 + flex-shrink: 0;
  1687 +
  1688 + i {
  1689 + font-size: 24px;
  1690 + }
  1691 +}
  1692 +
  1693 +.stat-content {
  1694 + flex: 1;
  1695 + min-width: 0; /* Fix flex overflow */
  1696 +}
  1697 +
  1698 +.stat-label {
  1699 + font-size: 13px;
  1700 + color: #909399;
  1701 + margin-bottom: 4px;
  1702 + white-space: nowrap;
  1703 +}
  1704 +
  1705 +.stat-value {
  1706 + font-size: 20px;
  1707 + font-weight: bold;
  1708 + color: #303133;
  1709 + line-height: 1.2;
  1710 +}
  1711 +
  1712 +.stat-progress {
  1713 + height: 6px;
  1714 + background: #E5E7EB;
  1715 + border-radius: 3px;
  1716 + overflow: hidden;
  1717 +}
  1718 +
  1719 +.progress-bar {
  1720 + height: 100%;
  1721 + border-radius: 3px;
  1722 + transition: width 0.8s ease;
  1723 +}
  1724 +
  1725 +.conversion-rate-card {
  1726 + background: linear-gradient(135deg, #fff 0%, #f9fafb 100%);
  1727 + border-radius: 8px;
  1728 + padding: 12px;
  1729 + display: flex;
  1730 + align-items: center;
  1731 + justify-content: center;
  1732 + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  1733 + transition: all 0.3s ease;
  1734 + min-height: 90px;
  1735 + height: 100%;
  1736 +
  1737 + &:hover {
  1738 + transform: translateY(-3px);
  1739 + box-shadow: 0 8px 24px rgba(236, 72, 153, 0.2);
  1740 + }
  1741 +}
  1742 +
  1743 +.donut-wrapper {
  1744 + width: 100%;
  1745 + height: 100%;
  1746 + position: relative;
  1747 + display: flex;
  1748 + align-items: center;
  1749 + justify-content: center;
  1750 +}
  1751 +
  1752 +.donut-chart {
  1753 + width: 100px;
  1754 + height: 100px;
  1755 + border-radius: 50%;
  1756 + display: flex;
  1757 + align-items: center;
  1758 + justify-content: center;
  1759 + padding: 6px;
  1760 + background: #fff;
  1761 + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  1762 +
  1763 + .donut-center {
  1764 + width: 88px;
  1765 + height: 88px;
  1766 + border-radius: 50%;
  1767 + background: #fff;
  1768 + display: flex;
  1769 + flex-direction: column;
  1770 + align-items: center;
  1771 + justify-content: center;
  1772 + padding: 6px;
  1773 +
  1774 + .conversion-title {
  1775 + font-size: 12px;
  1776 + color: #909399;
  1777 + margin-bottom: 4px;
  1778 + font-weight: 500;
  1779 + text-align: center;
  1780 + white-space: nowrap;
  1781 + }
  1782 +
  1783 + .percentage {
  1784 + font-size: 16px;
  1785 + font-weight: bold;
  1786 + color: #EC4899;
  1787 + line-height: 1;
  1788 + text-align: center;
  1789 + }
  1790 + }
  1791 +}
  1792 +
  1793 +/* 图表行容器 */
  1794 +.charts-row {
  1795 + /* display: grid; Removed */
  1796 + /* grid-template-columns: 1fr 1fr; Removed */
  1797 + /* gap: 20px; Removed */
  1798 + margin-bottom: 24px;
  1799 +}
  1800 +
  1801 +.half-width {
  1802 + margin-bottom: 0;
  1803 +}
  1804 +
  1805 +/* 图表容器 */
  1806 +.chart-container {
  1807 + width: 100%;
  1808 + height: 500px;
  1809 + margin-top: 16px;
  1810 +}
  1811 +
  1812 +.chart-container-half {
  1813 + width: 100%;
  1814 + height: 400px;
  1815 + margin-top: 16px;
  1816 +}
  1817 +
  1818 +/* 蝴蝶图样式 */
  1819 +.butterfly-chart-container {
  1820 + padding: 16px 0;
  1821 +}
  1822 +
  1823 +.chart-legend {
  1824 + display: flex;
  1825 + justify-content: center;
  1826 + gap: 20px;
  1827 + margin-bottom: 24px;
  1828 +}
  1829 +
  1830 +.legend-item {
  1831 + display: flex;
  1832 + align-items: center;
  1833 + gap: 8px;
  1834 +}
  1835 +
  1836 +.legend-color {
  1837 + width: 40px;
  1838 + height: 16px;
  1839 + border-radius: 4px;
  1840 +}
  1841 +
  1842 +.legend-purple {
  1843 + background: #409EFF;
  1844 +}
  1845 +
  1846 +.legend-orange {
  1847 + background: #67C23A;
  1848 +}
  1849 +
  1850 +.legend-text {
  1851 + font-size: 14px;
  1852 + color: #303133;
  1853 + font-weight: 500;
  1854 +}
  1855 +
  1856 +.chart-axis {
  1857 + padding: 0 50px;
  1858 + margin-bottom: 10px;
  1859 +}
  1860 +
  1861 +.axis-labels {
  1862 + display: grid;
  1863 + grid-template-columns: repeat(14, 1fr);
  1864 + gap: 0;
  1865 +}
  1866 +
  1867 +.axis-label {
  1868 + font-size: 11px;
  1869 + color: #909399;
  1870 + text-align: center;
  1871 +}
  1872 +
  1873 +.butterfly-chart-content {
  1874 + display: grid;
  1875 + grid-template-columns: 1fr auto 1fr;
  1876 + gap: 8px;
  1877 + align-items: center;
  1878 + padding: 0 10px;
  1879 +}
  1880 +
  1881 +.chart-left,
  1882 +.chart-right {
  1883 + display: flex;
  1884 + flex-direction: column;
  1885 + justify-content: space-around;
  1886 +}
  1887 +
  1888 +.chart-row {
  1889 + height: 32px;
  1890 + margin-bottom: 6px;
  1891 + display: flex;
  1892 + align-items: center;
  1893 +}
  1894 +
  1895 +.chart-left .chart-row {
  1896 + justify-content: flex-end;
  1897 +}
  1898 +
  1899 +.chart-right .chart-row {
  1900 + justify-content: flex-start;
  1901 +}
  1902 +
  1903 +.bar-left,
  1904 +.bar-right {
  1905 + height: 24px;
  1906 + display: flex;
  1907 + align-items: center;
  1908 + border-radius: 4px;
  1909 + min-width: 40px;
  1910 +}
  1911 +
  1912 +.bar-left {
  1913 + background: #409EFF;
  1914 + justify-content: flex-end;
  1915 + padding-right: 8px;
  1916 + transition: width 0.3s ease;
  1917 +}
  1918 +
  1919 +.bar-right {
  1920 + background: #67C23A;
  1921 + justify-content: flex-start;
  1922 + padding-left: 8px;
  1923 + transition: width 0.3s ease;
  1924 +}
  1925 +
  1926 +.bar-value {
  1927 + font-size: 11px;
  1928 + font-weight: 500;
  1929 + color: #fff;
  1930 +}
  1931 +
  1932 +.chart-center {
  1933 + min-width: 80px;
  1934 + display: flex;
  1935 + flex-direction: column;
  1936 + justify-content: space-around;
  1937 + align-items: center;
  1938 + padding: 0 8px;
  1939 +}
  1940 +
  1941 +.category-name {
  1942 + height: 32px;
  1943 + font-size: 12px;
  1944 + color: #303133;
  1945 + display: flex;
  1946 + align-items: center;
  1947 + margin-bottom: 6px;
  1948 + text-align: center;
  1949 + line-height: 1.4;
  1950 +}
  1951 +
  1952 +/* 字段配置对话框样式 */
  1953 +.field-config-content {
  1954 + max-height: 400px;
  1955 + overflow-y: auto;
  1956 +}
  1957 +
  1958 +.field-config-actions {
  1959 + display: flex;
  1960 + justify-content: flex-end;
  1961 + gap: 10px;
  1962 + padding-bottom: 10px;
  1963 + border-bottom: 1px solid #EBEEF5;
  1964 +}
  1965 +
  1966 +.field-config-item {
  1967 + padding: 10px 0;
  1968 + border-bottom: 1px solid #F5F7FA;
  1969 +
  1970 + &:last-child {
  1971 + border-bottom: none;
  1972 + }
  1973 +
  1974 + ::v-deep .el-checkbox {
  1975 + width: 100%;
  1976 +
  1977 + .el-checkbox__label {
  1978 + font-size: 14px;
  1979 + color: #303133;
  1980 + }
  1981 + }
  1982 +}
  1983 +
  1984 +/* 拓客转化漏斗图样式 */
  1985 +.funnel-container {
  1986 + padding: 16px 0;
  1987 +}
  1988 +
  1989 +.funnel-chart-container {
  1990 + width: 100%;
  1991 + height: 500px;
  1992 + margin-bottom: 24px;
  1993 +}
  1994 +
  1995 +.funnel-chart-container-half {
  1996 + width: 100%;
  1997 + height: 300px;
  1998 + margin-bottom: 16px;
  1999 +}
  2000 +
  2001 +.conversion-rate-cards {
  2002 + display: grid;
  2003 + grid-template-columns: repeat(3, 1fr);
  2004 + gap: 10px;
  2005 + margin-top: 10px;
  2006 +}
  2007 +
  2008 +.conversion-card {
  2009 + background: linear-gradient(135deg, #fff 0%, #f9fafb 100%);
  2010 + border-radius: 8px;
  2011 + padding: 12px;
  2012 + text-align: center;
  2013 + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
  2014 + transition: all 0.3s ease;
  2015 +
  2016 + &:hover {
  2017 + transform: translateY(-3px);
  2018 + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
  2019 + }
  2020 +}
  2021 +
  2022 +.conversion-label {
  2023 + font-size: 12px;
  2024 + color: #909399;
  2025 + margin-bottom: 6px;
  2026 + font-weight: 500;
  2027 +}
  2028 +
  2029 +.conversion-value {
  2030 + font-size: 20px;
  2031 + font-weight: bold;
  2032 + color: #409EFF;
  2033 + margin-bottom: 4px;
  2034 + line-height: 1.2;
  2035 +}
  2036 +
  2037 +.conversion-detail {
  2038 + font-size: 12px;
  2039 + color: #909399;
  2040 +}
  2041 +</style>
0 2042 \ No newline at end of file
... ...
excel/历史数据归档.xlsx 0 → 100644
No preview for this file type
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqAnnualSummary/AnnualStatDto.cs 0 → 100644
  1 +using System;
  2 +using System.Collections.Generic;
  3 +
  4 +namespace NCC.Extend.Entitys.Dto.LqAnnualSummary
  5 +{
  6 + /// <summary>
  7 + /// 全年门店月度数据统计输出 (用于报表 4.1 - 4.5)
  8 + /// </summary>
  9 + public class AnnualMonthlyStatOutput
  10 + {
  11 + /// <summary>
  12 + /// 1-12月数据列定义
  13 + /// </summary>
  14 + public List<string> MonthColumns { get; set; } = new List<string>
  15 + {
  16 + "Month1", "Month2", "Month3", "Month4", "Month5", "Month6",
  17 + "Month7", "Month8", "Month9", "Month10", "Month11", "Month12"
  18 + };
  19 +
  20 + /// <summary>
  21 + /// 数据行
  22 + /// </summary>
  23 + public List<MonthlyDataRow> Rows { get; set; } = new List<MonthlyDataRow>();
  24 + }
  25 +
  26 + public class MonthlyDataRow
  27 + {
  28 + public string BusinessUnitName { get; set; }
  29 + public string StoreName { get; set; }
  30 +
  31 + // 1-12月数值
  32 + public decimal Month1 { get; set; }
  33 + public decimal Month2 { get; set; }
  34 + public decimal Month3 { get; set; }
  35 + public decimal Month4 { get; set; }
  36 + public decimal Month5 { get; set; }
  37 + public decimal Month6 { get; set; }
  38 + public decimal Month7 { get; set; }
  39 + public decimal Month8 { get; set; }
  40 + public decimal Month9 { get; set; }
  41 + public decimal Month10 { get; set; }
  42 + public decimal Month11 { get; set; }
  43 + public decimal Month12 { get; set; }
  44 +
  45 + // 上年度1-12月数值 (用于同比走势图)
  46 + public decimal LastMonth1 { get; set; }
  47 + public decimal LastMonth2 { get; set; }
  48 + public decimal LastMonth3 { get; set; }
  49 + public decimal LastMonth4 { get; set; }
  50 + public decimal LastMonth5 { get; set; }
  51 + public decimal LastMonth6 { get; set; }
  52 + public decimal LastMonth7 { get; set; }
  53 + public decimal LastMonth8 { get; set; }
  54 + public decimal LastMonth9 { get; set; }
  55 + public decimal LastMonth10 { get; set; }
  56 + public decimal LastMonth11 { get; set; }
  57 + public decimal LastMonth12 { get; set; }
  58 +
  59 + public decimal TotalCurrentYear { get; set; }
  60 + public decimal AvgCurrentYear { get; set; }
  61 + public decimal TotalLastYear { get; set; }
  62 + public decimal AvgLastYear { get; set; }
  63 + public string GrowthRate { get; set; } // 百分比字符串
  64 + }
  65 +
  66 + /// <summary>
  67 + /// 指标统计输出 (用于报表 4.6, 4.7)
  68 + /// </summary>
  69 + public class IndicatorStatOutput
  70 + {
  71 + public List<IndicatorDataRow> Rows { get; set; } = new List<IndicatorDataRow>();
  72 + }
  73 +
  74 + public class IndicatorDataRow
  75 + {
  76 + public string BusinessUnitName { get; set; }
  77 + public string StoreName { get; set; }
  78 + public decimal LastYearValue { get; set; }
  79 + public decimal CurrentYearValue { get; set; }
  80 + public string GrowthRate { get; set; }
  81 + }
  82 +
  83 + /// <summary>
  84 + /// 事业部内部汇总输出 (用于报表 4.8)
  85 + /// </summary>
  86 + public class BusinessUnitSummaryOutput
  87 + {
  88 + public List<BusinessUnitSummaryRow> Rows { get; set; } = new List<BusinessUnitSummaryRow>();
  89 + }
  90 +
  91 + public class BusinessUnitSummaryRow
  92 + {
  93 + public string BusinessUnitName { get; set; }
  94 + public string StoreName { get; set; }
  95 +
  96 + // 业绩
  97 + public decimal LastYearPerformance { get; set; }
  98 + public decimal CurrentYearPerformance { get; set; }
  99 + public string PerformanceGrowthRate { get; set; }
  100 +
  101 + // 消耗
  102 + public decimal LastYearConsume { get; set; }
  103 + public decimal CurrentYearConsume { get; set; }
  104 + public string ConsumeGrowthRate { get; set; }
  105 +
  106 + // 人头
  107 + public decimal LastYearHeadCount { get; set; }
  108 + public decimal CurrentYearHeadCount { get; set; }
  109 + public string HeadCountGrowthRate { get; set; }
  110 +
  111 + // 人次
  112 + public decimal LastYearPersonTime { get; set; }
  113 + public decimal CurrentYearPersonTime { get; set; }
  114 + public string PersonTimeGrowthRate { get; set; }
  115 +
  116 + // 项目数
  117 + public decimal LastYearProjectCount { get; set; }
  118 + public decimal CurrentYearProjectCount { get; set; }
  119 + public string ProjectCountGrowthRate { get; set; }
  120 + }
  121 +}
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqAnnualSummary/AnnualSummaryDto.cs 0 → 100644
  1 +using System;
  2 +using System.ComponentModel.DataAnnotations;
  3 +using NCC.Common.Filter;
  4 +
  5 +namespace NCC.Extend.Entitys.Dto.LqAnnualSummary
  6 +{
  7 + public class AnnualSummaryQueryInput : NCC.Common.Filter.PageInputBase
  8 + {
  9 + public string StoreName { get; set; }
  10 + public string StoreId { get; set; }
  11 + public int? Year { get; set; }
  12 + public int? Month { get; set; }
  13 + }
  14 +
  15 + public class AnnualSummaryInput
  16 + {
  17 + public string Id { get; set; }
  18 + [Required(ErrorMessage = "门店不能为空")]
  19 + public string StoreId { get; set; }
  20 + public string StoreName { get; set; }
  21 + public string BusinessUnitId { get; set; }
  22 + public string BusinessUnitName { get; set; }
  23 + [Required(ErrorMessage = "年份不能为空")]
  24 + public int Year { get; set; }
  25 + [Required(ErrorMessage = "月份不能为空")]
  26 + public int Month { get; set; }
  27 + public decimal TotalPerformance { get; set; }
  28 + public decimal TotalConsume { get; set; }
  29 + public decimal HeadCount { get; set; }
  30 + public decimal PersonTime { get; set; }
  31 + public decimal ProjectCount { get; set; }
  32 + }
  33 +
  34 + public class AnnualSummaryImportDto
  35 + {
  36 + public string StoreName { get; set; }
  37 + public string BusinessUnitName { get; set; }
  38 + public int Year { get; set; }
  39 + public int Month { get; set; }
  40 + public decimal TotalPerformance { get; set; }
  41 + public decimal TotalConsume { get; set; }
  42 + public decimal HeadCount { get; set; }
  43 + public decimal PersonTime { get; set; }
  44 + public decimal ProjectCount { get; set; }
  45 + }
  46 +}
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqBusinessUnitManagerSalary/BusinessUnitManagerSalaryOutput.cs
... ... @@ -53,6 +53,36 @@ namespace NCC.Extend.Entitys.Dto.LqBusinessUnitManagerSalary
53 53 public string StorePerformanceDetail { get; set; }
54 54  
55 55 /// <summary>
  56 + /// 销售业绩(开单业绩-退款业绩)
  57 + /// </summary>
  58 + public decimal SalesPerformance { get; set; }
  59 +
  60 + /// <summary>
  61 + /// 产品物料(仓库领用金额)
  62 + /// </summary>
  63 + public decimal ProductMaterial { get; set; }
  64 +
  65 + /// <summary>
  66 + /// 合作项目成本
  67 + /// </summary>
  68 + public decimal CooperationCost { get; set; }
  69 +
  70 + /// <summary>
  71 + /// 店内支出
  72 + /// </summary>
  73 + public decimal StoreExpense { get; set; }
  74 +
  75 + /// <summary>
  76 + /// 洗毛巾费用
  77 + /// </summary>
  78 + public decimal LaundryCost { get; set; }
  79 +
  80 + /// <summary>
  81 + /// 毛利(销售业绩-产品物料-合作项目成本-店内支出-洗毛巾)
  82 + /// </summary>
  83 + public decimal GrossProfit { get; set; }
  84 +
  85 + /// <summary>
56 86 /// 底薪
57 87 /// </summary>
58 88 public decimal BaseSalary { get; set; }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdDeductinfo/LqKdDeductinfoListQueryInput.cs
1 1 using System;
  2 +using System.Collections.Generic;
2 3 using NCC.Common.Filter;
3 4  
4 5 namespace NCC.Extend.Entitys.Dto.LqKdDeductinfo
... ... @@ -67,5 +68,30 @@ namespace NCC.Extend.Entitys.Dto.LqKdDeductinfo
67 68 /// 结束创建时间
68 69 /// </summary>
69 70 public DateTime? EndCreateTime { get; set; }
  71 +
  72 + /// <summary>
  73 + /// 门店ID(筛选)
  74 + /// </summary>
  75 + public string StoreId { get; set; }
  76 +
  77 + /// <summary>
  78 + /// 门店ID列表(支持多门店筛选)
  79 + /// </summary>
  80 + public List<string> StoreIds { get; set; }
  81 +
  82 + /// <summary>
  83 + /// 开始时间(开单时间筛选)
  84 + /// </summary>
  85 + public DateTime? StartTime { get; set; }
  86 +
  87 + /// <summary>
  88 + /// 结束时间(开单时间筛选)
  89 + /// </summary>
  90 + public DateTime? EndTime { get; set; }
  91 +
  92 + /// <summary>
  93 + /// 品项分类(科美、医美、生美、产品等)
  94 + /// </summary>
  95 + public string ItemCategory { get; set; }
70 96 }
71 97 }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/HealthCoachStatisticsOutput.cs
... ... @@ -109,5 +109,15 @@ namespace NCC.Extend.Entitys.Dto.LqKdKdjlb
109 109 /// 加班手工费 - 统计该健康师在指定时间周期内消耗时的加班手工费总金额
110 110 /// </summary>
111 111 public decimal overtimeLaborCost { get; set; }
  112 +
  113 + /// <summary>
  114 + /// 金三角名称 - 该健康师所在的金三角战队名称
  115 + /// </summary>
  116 + public string goldTriangleName { get; set; }
  117 +
  118 + /// <summary>
  119 + /// 队伍业绩占比 - 该健康师所在金三角的业绩占门店总业绩的比例(百分比,0-100)
  120 + /// </summary>
  121 + public decimal teamPerformanceRatio { get; set; }
112 122 }
113 123 }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxInfoOutput.cs
... ... @@ -136,6 +136,11 @@ namespace NCC.Extend.Entitys.Dto.LqKhxx
136 136 public string subHealthUserName { get; set; }
137 137  
138 138 /// <summary>
  139 + /// 最后消费时间
  140 + /// </summary>
  141 + public DateTime? lastConsumeTime { get; set; }
  142 +
  143 + /// <summary>
139 144 /// 是否生美会员(0-否,1-是)
140 145 /// </summary>
141 146 public int isBeautyMember { get; set; }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxListOutput.cs
... ... @@ -163,6 +163,11 @@ namespace NCC.Extend.Entitys.Dto.LqKhxx
163 163 public string subHealthUserName { get; set; }
164 164  
165 165 /// <summary>
  166 + /// 最后消费时间
  167 + /// </summary>
  168 + public DateTime? lastConsumeTime { get; set; }
  169 +
  170 + /// <summary>
166 171 /// 是否生美会员(0-否,1-是)
167 172 /// </summary>
168 173 public int isBeautyMember { get; set; }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXmzl/ItemStoreStatisticsOutput.cs
... ... @@ -24,11 +24,31 @@ namespace NCC.Extend.Entitys.Dto.LqXmzl
24 24 public int BillingCount { get; set; }
25 25  
26 26 /// <summary>
27   - /// 项目数(项目次数总和
  27 + /// 项目数(项目次数总和,已废弃,使用TotalProjectCount代替
28 28 /// </summary>
29 29 public decimal ProjectCount { get; set; }
30 30  
31 31 /// <summary>
  32 + /// 总项目数(项目次数总和)
  33 + /// </summary>
  34 + public decimal TotalProjectCount { get; set; }
  35 +
  36 + /// <summary>
  37 + /// 购买项目数(来源类型为"购买"的项目次数总和)
  38 + /// </summary>
  39 + public decimal PurchaseProjectCount { get; set; }
  40 +
  41 + /// <summary>
  42 + /// 体验项目数(来源类型为"体验"的项目次数总和)
  43 + /// </summary>
  44 + public decimal ExperienceProjectCount { get; set; }
  45 +
  46 + /// <summary>
  47 + /// 赠送项目数(来源类型为"赠送"的项目次数总和)
  48 + /// </summary>
  49 + public decimal GiftProjectCount { get; set; }
  50 +
  51 + /// <summary>
32 52 /// 实付金额(实付金额总和)
33 53 /// </summary>
34 54 public decimal ActualAmount { get; set; }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXmzl/LqXmzlStatisticsInput.cs
... ... @@ -23,11 +23,16 @@ namespace NCC.Extend.Entitys.Dto.LqXmzl
23 23 public string StoreId { get; set; }
24 24  
25 25 /// <summary>
26   - /// 品项分类
  26 + /// 品项分类(已废弃,使用ItemCategory代替)
27 27 /// </summary>
28 28 public string Category { get; set; }
29 29  
30 30 /// <summary>
  31 + /// 品项分类筛选(科美、医美、生美、产品等,对应lq_xmzl表的qt2字段)
  32 + /// </summary>
  33 + public string ItemCategory { get; set; }
  34 +
  35 + /// <summary>
31 36 /// 品项ID(单个品项统计)
32 37 /// </summary>
33 38 public string ItemId { get; set; }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXmzl/LqXmzlStatisticsOutput.cs
... ... @@ -86,5 +86,20 @@ namespace NCC.Extend.Entitys.Dto.LqXmzl
86 86 /// 退卡次数
87 87 /// </summary>
88 88 public int RefundCount { get; set; }
  89 +
  90 + /// <summary>
  91 + /// 储扣金额
  92 + /// </summary>
  93 + public decimal DeductAmount { get; set; }
  94 +
  95 + /// <summary>
  96 + /// 储扣次数
  97 + /// </summary>
  98 + public int DeductCount { get; set; }
  99 +
  100 + /// <summary>
  101 + /// 品项分类(科美、医美、生美、产品等,对应lq_xmzl表的qt2字段)
  102 + /// </summary>
  103 + public string ItemCategory { get; set; }
89 104 }
90 105 }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_annual_summary/LqAnnualSummaryEntity.cs 0 → 100644
  1 +using SqlSugar;
  2 +using System;
  3 +using NCC.Common.Const;
  4 +
  5 +namespace NCC.Extend.Entitys.lq_annual_summary
  6 +{
  7 + /// <summary>
  8 + /// 年度汇总表
  9 + /// </summary>
  10 + [SugarTable("lq_annual_summary")]
  11 + [Tenant(ClaimConst.TENANT_ID)]
  12 + public class LqAnnualSummaryEntity
  13 + {
  14 + /// <summary>
  15 + /// 主键ID
  16 + /// </summary>
  17 + [SugarColumn(ColumnName = "F_Id", IsPrimaryKey = true)]
  18 + public string Id { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 门店ID
  22 + /// </summary>
  23 + [SugarColumn(ColumnName = "F_StoreId")]
  24 + public string StoreId { get; set; }
  25 +
  26 + /// <summary>
  27 + /// 门店名称
  28 + /// </summary>
  29 + [SugarColumn(ColumnName = "F_StoreName")]
  30 + public string StoreName { get; set; }
  31 +
  32 + /// <summary>
  33 + /// 归属事业部ID
  34 + /// </summary>
  35 + [SugarColumn(ColumnName = "F_BusinessUnitId")]
  36 + public string BusinessUnitId { get; set; }
  37 +
  38 + /// <summary>
  39 + /// 归属事业部名称
  40 + /// </summary>
  41 + [SugarColumn(ColumnName = "F_BusinessUnitName")]
  42 + public string BusinessUnitName { get; set; }
  43 +
  44 + /// <summary>
  45 + /// 年份
  46 + /// </summary>
  47 + [SugarColumn(ColumnName = "F_Year")]
  48 + public int Year { get; set; }
  49 +
  50 + /// <summary>
  51 + /// 月份
  52 + /// </summary>
  53 + [SugarColumn(ColumnName = "F_Month")]
  54 + public int Month { get; set; }
  55 +
  56 + /// <summary>
  57 + /// 总业绩
  58 + /// </summary>
  59 + [SugarColumn(ColumnName = "F_TotalPerformance")]
  60 + public decimal TotalPerformance { get; set; }
  61 +
  62 + /// <summary>
  63 + /// 总消耗
  64 + /// </summary>
  65 + [SugarColumn(ColumnName = "F_TotalConsume")]
  66 + public decimal TotalConsume { get; set; }
  67 +
  68 + /// <summary>
  69 + /// 人头数
  70 + /// </summary>
  71 + [SugarColumn(ColumnName = "F_HeadCount")]
  72 + public decimal HeadCount { get; set; }
  73 +
  74 + /// <summary>
  75 + /// 人次数
  76 + /// </summary>
  77 + [SugarColumn(ColumnName = "F_PersonTime")]
  78 + public decimal PersonTime { get; set; }
  79 +
  80 + /// <summary>
  81 + /// 项目数
  82 + /// </summary>
  83 + [SugarColumn(ColumnName = "F_ProjectCount")]
  84 + public decimal ProjectCount { get; set; }
  85 +
  86 + /// <summary>
  87 + /// 是否有效 0无效 1有效
  88 + /// </summary>
  89 + [SugarColumn(ColumnName = "F_IsEffective")]
  90 + public int IsEffective { get; set; } = 1;
  91 +
  92 + /// <summary>
  93 + /// 创建时间
  94 + /// </summary>
  95 + [SugarColumn(ColumnName = "F_CreateTime")]
  96 + public DateTime? CreateTime { get; set; }
  97 +
  98 + /// <summary>
  99 + /// 创建人
  100 + /// </summary>
  101 + [SugarColumn(ColumnName = "F_CreateUser")]
  102 + public string CreateUser { get; set; }
  103 +
  104 + /// <summary>
  105 + /// 更新时间
  106 + /// </summary>
  107 + [SugarColumn(ColumnName = "F_UpdateTime")]
  108 + public DateTime? UpdateTime { get; set; }
  109 +
  110 + /// <summary>
  111 + /// 更新人
  112 + /// </summary>
  113 + [SugarColumn(ColumnName = "F_UpdateUser")]
  114 + public string UpdateUser { get; set; }
  115 + }
  116 +}
... ...
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
72 72 public decimal BaseSalary { get; set; }
73 73  
74 74 /// <summary>
  75 + /// 销售业绩(开单业绩-退款业绩)
  76 + /// </summary>
  77 + [SugarColumn(ColumnName = "F_SalesPerformance")]
  78 + public decimal SalesPerformance { get; set; }
  79 +
  80 + /// <summary>
  81 + /// 产品物料(仓库领用金额,注意11月特殊规则:11月工资算10月数据)
  82 + /// </summary>
  83 + [SugarColumn(ColumnName = "F_ProductMaterial")]
  84 + public decimal ProductMaterial { get; set; }
  85 +
  86 + /// <summary>
  87 + /// 合作项目成本
  88 + /// </summary>
  89 + [SugarColumn(ColumnName = "F_CooperationCost")]
  90 + public decimal CooperationCost { get; set; }
  91 +
  92 + /// <summary>
  93 + /// 店内支出
  94 + /// </summary>
  95 + [SugarColumn(ColumnName = "F_StoreExpense")]
  96 + public decimal StoreExpense { get; set; }
  97 +
  98 + /// <summary>
  99 + /// 洗毛巾费用(只统计送出的记录,F_FlowType = 0)
  100 + /// </summary>
  101 + [SugarColumn(ColumnName = "F_LaundryCost")]
  102 + public decimal LaundryCost { get; set; }
  103 +
  104 + /// <summary>
  105 + /// 毛利(销售业绩-产品物料-合作项目成本-店内支出-洗毛巾)
  106 + /// </summary>
  107 + [SugarColumn(ColumnName = "F_GrossProfit")]
  108 + public decimal GrossProfit { get; set; }
  109 +
  110 + /// <summary>
75 111 /// 提成合计(所有门店提成金额汇总)
76 112 /// </summary>
77 113 [SugarColumn(ColumnName = "F_TotalCommission")]
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Mapper/LqKhxxMapper.cs
... ... @@ -23,6 +23,7 @@ namespace NCC.Extend.Entitys.Mapper.LqKhxx
23 23 .Map(dest => dest.techMemberTime, src => src.TechMemberTime)
24 24 .Map(dest => dest.firstVisitTime, src => src.FirstVisitTime)
25 25 .Map(dest => dest.lastVisitTime, src => src.LastVisitTime)
  26 + .Map(dest => dest.lastConsumeTime, src => src.LastConsumeTime)
26 27 .Map(dest => dest.visitDays, src => src.VisitDays)
27 28 .Map(dest => dest.sleepStartTime, src => src.SleepStartTime)
28 29 .Map(dest => dest.sleepDays, src => src.SleepDays)
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqAnnualSummaryService.cs 0 → 100644
  1 +using Microsoft.AspNetCore.Http;
  2 +using Microsoft.AspNetCore.Mvc;
  3 +using NCC.Common.Filter;
  4 +using NCC.Common.Helper;
  5 +using NCC.Dependency;
  6 +using NCC.DynamicApiController;
  7 +using NCC.Extend.Entitys.Dto.LqAnnualSummary;
  8 +using NCC.Extend.Entitys.lq_annual_summary;
  9 +using NCC.Extend.Entitys.lq_mdxx;
  10 +using NCC.System.Entitys.Permission;
  11 +using Mapster;
  12 +using NPOI.HSSF.UserModel;
  13 +using NPOI.SS.UserModel;
  14 +using NPOI.XSSF.UserModel;
  15 +using SqlSugar;
  16 +using System;
  17 +using System.Collections.Generic;
  18 +using System.Data;
  19 +using System.IO;
  20 +using System.Linq;
  21 +using System.Text.RegularExpressions;
  22 +using System.Threading.Tasks;
  23 +using Yitter.IdGenerator;
  24 +
  25 +namespace NCC.Extend
  26 +{
  27 + /// <summary>
  28 + /// 年度汇总表服务
  29 + /// </summary>
  30 + [ApiDescriptionSettings(Tag = "年度经营数据汇总", Name = "LqAnnualSummary", Order = 500)]
  31 + [Route("api/Extend/[controller]")]
  32 + public class LqAnnualSummaryService : IDynamicApiController, ITransient
  33 + {
  34 + private readonly ISqlSugarClient _db;
  35 +
  36 + /// <summary>
  37 + /// 初始化一个<see cref="LqAnnualSummaryService"/>类型的新实例
  38 + /// </summary>
  39 + /// <param name="db">数据库客户端</param>
  40 + public LqAnnualSummaryService(ISqlSugarClient db)
  41 + {
  42 + _db = db;
  43 + }
  44 +
  45 + #region 基础 CRUD
  46 +
  47 + /// <summary>
  48 + /// 分页查询
  49 + /// </summary>
  50 + [HttpGet("list")]
  51 + public async Task<dynamic> GetList([FromQuery] AnnualSummaryQueryInput input)
  52 + {
  53 + var query = _db.Queryable<LqAnnualSummaryEntity>()
  54 + .Where(x => x.IsEffective == 1);
  55 +
  56 + if (!string.IsNullOrEmpty(input.StoreName))
  57 + {
  58 + query = query.Where(x => x.StoreName.Contains(input.StoreName));
  59 + }
  60 + if (!string.IsNullOrEmpty(input.StoreId))
  61 + {
  62 + query = query.Where(x => x.StoreId == input.StoreId);
  63 + }
  64 + if (input.Year.HasValue)
  65 + {
  66 + query = query.Where(x => x.Year == input.Year.Value);
  67 + }
  68 + if (input.Month.HasValue)
  69 + {
  70 + query = query.Where(x => x.Month == input.Month.Value);
  71 + }
  72 +
  73 + var list = await query.OrderBy(x => x.Year, OrderByType.Desc)
  74 + .OrderBy(x => x.Month, OrderByType.Desc)
  75 + .OrderBy(x => x.StoreId)
  76 + .ToPagedListAsync(input.currentPage, input.pageSize);
  77 +
  78 + // 获取事业部名称映射
  79 + var buNameMap = await GetBusinessUnitNameMapAsync();
  80 +
  81 + // 获取所有门店信息,用于获取正确的事业部信息
  82 + var storeIds = list.list.Select(x => x.StoreId).Distinct().ToList();
  83 + var stores = await _db.Queryable<LqMdxxEntity>()
  84 + .Where(x => storeIds.Contains(x.Id))
  85 + .Select(x => new { x.Id, x.Syb, x.Kjb })
  86 + .ToListAsync();
  87 + var storeDict = stores.ToDictionary(x => x.Id, x => x);
  88 +
  89 + var result = new
  90 + {
  91 + pagination = list.pagination,
  92 + list = list.list.Select(x =>
  93 + {
  94 + // 确定正确的事业部ID
  95 + string correctBuId = null;
  96 + string correctBuName = null;
  97 +
  98 + // 1. 如果BusinessUnitId不为空,检查是否是科技部
  99 + if (!string.IsNullOrEmpty(x.BusinessUnitId))
  100 + {
  101 + var buName = GetBusinessUnitDisplayName(x.BusinessUnitId, buNameMap);
  102 + // 如果名称包含"科技",说明是科技部,需要从门店表获取事业部
  103 + if (buName.Contains("科技"))
  104 + {
  105 + // 从门店表获取事业部ID
  106 + if (storeDict.ContainsKey(x.StoreId))
  107 + {
  108 + correctBuId = storeDict[x.StoreId].Syb;
  109 + if (!string.IsNullOrEmpty(correctBuId))
  110 + {
  111 + correctBuName = GetBusinessUnitDisplayName(correctBuId, buNameMap);
  112 + }
  113 + }
  114 + }
  115 + else
  116 + {
  117 + // 不是科技部,使用原有的BusinessUnitId
  118 + correctBuId = x.BusinessUnitId;
  119 + correctBuName = buName;
  120 + }
  121 + }
  122 + else if (!string.IsNullOrEmpty(x.BusinessUnitName))
  123 + {
  124 + // 2. 如果BusinessUnitId为空,但BusinessUnitName不为空
  125 + var buName = GetBusinessUnitDisplayName(x.BusinessUnitName, buNameMap);
  126 + if (buName.Contains("科技"))
  127 + {
  128 + // 从门店表获取事业部ID
  129 + if (storeDict.ContainsKey(x.StoreId))
  130 + {
  131 + correctBuId = storeDict[x.StoreId].Syb;
  132 + if (!string.IsNullOrEmpty(correctBuId))
  133 + {
  134 + correctBuName = GetBusinessUnitDisplayName(correctBuId, buNameMap);
  135 + }
  136 + }
  137 + }
  138 + else
  139 + {
  140 + // 尝试从BusinessUnitName中解析ID
  141 + if (buNameMap.ContainsKey(x.BusinessUnitName))
  142 + {
  143 + correctBuName = buNameMap[x.BusinessUnitName];
  144 + }
  145 + else
  146 + {
  147 + correctBuName = buName;
  148 + }
  149 + }
  150 + }
  151 + else
  152 + {
  153 + // 3. 如果都为空,从门店表获取事业部
  154 + if (storeDict.ContainsKey(x.StoreId))
  155 + {
  156 + correctBuId = storeDict[x.StoreId].Syb;
  157 + if (!string.IsNullOrEmpty(correctBuId))
  158 + {
  159 + correctBuName = GetBusinessUnitDisplayName(correctBuId, buNameMap);
  160 + }
  161 + }
  162 + }
  163 +
  164 + // 如果还是没有找到,返回"未知"
  165 + if (string.IsNullOrEmpty(correctBuName))
  166 + {
  167 + correctBuName = "未知";
  168 + }
  169 +
  170 + return new
  171 + {
  172 + id = x.Id,
  173 + storeId = x.StoreId,
  174 + storeName = x.StoreName,
  175 + businessUnitId = correctBuId,
  176 + businessUnitName = correctBuName,
  177 + year = x.Year,
  178 + month = x.Month,
  179 + totalPerformance = x.TotalPerformance,
  180 + totalConsume = x.TotalConsume,
  181 + headCount = x.HeadCount,
  182 + personTime = x.PersonTime,
  183 + projectCount = x.ProjectCount
  184 + };
  185 + }).ToList()
  186 + };
  187 +
  188 + return result;
  189 + }
  190 +
  191 + /// <summary>
  192 + /// 保存(新增或更新)
  193 + /// </summary>
  194 + [HttpPost("save")]
  195 + public async Task Save([FromBody] AnnualSummaryInput input)
  196 + {
  197 + if (string.IsNullOrEmpty(input.Id))
  198 + {
  199 + // Uniqueness check
  200 + var exist = await _db.Queryable<LqAnnualSummaryEntity>()
  201 + .AnyAsync(x => x.StoreId == input.StoreId && x.Year == input.Year && x.Month == input.Month && x.IsEffective == 1);
  202 + if (exist) throw new Exception($"该门店 {input.Year}年{input.Month}月 数据已存在");
  203 +
  204 + var entity = input.Adapt<LqAnnualSummaryEntity>();
  205 + entity.Id = YitIdHelper.NextId().ToString();
  206 + entity.CreateTime = DateTime.Now;
  207 + entity.IsEffective = 1;
  208 + await _db.Insertable(entity).ExecuteCommandAsync();
  209 + }
  210 + else
  211 + {
  212 + var entity = await _db.Queryable<LqAnnualSummaryEntity>().FirstAsync(x => x.Id == input.Id);
  213 + if (entity == null) throw new Exception("数据不存在");
  214 +
  215 + // Update fields
  216 + entity.TotalPerformance = input.TotalPerformance;
  217 + entity.TotalConsume = input.TotalConsume;
  218 + entity.HeadCount = input.HeadCount;
  219 + entity.PersonTime = input.PersonTime;
  220 + entity.ProjectCount = input.ProjectCount;
  221 +
  222 + entity.UpdateTime = DateTime.Now;
  223 + await _db.Updateable(entity).ExecuteCommandAsync();
  224 + }
  225 + }
  226 +
  227 + /// <summary>
  228 + /// 删除
  229 + /// </summary>
  230 + [HttpPost("delete")]
  231 + public async Task Delete([FromBody] List<string> ids)
  232 + {
  233 + await _db.Updateable<LqAnnualSummaryEntity>()
  234 + .SetColumns(x => x.IsEffective == 0)
  235 + .Where(x => ids.Contains(x.Id))
  236 + .ExecuteCommandAsync();
  237 + }
  238 +
  239 + #endregion
  240 +
  241 + #region Excel 导入导出
  242 +
  243 + /// <summary>
  244 + /// 导入数据
  245 + /// </summary>
  246 + [HttpPost("import")]
  247 + public async Task Import(IFormFile file)
  248 + {
  249 + if (file == null || file.Length == 0) throw new Exception("请上传文件");
  250 +
  251 + // 检查文件格式
  252 + var allowedExtensions = new[] { ".xlsx", ".xls" };
  253 + var fileExtension = Path.GetExtension(file.FileName).ToLowerInvariant();
  254 + if (!allowedExtensions.Contains(fileExtension))
  255 + {
  256 + throw new Exception("只支持.xlsx和.xls格式的Excel文件");
  257 + }
  258 +
  259 + // 保存临时文件
  260 + var tempFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + Path.GetExtension(file.FileName));
  261 + try
  262 + {
  263 + using (var stream = new FileStream(tempFilePath, FileMode.Create))
  264 + {
  265 + await file.CopyToAsync(stream);
  266 + }
  267 +
  268 + // 使用NPOI直接读取Excel文件,支持读取公式的计算值
  269 + var list = new List<AnnualSummaryImportDto>();
  270 + var errorMessages = new List<string>();
  271 +
  272 + using (var fileStream = new FileStream(tempFilePath, FileMode.Open, FileAccess.Read))
  273 + {
  274 + IWorkbook workbook = null;
  275 + if (fileExtension == ".xlsx")
  276 + {
  277 + workbook = new XSSFWorkbook(fileStream);
  278 + }
  279 + else
  280 + {
  281 + workbook = new HSSFWorkbook(fileStream);
  282 + }
  283 +
  284 + ISheet sheet = workbook.GetSheetAt(0); // 第一个工作表
  285 + if (sheet == null || sheet.LastRowNum < 1)
  286 + {
  287 + throw new Exception("Excel文件中没有数据行(至少需要标题行和一行数据)");
  288 + }
  289 +
  290 + // 读取标题行,确定列索引
  291 + var headerRow = sheet.GetRow(0);
  292 + if (headerRow == null)
  293 + {
  294 + throw new Exception("Excel文件第一行(标题行)为空");
  295 + }
  296 +
  297 + // 根据列名查找列索引(支持多种可能的列名)
  298 + int GetColumnIndex(string[] possibleNames)
  299 + {
  300 + for (int col = 0; col < headerRow.LastCellNum; col++)
  301 + {
  302 + var cell = headerRow.GetCell(col);
  303 + if (cell == null) continue;
  304 + var columnName = GetCellValue(cell)?.Trim() ?? "";
  305 + foreach (var name in possibleNames)
  306 + {
  307 + if (columnName == name || columnName.Contains(name) || name.Contains(columnName))
  308 + {
  309 + return col;
  310 + }
  311 + }
  312 + }
  313 + return -1;
  314 + }
  315 +
  316 + // 查找各列的索引(根据实际Excel格式)
  317 + var businessUnitColIndex = GetColumnIndex(new[] { "事业部", "归属事业部", "BusinessUnit" });
  318 + var storeNameColIndex = GetColumnIndex(new[] { "门店", "门店名称", "店名", "店铺名称" });
  319 + var yearColIndex = GetColumnIndex(new[] { "年份", "年", "Year" });
  320 + var monthColIndex = GetColumnIndex(new[] { "月份", "月", "Month" });
  321 + var perfColIndex = GetColumnIndex(new[] { "总业绩", "业绩", "总营业额", "TotalPerformance" });
  322 + var consumeColIndex = GetColumnIndex(new[] { "总消耗", "消耗", "TotalConsume" });
  323 + var headColIndex = GetColumnIndex(new[] { "人头数", "客头数", "HeadCount" });
  324 + var personColIndex = GetColumnIndex(new[] { "人次数", "客次数", "PersonTime" });
  325 + var projectColIndex = GetColumnIndex(new[] { "项目数", "总项目数", "ProjectCount" });
  326 +
  327 + // 验证必填列
  328 + if (storeNameColIndex == -1)
  329 + {
  330 + throw new Exception("Excel文件中未找到门店名称列,请确保第一行包含'门店名称'列");
  331 + }
  332 + if (yearColIndex == -1)
  333 + {
  334 + throw new Exception("Excel文件中未找到年份列,请确保第一行包含'年份'列");
  335 + }
  336 + if (monthColIndex == -1)
  337 + {
  338 + throw new Exception("Excel文件中未找到月份列,请确保第一行包含'月份'列");
  339 + }
  340 +
  341 + // 从第1行开始读取数据(跳过标题行,索引0是标题行)
  342 + for (int i = 1; i <= sheet.LastRowNum; i++)
  343 + {
  344 + var row = sheet.GetRow(i);
  345 + if (row == null) continue;
  346 +
  347 + try
  348 + {
  349 + // 读取门店名称,支持公式计算值
  350 + var storeNameCell = row.GetCell(storeNameColIndex);
  351 + if (storeNameCell == null) continue;
  352 +
  353 + string storeName = GetCellValue(storeNameCell);
  354 + if (string.IsNullOrWhiteSpace(storeName)) continue; // 跳过空行
  355 +
  356 + // 检测是否为公式文本(如果GetCellValue返回的是公式文本,说明计算失败)
  357 + if (storeName.StartsWith("_xlfn.") || storeName.StartsWith("=") ||
  358 + storeName.Contains("XLOOKUP") || storeName.Contains("VLOOKUP"))
  359 + {
  360 + errorMessages.Add($"第{i + 1}行:门店名称包含无法计算的公式({storeName}),已跳过。请确保门店名称列是实际值而不是公式。");
  361 + continue;
  362 + }
  363 +
  364 + // 读取事业部名称(从Excel中读取)
  365 + string businessUnitName = null;
  366 + if (businessUnitColIndex >= 0)
  367 + {
  368 + var businessUnitCell = row.GetCell(businessUnitColIndex);
  369 + if (businessUnitCell != null)
  370 + {
  371 + businessUnitName = GetCellValue(businessUnitCell);
  372 + if (!string.IsNullOrWhiteSpace(businessUnitName))
  373 + {
  374 + businessUnitName = businessUnitName.Trim();
  375 + }
  376 + }
  377 + }
  378 +
  379 + // 读取其他字段
  380 + var yearCell = row.GetCell(yearColIndex);
  381 + var monthCell = row.GetCell(monthColIndex);
  382 + var perfCell = perfColIndex >= 0 ? row.GetCell(perfColIndex) : null;
  383 + var consumeCell = consumeColIndex >= 0 ? row.GetCell(consumeColIndex) : null;
  384 + var headCell = headColIndex >= 0 ? row.GetCell(headColIndex) : null;
  385 + var personCell = personColIndex >= 0 ? row.GetCell(personColIndex) : null;
  386 + var projectCell = projectColIndex >= 0 ? row.GetCell(projectColIndex) : null;
  387 +
  388 + // 解析年份(支持"2024年"格式)
  389 + int year = 0;
  390 + var yearStr = GetCellValue(yearCell);
  391 + if (!string.IsNullOrWhiteSpace(yearStr))
  392 + {
  393 + // 提取数字部分(如"2024年" -> "2024")
  394 + var yearMatch = Regex.Match(yearStr, @"(\d{4})");
  395 + if (yearMatch.Success)
  396 + {
  397 + int.TryParse(yearMatch.Groups[1].Value, out year);
  398 + }
  399 + else
  400 + {
  401 + int.TryParse(yearStr, out year);
  402 + }
  403 + }
  404 +
  405 + // 解析月份(支持"1月"、"01月"格式)
  406 + int month = 0;
  407 + var monthStr = GetCellValue(monthCell);
  408 + if (!string.IsNullOrWhiteSpace(monthStr))
  409 + {
  410 + // 提取数字部分(如"1月" -> "1")
  411 + var monthMatch = Regex.Match(monthStr, @"(\d{1,2})");
  412 + if (monthMatch.Success)
  413 + {
  414 + int.TryParse(monthMatch.Groups[1].Value, out month);
  415 + }
  416 + else
  417 + {
  418 + int.TryParse(monthStr, out month);
  419 + }
  420 + }
  421 +
  422 + var item = new AnnualSummaryImportDto
  423 + {
  424 + StoreName = storeName.Trim(),
  425 + BusinessUnitName = businessUnitName,
  426 + Year = year,
  427 + Month = month,
  428 + TotalPerformance = perfCell != null && decimal.TryParse(GetCellValue(perfCell), out var perf) ? perf : 0m,
  429 + TotalConsume = consumeCell != null && decimal.TryParse(GetCellValue(consumeCell), out var consume) ? consume : 0m,
  430 + HeadCount = headCell != null && decimal.TryParse(GetCellValue(headCell), out var head) ? head : 0m,
  431 + PersonTime = personCell != null && decimal.TryParse(GetCellValue(personCell), out var person) ? person : 0m,
  432 + ProjectCount = projectCell != null && decimal.TryParse(GetCellValue(projectCell), out var project) ? project : 0m
  433 + };
  434 + list.Add(item);
  435 + }
  436 + catch (Exception ex)
  437 + {
  438 + errorMessages.Add($"第{i + 1}行:读取数据时出错 - {ex.Message}");
  439 + continue;
  440 + }
  441 + }
  442 +
  443 + workbook.Close();
  444 + }
  445 +
  446 + if (!list.Any())
  447 + {
  448 + if (errorMessages.Any())
  449 + {
  450 + throw new Exception($"导入失败:没有有效数据。\n{string.Join("\n", errorMessages)}");
  451 + }
  452 + throw new Exception("导入失败:Excel文件中没有有效数据行");
  453 + }
  454 +
  455 + // 预加载所有门店信息,用于匹配 StoreId 和 BusinessUnit
  456 + var allStores = await _db.Queryable<LqMdxxEntity>().ToListAsync();
  457 +
  458 + // 预加载所有组织机构信息,用于匹配事业部
  459 + var allOrgs = await _db.Queryable<OrganizeEntity>()
  460 + .Where(x => x.DeleteMark == null)
  461 + .Select(x => new { x.Id, x.FullName })
  462 + .ToListAsync();
  463 +
  464 + var entities = new List<LqAnnualSummaryEntity>();
  465 + var successCount = 0;
  466 + var failCount = 0;
  467 +
  468 + foreach (var item in list)
  469 + {
  470 + if (string.IsNullOrEmpty(item.StoreName))
  471 + {
  472 + failCount++;
  473 + continue;
  474 + }
  475 +
  476 + try
  477 + {
  478 + // 增强匹配逻辑:支持模糊匹配和简称
  479 + // Excel中的门店名称可能是简化名称(如"紫荆"),需要匹配完整名称(如"绿纤紫荆店")
  480 + var store = allStores.FirstOrDefault(x => x.Dm == item.StoreName);
  481 + if (store == null)
  482 + {
  483 + // 精确匹配(包含关系)
  484 + store = allStores.FirstOrDefault(x => x.Dm.Contains(item.StoreName));
  485 + }
  486 + if (store == null)
  487 + {
  488 + // 去除 绿纤 和 店 之后再比
  489 + var cleanExcelName = item.StoreName.Replace("绿纤", "").Replace("店", "").Trim();
  490 + if (!string.IsNullOrEmpty(cleanExcelName))
  491 + {
  492 + store = allStores.FirstOrDefault(x =>
  493 + x.Dm.Replace("绿纤", "").Replace("店", "").Trim() == cleanExcelName);
  494 + }
  495 + }
  496 + if (store == null)
  497 + {
  498 + // 反向匹配:Excel名称可能是门店名称的一部分(如"紫荆"匹配"绿纤紫荆店")
  499 + store = allStores.FirstOrDefault(x =>
  500 + x.Dm.Replace("绿纤", "").Replace("店", "").Trim().Contains(item.StoreName.Trim()));
  501 + }
  502 + if (store == null)
  503 + {
  504 + // 最后尝试:Excel名称包含门店名称的一部分(如"静居寺"匹配"绿纤静居寺店")
  505 + store = allStores.FirstOrDefault(x =>
  506 + item.StoreName.Trim().Contains(x.Dm.Replace("绿纤", "").Replace("店", "").Trim()) ||
  507 + x.Dm.Replace("绿纤", "").Replace("店", "").Trim().Contains(item.StoreName.Trim()));
  508 + }
  509 +
  510 + if (store == null)
  511 + {
  512 + failCount++;
  513 + errorMessages.Add($"门店【{item.StoreName}】未找到匹配的门店。请确保 Excel 中的门店名称在系统中存在(系统示例:{allStores.FirstOrDefault()?.Dm ?? "无"})");
  514 + continue;
  515 + }
  516 +
  517 + var entity = new LqAnnualSummaryEntity
  518 + {
  519 + StoreId = store.Id,
  520 + StoreName = store.Dm,
  521 + Year = item.Year,
  522 + Month = item.Month,
  523 + TotalPerformance = item.TotalPerformance,
  524 + TotalConsume = item.TotalConsume,
  525 + HeadCount = item.HeadCount,
  526 + PersonTime = item.PersonTime,
  527 + ProjectCount = item.ProjectCount,
  528 + IsEffective = 1,
  529 + CreateTime = DateTime.Now,
  530 + CreateUser = "Import"
  531 + };
  532 +
  533 + // 处理事业部信息
  534 + // 1. 优先从Excel中读取的事业部名称匹配组织表(只匹配事业部,不匹配科技部)
  535 + if (!string.IsNullOrEmpty(item.BusinessUnitName))
  536 + {
  537 + // 先尝试精确匹配
  538 + var org = allOrgs.FirstOrDefault(x => x.FullName == item.BusinessUnitName);
  539 +
  540 + // 如果精确匹配失败,尝试模糊匹配,但只匹配包含"事业"的组织(排除科技部)
  541 + if (org == null)
  542 + {
  543 + org = allOrgs.FirstOrDefault(x =>
  544 + x.FullName.Contains("事业") && // 确保是事业部
  545 + (x.FullName.Contains(item.BusinessUnitName) ||
  546 + item.BusinessUnitName.Contains(x.FullName)));
  547 + }
  548 +
  549 + if (org != null)
  550 + {
  551 + // 验证匹配到的组织确实是事业部(不是科技部)
  552 + if (org.FullName.Contains("事业") && !org.FullName.Contains("科技"))
  553 + {
  554 + entity.BusinessUnitId = org.Id;
  555 + entity.BusinessUnitName = org.FullName;
  556 + }
  557 + else
  558 + {
  559 + // 如果匹配到的是科技部,记录错误,从门店表获取事业部
  560 + errorMessages.Add($"门店【{item.StoreName}】的事业部【{item.BusinessUnitName}】匹配到了科技部,已从门店表获取正确的事业部信息。");
  561 + // 从门店表获取事业部
  562 + if (!string.IsNullOrEmpty(store.Syb))
  563 + {
  564 + // 设置事业部ID
  565 + entity.BusinessUnitId = store.Syb;
  566 +
  567 + var sybOrg = allOrgs.FirstOrDefault(x => x.Id == store.Syb);
  568 + if (sybOrg != null)
  569 + {
  570 + // 设置事业部名称
  571 + entity.BusinessUnitName = sybOrg.FullName;
  572 + }
  573 + else
  574 + {
  575 + // 如果找不到组织,记录警告
  576 + errorMessages.Add($"门店【{item.StoreName}】的事业部ID【{store.Syb}】未找到对应的组织名称。");
  577 + entity.BusinessUnitName = null;
  578 + }
  579 + }
  580 + else
  581 + {
  582 + // 如果门店表中也没有事业部信息
  583 + entity.BusinessUnitId = null;
  584 + entity.BusinessUnitName = null;
  585 + }
  586 + }
  587 + }
  588 + else
  589 + {
  590 + // 如果找不到匹配的组织,从门店表获取事业部
  591 + errorMessages.Add($"门店【{item.StoreName}】的事业部【{item.BusinessUnitName}】未找到匹配的组织,已从门店表获取事业部信息。");
  592 + if (!string.IsNullOrEmpty(store.Syb))
  593 + {
  594 + // 设置事业部ID
  595 + entity.BusinessUnitId = store.Syb;
  596 +
  597 + var sybOrg = allOrgs.FirstOrDefault(x => x.Id == store.Syb);
  598 + if (sybOrg != null)
  599 + {
  600 + // 设置事业部名称
  601 + entity.BusinessUnitName = sybOrg.FullName;
  602 + }
  603 + else
  604 + {
  605 + // 如果找不到组织,记录警告
  606 + errorMessages.Add($"门店【{item.StoreName}】的事业部ID【{store.Syb}】未找到对应的组织名称。");
  607 + entity.BusinessUnitName = null;
  608 + }
  609 + }
  610 + else
  611 + {
  612 + // 如果门店表中也没有事业部信息
  613 + entity.BusinessUnitId = null;
  614 + entity.BusinessUnitName = null;
  615 + }
  616 + }
  617 + }
  618 + else
  619 + {
  620 + // 2. 如果Excel中没有事业部信息,从门店信息中获取 Syb (事业部)
  621 + // 注意:只使用Syb,不使用Kjb(科技部)
  622 + string buId = store.Syb;
  623 +
  624 + if (!string.IsNullOrEmpty(buId))
  625 + {
  626 + // 设置事业部ID
  627 + entity.BusinessUnitId = buId;
  628 +
  629 + // 从组织表获取事业部名称
  630 + var org = allOrgs.FirstOrDefault(x => x.Id == buId);
  631 + if (org != null)
  632 + {
  633 + // 设置事业部名称
  634 + entity.BusinessUnitName = org.FullName;
  635 + }
  636 + else
  637 + {
  638 + // 如果找不到组织,记录警告,但不设置名称(保持为空)
  639 + errorMessages.Add($"门店【{item.StoreName}】的事业部ID【{buId}】未找到对应的组织名称。");
  640 + entity.BusinessUnitName = null; // 明确设置为null,而不是ID
  641 + }
  642 + }
  643 + else
  644 + {
  645 + // 如果门店表中也没有事业部信息,记录警告
  646 + errorMessages.Add($"门店【{item.StoreName}】没有事业部信息,请检查门店配置。");
  647 + // 明确设置为null
  648 + entity.BusinessUnitId = null;
  649 + entity.BusinessUnitName = null;
  650 + }
  651 + }
  652 +
  653 + entities.Add(entity);
  654 + }
  655 + catch (Exception ex)
  656 + {
  657 + failCount++;
  658 + errorMessages.Add($"处理门店【{item.StoreName}】时出错:{ex.Message}");
  659 + continue;
  660 + }
  661 + }
  662 +
  663 + // 批量处理:覆盖逻辑
  664 + // 先删除已存在的 (StoreId + Year + Month)
  665 + foreach (var group in entities.GroupBy(x => new { x.StoreId, x.Year, x.Month }))
  666 + {
  667 + // 删除旧数据
  668 + await _db.Deleteable<LqAnnualSummaryEntity>()
  669 + .Where(x => x.StoreId == group.Key.StoreId && x.Year == group.Key.Year && x.Month == group.Key.Month)
  670 + .ExecuteCommandAsync();
  671 +
  672 + // 插入新数据 (取Excel中最后一条,防止Excel自身重复)
  673 + var toInsert = group.Last();
  674 + toInsert.Id = YitIdHelper.NextId().ToString();
  675 + toInsert.CreateTime = DateTime.Now;
  676 + toInsert.CreateUser = "Import";
  677 + await _db.Insertable(toInsert).ExecuteCommandAsync();
  678 + successCount++;
  679 + }
  680 +
  681 + // 返回导入结果
  682 + var resultMessage = $"导入完成:成功 {successCount} 条";
  683 + if (failCount > 0 || errorMessages.Any())
  684 + {
  685 + resultMessage += $",失败 {failCount} 条";
  686 + if (errorMessages.Any())
  687 + {
  688 + resultMessage += $"\n错误详情:\n{string.Join("\n", errorMessages)}";
  689 + }
  690 + }
  691 +
  692 + // 如果有错误但部分成功,返回警告信息(通过异常返回,但包含成功信息)
  693 + if (successCount > 0 && (failCount > 0 || errorMessages.Any()))
  694 + {
  695 + // 部分成功,抛出包含成功和失败信息的异常
  696 + throw new Exception(resultMessage);
  697 + }
  698 + else if (successCount == 0)
  699 + {
  700 + // 全部失败,抛出异常
  701 + throw new Exception(resultMessage);
  702 + }
  703 + }
  704 + finally
  705 + {
  706 + // 删除临时文件
  707 + if (File.Exists(tempFilePath))
  708 + {
  709 + File.Delete(tempFilePath);
  710 + }
  711 + }
  712 + }
  713 +
  714 + #endregion
  715 +
  716 + #region 统计报表
  717 +
  718 + /// <summary>
  719 + /// 4.1 全年门店业绩表
  720 + /// </summary>
  721 + [HttpPost("GetTotalPerformanceStat")]
  722 + public async Task<dynamic> GetTotalPerformanceStat([FromBody] AnnualSummaryQueryInput input)
  723 + {
  724 + return await GetMonthlyStat(input, x => x.TotalPerformance);
  725 + }
  726 +
  727 + /// <summary>
  728 + /// 4.2 全年门店消耗表
  729 + /// </summary>
  730 + [HttpPost("GetTotalConsumeStat")]
  731 + public async Task<dynamic> GetTotalConsumeStat([FromBody] AnnualSummaryQueryInput input)
  732 + {
  733 + return await GetMonthlyStat(input, x => x.TotalConsume);
  734 + }
  735 +
  736 + /// <summary>
  737 + /// 4.3 年度门店人头表
  738 + /// </summary>
  739 + [HttpPost("GetHeadCountStat")]
  740 + public async Task<dynamic> GetHeadCountStat([FromBody] AnnualSummaryQueryInput input)
  741 + {
  742 + return await GetMonthlyStat(input, x => x.HeadCount);
  743 + }
  744 +
  745 + /// <summary>
  746 + /// 4.4 年度门店项目数表
  747 + /// </summary>
  748 + [HttpPost("GetProjectCountStat")]
  749 + public async Task<dynamic> GetProjectCountStat([FromBody] AnnualSummaryQueryInput input)
  750 + {
  751 + return await GetMonthlyStat(input, x => x.ProjectCount);
  752 + }
  753 +
  754 + /// <summary>
  755 + /// 4.5 年度门店人次表
  756 + /// </summary>
  757 + [HttpGet("GetPersonTimeStat")]
  758 + public async Task<dynamic> GetPersonTimeStat([FromQuery] AnnualSummaryQueryInput input)
  759 + {
  760 + return await GetMonthlyStat(input, x => x.PersonTime);
  761 + }
  762 +
  763 + /// <summary>
  764 + /// 通用月度趋势统计
  765 + /// </summary>
  766 + [HttpGet("GetMonthlyTrend")]
  767 + public async Task<dynamic> GetMonthlyTrend([FromQuery] AnnualSummaryQueryInput input, [FromQuery] string type)
  768 + {
  769 + // 确保 type 参数正确绑定
  770 + if (string.IsNullOrEmpty(type))
  771 + {
  772 + type = "totalperformance"; // 默认值
  773 + }
  774 +
  775 + // 根据 type 参数选择正确的字段选择器
  776 + Func<LqAnnualSummaryEntity, decimal> fieldSelector;
  777 + switch (type.ToLower())
  778 + {
  779 + case "totalperformance":
  780 + fieldSelector = x => x.TotalPerformance;
  781 + break;
  782 + case "totalconsume":
  783 + fieldSelector = x => x.TotalConsume;
  784 + break;
  785 + case "headcount":
  786 + fieldSelector = x => x.HeadCount;
  787 + break;
  788 + case "persontime":
  789 + fieldSelector = x => x.PersonTime;
  790 + break;
  791 + case "projectcount":
  792 + fieldSelector = x => x.ProjectCount;
  793 + break;
  794 + default:
  795 + fieldSelector = x => x.TotalPerformance;
  796 + break;
  797 + }
  798 +
  799 + return await GetMonthlyStat(input, fieldSelector);
  800 + }
  801 +
  802 + /// <summary>
  803 + /// 4.6 门店五项指标统计图
  804 + /// </summary>
  805 + [HttpPost("GetStoreIndicatorsStat")]
  806 + public async Task<dynamic> GetStoreIndicatorsStat([FromBody] AnnualSummaryQueryInput input)
  807 + {
  808 + return await GetIndicatorStat(input, x => x.TotalPerformance);
  809 + }
  810 +
  811 + /// <summary>
  812 + /// 获取门店指标详情
  813 + /// </summary>
  814 + /// <param name="input">查询参数</param>
  815 + /// <param name="type">指标类型</param>
  816 + /// <returns>指标统计输出</returns>
  817 + [HttpGet("GetStoreIndicatorDetail")]
  818 + public async Task<dynamic> GetStoreIndicatorDetail([FromQuery] AnnualSummaryQueryInput input, string type)
  819 + {
  820 + switch (type?.ToLower())
  821 + {
  822 + case "totalperformance": return await GetIndicatorStat(input, x => x.TotalPerformance);
  823 + case "totalconsume": return await GetIndicatorStat(input, x => x.TotalConsume);
  824 + case "headcount": return await GetIndicatorStat(input, x => x.HeadCount);
  825 + case "persontime": return await GetIndicatorStat(input, x => x.PersonTime);
  826 + case "projectcount": return await GetIndicatorStat(input, x => x.ProjectCount);
  827 + default: return await GetIndicatorStat(input, x => x.TotalPerformance);
  828 + }
  829 + }
  830 +
  831 + /// <summary>
  832 + /// 4.7 事业部五项指标总计图
  833 + /// </summary>
  834 + [HttpGet("GetBusinessUnitIndicatorsStat")]
  835 + public async Task<dynamic> GetBusinessUnitIndicatorsStat([FromQuery] AnnualSummaryQueryInput input, string type)
  836 + {
  837 + // 类似 4.6,但是按 BusinessUnitName 分组
  838 + switch (type?.ToLower())
  839 + {
  840 + case "totalperformance": return await GetBuIndicatorStat(input, x => x.TotalPerformance);
  841 + case "totalconsume": return await GetBuIndicatorStat(input, x => x.TotalConsume);
  842 + case "headcount": return await GetBuIndicatorStat(input, x => x.HeadCount);
  843 + case "persontime": return await GetBuIndicatorStat(input, x => x.PersonTime);
  844 + case "projectcount": return await GetBuIndicatorStat(input, x => x.ProjectCount);
  845 + default: return await GetBuIndicatorStat(input, x => x.TotalPerformance);
  846 + }
  847 + }
  848 +
  849 + /// <summary>
  850 + /// 4.8 事业部内部汇总 (宽表)
  851 + /// </summary>
  852 + [HttpPost("GetBusinessUnitSummaryStat")]
  853 + public async Task<dynamic> GetBusinessUnitSummaryStat([FromBody] AnnualSummaryQueryInput input)
  854 + {
  855 + int year = input.Year ?? DateTime.Now.Year;
  856 +
  857 + // 获取事业部名称映射
  858 + var buNameMap = await GetBusinessUnitNameMapAsync();
  859 +
  860 + var currentData = await _db.Queryable<LqAnnualSummaryEntity>()
  861 + .Where(x => x.IsEffective == 1 && x.Year == year)
  862 + .WhereIF(!string.IsNullOrEmpty(input.StoreName), x => x.StoreName.Contains(input.StoreName))
  863 + .ToListAsync();
  864 +
  865 + var lastYearData = await _db.Queryable<LqAnnualSummaryEntity>()
  866 + .Where(x => x.IsEffective == 1 && x.Year == year - 1)
  867 + .WhereIF(!string.IsNullOrEmpty(input.StoreName), x => x.StoreName.Contains(input.StoreName))
  868 + .ToListAsync();
  869 +
  870 + // 聚合数据:按 (BusinessUnitName, StoreName) - 其实就是按 StoreName,因为 Store 属于 BU
  871 + // 先获取所有涉及的门店
  872 + var allStores = currentData.Select(x => new { x.StoreId, x.StoreName, x.BusinessUnitName, x.BusinessUnitId })
  873 + .Union(lastYearData.Select(x => new { x.StoreId, x.StoreName, x.BusinessUnitName, x.BusinessUnitId }))
  874 + .Distinct()
  875 + .OrderBy(x => x.BusinessUnitName)
  876 + .ThenBy(x => x.StoreName)
  877 + .ToList();
  878 +
  879 + var output = new BusinessUnitSummaryOutput();
  880 +
  881 + foreach (var store in allStores)
  882 + {
  883 + // Current Year Sums
  884 + var curr = currentData.Where(x => x.StoreId == store.StoreId).ToList();
  885 + var last = lastYearData.Where(x => x.StoreId == store.StoreId).ToList();
  886 +
  887 + // 优先使用BusinessUnitId,如果没有则使用BusinessUnitName
  888 + var buId = store.BusinessUnitId ?? store.BusinessUnitName;
  889 + var buDisplayName = GetBusinessUnitDisplayName(buId, buNameMap);
  890 +
  891 + var row = new BusinessUnitSummaryRow
  892 + {
  893 + BusinessUnitName = buDisplayName,
  894 + StoreName = store.StoreName,
  895 +
  896 + CurrentYearPerformance = curr.Sum(x => x.TotalPerformance),
  897 + LastYearPerformance = last.Sum(x => x.TotalPerformance),
  898 +
  899 + CurrentYearConsume = curr.Sum(x => x.TotalConsume),
  900 + LastYearConsume = last.Sum(x => x.TotalConsume),
  901 +
  902 + CurrentYearHeadCount = curr.Sum(x => x.HeadCount),
  903 + LastYearHeadCount = last.Sum(x => x.HeadCount),
  904 +
  905 + CurrentYearPersonTime = curr.Sum(x => x.PersonTime),
  906 + LastYearPersonTime = last.Sum(x => x.PersonTime),
  907 +
  908 + CurrentYearProjectCount = curr.Sum(x => x.ProjectCount),
  909 + LastYearProjectCount = last.Sum(x => x.ProjectCount),
  910 + };
  911 +
  912 + row.PerformanceGrowthRate = CalculateGrowthRate(row.CurrentYearPerformance, row.LastYearPerformance);
  913 + row.ConsumeGrowthRate = CalculateGrowthRate(row.CurrentYearConsume, row.LastYearConsume);
  914 + row.HeadCountGrowthRate = CalculateGrowthRate(row.CurrentYearHeadCount, row.LastYearHeadCount);
  915 + row.PersonTimeGrowthRate = CalculateGrowthRate(row.CurrentYearPersonTime, row.LastYearPersonTime);
  916 + row.ProjectCountGrowthRate = CalculateGrowthRate(row.CurrentYearProjectCount, row.LastYearProjectCount);
  917 +
  918 + output.Rows.Add(row);
  919 + }
  920 +
  921 + return new
  922 + {
  923 + list = output.Rows.Select(x => new
  924 + {
  925 + businessUnitName = x.BusinessUnitName,
  926 + storeName = x.StoreName,
  927 + currentPerformance = x.CurrentYearPerformance,
  928 + lastPerformance = x.LastYearPerformance,
  929 + performanceGrowthRate = x.PerformanceGrowthRate,
  930 + currentConsume = x.CurrentYearConsume,
  931 + lastConsume = x.LastYearConsume,
  932 + consumeGrowthRate = x.ConsumeGrowthRate,
  933 + currentHeadCount = x.CurrentYearHeadCount,
  934 + lastHeadCount = x.LastYearHeadCount,
  935 + headCountGrowthRate = x.HeadCountGrowthRate,
  936 + currentPersonTime = x.CurrentYearPersonTime,
  937 + lastPersonTime = x.LastYearPersonTime,
  938 + personTimeGrowthRate = x.PersonTimeGrowthRate,
  939 + currentProjectCount = x.CurrentYearProjectCount,
  940 + lastProjectCount = x.LastYearProjectCount,
  941 + projectCountGrowthRate = x.ProjectCountGrowthRate
  942 + }).ToList()
  943 + };
  944 + }
  945 +
  946 + #endregion
  947 +
  948 + #region 私有辅助方法
  949 +
  950 + private async Task<dynamic> GetMonthlyStat(AnnualSummaryQueryInput input, Func<LqAnnualSummaryEntity, decimal> fieldSelector)
  951 + {
  952 + int year = input.Year ?? DateTime.Now.Year;
  953 +
  954 + // 获取事业部名称映射
  955 + var buNameMap = await GetBusinessUnitNameMapAsync();
  956 +
  957 + // 获取本年度数据
  958 + var currentYearData = await _db.Queryable<LqAnnualSummaryEntity>()
  959 + .Where(x => x.IsEffective == 1 && x.Year == year)
  960 + .WhereIF(!string.IsNullOrEmpty(input.StoreName), x => x.StoreName.Contains(input.StoreName))
  961 + .ToListAsync();
  962 +
  963 + // 获取上年度数据 (用于计算同比/增长率)
  964 + var lastYearData = await _db.Queryable<LqAnnualSummaryEntity>()
  965 + .Where(x => x.IsEffective == 1 && x.Year == year - 1)
  966 + .WhereIF(!string.IsNullOrEmpty(input.StoreName), x => x.StoreName.Contains(input.StoreName))
  967 + .ToListAsync();
  968 +
  969 + // 整理所有门店 (包括今年有数据和去年有数据的)
  970 + var allStores = currentYearData.Select(x => new { x.StoreId, x.StoreName, x.BusinessUnitName, x.BusinessUnitId })
  971 + .Distinct()
  972 + .OrderBy(x => x.BusinessUnitName)
  973 + .ThenBy(x => x.StoreName)
  974 + .ToList();
  975 +
  976 + var output = new AnnualMonthlyStatOutput();
  977 +
  978 + foreach (var store in allStores)
  979 + {
  980 + // 优先使用BusinessUnitId,如果没有则使用BusinessUnitName
  981 + var buId = store.BusinessUnitId ?? store.BusinessUnitName;
  982 + var buDisplayName = GetBusinessUnitDisplayName(buId, buNameMap);
  983 +
  984 + var row = new MonthlyDataRow
  985 + {
  986 + BusinessUnitName = buDisplayName,
  987 + StoreName = store.StoreName
  988 + };
  989 +
  990 + var storeCurrData = currentYearData.Where(x => x.StoreId == store.StoreId).ToList();
  991 + var storeLastData = lastYearData.Where(x => x.StoreId == store.StoreId).ToList();
  992 +
  993 + // Fill months (Current Year) - 优化:避免重复调用 FirstOrDefault
  994 + for (int month = 1; month <= 12; month++)
  995 + {
  996 + var currMonthData = storeCurrData.FirstOrDefault(x => x.Month == month);
  997 + var lastMonthData = storeLastData.FirstOrDefault(x => x.Month == month);
  998 +
  999 + switch (month)
  1000 + {
  1001 + case 1: row.Month1 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth1 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break;
  1002 + case 2: row.Month2 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth2 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break;
  1003 + case 3: row.Month3 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth3 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break;
  1004 + case 4: row.Month4 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth4 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break;
  1005 + case 5: row.Month5 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth5 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break;
  1006 + case 6: row.Month6 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth6 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break;
  1007 + case 7: row.Month7 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth7 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break;
  1008 + case 8: row.Month8 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth8 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break;
  1009 + case 9: row.Month9 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth9 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break;
  1010 + case 10: row.Month10 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth10 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break;
  1011 + case 11: row.Month11 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth11 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break;
  1012 + case 12: row.Month12 = currMonthData != null ? fieldSelector(currMonthData) : 0; row.LastMonth12 = lastMonthData != null ? fieldSelector(lastMonthData) : 0; break;
  1013 + }
  1014 + }
  1015 +
  1016 + row.TotalCurrentYear = storeCurrData.Sum(x => fieldSelector(x));
  1017 + row.AvgCurrentYear = storeCurrData.Any() ? row.TotalCurrentYear / 12 : 0;
  1018 +
  1019 + row.TotalLastYear = storeLastData.Sum(x => fieldSelector(x));
  1020 + row.AvgLastYear = storeLastData.Any() ? row.TotalLastYear / 12 : 0;
  1021 +
  1022 + row.GrowthRate = CalculateGrowthRate(row.TotalCurrentYear, row.TotalLastYear);
  1023 +
  1024 + output.Rows.Add(row);
  1025 + }
  1026 +
  1027 + return new
  1028 + {
  1029 + monthColumns = output.MonthColumns.Select(c => c.Substring(0, 1).ToLower() + c.Substring(1)).ToList(),
  1030 + rows = output.Rows.Select(x => new
  1031 + {
  1032 + businessUnitName = x.BusinessUnitName,
  1033 + storeName = x.StoreName,
  1034 + month1 = x.Month1,
  1035 + month2 = x.Month2,
  1036 + month3 = x.Month3,
  1037 + month4 = x.Month4,
  1038 + month5 = x.Month5,
  1039 + month6 = x.Month6,
  1040 + month7 = x.Month7,
  1041 + month8 = x.Month8,
  1042 + month9 = x.Month9,
  1043 + month10 = x.Month10,
  1044 + month11 = x.Month11,
  1045 + month12 = x.Month12,
  1046 + lastMonth1 = x.LastMonth1,
  1047 + lastMonth2 = x.LastMonth2,
  1048 + lastMonth3 = x.LastMonth3,
  1049 + lastMonth4 = x.LastMonth4,
  1050 + lastMonth5 = x.LastMonth5,
  1051 + lastMonth6 = x.LastMonth6,
  1052 + lastMonth7 = x.LastMonth7,
  1053 + lastMonth8 = x.LastMonth8,
  1054 + lastMonth9 = x.LastMonth9,
  1055 + lastMonth10 = x.LastMonth10,
  1056 + lastMonth11 = x.LastMonth11,
  1057 + lastMonth12 = x.LastMonth12,
  1058 + totalCurrentYear = x.TotalCurrentYear,
  1059 + avgCurrentYear = x.AvgCurrentYear,
  1060 + totalLastYear = x.TotalLastYear,
  1061 + avgLastYear = x.AvgLastYear,
  1062 + growthRate = x.GrowthRate
  1063 + }).ToList()
  1064 + };
  1065 + }
  1066 +
  1067 + private async Task<dynamic> GetIndicatorStat(AnnualSummaryQueryInput input, Func<LqAnnualSummaryEntity, decimal> fieldSelector)
  1068 + {
  1069 + int year = input.Year ?? DateTime.Now.Year;
  1070 +
  1071 + // 获取事业部名称映射
  1072 + var buNameMap = await GetBusinessUnitNameMapAsync();
  1073 +
  1074 + var currentData = await _db.Queryable<LqAnnualSummaryEntity>()
  1075 + .Where(x => x.IsEffective == 1 && x.Year == year)
  1076 + .WhereIF(!string.IsNullOrEmpty(input.StoreName), x => x.StoreName.Contains(input.StoreName))
  1077 + .ToListAsync();
  1078 +
  1079 + var lastData = await _db.Queryable<LqAnnualSummaryEntity>()
  1080 + .Where(x => x.IsEffective == 1 && x.Year == year - 1)
  1081 + .WhereIF(!string.IsNullOrEmpty(input.StoreName), x => x.StoreName.Contains(input.StoreName))
  1082 + .ToListAsync();
  1083 +
  1084 + // 按门店ID去重,确保每个门店只出现一次
  1085 + // 如果同一门店有多个事业部,取第一个(通常应该只有一个)
  1086 + var allStores = currentData
  1087 + .GroupBy(x => x.StoreId)
  1088 + .Select(g => new
  1089 + {
  1090 + StoreId = g.Key,
  1091 + StoreName = g.First().StoreName,
  1092 + BusinessUnitName = g.First().BusinessUnitName,
  1093 + BusinessUnitId = g.First().BusinessUnitId
  1094 + })
  1095 + .OrderBy(x => x.BusinessUnitName)
  1096 + .ThenBy(x => x.StoreName)
  1097 + .ToList();
  1098 +
  1099 + var output = new IndicatorStatOutput();
  1100 + foreach (var store in allStores)
  1101 + {
  1102 + // 按门店ID汇总所有月份的数据
  1103 + var curVal = currentData.Where(x => x.StoreId == store.StoreId).Sum(x => fieldSelector(x));
  1104 + var lastVal = lastData.Where(x => x.StoreId == store.StoreId).Sum(x => fieldSelector(x));
  1105 +
  1106 + // 优先使用BusinessUnitId,如果没有则使用BusinessUnitName
  1107 + var buId = store.BusinessUnitId ?? store.BusinessUnitName;
  1108 + var buDisplayName = GetBusinessUnitDisplayName(buId, buNameMap);
  1109 +
  1110 + output.Rows.Add(new IndicatorDataRow
  1111 + {
  1112 + BusinessUnitName = buDisplayName,
  1113 + StoreName = store.StoreName,
  1114 + CurrentYearValue = curVal,
  1115 + LastYearValue = lastVal,
  1116 + GrowthRate = CalculateGrowthRate(curVal, lastVal)
  1117 + });
  1118 + }
  1119 + return new
  1120 + {
  1121 + rows = output.Rows.Select(x => new
  1122 + {
  1123 + businessUnitName = x.BusinessUnitName,
  1124 + storeName = x.StoreName,
  1125 + currentYearValue = x.CurrentYearValue,
  1126 + lastYearValue = x.LastYearValue,
  1127 + growthRate = x.GrowthRate
  1128 + }).ToList()
  1129 + };
  1130 + }
  1131 +
  1132 + private async Task<dynamic> GetBuIndicatorStat(AnnualSummaryQueryInput input, Func<LqAnnualSummaryEntity, decimal> fieldSelector)
  1133 + {
  1134 + int year = input.Year ?? DateTime.Now.Year;
  1135 +
  1136 + // 获取所有事业部(事业一部到事业六部)
  1137 + var allBusinessUnits = await _db.Queryable<OrganizeEntity>()
  1138 + .Where(x => x.Category == "department" && (x.DeleteMark == null || x.DeleteMark != 1))
  1139 + .Where(x => x.FullName.Contains("事业") && x.FullName != "事业部")
  1140 + .OrderBy(x => x.FullName)
  1141 + .ToListAsync();
  1142 +
  1143 + // 获取本年度数据
  1144 + var currentData = await _db.Queryable<LqAnnualSummaryEntity>()
  1145 + .Where(x => x.IsEffective == 1 && x.Year == year)
  1146 + .WhereIF(!string.IsNullOrEmpty(input.StoreName), x => x.StoreName.Contains(input.StoreName))
  1147 + .ToListAsync();
  1148 +
  1149 + // 获取上年度数据
  1150 + var lastData = await _db.Queryable<LqAnnualSummaryEntity>()
  1151 + .Where(x => x.IsEffective == 1 && x.Year == year - 1)
  1152 + .WhereIF(!string.IsNullOrEmpty(input.StoreName), x => x.StoreName.Contains(input.StoreName))
  1153 + .ToListAsync();
  1154 +
  1155 + // 创建事业部ID到名称的映射
  1156 + var buIdToNameMap = allBusinessUnits.ToDictionary(x => x.Id, x => x.FullName);
  1157 +
  1158 + // 获取年度汇总数据中有数据的事业部ID(从BusinessUnitId或BusinessUnitName中获取)
  1159 + var dataBuIds = currentData
  1160 + .Where(x => !string.IsNullOrEmpty(x.BusinessUnitId))
  1161 + .Select(x => x.BusinessUnitId)
  1162 + .Distinct()
  1163 + .ToList();
  1164 +
  1165 + // 如果BusinessUnitId为空,尝试从BusinessUnitName中解析(可能是ID)
  1166 + var dataBuNames = currentData
  1167 + .Where(x => string.IsNullOrEmpty(x.BusinessUnitId) && !string.IsNullOrEmpty(x.BusinessUnitName))
  1168 + .Select(x => x.BusinessUnitName)
  1169 + .Distinct()
  1170 + .ToList();
  1171 +
  1172 + // 合并所有有数据的事业部ID
  1173 + var allDataBuIds = dataBuIds.Union(dataBuNames).Distinct().ToList();
  1174 +
  1175 + var output = new IndicatorStatOutput();
  1176 +
  1177 + // 遍历所有事业部(确保显示所有6个事业部)
  1178 + foreach (var bu in allBusinessUnits)
  1179 + {
  1180 + // 查找该事业部的数据(通过BusinessUnitId或BusinessUnitName匹配)
  1181 + var curVal = currentData
  1182 + .Where(x => (x.BusinessUnitId == bu.Id || x.BusinessUnitName == bu.Id || x.BusinessUnitName == bu.FullName))
  1183 + .Sum(x => fieldSelector(x));
  1184 +
  1185 + var lastVal = lastData
  1186 + .Where(x => (x.BusinessUnitId == bu.Id || x.BusinessUnitName == bu.Id || x.BusinessUnitName == bu.FullName))
  1187 + .Sum(x => fieldSelector(x));
  1188 +
  1189 + output.Rows.Add(new IndicatorDataRow
  1190 + {
  1191 + BusinessUnitName = bu.FullName, // 使用事业部名称而不是ID
  1192 + StoreName = "汇总",
  1193 + CurrentYearValue = curVal,
  1194 + LastYearValue = lastVal,
  1195 + GrowthRate = CalculateGrowthRate(curVal, lastVal)
  1196 + });
  1197 + }
  1198 +
  1199 + return new
  1200 + {
  1201 + rows = output.Rows.Select(x => new
  1202 + {
  1203 + businessUnitName = x.BusinessUnitName,
  1204 + storeName = x.StoreName,
  1205 + currentYearValue = x.CurrentYearValue,
  1206 + lastYearValue = x.LastYearValue,
  1207 + growthRate = x.GrowthRate
  1208 + }).ToList()
  1209 + };
  1210 + }
  1211 +
  1212 + private string CalculateGrowthRate(decimal current, decimal last)
  1213 + {
  1214 + if (last == 0) return current > 0 ? "100%" : "0%";
  1215 + var rate = (current - last) / last;
  1216 + return (rate * 100).ToString("F2") + "%";
  1217 + }
  1218 +
  1219 + /// <summary>
  1220 + /// 获取事业部ID到名称的映射
  1221 + /// </summary>
  1222 + private async Task<Dictionary<string, string>> GetBusinessUnitNameMapAsync()
  1223 + {
  1224 + var orgs = await _db.Queryable<OrganizeEntity>()
  1225 + .Where(x => x.Category == "department" && (x.DeleteMark == null || x.DeleteMark != 1))
  1226 + .Select(x => new { x.Id, x.FullName })
  1227 + .ToListAsync();
  1228 +
  1229 + var map = new Dictionary<string, string>();
  1230 + foreach (var org in orgs)
  1231 + {
  1232 + map[org.Id] = org.FullName;
  1233 + // 如果名称本身也是ID格式,也添加映射
  1234 + if (!map.ContainsKey(org.FullName))
  1235 + {
  1236 + map[org.FullName] = org.FullName;
  1237 + }
  1238 + }
  1239 + return map;
  1240 + }
  1241 +
  1242 + /// <summary>
  1243 + /// 将事业部ID或名称转换为显示名称
  1244 + /// </summary>
  1245 + private string GetBusinessUnitDisplayName(string buIdOrName, Dictionary<string, string> buNameMap)
  1246 + {
  1247 + if (string.IsNullOrEmpty(buIdOrName))
  1248 + return "未知";
  1249 +
  1250 + // 如果映射中存在,返回名称
  1251 + if (buNameMap.ContainsKey(buIdOrName))
  1252 + return buNameMap[buIdOrName];
  1253 +
  1254 + // 如果本身就是名称(包含"事业"或"科技"等),直接返回
  1255 + if (buIdOrName.Contains("事业") || buIdOrName.Contains("科技") || buIdOrName.Contains("教育") || buIdOrName.Contains("大项目"))
  1256 + return buIdOrName;
  1257 +
  1258 + // 否则返回原值(可能是ID)
  1259 + return buIdOrName;
  1260 + }
  1261 +
  1262 + /// <summary>
  1263 + /// 获取Excel单元格的值(支持公式计算值)
  1264 + /// </summary>
  1265 + /// <param name="cell">单元格</param>
  1266 + /// <returns>单元格的值(字符串形式)</returns>
  1267 + private string GetCellValue(ICell cell)
  1268 + {
  1269 + if (cell == null) return string.Empty;
  1270 +
  1271 + try
  1272 + {
  1273 + // 如果是公式类型,先尝试获取计算后的值
  1274 + if (cell.CellType == CellType.Formula)
  1275 + {
  1276 + try
  1277 + {
  1278 + var formulaEvaluator = cell.Sheet.Workbook.GetCreationHelper().CreateFormulaEvaluator();
  1279 + var cellValue = formulaEvaluator.Evaluate(cell);
  1280 +
  1281 + // 根据计算结果类型返回相应的值
  1282 + switch (cellValue.CellType)
  1283 + {
  1284 + case CellType.String:
  1285 + return cellValue.StringValue?.Trim() ?? string.Empty;
  1286 + case CellType.Numeric:
  1287 + if (DateUtil.IsCellDateFormatted(cell))
  1288 + {
  1289 + return cellValue.NumberValue.ToString("yyyy/MM/dd");
  1290 + }
  1291 + var numValue = cellValue.NumberValue;
  1292 + if (numValue == Math.Floor(numValue))
  1293 + {
  1294 + return ((long)numValue).ToString();
  1295 + }
  1296 + return numValue.ToString();
  1297 + case CellType.Boolean:
  1298 + return cellValue.BooleanValue.ToString();
  1299 + case CellType.Error:
  1300 + // 公式计算错误,返回空字符串
  1301 + return string.Empty;
  1302 + default:
  1303 + return string.Empty;
  1304 + }
  1305 + }
  1306 + catch
  1307 + {
  1308 + // 如果公式计算失败,尝试读取公式文本
  1309 + // 如果包含公式特征,返回空字符串(会被后续逻辑跳过)
  1310 + var formulaText = cell.CellFormula;
  1311 + if (!string.IsNullOrEmpty(formulaText) &&
  1312 + (formulaText.Contains("XLOOKUP") || formulaText.Contains("VLOOKUP") ||
  1313 + formulaText.Contains("_xlfn.") || formulaText.StartsWith("=")))
  1314 + {
  1315 + return string.Empty;
  1316 + }
  1317 + return string.Empty;
  1318 + }
  1319 + }
  1320 +
  1321 + // 非公式类型,直接读取值
  1322 + switch (cell.CellType)
  1323 + {
  1324 + case CellType.String:
  1325 + return cell.StringCellValue?.Trim() ?? string.Empty;
  1326 +
  1327 + case CellType.Numeric:
  1328 + if (DateUtil.IsCellDateFormatted(cell))
  1329 + {
  1330 + return cell.DateCellValue.ToString("yyyy/MM/dd");
  1331 + }
  1332 + // 处理数字,避免科学计数法
  1333 + var numericValue = cell.NumericCellValue;
  1334 + if (numericValue == Math.Floor(numericValue))
  1335 + {
  1336 + return ((long)numericValue).ToString();
  1337 + }
  1338 + return numericValue.ToString();
  1339 +
  1340 + case CellType.Boolean:
  1341 + return cell.BooleanCellValue.ToString();
  1342 +
  1343 + case CellType.Blank:
  1344 + return string.Empty;
  1345 +
  1346 + default:
  1347 + return cell.ToString()?.Trim() ?? string.Empty;
  1348 + }
  1349 + }
  1350 + catch
  1351 + {
  1352 + return string.Empty;
  1353 + }
  1354 + }
  1355 +
  1356 + #endregion
  1357 + }
  1358 +}
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqAssistantSalaryService.cs
... ... @@ -334,9 +334,28 @@ namespace NCC.Extend
334 334 decimal performanceRatio = salary.StoreTotalPerformance / salary.StoreLifeline;
335 335  
336 336 // 根据岗位类型确定提成比例
337   - // 店助和店助主任使用相同的阶梯提成规则
338   - // 先计算总提成金额(阶梯计算)
339   - decimal totalCommission = CalculateAssistantCommission(salary.StoreTotalPerformance, salary.StoreLifeline);
  337 + // 店助和店助主任使用相同的分段提成规则,但100%以上部分比例不同
  338 + decimal totalCommission;
  339 + if (isDirector)
  340 + {
  341 + // 店助主任提成规则(分段式):
  342 + // 1. 前提条件:门店业绩必须达到门店生命线的70%,否则无提成
  343 + // 2. 70% ≤ 业绩 < 100%:整个业绩按0.4%
  344 + // 3. 业绩 ≥ 100%:分段式
  345 + // - 0-100%部分(整个生命线):0.4%
  346 + // - 100%以上部分:1.6%(与店助的0.6%不同)
  347 + totalCommission = CalculateDirectorCommission(salary.StoreTotalPerformance, salary.StoreLifeline);
  348 + }
  349 + else
  350 + {
  351 + // 店助提成规则(分段式):
  352 + // 1. 前提条件:门店业绩必须达到门店生命线的70%,否则无提成
  353 + // 2. 70% ≤ 业绩 < 100%:整个业绩按0.4%
  354 + // 3. 业绩 ≥ 100%:分段式
  355 + // - 0-100%部分(整个生命线):0.4%
  356 + // - 100%以上部分:0.6%
  357 + totalCommission = CalculateAssistantCommission(salary.StoreTotalPerformance, salary.StoreLifeline);
  358 + }
340 359  
341 360 // 计算平均提成比例(用于显示)
342 361 if (salary.StoreTotalPerformance > 0)
... ... @@ -436,8 +455,16 @@ namespace NCC.Extend
436 455 // 阶段奖励 = 门店总奖励 / 当月天数 × 在店天数
437 456 if (daysInMonth > 0 && workingDays > 0)
438 457 {
439   - // 先计算门店总提成(阶梯计算)- 店助和店助主任使用相同的规则
440   - decimal storeTotalCommission = CalculateAssistantCommission(salary.StoreTotalPerformance, salary.StoreLifeline);
  458 + // 先计算门店总提成(阶梯计算)- 根据岗位类型使用不同规则
  459 + decimal storeTotalCommission;
  460 + if (isDirector)
  461 + {
  462 + storeTotalCommission = CalculateDirectorCommission(salary.StoreTotalPerformance, salary.StoreLifeline);
  463 + }
  464 + else
  465 + {
  466 + storeTotalCommission = CalculateAssistantCommission(salary.StoreTotalPerformance, salary.StoreLifeline);
  467 + }
441 468  
442 469 // 按比例计算提成:门店总提成 / 当月天数 × 在店天数
443 470 salary.CommissionAmount = storeTotalCommission / daysInMonth * workingDays;
... ... @@ -573,27 +600,24 @@ namespace NCC.Extend
573 600 }
574 601  
575 602 /// <summary>
576   - /// 计算底薪(店助)
577   - /// </summary>
578   - /// <param name="storeCategory">门店分类(1=A类,2=B类,3=C类)</param>
579   - /// <returns>底薪金额</returns>
580   - private decimal CalculateBaseSalary(int storeCategory)
581   - {
582   - return storeCategory switch
583   - {
584   - 1 => 3000m, // A类门店
585   - 2 => 3100m, // B类门店
586   - 3 => 3200m, // C类门店
587   - _ => throw new Exception($"门店分类值无效:{storeCategory},有效值为1(A类)、2(B类)、3(C类)")
588   - };
589   - }
590   -
591   - /// <summary>
592   - /// 计算店助主任提成(阶梯提成模式)
  603 + /// 计算店助主任提成(分段提成模式)
593 604 /// </summary>
594 605 /// <param name="storePerformance">门店业绩</param>
595 606 /// <param name="storeLifeline">门店生命线</param>
596 607 /// <returns>提成金额</returns>
  608 + /// <remarks>
  609 + /// 店助主任提成规则(分段式,与店助相同,但100%以上部分比例不同):
  610 + /// 1. 前提条件:门店业绩必须达到门店生命线的70%,否则无提成
  611 + /// 2. 70% ≤ 业绩 < 100%:整个业绩按0.4%计算
  612 + /// 3. 业绩 ≥ 100%:分段式提成
  613 + /// - 0-100%部分(整个生命线):按0.4%计算
  614 + /// - 100%以上部分:按1.6%计算(与店助的0.6%不同)
  615 + ///
  616 + /// 计算公式:
  617 + /// - 如果业绩 < 70%:提成 = 0
  618 + /// - 如果 70% ≤ 业绩 < 100%:提成 = 业绩 × 0.4%
  619 + /// - 如果业绩 ≥ 100%:提成 = 生命线 × 0.4% + (业绩 - 生命线) × 1.6%
  620 + /// </remarks>
597 621 private decimal CalculateDirectorCommission(decimal storePerformance, decimal storeLifeline)
598 622 {
599 623 if (storeLifeline <= 0)
... ... @@ -603,37 +627,46 @@ namespace NCC.Extend
603 627  
604 628 decimal ratio = storePerformance / storeLifeline;
605 629  
  630 + // 前提条件:必须达到70%才有提成
606 631 if (ratio < 0.7m)
607 632 {
608   - // 门店业绩 < 门店生命线 × 70% → 0%
  633 + // 门店业绩 < 门店生命线 × 70% → 0%(无提成)
609 634 return 0;
610 635 }
611 636 else if (ratio < 1.0m)
612 637 {
613   - // 门店生命线 × 70% ≤ 门店业绩 < 门店生命线 × 100% → 0.4%(阶梯)
614   - // 70%以下部分:0%
615   - // 70%-100%部分:0.4%
616   - decimal stage70 = storeLifeline * 0.7m;
617   - decimal performanceInRange = storePerformance - stage70;
618   - return performanceInRange * 0.004m;
  638 + // 门店生命线 × 70% ≤ 门店业绩 < 门店生命线 × 100% → 整个业绩按0.4%计算
  639 + return storePerformance * 0.004m;
619 640 }
620 641 else
621 642 {
622   - // 门店业绩 ≥ 门店生命线 × 100% → 阶梯提成
623   - // ≤生命线部分(整个生命线):0.6%
624   - // >生命线部分:1%
625   - decimal stage100 = storeLifeline;
626   - decimal performanceUpToLifeline = stage100; // ≤生命线部分(整个生命线)
627   - decimal performanceAbove100 = storePerformance - stage100; // >生命线部分
628   -
629   - // ≤生命线部分:0.6%
630   - decimal commissionUpToLifeline = performanceUpToLifeline * 0.006m;
631   - // >生命线部分:1%
632   - decimal commissionAbove100 = performanceAbove100 * 0.01m; // 1%
633   -
634   - return commissionUpToLifeline + commissionAbove100;
  643 + // 门店业绩 ≥ 门店生命线 × 100% → 分段式提成
  644 + // 0-100%部分(整个生命线):按0.4%计算
  645 + // 100%以上部分:按1.6%计算(店助主任与店助的区别)
  646 + decimal commissionBelow100 = storeLifeline * 0.004m; // 0-100%部分(整个生命线)按0.4%
  647 + decimal commissionAbove100 = (storePerformance - storeLifeline) * 0.016m; // 100%以上部分按1.6%
  648 +
  649 + return commissionBelow100 + commissionAbove100;
635 650 }
636 651 }
  652 +
  653 + /// <summary>
  654 + /// 计算底薪(店助)
  655 + /// </summary>
  656 + /// <param name="storeCategory">门店分类(1=A类,2=B类,3=C类)</param>
  657 + /// <returns>底薪金额</returns>
  658 + private decimal CalculateBaseSalary(int storeCategory)
  659 + {
  660 + return storeCategory switch
  661 + {
  662 + 1 => 3000m, // A类门店
  663 + 2 => 3100m, // B类门店
  664 + 3 => 3200m, // C类门店
  665 + _ => throw new Exception($"门店分类值无效:{storeCategory},有效值为1(A类)、2(B类)、3(C类)")
  666 + };
  667 + }
  668 +
637 669 }
638 670 }
639 671  
  672 +
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqBusinessUnitManagerSalaryService.cs
1 1 using Microsoft.AspNetCore.Authorization;
2 2 using Microsoft.AspNetCore.Mvc;
  3 +using NCC.Common.Enum;
3 4 using NCC.Common.Filter;
4 5 using NCC.Common.Helper;
5 6 using NCC.Dependency;
6 7 using NCC.DynamicApiController;
7 8 using NCC.Extend.Entitys.Dto.LqBusinessUnitManagerSalary;
  9 +using NCC.Extend.Entitys.Enum;
8 10 using NCC.Extend.Entitys.lq_attendance_summary;
  11 +using NCC.Extend.Entitys.lq_cooperation_cost;
9 12 using NCC.Extend.Entitys.lq_hytk_hytk;
10 13 using NCC.Extend.Entitys.lq_kd_kdjlb;
  14 +using NCC.Extend.Entitys.lq_laundry_flow;
11 15 using NCC.Extend.Entitys.lq_md_general_manager_lifeline;
12 16 using NCC.Extend.Entitys.lq_md_target;
13 17 using NCC.Extend.Entitys.lq_mdxx;
14 18 using NCC.Extend.Entitys.lq_business_unit_manager_salary_statistics;
  19 +using NCC.Extend.Entitys.lq_store_expense;
15 20 using NCC.System.Entitys.Permission;
16 21 using SqlSugar;
17 22 using System;
... ... @@ -84,6 +89,12 @@ namespace NCC.Extend
84 89 ManagerType = x.ManagerType,
85 90 IsTerminated = x.IsTerminated,
86 91 StorePerformanceDetail = x.StorePerformanceDetail,
  92 + SalesPerformance = x.SalesPerformance,
  93 + ProductMaterial = x.ProductMaterial,
  94 + CooperationCost = x.CooperationCost,
  95 + StoreExpense = x.StoreExpense,
  96 + LaundryCost = x.LaundryCost,
  97 + GrossProfit = x.GrossProfit,
87 98 BaseSalary = x.BaseSalary,
88 99 TotalCommission = x.TotalCommission,
89 100 WorkingDays = x.WorkingDays,
... ... @@ -172,7 +183,7 @@ namespace NCC.Extend
172 183 .Where(x => !string.IsNullOrEmpty(x.StoreId))
173 184 .ToDictionary(x => x.StoreId, x => x.StoreLifeline);
174 185  
175   - // 1.6 门店业绩计算 (开单实付 - 退卡金额)
  186 + // 1.6 门店销售业绩计算 (开单实付 - 退卡金额)
176 187 // 开单实付(从lq_kd_kdjlb表统计sfyj字段)
177 188 var storeBillingList = await _db.Queryable<LqKdKdjlbEntity>()
178 189 .Where(x => x.Kdrq >= startDate && x.Kdrq <= endDate.AddDays(1) && x.IsEffective == 1)
... ... @@ -193,13 +204,78 @@ namespace NCC.Extend
193 204 .GroupBy(x => x.Md)
194 205 .ToDictionary(g => g.Key, g => g.Sum(x => x.ActualRefundAmount ?? x.Tkje ?? 0));
195 206  
196   - // 1.7 考勤数据 (lq_attendance_summary)
  207 + // 1.7 产品物料统计(仓库领用金额,注意11月特殊规则)
  208 + var queryMonth = monthStr;
  209 + if (month == 11)
  210 + {
  211 + // 11月工资算10月数据
  212 + queryMonth = $"{year}10";
  213 + }
  214 + var productMaterialSql = $@"
  215 + SELECT
  216 + F_StoreId as StoreId,
  217 + COALESCE(SUM(F_TotalAmount), 0) as MaterialAmount
  218 + FROM lq_inventory_usage
  219 + WHERE F_IsEffective = 1
  220 + AND DATE_FORMAT(F_UsageTime, '%Y%m') = @queryMonth
  221 + GROUP BY F_StoreId";
  222 +
  223 + var productMaterialData = await _db.Ado.SqlQueryAsync<dynamic>(productMaterialSql, new { queryMonth });
  224 + var productMaterialDict = productMaterialData
  225 + .Where(x => x.StoreId != null)
  226 + .ToDictionary(x => x.StoreId.ToString(), x => Convert.ToDecimal(x.MaterialAmount ?? 0));
  227 +
  228 + // 1.8 合作项目成本统计
  229 + // Month字段格式为"11"(月份数字),不是"202511"(YYYYMM格式)
  230 + var cooperationCostMonth = $"{month:D2}"; // 格式化为"11"
  231 + var cooperationCostList = await _db.Queryable<LqCooperationCostEntity>()
  232 + .Where(x => x.Year == year && x.Month == cooperationCostMonth && x.IsEffective == StatusEnum.有效.GetHashCode())
  233 + .Select(x => new { x.StoreId, x.TotalAmount })
  234 + .ToListAsync();
  235 + var cooperationCostDict = cooperationCostList
  236 + .Where(x => !string.IsNullOrEmpty(x.StoreId))
  237 + .GroupBy(x => x.StoreId)
  238 + .ToDictionary(g => g.Key, g => g.Sum(x => x.TotalAmount));
  239 +
  240 + // 1.9 店内支出统计
  241 + var storeExpenseSql = $@"
  242 + SELECT
  243 + F_StoreId as StoreId,
  244 + COALESCE(SUM(F_Amount), 0) as ExpenseAmount
  245 + FROM lq_store_expense
  246 + WHERE F_IsEffective = 1
  247 + AND DATE_FORMAT(F_ExpenseDate, '%Y%m') = @monthStr
  248 + GROUP BY F_StoreId";
  249 +
  250 + var storeExpenseData = await _db.Ado.SqlQueryAsync<dynamic>(storeExpenseSql, new { monthStr });
  251 + var storeExpenseDict = storeExpenseData
  252 + .Where(x => x.StoreId != null)
  253 + .ToDictionary(x => x.StoreId.ToString(), x => Convert.ToDecimal(x.ExpenseAmount ?? 0));
  254 +
  255 + // 1.10 洗毛巾费用统计(只统计送出的记录,F_FlowType = 0)
  256 + // 优先使用送出时间(F_SendTime),如果为空则使用创建时间(F_CreateTime)
  257 + var laundryCostSql = $@"
  258 + SELECT
  259 + F_StoreId as StoreId,
  260 + COALESCE(SUM(F_TotalPrice), 0) as LaundryAmount
  261 + FROM lq_laundry_flow
  262 + WHERE F_IsEffective = 1
  263 + AND F_FlowType = 0
  264 + AND DATE_FORMAT(COALESCE(F_SendTime, F_CreateTime), '%Y%m') = @monthStr
  265 + GROUP BY F_StoreId";
  266 +
  267 + var laundryCostData = await _db.Ado.SqlQueryAsync<dynamic>(laundryCostSql, new { monthStr });
  268 + var laundryCostDict = laundryCostData
  269 + .Where(x => x.StoreId != null)
  270 + .ToDictionary(x => x.StoreId.ToString(), x => Convert.ToDecimal(x.LaundryAmount ?? 0));
  271 +
  272 + // 1.11 考勤数据 (lq_attendance_summary)
197 273 var attendanceList = await _db.Queryable<LqAttendanceSummaryEntity>()
198 274 .Where(x => x.Year == year && x.Month == month && x.IsEffective == 1)
199 275 .ToListAsync();
200 276 var attendanceDict = attendanceList.ToDictionary(x => x.UserId, x => x);
201 277  
202   - // 1.8 获取员工信息 (BASE_USER)
  278 + // 1.12 获取员工信息 (BASE_USER)
203 279 var userList = await _db.Queryable<UserEntity>()
204 280 .Where(x => allManagerIds.Contains(x.Id))
205 281 .Select(x => new { x.Id, x.RealName, x.Account, x.IsOnJob })
... ... @@ -257,6 +333,12 @@ namespace NCC.Extend
257 333 // 2.5 遍历该总经理/经理管理的每个门店,计算提成
258 334 var storePerformanceDetails = new List<StorePerformanceDetail>();
259 335 decimal totalCommission = 0m;
  336 + decimal totalSalesPerformance = 0m;
  337 + decimal totalProductMaterial = 0m;
  338 + decimal totalCooperationCost = 0m;
  339 + decimal totalStoreExpense = 0m;
  340 + decimal totalLaundryCost = 0m;
  341 + decimal totalGrossProfit = 0m;
260 342  
261 343 // 获取该总经理/经理管理的门店列表(如果没有管理的门店,则为空列表)
262 344 var managedStores = managerStoreDict.ContainsKey(managerId) ? managerStoreDict[managerId] : new List<string>();
... ... @@ -277,69 +359,38 @@ namespace NCC.Extend
277 359 // 获取门店信息
278 360 var storeName = storeDict.ContainsKey(storeId) ? storeDict[storeId].Dm ?? "" : "";
279 361  
280   - // 获取门店生命线(提成门槛)
281   - if (!storeLifelineDict.ContainsKey(storeId))
282   - {
283   - // 门店生命线未设置,跳过该门店
284   - storePerformanceDetails.Add(new StorePerformanceDetail
285   - {
286   - StoreId = storeId,
287   - StoreName = storeName,
288   - StoreLifeline = 0,
289   - BillingPerformance = 0,
290   - RefundPerformance = 0,
291   - StorePerformance = 0,
292   - ReachedLifeline = false,
293   - CommissionAmount = 0,
294   - CalculationDetail = "门店生命线未设置,无法计算提成"
295   - });
296   - continue;
297   - }
298   -
299   - var storeLifeline = storeLifelineDict[storeId];
300   - if (storeLifeline <= 0)
301   - {
302   - // 门店生命线未设置或为0,跳过该门店
303   - storePerformanceDetails.Add(new StorePerformanceDetail
304   - {
305   - StoreId = storeId,
306   - StoreName = storeName,
307   - StoreLifeline = 0,
308   - BillingPerformance = 0,
309   - RefundPerformance = 0,
310   - StorePerformance = 0,
311   - ReachedLifeline = false,
312   - CommissionAmount = 0,
313   - CalculationDetail = "门店生命线未设置或为0,无法计算提成"
314   - });
315   - continue;
316   - }
  362 + // 获取门店生命线(仅用于记录)
  363 + var storeLifeline = storeLifelineDict.ContainsKey(storeId) ? storeLifelineDict[storeId] : 0;
317 364  
318   - // 获取门店业绩
  365 + // 计算销售业绩(开单业绩-退款业绩)
319 366 var billing = storeBillingDict.ContainsKey(storeId) ? storeBillingDict[storeId] : 0;
320 367 var refund = storeRefundDict.ContainsKey(storeId) ? storeRefundDict[storeId] : 0;
321   - var storePerformance = billing - refund;
  368 + var salesPerformance = billing - refund;
322 369  
323   - // 判断是否达到门店生命线
324   - var reachedLifeline = storePerformance >= storeLifeline;
  370 + // 统计各项成本
  371 + var productMaterial = productMaterialDict.ContainsKey(storeId) ? productMaterialDict[storeId] : 0;
  372 + var cooperationCost = cooperationCostDict.ContainsKey(storeId) ? cooperationCostDict[storeId] : 0;
  373 + var storeExpense = storeExpenseDict.ContainsKey(storeId) ? storeExpenseDict[storeId] : 0;
  374 + var laundryCost = laundryCostDict.ContainsKey(storeId) ? laundryCostDict[storeId] : 0;
325 375  
326   - // 计算提成
327   - decimal commissionAmount = 0m;
328   - string calculationDetail = "";
  376 + // 计算毛利
  377 + // 毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾
  378 + var grossProfit = salesPerformance - productMaterial - cooperationCost - storeExpense - laundryCost;
329 379  
330   - if (reachedLifeline)
331   - {
332   - // 达到门店生命线,使用提成阶梯计算提成(分段累进)
333   - var commissionResult = CalculateStoreCommission(storePerformance, storeLifelineSetting);
334   - commissionAmount = commissionResult.Amount;
335   - calculationDetail = commissionResult.Detail;
  380 + // 累加各项数据
  381 + totalSalesPerformance += salesPerformance;
  382 + totalProductMaterial += productMaterial;
  383 + totalCooperationCost += cooperationCost;
  384 + totalStoreExpense += storeExpense;
  385 + totalLaundryCost += laundryCost;
  386 + totalGrossProfit += grossProfit;
336 387  
337   - totalCommission += commissionAmount;
338   - }
339   - else
340   - {
341   - calculationDetail = $"业绩{storePerformance:N2}元,未达到门店生命线{storeLifeline:N2}元,无提成";
342   - }
  388 + // 计算提成(必须满足提成阶梯1才能有提成资格,使用毛利计算)
  389 + var commissionResult = CalculateStoreCommission(grossProfit, storeLifelineSetting);
  390 + var commissionAmount = commissionResult.Amount;
  391 + var calculationDetail = commissionResult.Detail;
  392 +
  393 + totalCommission += commissionAmount;
343 394  
344 395 // 添加到门店业绩明细
345 396 storePerformanceDetails.Add(new StorePerformanceDetail
... ... @@ -349,8 +400,14 @@ namespace NCC.Extend
349 400 StoreLifeline = storeLifeline,
350 401 BillingPerformance = billing,
351 402 RefundPerformance = refund,
352   - StorePerformance = storePerformance,
353   - ReachedLifeline = reachedLifeline,
  403 + SalesPerformance = salesPerformance,
  404 + ProductMaterial = productMaterial,
  405 + CooperationCost = cooperationCost,
  406 + StoreExpense = storeExpense,
  407 + LaundryCost = laundryCost,
  408 + GrossProfit = grossProfit,
  409 + StorePerformance = grossProfit, // 用于提成计算的业绩是毛利
  410 + ReachedLifeline1 = grossProfit >= storeLifelineSetting.Lifeline1, // 是否达到提成阶梯1
354 411 Lifeline1 = storeLifelineSetting.Lifeline1,
355 412 CommissionRate1 = storeLifelineSetting.CommissionRate1,
356 413 Lifeline2 = storeLifelineSetting.Lifeline2,
... ... @@ -365,10 +422,18 @@ namespace NCC.Extend
365 422 // 2.6 保存门店业绩明细(JSON格式)
366 423 salary.StorePerformanceDetail = storePerformanceDetails.ToJson();
367 424  
368   - // 2.7 提成合计
  425 + // 2.7 保存毛利相关数据
  426 + salary.SalesPerformance = totalSalesPerformance;
  427 + salary.ProductMaterial = totalProductMaterial;
  428 + salary.CooperationCost = totalCooperationCost;
  429 + salary.StoreExpense = totalStoreExpense;
  430 + salary.LaundryCost = totalLaundryCost;
  431 + salary.GrossProfit = totalGrossProfit;
  432 +
  433 + // 2.8 提成合计
369 434 salary.TotalCommission = totalCommission;
370 435  
371   - // 2.8 计算应发工资
  436 + // 2.9 计算应发工资
372 437 salary.CalculatedGrossSalary = salary.BaseSalary + salary.TotalCommission;
373 438 salary.FinalGrossSalary = salary.CalculatedGrossSalary;
374 439  
... ... @@ -413,12 +478,25 @@ namespace NCC.Extend
413 478 }
414 479  
415 480 /// <summary>
416   - /// 计算门店提成(分段累进
  481 + /// 计算门店提成(分段累进
417 482 /// </summary>
418   - /// <param name="storePerformance">门店业绩</param>
  483 + /// <param name="grossProfit">门店毛利</param>
419 484 /// <param name="lifelineSetting">提成阶梯设置</param>
420 485 /// <returns>提成金额和计算说明</returns>
421   - private (decimal Amount, string Detail) CalculateStoreCommission(decimal storePerformance, LqMdGeneralManagerLifelineEntity lifelineSetting)
  486 + /// <remarks>
  487 + /// 提成规则(分段累进式):
  488 + /// 1. 前提条件:必须满足提成阶梯1才能有提成资格
  489 + /// 2. 提成基数:使用毛利计算,而不是开单业绩
  490 + /// 3. 分段累进式计算:不同区间按不同比例分别计算后累加
  491 + /// - 毛利 < 提成阶梯1:无提成
  492 + /// - 提成阶梯1 ≤ 毛利 < 提成阶梯2:提成阶梯1 × 提成比例1 + (毛利 - 提成阶梯1) × 提成比例2
  493 + /// - 提成阶梯2 ≤ 毛利 < 提成阶梯3:提成阶梯1 × 提成比例1 + (提成阶梯2 - 提成阶梯1) × 提成比例2 + (毛利 - 提成阶梯2) × 提成比例3
  494 + /// - 毛利 ≥ 提成阶梯3:提成阶梯1 × 提成比例1 + (提成阶梯2 - 提成阶梯1) × 提成比例2 + (提成阶梯3 - 提成阶梯2) × 提成比例3 + (毛利 - 提成阶梯3) × 提成比例3
  495 + ///
  496 + /// 示例:毛利 = 100,000元,提成阶梯1 = 150,000元,提成比例1 = 1%
  497 + /// 计算:100,000 < 150,000,未达到提成阶梯1,提成 = 0元
  498 + /// </remarks>
  499 + private (decimal Amount, string Detail) CalculateStoreCommission(decimal grossProfit, LqMdGeneralManagerLifelineEntity lifelineSetting)
422 500 {
423 501 // 验证提成阶梯1和提成比例1必须设置
424 502 if (lifelineSetting.Lifeline1 <= 0 || lifelineSetting.CommissionRate1 <= 0)
... ... @@ -426,6 +504,12 @@ namespace NCC.Extend
426 504 return (0m, "提成阶梯1或提成比例1未设置,无法计算提成");
427 505 }
428 506  
  507 + // 必须满足提成阶梯1才能有提成资格
  508 + if (grossProfit < lifelineSetting.Lifeline1)
  509 + {
  510 + return (0m, $"毛利{grossProfit:N2}元,< 提成阶梯1({lifelineSetting.Lifeline1:N2}元),未达到提成资格,提成 = 0元");
  511 + }
  512 +
429 513 decimal commissionAmount = 0m;
430 514 string detail = "";
431 515  
... ... @@ -436,53 +520,49 @@ namespace NCC.Extend
436 520 var lifeline3 = lifelineSetting.Lifeline3 ?? 0;
437 521 var rate3 = lifelineSetting.CommissionRate3 ?? 0;
438 522  
439   - // 分段累进计算
440   - if (storePerformance <= lifeline1)
441   - {
442   - // 业绩 ≤ 提成阶梯1
443   - commissionAmount = storePerformance * (rate1 / 100m);
444   - detail = $"业绩{storePerformance:N2}元,≤ 提成阶梯1({lifeline1:N2}元),提成 = {storePerformance:N2} × {rate1}% = {commissionAmount:N2}元";
445   - }
446   - else if (lifeline2 > 0 && storePerformance <= lifeline2)
  523 + // 分段累进式计算(已通过提成阶梯1检查)
  524 + if (lifeline2 > 0 && grossProfit < lifeline2)
447 525 {
448   - // 提成阶梯1 < 业绩 ≤ 提成阶梯2
  526 + // 提成阶梯1 ≤ 毛利 < 提成阶梯2:分段累进计算
449 527 var part1 = lifeline1 * (rate1 / 100m);
450   - var part2 = (storePerformance - lifeline1) * (rate2 / 100m);
  528 + var part2 = (grossProfit - lifeline1) * (rate2 / 100m);
451 529 commissionAmount = part1 + part2;
452   - detail = $"业绩{storePerformance:N2}元,> 提成阶梯1({lifeline1:N2}元) 且 ≤ 提成阶梯2({lifeline2:N2}元),提成 = {lifeline1:N2} × {rate1}% + ({storePerformance:N2} - {lifeline1:N2}) × {rate2}% = {part1:N2} + {part2:N2} = {commissionAmount:N2}元";
  530 + detail = $"毛利{grossProfit:N2}元,≥ 提成阶梯1({lifeline1:N2}元) 且 < 提成阶梯2({lifeline2:N2}元),提成 = {lifeline1:N2} × {rate1}% + ({grossProfit:N2} - {lifeline1:N2}) × {rate2}% = {part1:N2} + {part2:N2} = {commissionAmount:N2}元";
453 531 }
454   - else if (lifeline3 > 0 && storePerformance <= lifeline3)
  532 + else if (lifeline3 > 0 && grossProfit < lifeline3)
455 533 {
456   - // 提成阶梯2 < 业绩 ≤ 提成阶梯3
  534 + // 提成阶梯2 ≤ 毛利 < 提成阶梯3:分段累进计算
457 535 var part1 = lifeline1 * (rate1 / 100m);
458 536 var part2 = (lifeline2 - lifeline1) * (rate2 / 100m);
459   - var part3 = (storePerformance - lifeline2) * (rate3 / 100m);
  537 + var part3 = (grossProfit - lifeline2) * (rate3 / 100m);
460 538 commissionAmount = part1 + part2 + part3;
461   - 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}元";
  539 + 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}元";
462 540 }
463 541 else if (lifeline3 > 0)
464 542 {
465   - // 业绩 > 提成阶梯3
  543 + // 毛利 ≥ 提成阶梯3:分段累进计算
466 544 var part1 = lifeline1 * (rate1 / 100m);
467 545 var part2 = (lifeline2 - lifeline1) * (rate2 / 100m);
468 546 var part3 = (lifeline3 - lifeline2) * (rate3 / 100m);
469   - var part4 = (storePerformance - lifeline3) * (rate3 / 100m);
  547 + var part4 = (grossProfit - lifeline3) * (rate3 / 100m);
470 548 commissionAmount = part1 + part2 + part3 + part4;
471   - 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}元";
  549 + 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}元";
472 550 }
473 551 else if (lifeline2 > 0)
474 552 {
475   - // 提成阶梯3未设置,业绩 > 提成阶梯2,按提成比例2计算超出部分
  553 + // 提成阶梯3未设置,毛利 ≥ 提成阶梯2:分段累进计算
476 554 var part1 = lifeline1 * (rate1 / 100m);
477   - var part2 = (storePerformance - lifeline1) * (rate2 / 100m);
  555 + var part2 = (grossProfit - lifeline1) * (rate2 / 100m);
478 556 commissionAmount = part1 + part2;
479   - detail = $"业绩{storePerformance:N2}元,> 提成阶梯2({lifeline2:N2}元),提成阶梯3未设置,提成 = {lifeline1:N2} × {rate1}% + ({storePerformance:N2} - {lifeline1:N2}) × {rate2}% = {part1:N2} + {part2:N2} = {commissionAmount:N2}元";
  557 + detail = $"毛利{grossProfit:N2}元,≥ 提成阶梯2({lifeline2:N2}元),提成阶梯3未设置,提成 = {lifeline1:N2} × {rate1}% + ({grossProfit:N2} - {lifeline1:N2}) × {rate2}% = {part1:N2} + {part2:N2} = {commissionAmount:N2}元";
480 558 }
481 559 else
482 560 {
483   - // 只有提成阶梯1,业绩 > 提成阶梯1,按提成比例1计算
484   - commissionAmount = storePerformance * (rate1 / 100m);
485   - detail = $"业绩{storePerformance:N2}元,> 提成阶梯1({lifeline1:N2}元),提成阶梯2未设置,提成 = {storePerformance:N2} × {rate1}% = {commissionAmount:N2}元";
  561 + // 只有提成阶梯1,毛利 ≥ 提成阶梯1:分段累进计算(提成阶梯1部分 × 提成比例1 + 超出部分 × 提成比例1)
  562 + var part1 = lifeline1 * (rate1 / 100m);
  563 + var part2 = (grossProfit - lifeline1) * (rate1 / 100m);
  564 + commissionAmount = part1 + part2;
  565 + detail = $"毛利{grossProfit:N2}元,≥ 提成阶梯1({lifeline1:N2}元),提成阶梯2未设置,提成 = {lifeline1:N2} × {rate1}% + ({grossProfit:N2} - {lifeline1:N2}) × {rate1}% = {part1:N2} + {part2:N2} = {commissionAmount:N2}元";
486 566 }
487 567  
488 568 return (commissionAmount, detail);
... ... @@ -498,8 +578,14 @@ namespace NCC.Extend
498 578 public decimal StoreLifeline { get; set; }
499 579 public decimal BillingPerformance { get; set; }
500 580 public decimal RefundPerformance { get; set; }
501   - public decimal StorePerformance { get; set; }
502   - public bool ReachedLifeline { get; set; }
  581 + public decimal SalesPerformance { get; set; }
  582 + public decimal ProductMaterial { get; set; }
  583 + public decimal CooperationCost { get; set; }
  584 + public decimal StoreExpense { get; set; }
  585 + public decimal LaundryCost { get; set; }
  586 + public decimal GrossProfit { get; set; }
  587 + public decimal StorePerformance { get; set; } // 用于提成计算的业绩(等于毛利)
  588 + public bool ReachedLifeline1 { get; set; } // 是否达到提成阶梯1
503 589 public decimal Lifeline1 { get; set; }
504 590 public decimal CommissionRate1 { get; set; }
505 591 public decimal? Lifeline2 { get; set; }
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs
... ... @@ -3328,6 +3328,17 @@ namespace NCC.Extend.LqKdKdjlb
3328 3328 var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
3329 3329 var endTime = input.EndTime ?? DateTime.Now;
3330 3330  
  3331 + // 确保endTime包含当天的结束时间
  3332 + if (input.EndTime.HasValue)
  3333 + {
  3334 + endTime = input.EndTime.Value.Date.AddHours(23).AddMinutes(59).AddSeconds(59);
  3335 + }
  3336 +
  3337 + // 计算月份(用于匹配金三角设定,使用startTime的月份)
  3338 + var statisticsMonth = startTime.ToString("yyyyMM");
  3339 +
  3340 + _logger.LogInformation($"健康师统计查询 - StartTime: {startTime:yyyy-MM-dd HH:mm:ss}, EndTime: {endTime:yyyy-MM-dd HH:mm:ss}, Month: {statisticsMonth}");
  3341 +
3331 3342 // 构建SQL查询
3332 3343 var sql = $@"
3333 3344 SELECT
... ... @@ -3365,37 +3376,97 @@ namespace NCC.Extend.LqKdKdjlb
3365 3376 -- 手工费相关统计
3366 3377 COALESCE(consume_stats.LaborCost, 0) as LaborCost,
3367 3378 COALESCE(consume_stats.OriginalLaborCost, 0) as OriginalLaborCost,
3368   - COALESCE(consume_stats.OvertimeLaborCost, 0) as OvertimeLaborCost
  3379 + COALESCE(consume_stats.OvertimeLaborCost, 0) as OvertimeLaborCost,
  3380 +
  3381 + -- 金三角名称
  3382 + COALESCE(jsj_info.GoldTriangleName, '') as GoldTriangleName,
  3383 +
  3384 + -- 队伍业绩占比(该健康师所在金三角的业绩占门店总业绩的比例)
  3385 + CASE
  3386 + WHEN store_total_stats.StoreTotalAmount > 0 THEN
  3387 + CAST(COALESCE(team_performance_stats.TeamPerformance, 0) * 100.0 / store_total_stats.StoreTotalAmount AS DECIMAL(18,2))
  3388 + ELSE 0
  3389 + END as TeamPerformanceRatio
3369 3390  
3370 3391 FROM BASE_USER u
3371 3392 LEFT JOIN lq_mdxx md ON u.F_MDID = md.F_Id
3372 3393 LEFT JOIN base_organize dept ON md.syb = dept.F_Id
3373   -
3374   - -- 邀约统计子查询
  3394 +
  3395 + -- 金三角信息子查询(获取健康师所在的金三角名称)
  3396 + LEFT JOIN (
  3397 + SELECT
  3398 + jsjUser.user_id as EmployeeId,
  3399 + jsj.jsj as GoldTriangleName
  3400 + FROM lq_jinsanjiao_user jsjUser
  3401 + INNER JOIN lq_ycsd_jsj jsj ON jsjUser.jsj_id COLLATE utf8mb4_general_ci = jsj.F_Id COLLATE utf8mb4_general_ci
  3402 + WHERE jsjUser.status = 'ACTIVE'
  3403 + AND jsjUser.F_DeleteMark = 0
  3404 + AND jsj.yf = @statisticsMonth
  3405 + GROUP BY jsjUser.user_id, jsj.jsj
  3406 + ) jsj_info ON u.F_Id = jsj_info.EmployeeId
  3407 +
  3408 + -- 门店总业绩统计(用于计算占比)
  3409 + LEFT JOIN (
  3410 + SELECT
  3411 + kd.Djmd as StoreId,
  3412 + SUM(CAST(kd.sfyj AS DECIMAL(18,2))) as StoreTotalAmount
  3413 + FROM lq_kd_kdjlb kd
  3414 + WHERE kd.F_IsEffective = 1
  3415 + AND kd.kdrq >= @startTime
  3416 + AND kd.kdrq <= @endTime
  3417 + GROUP BY kd.Djmd
  3418 + ) store_total_stats ON u.F_MDID = store_total_stats.StoreId
  3419 +
  3420 + -- 队伍业绩统计(该健康师所在金三角的所有成员业绩总和)
  3421 + LEFT JOIN (
  3422 + SELECT
  3423 + jsjUser.user_id as EmployeeId,
  3424 + SUM(CAST(jksyj.jksyj AS DECIMAL(18,2))) as TeamPerformance
  3425 + FROM lq_jinsanjiao_user jsjUser
  3426 + INNER JOIN lq_ycsd_jsj jsj ON jsjUser.jsj_id COLLATE utf8mb4_general_ci = jsj.F_Id COLLATE utf8mb4_general_ci
  3427 + INNER JOIN lq_jinsanjiao_user jsu_team ON jsu_team.jsj_id = jsj.F_Id
  3428 + AND jsu_team.status = 'ACTIVE'
  3429 + AND jsu_team.F_DeleteMark = 0
  3430 + INNER JOIN lq_kd_jksyj jksyj ON jksyj.jkszh = jsu_team.user_id
  3431 + INNER JOIN lq_kd_kdjlb kd ON jksyj.glkdbh = kd.F_Id
  3432 + WHERE jsjUser.status = 'ACTIVE'
  3433 + AND jsjUser.F_DeleteMark = 0
  3434 + AND jsj.yf = @statisticsMonth
  3435 + AND jksyj.F_IsEffective = 1
  3436 + AND kd.F_IsEffective = 1
  3437 + AND kd.Djmd = jsj.md
  3438 + AND jksyj.yjsj >= @startTime
  3439 + AND jksyj.yjsj <= @endTime
  3440 + AND kd.kdrq >= @startTime
  3441 + AND kd.kdrq <= @endTime
  3442 + GROUP BY jsjUser.user_id
  3443 + ) team_performance_stats ON u.F_Id = team_performance_stats.EmployeeId
  3444 +
  3445 + -- 邀约统计子查询(使用邀约时间yysj筛选,而不是创建时间)
3375 3446 LEFT JOIN (
3376 3447 SELECT
3377 3448 yyr as EmployeeId,
3378 3449 COUNT(DISTINCT yykh) as InviteCount
3379 3450 FROM lq_yaoyjl
3380 3451 WHERE yyr IS NOT NULL
3381   - AND F_CreateTime >= @startTime
3382   - AND F_CreateTime <= @endTime
  3452 + AND yysj >= @startTime
  3453 + AND yysj <= @endTime
3383 3454 GROUP BY yyr
3384 3455 ) invite_stats ON u.F_Id = invite_stats.EmployeeId
3385 3456  
3386   - -- 预约统计子查询
  3457 + -- 预约统计子查询(使用预约时间yysj筛选,而不是创建时间)
3387 3458 LEFT JOIN (
3388 3459 SELECT
3389 3460 yyr as EmployeeId,
3390 3461 COUNT(DISTINCT gk) as AppointmentCount
3391 3462 FROM lq_yyjl
3392 3463 WHERE yyr IS NOT NULL
3393   - AND F_CreateTime >= @startTime
3394   - AND F_CreateTime <= @endTime
  3464 + AND yysj >= @startTime
  3465 + AND yysj <= @endTime
3395 3466 GROUP BY yyr
3396 3467 ) appointment_stats ON u.F_Id = appointment_stats.EmployeeId
3397 3468  
3398   - -- 到店统计子查询
  3469 + -- 到店统计子查询(使用预约时间yysj筛选,而不是创建时间)
3399 3470 LEFT JOIN (
3400 3471 SELECT
3401 3472 yyr as EmployeeId,
... ... @@ -3403,8 +3474,8 @@ namespace NCC.Extend.LqKdKdjlb
3403 3474 FROM lq_yyjl
3404 3475 WHERE yyr IS NOT NULL
3405 3476 AND F_Status = '已确认'
3406   - AND F_CreateTime >= @startTime
3407   - AND F_CreateTime <= @endTime
  3477 + AND yysj >= @startTime
  3478 + AND yysj <= @endTime
3408 3479 GROUP BY yyr
3409 3480 ) visit_stats ON u.F_Id = visit_stats.EmployeeId
3410 3481  
... ... @@ -3554,7 +3625,8 @@ namespace NCC.Extend.LqKdKdjlb
3554 3625 var parameters = new List<SugarParameter>
3555 3626 {
3556 3627 new SugarParameter("@startTime", startTime),
3557   - new SugarParameter("@endTime", endTime)
  3628 + new SugarParameter("@endTime", endTime),
  3629 + new SugarParameter("@statisticsMonth", statisticsMonth)
3558 3630 };
3559 3631  
3560 3632 if (!string.IsNullOrEmpty(input.DepartmentId))
... ... @@ -3582,9 +3654,15 @@ namespace NCC.Extend.LqKdKdjlb
3582 3654  
3583 3655 sql += " ORDER BY u.F_REALNAME";
3584 3656  
  3657 + // 记录SQL和参数用于调试
  3658 + _logger.LogInformation($"健康师统计SQL执行 - 参数: startTime={startTime:yyyy-MM-dd HH:mm:ss}, endTime={endTime:yyyy-MM-dd HH:mm:ss}");
  3659 +
3585 3660 // 执行查询
3586 3661 var allData = await _db.Ado.SqlQueryAsync<HealthCoachStatisticsOutput>(sql, parameters);
3587 3662  
  3663 + // 记录查询结果数量
  3664 + _logger.LogInformation($"健康师统计查询结果 - 总记录数: {allData.Count}, 有到店人数的记录数: {allData.Count(x => x.visitCount > 0)}");
  3665 +
3588 3666 // 手动分页
3589 3667 var totalCount = allData.Count;
3590 3668 var pagedData = allData
... ... @@ -3891,12 +3969,32 @@ namespace NCC.Extend.LqKdKdjlb
3891 3969 /// "DeductType": "储值",
3892 3970 /// "BillingId": "开单ID",
3893 3971 /// "ItemName": "品项名称",
  3972 + /// "StoreId": "门店ID",
  3973 + /// "StoreIds": ["门店ID1", "门店ID2"],
  3974 + /// "StartTime": "2025-01-01T00:00:00",
  3975 + /// "EndTime": "2025-12-31T23:59:59",
  3976 + /// "ItemCategory": "科美",
3894 3977 /// "MinAmount": 100,
3895 3978 /// "MaxAmount": 1000
3896 3979 /// }
3897 3980 /// ```
3898 3981 ///
  3982 + /// 参数说明:
  3983 + /// - StoreId: 门店ID(单个门店筛选)
  3984 + /// - StoreIds: 门店ID列表(支持多门店筛选)
  3985 + /// - StartTime: 开始时间(开单时间筛选)
  3986 + /// - EndTime: 结束时间(开单时间筛选)
  3987 + /// - ItemCategory: 品项分类(科美、医美、生美、产品等)
  3988 + ///
3899 3989 /// 返回字段说明:
  3990 + /// - list: 分页数据列表
  3991 + /// - pagination: 分页信息
  3992 + /// - statistics: 统计信息(针对所有符合条件的数据)
  3993 + /// - totalCount: 总记录数
  3994 + /// - totalAmount: 总金额
  3995 + /// - totalProjectNumber: 总项目数
  3996 + ///
  3997 + /// 列表字段说明:
3900 3998 /// - Id: 储扣记录ID
3901 3999 /// - DeductType: 扣减类型
3902 4000 /// - DeductTypeName: 扣减类型名称
... ... @@ -3920,24 +4018,30 @@ namespace NCC.Extend.LqKdKdjlb
3920 4018 {
3921 4019 var sidx = string.IsNullOrEmpty(input.sidx) ? "CreateTime" : input.sidx;
3922 4020  
3923   - // 构建基础查询:储扣信息 JOIN 开单记录 JOIN 客户信息 JOIN 门店信息
  4021 + // 构建基础查询:储扣信息 LEFT JOIN 开单记录 LEFT JOIN 客户信息 LEFT JOIN 门店信息
3924 4022 var baseQuery = _db.Queryable<LqKdDeductinfoEntity, LqKdKdjlbEntity, LqKhxxEntity, LqMdxxEntity>(
3925   - (deduct, billing, member, store) =>
3926   - deduct.BillingId == billing.Id &&
3927   - billing.Kdhy == member.Id &&
3928   - billing.Djmd == store.Id)
  4023 + (deduct, billing, member, store) => new JoinQueryInfos(
  4024 + JoinType.Left, deduct.BillingId == billing.Id,
  4025 + JoinType.Left, billing.Kdhy == member.Id,
  4026 + JoinType.Left, billing.Djmd == store.Id
  4027 + ))
  4028 + .WhereIF(input.IsEffective.HasValue, (deduct, billing, member, store) => deduct.IsEffective == input.IsEffective.Value)
3929 4029 .WhereIF(!string.IsNullOrEmpty(input.DeductType), (deduct, billing, member, store) => deduct.DeductType == input.DeductType)
3930 4030 .WhereIF(!string.IsNullOrEmpty(input.DeductId), (deduct, billing, member, store) => deduct.DeductId == input.DeductId)
3931 4031 .WhereIF(!string.IsNullOrEmpty(input.BillingId), (deduct, billing, member, store) => deduct.BillingId == input.BillingId)
3932 4032 .WhereIF(input.MinAmount.HasValue, (deduct, billing, member, store) => deduct.Amount >= input.MinAmount.Value)
3933 4033 .WhereIF(input.MaxAmount.HasValue, (deduct, billing, member, store) => deduct.Amount <= input.MaxAmount.Value)
3934   - .WhereIF(input.IsEffective.HasValue, (deduct, billing, member, store) => deduct.IsEffective == input.IsEffective.Value)
3935 4034 .WhereIF(!string.IsNullOrEmpty(input.ItemName), (deduct, billing, member, store) => deduct.ItemName != null && deduct.ItemName.Contains(input.ItemName))
3936 4035 .WhereIF(!string.IsNullOrEmpty(input.ItemId), (deduct, billing, member, store) => deduct.ItemId == input.ItemId)
3937 4036 .WhereIF(input.MinUnitPrice.HasValue, (deduct, billing, member, store) => deduct.UnitPrice >= input.MinUnitPrice.Value)
3938 4037 .WhereIF(input.MaxUnitPrice.HasValue, (deduct, billing, member, store) => deduct.UnitPrice <= input.MaxUnitPrice.Value)
3939 4038 .WhereIF(input.StartCreateTime.HasValue, (deduct, billing, member, store) => deduct.CreateTime >= input.StartCreateTime.Value)
3940 4039 .WhereIF(input.EndCreateTime.HasValue, (deduct, billing, member, store) => deduct.CreateTime <= input.EndCreateTime.Value)
  4040 + .WhereIF(!string.IsNullOrEmpty(input.StoreId), (deduct, billing, member, store) => billing.Djmd == input.StoreId)
  4041 + .WhereIF(input.StoreIds != null && input.StoreIds.Any(), (deduct, billing, member, store) => input.StoreIds.Contains(billing.Djmd))
  4042 + .WhereIF(input.StartTime.HasValue, (deduct, billing, member, store) => (deduct.BillingTime ?? billing.Kdrq) >= input.StartTime.Value)
  4043 + .WhereIF(input.EndTime.HasValue, (deduct, billing, member, store) => (deduct.BillingTime ?? billing.Kdrq) <= input.EndTime.Value)
  4044 + .WhereIF(!string.IsNullOrEmpty(input.ItemCategory), (deduct, billing, member, store) => deduct.ItemCategory == input.ItemCategory)
3941 4045 .WhereIF(!string.IsNullOrEmpty(input.keyword), (deduct, billing, member, store) =>
3942 4046 (deduct.ItemName != null && deduct.ItemName.Contains(input.keyword)) ||
3943 4047 (member.Khmc != null && member.Khmc.Contains(input.keyword)) ||
... ... @@ -3961,20 +4065,104 @@ namespace NCC.Extend.LqKdKdjlb
3961 4065 CreateTime = deduct.CreateTime,
3962 4066 ProjectNumber = deduct.ProjectNumber,
3963 4067 ItemCategory = deduct.ItemCategory ?? "",
3964   - BillingDate = deduct.BillingTime ?? billing.Kdrq, // 优先使用储扣记录表中的开单时间
  4068 + BillingDate = deduct.BillingTime ?? billing.Kdrq,
3965 4069 MemberId = billing.Kdhy ?? "",
3966 4070 MemberName = member.Khmc ?? "",
3967 4071 MemberPhone = member.Sjh ?? "",
3968 4072 StoreId = billing.Djmd ?? "",
3969 4073 StoreName = store.Dm ?? "",
3970   - TimePeriod = deduct.BillingTime ?? billing.Kdrq, // 优先使用储扣记录表中的开单时间
  4074 + TimePeriod = deduct.BillingTime ?? billing.Kdrq,
3971 4075 BillingType = SqlFunc.Subqueryable<LqKdPxmxEntity>()
3972 4076 .Where(pxmx => pxmx.Id == deduct.DeductId && pxmx.Px == deduct.ItemId)
3973 4077 .Select(pxmx => pxmx.SourceType),
3974 4078 CooperationInstitution = billing.Hgjg ?? ""
3975 4079 }).MergeTable().OrderBy(sidx + " " + input.sort).ToPagedListAsync(input.currentPage, input.pageSize);
3976 4080  
3977   - return PageResult<LqKdDeductinfoListOutput>.SqlSugarPageResult(data);
  4081 + // 构建返回结果
  4082 + var result = PageResult<LqKdDeductinfoListOutput>.SqlSugarPageResult(data);
  4083 +
  4084 + // 单独查询统计数据,避免复杂JOIN导致的问题
  4085 + try
  4086 + {
  4087 + // 先获取符合条件的开单记录ID列表(用于门店、时间、关键词筛选)
  4088 + var billingIds = new List<string>();
  4089 + if (!string.IsNullOrEmpty(input.StoreId) || (input.StoreIds != null && input.StoreIds.Any()) ||
  4090 + input.StartTime.HasValue || input.EndTime.HasValue || !string.IsNullOrEmpty(input.keyword))
  4091 + {
  4092 + var billingQuery = _db.Queryable<LqKdKdjlbEntity>()
  4093 + .WhereIF(!string.IsNullOrEmpty(input.StoreId), billing => billing.Djmd == input.StoreId)
  4094 + .WhereIF(input.StoreIds != null && input.StoreIds.Any(), billing => input.StoreIds.Contains(billing.Djmd))
  4095 + .WhereIF(input.StartTime.HasValue, billing => billing.Kdrq >= input.StartTime.Value)
  4096 + .WhereIF(input.EndTime.HasValue, billing => billing.Kdrq <= input.EndTime.Value)
  4097 + .WhereIF(!string.IsNullOrEmpty(input.keyword), billing =>
  4098 + billing.Id != null && billing.Id.Contains(input.keyword) ||
  4099 + SqlFunc.Subqueryable<LqKhxxEntity>()
  4100 + .Where(member => member.Id == billing.Kdhy)
  4101 + .Where(member => (member.Khmc != null && member.Khmc.Contains(input.keyword)) ||
  4102 + (member.Sjh != null && member.Sjh.Contains(input.keyword)))
  4103 + .Any());
  4104 + billingIds = await billingQuery.Select(billing => billing.Id).ToListAsync();
  4105 + }
  4106 +
  4107 + // 构建统计查询(只查询储扣表)
  4108 + var statisticsQuery = _db.Queryable<LqKdDeductinfoEntity>()
  4109 + .WhereIF(input.IsEffective.HasValue, deduct => deduct.IsEffective == input.IsEffective.Value)
  4110 + .WhereIF(!string.IsNullOrEmpty(input.DeductType), deduct => deduct.DeductType == input.DeductType)
  4111 + .WhereIF(!string.IsNullOrEmpty(input.DeductId), deduct => deduct.DeductId == input.DeductId)
  4112 + .WhereIF(!string.IsNullOrEmpty(input.BillingId), deduct => deduct.BillingId == input.BillingId)
  4113 + .WhereIF(billingIds.Any(), deduct => billingIds.Contains(deduct.BillingId))
  4114 + .WhereIF(input.MinAmount.HasValue, deduct => deduct.Amount >= input.MinAmount.Value)
  4115 + .WhereIF(input.MaxAmount.HasValue, deduct => deduct.Amount <= input.MaxAmount.Value)
  4116 + .WhereIF(!string.IsNullOrEmpty(input.ItemName), deduct => deduct.ItemName != null && deduct.ItemName.Contains(input.ItemName))
  4117 + .WhereIF(!string.IsNullOrEmpty(input.ItemId), deduct => deduct.ItemId == input.ItemId)
  4118 + .WhereIF(input.MinUnitPrice.HasValue, deduct => deduct.UnitPrice >= input.MinUnitPrice.Value)
  4119 + .WhereIF(input.MaxUnitPrice.HasValue, deduct => deduct.UnitPrice <= input.MaxUnitPrice.Value)
  4120 + .WhereIF(input.StartCreateTime.HasValue, deduct => deduct.CreateTime >= input.StartCreateTime.Value)
  4121 + .WhereIF(input.EndCreateTime.HasValue, deduct => deduct.CreateTime <= input.EndCreateTime.Value)
  4122 + .WhereIF(!string.IsNullOrEmpty(input.ItemCategory), deduct => deduct.ItemCategory == input.ItemCategory)
  4123 + // 关键词筛选:检查品项名称
  4124 + .WhereIF(!string.IsNullOrEmpty(input.keyword) && !billingIds.Any(), deduct =>
  4125 + deduct.ItemName != null && deduct.ItemName.Contains(input.keyword))
  4126 + // 时间筛选:如果储扣记录有开单时间,也需要检查
  4127 + .WhereIF(input.StartTime.HasValue && !billingIds.Any(), deduct =>
  4128 + deduct.BillingTime.HasValue && deduct.BillingTime >= input.StartTime.Value)
  4129 + .WhereIF(input.EndTime.HasValue && !billingIds.Any(), deduct =>
  4130 + deduct.BillingTime.HasValue && deduct.BillingTime <= input.EndTime.Value);
  4131 +
  4132 + // 统计总记录数
  4133 + var totalCount = await statisticsQuery.CountAsync();
  4134 +
  4135 + // 统计总金额和总项目数
  4136 + var statisticsList = await statisticsQuery
  4137 + .Select(deduct => new
  4138 + {
  4139 + Amount = deduct.Amount ?? 0m,
  4140 + ProjectNumber = deduct.ProjectNumber ?? 0m
  4141 + })
  4142 + .ToListAsync();
  4143 +
  4144 + var totalAmount = statisticsList?.Sum(x => x.Amount) ?? 0m;
  4145 + var totalProjectNumber = statisticsList?.Sum(x => x.ProjectNumber) ?? 0m;
  4146 +
  4147 + // 拼接统计信息到返回结果
  4148 + return new
  4149 + {
  4150 + list = result.list,
  4151 + pagination = result.pagination,
  4152 + statistics = new
  4153 + {
  4154 + totalCount = totalCount,
  4155 + totalAmount = totalAmount,
  4156 + totalProjectNumber = totalProjectNumber
  4157 + }
  4158 + };
  4159 + }
  4160 + catch (Exception statEx)
  4161 + {
  4162 + _logger.LogError(statEx, $"统计查询失败: {statEx.Message}, StackTrace: {statEx.StackTrace}");
  4163 + // 如果统计查询失败,只返回基础查询结果
  4164 + return result;
  4165 + }
3978 4166 }
3979 4167 catch (Exception ex)
3980 4168 {
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs
... ... @@ -162,6 +162,7 @@ namespace NCC.Extend.LqKhxx
162 162 mainHealthUserName = SqlFunc.Subqueryable<UserEntity>().Where(u => u.Id == it.MainHealthUser).Select(u => u.RealName),
163 163 subHealthUserName = SqlFunc.Subqueryable<UserEntity>().Where(u => u.Id == it.SubHealthUser).Select(u => u.RealName),
164 164 tjrName = SqlFunc.Subqueryable<LqKhxxEntity>().Where(u => u.Id == it.Tjr).Select(u => u.Khmc),
  165 + lastConsumeTime = it.LastConsumeTime,
165 166 isBeautyMember = it.IsBeautyMember,
166 167 isMedicalMember = it.IsMedicalMember,
167 168 isTechMember = it.IsTechMember,
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectDirectorSalaryService.cs
... ... @@ -372,10 +372,27 @@ namespace NCC.Extend
372 372 }
373 373  
374 374 /// <summary>
375   - /// 计算提成(分段式)
  375 + /// 计算提成(分段累进式)
376 376 /// </summary>
377 377 /// <param name="totalPerformance">总业绩</param>
378 378 /// <returns>提成金额和比例</returns>
  379 + /// <remarks>
  380 + /// 提成规则(分段累进式):
  381 + /// 1. 前提条件:总业绩必须大于50万才有提成资格
  382 + /// 2. 如果有提成资格后,分段计算:
  383 + /// - 0-70万部分:1%(整个0-70万部分都按1%计算)
  384 + /// - 70万以上部分:1.5%
  385 + ///
  386 + /// 计算公式(分段累进):
  387 + /// - 如果总业绩 ≤ 50万:提成 = 0(无提成资格)
  388 + /// - 如果 50万 < 总业绩 ≤ 70万:提成 = 总业绩 × 1%
  389 + /// - 如果总业绩 > 70万:提成 = 70万 × 1% + (总业绩 - 70万) × 1.5%
  390 + ///
  391 + /// 示例:
  392 + /// - 总业绩 = 40万 → 提成 = 0(无提成资格)
  393 + /// - 总业绩 = 60万 → 提成 = 60万 × 1% = 6,000元
  394 + /// - 总业绩 = 80万 → 提成 = 70万 × 1% + (80万 - 70万) × 1.5% = 7,000 + 1,500 = 8,500元
  395 + /// </remarks>
379 396 private (decimal Amount, decimal? Rate) CalculateCommission(decimal totalPerformance)
380 397 {
381 398 if (totalPerformance <= 0)
... ... @@ -388,21 +405,27 @@ namespace NCC.Extend
388 405  
389 406 if (totalPerformance <= 500000m)
390 407 {
391   - // ≤ 50万:无提成
  408 + // ≤ 50万:无提成资格
392 409 commissionAmount = 0m;
393 410 rate = null;
394 411 }
395 412 else if (totalPerformance <= 700000m)
396 413 {
397   - // 50万 < 总业绩 ≤ 70万:1%提成
  414 + // 50万 < 总业绩 ≤ 70万:整个业绩按1%计算
398 415 commissionAmount = totalPerformance * 0.01m;
399   - rate = 1.00m;
  416 + // 计算平均提成比例(用于显示)
  417 + rate = 1m; // 1%
400 418 }
401 419 else
402 420 {
403   - // > 70万:1.5%提成
404   - commissionAmount = totalPerformance * 0.015m;
405   - rate = 1.50m;
  421 + // 总业绩 > 70万:分段累进计算
  422 + // 0-70万部分:1%
  423 + decimal part1 = 700000m * 0.01m; // 70万 × 1% = 7,000元
  424 + // 70万以上部分:1.5%
  425 + decimal part2 = (totalPerformance - 700000m) * 0.015m;
  426 + commissionAmount = part1 + part2;
  427 + // 计算平均提成比例(用于显示)
  428 + rate = commissionAmount > 0 && totalPerformance > 0 ? (commissionAmount / totalPerformance) * 100m : null;
406 429 }
407 430  
408 431 return (commissionAmount, rate);
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs
... ... @@ -1391,8 +1391,8 @@ namespace NCC.Extend.LqStatistics
1391 1391 AND jksyj.F_kdpxid IS NOT NULL
1392 1392 AND jksyj.F_kdpxid != ''
1393 1393 AND jksyj.F_IsEffective = 1
1394   - AND YEAR(jksyj.yjsj) = @year
1395   - AND MONTH(jksyj.yjsj) = @month
  1394 + AND jksyj.yjsj >= @startDate
  1395 + AND jksyj.yjsj <= @endDate
1396 1396 AND xmzl.fl3 = '合作业绩'
1397 1397 GROUP BY jksyj.jkszh
1398 1398 ) coop_stats ON order_stats.EmployeeId = coop_stats.EmployeeId
... ... @@ -1411,8 +1411,8 @@ namespace NCC.Extend.LqStatistics
1411 1411 AND jksyj.F_kdpxid IS NOT NULL
1412 1412 AND jksyj.F_kdpxid != ''
1413 1413 AND jksyj.F_IsEffective = 1
1414   - AND YEAR(jksyj.yjsj) = @year
1415   - AND MONTH(jksyj.yjsj) = @month
  1414 + AND jksyj.yjsj >= @startDate
  1415 + AND jksyj.yjsj <= @endDate
1416 1416 AND (xmzl.fl3 IS NULL OR xmzl.fl3 != '合作业绩')
1417 1417 GROUP BY jksyj.jkszh
1418 1418 ) base_stats ON order_stats.EmployeeId = base_stats.EmployeeId
... ... @@ -1429,8 +1429,8 @@ namespace NCC.Extend.LqStatistics
1429 1429 AND hytk_jksyj.jksyj != '0'
1430 1430 AND hytk_jksyj.F_IsEffective = 1
1431 1431 AND hytk.F_IsEffective = 1
1432   - AND YEAR(hytk_jksyj.tksj) = @year
1433   - AND MONTH(hytk_jksyj.tksj) = @month
  1432 + AND hytk_jksyj.tksj >= @startDate
  1433 + AND hytk_jksyj.tksj <= @endDate
1434 1434 GROUP BY hytk_jksyj.jks
1435 1435 ) refund_stats ON order_stats.EmployeeId = refund_stats.EmployeeId
1436 1436 ORDER BY order_stats.TotalPerformance DESC";
... ... @@ -1560,10 +1560,10 @@ namespace NCC.Extend.LqStatistics
1560 1560 }
1561 1561  
1562 1562 /// <summary>
1563   - /// 分页查询个人开单业绩统计数据(在用
  1563 + /// 分页查询个人开单业绩统计数据(实时查询
1564 1564 /// </summary>
1565 1565 /// <remarks>
1566   - /// 分页查询个人业绩统计数据,支持多条件筛选
  1566 + /// 实时查询个人业绩统计数据,支持多条件筛选,直接从开单记录表统计
1567 1567 ///
1568 1568 /// 示例请求:
1569 1569 /// ```json
... ... @@ -1572,7 +1572,7 @@ namespace NCC.Extend.LqStatistics
1572 1572 /// "statisticsMonth": "202401",
1573 1573 /// "storeId": "store123",
1574 1574 /// "employeeName": "张三",
1575   - /// "pageIndex": 1,
  1575 + /// "currentPage": 1,
1576 1576 /// "pageSize": 20
1577 1577 /// }
1578 1578 /// ```
... ... @@ -1586,56 +1586,259 @@ namespace NCC.Extend.LqStatistics
1586 1586 {
1587 1587 try
1588 1588 {
1589   - var query = _db.Queryable<LqStatisticsPersonalPerformanceEntity>();
  1589 + // 验证统计月份必填
  1590 + if (string.IsNullOrEmpty(input.StatisticsMonth) || input.StatisticsMonth.Length != 6)
  1591 + {
  1592 + throw NCCException.Oh("统计月份不能为空,格式为YYYYMM");
  1593 + }
1590 1594  
1591   - // 添加查询条件
1592   - query = query.WhereIF(!string.IsNullOrEmpty(input.StatisticsMonth), x => x.StatisticsMonth == input.StatisticsMonth);
1593   - query = query.WhereIF(!string.IsNullOrEmpty(input.StoreId), x => x.StoreId == input.StoreId);
1594   - query = query.WhereIF(!string.IsNullOrEmpty(input.StoreName), x => x.StoreName.Contains(input.StoreName));
1595   - query = query.WhereIF(!string.IsNullOrEmpty(input.EmployeeId), x => x.EmployeeId == input.EmployeeId);
1596   - query = query.WhereIF(!string.IsNullOrEmpty(input.EmployeeName), x => x.EmployeeName.Contains(input.EmployeeName));
1597   - query = query.WhereIF(!string.IsNullOrEmpty(input.GoldTriangleId), x => x.GoldTriangleId == input.GoldTriangleId);
1598   - query = query.WhereIF(!string.IsNullOrEmpty(input.Position), x => x.Position == input.Position);
  1595 + var statisticsMonth = input.StatisticsMonth;
  1596 + var year = int.Parse(statisticsMonth.Substring(0, 4));
  1597 + var month = int.Parse(statisticsMonth.Substring(4, 2));
1599 1598  
1600   - // 按总业绩降序排序
1601   - query = query.OrderBy(x => x.TotalPerformance, OrderByType.Desc);
  1599 + // 计算日期范围(使用日期范围查询替代YEAR/MONTH函数,提升性能)
  1600 + var startDate = new DateTime(year, month, 1);
  1601 + var endDate = startDate.AddMonths(1).AddDays(-1).Date.AddHours(23).AddMinutes(59).AddSeconds(59);
1602 1602  
1603   - // 分页查询并映射到DTO
1604   - var result = await query.Select(it => new LqStatisticsPersonalPerformanceListOutput
  1603 + // 构建筛选条件
  1604 + var innerWhereConditions = new List<string>(); // 子查询中的筛选条件
  1605 + var outerWhereConditions = new List<string>(); // 外层查询的筛选条件
  1606 + var parameters = new Dictionary<string, object>
1605 1607 {
1606   - Id = it.Id,
1607   - StatisticsMonth = it.StatisticsMonth,
1608   - StoreId = it.StoreId,
1609   - StoreName = it.StoreName,
1610   - GoldTriangleId = it.GoldTriangleId,
1611   - GoldTriangleName = it.GoldTriangleName,
1612   - Position = it.Position,
1613   - EmployeeId = it.EmployeeId,
1614   - EmployeeName = it.EmployeeName,
1615   - TotalPerformance = it.TotalPerformance,
1616   - BasePerformance = it.BasePerformance,
1617   - CooperationPerformance = it.CooperationPerformance,
1618   - OrderCount = it.OrderCount,
1619   - FirstOrderCount = it.FirstOrderCount,
1620   - UpgradeOrderCount = it.UpgradeOrderCount,
1621   - FirstOrderPerformance = it.FirstOrderPerformance,
1622   - UpgradeOrderPerformance = it.UpgradeOrderPerformance,
1623   - LastOrderDate = it.LastOrderDate,
1624   - FirstOrderDate = it.FirstOrderDate,
1625   - CreateTime = it.CreateTime,
1626   - RefundPerformance = it.RefundPerformance,
1627   - RefundCount = it.RefundCount,
1628   - ActualPerformance = it.ActualPerformance
1629   - }).ToPagedListAsync(input.currentPage, input.pageSize);
  1608 + { "@statisticsMonth", statisticsMonth },
  1609 + { "@startDate", startDate },
  1610 + { "@endDate", endDate }
  1611 + };
  1612 +
  1613 + // 员工ID筛选(在子查询中)
  1614 + if (!string.IsNullOrEmpty(input.EmployeeId))
  1615 + {
  1616 + innerWhereConditions.Add("jksyj.jkszh = @EmployeeId");
  1617 + parameters.Add("@EmployeeId", input.EmployeeId);
  1618 + }
  1619 +
  1620 + // 门店ID筛选(在JOIN后)
  1621 + if (!string.IsNullOrEmpty(input.StoreId))
  1622 + {
  1623 + outerWhereConditions.Add("u.F_MDID = @StoreId");
  1624 + parameters.Add("@StoreId", input.StoreId);
  1625 + }
  1626 +
  1627 + // 门店名称筛选(在JOIN后)
  1628 + if (!string.IsNullOrEmpty(input.StoreName))
  1629 + {
  1630 + outerWhereConditions.Add("md.dm LIKE @StoreName");
  1631 + parameters.Add("@StoreName", $"%{input.StoreName}%");
  1632 + }
  1633 +
  1634 + // 员工姓名筛选(在JOIN后)
  1635 + if (!string.IsNullOrEmpty(input.EmployeeName))
  1636 + {
  1637 + outerWhereConditions.Add("u.F_REALNAME LIKE @EmployeeName");
  1638 + parameters.Add("@EmployeeName", $"%{input.EmployeeName}%");
  1639 + }
  1640 +
  1641 + // 金三角ID筛选(在JOIN后)
  1642 + if (!string.IsNullOrEmpty(input.GoldTriangleId))
  1643 + {
  1644 + outerWhereConditions.Add("jsjUser.F_Id = @GoldTriangleId");
  1645 + parameters.Add("@GoldTriangleId", input.GoldTriangleId);
  1646 + }
  1647 +
  1648 + var innerWhereClause = innerWhereConditions.Any() ? " AND " + string.Join(" AND ", innerWhereConditions) : "";
  1649 + var outerWhereClause = outerWhereConditions.Any() ? "WHERE " + string.Join(" AND ", outerWhereConditions) : "";
  1650 +
  1651 + // 构建优化的主查询SQL - 合并查询减少扫描次数
  1652 + var sql = $@"
  1653 + SELECT
  1654 + order_stats.EmployeeId,
  1655 + order_stats.EmployeeName,
  1656 + order_stats.StoreId,
  1657 + order_stats.StoreName,
  1658 + order_stats.GoldTriangleId,
  1659 + order_stats.GoldTriangleName,
  1660 + order_stats.Position,
  1661 + order_stats.OrderCount,
  1662 + order_stats.FirstOrderCount,
  1663 + order_stats.UpgradeOrderCount,
  1664 + order_stats.FirstOrderPerformance,
  1665 + order_stats.UpgradeOrderPerformance,
  1666 + order_stats.LastOrderDate,
  1667 + order_stats.FirstOrderDate,
  1668 + COALESCE(coop_stats.CooperationPerformance, 0) AS CooperationPerformance,
  1669 + COALESCE(order_stats.TotalPerformance, 0) - COALESCE(coop_stats.CooperationPerformance, 0) AS BasePerformance,
  1670 + COALESCE(refund_stats.RefundPerformance, 0) AS RefundPerformance,
  1671 + COALESCE(refund_stats.RefundCount, 0) AS RefundCount,
  1672 + order_stats.TotalPerformance
  1673 + FROM (
  1674 + SELECT
  1675 + order_base.jkszh AS EmployeeId,
  1676 + u.F_REALNAME AS EmployeeName,
  1677 + u.F_MDID AS StoreId,
  1678 + COALESCE(md.dm, '') AS StoreName,
  1679 + COALESCE(jsjUser.F_Id, '') AS GoldTriangleId,
  1680 + COALESCE(jsjUser.jsj, '') AS GoldTriangleName,
  1681 + CASE
  1682 + WHEN jsjUser.is_leader = 1 THEN '顾问'
  1683 + ELSE COALESCE(u.F_GW, '')
  1684 + END AS Position,
  1685 + COUNT(*) AS OrderCount,
  1686 + COUNT(CASE WHEN order_base.sfskdd = '是' THEN 1 END) AS FirstOrderCount,
  1687 + COUNT(CASE WHEN order_base.sfskdd = '否' THEN 1 END) AS UpgradeOrderCount,
  1688 + SUM(CASE WHEN order_base.sfskdd = '是' THEN order_base.order_performance ELSE 0 END) AS FirstOrderPerformance,
  1689 + SUM(CASE WHEN order_base.sfskdd = '否' THEN order_base.order_performance ELSE 0 END) AS UpgradeOrderPerformance,
  1690 + MAX(order_base.yjsj) AS LastOrderDate,
  1691 + MIN(order_base.yjsj) AS FirstOrderDate,
  1692 + SUM(order_base.order_performance) AS TotalPerformance,
  1693 + 0 AS CooperationPerformance,
  1694 + 0 AS BasePerformance
  1695 + FROM (
  1696 + -- 基础开单数据汇总(不包含合作业绩分类,提升性能)
  1697 + SELECT
  1698 + jksyj.jkszh,
  1699 + jksyj.glkdbh,
  1700 + kd.sfskdd,
  1701 + MAX(jksyj.yjsj) as yjsj,
  1702 + SUM(CAST(jksyj.jksyj AS DECIMAL(18,2))) as order_performance
  1703 + FROM lq_kd_jksyj jksyj
  1704 + INNER JOIN lq_kd_pxmx pxmx ON jksyj.F_kdpxid = pxmx.F_Id AND pxmx.F_IsEffective = 1
  1705 + INNER JOIN lq_kd_kdjlb kd ON jksyj.glkdbh = CONVERT(kd.F_Id USING utf8mb4)
  1706 + WHERE jksyj.yjsj IS NOT NULL
  1707 + AND jksyj.jksyj IS NOT NULL
  1708 + AND jksyj.jksyj != ''
  1709 + AND jksyj.jksyj != '0'
  1710 + AND jksyj.F_kdpxid IS NOT NULL
  1711 + AND jksyj.F_kdpxid != ''
  1712 + AND jksyj.F_IsEffective = 1
  1713 + AND jksyj.yjsj >= @startDate
  1714 + AND jksyj.yjsj <= @endDate
  1715 + {innerWhereClause}
  1716 + GROUP BY jksyj.jkszh, jksyj.glkdbh, kd.sfskdd
  1717 + ) order_base
  1718 + INNER JOIN BASE_USER u ON order_base.jkszh = u.F_Id
  1719 + LEFT JOIN lq_mdxx md ON u.F_MDID = md.F_Id
  1720 + LEFT JOIN (
  1721 + SELECT
  1722 + jsjUser.user_id,
  1723 + MIN(jsjUser.jsj_id) as F_Id,
  1724 + MIN(jsj.jsj) as jsj,
  1725 + MIN(jsjUser.is_leader) as is_leader
  1726 + FROM lq_jinsanjiao_user jsjUser
  1727 + 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
  1728 + WHERE jsjUser.F_Month = @statisticsMonth
  1729 + AND jsjUser.status = 'ACTIVE'
  1730 + AND jsjUser.F_DeleteMark = 0
  1731 + GROUP BY jsjUser.user_id
  1732 + ) jsjUser ON order_base.jkszh = jsjUser.user_id
  1733 + {outerWhereClause}
  1734 + GROUP BY
  1735 + order_base.jkszh,
  1736 + u.F_REALNAME,
  1737 + u.F_MDID,
  1738 + md.dm,
  1739 + jsjUser.F_Id,
  1740 + jsjUser.jsj,
  1741 + jsjUser.is_leader,
  1742 + u.F_GW
  1743 + ) order_stats
  1744 + LEFT JOIN (
  1745 + -- 合作业绩统计(单独查询,提升性能)
  1746 + SELECT
  1747 + jksyj.jkszh AS EmployeeId,
  1748 + SUM(CAST(jksyj.jksyj AS DECIMAL(18,2))) AS CooperationPerformance
  1749 + FROM lq_kd_jksyj jksyj
  1750 + INNER JOIN lq_kd_pxmx pxmx ON jksyj.F_kdpxid = pxmx.F_Id AND pxmx.F_IsEffective = 1
  1751 + INNER JOIN lq_xmzl xmzl ON pxmx.px = xmzl.F_Id AND xmzl.fl3 = '合作业绩'
  1752 + WHERE jksyj.yjsj IS NOT NULL
  1753 + AND jksyj.jksyj IS NOT NULL
  1754 + AND jksyj.jksyj != ''
  1755 + AND jksyj.jksyj != '0'
  1756 + AND jksyj.F_kdpxid IS NOT NULL
  1757 + AND jksyj.F_kdpxid != ''
  1758 + AND jksyj.F_IsEffective = 1
  1759 + AND jksyj.yjsj >= @startDate
  1760 + AND jksyj.yjsj <= @endDate
  1761 + GROUP BY jksyj.jkszh
  1762 + ) coop_stats ON order_stats.EmployeeId = coop_stats.EmployeeId
  1763 + LEFT JOIN (
  1764 + -- 退单业绩统计
  1765 + SELECT
  1766 + hytk_jksyj.jkszh AS EmployeeId,
  1767 + SUM(CAST(hytk_jksyj.jksyj AS DECIMAL(18,2))) AS RefundPerformance,
  1768 + COUNT(*) AS RefundCount
  1769 + FROM lq_hytk_jksyj hytk_jksyj
  1770 + INNER JOIN lq_hytk_hytk hytk ON hytk_jksyj.gltkbh = hytk.F_Id
  1771 + WHERE hytk_jksyj.jksyj IS NOT NULL
  1772 + AND hytk_jksyj.jksyj != ''
  1773 + AND hytk_jksyj.jksyj != '0'
  1774 + AND hytk_jksyj.F_IsEffective = 1
  1775 + AND hytk.F_IsEffective = 1
  1776 + AND hytk_jksyj.tksj >= @startDate
  1777 + AND hytk_jksyj.tksj <= @endDate
  1778 + GROUP BY hytk_jksyj.jkszh
  1779 + ) refund_stats ON order_stats.EmployeeId = refund_stats.EmployeeId";
  1780 +
  1781 + // 岗位筛选(需要在最外层,因为岗位是通过CASE计算的)
  1782 + var finalWhereConditions = new List<string>();
  1783 + if (!string.IsNullOrEmpty(input.Position))
  1784 + {
  1785 + finalWhereConditions.Add("order_stats.Position = @Position");
  1786 + parameters.Add("@Position", input.Position);
  1787 + }
  1788 +
  1789 + var finalWhereClause = finalWhereConditions.Any() ? " WHERE " + string.Join(" AND ", finalWhereConditions) : "";
  1790 + var finalSql = $@"SELECT * FROM ({sql}) AS order_stats{finalWhereClause} ORDER BY order_stats.TotalPerformance DESC";
  1791 +
  1792 + // 查询总数
  1793 + var countSql = $"SELECT COUNT(*) FROM ({finalSql}) AS total_count";
  1794 + var totalCount = await _db.Ado.GetIntAsync(countSql, parameters);
  1795 +
  1796 + // 分页查询
  1797 + var pageIndex = input.currentPage > 0 ? input.currentPage : 1;
  1798 + var pageSize = input.pageSize > 0 ? input.pageSize : 20;
  1799 + var offset = (pageIndex - 1) * pageSize;
  1800 + var pagedSql = $"{finalSql} LIMIT {pageSize} OFFSET {offset}";
  1801 +
  1802 + _logger.LogInformation($"执行个人业绩统计实时查询SQL - 月份: {statisticsMonth}, 页码: {pageIndex}, 每页: {pageSize}");
  1803 +
  1804 + var statisticsData = await _db.Ado.SqlQueryAsync<dynamic>(pagedSql, parameters);
  1805 +
  1806 + // 映射到输出DTO
  1807 + var outputList = statisticsData.Select(stats => new LqStatisticsPersonalPerformanceListOutput
  1808 + {
  1809 + Id = YitIdHelper.NextId().ToString(), // 实时查询没有ID,生成临时ID
  1810 + StatisticsMonth = statisticsMonth,
  1811 + StoreId = stats.StoreId?.ToString() ?? "",
  1812 + StoreName = stats.StoreName?.ToString() ?? "",
  1813 + GoldTriangleId = stats.GoldTriangleId?.ToString() ?? "",
  1814 + GoldTriangleName = stats.GoldTriangleName?.ToString() ?? "",
  1815 + Position = stats.Position?.ToString() ?? "",
  1816 + EmployeeId = stats.EmployeeId?.ToString() ?? "",
  1817 + EmployeeName = stats.EmployeeName?.ToString() ?? "",
  1818 + TotalPerformance = Convert.ToDecimal(stats.TotalPerformance ?? 0) - Convert.ToDecimal(stats.RefundPerformance ?? 0),
  1819 + BasePerformance = Convert.ToDecimal(stats.BasePerformance ?? 0),
  1820 + CooperationPerformance = Convert.ToDecimal(stats.CooperationPerformance ?? 0),
  1821 + RefundPerformance = Convert.ToDecimal(stats.RefundPerformance ?? 0),
  1822 + RefundCount = Convert.ToInt32(stats.RefundCount ?? 0),
  1823 + ActualPerformance = Convert.ToDecimal(stats.TotalPerformance ?? 0) - Convert.ToDecimal(stats.RefundPerformance ?? 0),
  1824 + OrderCount = Convert.ToInt32(stats.OrderCount ?? 0),
  1825 + FirstOrderCount = Convert.ToInt32(stats.FirstOrderCount ?? 0),
  1826 + UpgradeOrderCount = Convert.ToInt32(stats.UpgradeOrderCount ?? 0),
  1827 + FirstOrderPerformance = Convert.ToDecimal(stats.FirstOrderPerformance ?? 0),
  1828 + UpgradeOrderPerformance = Convert.ToDecimal(stats.UpgradeOrderPerformance ?? 0),
  1829 + LastOrderDate = stats.LastOrderDate as DateTime?,
  1830 + FirstOrderDate = stats.FirstOrderDate as DateTime?,
  1831 + CreateTime = DateTime.Now
  1832 + }).ToList();
1630 1833  
1631 1834 return new
1632 1835 {
1633   - list = result.list,
  1836 + list = outputList,
1634 1837 pagination = new
1635 1838 {
1636   - pageIndex = input.currentPage,
1637   - pageSize = input.pageSize,
1638   - total = result.pagination.Total
  1839 + pageIndex = pageIndex,
  1840 + pageSize = pageSize,
  1841 + total = totalCount
1639 1842 }
1640 1843 };
1641 1844 }
... ... @@ -3195,44 +3398,156 @@ namespace NCC.Extend.LqStatistics
3195 3398 }
3196 3399  
3197 3400 /// <summary>
3198   - /// 获取门店总业绩统计列表
  3401 + /// 获取门店总业绩统计列表(实时查询)
3199 3402 /// </summary>
  3403 + /// <remarks>
  3404 + /// 实时查询门店总业绩统计数据,支持多条件筛选,直接从开单记录表统计
  3405 + ///
  3406 + /// 示例请求:
  3407 + /// ```json
  3408 + /// POST /api/Extend/LqStatistics/get-store-total-performance-statistics-list
  3409 + /// {
  3410 + /// "statisticsMonth": "202401",
  3411 + /// "storeName": "门店名称",
  3412 + /// "pageIndex": 1,
  3413 + /// "pageSize": 20
  3414 + /// }
  3415 + /// ```
  3416 + /// </remarks>
3200 3417 /// <param name="input">查询参数</param>
3201 3418 /// <returns>分页结果</returns>
  3419 + /// <response code="200">成功返回分页数据</response>
  3420 + /// <response code="400">参数错误</response>
  3421 + /// <response code="500">服务器错误</response>
3202 3422 [HttpPost("get-store-total-performance-statistics-list")]
3203 3423 public async Task<dynamic> GetStoreTotalPerformanceStatisticsList([FromBody] LqStoreTotalPerformanceStatisticsListQueryInput input)
3204 3424 {
3205 3425 try
3206 3426 {
3207   - var query = _db.Queryable<LqStatisticsStoreTotalPerformanceEntity>();
  3427 + // 验证统计月份必填
  3428 + if (string.IsNullOrEmpty(input.StatisticsMonth) || input.StatisticsMonth.Length != 6)
  3429 + {
  3430 + throw NCCException.Oh("统计月份不能为空,格式为YYYYMM");
  3431 + }
3208 3432  
3209   - // 添加查询条件
3210   - query = query.WhereIF(!string.IsNullOrEmpty(input.StatisticsMonth), x => x.StatisticsMonth == input.StatisticsMonth);
3211   - query = query.WhereIF(!string.IsNullOrEmpty(input.StoreName), x => x.StoreName.Contains(input.StoreName));
  3433 + var statisticsMonth = input.StatisticsMonth;
  3434 + var year = int.Parse(statisticsMonth.Substring(0, 4));
  3435 + var month = int.Parse(statisticsMonth.Substring(4, 2));
3212 3436  
3213   - // 按创建时间降序排序
3214   - query = query.OrderBy(x => x.CreateTime, OrderByType.Desc);
  3437 + // 计算日期范围(使用日期范围查询替代DATE_FORMAT函数,提升性能)
  3438 + var startDate = new DateTime(year, month, 1);
  3439 + var endDate = startDate.AddMonths(1).AddDays(-1).Date.AddHours(23).AddMinutes(59).AddSeconds(59);
3215 3440  
3216   - // 分页查询并映射到DTO
3217   - var pagedResult = await query.ToPagedListAsync(input.PageIndex, input.PageSize);
  3441 + // 构建筛选条件
  3442 + var whereConditions = new List<string>();
  3443 + var parameters = new Dictionary<string, object>
  3444 + {
  3445 + { "@statisticsMonth", statisticsMonth },
  3446 + { "@startDate", startDate },
  3447 + { "@endDate", endDate }
  3448 + };
3218 3449  
3219   - var outputList = pagedResult.list.Select(it => new LqStoreTotalPerformanceStatisticsListOutput
  3450 + // 门店名称筛选
  3451 + if (!string.IsNullOrEmpty(input.StoreName))
3220 3452 {
3221   - Id = it.Id,
3222   - StatisticsMonth = it.StatisticsMonth,
3223   - StoreId = it.StoreId,
3224   - StoreName = it.StoreName,
3225   - TotalPerformance = it.TotalPerformance,
3226   - DebtAmount = it.DebtAmount,
3227   - TotalOrderPerformance = it.TotalOrderPerformance,
3228   - StorageDeductionAmount = it.StorageDeductionAmount,
3229   - ItemQuantity = it.ItemQuantity,
3230   - FirstOrderCount = it.FirstOrderCount,
3231   - UpgradeOrderCount = it.UpgradeOrderCount,
3232   - RefundAmount = it.RefundAmount,
3233   - RefundCount = it.RefundCount,
3234   - CreateTime = it.CreateTime.HasValue ? it.CreateTime.Value : DateTime.Now,
3235   - ActualPerformance = it.ActualPerformance
  3453 + whereConditions.Add("md.dm LIKE @StoreName");
  3454 + parameters.Add("@StoreName", $"%{input.StoreName}%");
  3455 + }
  3456 +
  3457 + var whereClause = whereConditions.Any() ? "WHERE " + string.Join(" AND ", whereConditions) : "";
  3458 +
  3459 + // 构建实时查询SQL - 参考SaveStoreTotalPerformanceStatistics的逻辑
  3460 + var sql = $@"
  3461 + SELECT
  3462 + store_data.F_StoreId,
  3463 + store_data.F_StoreName,
  3464 + @statisticsMonth as F_StatisticsMonth,
  3465 + store_data.F_TotalPerformance,
  3466 + store_data.F_DebtAmount,
  3467 + store_data.F_TotalOrderPerformance,
  3468 + store_data.F_StorageDeductionAmount,
  3469 + COALESCE(item_data.F_ItemQuantity, 0) as F_ItemQuantity,
  3470 + store_data.F_FirstOrderCount,
  3471 + store_data.F_UpgradeOrderCount,
  3472 + store_data.F_FirstOrderPerformance,
  3473 + store_data.F_UpgradeOrderPerformance,
  3474 + COALESCE(refund_data.F_RefundAmount, 0) as F_RefundAmount,
  3475 + COALESCE(refund_data.F_RefundCount, 0) as F_RefundCount
  3476 + FROM (
  3477 + SELECT
  3478 + kd.djmd as F_StoreId,
  3479 + md.dm as F_StoreName,
  3480 + COALESCE(SUM(kd.zdyj), 0) as F_TotalPerformance,
  3481 + COALESCE(SUM(kd.qk), 0) as F_DebtAmount,
  3482 + COALESCE(SUM(kd.sfyj), 0) as F_TotalOrderPerformance,
  3483 + COALESCE(SUM(kd.F_DeductAmount), 0) as F_StorageDeductionAmount,
  3484 + COUNT(DISTINCT CASE WHEN kd.sfskdd = '是' THEN kd.F_Id END) as F_FirstOrderCount,
  3485 + COUNT(DISTINCT CASE WHEN kd.sfskdd = '否' THEN kd.F_Id END) as F_UpgradeOrderCount,
  3486 + SUM(CASE WHEN kd.sfskdd = '是' THEN COALESCE(kd.zdyj, 0) ELSE 0 END) as F_FirstOrderPerformance,
  3487 + SUM(CASE WHEN kd.sfskdd = '否' THEN COALESCE(kd.zdyj, 0) ELSE 0 END) as F_UpgradeOrderPerformance
  3488 + FROM lq_kd_kdjlb kd
  3489 + LEFT JOIN lq_mdxx md ON CONVERT(kd.djmd USING utf8mb4) = md.F_Id
  3490 + WHERE kd.F_IsEffective = 1
  3491 + AND kd.kdrq >= @startDate
  3492 + AND kd.kdrq <= @endDate
  3493 + GROUP BY kd.djmd, md.dm
  3494 + ) store_data
  3495 + LEFT JOIN (
  3496 + SELECT
  3497 + kd.djmd as F_StoreId,
  3498 + COUNT(pxmx.F_ProjectNumber) as F_ItemQuantity
  3499 + FROM lq_kd_kdjlb kd
  3500 + LEFT JOIN lq_kd_pxmx pxmx ON CONVERT(kd.F_Id USING utf8mb4) = pxmx.glkdbh AND pxmx.F_IsEffective = 1
  3501 + WHERE kd.F_IsEffective = 1
  3502 + AND kd.kdrq >= @startDate
  3503 + AND kd.kdrq <= @endDate
  3504 + GROUP BY kd.djmd
  3505 + ) item_data ON store_data.F_StoreId = item_data.F_StoreId
  3506 + LEFT JOIN (
  3507 + SELECT
  3508 + hytk.md as F_StoreId,
  3509 + COALESCE(SUM(hytk.F_ActualRefundAmount), 0) as F_RefundAmount,
  3510 + COUNT(DISTINCT hytk.F_Id) as F_RefundCount
  3511 + FROM lq_hytk_hytk hytk
  3512 + WHERE hytk.F_IsEffective = 1
  3513 + AND hytk.tksj >= @startDate
  3514 + AND hytk.tksj <= @endDate
  3515 + GROUP BY hytk.md
  3516 + ) refund_data ON store_data.F_StoreId = refund_data.F_StoreId
  3517 + {whereClause}";
  3518 +
  3519 + // 查询总数
  3520 + var countSql = $"SELECT COUNT(*) FROM ({sql}) AS total_count";
  3521 + var totalCount = await _db.Ado.GetIntAsync(countSql, parameters);
  3522 +
  3523 + // 分页查询
  3524 + var pageIndex = input.PageIndex > 0 ? input.PageIndex : 1;
  3525 + var pageSize = input.PageSize > 0 ? input.PageSize : 20;
  3526 + var offset = (pageIndex - 1) * pageSize;
  3527 + var pagedSql = $"{sql} ORDER BY store_data.F_TotalPerformance DESC LIMIT {pageSize} OFFSET {offset}";
  3528 +
  3529 + _logger.LogInformation($"执行门店总业绩统计实时查询SQL - 月份: {statisticsMonth}, 页码: {pageIndex}, 每页: {pageSize}");
  3530 +
  3531 + var statisticsData = await _db.Ado.SqlQueryAsync<dynamic>(pagedSql, parameters);
  3532 +
  3533 + // 映射到输出DTO
  3534 + var outputList = statisticsData.Select(data => new LqStoreTotalPerformanceStatisticsListOutput
  3535 + {
  3536 + Id = YitIdHelper.NextId().ToString(), // 实时查询没有ID,生成临时ID
  3537 + StatisticsMonth = statisticsMonth,
  3538 + StoreId = data.F_StoreId?.ToString() ?? "",
  3539 + StoreName = data.F_StoreName?.ToString() ?? "",
  3540 + TotalPerformance = Convert.ToDecimal(data.F_TotalPerformance ?? 0),
  3541 + DebtAmount = Convert.ToDecimal(data.F_DebtAmount ?? 0),
  3542 + TotalOrderPerformance = Convert.ToDecimal(data.F_TotalOrderPerformance ?? 0),
  3543 + StorageDeductionAmount = Convert.ToDecimal(data.F_StorageDeductionAmount ?? 0),
  3544 + ItemQuantity = Convert.ToInt32(data.F_ItemQuantity ?? 0),
  3545 + FirstOrderCount = Convert.ToInt32(data.F_FirstOrderCount ?? 0),
  3546 + UpgradeOrderCount = Convert.ToInt32(data.F_UpgradeOrderCount ?? 0),
  3547 + RefundAmount = Convert.ToDecimal(data.F_RefundAmount ?? 0),
  3548 + RefundCount = Convert.ToInt32(data.F_RefundCount ?? 0),
  3549 + ActualPerformance = Convert.ToDecimal(data.F_TotalOrderPerformance ?? 0) - Convert.ToDecimal(data.F_RefundAmount ?? 0),
  3550 + CreateTime = DateTime.Now
3236 3551 }).ToList();
3237 3552  
3238 3553 return new
... ... @@ -3240,9 +3555,9 @@ namespace NCC.Extend.LqStatistics
3240 3555 list = outputList,
3241 3556 pagination = new
3242 3557 {
3243   - pageIndex = input.PageIndex,
3244   - pageSize = input.PageSize,
3245   - total = pagedResult.pagination.Total
  3558 + pageIndex = pageIndex,
  3559 + pageSize = pageSize,
  3560 + total = totalCount
3246 3561 }
3247 3562 };
3248 3563 }
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqTechGeneralManagerSalaryService.cs
... ... @@ -7,7 +7,9 @@ using NCC.DynamicApiController;
7 7 using NCC.Extend.Entitys.Dto.LqTechGeneralManagerSalary;
8 8 using NCC.Extend.Entitys.lq_attendance_summary;
9 9 using NCC.Extend.Entitys.lq_hytk_hytk;
  10 +using NCC.Extend.Entitys.lq_hytk_jksyj;
10 11 using NCC.Extend.Entitys.lq_hytk_mx;
  12 +using NCC.Extend.Entitys.lq_kd_jksyj;
11 13 using NCC.Extend.Entitys.lq_kd_kdjlb;
12 14 using NCC.Extend.Entitys.lq_kd_pxmx;
13 15 using NCC.Extend.Entitys.lq_md_general_manager_lifeline;
... ... @@ -236,57 +238,47 @@ namespace NCC.Extend
236 238 {
237 239 foreach (var storeId in allManagedStoreIds)
238 240 {
239   - // 该门店的开单溯源金额
240   - var storeTraceabilityBilling = await _db.Queryable<LqKdPxmxEntity, LqKdKdjlbEntity, LqXmzlEntity>(
241   - (pxmx, billing, item) => pxmx.Glkdbh == billing.Id && pxmx.Px == item.Id)
242   - .Where((pxmx, billing, item) =>
243   - pxmx.IsEffective == 1
244   - && billing.IsEffective == 1
245   - && item.IsEffective == 1
246   - && (pxmx.BeautyType == "溯源系统" || pxmx.BeautyType == "溯源"
247   - || item.BeautyType == "溯源系统" || item.BeautyType == "溯源")
248   - && billing.Djmd == storeId
249   - && billing.Kdrq >= startDate && billing.Kdrq <= endDate.AddDays(1))
250   - .SumAsync((pxmx, billing, item) => (decimal?)pxmx.ActualPrice) ?? 0m;
251   -
252   - // 该门店的退卡溯源金额
253   - var storeTraceabilityRefund = await _db.Queryable<LqHytkMxEntity, LqHytkHytkEntity, LqXmzlEntity>(
254   - (tkmx, refund, item) => tkmx.RefundInfoId == refund.Id && tkmx.Px == item.Id)
255   - .Where((tkmx, refund, item) =>
256   - tkmx.IsEffective == 1
257   - && refund.IsEffective == 1
258   - && item.IsEffective == 1
259   - && (tkmx.BeautyType == "溯源系统" || tkmx.BeautyType == "溯源"
260   - || item.BeautyType == "溯源系统" || item.BeautyType == "溯源")
261   - && refund.Md == storeId
262   - && refund.Tksj >= startDate && refund.Tksj <= endDate.AddDays(1))
263   - .SumAsync((tkmx, refund, item) => (decimal?)tkmx.Tkje) ?? 0m;
264   -
265   - // 该门店的开单Cell金额
266   - var storeCellBilling = await _db.Queryable<LqKdPxmxEntity, LqKdKdjlbEntity, LqXmzlEntity>(
267   - (pxmx, billing, item) => pxmx.Glkdbh == billing.Id && pxmx.Px == item.Id)
268   - .Where((pxmx, billing, item) =>
269   - pxmx.IsEffective == 1
270   - && billing.IsEffective == 1
271   - && item.IsEffective == 1
272   - && (pxmx.BeautyType == "cell" || pxmx.BeautyType == "Cell"
273   - || item.BeautyType == "cell" || item.BeautyType == "Cell")
274   - && billing.Djmd == storeId
275   - && billing.Kdrq >= startDate && billing.Kdrq <= endDate.AddDays(1))
276   - .SumAsync((pxmx, billing, item) => (decimal?)pxmx.ActualPrice) ?? 0m;
277   -
278   - // 该门店的退卡Cell金额
279   - var storeCellRefund = await _db.Queryable<LqHytkMxEntity, LqHytkHytkEntity, LqXmzlEntity>(
280   - (tkmx, refund, item) => tkmx.RefundInfoId == refund.Id && tkmx.Px == item.Id)
281   - .Where((tkmx, refund, item) =>
282   - tkmx.IsEffective == 1
283   - && refund.IsEffective == 1
284   - && item.IsEffective == 1
285   - && (tkmx.BeautyType == "cell" || tkmx.BeautyType == "Cell"
286   - || item.BeautyType == "cell" || item.BeautyType == "Cell")
287   - && refund.Md == storeId
288   - && refund.Tksj >= startDate && refund.Tksj <= endDate.AddDays(1))
289   - .SumAsync((tkmx, refund, item) => (decimal?)tkmx.Tkje) ?? 0m;
  241 + // 该门店的开单溯源金额(从健康师业绩表统计)
  242 + var storeTraceabilityBillingList = await _db.Queryable<LqKdJksyjEntity>()
  243 + .Where(x => x.IsEffective == 1
  244 + && x.StoreId == storeId
  245 + && (x.BeautyType == "溯源系统" || x.BeautyType == "溯源")
  246 + && x.Yjsj >= startDate && x.Yjsj <= endDate.AddDays(1))
  247 + .Select(x => x.Jksyj)
  248 + .ToListAsync();
  249 +
  250 + var storeTraceabilityBilling = storeTraceabilityBillingList
  251 + .Where(x => !string.IsNullOrEmpty(x))
  252 + .Sum(x => decimal.TryParse(x, out var val) ? val : 0m);
  253 +
  254 + // 该门店的退卡溯源金额(从退卡健康师业绩表统计)
  255 + var storeTraceabilityRefund = await _db.Queryable<LqHytkJksyjEntity>()
  256 + .Where(x => x.IsEffective == 1
  257 + && x.StoreId == storeId
  258 + && (x.BeautyType == "溯源系统" || x.BeautyType == "溯源")
  259 + && x.Tksj >= startDate && x.Tksj <= endDate.AddDays(1))
  260 + .SumAsync(x => (decimal?)x.Jksyj) ?? 0m;
  261 +
  262 + // 该门店的开单Cell金额(从健康师业绩表统计)
  263 + var storeCellBillingList = await _db.Queryable<LqKdJksyjEntity>()
  264 + .Where(x => x.IsEffective == 1
  265 + && x.StoreId == storeId
  266 + && (x.BeautyType == "cell" || x.BeautyType == "Cell")
  267 + && x.Yjsj >= startDate && x.Yjsj <= endDate.AddDays(1))
  268 + .Select(x => x.Jksyj)
  269 + .ToListAsync();
  270 +
  271 + var storeCellBilling = storeCellBillingList
  272 + .Where(x => !string.IsNullOrEmpty(x))
  273 + .Sum(x => decimal.TryParse(x, out var val) ? val : 0m);
  274 +
  275 + // 该门店的退卡Cell金额(从退卡健康师业绩表统计)
  276 + var storeCellRefund = await _db.Queryable<LqHytkJksyjEntity>()
  277 + .Where(x => x.IsEffective == 1
  278 + && x.StoreId == storeId
  279 + && (x.BeautyType == "cell" || x.BeautyType == "Cell")
  280 + && x.Tksj >= startDate && x.Tksj <= endDate.AddDays(1))
  281 + .SumAsync(x => (decimal?)x.Jksyj) ?? 0m;
290 282  
291 283 // 获取该门店属于哪些科技部总经理
292 284 // 通过门店的kjb字段确定:如果门店的kjb等于科技一部的组织ID,则该门店属于科技一部总经理
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqTechTeacherSalaryService.cs
... ... @@ -453,43 +453,72 @@ namespace NCC.Extend
453 453 }
454 454  
455 455 /// <summary>
456   - /// 计算业绩提成(阶梯式)
  456 + /// 计算业绩提成(分段累进式)
457 457 /// </summary>
458 458 /// <param name="totalPerformance">总业绩</param>
459 459 /// <returns>提成比例和金额</returns>
  460 + /// <remarks>
  461 + /// 提成规则(分段累进式):
  462 + /// 1. 前提条件:业绩必须大于1万才能进行提成
  463 + /// 2. 如果有提成资格后,分段计算:
  464 + /// - 0-7万部分:2%(整个0-7万部分都按2%计算)
  465 + /// - 7万-15万部分:2.5%
  466 + /// - 15万以上部分:3%
  467 + ///
  468 + /// 计算公式(分段累进):
  469 + /// - 如果业绩 ≤ 1万:提成 = 0(无提成资格)
  470 + /// - 如果 1万 < 业绩 ≤ 7万:提成 = 业绩 × 2%
  471 + /// - 如果 7万 < 业绩 ≤ 15万:提成 = 7万 × 2% + (业绩 - 7万) × 2.5%
  472 + /// - 如果业绩 > 15万:提成 = 7万 × 2% + (15万 - 7万) × 2.5% + (业绩 - 15万) × 3%
  473 + ///
  474 + /// 示例:
  475 + /// - 总业绩 = 5,000元 → 提成 = 0(无提成资格)
  476 + /// - 总业绩 = 50,000元 → 提成 = 50,000 × 2% = 1,000元
  477 + /// - 总业绩 = 100,000元 → 提成 = 70,000 × 2% + (100,000 - 70,000) × 2.5% = 1,400 + 750 = 2,150元
  478 + /// - 总业绩 = 200,000元 → 提成 = 70,000 × 2% + (150,000 - 70,000) × 2.5% + (200,000 - 150,000) × 3% = 1,400 + 2,000 + 1,500 = 4,900元
  479 + /// </remarks>
460 480 private (decimal Rate, decimal Amount) CalculatePerformanceCommission(decimal totalPerformance)
461 481 {
462 482 // 提成前提:业绩必须大于1万才能进行提成
463 483 if (totalPerformance <= 10000m)
464 484 {
465   - // ≤ 10,000元 → 0%(无提成
  485 + // ≤ 10,000元 → 0%(无提成资格
466 486 return (0m, 0m);
467 487 }
468 488  
469   - decimal rate;
470   - decimal amount;
  489 + decimal totalCommission = 0m;
471 490  
472   - // 阶梯式提成计算(整个业绩按对应比例
  491 + // 分段累进式提成计算(已通过提成资格检查
473 492 if (totalPerformance > 150000m)
474 493 {
475   - // > 15万 → 3%
476   - rate = 3m;
477   - amount = totalPerformance * 0.03m;
  494 + // 业绩 > 15万:分段计算
  495 + // 0-7万部分:2%
  496 + decimal part1 = 70000m * 0.02m; // 7万 × 2% = 1,400元
  497 + // 7万-15万部分:2.5%
  498 + decimal part2 = (150000m - 70000m) * 0.025m; // 8万 × 2.5% = 2,000元
  499 + // 15万以上部分:3%
  500 + decimal part3 = (totalPerformance - 150000m) * 0.03m;
  501 + totalCommission = part1 + part2 + part3;
478 502 }
479 503 else if (totalPerformance > 70000m)
480 504 {
481   - // > 7万 且 ≤ 15万 → 2.5%
482   - rate = 2.5m;
483   - amount = totalPerformance * 0.025m;
  505 + // 业绩 > 7万 且 ≤ 15万:分段计算
  506 + // 0-7万部分:2%
  507 + decimal part1 = 70000m * 0.02m; // 7万 × 2% = 1,400元
  508 + // 7万以上部分:2.5%
  509 + decimal part2 = (totalPerformance - 70000m) * 0.025m;
  510 + totalCommission = part1 + part2;
484 511 }
485 512 else
486 513 {
487   - // > 1万 且 ≤ 7万 → 2%
488   - rate = 2m;
489   - amount = totalPerformance * 0.02m;
  514 + // 业绩 > 1万 且 ≤ 7万:整个业绩按2%计算
  515 + totalCommission = totalPerformance * 0.02m;
490 516 }
491 517  
492   - return (rate, amount);
  518 + // 计算平均提成比例(用于显示)
  519 + decimal averageRate = totalCommission > 0 && totalPerformance > 0 ? (totalCommission / totalPerformance) * 100m : 0m;
  520 +
  521 + return (averageRate, totalCommission);
493 522 }
494 523  
495 524 /// <summary>
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqTkjlbService.cs
... ... @@ -898,6 +898,8 @@ namespace NCC.Extend.LqTkjlb
898 898 tk.F_MemberId as member_id, -- 会员ID
899 899 tk.F_CustomerName as customer_name, -- 顾客姓名
900 900 tk.F_CreateTime as tk_time, -- 拓客时间
  901 + tk.F_ExpansionUserId as expansion_user_id, -- 拓客人员ID
  902 + COALESCE(expansion_user.F_REALNAME, '') as expansion_user_name, -- 拓客人员姓名
901 903 -- 邀约信息
902 904 yaoy.F_Id as yaoy_id, -- 邀约ID
903 905 yaoy.F_CreateTime as yaoy_time, -- 邀约时间
... ... @@ -936,6 +938,7 @@ namespace NCC.Extend.LqTkjlb
936 938 ELSE '未开卡'
937 939 END as billing_status -- 开卡状态描述
938 940 FROM lq_tkjlb tk
  941 + LEFT JOIN BASE_USER expansion_user ON tk.F_ExpansionUserId = expansion_user.F_Id
939 942 LEFT JOIN lq_yaoyjl yaoy ON tk.F_MemberId = yaoy.yykh
940 943 AND yaoy.F_StoreId = tk.F_StoreId
941 944 LEFT JOIN lq_yyjl yy ON tk.F_MemberId = yy.gk
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqXmzlService.cs
... ... @@ -22,6 +22,7 @@ using NCC.Extend.Entitys.lq_hytk_mx;
22 22 using NCC.Extend.Entitys.lq_kd_kdjlb;
23 23 using NCC.Extend.Entitys.lq_xh_hyhk;
24 24 using NCC.Extend.Entitys.lq_hytk_hytk;
  25 +using NCC.Extend.Entitys.lq_kd_deductinfo;
25 26 using Yitter.IdGenerator;
26 27 using NCC.Common.Helper;
27 28 using NCC.JsonSerialization;
... ... @@ -421,8 +422,8 @@ namespace NCC.Extend.LqXmzl
421 422 /// 品项维度统计
422 423 /// </summary>
423 424 /// <remarks>
424   - /// 按品项维度统计开卡、消耗、退卡等数据
425   - /// 包括业绩、人数、占比、复购率等指标
  425 + /// 按品项维度统计开卡、消耗、退卡、储扣等数据
  426 + /// 包括业绩、人数、占比、复购率、储扣次数及金额等指标
426 427 ///
427 428 /// 示例请求:
428 429 /// ```json
... ... @@ -430,7 +431,7 @@ namespace NCC.Extend.LqXmzl
430 431 /// "startTime": "2024-01-01",
431 432 /// "endTime": "2024-12-31",
432 433 /// "storeId": "store001",
433   - /// "category": "美容",
  434 + /// "itemCategory": "科美",
434 435 /// "itemId": "item001"
435 436 /// }
436 437 /// ```
... ... @@ -439,8 +440,14 @@ namespace NCC.Extend.LqXmzl
439 440 /// - startTime: 开始时间(可选)
440 441 /// - endTime: 结束时间(可选)
441 442 /// - storeId: 门店ID(可选)
442   - /// - category: 品项分类(可选)
  443 + /// - category: 品项分类(可选,已废弃,建议使用itemCategory)
  444 + /// - itemCategory: 品项分类筛选(可选,支持:科美、医美、生美、产品等,对应lq_xmzl表的qt2字段)
443 445 /// - itemId: 品项ID(可选,单个品项统计)
  446 + ///
  447 + /// 返回字段说明:
  448 + /// - ItemCategory: 品项分类(科美、医美、生美、产品等)
  449 + /// - DeductAmount: 储扣金额
  450 + /// - DeductCount: 储扣次数
444 451 /// </remarks>
445 452 /// <param name="input">统计输入参数</param>
446 453 /// <returns>品项维度统计数据</returns>
... ... @@ -452,7 +459,7 @@ namespace NCC.Extend.LqXmzl
452 459 {
453 460 try
454 461 {
455   - // 第一步:获取品项基础信息
  462 + // 第一步:获取品项基础信息(优化:一次性查询,包含分类字段)
456 463 var itemsQuery = _db.Queryable<LqXmzlEntity>()
457 464 .Where(x => x.IsEffective == 1);
458 465  
... ... @@ -461,12 +468,19 @@ namespace NCC.Extend.LqXmzl
461 468 itemsQuery = itemsQuery.Where(x => x.Id == input.ItemId);
462 469 }
463 470  
  471 + // 兼容旧的Category字段
464 472 if (!string.IsNullOrEmpty(input.Category))
465 473 {
466 474 itemsQuery = itemsQuery.Where(x => x.Fl1 == input.Category || x.Fl2 == input.Category || x.Fl == input.Category);
467 475 }
468 476  
469   - var items = await itemsQuery.ToListAsync();
  477 + // 新增:按品项分类筛选(qt2字段:科美、医美、生美、产品等)
  478 + if (!string.IsNullOrEmpty(input.ItemCategory))
  479 + {
  480 + itemsQuery = itemsQuery.Where(x => x.Qt2 == input.ItemCategory);
  481 + }
  482 +
  483 + var items = await itemsQuery.Select(x => new { x.Id, x.Xmmc, x.Xmbh, x.Qt2 }).ToListAsync();
470 484  
471 485 if (!items.Any())
472 486 {
... ... @@ -480,6 +494,9 @@ namespace NCC.Extend.LqXmzl
480 494  
481 495 var itemIds = items.Select(x => x.Id).ToList();
482 496  
  497 + // 创建品项信息字典,便于后续查找
  498 + var itemInfoDict = items.ToDictionary(x => x.Id, x => new { x.Xmmc, x.Xmbh, x.Qt2 });
  499 +
483 500 // 第二步:开卡数据统计
484 501 var billingStats = await GetBillingStatistics(itemIds, input);
485 502  
... ... @@ -489,25 +506,37 @@ namespace NCC.Extend.LqXmzl
489 506 // 第四步:退卡数据统计
490 507 var refundStats = await GetRefundStatistics(itemIds, input);
491 508  
492   - // 第五步:计算总数据用于占比计算
  509 + // 第五步:储扣数据统计(新增)
  510 + var deductStats = await GetDeductStatistics(itemIds, input);
  511 +
  512 + // 第六步:计算总数据用于占比计算
493 513 var totalBillingAmount = billingStats.Sum(x => x.BillingAmount);
494 514 var totalConsumeAmount = consumeStats.Sum(x => x.ConsumeAmount);
495 515 var totalBuyers = billingStats.Sum(x => x.TotalBuyers);
496 516  
497   - // 第六步:合并数据并计算占比
  517 + // 第七步:合并数据并计算占比(优化:使用字典提升查找效率)
  518 + var billingDict = billingStats.ToDictionary(x => x.ItemId);
  519 + var consumeDict = consumeStats.ToDictionary(x => x.ItemId);
  520 + var refundDict = refundStats.ToDictionary(x => x.ItemId);
  521 + var deductDict = deductStats.ToDictionary(x => x.ItemId);
  522 +
498 523 var result = new List<LqXmzlStatisticsOutput>();
499 524  
500 525 foreach (var item in items)
501 526 {
502   - var billingData = billingStats.FirstOrDefault(x => x.ItemId == item.Id);
503   - var consumeData = consumeStats.FirstOrDefault(x => x.ItemId == item.Id);
504   - var refundData = refundStats.FirstOrDefault(x => x.ItemId == item.Id);
  527 + billingDict.TryGetValue(item.Id, out var billingData);
  528 + consumeDict.TryGetValue(item.Id, out var consumeData);
  529 + refundDict.TryGetValue(item.Id, out var refundData);
  530 + deductDict.TryGetValue(item.Id, out var deductData);
  531 +
  532 + var itemInfo = itemInfoDict[item.Id];
505 533  
506 534 var output = new LqXmzlStatisticsOutput
507 535 {
508 536 ItemId = item.Id,
509   - ItemName = item.Xmmc,
510   - ItemNumber = item.Xmbh,
  537 + ItemName = itemInfo.Xmmc,
  538 + ItemNumber = itemInfo.Xmbh,
  539 + ItemCategory = itemInfo.Qt2 ?? "", // 显示品项分类
511 540 BillingAmount = billingData?.BillingAmount ?? 0,
512 541 BillingAmountRatio = totalBillingAmount > 0 ? (billingData?.BillingAmount ?? 0) / totalBillingAmount : 0,
513 542 TotalBuyers = billingData?.TotalBuyers ?? 0,
... ... @@ -520,7 +549,9 @@ namespace NCC.Extend.LqXmzl
520 549 ConsumeGiftCount = consumeData?.ConsumeGiftCount ?? 0,
521 550 ConsumeExperienceCount = consumeData?.ConsumeExperienceCount ?? 0,
522 551 RefundAmount = refundData?.RefundAmount ?? 0,
523   - RefundCount = refundData?.RefundCount ?? 0
  552 + RefundCount = refundData?.RefundCount ?? 0,
  553 + DeductAmount = deductData?.DeductAmount ?? 0, // 新增:储扣金额
  554 + DeductCount = deductData?.DeductCount ?? 0 // 新增:储扣次数
524 555 };
525 556 result.Add(output);
526 557 }
... ... @@ -575,34 +606,47 @@ namespace NCC.Extend.LqXmzl
575 606 })
576 607 .ToListAsync();
577 608  
578   - // 单独计算复购人数
579   - foreach (var item in result)
  609 + // 优化:批量计算复购人数,避免循环查询
  610 + if (result.Any())
580 611 {
581   - var memberCountQuery = _db.Queryable<LqKdPxmxEntity, LqKdKdjlbEntity>((px, kd) => new JoinQueryInfos(
  612 + var resultItemIds = result.Select(x => x.ItemId).ToList();
  613 +
  614 + // 一次性查询所有品项的复购人数
  615 + var repeatBuyerQuery = _db.Queryable<LqKdPxmxEntity, LqKdKdjlbEntity>((px, kd) => new JoinQueryInfos(
582 616 JoinType.Inner, px.Glkdbh == kd.Id))
583   - .Where((px, kd) => px.Px == item.ItemId && px.IsEffective == 1 && kd.IsEffective == 1);
  617 + .Where((px, kd) => resultItemIds.Contains(px.Px) && px.IsEffective == 1 && kd.IsEffective == 1);
584 618  
585 619 if (input.StartTime.HasValue)
586 620 {
587   - memberCountQuery = memberCountQuery.Where((px, kd) => kd.Kdrq >= input.StartTime.Value);
  621 + repeatBuyerQuery = repeatBuyerQuery.Where((px, kd) => kd.Kdrq >= input.StartTime.Value);
588 622 }
589 623 if (input.EndTime.HasValue)
590 624 {
591   - memberCountQuery = memberCountQuery.Where((px, kd) => kd.Kdrq <= input.EndTime.Value);
  625 + repeatBuyerQuery = repeatBuyerQuery.Where((px, kd) => kd.Kdrq <= input.EndTime.Value);
592 626 }
593 627  
594 628 if (!string.IsNullOrEmpty(input.StoreId))
595 629 {
596   - memberCountQuery = memberCountQuery.Where((px, kd) => kd.Djmd == input.StoreId);
  630 + repeatBuyerQuery = repeatBuyerQuery.Where((px, kd) => kd.Djmd == input.StoreId);
597 631 }
598 632  
599   - var memberStats = await memberCountQuery
600   - .GroupBy((px, kd) => px.MemberId)
601   - .Having((px, kd) => SqlFunc.AggregateCount(px.MemberId) > 1)
602   - .Select((px, kd) => SqlFunc.AggregateCount(px.MemberId))
  633 + // 按品项和会员分组,统计每个会员购买次数
  634 + var memberPurchaseStats = await repeatBuyerQuery
  635 + .GroupBy((px, kd) => new { px.Px, px.MemberId })
  636 + .Select((px, kd) => new { px.Px, px.MemberId, PurchaseCount = SqlFunc.AggregateCount(px.MemberId) })
603 637 .ToListAsync();
604 638  
605   - item.RepeatBuyers = memberStats.Count;
  639 + // 统计每个品项的复购人数(购买次数>1的会员数)
  640 + var repeatBuyerDict = memberPurchaseStats
  641 + .Where(x => x.PurchaseCount > 1)
  642 + .GroupBy(x => x.Px)
  643 + .ToDictionary(g => g.Key, g => g.Count());
  644 +
  645 + // 填充复购人数
  646 + foreach (var item in result)
  647 + {
  648 + item.RepeatBuyers = repeatBuyerDict.ContainsKey(item.ItemId) ? repeatBuyerDict[item.ItemId] : 0;
  649 + }
606 650 }
607 651  
608 652 return result;
... ... @@ -689,6 +733,45 @@ namespace NCC.Extend.LqXmzl
689 733  
690 734 return result;
691 735 }
  736 +
  737 + /// <summary>
  738 + /// 获取储扣统计数据(新增)
  739 + /// </summary>
  740 + private async Task<List<ItemDeductStatisticsData>> GetDeductStatistics(List<string> itemIds, LqXmzlStatisticsInput input)
  741 + {
  742 + // 使用JOIN关联开单记录表,以便使用开单时间进行过滤
  743 + var query = _db.Queryable<LqKdDeductinfoEntity, LqKdKdjlbEntity>((deduct, kd) => new JoinQueryInfos(
  744 + JoinType.Inner, deduct.BillingId == kd.Id))
  745 + .Where((deduct, kd) => itemIds.Contains(deduct.ItemId) && deduct.IsEffective == 1 && kd.IsEffective == 1);
  746 +
  747 + // 时间过滤(使用开单时间)
  748 + if (input.StartTime.HasValue)
  749 + {
  750 + query = query.Where((deduct, kd) => (deduct.BillingTime ?? kd.Kdrq) >= input.StartTime.Value);
  751 + }
  752 + if (input.EndTime.HasValue)
  753 + {
  754 + query = query.Where((deduct, kd) => (deduct.BillingTime ?? kd.Kdrq) <= input.EndTime.Value);
  755 + }
  756 +
  757 + // 门店过滤
  758 + if (!string.IsNullOrEmpty(input.StoreId))
  759 + {
  760 + query = query.Where((deduct, kd) => kd.Djmd == input.StoreId);
  761 + }
  762 +
  763 + var result = await query
  764 + .GroupBy((deduct, kd) => deduct.ItemId)
  765 + .Select((deduct, kd) => new ItemDeductStatisticsData
  766 + {
  767 + ItemId = deduct.ItemId,
  768 + DeductAmount = SqlFunc.AggregateSum(deduct.Amount ?? 0),
  769 + DeductCount = SqlFunc.AggregateCount(deduct.Id)
  770 + })
  771 + .ToListAsync();
  772 +
  773 + return result;
  774 + }
692 775 #endregion
693 776  
694 777 #region 获取品项门店统计
... ... @@ -716,8 +799,12 @@ namespace NCC.Extend.LqXmzl
716 799 /// - StoreId: 门店ID
717 800 /// - StoreName: 门店名称
718 801 /// - BillingCount: 开单数(去重后的开单编号数量)
719   - /// - ProjectCount: 项目数(项目次数总和)
720   - /// - ActualAmount: 实付金额(实付金额总和)
  802 + /// - ProjectCount: 项目数(项目次数总和,已废弃,使用TotalProjectCount代替)
  803 + /// - TotalProjectCount: 总项目数(项目次数总和)
  804 + /// - PurchaseProjectCount: 购买项目数(来源类型为"购买"的项目次数总和)
  805 + /// - ExperienceProjectCount: 体验项目数(来源类型为"体验"的项目次数总和)
  806 + /// - GiftProjectCount: 赠送项目数(来源类型为"赠送"的项目次数总和)
  807 + /// - ActualAmount: 实付金额(实付金额总和,使用开单记录的sfyj字段)
721 808 /// - RefundAmount: 退款金额(退款金额总和)
722 809 /// </remarks>
723 810 /// <param name="input">查询参数</param>
... ... @@ -734,13 +821,18 @@ namespace NCC.Extend.LqXmzl
734 821 }
735 822  
736 823 // 查询开单统计数据(按门店分组)
  824 + // 修改:ActualAmount使用开单记录的实付金额(sfyj),而不是品项明细的实际价格
  825 + // 注意:需要先按开单去重,避免一个开单包含多个该品项时重复计算sfyj
  826 + // 新增:按来源类型(SourceType)拆分项目数统计
737 827 var billingSql = $@"
738 828 SELECT
739 829 store.F_Id as StoreId,
740 830 store.dm as StoreName,
741 831 COUNT(DISTINCT billing.F_Id) as BillingCount,
742   - COALESCE(SUM(pxmx.F_ProjectNumber), 0) as ProjectCount,
743   - COALESCE(SUM(pxmx.F_ActualPrice), 0) as ActualAmount
  832 + COALESCE(SUM(pxmx.F_ProjectNumber), 0) as TotalProjectCount,
  833 + COALESCE(SUM(CASE WHEN pxmx.F_SourceType = '购买' THEN pxmx.F_ProjectNumber ELSE 0 END), 0) as PurchaseProjectCount,
  834 + COALESCE(SUM(CASE WHEN pxmx.F_SourceType = '体验' THEN pxmx.F_ProjectNumber ELSE 0 END), 0) as ExperienceProjectCount,
  835 + COALESCE(SUM(CASE WHEN pxmx.F_SourceType = '赠送' THEN pxmx.F_ProjectNumber ELSE 0 END), 0) as GiftProjectCount
744 836 FROM lq_kd_pxmx pxmx
745 837 INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
746 838 INNER JOIN lq_mdxx store ON billing.djmd = store.F_Id
... ... @@ -751,6 +843,33 @@ namespace NCC.Extend.LqXmzl
751 843 AND billing.kdrq < '{input.EndTime.AddDays(1):yyyy-MM-dd} 00:00:00'
752 844 GROUP BY store.F_Id, store.dm";
753 845  
  846 + // 查询实付金额(按门店分组,使用开单的sfyj字段,去重开单ID)
  847 + var actualAmountSql = $@"
  848 + SELECT
  849 + billing.djmd as StoreId,
  850 + COALESCE(SUM(billing.sfyj), 0) as ActualAmount
  851 + FROM (
  852 + SELECT DISTINCT billing2.F_Id, billing2.djmd, billing2.sfyj
  853 + FROM lq_kd_kdjlb billing2
  854 + INNER JOIN lq_kd_pxmx pxmx2 ON billing2.F_Id = pxmx2.glkdbh
  855 + WHERE pxmx2.px = '{input.ItemId}'
  856 + AND pxmx2.F_IsEffective = 1
  857 + AND billing2.F_IsEffective = 1
  858 + AND billing2.kdrq >= '{input.StartTime:yyyy-MM-dd} 00:00:00'
  859 + AND billing2.kdrq < '{input.EndTime.AddDays(1):yyyy-MM-dd} 00:00:00'
  860 + ) billing
  861 + GROUP BY billing.djmd";
  862 +
  863 + var actualAmountData = await _db.Ado.SqlQueryAsync<dynamic>(actualAmountSql);
  864 +
  865 + // 创建实付金额字典
  866 + var actualAmountDict = actualAmountData
  867 + .Where(x => x.StoreId != null)
  868 + .ToDictionary(
  869 + x => x.StoreId.ToString(),
  870 + x => Convert.ToDecimal(x.ActualAmount ?? 0)
  871 + );
  872 +
754 873 var billingData = await _db.Ado.SqlQueryAsync<dynamic>(billingSql);
755 874  
756 875 // 查询退款统计数据(按门店分组)
... ... @@ -778,13 +897,19 @@ namespace NCC.Extend.LqXmzl
778 897 foreach (var item in billingData ?? Enumerable.Empty<dynamic>())
779 898 {
780 899 var storeId = item.StoreId.ToString();
  900 + // 从实付金额字典中获取该门店的实付金额
  901 + var actualAmount = actualAmountDict.ContainsKey(storeId) ? actualAmountDict[storeId] : 0;
781 902 resultDict[storeId] = new ItemStoreStatisticsOutput
782 903 {
783 904 StoreId = storeId,
784 905 StoreName = item.StoreName.ToString(),
785 906 BillingCount = Convert.ToInt32(item.BillingCount),
786   - ProjectCount = Convert.ToDecimal(item.ProjectCount),
787   - ActualAmount = Convert.ToDecimal(item.ActualAmount),
  907 + ProjectCount = Convert.ToDecimal(item.TotalProjectCount ?? item.ProjectCount ?? 0), // 保持兼容性
  908 + TotalProjectCount = Convert.ToDecimal(item.TotalProjectCount ?? 0),
  909 + PurchaseProjectCount = Convert.ToDecimal(item.PurchaseProjectCount ?? 0),
  910 + ExperienceProjectCount = Convert.ToDecimal(item.ExperienceProjectCount ?? 0),
  911 + GiftProjectCount = Convert.ToDecimal(item.GiftProjectCount ?? 0),
  912 + ActualAmount = actualAmount, // 使用开单记录的实付金额(sfyj)
788 913 RefundAmount = 0
789 914 };
790 915 }
... ... @@ -806,6 +931,10 @@ namespace NCC.Extend.LqXmzl
806 931 StoreName = item.StoreName.ToString(),
807 932 BillingCount = 0,
808 933 ProjectCount = 0,
  934 + TotalProjectCount = 0,
  935 + PurchaseProjectCount = 0,
  936 + ExperienceProjectCount = 0,
  937 + GiftProjectCount = 0,
809 938 ActualAmount = 0,
810 939 RefundAmount = Convert.ToDecimal(item.RefundAmount)
811 940 };
... ... @@ -851,4 +980,14 @@ namespace NCC.Extend.LqXmzl
851 980 public decimal RefundAmount { get; set; }
852 981 public int RefundCount { get; set; }
853 982 }
  983 +
  984 + /// <summary>
  985 + /// 品项储扣统计数据(内部类)
  986 + /// </summary>
  987 + public class ItemDeductStatisticsData
  988 + {
  989 + public string ItemId { get; set; }
  990 + public decimal DeductAmount { get; set; }
  991 + public int DeductCount { get; set; }
  992 + }
854 993 }
... ...
netcore/src/Modularity/Extend/NCC.Extend/NCC.Extend.csproj
... ... @@ -15,5 +15,6 @@
15 15 </ItemGroup>
16 16 <ItemGroup>
17 17 <PackageReference Include="EPPlus" Version="6.2.10" />
  18 + <PackageReference Include="Magicodes.IE.Excel" Version="2.7.4.5" />
18 19 </ItemGroup>
19 20 </Project>
... ...
sql/事业部总经理经理工资表新增毛利相关字段.sql 0 → 100644
  1 +-- ============================================
  2 +-- 事业部总经理/经理工资表新增毛利相关字段
  3 +-- 表名:lq_business_unit_manager_salary_statistics
  4 +-- 说明:为事业部总经理/经理工资计算添加毛利相关字段,用于计算基于毛利的提成
  5 +-- 执行时间:2025年
  6 +-- ============================================
  7 +
  8 +-- 1. 销售业绩(开单业绩-退款业绩)
  9 +ALTER TABLE lq_business_unit_manager_salary_statistics
  10 +ADD COLUMN F_SalesPerformance DECIMAL(18,2) DEFAULT 0.00 COMMENT '销售业绩(开单业绩-退款业绩)' AFTER F_StorePerformanceDetail;
  11 +
  12 +-- 2. 产品物料(仓库领用金额)
  13 +ALTER TABLE lq_business_unit_manager_salary_statistics
  14 +ADD COLUMN F_ProductMaterial DECIMAL(18,2) DEFAULT 0.00 COMMENT '产品物料(仓库领用金额,注意11月特殊规则:11月工资算10月数据)' AFTER F_SalesPerformance;
  15 +
  16 +-- 3. 合作项目成本
  17 +ALTER TABLE lq_business_unit_manager_salary_statistics
  18 +ADD COLUMN F_CooperationCost DECIMAL(18,2) DEFAULT 0.00 COMMENT '合作项目成本' AFTER F_ProductMaterial;
  19 +
  20 +-- 4. 店内支出
  21 +ALTER TABLE lq_business_unit_manager_salary_statistics
  22 +ADD COLUMN F_StoreExpense DECIMAL(18,2) DEFAULT 0.00 COMMENT '店内支出' AFTER F_CooperationCost;
  23 +
  24 +-- 5. 洗毛巾费用
  25 +ALTER TABLE lq_business_unit_manager_salary_statistics
  26 +ADD COLUMN F_LaundryCost DECIMAL(18,2) DEFAULT 0.00 COMMENT '洗毛巾费用(只统计送出的记录,F_FlowType = 0)' AFTER F_StoreExpense;
  27 +
  28 +-- 6. 毛利(销售业绩-产品物料-合作项目成本-店内支出-洗毛巾)
  29 +ALTER TABLE lq_business_unit_manager_salary_statistics
  30 +ADD COLUMN F_GrossProfit DECIMAL(18,2) DEFAULT 0.00 COMMENT '毛利(销售业绩-产品物料-合作项目成本-店内支出-洗毛巾)' AFTER F_LaundryCost;
  31 +
  32 +-- ============================================
  33 +-- 字段说明
  34 +-- ============================================
  35 +-- F_SalesPerformance: 销售业绩 = 开单业绩 - 退款业绩
  36 +-- F_ProductMaterial: 产品物料 = 仓库领用金额合计(注意11月特殊规则:11月工资算10月数据)
  37 +-- F_CooperationCost: 合作项目成本 = 合作成本表合计金额
  38 +-- F_StoreExpense: 店内支出 = 店内支出表合计金额
  39 +-- F_LaundryCost: 洗毛巾费用 = 送洗记录总费用(只统计送出的记录,F_FlowType = 0)
  40 +-- F_GrossProfit: 毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾
  41 +--
  42 +-- 重要说明:
  43 +-- 1. 提成计算基于毛利,而不是开单业绩
  44 +-- 2. 必须满足提成阶梯1才能有提成资格
  45 +-- 3. 提成计算方式:分段累进式
  46 +
... ...
sql/年度经营数据菜单配置.sql 0 → 100644
  1 +-- 年度汇总表菜单配置脚本 (增强版:包含字段补全与管理员授权)
  2 +
  3 +SET @AdminRoleId = '94e3a9bb0fce4547886972998fddba1c'; -- 系统管理员角色ID
  4 +
  5 +-- 1. 清理旧数据 (防止重复执行报错)
  6 +DELETE FROM BASE_MODULE WHERE F_Id IN ('annual-summary-catalog', 'annual-summary-data', 'annual-summary-dashboard');
  7 +DELETE FROM BASE_AUTHORIZE WHERE F_ItemId IN ('annual-summary-catalog', 'annual-summary-data', 'annual-summary-dashboard');
  8 +
  9 +-- 2. 创建目录: 年度经营数据 (父级: 报表中心 725873504657868037)
  10 +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)
  11 +VALUES
  12 +('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());
  13 +
  14 +-- 3. 创建菜单: 汇总数据列表 (父级: 年度经营数据)
  15 +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)
  16 +VALUES
  17 +('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());
  18 +
  19 +-- 4. 创建菜单: 经营统计分析 (父级: 年度经营数据)
  20 +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)
  21 +VALUES
  22 +('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());
  23 +
  24 +-- 5. 授权给 系统管理员 角色
  25 +INSERT INTO BASE_AUTHORIZE (F_Id, F_ItemType, F_ItemId, F_ObjectType, F_ObjectId, F_SortCode, F_CreatorTime, F_CreatorUserId)
  26 +VALUES
  27 +(REPLACE(UUID(), '-', ''), 'module', 'annual-summary-catalog', 'Role', @AdminRoleId, 1, NOW(), 'admin'),
  28 +(REPLACE(UUID(), '-', ''), 'module', 'annual-summary-data', 'Role', @AdminRoleId, 2, NOW(), 'admin'),
  29 +(REPLACE(UUID(), '-', ''), 'module', 'annual-summary-dashboard', 'Role', @AdminRoleId, 3, NOW(), 'admin');
  30 +
... ...
sql/排查生美业绩统计差异-简化版.sql
... ... @@ -128,3 +128,6 @@ ORDER BY 生美业绩 DESC;
128 128  
129 129  
130 130  
  131 +
  132 +
  133 +
... ...
sql/排查生美业绩统计差异详细.sql
... ... @@ -180,3 +180,6 @@ HAVING COUNT(*) &gt; 1;
180 180  
181 181  
182 182  
  183 +
  184 +
  185 +
... ...
sql/检查生美业绩统计差异.sql
... ... @@ -150,3 +150,6 @@ ORDER BY 生美业绩 DESC;
150 150  
151 151  
152 152  
  153 +
  154 +
  155 +
... ...
test_deduct_list.sh 0 → 100755
  1 +#!/bin/bash
  2 +
  3 +# 测试储扣列表接口
  4 +
  5 +echo "=== 测试储扣列表接口 ==="
  6 +
  7 +# 获取Token
  8 +TOKEN=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \
  9 + -H "Content-Type: application/x-www-form-urlencoded" \
  10 + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e" | python3 -c "import sys, json; print(json.load(sys.stdin)['data']['token'])" 2>/dev/null)
  11 +
  12 +if [ -z "$TOKEN" ]; then
  13 + echo "❌ 获取Token失败"
  14 + exit 1
  15 +fi
  16 +
  17 +echo "✅ Token获取成功"
  18 +echo ""
  19 +
  20 +# 测试1: 基础查询
  21 +echo "--- 测试1: 基础查询(第1页,每页5条) ---"
  22 +RESPONSE1=$(curl -s -w "\nTIME:%{time_total}" -X GET "http://localhost:2011/api/Extend/lqkdkdjlb/deduct-list?currentPage=1&pageSize=5" \
  23 + -H "Authorization: $TOKEN")
  24 +TIME1=$(echo "$RESPONSE1" | grep "TIME:" | cut -d: -f2)
  25 +RESPONSE_BODY1=$(echo "$RESPONSE1" | sed '/TIME:/d')
  26 +echo "响应时间: ${TIME1}秒"
  27 +echo "$RESPONSE_BODY1" | python3 -c "
  28 +import sys, json
  29 +try:
  30 + data = json.load(sys.stdin)
  31 + if data.get('code') == 200:
  32 + result = data.get('data', {})
  33 + if 'list' in result:
  34 + print(f'✅ 接口调用成功')
  35 + print(f'当前页记录数: {len(result.get(\"list\", []))}')
  36 + print(f'总记录数: {result.get(\"pagination\", {}).get(\"total\", 0)}')
  37 + if 'statistics' in result:
  38 + stats = result['statistics']
  39 + print(f'统计信息:')
  40 + print(f' - 总记录数: {stats.get(\"totalCount\", 0)}')
  41 + print(f' - 总金额: {stats.get(\"totalAmount\", 0):,.2f}')
  42 + print(f' - 总项目数: {stats.get(\"totalProjectNumber\", 0)}')
  43 + else:
  44 + print('❌ 缺少统计信息')
  45 + else:
  46 + print('❌ 返回结构不正确')
  47 + print('返回的keys:', list(result.keys()) if isinstance(result, dict) else 'N/A')
  48 + else:
  49 + print(f'❌ 接口返回错误: {data.get(\"msg\", \"未知错误\")}')
  50 +except Exception as e:
  51 + print(f'❌ 解析响应失败: {e}')
  52 + import traceback
  53 + traceback.print_exc()
  54 +" 2>&1
  55 +echo ""
  56 +
  57 +echo "=== 测试完成 ==="
... ...
test_gold_triangle.sh 0 → 100755
  1 +#!/bin/bash
  2 +
  3 +# 测试金三角名称和队伍业绩占比
  4 +
  5 +echo "=== 测试金三角名称和队伍业绩占比 ==="
  6 +echo ""
  7 +
  8 +# 1. 获取token
  9 +TOKEN_RESPONSE=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \
  10 + -H "Content-Type: application/x-www-form-urlencoded" \
  11 + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e")
  12 +
  13 +TOKEN=$(echo "$TOKEN_RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data['data']['token'])" 2>/dev/null)
  14 +
  15 +if [ -z "$TOKEN" ]; then
  16 + echo "❌ Token获取失败"
  17 + exit 1
  18 +fi
  19 +
  20 +echo "✅ Token获取成功"
  21 +echo ""
  22 +
  23 +# 2. 查询有金三角的健康师
  24 +echo "=== 查询有金三角的健康师(冷忠翠)==="
  25 +RESPONSE=$(curl -s -X GET "http://localhost:2011/api/Extend/lqkdkdjlb/get-health-coach-statistics?startTime=2025-12-01&endTime=2025-12-31&employeeName=冷忠翠&currentPage=1&pageSize=5" \
  26 + -H "Authorization: $TOKEN")
  27 +
  28 +echo "$RESPONSE" | python3 -c "
  29 +import sys, json
  30 +try:
  31 + data = json.load(sys.stdin)
  32 + items = data.get('data', {}).get('list', [])
  33 + print(f'查询到 {len(items)} 条记录')
  34 + print('')
  35 + for i, item in enumerate(items, 1):
  36 + print(f'{i}. 健康师: {item.get(\"employeeName\", \"N/A\")}')
  37 + print(f' 门店: {item.get(\"storeName\", \"N/A\")}')
  38 + print(f' 金三角名称: {item.get(\"goldTriangleName\", \"无\")}')
  39 + print(f' 队伍业绩占比: {item.get(\"teamPerformanceRatio\", 0)}%')
  40 + print(f' 开单金额: {item.get(\"billingAmount\", 0)}')
  41 + print('')
  42 +except Exception as e:
  43 + print(f'解析错误: {e}')
  44 + print('原始响应:')
  45 + print(sys.stdin.read())
  46 +" 2>/dev/null
  47 +
... ...
test_health_coach_statistics.sh 0 → 100755
  1 +#!/bin/bash
  2 +
  3 +# 测试健康师统计接口
  4 +
  5 +echo "=== 测试健康师统计接口 ==="
  6 +echo ""
  7 +
  8 +# 1. 获取token
  9 +echo "=== 1. 获取Token ==="
  10 +TOKEN_RESPONSE=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \
  11 + -H "Content-Type: application/x-www-form-urlencoded" \
  12 + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e")
  13 +
  14 +TOKEN=$(echo "$TOKEN_RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data['data']['token'])" 2>/dev/null)
  15 +
  16 +if [ -z "$TOKEN" ]; then
  17 + echo "❌ Token获取失败"
  18 + echo "$TOKEN_RESPONSE"
  19 + exit 1
  20 +fi
  21 +
  22 +echo "✅ Token获取成功"
  23 +echo ""
  24 +
  25 +# 2. 测试接口 - 查询2025年12月的数据
  26 +echo "=== 2. 测试接口 - 查询2025年12月的数据 ==="
  27 +START_TIME=$(date +%s%N)
  28 +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&currentPage=1&pageSize=10" \
  29 + -H "Authorization: $TOKEN")
  30 +END_TIME=$(date +%s%N)
  31 +
  32 +HTTP_CODE=$(echo "$RESPONSE" | tail -2 | head -1)
  33 +TIME_TOTAL=$(echo "$RESPONSE" | tail -1)
  34 +RESPONSE_BODY=$(echo "$RESPONSE" | sed '$d' | sed '$d')
  35 +
  36 +echo "HTTP状态码: $HTTP_CODE"
  37 +echo "响应时间: ${TIME_TOTAL}秒"
  38 +ELAPSED_MS=$((($END_TIME - $START_TIME) / 1000000))
  39 +echo "总耗时: ${ELAPSED_MS}毫秒"
  40 +echo ""
  41 +
  42 +# 检查返回结果
  43 +if [ "$HTTP_CODE" = "200" ]; then
  44 + CODE=$(echo "$RESPONSE_BODY" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('code', ''))" 2>/dev/null)
  45 + if [ "$CODE" = "200" ]; then
  46 + echo "✅ 接口调用成功"
  47 + 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)
  48 + 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)
  49 + echo "返回数据条数: $LIST_COUNT"
  50 + echo "总记录数: $TOTAL"
  51 +
  52 + # 显示前3条数据的详细信息
  53 + echo ""
  54 + echo "前3条数据的详细信息:"
  55 + echo "$RESPONSE_BODY" | python3 -c "
  56 +import sys, json
  57 +try:
  58 + data = json.load(sys.stdin)
  59 + items = data.get('data', {}).get('list', [])[:3]
  60 + for i, item in enumerate(items, 1):
  61 + print(f\"{i}. {item.get('employeeName', 'N/A')}: 到店人数={item.get('visitCount', 0)}, 预约人数={item.get('appointmentCount', 0)}, 邀约人数={item.get('inviteCount', 0)}\")
  62 + print(f\" 金三角名称: {item.get('goldTriangleName', '无')}, 队伍业绩占比: {item.get('teamPerformanceRatio', 0)}%\")
  63 + print(f\" 开单金额: {item.get('billingAmount', 0)}\")
  64 +except Exception as e:
  65 + print(f'解析错误: {e}')
  66 +" 2>/dev/null
  67 +
  68 + # 检查是否有到店人数大于0的记录
  69 + HAS_VISIT=$(echo "$RESPONSE_BODY" | python3 -c "
  70 +import sys, json
  71 +try:
  72 + data = json.load(sys.stdin)
  73 + items = data.get('data', {}).get('list', [])
  74 + has_visit = any(item.get('visitCount', 0) > 0 for item in items)
  75 + print('1' if has_visit else '0')
  76 +except:
  77 + print('0')
  78 +" 2>/dev/null)
  79 +
  80 + if [ "$HAS_VISIT" = "1" ]; then
  81 + echo ""
  82 + echo "✅ 有到店人数大于0的记录"
  83 + else
  84 + echo ""
  85 + echo "⚠️ 所有记录的到店人数都是0,可能存在问题"
  86 + fi
  87 + else
  88 + echo "❌ 接口返回错误"
  89 + echo "$RESPONSE_BODY" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE_BODY"
  90 + fi
  91 +else
  92 + echo "❌ HTTP请求失败"
  93 + echo "$RESPONSE_BODY"
  94 +fi
  95 +
... ...
test_item_statistics.sh 0 → 100755
  1 +#!/bin/bash
  2 +
  3 +# 测试品项维度统计接口
  4 +
  5 +echo "=== 测试品项维度统计接口 ==="
  6 +
  7 +# 获取Token
  8 +TOKEN=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \
  9 + -H "Content-Type: application/x-www-form-urlencoded" \
  10 + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e" | python3 -c "import sys, json; print(json.load(sys.stdin)['data']['token'])" 2>/dev/null)
  11 +
  12 +if [ -z "$TOKEN" ]; then
  13 + echo "❌ 获取Token失败"
  14 + exit 1
  15 +fi
  16 +
  17 +echo "✅ Token获取成功"
  18 +echo ""
  19 +
  20 +# 测试1: 基础查询(2025年12月)
  21 +echo "--- 测试1: 基础查询(2025年12月,前5条) ---"
  22 +RESPONSE1=$(curl -s -w "\nTIME:%{time_total}" -X POST "http://localhost:2011/api/Extend/lqxmzl/GetItemStatistics" \
  23 + -H "Authorization: $TOKEN" \
  24 + -H "Content-Type: application/json" \
  25 + -d '{
  26 + "startTime": "2025-12-01T00:00:00",
  27 + "endTime": "2025-12-31T23:59:59"
  28 + }')
  29 +TIME1=$(echo "$RESPONSE1" | grep "TIME:" | cut -d: -f2)
  30 +RESPONSE_BODY1=$(echo "$RESPONSE1" | sed '/TIME:/d')
  31 +COUNT1=$(echo "$RESPONSE_BODY1" | python3 -c "import sys, json; data=json.load(sys.stdin); print(len(data.get('data', [])))" 2>/dev/null)
  32 +echo "响应时间: ${TIME1}秒"
  33 +echo "返回记录数: $COUNT1"
  34 +echo "前3条记录:"
  35 +echo "$RESPONSE_BODY1" | python3 -c "
  36 +import sys, json
  37 +data = json.load(sys.stdin)
  38 +items = data.get('data', [])[:3]
  39 +for i, item in enumerate(items, 1):
  40 + print(f'{i}. {item.get(\"itemName\", \"无\")} - 分类: {item.get(\"itemCategory\", \"无\")}, 开卡业绩: {item.get(\"billingAmount\", 0):,.2f}, 储扣金额: {item.get(\"deductAmount\", 0):,.2f}, 储扣次数: {item.get(\"deductCount\", 0)}')
  41 +" 2>/dev/null
  42 +echo ""
  43 +
  44 +# 测试2: 按分类筛选(科美)
  45 +echo "--- 测试2: 按分类筛选(科美) ---"
  46 +RESPONSE2=$(curl -s -w "\nTIME:%{time_total}" -X POST "http://localhost:2011/api/Extend/lqxmzl/GetItemStatistics" \
  47 + -H "Authorization: $TOKEN" \
  48 + -H "Content-Type: application/json" \
  49 + -d '{
  50 + "startTime": "2025-12-01T00:00:00",
  51 + "endTime": "2025-12-31T23:59:59",
  52 + "itemCategory": "科美"
  53 + }')
  54 +TIME2=$(echo "$RESPONSE2" | grep "TIME:" | cut -d: -f2)
  55 +RESPONSE_BODY2=$(echo "$RESPONSE2" | sed '/TIME:/d')
  56 +COUNT2=$(echo "$RESPONSE_BODY2" | python3 -c "import sys, json; data=json.load(sys.stdin); print(len(data.get('data', [])))" 2>/dev/null)
  57 +echo "响应时间: ${TIME2}秒"
  58 +echo "返回记录数: $COUNT2"
  59 +echo "前3条记录:"
  60 +echo "$RESPONSE_BODY2" | python3 -c "
  61 +import sys, json
  62 +data = json.load(sys.stdin)
  63 +items = data.get('data', [])[:3]
  64 +for i, item in enumerate(items, 1):
  65 + print(f'{i}. {item.get(\"itemName\", \"无\")} - 分类: {item.get(\"itemCategory\", \"无\")}, 储扣金额: {item.get(\"deductAmount\", 0):,.2f}, 储扣次数: {item.get(\"deductCount\", 0)}')
  66 +" 2>/dev/null
  67 +echo ""
  68 +
  69 +# 测试3: 按分类筛选(医美)
  70 +echo "--- 测试3: 按分类筛选(医美) ---"
  71 +RESPONSE3=$(curl -s -w "\nTIME:%{time_total}" -X POST "http://localhost:2011/api/Extend/lqxmzl/GetItemStatistics" \
  72 + -H "Authorization: $TOKEN" \
  73 + -H "Content-Type: application/json" \
  74 + -d '{
  75 + "startTime": "2025-12-01T00:00:00",
  76 + "endTime": "2025-12-31T23:59:59",
  77 + "itemCategory": "医美"
  78 + }')
  79 +TIME3=$(echo "$RESPONSE3" | grep "TIME:" | cut -d: -f2)
  80 +RESPONSE_BODY3=$(echo "$RESPONSE3" | sed '/TIME:/d')
  81 +COUNT3=$(echo "$RESPONSE_BODY3" | python3 -c "import sys, json; data=json.load(sys.stdin); print(len(data.get('data', [])))" 2>/dev/null)
  82 +echo "响应时间: ${TIME3}秒"
  83 +echo "返回记录数: $COUNT3"
  84 +echo ""
  85 +
  86 +# 测试4: 验证数据完整性
  87 +echo "--- 测试4: 验证数据完整性 ---"
  88 +echo "$RESPONSE_BODY1" | python3 -c "
  89 +import sys, json
  90 +data = json.load(sys.stdin)
  91 +items = data.get('data', [])[:5]
  92 +print('数据完整性检查:')
  93 +for item in items:
  94 + has_category = 'itemCategory' in item and item['itemCategory'] is not None
  95 + has_deduct_amount = 'deductAmount' in item
  96 + has_deduct_count = 'deductCount' in item
  97 + status = '✅' if (has_category and has_deduct_amount and has_deduct_count) else '❌'
  98 + print(f'{status} {item.get(\"itemName\", \"无\")}: 分类={has_category}, 储扣金额={has_deduct_amount}, 储扣次数={has_deduct_count}')
  99 +" 2>/dev/null
  100 +
  101 +echo ""
  102 +echo "=== 测试完成 ==="
  103 +echo "性能总结:"
  104 +echo "- 基础查询: ${TIME1}秒"
  105 +echo "- 科美筛选: ${TIME2}秒"
  106 +echo "- 医美筛选: ${TIME3}秒"
  107 +
... ...
test_personal_performance_api.sh 0 → 100755
  1 +#!/bin/bash
  2 +
  3 +# 测试个人业绩统计接口性能和返回数据
  4 +
  5 +echo "=== 测试个人业绩统计接口 ==="
  6 +echo ""
  7 +
  8 +# 1. 获取token
  9 +echo "=== 1. 获取Token ==="
  10 +TOKEN_RESPONSE=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \
  11 + -H "Content-Type: application/x-www-form-urlencoded" \
  12 + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e")
  13 +
  14 +TOKEN=$(echo "$TOKEN_RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data['data']['token'])" 2>/dev/null)
  15 +
  16 +if [ -z "$TOKEN" ]; then
  17 + echo "❌ Token获取失败"
  18 + echo "$TOKEN_RESPONSE"
  19 + exit 1
  20 +fi
  21 +
  22 +echo "✅ Token获取成功"
  23 +echo ""
  24 +
  25 +# 2. 测试接口 - 无筛选条件,第一页
  26 +echo "=== 2. 测试接口 - 无筛选条件,第一页(20条)==="
  27 +START_TIME=$(date +%s%N)
  28 +RESPONSE=$(curl -s -w "\n%{http_code}\n%{time_total}" -X POST "http://localhost:2011/api/Extend/LqStatistics/get-personal-performance-statistics-list" \
  29 + -H "Authorization: $TOKEN" \
  30 + -H "Content-Type: application/json" \
  31 + -d '{
  32 + "statisticsMonth": "202512",
  33 + "currentPage": 1,
  34 + "pageSize": 20
  35 + }')
  36 +END_TIME=$(date +%s%N)
  37 +
  38 +HTTP_CODE=$(echo "$RESPONSE" | tail -2 | head -1)
  39 +TIME_TOTAL=$(echo "$RESPONSE" | tail -1)
  40 +RESPONSE_BODY=$(echo "$RESPONSE" | sed '$d' | sed '$d')
  41 +
  42 +echo "HTTP状态码: $HTTP_CODE"
  43 +echo "响应时间: ${TIME_TOTAL}秒"
  44 +ELAPSED_MS=$((($END_TIME - $START_TIME) / 1000000))
  45 +echo "总耗时: ${ELAPSED_MS}毫秒"
  46 +echo ""
  47 +
  48 +# 检查返回结果
  49 +if [ "$HTTP_CODE" = "200" ]; then
  50 + CODE=$(echo "$RESPONSE_BODY" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('code', ''))" 2>/dev/null)
  51 + if [ "$CODE" = "200" ]; then
  52 + echo "✅ 接口调用成功"
  53 + 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)
  54 + 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)
  55 + echo "返回数据条数: $LIST_COUNT"
  56 + echo "总记录数: $TOTAL"
  57 +
  58 + # 显示第一条数据示例
  59 + echo ""
  60 + echo "第一条数据示例:"
  61 + 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
  62 + else
  63 + echo "❌ 接口返回错误"
  64 + echo "$RESPONSE_BODY" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE_BODY"
  65 + fi
  66 +else
  67 + echo "❌ HTTP请求失败"
  68 + echo "$RESPONSE_BODY"
  69 +fi
  70 +
  71 +echo ""
  72 +echo ""
  73 +
  74 +# 3. 测试接口 - 带门店筛选
  75 +echo "=== 3. 测试接口 - 带门店筛选 ==="
  76 +START_TIME=$(date +%s%N)
  77 +RESPONSE2=$(curl -s -w "\n%{http_code}\n%{time_total}" -X POST "http://localhost:2011/api/Extend/LqStatistics/get-personal-performance-statistics-list" \
  78 + -H "Authorization: $TOKEN" \
  79 + -H "Content-Type: application/json" \
  80 + -d '{
  81 + "statisticsMonth": "202512",
  82 + "storeName": "测试",
  83 + "currentPage": 1,
  84 + "pageSize": 20
  85 + }')
  86 +END_TIME=$(date +%s%N)
  87 +
  88 +HTTP_CODE2=$(echo "$RESPONSE2" | tail -2 | head -1)
  89 +TIME_TOTAL2=$(echo "$RESPONSE2" | tail -1)
  90 +RESPONSE_BODY2=$(echo "$RESPONSE2" | sed '$d' | sed '$d')
  91 +
  92 +echo "HTTP状态码: $HTTP_CODE2"
  93 +echo "响应时间: ${TIME_TOTAL2}秒"
  94 +ELAPSED_MS2=$((($END_TIME - $START_TIME) / 1000000))
  95 +echo "总耗时: ${ELAPSED_MS2}毫秒"
  96 +
  97 +if [ "$HTTP_CODE2" = "200" ]; then
  98 + CODE2=$(echo "$RESPONSE_BODY2" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('code', ''))" 2>/dev/null)
  99 + if [ "$CODE2" = "200" ]; then
  100 + echo "✅ 接口调用成功"
  101 + 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)
  102 + 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)
  103 + echo "返回数据条数: $LIST_COUNT2"
  104 + echo "总记录数: $TOTAL2"
  105 + else
  106 + echo "❌ 接口返回错误"
  107 + echo "$RESPONSE_BODY2" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE_BODY2"
  108 + fi
  109 +fi
  110 +
  111 +echo ""
  112 +echo ""
  113 +
  114 +# 4. 性能评估
  115 +echo "=== 4. 性能评估 ==="
  116 +if [ -n "$ELAPSED_MS" ]; then
  117 + if [ "$ELAPSED_MS" -lt 1000 ]; then
  118 + echo "✅ 性能优秀 (< 1秒)"
  119 + elif [ "$ELAPSED_MS" -lt 3000 ]; then
  120 + echo "⚠️ 性能良好 (1-3秒)"
  121 + elif [ "$ELAPSED_MS" -lt 5000 ]; then
  122 + echo "⚠️ 性能一般 (3-5秒)"
  123 + else
  124 + echo "❌ 性能较差 (> 5秒),需要优化"
  125 + fi
  126 +fi
  127 +
... ...
test_store_customer_details.sh 0 → 100644
  1 +#!/bin/bash
  2 +
  3 +# 测试门店客户详情接口,验证拓客人员姓名
  4 +
  5 +echo "=== 测试门店客户详情接口 ==="
  6 +echo ""
  7 +
  8 +# 1. 获取token
  9 +TOKEN_RESPONSE=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \
  10 + -H "Content-Type: application/x-www-form-urlencoded" \
  11 + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e")
  12 +
  13 +TOKEN=$(echo "$TOKEN_RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data['data']['token'])" 2>/dev/null)
  14 +
  15 +if [ -z "$TOKEN" ]; then
  16 + echo "❌ Token获取失败"
  17 + exit 1
  18 +fi
  19 +
  20 +echo "✅ Token获取成功"
  21 +echo ""
  22 +
  23 +# 2. 查询门店客户详情(需要先获取一个有效的eventId和storeId)
  24 +echo "=== 查询门店客户详情 ==="
  25 +echo "提示: 需要提供有效的eventId和storeId参数"
  26 +echo ""
  27 +
  28 +# 先查询一个活动ID和门店ID
  29 +EVENT_RESPONSE=$(curl -s -X GET "http://localhost:2011/api/Extend/lqtkjlb" \
  30 + -H "Authorization: $TOKEN")
  31 +
  32 +echo "查询活动列表..."
  33 +echo "$EVENT_RESPONSE" | python3 -c "
  34 +import sys, json
  35 +try:
  36 + data = json.load(sys.stdin)
  37 + events = data.get('data', {}).get('list', [])[:3]
  38 + if events:
  39 + print('前3个活动:')
  40 + for i, event in enumerate(events, 1):
  41 + print(f'{i}. 活动ID: {event.get(\"id\", \"N/A\")}, 活动名称: {event.get(\"eventName\", \"N/A\")}')
  42 + else:
  43 + print('未找到活动数据')
  44 +except Exception as e:
  45 + print(f'解析错误: {e}')
  46 +" 2>/dev/null
  47 +
  48 +echo ""
  49 +echo "请手动测试接口,使用以下格式:"
  50 +echo "curl -X GET \"http://localhost:2011/api/Extend/lqtkjlb/GetStoreCustomerDetailsPaged/{eventId}/{storeId}?pageIndex=1&pageSize=10\" -H \"Authorization: \$TOKEN\""
  51 +
... ...
test_store_total_performance_performance.sh 0 → 100755
  1 +#!/bin/bash
  2 +
  3 +# 门店总业绩统计接口性能测试
  4 +
  5 +echo "=== 门店总业绩统计接口性能测试 ==="
  6 +
  7 +# 获取Token
  8 +TOKEN=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \
  9 + -H "Content-Type: application/x-www-form-urlencoded" \
  10 + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e" | python3 -c "import sys, json; print(json.load(sys.stdin)['data']['token'])" 2>/dev/null)
  11 +
  12 +if [ -z "$TOKEN" ]; then
  13 + echo "❌ 获取Token失败"
  14 + exit 1
  15 +fi
  16 +
  17 +echo "✅ Token获取成功"
  18 +echo ""
  19 +
  20 +# 测试1: 分页查询(每页10条)
  21 +echo "--- 测试1: 分页查询(每页10条) ---"
  22 +START=$(date +%s.%3N)
  23 +RESPONSE1=$(curl -s -w "\nTIME:%{time_total}" -X POST "http://localhost:2011/api/Extend/LqStatistics/get-store-total-performance-statistics-list" \
  24 + -H "Authorization: $TOKEN" \
  25 + -H "Content-Type: application/json" \
  26 + -d '{"statisticsMonth": "202512", "pageIndex": 1, "pageSize": 10}')
  27 +END=$(date +%s.%3N)
  28 +TIME1=$(echo "$RESPONSE1" | grep "TIME:" | cut -d: -f2)
  29 +RESPONSE_BODY1=$(echo "$RESPONSE1" | sed '/TIME:/d')
  30 +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)
  31 +echo "响应时间: ${TIME1}秒"
  32 +echo "总记录数: $TOTAL1"
  33 +echo ""
  34 +
  35 +# 测试2: 分页查询(每页50条)
  36 +echo "--- 测试2: 分页查询(每页50条) ---"
  37 +START=$(date +%s.%3N)
  38 +RESPONSE2=$(curl -s -w "\nTIME:%{time_total}" -X POST "http://localhost:2011/api/Extend/LqStatistics/get-store-total-performance-statistics-list" \
  39 + -H "Authorization: $TOKEN" \
  40 + -H "Content-Type: application/json" \
  41 + -d '{"statisticsMonth": "202512", "pageIndex": 1, "pageSize": 50}')
  42 +END=$(date +%s.%3N)
  43 +TIME2=$(echo "$RESPONSE2" | grep "TIME:" | cut -d: -f2)
  44 +RESPONSE_BODY2=$(echo "$RESPONSE2" | sed '/TIME:/d')
  45 +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)
  46 +echo "响应时间: ${TIME2}秒"
  47 +echo "总记录数: $TOTAL2"
  48 +echo ""
  49 +
  50 +# 测试3: 门店名称筛选
  51 +echo "--- 测试3: 门店名称筛选(搜索'紫荆') ---"
  52 +START=$(date +%s.%3N)
  53 +RESPONSE3=$(curl -s -w "\nTIME:%{time_total}" -X POST "http://localhost:2011/api/Extend/LqStatistics/get-store-total-performance-statistics-list" \
  54 + -H "Authorization: $TOKEN" \
  55 + -H "Content-Type: application/json" \
  56 + -d '{"statisticsMonth": "202512", "storeName": "紫荆", "pageIndex": 1, "pageSize": 10}')
  57 +END=$(date +%s.%3N)
  58 +TIME3=$(echo "$RESPONSE3" | grep "TIME:" | cut -d: -f2)
  59 +RESPONSE_BODY3=$(echo "$RESPONSE3" | sed '/TIME:/d')
  60 +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)
  61 +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)
  62 +echo "响应时间: ${TIME3}秒"
  63 +echo "总记录数: $TOTAL3"
  64 +echo "当前页记录数: $LIST_COUNT3"
  65 +echo ""
  66 +
  67 +# 测试4: 连续5次请求,测试稳定性
  68 +echo "--- 测试4: 连续5次请求,测试稳定性 ---"
  69 +TIMES=()
  70 +for i in {1..5}; do
  71 + START=$(date +%s.%3N)
  72 + RESPONSE=$(curl -s -w "\nTIME:%{time_total}" -X POST "http://localhost:2011/api/Extend/LqStatistics/get-store-total-performance-statistics-list" \
  73 + -H "Authorization: $TOKEN" \
  74 + -H "Content-Type: application/json" \
  75 + -d '{"statisticsMonth": "202512", "pageIndex": 1, "pageSize": 10}')
  76 + END=$(date +%s.%3N)
  77 + TIME=$(echo "$RESPONSE" | grep "TIME:" | cut -d: -f2)
  78 + TIMES+=($TIME)
  79 + echo "第${i}次请求: ${TIME}秒"
  80 +done
  81 +
  82 +# 计算平均时间
  83 +SUM=0
  84 +for t in "${TIMES[@]}"; do
  85 + SUM=$(echo "$SUM + $t" | bc)
  86 +done
  87 +AVG=$(echo "scale=3; $SUM / ${#TIMES[@]}" | bc)
  88 +echo "平均响应时间: ${AVG}秒"
  89 +echo ""
  90 +
  91 +# 测试5: 查询不同月份(2025年11月)
  92 +echo "--- 测试5: 查询2025年11月数据 ---"
  93 +START=$(date +%s.%3N)
  94 +RESPONSE5=$(curl -s -w "\nTIME:%{time_total}" -X POST "http://localhost:2011/api/Extend/LqStatistics/get-store-total-performance-statistics-list" \
  95 + -H "Authorization: $TOKEN" \
  96 + -H "Content-Type: application/json" \
  97 + -d '{"statisticsMonth": "202511", "pageIndex": 1, "pageSize": 10}')
  98 +END=$(date +%s.%3N)
  99 +TIME5=$(echo "$RESPONSE5" | grep "TIME:" | cut -d: -f2)
  100 +RESPONSE_BODY5=$(echo "$RESPONSE5" | sed '/TIME:/d')
  101 +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)
  102 +echo "响应时间: ${TIME5}秒"
  103 +echo "总记录数: $TOTAL5"
  104 +echo ""
  105 +
  106 +echo "=== 性能测试总结 ==="
  107 +echo "1. 分页查询(10条): ${TIME1}秒"
  108 +echo "2. 分页查询(50条): ${TIME2}秒"
  109 +echo "3. 门店名称筛选: ${TIME3}秒"
  110 +echo "4. 连续5次平均: ${AVG}秒"
  111 +echo "5. 不同月份查询: ${TIME5}秒"
  112 +echo ""
  113 +echo "✅ 所有测试完成"
  114 +
... ...
test_store_total_performance_statistics.sh 0 → 100755
  1 +#!/bin/bash
  2 +
  3 +# 测试门店总业绩统计接口(实时查询)
  4 +
  5 +echo "=== 测试门店总业绩统计接口(实时查询) ==="
  6 +
  7 +# 获取Token
  8 +TOKEN=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \
  9 + -H "Content-Type: application/x-www-form-urlencoded" \
  10 + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e" | python3 -c "import sys, json; print(json.load(sys.stdin)['data']['token'])" 2>/dev/null)
  11 +
  12 +if [ -z "$TOKEN" ]; then
  13 + echo "❌ 获取Token失败"
  14 + exit 1
  15 +fi
  16 +
  17 +echo "✅ Token获取成功"
  18 +
  19 +# 测试查询2025年12月的门店总业绩统计(带性能测试)
  20 +echo ""
  21 +echo "--- 测试查询2025年12月的门店总业绩统计(第1页,每页10条) ---"
  22 +echo "开始时间: $(date +%s.%3N)"
  23 +START_TIME=$(date +%s.%3N)
  24 +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" \
  25 + -H "Authorization: $TOKEN" \
  26 + -H "Content-Type: application/json" \
  27 + -d '{
  28 + "statisticsMonth": "202512",
  29 + "pageIndex": 1,
  30 + "pageSize": 10
  31 + }')
  32 +END_TIME=$(date +%s.%3N)
  33 +
  34 +# 提取HTTP状态码和响应时间
  35 +HTTP_CODE=$(echo "$RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2)
  36 +TIME_TOTAL=$(echo "$RESPONSE" | grep "TIME_TOTAL:" | cut -d: -f2)
  37 +RESPONSE_BODY=$(echo "$RESPONSE" | sed '/HTTP_CODE:/d' | sed '/TIME_TOTAL:/d')
  38 +
  39 +echo "响应时间: ${TIME_TOTAL}秒"
  40 +echo "HTTP状态码: $HTTP_CODE"
  41 +echo ""
  42 +echo "响应内容(前500字符):"
  43 +echo "$RESPONSE_BODY" | head -c 500
  44 +echo ""
  45 +echo "..."
  46 +
  47 +# 使用RESPONSE_BODY进行后续处理
  48 +RESPONSE="$RESPONSE_BODY"
  49 +
  50 +# 解析响应
  51 +CODE=$(echo "$RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('code', 0))" 2>/dev/null)
  52 +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)
  53 +
  54 +if [ "$CODE" = "200" ] && [ "$HAS_DATA" = "yes" ]; then
  55 + echo ""
  56 + echo "✅ 接口调用成功"
  57 +
  58 + # 显示统计信息
  59 + 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)
  60 + LIST_COUNT=$(echo "$RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(len(data.get('data', {}).get('list', [])))" 2>/dev/null)
  61 +
  62 + echo "总记录数: $TOTAL"
  63 + echo "当前页记录数: $LIST_COUNT"
  64 +
  65 + # 显示前3条记录的关键信息
  66 + echo ""
  67 + echo "前3条记录详情:"
  68 + echo "$RESPONSE" | python3 -c "
  69 +import sys, json
  70 +try:
  71 + data = json.load(sys.stdin)
  72 + items = data.get('data', {}).get('list', [])[:3]
  73 + if items:
  74 + for i, item in enumerate(items, 1):
  75 + store_name = item.get('StoreName', item.get('storeName', '无'))
  76 + total_perf = item.get('TotalPerformance', item.get('totalPerformance', 0))
  77 + actual_perf = item.get('ActualPerformance', item.get('actualPerformance', 0))
  78 + first_count = item.get('FirstOrderCount', item.get('firstOrderCount', 0))
  79 + upgrade_count = item.get('UpgradeOrderCount', item.get('upgradeOrderCount', 0))
  80 + print(f'{i}. 门店: {store_name}, 总业绩: {total_perf:,.2f}, 实际业绩: {actual_perf:,.2f}, 首开单: {first_count}, 升单: {upgrade_count}')
  81 + else:
  82 + print('未找到数据')
  83 +except Exception as e:
  84 + print(f'解析错误: {e}')
  85 +" 2>/dev/null
  86 +else
  87 + echo ""
  88 + echo "❌ 接口调用失败,返回码: $CODE"
  89 +fi
  90 +
  91 +echo ""
  92 +echo "=== 测试完成 ==="
  93 +
... ...
test_tianwang_api.py
... ... @@ -81,3 +81,6 @@ print(f&quot;\n教育一部+教育二部合计 BillingPerformance: {total2}&quot;)
81 81  
82 82  
83 83  
  84 +
  85 +
  86 +
... ...
verify_store_total_performance_data.sh 0 → 100755
  1 +#!/bin/bash
  2 +
  3 +# 验证门店总业绩统计数据准确性
  4 +
  5 +echo "=== 验证门店总业绩统计数据准确性 ==="
  6 +
  7 +# 获取Token
  8 +TOKEN=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \
  9 + -H "Content-Type: application/x-www-form-urlencoded" \
  10 + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e" | python3 -c "import sys, json; print(json.load(sys.stdin)['data']['token'])" 2>/dev/null)
  11 +
  12 +if [ -z "$TOKEN" ]; then
  13 + echo "❌ 获取Token失败"
  14 + exit 1
  15 +fi
  16 +
  17 +echo "✅ Token获取成功"
  18 +echo ""
  19 +
  20 +# 查询实时数据
  21 +echo "--- 查询实时数据(2025年12月,前5条) ---"
  22 +RESPONSE=$(curl -s -X POST "http://localhost:2011/api/Extend/LqStatistics/get-store-total-performance-statistics-list" \
  23 + -H "Authorization: $TOKEN" \
  24 + -H "Content-Type: application/json" \
  25 + -d '{"statisticsMonth": "202512", "pageIndex": 1, "pageSize": 5}')
  26 +
  27 +echo "$RESPONSE" | python3 -c "
  28 +import sys, json
  29 +data = json.load(sys.stdin)
  30 +items = data.get('data', {}).get('list', [])[:5]
  31 +print('实时查询结果(前5条):')
  32 +print('=' * 100)
  33 +for i, item in enumerate(items, 1):
  34 + print(f'{i}. {item.get(\"StoreName\", \"无\")}')
  35 + print(f' 总业绩: {item.get(\"TotalPerformance\", 0):,.2f}')
  36 + print(f' 总单业绩: {item.get(\"TotalOrderPerformance\", 0):,.2f}')
  37 + print(f' 实际业绩: {item.get(\"ActualPerformance\", 0):,.2f}')
  38 + print(f' 首开单: {item.get(\"FirstOrderCount\", 0)}, 升单: {item.get(\"UpgradeOrderCount\", 0)}')
  39 + print(f' 品项数量: {item.get(\"ItemQuantity\", 0)}')
  40 + print(f' 退款金额: {item.get(\"RefundAmount\", 0):,.2f}, 退款次数: {item.get(\"RefundCount\", 0)}')
  41 + print('')
  42 +" 2>/dev/null
  43 +
  44 +# 验证数据完整性
  45 +echo "--- 数据完整性验证 ---"
  46 +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)
  47 +LIST_COUNT=$(echo "$RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(len(data.get('data', {}).get('list', [])))" 2>/dev/null)
  48 +
  49 +echo "总记录数: $TOTAL"
  50 +echo "当前页记录数: $LIST_COUNT"
  51 +
  52 +# 检查数据字段完整性
  53 +echo ""
  54 +echo "--- 字段完整性检查 ---"
  55 +echo "$RESPONSE" | python3 -c "
  56 +import sys, json
  57 +data = json.load(sys.stdin)
  58 +items = data.get('data', {}).get('list', [])
  59 +if items:
  60 + first_item = items[0]
  61 + required_fields = ['StoreId', 'StoreName', 'StatisticsMonth', 'TotalPerformance',
  62 + 'TotalOrderPerformance', 'ActualPerformance', 'FirstOrderCount',
  63 + 'UpgradeOrderCount', 'ItemQuantity', 'RefundAmount', 'RefundCount']
  64 + missing_fields = []
  65 + for field in required_fields:
  66 + if field not in first_item:
  67 + missing_fields.append(field)
  68 + if missing_fields:
  69 + print(f'❌ 缺少字段: {missing_fields}')
  70 + else:
  71 + print('✅ 所有必需字段都存在')
  72 +
  73 + # 验证数据逻辑
  74 + print('')
  75 + print('--- 数据逻辑验证 ---')
  76 + for i, item in enumerate(items[:3], 1):
  77 + total_perf = item.get('TotalPerformance', 0)
  78 + total_order = item.get('TotalOrderPerformance', 0)
  79 + actual = item.get('ActualPerformance', 0)
  80 + refund = item.get('RefundAmount', 0)
  81 +
  82 + # 验证:实际业绩 = 总单业绩 - 退款金额
  83 + expected_actual = total_order - refund
  84 + if abs(actual - expected_actual) < 0.01:
  85 + print(f'{i}. {item.get(\"StoreName\", \"无\")}: ✅ 实际业绩计算正确')
  86 + else:
  87 + print(f'{i}. {item.get(\"StoreName\", \"无\")}: ❌ 实际业绩计算错误 (期望: {expected_actual:.2f}, 实际: {actual:.2f})')
  88 +else:
  89 + print('❌ 未找到数据')
  90 +" 2>/dev/null
  91 +
  92 +echo ""
  93 +echo "=== 验证完成 ==="
  94 +
... ...
事业部总经理经理工资计算规则梳理.md
... ... @@ -13,13 +13,14 @@
13 13  
14 14 事业部总经理/经理工资由以下几个部分组成:
15 15 1. **底薪**:固定4000元
16   -2. **提成**:根据管理的门店业绩,使用阶梯式提成计算(基于门店总业绩
  16 +2. **提成**:根据管理的门店毛利,使用分段累进式提成计算(基于门店毛利
17 17  
18 18 **重要说明**:
19 19 - 每个总经理/经理都会管理多个门店
20   -- 提成计算基于门店总业绩(开单业绩 - 退卡业绩)
  20 +- **提成计算基于门店毛利**,而不是开单业绩
  21 +- **必须满足提成阶梯1才能有提成资格**
21 22 - 总经理和经理的计算规则相同
22   -- 必须先达到门店生命线才能计算提成
  23 +- 提成计算方式:分段累进式
23 24  
24 25 ---
25 26  
... ... @@ -35,22 +36,17 @@
35 36  
36 37 ---
37 38  
38   -### 2. 提成规则(阶梯式)
  39 +### 2. 提成规则(分段累进式)
39 40  
40   -**提成计算方式**:根据管理的门店业绩,使用阶梯式提成计算
  41 +**提成计算方式**:根据管理的门店毛利,使用分段累进式提成计算
41 42  
42   -#### 2.1 提成门槛:门店生命线
  43 +#### 2.1 提成门槛
43 44  
44   -**重要概念**:门店生命线是提成的**门槛条件**,不是提成阶梯
  45 +**重要规则**:**必须满足提成阶梯1才能有提成资格**
45 46  
46   -- **数据来源**:`lq_md_target` 表的 `F_StoreLifeline` 字段
47   -- **判断条件**:
48   - - 如果门店业绩 < 门店生命线 → **无提成**
49   - - 如果门店业绩 ≥ 门店生命线 → **可以计算提成**
50   -
51   -**重要说明**:
52   -- 门店生命线是**必须设置的**,未设置应报错
53   -- 门店生命线是门店级别的指标,用于判断是否达到提成门槛
  47 +- 如果门店毛利 < 提成阶梯1,则无提成(提成 = 0)
  48 +- 如果门店毛利 ≥ 提成阶梯1,则可以计算提成
  49 +- 提成阶梯1是提成资格的门槛,不是提成计算的起点
54 50  
55 51 #### 2.2 提成阶梯设置
56 52  
... ... @@ -74,43 +70,62 @@
74 70 - **门店生命线**:判断是否达到提成门槛
75 71 - **提成阶梯**:计算提成金额的阶梯
76 72  
77   -#### 2.3 阶梯式提成计算规则
  73 +#### 2.3 毛利计算
  74 +
  75 +**核心公式**:
  76 +```
  77 +毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾
  78 +```
  79 +
  80 +其中:
  81 +- **销售业绩** = 开单业绩 - 退款业绩
  82 +- **产品物料** = 仓库领用金额合计(注意11月特殊规则:11月工资算10月数据)
  83 +- **合作项目成本** = 合作成本表合计金额
  84 +- **店内支出** = 店内支出表合计金额
  85 +- **洗毛巾** = 送洗记录总费用(只统计送出的记录,F_FlowType = 0)
  86 +
  87 +**重要说明**:
  88 +- **提成计算基于毛利**,而不是开单业绩
  89 +- 所有门店的毛利分别计算,然后汇总
  90 +
  91 +#### 2.4 分段累进式提成计算规则
78 92  
79   -**前提条件**:门店业绩必须 ≥ 门店生命线,否则无提成
  93 +**前提条件**:**必须满足提成阶梯1才能有提成资格**
80 94  
81   -**计算逻辑**:根据门店业绩落在哪个提成阶梯区间,使用对应的提成比例计算(分段累进)
  95 +- 如果门店毛利 < 提成阶梯1,则无提成(提成 = 0)
  96 +- 如果门店毛利 ≥ 提成阶梯1,则可以计算提成
  97 +
  98 +**计算逻辑**:根据门店毛利落在哪个提成阶梯区间,使用分段累进方式计算(不同区间按不同比例分别计算后累加)
82 99  
83 100 **示例**(假设某门店的设置):
84   -- 门店生命线(`lq_md_target.F_StoreLifeline`)= 300,000元
85   -- 提成阶梯1 = 350,000元,提成比例1 = 1.0%
  101 +- 提成阶梯1 = 150,000元,提成比例1 = 1.0%
86 102 - 提成阶梯2 = 400,000元,提成比例2 = 1.5%
87 103 - 提成阶梯3 = 450,000元,提成比例3 = 2.0%
88 104  
89   -**计算规则**(分段累进):
90   -
91   -1. **业绩 < 门店生命线**:
92   - - 提成 = 0(无提成)
93   - - 示例:业绩 = 280,000元 → 提成 = 0
  105 +**计算规则**(分段累进式):
94 106  
95   -2. **门店生命线 ≤ 业绩 ≤ 提成阶梯1**:
96   - - 提成 = 业绩 × 提成比例1
97   - - 示例:业绩 = 320,000元 → 提成 = 320,000 × 1.0% = 3,200元
  107 +1. **毛利 < 提成阶梯1**:
  108 + - 提成 = 0元(未达到提成资格)
  109 + - 示例:毛利 = 100,000元 → 提成 = 0元
98 110  
99   -3. **提成阶梯1 < 业绩 ≤ 提成阶梯2**:
100   - - 提成 = 提成阶梯1 × 提成比例1 + (业绩 - 提成阶梯1) × 提成比例2
101   - - 示例:业绩 = 380,000元 → 提成 = 350,000 × 1.0% + (380,000 - 350,000) × 1.5% = 3,500 + 450 = 3,950元
  111 +2. **提成阶梯1 ≤ 毛利 < 提成阶梯2**:
  112 + - 提成 = 提成阶梯1 × 提成比例1 + (毛利 - 提成阶梯1) × 提成比例2
  113 + - 示例:毛利 = 380,000元 → 提成 = 150,000 × 1.0% + (380,000 - 150,000) × 1.5% = 1,500 + 3,450 = 4,950元
102 114  
103   -4. **提成阶梯2 < 业绩 ≤ 提成阶梯3**:
104   - - 提成 = 提成阶梯1 × 提成比例1 + (提成阶梯2 - 提成阶梯1) × 提成比例2 + (业绩 - 提成阶梯2) × 提成比例3
105   - - 示例:业绩 = 420,000元 → 提成 = 350,000 × 1.0% + (400,000 - 350,000) × 1.5% + (420,000 - 400,000) × 2.0% = 3,500 + 750 + 400 = 4,650元
  115 +3. **提成阶梯2 ≤ 毛利 < 提成阶梯3**:
  116 + - 提成 = 提成阶梯1 × 提成比例1 + (提成阶梯2 - 提成阶梯1) × 提成比例2 + (毛利 - 提成阶梯2) × 提成比例3
  117 + - 示例:毛利 = 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元
106 118  
107   -5. **业绩 > 提成阶梯3**:
108   - - 提成 = 提成阶梯1 × 提成比例1 + (提成阶梯2 - 提成阶梯1) × 提成比例2 + (提成阶梯3 - 提成阶梯2) × 提成比例3 + (业绩 - 提成阶梯3) × 提成比例3
109   - - 示例:业绩 = 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元
  119 +4. **毛利 ≥ 提成阶梯3**:
  120 + - 提成 = 提成阶梯1 × 提成比例1 + (提成阶梯2 - 提成阶梯1) × 提成比例2 + (提成阶梯3 - 提成阶梯2) × 提成比例3 + (毛利 - 提成阶梯3) × 提成比例3
  121 + - 示例:毛利 = 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元
110 122  
111   -**注意**:
  123 +**重要说明**:
  124 +- 采用**分段累进式**计算,不同区间按不同比例分别计算后累加
  125 +- **必须满足提成阶梯1才能有提成资格**
  126 +- **提成计算基于毛利**,而不是开单业绩
112 127 - 如果提成阶梯2或提成阶梯3未设置(为NULL或0),则只使用提成阶梯1计算
113   -- 如果提成阶梯2设置但提成阶梯3未设置,则业绩超过提成阶梯2的部分按提成比例2计算
  128 +- 如果提成阶梯2设置但提成阶梯3未设置,则毛利 ≥ 提成阶梯2时,超出提成阶梯1的部分按提成比例2计算
114 129  
115 130 #### 2.4 多门店提成汇总
116 131  
... ... @@ -194,9 +209,13 @@
194 209 - 按门店ID、月份、总经理/经理ID查询
195 210 - 如果未找到提成阶梯设置,则无法计算提成(应报错或跳过该门店)
196 211  
197   -### 4. 门店总业绩
  212 +### 4. 门店毛利计算
  213 +
  214 +**定义**:门店毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾
198 215  
199   -**定义**:门店总业绩 = 开单业绩 - 退卡业绩
  216 +#### 4.1 销售业绩
  217 +
  218 +**定义**:销售业绩 = 开单业绩 - 退卡业绩
200 219  
201 220 **数据来源表及字段**:
202 221  
... ... @@ -207,16 +226,57 @@
207 226  
208 227 **计算方式**:
209 228 ```sql
210   -门店总业绩 = SUM(门店开单实付金额) - SUM(门店退卡金额)
  229 +销售业绩 = SUM(门店开单实付金额) - SUM(门店退卡金额)
211 230 ```
212 231  
213   -**过滤条件**:
214   -- 所有表记录必须满足:`F_IsEffective = 1`(有效记录)
215   -- 按统计月份(YYYYMM格式)过滤时间范围
216   -- 按门店ID(`djmd` 或 `md`)过滤
  232 +#### 4.2 产品物料
  233 +
  234 +**数据来源**:
  235 +- 表:`lq_inventory_usage`(库存使用记录表)
  236 +- 字段:`F_TotalAmount`(合计金额)
  237 +- 条件:
  238 + - `F_IsEffective = 1`(有效记录)
  239 + - `F_StoreId = @StoreId`(门店ID)
  240 + - **特殊规则**:11月工资算10月数据
  241 + - 如果计算月份是11月,则查询10月的数据
  242 + - 其他月份正常查询当月数据
  243 +
  244 +#### 4.3 合作项目成本
  245 +
  246 +**数据来源**:
  247 +- 表:`lq_cooperation_cost`(合作成本表)
  248 +- 字段:`F_TotalAmount`(合计金额)
  249 +- 条件:
  250 + - `F_Year = @Year`(年份)
  251 + - `F_Month = @MonthStr`(月份,格式为"11",不是"202511")
  252 + - `F_StoreId = @StoreId`(门店ID)
  253 + - `F_IsEffective = 1`(有效记录)
  254 +
  255 +#### 4.4 店内支出
  256 +
  257 +**数据来源**:
  258 +- 表:`lq_store_expense`(店内支出表)
  259 +- 字段:`F_Amount`(金额)
  260 +- 条件:
  261 + - `F_IsEffective = 1`(有效记录)
  262 + - `F_StoreId = @StoreId`(门店ID)
  263 + - `DATE_FORMAT(F_ExpenseDate, '%Y%m') = @MonthStr`(月份,YYYYMM格式)
  264 +
  265 +#### 4.5 洗毛巾费用
  266 +
  267 +**数据来源**:
  268 +- 表:`lq_laundry_flow`(清洗流水表)
  269 +- 字段:`F_TotalPrice`(总费用)
  270 +- 条件:
  271 + - `F_IsEffective = 1`(有效记录)
  272 + - `F_FlowType = 0`(只统计送出的记录)
  273 + - `F_StoreId = @StoreId`(门店ID)
  274 + - 优先使用 `F_SendTime`,如果为空则使用 `F_CreateTime`
  275 + - `DATE_FORMAT(COALESCE(F_SendTime, F_CreateTime), '%Y%m') = @MonthStr`(月份,YYYYMM格式)
217 276  
218 277 **重要说明**:
219   -- **确认**:提成计算基于门店总业绩(开单 - 退卡)
  278 +- **提成计算基于门店毛利**,而不是开单业绩
  279 +- 所有门店的毛利分别计算,然后汇总
220 280  
221 281 ---
222 282  
... ... @@ -269,10 +329,15 @@
269 329 - 从 `lq_md_general_manager_lifeline` 表获取每个门店的提成阶梯设置
270 330 - 提成阶梯1和提成比例1是必填项,未设置应报错
271 331  
272   -4. **获取门店总业绩**:
  332 +4. **获取门店毛利**:
273 333 - 从 `lq_kd_kdjlb` 表统计每个门店的开单业绩(`sfyj`)
274 334 - 从 `lq_hytk_hytk` 表统计每个门店的退卡业绩(`F_ActualRefundAmount` 或 `tkje`)
275   - - 计算每个门店的总业绩 = 开单业绩 - 退卡业绩
  335 + - 计算销售业绩 = 开单业绩 - 退卡业绩
  336 + - 从 `lq_inventory_usage` 表统计产品物料(注意11月特殊规则)
  337 + - 从 `lq_cooperation_cost` 表统计合作项目成本
  338 + - 从 `lq_store_expense` 表统计店内支出
  339 + - 从 `lq_laundry_flow` 表统计洗毛巾费用
  340 + - 计算每个门店的毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾
276 341  
277 342 ### 2. 工资计算
278 343  
... ... @@ -284,15 +349,22 @@
284 349  
285 350 2. **遍历该总经理/经理管理的每个门店**:
286 351  
287   - a. **判断是否达到提成门槛**:
288   - - 获取该门店的生命线(`lq_md_target.F_StoreLifeline`)
289   - - 获取该门店的总业绩
290   - - 如果门店业绩 < 门店生命线 → 该门店提成 = 0,跳过
291   - - 如果门店业绩 ≥ 门店生命线 → 继续计算提成
  352 + a. **计算门店毛利**:
  353 + - 计算销售业绩 = 开单业绩 - 退卡业绩
  354 + - 统计产品物料(注意11月特殊规则)
  355 + - 统计合作项目成本
  356 + - 统计店内支出
  357 + - 统计洗毛巾费用
  358 + - 计算毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾
292 359  
293   - b. **计算该门店的提成**(如果达到门槛)
  360 + b. **判断是否达到提成资格**
294 361 - 获取该门店的提成阶梯设置(`lq_md_general_manager_lifeline`)
295   - - 根据门店业绩和提成阶梯,使用分段累进方式计算提成金额
  362 + - 获取提成阶梯1(`F_Lifeline1`)
  363 + - 如果门店毛利 < 提成阶梯1 → 该门店提成 = 0,跳过
  364 + - 如果门店毛利 ≥ 提成阶梯1 → 继续计算提成
  365 +
  366 + c. **计算该门店的提成**(如果达到资格):
  367 + - 根据门店毛利和提成阶梯,使用分段累进方式计算提成金额
296 368 - 累加到总提成
297 369  
298 370 3. **计算最终工资**:
... ... @@ -318,10 +390,11 @@
318 390 - 如果门店未在 `lq_md_general_manager_lifeline` 表中设置,则无法计算该门店的提成
319 391  
320 392 3. **边界情况**:
321   - - 如果门店没有业绩数据,业绩为0,未达到门店生命线,提成为0
322   - - 如果门店业绩 < 门店生命线,提成为0
  393 + - 如果门店没有业绩数据,毛利为0,未达到提成阶梯1,提成为0
  394 + - 如果门店毛利 < 提成阶梯1,提成为0(未达到提成资格)
323 395 - 如果总经理/经理没有管理的门店,总提成为0,应发工资 = 底薪(4000元)
324 396 - 如果提成阶梯2或提成阶梯3未设置,则只使用提成阶梯1计算
  397 + - 如果门店毛利为负数(成本大于销售业绩),仍按负数计算,但不会产生提成(因为负数 < 提成阶梯1)
325 398  
326 399 4. **计算精度**:
327 400 - 涉及金额计算时,建议保留2位小数
... ... @@ -329,8 +402,9 @@
329 402  
330 403 5. **总经理和经理**:
331 404 - 总经理和经理的计算规则相同
332   - - 都使用门店生命线来判断是否达到提成门槛
  405 + - 都使用提成阶梯1来判断是否达到提成资格
333 406 - 都使用 `lq_md_general_manager_lifeline` 表的提成阶梯来计算提成
  407 + - 都使用毛利作为提成计算的基数
334 408  
335 409 6. **保底工资**:
336 410 - 暂时不考虑保底工资规则
... ...
大项目主管工资计算规则梳理.md
... ... @@ -21,7 +21,8 @@
21 21 - 大项目主管从 `BASE_USER` 表获取,岗位字段(`F_GW`)为"主管",组织ID为大项目一部或大项目二部
22 22 - 每个大项目主管管理的门店归属在 `lq_md_target` 表中(通过 `F_MajorProjectDepartment` 字段)
23 23 - 需要统计该大项目主管管理的**所有门店**的总业绩(开单-退卡)
24   -- 提成采用分段方式计算(不是分段累进)
  24 +- **只统计医美类型的业绩**(从 `lq_kd_jksyj` 表统计医美类型的健康师业绩,从 `lq_hytk_mx` 表统计医美类型的退卡金额)
  25 +- 提成采用分段累进式计算
25 26  
26 27 ---
27 28  
... ... @@ -39,23 +40,31 @@
39 40  
40 41 ### 2. 业绩提成规则
41 42  
42   -**提成计算方式**:根据管理的所有门店的总业绩分段计算
  43 +**提成计算方式**:根据管理的所有门店的总业绩**分段累进式**计算
43 44  
44   -| 总业绩范围 | 提成比例 | 说明 |
45   -|-----------|---------|------|
46   -| ≤ 50万 | 0% | 无提成 |
47   -| > 50万 且 ≤ 70万 | 1% | 按1%计算提成 |
48   -| > 70万 | 1.5% | 按1.5%计算提成 |
  45 +| 业绩区间 | 提成比例 | 说明 |
  46 +|---------|---------|------|
  47 +| ≤ 50万 | 0% | 无提成资格 |
  48 +| > 50万 | 分段计算 | 有提成资格后,0-70万部分按1%,70万以上部分按1.5% |
49 49  
50 50 **计算说明**:
51   -- 提成金额 = 总业绩 × 对应提成比例
52   -- 采用分段方式计算,不同区间按不同比例计算
53   -- **注意**:不是分段累进,而是整个总业绩按对应比例计算
  51 +- 采用**分段累进式**计算,不同区间按不同比例分别计算后累加
  52 +- **必须大于50万才有提成资格**
  53 +- 如果有提成资格后:
  54 + - 0-70万部分:1%(整个0-70万部分都按1%计算)
  55 + - 70万以上部分:1.5%
  56 +
  57 +**计算公式(分段累进)**:
  58 +```
  59 +如果 总业绩 ≤ 50万:提成 = 0(无提成资格)
  60 +如果 50万 < 总业绩 ≤ 70万:提成 = 总业绩 × 1%
  61 +如果 总业绩 > 70万:提成 = 70万 × 1% + (总业绩 - 70万) × 1.5%
  62 +```
54 63  
55 64 **示例**:
56   -- 总业绩 = 40万 → 提成 = 0(无提成)
57   -- 总业绩 = 60万 → 提成 = 60万 × 1% = 6000元
58   -- 总业绩 = 80万 → 提成 = 80万 × 1.5% = 12000元
  65 +- 总业绩 = 40万 → 提成 = 0(无提成资格,未达到50万门槛)
  66 +- 总业绩 = 60万 → 提成 = 60万 × 1% = 6,000元
  67 +- 总业绩 = 80万 → 提成 = 70万 × 1% + (80万 - 70万) × 1.5% = 7,000 + 1,500 = 8,500元
59 68  
60 69 ---
61 70  
... ... @@ -105,34 +114,28 @@
105 114  
106 115 ### 总业绩统计
107 116  
108   -**定义**:该大项目主管管理的所有门店中,开单金额总和减去退卡金额总和
  117 +**定义**:该大项目主管管理的所有门店中,**医美类型**的开单金额总和减去退卡金额总和
  118 +
  119 +**重要说明**:**只统计医美类型的业绩**
109 120  
110 121 **数据来源表及字段**:
111 122  
112 123 | 业绩类型 | 数据表 | 字段 | 说明 |
113 124 |---------|--------|------|------|
114   -| **开单金额** | `lq_kd_kdjlb` | `sfyj` | 门店开单实付金额 |
115   -| **退卡金额** | `lq_hytk_hytk` | `F_ActualRefundAmount` 或 `tkje` | 门店退卡金额 |
  125 +| **开单金额** | `lq_kd_jksyj` | `Jksyj` | 医美类型的健康师业绩(`ItemCategory == "医美"`) |
  126 +| **退卡金额** | `lq_hytk_mx` | `Tkje` | 医美类型的退卡金额(`ItemCategory == "医美"`) |
116 127  
117 128 **统计逻辑**:
118 129  
119   -1. **统计开单金额**(按管理的门店筛选):
120   - ```sql
121   - SELECT COALESCE(SUM(billing.sfyj), 0) as BillingAmount
122   - FROM lq_kd_kdjlb billing
123   - WHERE billing.F_IsEffective = 1
124   - AND billing.djmd IN (@管理的门店ID列表)
125   - AND DATE_FORMAT(billing.kdrq, '%Y%m') = @统计月份
126   - ```
  130 +1. **统计开单金额**(只统计医美类型):
  131 + - 从 `lq_kd_jksyj` 表关联 `lq_kd_kdjlb` 表
  132 + - 条件:`ItemCategory == "医美"`,`F_IsEffective = 1`,`djmd IN (@管理的门店ID列表)`,`Kdrq` 在统计月份范围内
  133 + - 统计 `Jksyj` 字段(健康师业绩)的总和
127 134  
128   -2. **统计退卡金额**(按管理的门店筛选):
129   - ```sql
130   - SELECT COALESCE(SUM(refund.F_ActualRefundAmount), 0) as RefundAmount
131   - FROM lq_hytk_hytk refund
132   - WHERE refund.F_IsEffective = 1
133   - AND refund.djmd IN (@管理的门店ID列表)
134   - AND DATE_FORMAT(refund.tkrq, '%Y%m') = @统计月份
135   - ```
  135 +2. **统计退卡金额**(只统计医美类型):
  136 + - 从 `lq_hytk_mx` 表关联 `lq_hytk_hytk` 表
  137 + - 条件:`ItemCategory == "医美"`,`F_IsEffective = 1`,`md IN (@管理的门店ID列表)`,`Tksj` 在统计月份范围内
  138 + - 统计 `Tkje` 字段(退卡金额)的总和
136 139  
137 140 3. **计算净总业绩**:
138 141 - 净总业绩 = 开单金额 - 退卡金额
... ... @@ -213,11 +216,11 @@
213 216  
214 217 ### 步骤4:计算提成
215 218  
216   -根据总业绩分段计算提成:
  219 +根据总业绩分段累进计算提成:
217 220  
218   -- 如果总业绩 ≤ 50万:提成 = 0(无提成
  221 +- 如果总业绩 ≤ 50万:提成 = 0(无提成资格
219 222 - 如果 50万 < 总业绩 ≤ 70万:提成 = 总业绩 × 1%
220   -- 如果总业绩 > 70万:提成 = 总业绩 × 1.5%
  223 +- 如果总业绩 > 70万:提成 = 70万 × 1% + (总业绩 - 70万) × 1.5%
221 224  
222 225 ---
223 226  
... ... @@ -246,5 +249,5 @@
246 249 - 必须从 `lq_md_target` 表获取管理的门店(通过 `F_MajorProjectDepartment` 字段)
247 250 - 必须正确统计总业绩(开单金额 - 退卡金额)
248 251 - 必须按管理的门店筛选,只统计该大项目主管管理的门店
249   -- 采用分段方式计算提成(不是分段累进),整个总业绩按对应比例计算
  252 +- 采用**分段累进式**计算提成,不同区间按不同比例分别计算后累加
250 253  
... ...
年度汇总表建表.sql 0 → 100644
  1 +CREATE TABLE `lq_annual_summary` (
  2 + `F_Id` varchar(50) NOT NULL COMMENT '主键',
  3 + `F_StoreId` varchar(50) NOT NULL COMMENT '门店ID',
  4 + `F_StoreName` varchar(100) DEFAULT NULL COMMENT '门店名称',
  5 + `F_BusinessUnitId` varchar(50) DEFAULT NULL COMMENT '归属事业部ID',
  6 + `F_BusinessUnitName` varchar(100) DEFAULT NULL COMMENT '归属事业部名称',
  7 + `F_Year` int(11) NOT NULL COMMENT '年份',
  8 + `F_Month` int(11) NOT NULL COMMENT '月份',
  9 + `F_TotalPerformance` decimal(18,2) DEFAULT '0.00' COMMENT '总业绩',
  10 + `F_TotalConsume` decimal(18,2) DEFAULT '0.00' COMMENT '总消耗',
  11 + `F_HeadCount` decimal(18,2) DEFAULT '0.00' COMMENT '人头数',
  12 + `F_PersonTime` decimal(18,2) DEFAULT '0.00' COMMENT '人次数',
  13 + `F_ProjectCount` decimal(18,2) DEFAULT '0.00' COMMENT '项目数',
  14 + `F_CreateTime` datetime DEFAULT NULL COMMENT '创建时间',
  15 + `F_CreateUser` varchar(50) DEFAULT NULL COMMENT '创建人',
  16 + `F_UpdateTime` datetime DEFAULT NULL COMMENT '更新时间',
  17 + `F_UpdateUser` varchar(50) DEFAULT NULL COMMENT '更新人',
  18 + `F_IsEffective` int(11) DEFAULT '1' COMMENT '是否有效 0无效 1有效',
  19 + PRIMARY KEY (`F_Id`),
  20 + UNIQUE KEY `UK_Store_Year_Month` (`F_StoreId`,`F_Year`,`F_Month`) USING BTREE
  21 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='年度汇总表';
... ...
店助主任工资计算规则梳理.md
... ... @@ -26,43 +26,63 @@
26 26  
27 27 ### 2. 提成规则
28 28  
29   -**计算公式**:根据门店业绩与门店生命线的比例确定提成比例,使用阶梯提成模式计算
  29 +**计算公式**:分段式提成模式,根据门店业绩与门店生命线的比例分段计算提成
30 30  
31   -#### 提成比例规则
  31 +#### 提成前提条件
32 32  
33   -| 门店业绩范围 | 提成计算方式 |
34   -|------------|------------|
35   -| 门店业绩 < 门店生命线 × 70% | 0%(无提成) |
36   -| 门店生命线 × 70% ≤ 门店业绩 < 门店生命线 × 100% | 0.4%(全部业绩按0.4%计算) |
37   -| 门店业绩 ≥ 门店生命线 × 100% | **阶梯提成**:<br>- 超过生命线部分:1%<br>- 剩余部分(≤生命线):0.6% |
  33 +- **必须达到70%**:门店业绩必须达到门店生命线的70%,否则无提成
38 34  
39   -#### 提成计算示例
  35 +#### 提成比例规则(分段式)
40 36  
41   -**示例1:业绩未达到70%**
42   -- 门店生命线 = 100,000元
43   -- 门店业绩 = 60,000元
44   -- 计算:60,000 < 100,000 × 70% = 70,000
45   -- 提成金额 = 0元(无提成)
46   -
47   -**示例2:业绩在70%-100%之间**
48   -- 门店生命线 = 100,000元
49   -- 门店业绩 = 85,000元
50   -- 计算:70,000 ≤ 85,000 < 100,000
51   -- 提成金额 = 85,000 × 0.4% = 340元
52   -
53   -**示例3:业绩超过100%(阶梯提成)**
54   -- 门店生命线 = 100,000元
55   -- 门店业绩 = 150,000元
56   -- 计算过程:
57   - 1. 业绩(150,000)> 生命线(100,000)
58   - 2. ≤生命线部分:100,000 × 0.6% = 600元
59   - 3. >生命线部分:(150,000 - 100,000) × 1% = 50,000 × 1% = 500元
60   - 4. 总提成 = 600 + 500 = 1,100元
  37 +| 业绩区间 | 提成比例 | 计算说明 |
  38 +|---------|---------|---------|
  39 +| 门店业绩 < 门店生命线 × 70% | 0% | 无提成 |
  40 +| 门店生命线 × 70% ≤ 门店业绩 < 门店生命线 × 100% | 0.4% | 整个业绩按0.4%计算 |
  41 +| 门店业绩 ≥ 门店生命线 × 100% | 0.4% + 1.6% | **分段式**:<br>- 0-100%部分(整个生命线):0.4%<br>- 100%以上部分:1.6% |
61 42  
62 43 **重要说明**:
63   -- 门店生命线必须在 `lq_md_target` 表中进行设置,如果未设置,系统应报错提示
64   -- 当业绩超过生命线时,必须使用阶梯提成模式,不能使用单一提成比例
65   -- 提成按门店业绩的百分比计算
  44 +- 店助主任和店助使用**相同的分段式提成规则**,但100%以上部分比例不同
  45 +- 店助:100%以上部分按0.6%
  46 +- 店助主任:100%以上部分按1.6%
  47 +- 提成采用**分段式**计算方式,不同区间按不同比例分别计算后累加
  48 +- 前提条件:必须达到70%才有提成资格
  49 +- **分段式**:业绩超过100%时,0-100%部分和100%以上部分分别按不同比例计算
  50 +
  51 +#### 提成计算示例
  52 +
  53 +**示例1:业绩在70%-100%之间**
  54 +
  55 +假设:
  56 +- 门店生命线 = 200,000元
  57 +- 门店业绩 = 147,685.20元
  58 +
  59 +计算过程:
  60 +1. 判断比例:147,685.20 / 200,000 = 73.84%(≥ 70%,满足前提条件)
  61 +2. 判断区间:70% ≤ 73.84% < 100%
  62 +3. 计算:整个业绩按0.4%计算
  63 + - 提成金额:147,685.20元 × 0.4% = 590.74元
  64 +4. 总提成金额:590.74元
  65 +
  66 +**示例2:业绩超过100%(分段式)**
  67 +
  68 +假设:
  69 +- 门店生命线 = 320,000元
  70 +- 门店业绩 = 408,593.42元
  71 +
  72 +计算过程:
  73 +1. 判断比例:408,593.42 / 320,000 = 127.69%(≥ 70%,满足前提条件)
  74 +2. 判断区间:≥ 100%
  75 +3. **分段式计算**:
  76 + - 0-100%部分(整个生命线):320,000元 × 0.4% = 1,280元
  77 + - 100%以上部分:(408,593.42 - 320,000)元 × 1.6% = 88,593.42元 × 1.6% = 1,417.49元
  78 +4. 总提成金额:1,280元 + 1,417.49元 = 2,697.49元
  79 +
  80 +**计算公式**:
  81 +```
  82 +如果 业绩 < 70%:提成 = 0
  83 +如果 70% ≤ 业绩 < 100%:提成 = 业绩 × 0.4%
  84 +如果 业绩 ≥ 100%:提成 = 生命线 × 0.4% + (业绩 - 生命线) × 1.6%
  85 +```
66 86  
67 87 ---
68 88  
... ... @@ -140,7 +160,11 @@
140 160 4. **提成比例判断**:
141 161 - 严格按照门店业绩与门店生命线的比例判断
142 162 - 注意边界值:70% 和 100%
143   - - **重要**:当业绩超过生命线时,必须使用阶梯提成模式(超过部分1%,剩余部分0.6%)
  163 + - **重要**:店助主任和店助使用**相同的分段式提成规则**,但100%以上部分比例不同
  164 + - **重要**:当业绩超过100%时,使用**分段式**提成模式:
  165 + - 0-100%部分(整个生命线)按0.4%计算
  166 + - 100%以上部分:店助按0.6%计算,店助主任按1.6%计算
  167 + - **分段式说明**:不同区间按不同比例分别计算后累加
144 168  
145 169 5. **数据一致性**:
146 170 - 门店业绩的计算逻辑必须与门店总业绩统计保持一致
... ... @@ -423,20 +447,24 @@ LIMIT 1
423 447  
424 448 | 岗位 | 业绩 ≥ 100%时的提成计算 |
425 449 |-----|---------------------|
426   -| 店助 | 全部业绩按 0.6% 计算 |
427   -| 店助主任 | **阶梯提成**:<br>- 超过生命线部分:1%<br>- 剩余部分(≤生命线):0.6% |
  450 +| 店助 | 分段式:<br>- 0-100%部分(整个生命线):0.4%<br>- 100%以上部分:0.6% |
  451 +| 店助主任 | **分段式**:<br>- 0-100%部分(整个生命线):0.4%<br>- 100%以上部分:1.6% |
428 452  
429   -**示例对比**:
430   -- 门店生命线 = 100,000元
431   -- 门店业绩 = 150,000元
432   -
433   -**店助提成**:
434   -- 150,000 × 0.6% = 900元
  453 +**重要说明**:店助主任和店助使用**相同的分段式提成规则**,但100%以上部分比例不同
435 454  
436   -**店助主任提成**:
437   -- ≤生命线部分:100,000 × 0.6% = 600元
438   -- >生命线部分:(150,000 - 100,000) × 1% = 500元
439   -- 总提成 = 600 + 500 = 1,100元
  455 +**示例对比**:
  456 +- 门店生命线 = 320,000元
  457 +- 门店业绩 = 408,593.42元
  458 +
  459 +**店助提成(分段式)**:
  460 +- 0-100%部分:320,000 × 0.4% = 1,280元
  461 +- 100%以上部分:(408,593.42 - 320,000) × 0.6% = 531.56元
  462 +- 总提成 = 1,280 + 531.56 = 1,811.56元
  463 +
  464 +**店助主任提成(分段式)**:
  465 +- 0-100%部分:320,000 × 0.4% = 1,280元
  466 +- 100%以上部分:(408,593.42 - 320,000) × 1.6% = 1,417.49元
  467 +- 总提成 = 1,280 + 1,417.49 = 2,697.49元
440 468  
441 469 ### 3. 阶段奖励规则
442 470  
... ...
科技部总经理工资计算规则梳理.md
... ... @@ -121,51 +121,46 @@
121 121  
122 122 ### 溯源金额统计
123 123  
124   -**定义**:该科技部总经理管理的所有门店中,品项的 `F_BeautyType` 为 "溯源系统" 或 "溯源" 的品项明细的实付金额总和(开单金额 - 退卡金额)
  124 +**定义**:该科技部总经理管理的所有门店中,品项的 `F_BeautyType` 为 "溯源系统" 或 "溯源" 的健康师业绩总和(开单金额 - 退卡金额)
  125 +
  126 +**重要说明**:
  127 +- **数据来源**:从健康师业绩表(`lq_kd_jksyj`)和退卡健康师业绩表(`lq_hytk_jksyj`)统计
  128 +- **统计依据**:使用健康师业绩表中的业绩金额,而不是开单品项明细表的实付金额
  129 +- **原因**:健康师业绩表中的金额是经过分配和计算的准确业绩,更符合业务逻辑
125 130  
126 131 **数据来源表及字段**:
127 132  
128 133 | 数据表 | 字段 | 说明 |
129 134 |--------|------|------|
130   -| `lq_kd_pxmx` | `F_ActualPrice` | 品项明细实付金额 |
131   -| `lq_kd_pxmx` | `F_BeautyType` | 科美类型(用于区分溯源和Cell) |
132   -| `lq_kd_kdjlb` | `kdrq` | 开单日期(用于按月统计) |
133   -| `lq_kd_kdjlb` | `djmd` | 单据门店ID(用于筛选管理的门店) |
134   -| `lq_xmzl` | `F_BeautyType` | 品项的科美类型(如果明细表没有,从品项表获取) |
135   -| `lq_hytk_mx` | `tkje` | 退卡明细退款金额 |
136   -| `lq_hytk_hytk` | `tkrq` | 退卡日期(用于按月统计) |
137   -| `lq_hytk_hytk` | `djmd` | 单据门店ID(用于筛选管理的门店) |
  135 +| `lq_kd_jksyj` | `jksyj` | 健康师业绩(开单业绩,字符串类型,需转换为decimal) |
  136 +| `lq_kd_jksyj` | `F_BeautyType` | 科美类型(用于区分溯源和Cell) |
  137 +| `lq_kd_jksyj` | `F_StoreId` | 门店ID(用于筛选管理的门店) |
  138 +| `lq_kd_jksyj` | `yjsj` | 业绩时间(用于按月统计) |
  139 +| `lq_hytk_jksyj` | `jksyj` | 健康师业绩(退卡业绩,decimal类型) |
  140 +| `lq_hytk_jksyj` | `F_BeautyType` | 科美类型(用于区分溯源和Cell) |
  141 +| `lq_hytk_jksyj` | `F_StoreId` | 门店ID(用于筛选管理的门店) |
  142 +| `lq_hytk_jksyj` | `tksj` | 退卡时间(用于按月统计) |
138 143  
139 144 **统计逻辑**:
140 145  
141 146 1. **统计开单溯源金额**(按管理的门店筛选):
142 147 ```sql
143   - SELECT COALESCE(SUM(pxmx.F_ActualPrice), 0) as TraceabilityAmount
144   - FROM lq_kd_pxmx pxmx
145   - INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
146   - INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
147   - WHERE pxmx.F_IsEffective = 1
148   - AND billing.F_IsEffective = 1
149   - AND item.F_IsEffective = 1
150   - AND (pxmx.F_BeautyType = '溯源系统' OR pxmx.F_BeautyType = '溯源'
151   - OR item.F_BeautyType = '溯源系统' OR item.F_BeautyType = '溯源')
152   - AND billing.djmd IN (@管理的门店ID列表)
153   - AND DATE_FORMAT(billing.kdrq, '%Y%m') = @统计月份
  148 + SELECT COALESCE(SUM(CAST(jksyj AS DECIMAL(18,2))), 0) as TraceabilityAmount
  149 + FROM lq_kd_jksyj
  150 + WHERE F_IsEffective = 1
  151 + AND F_StoreId IN (@管理的门店ID列表)
  152 + AND (F_BeautyType = '溯源系统' OR F_BeautyType = '溯源')
  153 + AND DATE_FORMAT(yjsj, '%Y%m') = @统计月份
154 154 ```
155 155  
156 156 2. **统计退卡溯源金额**(按管理的门店筛选):
157 157 ```sql
158   - SELECT COALESCE(SUM(tkmx.tkje), 0) as RefundTraceabilityAmount
159   - FROM lq_hytk_mx tkmx
160   - INNER JOIN lq_hytk_hytk refund ON tkmx.glhytkbh = refund.F_Id
161   - INNER JOIN lq_xmzl item ON tkmx.px = item.F_Id
162   - WHERE tkmx.F_IsEffective = 1
163   - AND refund.F_IsEffective = 1
164   - AND item.F_IsEffective = 1
165   - AND (tkmx.F_BeautyType = '溯源系统' OR tkmx.F_BeautyType = '溯源'
166   - OR item.F_BeautyType = '溯源系统' OR item.F_BeautyType = '溯源')
167   - AND refund.djmd IN (@管理的门店ID列表)
168   - AND DATE_FORMAT(refund.tkrq, '%Y%m') = @统计月份
  158 + SELECT COALESCE(SUM(jksyj), 0) as RefundTraceabilityAmount
  159 + FROM lq_hytk_jksyj
  160 + WHERE F_IsEffective = 1
  161 + AND F_StoreId IN (@管理的门店ID列表)
  162 + AND (F_BeautyType = '溯源系统' OR F_BeautyType = '溯源')
  163 + AND DATE_FORMAT(tksj, '%Y%m') = @统计月份
169 164 ```
170 165  
171 166 3. **计算净溯源金额**:
... ... @@ -175,7 +170,12 @@
175 170  
176 171 ### Cell金额统计
177 172  
178   -**定义**:该科技部总经理管理的所有门店中,品项的 `F_BeautyType` 为 "cell" 或 "Cell" 的品项明细的实付金额总和(开单金额 - 退卡金额)
  173 +**定义**:该科技部总经理管理的所有门店中,品项的 `F_BeautyType` 为 "cell" 或 "Cell" 的健康师业绩总和(开单金额 - 退卡金额)
  174 +
  175 +**重要说明**:
  176 +- **数据来源**:从健康师业绩表(`lq_kd_jksyj`)和退卡健康师业绩表(`lq_hytk_jksyj`)统计
  177 +- **统计依据**:使用健康师业绩表中的业绩金额,而不是开单品项明细表的实付金额
  178 +- **原因**:健康师业绩表中的金额是经过分配和计算的准确业绩,更符合业务逻辑
179 179  
180 180 **数据来源表及字段**:同溯源金额统计
181 181  
... ... @@ -183,32 +183,22 @@
183 183  
184 184 1. **统计开单Cell金额**(按管理的门店筛选):
185 185 ```sql
186   - SELECT COALESCE(SUM(pxmx.F_ActualPrice), 0) as CellAmount
187   - FROM lq_kd_pxmx pxmx
188   - INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
189   - INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
190   - WHERE pxmx.F_IsEffective = 1
191   - AND billing.F_IsEffective = 1
192   - AND item.F_IsEffective = 1
193   - AND (pxmx.F_BeautyType = 'cell' OR pxmx.F_BeautyType = 'Cell'
194   - OR item.F_BeautyType = 'cell' OR item.F_BeautyType = 'Cell')
195   - AND billing.djmd IN (@管理的门店ID列表)
196   - AND DATE_FORMAT(billing.kdrq, '%Y%m') = @统计月份
  186 + SELECT COALESCE(SUM(CAST(jksyj AS DECIMAL(18,2))), 0) as CellAmount
  187 + FROM lq_kd_jksyj
  188 + WHERE F_IsEffective = 1
  189 + AND F_StoreId IN (@管理的门店ID列表)
  190 + AND (F_BeautyType = 'cell' OR F_BeautyType = 'Cell')
  191 + AND DATE_FORMAT(yjsj, '%Y%m') = @统计月份
197 192 ```
198 193  
199 194 2. **统计退卡Cell金额**(按管理的门店筛选):
200 195 ```sql
201   - SELECT COALESCE(SUM(tkmx.tkje), 0) as RefundCellAmount
202   - FROM lq_hytk_mx tkmx
203   - INNER JOIN lq_hytk_hytk refund ON tkmx.glhytkbh = refund.F_Id
204   - INNER JOIN lq_xmzl item ON tkmx.px = item.F_Id
205   - WHERE tkmx.F_IsEffective = 1
206   - AND refund.F_IsEffective = 1
207   - AND item.F_IsEffective = 1
208   - AND (tkmx.F_BeautyType = 'cell' OR tkmx.F_BeautyType = 'Cell'
209   - OR item.F_BeautyType = 'cell' OR item.F_BeautyType = 'Cell')
210   - AND refund.djmd IN (@管理的门店ID列表)
211   - AND DATE_FORMAT(refund.tkrq, '%Y%m') = @统计月份
  196 + SELECT COALESCE(SUM(jksyj), 0) as RefundCellAmount
  197 + FROM lq_hytk_jksyj
  198 + WHERE F_IsEffective = 1
  199 + AND F_StoreId IN (@管理的门店ID列表)
  200 + AND (F_BeautyType = 'cell' OR F_BeautyType = 'Cell')
  201 + AND DATE_FORMAT(tksj, '%Y%m') = @统计月份
212 202 ```
213 203  
214 204 3. **计算净Cell金额**:
... ... @@ -310,30 +300,22 @@
310 300 ### 步骤4:统计Cell金额(所有管理的门店总和)
311 301  
312 302 1. **统计开单Cell金额**(按管理的门店筛选):
313   - - 从 `lq_kd_pxmx` 表统计
314   - - 关联 `lq_kd_kdjlb` 表获取开单日期和门店ID
315   - - 关联 `lq_xmzl` 表获取品项的 `F_BeautyType`
  303 + - 从 `lq_kd_jksyj` 表(健康师业绩表)统计
316 304 - 筛选条件:
317   - - `lq_kd_pxmx.F_IsEffective = 1`(有效记录)
318   - - `lq_kd_kdjlb.F_IsEffective = 1`(有效开单)
319   - - `lq_xmzl.F_IsEffective = 1`(有效品项)
  305 + - `F_IsEffective = 1`(有效记录)
  306 + - `F_StoreId IN (@管理的门店ID列表)`
320 307 - `F_BeautyType = 'cell'` 或 `'Cell'`
321   - - `lq_kd_kdjlb.djmd IN (@管理的门店ID列表)`
322   - - 开单日期在统计月份范围内
323   - - 汇总:`SUM(lq_kd_pxmx.F_ActualPrice)`
  308 + - 业绩时间(`yjsj`)在统计月份范围内
  309 + - 汇总:`SUM(CAST(jksyj AS DECIMAL(18,2)))`(注意:`jksyj`字段是字符串类型,需要转换为decimal)
324 310  
325 311 2. **统计退卡Cell金额**(按管理的门店筛选):
326   - - 从 `lq_hytk_mx` 表统计
327   - - 关联 `lq_hytk_hytk` 表获取退卡日期和门店ID
328   - - 关联 `lq_xmzl` 表获取品项的 `F_BeautyType`
  312 + - 从 `lq_hytk_jksyj` 表(退卡健康师业绩表)统计
329 313 - 筛选条件:
330   - - `lq_hytk_mx.F_IsEffective = 1`(有效记录)
331   - - `lq_hytk_hytk.F_IsEffective = 1`(有效退卡)
332   - - `lq_xmzl.F_IsEffective = 1`(有效品项)
  314 + - `F_IsEffective = 1`(有效记录)
  315 + - `F_StoreId IN (@管理的门店ID列表)`
333 316 - `F_BeautyType = 'cell'` 或 `'Cell'`
334   - - `lq_hytk_hytk.djmd IN (@管理的门店ID列表)`
335   - - 退卡日期在统计月份范围内
336   - - 汇总:`SUM(lq_hytk_mx.tkje)`
  317 + - 退卡时间(`tksj`)在统计月份范围内
  318 + - 汇总:`SUM(jksyj)`(注意:`jksyj`字段是decimal类型)
337 319  
338 320 3. **计算净Cell金额**:
339 321 - 净Cell金额 = 开单Cell金额 - 退卡Cell金额
... ...
科技部老师工资计算规则.md
... ... @@ -29,27 +29,36 @@
29 29  
30 30 ### 2. 业绩提成规则
31 31  
32   -业绩提成基于**总业绩**计算,采用阶梯式方式:
  32 +业绩提成基于**总业绩**计算,采用**分段累进式**方式:
33 33  
34 34 **提成前提**:业绩必须大于1万才能进行提成
35 35  
36   -| 总业绩范围 | 提成比例 | 说明 |
37   -|-----------|---------|------|
38   -| ≤ 10,000元 | 0% | 无提成 |
39   -| > 10,000元 且 ≤ 70,000元 | 2% | 整个业绩按2%计算 |
40   -| > 70,000元 且 ≤ 150,000元 | 2.5% | 整个业绩按2.5%计算 |
41   -| > 150,000元 | 3% | 整个业绩按3%计算 |
  36 +| 业绩区间 | 提成比例 | 说明 |
  37 +|---------|---------|------|
  38 +| ≤ 10,000元 | 0% | 无提成资格 |
  39 +| > 10,000元 | 分段计算 | 有提成资格后,0-7万部分按2%,7万-15万部分按2.5%,15万以上部分按3% |
42 40  
43 41 **计算说明**:
44   -- 提成金额 = 总业绩 × 对应提成比例
45   -- 采用阶梯式计算,整个业绩按对应区间的比例计算(不是分段累进)
46   -- 业绩必须大于1万才有提成资格
  42 +- 采用**分段累进式**计算,不同区间按不同比例分别计算后累加
  43 +- **必须大于1万才有提成资格**
  44 +- 如果有提成资格后:
  45 + - 0-7万部分:2%(整个0-7万部分都按2%计算)
  46 + - 7万-15万部分:2.5%
  47 + - 15万以上部分:3%
  48 +
  49 +**计算公式(分段累进)**:
  50 +```
  51 +如果 业绩 ≤ 1万:提成 = 0(无提成资格)
  52 +如果 1万 < 业绩 ≤ 7万:提成 = 业绩 × 2%
  53 +如果 7万 < 业绩 ≤ 15万:提成 = 7万 × 2% + (业绩 - 7万) × 2.5%
  54 +如果 业绩 > 15万:提成 = 7万 × 2% + (15万 - 7万) × 2.5% + (业绩 - 15万) × 3%
  55 +```
47 56  
48 57 **示例**:
49   -- 总业绩 = 5,000元 → 提成 = 0(无提成,未达到1万门槛)
  58 +- 总业绩 = 5,000元 → 提成 = 0(无提成资格,未达到1万门槛)
50 59 - 总业绩 = 50,000元 → 提成 = 50,000 × 2% = 1,000元
51   -- 总业绩 = 100,000元 → 提成 = 100,000 × 2.5% = 2,500元
52   -- 总业绩 = 200,000元 → 提成 = 200,000 × 3% = 6,000元
  60 +- 总业绩 = 100,000元 → 提成 = 70,000 × 2% + (100,000 - 70,000) × 2.5% = 1,400 + 750 = 2,150元
  61 +- 总业绩 = 200,000元 → 提成 = 70,000 × 2% + (150,000 - 70,000) × 2.5% + (200,000 - 150,000) × 3% = 1,400 + 2,000 + 1,500 = 4,900元
53 62  
54 63 ---
55 64  
... ... @@ -198,12 +207,12 @@ WHERE lq_xh_kjbsyj.F_IsEffective = 1
198 207 - 都不满足 → 默认第一档 2,500元
199 208  
200 209 #### 4.2 计算业绩提成
201   -- 根据总业绩范围确定提成比例:
202   - - < 10,000元 → 0%
203   - - 10,000-70,000元 → 2%
204   - - 70,000-150,000元 → 2.5%
205   - - > 150,000元 → 3%
206   -- 业绩提成金额 = 总业绩 × 提成比例
  210 +- 前提条件:业绩必须大于1万才有提成资格
  211 +- 如果有提成资格后,分段计算:
  212 + - 0-7万部分:2%(整个0-7万部分都按2%计算)
  213 + - 7万-15万部分:2.5%
  214 + - 15万以上部分:3%
  215 +- 业绩提成金额按分段累进式计算
207 216  
208 217 #### 4.3 计算消耗提成
209 218 - 根据消耗业绩范围确定提成规则:
... ...