You need to sign in before continuing.
Commit 1fffa8765e003b2ef73d283fcc1570e1e97dca5a
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
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
sql/排查生美业绩统计差异详细.sql
sql/检查生美业绩统计差异.sql
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=冷忠翠¤tPage=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¤tPage=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
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 | - 根据消耗业绩范围确定提成规则: | ... | ... |