From ac14c8dbe991d749b70a5a05dff87bb1dd975c90 Mon Sep 17 00:00:00 2001 From: “wangming” <“wangming@antissoft.com”> Date: Tue, 23 Dec 2025 18:04:13 +0800 Subject: [PATCH] chore: update dashboard modal and docs --- antis-ncc-admin/src/views/report/index.vue | 373 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ antis-ncc-admin/src/views/statisticsList/form9.vue | 992 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- netcore/src/Modularity/Extend/NCC.Extend/LqReimbursementApplicationService.cs | 5 ++++- netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- sql/排查生美业绩统计差异-简化版.sql | 1 + sql/排查生美业绩统计差异详细.sql | 1 + sql/检查生美业绩统计差异.sql | 1 + test_goddess_card_members.sh | 135 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test_tianwang_api.py | 1 + test_update_billing_info.sh | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 用户画像数据清单.md | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 1798 insertions(+), 6 deletions(-) create mode 100755 test_goddess_card_members.sh create mode 100755 test_update_billing_info.sh create mode 100644 用户画像数据清单.md diff --git a/antis-ncc-admin/src/views/report/index.vue b/antis-ncc-admin/src/views/report/index.vue index 9b8ca98..f7fdfab 100644 --- a/antis-ncc-admin/src/views/report/index.vue +++ b/antis-ncc-admin/src/views/report/index.vue @@ -57,6 +57,119 @@ + +
+ + + + +
+
+ +
+
+
总会员数
+
{{ formatNumber(memberCards.totalMembers) }}
+
+
本月新增:{{ formatNumber(memberCards.newMembers) }}人
+
+
+
+
+ + + + +
+
+ +
+
+
活跃会员数
+
{{ formatNumber(memberCards.activeMembers) }}
+
+
活跃率:{{ memberCards.activeRate }}%
+
+
+
+
+ + + + +
+
+ +
+
+
剩余权益总金额
+
{{ formatNumber(memberCards.totalRemainingAmount / 10000) }} +
+
万元
+
人均:{{ formatNumber(memberCards.avgRemainingAmount) }}元
+
+
+
+
+ + + + +
+
+ +
+
+
沉睡会员数
+
{{ formatNumber(memberCards.totalSleepMembers) }}
+
+
30-90天:{{ formatNumber(memberCards.sleep30_90) }} | 90天+:{{ + formatNumber(memberCards.sleepOver90) }}
+
+
+
+
+
+ + + + + +
+ + + 会员类型分布 + +
+
+
+
+ + + + +
+ + + 会员分类统计 + +
+
+
+
+ +
+
+
{{ item.name }}
+
{{ formatNumber(item.value) }}人
+
+
+
+
+
+
+
+
@@ -263,6 +376,30 @@ export default { } ], + // 会员统计卡片数据 + memberCards: { + totalMembers: 0, + newMembers: 0, + activeMembers: 0, + activeRate: 0, + totalRemainingAmount: 0, + avgRemainingAmount: 0, + totalSleepMembers: 0, + sleep30_90: 0, + sleepOver90: 0 + }, + + // 会员分类列表 + memberCategoryList: [ + { name: '生美会员', value: 0, icon: 'el-icon-star-on' }, + { name: '医美会员', value: 0, icon: 'el-icon-medicine-box' }, + { name: '科技部会员', value: 0, icon: 'el-icon-cpu' }, + { name: '教育部会员', value: 0, icon: 'el-icon-reading' } + ], + + // 会员类型分布数据 + memberTypeDistribution: [], + // 图表实例 charts: {} } @@ -307,6 +444,34 @@ export default { this.overviewCards[1].value = Math.round(data.StorePerformance.TotalPerformance / 10000 * 100) / 100 this.overviewCards[2].value = data.HealthCoachPerformance.HealthCoachCount this.overviewCards[3].value = data.GoldTrianglePerformance.GoldTriangleCount + + // 更新会员统计数据 + if (data.MemberStatistics) { + const memberStats = data.MemberStatistics + this.memberCards.totalMembers = memberStats.TotalMembers || 0 + this.memberCards.newMembers = memberStats.NewMembersThisMonth || 0 + this.memberCards.activeMembers = memberStats.ActiveMembers || 0 + this.memberCards.activeRate = memberStats.ActiveRate || 0 + this.memberCards.totalRemainingAmount = memberStats.TotalRemainingAmount || 0 + this.memberCards.avgRemainingAmount = memberStats.AvgRemainingAmount || 0 + this.memberCards.totalSleepMembers = memberStats.TotalSleepMembers || 0 + this.memberCards.sleep30_90 = memberStats.SleepMembers30_90 || 0 + this.memberCards.sleepOver90 = memberStats.SleepMembersOver90 || 0 + + // 更新会员分类列表 + this.memberCategoryList[0].value = memberStats.BeautyMembers || 0 + this.memberCategoryList[1].value = memberStats.MedicalMembers || 0 + this.memberCategoryList[2].value = memberStats.TechMembers || 0 + this.memberCategoryList[3].value = memberStats.EducationMembers || 0 + + // 更新会员类型分布 + this.memberTypeDistribution = memberStats.MemberTypeDistribution || [] + + // 加载会员类型分布饼图 + this.$nextTick(() => { + this.loadMemberTypeChart() + }) + } } } catch (error) { this.$message.error('加载仪表盘数据失败') @@ -327,6 +492,68 @@ export default { ]) }, + // 加载会员类型分布饼图 + loadMemberTypeChart() { + if (!this.$refs.memberTypeChart) return + + // 如果图表已存在,先销毁 + if (this.charts.memberTypeChart) { + this.charts.memberTypeChart.dispose() + } + + const chartDom = this.$refs.memberTypeChart + this.charts.memberTypeChart = echarts.init(chartDom) + + const option = { + tooltip: { + trigger: 'item', + formatter: '{a}
{b}: {c} ({d}%)' + }, + legend: { + orient: 'vertical', + left: 'left', + top: 'middle' + }, + series: [ + { + name: '会员类型', + type: 'pie', + radius: ['40%', '70%'], + avoidLabelOverlap: false, + itemStyle: { + borderRadius: 10, + borderColor: '#fff', + borderWidth: 2 + }, + label: { + show: true, + formatter: '{b}\n{c}人 ({d}%)' + }, + emphasis: { + label: { + show: true, + fontSize: 16, + fontWeight: 'bold' + } + }, + data: this.memberTypeDistribution.map(item => ({ + value: item.Count, + name: item.MemberType + })) + } + ] + } + + this.charts.memberTypeChart.setOption(option) + + // 响应式调整 + window.addEventListener('resize', () => { + if (this.charts.memberTypeChart) { + this.charts.memberTypeChart.resize() + } + }) + }, + // 加载门店业绩趋势图 async loadStoreTrendChart() { try { @@ -973,6 +1200,152 @@ export default { } } + .member-statistics-overview { + margin-bottom: 30px; + + .member-card { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border-radius: 15px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + + &:hover { + transform: translateY(-5px); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15); + } + + &.card-member-total { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + } + + &.card-member-active { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + color: white; + } + + &.card-member-amount { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + color: white; + } + + &.card-member-sleep { + background: linear-gradient(135deg, #ff9a56 0%, #ff6a88 100%); + color: white; + } + + .card-content { + display: flex; + align-items: center; + padding: 20px; + + .card-icon { + font-size: 48px; + margin-right: 20px; + opacity: 0.8; + } + + .card-info { + flex: 1; + + .card-title { + font-size: 14px; + font-weight: 600; + margin-bottom: 8px; + opacity: 0.9; + } + + .card-value { + font-size: 32px; + font-weight: bold; + margin-bottom: 4px; + } + + .card-unit { + font-size: 12px; + opacity: 0.8; + margin-bottom: 4px; + } + + .card-subtitle { + font-size: 12px; + opacity: 0.8; + margin-top: 4px; + } + } + } + } + + .member-category-list { + padding: 20px; + + .category-item { + display: flex; + align-items: center; + padding: 15px; + margin-bottom: 12px; + background: rgba(245, 247, 250, 0.8); + border-radius: 10px; + transition: all 0.3s ease; + + &:hover { + background: rgba(245, 247, 250, 1); + transform: translateX(5px); + } + + &:last-child { + margin-bottom: 0; + } + + .category-icon { + width: 50px; + height: 50px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 15px; + font-size: 24px; + color: white; + + &.category-1 { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + } + + &.category-2 { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + } + + &.category-3 { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + } + + &.category-4 { + background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); + } + } + + .category-info { + flex: 1; + + .category-name { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 4px; + } + + .category-value { + font-size: 20px; + font-weight: bold; + color: #409EFF; + } + } + } + } + } + .dashboard-overview { margin-bottom: 30px; diff --git a/antis-ncc-admin/src/views/statisticsList/form9.vue b/antis-ncc-admin/src/views/statisticsList/form9.vue index e9717a9..131963f 100644 --- a/antis-ncc-admin/src/views/statisticsList/form9.vue +++ b/antis-ncc-admin/src/views/statisticsList/form9.vue @@ -3,7 +3,7 @@
-

领导驾驶舱 本月经营动态监测

+

集团驾驶舱 本月经营动态监测

@@ -22,6 +22,11 @@ 更新看板 + + + 测试弹窗 + +
@@ -46,6 +51,232 @@
+ + + + + +
+ 会员核心指标 +
+
+
+
+
+ +
+
+
总会员数
+
{{ formatNumber(memberStatistics.totalMembers) }}
+
+
+
+ + + + + 本月新增: {{ formatNumber(memberStatistics.newMembers) }}人 + + + + + + + + 上月新增: {{ formatNumber(memberStatistics.newMembersLastMonth) }}人 + + + +
+
+
+
+
+ +
+
+
活跃会员数
+
{{ formatNumber(memberStatistics.activeMembers) }}
+
+
+
+ + + + + 活跃(≤3天): {{ formatNumber(memberStatistics.active_0_3) }} + + + + + + + + 常到店(4-59天): {{ formatNumber(memberStatistics.active_4_59) }} + + + + + + + + 60天活跃率: {{ memberStatistics.activeRate }}% + + + + + + + + 30天活跃率: {{ memberStatistics.activeRate30 }}% + + + +
+
+
+
+
+ +
+
+
剩余权益总金额
+
¥{{ formatMoney(memberStatistics.totalRemainingAmount) }}
+
+
+
+ + + + + 人均: {{ formatMoney(memberStatistics.avgRemainingAmount) }}元 + + + + + + + + 最高剩余权益金额: {{ memberStatistics.topRemainingMemberName || '无' }} ¥{{ + formatMoney(memberStatistics.topRemainingAmount) }} + + + + + + + + 本月开单最高: {{ memberStatistics.topBillingMemberName || '无' }} ¥{{ + formatMoney(memberStatistics.topBillingAmount) }} + + + + + + + + 本月消耗最高: {{ memberStatistics.topConsumeMemberName || '无' }} ¥{{ + formatMoney(memberStatistics.topConsumeAmount) }} + + + +
+
+
+
+
+ +
+
+
沉睡会员数
+
{{ formatNumber(memberStatistics.totalSleepMembers) }}
+
+
+
+ + + + + 60-89天: {{ formatNumber(memberStatistics.sleep_60_89) }} + + + + + + + + 90-179天: {{ formatNumber(memberStatistics.sleep_90_179) }} + + + + + + + + 180-359天: {{ formatNumber(memberStatistics.sleep_180_359) }} + + + + + + + + 360天+: {{ formatNumber(memberStatistics.sleep_360_plus) }} + + + +
+
+
+
+
+ + + + +
+ 会员类型分布 +
+
+
+
+ + + + +
+ 会员分类统计 +
+
+
+
+
+
+
+
生美会员
+
{{ formatNumber(memberStatistics.beautyMembers) }}人
+
+
+
+
+
+
医美会员
+
{{ formatNumber(memberStatistics.medicalMembers) }}人
+
+
+
+
+
+
科技部会员
+
{{ formatNumber(memberStatistics.techMembers) }}人
+
+
+
+
+
+
+
+ @@ -190,6 +421,52 @@ 确 定
+ + + +
+ + + + 重置 +
+
+
+
+
核心权益
+
¥{{ formatMoney(memberStatistics.totalRemainingAmount) }}
+
剩余权益总额
+
+
+
活跃度
+
{{ memberStatistics.activeRate }}%
+
60天活跃率
+
+
+
睡眠会员
+
{{ formatNumber(memberStatistics.totalSleepMembers) }}
+
需唤醒用户
+
+
+
+
+ 组件内容区域 + 超出高度自动出现滚动条 +
+
+
+
+
+
事件 {{ i }}
+
这里放任意组件或文本,模拟实际穿透内容。
+
+
T-{{ i }}
+
+
+
+
+
@@ -209,7 +486,7 @@ export default { storeOptions: [], // 核心 KPI 数据 kpiData: {}, - trendType: 'month', + trendType: 'day', coachRankType: 'billing', // 看板核心数据 trendData: [], @@ -224,9 +501,43 @@ export default { billingItemTop10: [], // 开单品项TOP10 tkStatisticsData: null, customerVisitFrequencyData: [], + // 会员统计数据 + memberStatistics: { + totalMembers: 0, + newMembers: 0, + newMembersLastMonth: 0, + active_0_3: 0, + active_4_59: 0, + activeMembers: 0, + activeRate: 0, + activeRate30: 0, + totalRemainingAmount: 0, + avgRemainingAmount: 0, + topRemainingMemberName: '', + topRemainingAmount: 0, + topBillingMemberName: '', + topBillingAmount: 0, + topConsumeMemberName: '', + topConsumeAmount: 0, + totalSleepMembers: 0, + sleep_60_89: 0, + sleep_90_179: 0, + sleep_180_359: 0, + sleep_360_plus: 0, + beautyMembers: 0, + medicalMembers: 0, + techMembers: 0, + educationMembers: 0 + }, + memberTypeDistribution: [], // 会员类型分布 // 内部状态 loading: false, showFieldConfigDialog: false, + // 科技感弹窗 + showTechModal: false, + techModalTitle: '会员画像穿透预览', + techModalWidth: '960px', + techModalHeight: '70vh', availableFields: [ { prop: 'StoreName', label: '门店名称' }, { prop: 'BillingAmount', label: '开单金额' }, @@ -242,7 +553,7 @@ export default { const d = this.kpiData || {} return [ { label: '本月成交总额', value: this.formatMoney(d.TotalBillingAmount), icon: 'el-icon-wallet', type: 'primary', isMoney: true }, - { label: '本月消耗价值', value: this.formatMoney(d.TotalConsumeAmount), icon: 'el-icon-medal', type: 'success', isMoney: true }, + { label: '本月消耗金额', value: this.formatMoney(d.TotalConsumeAmount), icon: 'el-icon-medal', type: 'success', isMoney: true }, { label: '完成业绩(净额)', value: this.formatMoney(d.CompletedBillingAmount), icon: 'el-icon-trophy', type: 'warning', isMoney: true }, { label: '开单目标达成', value: d.BillingCompletionRate || 0, icon: 'el-icon-pie-chart', type: 'info', isPercent: true, target: this.formatMoney(d.TargetBillingAmount), status: (d.BillingCompletionRate >= 100) ? 'up' : 'down' }, { label: '本月拓客人数', value: (this.tkStatisticsData && this.tkStatisticsData.TkCount) ? this.tkStatisticsData.TkCount : 0, icon: 'el-icon-user-solid', type: 'danger', isPercent: false, target: null, status: null }, @@ -296,12 +607,28 @@ export default { beforeDestroy() { window.removeEventListener('resize', this.handleResize) Object.values(this.charts).forEach(c => c && c.dispose()) + if (this.charts.memberTypeChart) { + this.charts.memberTypeChart.dispose() + } + if (this.charts.memberCategoryChart) { + this.charts.memberCategoryChart.dispose() + } }, methods: { // 设置默认查询范围:本月 setDefaultTimeRange() { this.query.month = dayjs().format('YYYY-MM') }, + // 打开科技感弹窗 + openTechModal() { + this.showTechModal = true + }, + // 重置弹窗大小与标题 + resetTechModal() { + this.techModalTitle = '会员画像穿透预览' + this.techModalWidth = '960px' + this.techModalHeight = '70vh' + }, // 年月选择器变化处理 handleMonthChange() { // 自动触发查询 @@ -391,7 +718,8 @@ export default { this.loadTrends(trendParams), this.loadTkFunnel(dateParams), this.loadRankings({ ...monthParams, ...dateParams }), - this.loadInsights(dateParams) + this.loadInsights(dateParams), + this.loadMemberStatistics({ statisticsMonth: currentMonth }) ]) // 确保数据加载完成后再渲染图表 await this.$nextTick() @@ -603,6 +931,82 @@ export default { this.goldTriangleRankings = [] } }, + async loadMemberStatistics(p) { + try { + const res = await request({ + url: '/api/Extend/LqReport/get-dashboard-data', + method: 'POST', + data: { + StatisticsMonth: p.statisticsMonth + } + }) + if (res.data && res.data.Success && res.data.Data && res.data.Data.MemberStatistics) { + const ms = res.data.Data.MemberStatistics + const active_0_3 = ms.ActiveMembers0_3 || 0 + const active_4_59 = ms.ActiveMembers4_59 || 0 + const totalMembers = ms.TotalMembers || 0 + const active30 = ms.ActiveMembers || 0 + const active60 = active_0_3 + active_4_59 + const activeRate60 = totalMembers > 0 ? Math.round((active60 / totalMembers) * 10000) / 100 : 0 + const activeRate30 = totalMembers > 0 ? Math.round((active30 / totalMembers) * 10000) / 100 : 0 + this.memberStatistics = { + totalMembers, + newMembers: ms.NewMembersThisMonth || 0, + newMembersLastMonth: ms.NewMembersLastMonth || 0, + active_0_3, + active_4_59, + // 活跃会员数:沉睡天数 ≤ 59 天(即活跃 + 常到店) + activeMembers: active60, + // 60天活跃率 + activeRate: activeRate60, + // 30天活跃率(后端ActiveMembers本身为30天口径) + activeRate30, + totalRemainingAmount: ms.TotalRemainingAmount || 0, + avgRemainingAmount: ms.AvgRemainingAmount || 0, + topRemainingMemberName: ms.TopRemainingMemberName || '', + topRemainingAmount: ms.TopRemainingAmount || 0, + topBillingMemberName: ms.TopBillingMemberName || '', + topBillingAmount: ms.TopBillingAmount || 0, + topConsumeMemberName: ms.TopConsumeMemberName || '', + topConsumeAmount: ms.TopConsumeAmount || 0, + totalSleepMembers: ms.TotalSleepMembers || 0, + sleep_60_89: ms.SleepMembers60_89 || 0, + sleep_90_179: ms.SleepMembers90_179 || 0, + sleep_180_359: ms.SleepMembers180_359 || 0, + sleep_360_plus: ms.SleepMembers360Plus || 0, + beautyMembers: ms.BeautyMembers || 0, + medicalMembers: ms.MedicalMembers || 0, + techMembers: ms.TechMembers || 0, + educationMembers: ms.EducationMembers || 0 + } + this.memberTypeDistribution = ms.MemberTypeDistribution || [] + // 等待DOM更新后渲染会员类型分布饼图和分类雷达图,并更新tag滚动 + this.$nextTick(() => { + this.initMemberTypeChart() + this.initMemberCategoryChart() + this.updateStatTagMarquee() + }) + } + } catch (error) { + console.error('加载会员统计数据失败:', error) + this.memberStatistics = {} + this.memberTypeDistribution = [] + } + }, + updateStatTagMarquee() { + if (!this.$el) return + const tags = this.$el.querySelectorAll('.member-stat-item .stat-tag') + tags.forEach(tag => { + const inner = tag.querySelector('.stat-tag-inner') + if (!inner) return + // 以整个tag的可视宽度作为判断标准,内容超过tag宽度才滚动 + if (inner.scrollWidth > tag.clientWidth + 1) { + inner.classList.add('is-marquee') + } else { + inner.classList.remove('is-marquee') + } + }) + }, async loadInsights(p) { try { const [freq, item] = await Promise.all([ @@ -673,6 +1077,201 @@ export default { // renderGoldChart 已移除,改为列表显示 this.renderVisitChart() // renderCategoryChart 已移除,改为列表显示 + this.initMemberTypeChart() + this.initMemberCategoryChart() + }, + // 初始化会员类型分布饼图 + initMemberTypeChart() { + if (!this.$refs.memberTypeChart || !this.memberTypeDistribution || this.memberTypeDistribution.length === 0) { + return + } + if (this.charts.memberTypeChart) { + this.charts.memberTypeChart.dispose() + } + const chartDom = this.$refs.memberTypeChart + if (!chartDom) return + this.charts.memberTypeChart = echarts.init(chartDom) + const option = { + tooltip: { + trigger: 'item', + formatter: '{a}
{b}: {c}人 ({d}%)' + }, + legend: { + orient: 'vertical', + left: '5%', + top: 'middle', + itemWidth: 14, + itemHeight: 14, + textStyle: { + fontSize: 13, + color: '#606266', + fontWeight: 'normal' + }, + itemGap: 12 + }, + series: [ + { + name: '会员类型', + type: 'pie', + radius: ['50%', '85%'], + center: ['65%', '50%'], + avoidLabelOverlap: false, + itemStyle: { + borderRadius: 8, + borderColor: '#fff', + borderWidth: 3 + }, + label: { + show: true, + formatter: '{b}\n{c}人\n({d}%)', + fontSize: 12, + fontWeight: 'normal', + color: '#303133', + lineHeight: 16 + }, + labelLine: { + show: true, + length: 15, + length2: 8, + lineStyle: { + width: 1 + } + }, + emphasis: { + label: { + show: true, + fontSize: 13, + fontWeight: 'bold' + }, + itemStyle: { + shadowBlur: 10, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.3)' + } + }, + data: this.memberTypeDistribution.map((item, index) => { + const colors = ['#F56C6C', '#67C23A', '#409EFF', '#E6A23C'] + return { + value: item.Count || 0, + name: item.MemberType || '未知', + itemStyle: { + color: colors[index % colors.length] + } + } + }) + } + ] + } + this.charts.memberTypeChart.setOption(option) + + // 响应式调整 + window.addEventListener('resize', () => { + if (this.charts.memberTypeChart) { + this.charts.memberTypeChart.resize() + } + }) + }, + // 初始化会员分类雷达图(移除教育部会员) + initMemberCategoryChart() { + if (!this.$refs.memberCategoryChart) { + return + } + if (this.charts.memberCategoryChart) { + this.charts.memberCategoryChart.dispose() + } + const chartDom = this.$refs.memberCategoryChart + if (!chartDom) return + this.charts.memberCategoryChart = echarts.init(chartDom) + + // 计算最大值用于归一化(只计算三个分类) + const maxValue = Math.max( + this.memberStatistics.beautyMembers || 0, + this.memberStatistics.medicalMembers || 0, + this.memberStatistics.techMembers || 0 + ) || 100 + + const option = { + tooltip: { + trigger: 'item', + formatter: (params) => { + const name = params.name + let actualValue = 0 + switch (name) { + case '生美会员': actualValue = this.memberStatistics.beautyMembers || 0; break + case '医美会员': actualValue = this.memberStatistics.medicalMembers || 0; break + case '科技部会员': actualValue = this.memberStatistics.techMembers || 0; break + } + return `${name}
${actualValue}人` + } + }, + radar: { + indicator: [ + { name: '生美会员', max: maxValue }, + { name: '医美会员', max: maxValue }, + { name: '科技部会员', max: maxValue } + ], + center: ['50%', '55%'], + radius: '90%', + nameGap: 10, + splitNumber: 4, + axisName: { + color: '#303133', + fontSize: 13, + fontWeight: 'bold' + }, + splitArea: { + areaStyle: { + color: ['rgba(64, 158, 255, 0.1)', 'rgba(64, 158, 255, 0.05)'] + } + }, + splitLine: { + lineStyle: { + color: 'rgba(64, 158, 255, 0.2)' + } + }, + axisLine: { + lineStyle: { + color: 'rgba(64, 158, 255, 0.3)' + } + } + }, + series: [ + { + name: '会员分类统计', + type: 'radar', + data: [ + { + value: [ + this.memberStatistics.beautyMembers || 0, + this.memberStatistics.medicalMembers || 0, + this.memberStatistics.techMembers || 0 + ], + name: '会员分布', + areaStyle: { + color: 'rgba(64, 158, 255, 0.2)' + }, + lineStyle: { + color: '#409EFF', + width: 2 + }, + itemStyle: { + color: '#409EFF' + }, + symbol: 'circle', + symbolSize: 6 + } + ] + } + ] + } + this.charts.memberCategoryChart.setOption(option) + + // 响应式调整 + window.addEventListener('resize', () => { + if (this.charts.memberCategoryChart) { + this.charts.memberCategoryChart.resize() + } + }) }, renderTrendChart() { const chart = this.getChart('revenueTrendChart') @@ -917,6 +1516,10 @@ export default { } }, formatMoney(v) { return Number(v || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }, + formatNumber(v) { + const num = Number(v || 0) + return num.toLocaleString() + }, formatPercent(v) { return Number(v || 0).toFixed(1) }, getTkInviteRate() { if (!this.tkStatisticsData || !this.tkStatisticsData.TkCount) return 0 @@ -1124,6 +1727,240 @@ export default { width: 100%; } + /* 会员统计区域样式 */ + .member-statistics-section { + width: 100%; + margin-right: 0px !important; + margin-left: 0px !important; + } + + /* 统一三个卡片的高度 */ + .member-overview-card, + .member-type-card, + .member-category-card { + &::v-deep .el-card__body { + height: 360px; + padding: 16px; + display: flex; + flex-direction: column; + } + } + + .member-overview-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + flex: 1; + width: 100%; + } + + .member-stat-item { + background: #f5f7fa; + border-radius: 8px; + padding: 16px; + display: flex; + flex-direction: column; + transition: all 0.3s ease; + border: none; + box-shadow: none; + height: 160px; + + &:hover { + box-shadow: 0 8px 20px rgba(31, 45, 61, 0.12); + transform: translateY(-2px); + } + + .stat-main { + display: flex; + align-items: flex-start; + margin-bottom: 12px; + } + + .stat-icon { + width: 56px; + height: 56px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 28px; + margin-right: 14px; + flex-shrink: 0; + } + + &.stat-item-1 .stat-icon { + background: rgba(64, 158, 255, 0.1); + color: #409EFF; + } + + &.stat-item-2 .stat-icon { + background: rgba(103, 194, 58, 0.1); + color: #67C23A; + } + + &.stat-item-3 .stat-icon { + background: rgba(230, 162, 60, 0.1); + color: #E6A23C; + } + + &.stat-item-4 .stat-icon { + background: rgba(245, 108, 108, 0.1); + color: #F56C6C; + } + + .stat-content { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + justify-content: center; + } + + .stat-label { + font-size: 13px; + color: #909399; + font-weight: normal; + margin-bottom: 6px; + } + + .stat-value { + font-size: 26px; + font-weight: bold; + color: #303133; + line-height: 1.2; + } + + .stat-tags { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 6px 8px; + margin-top: 8px; + } + + .stat-tag { + display: block; + background: #fff; + border-radius: 12px; + padding: 6px 10px; + font-size: 12px; + font-weight: 500; + color: #303133; + overflow: hidden; + } + + .stat-tag-inner { + display: inline-flex; + align-items: center; + } + + .stat-tag-text { + display: inline-block; + white-space: nowrap; + } + + .stat-tag-inner.is-marquee { + animation: stat-tag-marquee 8s linear infinite; + } + + &.stat-item-1 .stat-tag-inner i { + color: #409EFF; + } + + &.stat-item-2 .stat-tag-inner i { + color: #67C23A; + } + + &.stat-item-3 .stat-tag-inner i { + color: #E6A23C; + } + + &.stat-item-4 .stat-tag-inner i { + color: #f56c6c; + } + } + + @keyframes stat-tag-marquee { + 0% { + transform: translateX(0); + } + + 100% { + transform: translateX(-100%); + } + } + + /* 会员类型分布图表 */ + .member-type-chart { + height: 100%; + width: 100%; + flex: 1; + min-height: 0; + } + + /* 会员分类统计 */ + .member-category-content { + display: flex; + align-items: center; + gap: 12px; + height: 100%; + flex: 1; + min-height: 0; + } + + .member-category-chart { + flex: 2; + height: 100%; + min-width: 0; + } + + .member-category-legend { + width: 140px; + display: flex; + flex-direction: column; + gap: 12px; + padding: 12px; + background: #f5f7fa; + border-radius: 6px; + } + + .legend-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px; + background: white; + border-radius: 4px; + transition: all 0.3s ease; + + &:hover { + background: #f0f2f5; + } + } + + .legend-color { + width: 24px; + height: 24px; + border-radius: 4px; + flex-shrink: 0; + } + + .legend-text { + flex: 1; + + .legend-name { + font-size: 12px; + font-weight: normal; + color: #606266; + margin-bottom: 2px; + } + + .legend-value { + font-size: 14px; + font-weight: bold; + color: #303133; + } + } + /* 统一卡片高度 */ .analysis-row { .dashboard-card { @@ -1301,5 +2138,152 @@ export default { } } } + + /* 科技感弹窗 */ + ::v-deep .tech-dialog { + .el-dialog__header { + background: linear-gradient(90deg, rgba(64, 158, 255, 0.18), rgba(103, 194, 58, 0.12)); + border-bottom: 1px solid rgba(64, 158, 255, 0.15); + } + + .el-dialog__body { + background: radial-gradient(circle at 20% 20%, rgba(64, 158, 255, 0.08), transparent 35%), + radial-gradient(circle at 80% 20%, rgba(103, 194, 58, 0.08), transparent 35%), + #0f172a; + color: #e5eaf3; + padding: 16px; + } + + .el-dialog { + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35); + border: 1px solid rgba(64, 158, 255, 0.25); + } + } + + .tech-dialog-controls { + display: flex; + gap: 8px; + margin-bottom: 12px; + + .control-item { + width: 180px; + } + } + + .tech-dialog-body { + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 12px; + padding: 12px; + background: rgba(255, 255, 255, 0.03); + overflow: auto; + } + + .tech-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 12px; + } + + .tech-card { + border-radius: 12px; + padding: 14px; + color: #fff; + position: relative; + overflow: hidden; + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25); + } + + .neon-blue { + background: linear-gradient(135deg, #1d4ed8, #0ea5e9); + } + + .neon-green { + background: linear-gradient(135deg, #15803d, #22c55e); + } + + .neon-orange { + background: linear-gradient(135deg, #c2410c, #f97316); + } + + .tech-card-title { + font-size: 13px; + opacity: 0.9; + } + + .tech-card-value { + margin-top: 6px; + font-size: 24px; + font-weight: 700; + } + + .tech-card-desc { + margin-top: 4px; + font-size: 12px; + opacity: 0.8; + } + + .tech-section { + margin-top: 14px; + border-radius: 12px; + padding: 12px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.06); + } + + .tech-section-title { + display: flex; + justify-content: space-between; + color: #9ca3af; + margin-bottom: 10px; + + .subtitle { + font-size: 12px; + opacity: 0.8; + } + } + + .tech-timeline { + display: flex; + flex-direction: column; + gap: 10px; + } + + .tech-timeline-item { + display: grid; + grid-template-columns: 16px 1fr 80px; + align-items: center; + gap: 10px; + padding: 8px 10px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.05); + } + + .tech-timeline-item .dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: #22c55e; + box-shadow: 0 0 8px #22c55e; + margin-left: 3px; + } + + .tech-timeline-item .content .title { + color: #e5e7eb; + font-size: 13px; + font-weight: 600; + } + + .tech-timeline-item .content .desc { + color: #9ca3af; + font-size: 12px; + margin-top: 2px; + } + + .tech-timeline-item .time { + text-align: right; + color: #60a5fa; + font-size: 12px; + } } \ No newline at end of file diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqReimbursementApplicationService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqReimbursementApplicationService.cs index e845661..379798a 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqReimbursementApplicationService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqReimbursementApplicationService.cs @@ -658,7 +658,10 @@ namespace NCC.Extend.LqReimbursementApplication excelconfig.HeadPoint = 10; excelconfig.IsAllSizeColumn = true; excelconfig.ColumnModel = new List(); - List selectKeyList = input.selectKey.Split(',').ToList(); + // 当未传入 selectKey 时,默认导出全部字段,避免空引用异常 + List selectKeyList = !string.IsNullOrEmpty(input.selectKey) + ? input.selectKey.Split(',').ToList() + : paramList.Select(p => p.field).ToList(); foreach (var item in selectKeyList) { var isExist = paramList.Find(p => p.field == item); diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs index 0fb404b..397132c 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs @@ -890,6 +890,109 @@ namespace NCC.Extend var consumePerformance = await _db.Ado.SqlQueryAsync(consumePerformanceSql, new { statisticsMonth }); + // 5. 会员统计汇总 + var lastMonth = DateTime.ParseExact(statisticsMonth, "yyyyMM", null).AddMonths(-1).ToString("yyyyMM"); + var memberStatisticsSql = $@" + SELECT + COUNT(*) as TotalMembers, + SUM(CASE WHEN DATE_FORMAT(F_CreateTime, '%Y%m') = @statisticsMonth THEN 1 ELSE 0 END) as NewMembersThisMonth, + SUM(CASE WHEN DATE_FORMAT(F_CreateTime, '%Y%m') = '{lastMonth}' THEN 1 ELSE 0 END) as NewMembersLastMonth, + SUM(CASE WHEN F_SleepDays IS NULL OR F_SleepDays <= 3 THEN 1 ELSE 0 END) as ActiveMembers0_3, + SUM(CASE WHEN F_SleepDays > 3 AND F_SleepDays < 60 THEN 1 ELSE 0 END) as ActiveMembers4_59, + SUM(CASE WHEN F_LastVisitTime IS NOT NULL AND DATEDIFF(NOW(), F_LastVisitTime) <= 30 THEN 1 ELSE 0 END) as ActiveMembers30d, + SUM(CASE WHEN F_SleepDays >= 60 AND F_SleepDays < 90 THEN 1 ELSE 0 END) as SleepMembers60_89, + SUM(CASE WHEN F_SleepDays >= 90 AND F_SleepDays < 180 THEN 1 ELSE 0 END) as SleepMembers90_179, + SUM(CASE WHEN F_SleepDays >= 180 AND F_SleepDays < 360 THEN 1 ELSE 0 END) as SleepMembers180_359, + SUM(CASE WHEN F_SleepDays >= 360 THEN 1 ELSE 0 END) as SleepMembers360Plus, + COALESCE(SUM(F_RemainingRightsAmount), 0) as TotalRemainingAmount, + SUM(CASE WHEN F_IsBeautyMember = 1 THEN 1 ELSE 0 END) as BeautyMembers, + SUM(CASE WHEN F_IsMedicalMember = 1 THEN 1 ELSE 0 END) as MedicalMembers, + SUM(CASE WHEN F_IsTechMember = 1 THEN 1 ELSE 0 END) as TechMembers, + SUM(CASE WHEN F_IsEducationMember = 1 THEN 1 ELSE 0 END) as EducationMembers + FROM lq_khxx + WHERE F_IsEffective = 1"; + + var memberStatistics = await _db.Ado.SqlQueryAsync(memberStatisticsSql, new { statisticsMonth }); + var memberStats = memberStatistics.FirstOrDefault(); + + // 6. 会员类型分布统计 + var memberTypeDistributionSql = @" + SELECT + CASE + WHEN khlx = '0' THEN '线索' + WHEN khlx = '1' THEN '新客' + WHEN khlx = '2' THEN '散客' + WHEN khlx = '3' THEN '会员' + ELSE '未知' + END as MemberType, + COUNT(*) as Count + FROM lq_khxx + WHERE F_IsEffective = 1 + GROUP BY khlx + ORDER BY + CASE khlx + WHEN '0' THEN 1 + WHEN '1' THEN 2 + WHEN '2' THEN 3 + WHEN '3' THEN 4 + ELSE 5 + END"; + + var memberTypeDistribution = await _db.Ado.SqlQueryAsync(memberTypeDistributionSql); + + // 7. 最高剩余权益会员、本月开单金额最高会员、本月消耗金额最高会员 + var topRemainingSql = @" + SELECT + kh.Khmc as MemberName, + COALESCE(kh.F_RemainingRightsAmount, 0) as Amount + FROM lq_khxx kh + WHERE kh.F_IsEffective = 1 + ORDER BY Amount DESC + LIMIT 1"; + + var topRemaining = (await _db.Ado.SqlQueryAsync(topRemainingSql)).FirstOrDefault(); + + var topBillingSql = @" + SELECT + kh.Khmc as MemberName, + COALESCE(SUM(CAST(kd.sfyj AS DECIMAL(18,2))), 0) as Amount + FROM lq_kd_kdjlb kd + INNER JOIN lq_khxx kh ON kh.F_Id = kd.kdhy + WHERE kd.F_IsEffective = 1 + AND DATE_FORMAT(kd.kdrq, '%Y%m') = @statisticsMonth + GROUP BY kh.F_Id, kh.Khmc + ORDER BY Amount DESC + LIMIT 1"; + + var topBilling = (await _db.Ado.SqlQueryAsync(topBillingSql, new { statisticsMonth })).FirstOrDefault(); + + var topConsumeSql = @" + SELECT + kh.Khmc as MemberName, + COALESCE(SUM(CAST(xh.xfje AS DECIMAL(18,2))), 0) as Amount + FROM lq_xh_hyhk xh + INNER JOIN lq_khxx kh ON kh.F_Id = xh.hy + WHERE xh.F_IsEffective = 1 + AND DATE_FORMAT(xh.hksj, '%Y%m') = @statisticsMonth + GROUP BY kh.F_Id, kh.Khmc + ORDER BY Amount DESC + LIMIT 1"; + + var topConsume = (await _db.Ado.SqlQueryAsync(topConsumeSql, new { statisticsMonth })).FirstOrDefault(); + + var totalMembers = Convert.ToInt32(memberStats?.TotalMembers ?? 0); + var activeMembers0_3 = Convert.ToInt32(memberStats?.ActiveMembers0_3 ?? 0); + var activeMembers4_59 = Convert.ToInt32(memberStats?.ActiveMembers4_59 ?? 0); + var activeMembers = Convert.ToInt32(memberStats?.ActiveMembers30d ?? 0); + var activeRate = totalMembers > 0 ? Math.Round((double)activeMembers / totalMembers * 100, 2) : 0; + var totalRemainingAmount = Convert.ToDecimal(memberStats?.TotalRemainingAmount ?? 0m); + var avgRemainingAmount = totalMembers > 0 ? Math.Round((double)totalRemainingAmount / totalMembers, 2) : 0; + + var sleep60_89 = Convert.ToInt32(memberStats?.SleepMembers60_89 ?? 0); + var sleep90_179 = Convert.ToInt32(memberStats?.SleepMembers90_179 ?? 0); + var sleep180_359 = Convert.ToInt32(memberStats?.SleepMembers180_359 ?? 0); + var sleep360Plus = Convert.ToInt32(memberStats?.SleepMembers360Plus ?? 0); + var result = new { StatisticsMonth = statisticsMonth, @@ -914,7 +1017,6 @@ namespace NCC.Extend GoldTriangleCount = goldTrianglePerformance.FirstOrDefault()?.GoldTriangleCount ?? 0, TotalPerformance = goldTrianglePerformance.FirstOrDefault()?.TotalPerformance ?? 0, TotalOrderCount = goldTrianglePerformance.FirstOrDefault()?.TotalOrderCount ?? 0, - TotalMemberCount = goldTrianglePerformance.FirstOrDefault()?.TotalMemberCount ?? 0, AvgPerformance = goldTrianglePerformance.FirstOrDefault()?.AvgPerformance ?? 0 }, ConsumePerformance = new @@ -924,6 +1026,38 @@ namespace NCC.Extend TotalConsumeQuantity = consumePerformance.FirstOrDefault()?.TotalConsumeQuantity ?? 0, TotalHeadCount = consumePerformance.FirstOrDefault()?.TotalHeadCount ?? 0, TotalPersonCount = consumePerformance.FirstOrDefault()?.TotalPersonCount ?? 0 + }, + MemberStatistics = new + { + TotalMembers = totalMembers, + NewMembersThisMonth = Convert.ToInt32(memberStats?.NewMembersThisMonth ?? 0), + NewMembersLastMonth = Convert.ToInt32(memberStats?.NewMembersLastMonth ?? 0), + ActiveMembers0_3 = activeMembers0_3, + ActiveMembers4_59 = activeMembers4_59, + ActiveMembers = activeMembers, + ActiveRate = activeRate, + SleepMembers60_89 = sleep60_89, + SleepMembers90_179 = sleep90_179, + SleepMembers180_359 = sleep180_359, + SleepMembers360Plus = sleep360Plus, + TotalSleepMembers = sleep60_89 + sleep90_179 + sleep180_359 + sleep360Plus, + TotalRemainingAmount = totalRemainingAmount, + AvgRemainingAmount = avgRemainingAmount, + TopRemainingMemberName = topRemaining?.MemberName ?? string.Empty, + TopRemainingAmount = Convert.ToDecimal(topRemaining?.Amount ?? 0m), + TopBillingMemberName = topBilling?.MemberName ?? string.Empty, + TopBillingAmount = Convert.ToDecimal(topBilling?.Amount ?? 0m), + TopConsumeMemberName = topConsume?.MemberName ?? string.Empty, + TopConsumeAmount = Convert.ToDecimal(topConsume?.Amount ?? 0m), + BeautyMembers = Convert.ToInt32(memberStats?.BeautyMembers ?? 0), + MedicalMembers = Convert.ToInt32(memberStats?.MedicalMembers ?? 0), + TechMembers = Convert.ToInt32(memberStats?.TechMembers ?? 0), + EducationMembers = Convert.ToInt32(memberStats?.EducationMembers ?? 0), + MemberTypeDistribution = memberTypeDistribution.Select(x => new + { + MemberType = x.MemberType?.ToString() ?? "", + Count = Convert.ToInt32(x.Count ?? 0) + }).ToList() } }; diff --git a/sql/排查生美业绩统计差异-简化版.sql b/sql/排查生美业绩统计差异-简化版.sql index c117101..f1508bf 100644 --- a/sql/排查生美业绩统计差异-简化版.sql +++ b/sql/排查生美业绩统计差异-简化版.sql @@ -131,3 +131,4 @@ ORDER BY 生美业绩 DESC; + diff --git a/sql/排查生美业绩统计差异详细.sql b/sql/排查生美业绩统计差异详细.sql index de0263b..8a90590 100644 --- a/sql/排查生美业绩统计差异详细.sql +++ b/sql/排查生美业绩统计差异详细.sql @@ -183,3 +183,4 @@ HAVING COUNT(*) > 1; + diff --git a/sql/检查生美业绩统计差异.sql b/sql/检查生美业绩统计差异.sql index f377371..a1357e3 100644 --- a/sql/检查生美业绩统计差异.sql +++ b/sql/检查生美业绩统计差异.sql @@ -153,3 +153,4 @@ ORDER BY 生美业绩 DESC; + diff --git a/test_goddess_card_members.sh b/test_goddess_card_members.sh new file mode 100755 index 0000000..39cb6e7 --- /dev/null +++ b/test_goddess_card_members.sh @@ -0,0 +1,135 @@ +#!/bin/bash + +# 测试女神卡列表接口(带门店筛选) + +echo "==========================================" +echo "测试女神卡列表接口" +echo "==========================================" + +# 获取token +echo "1. 获取登录token..." +TOKEN=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e" | \ + python3 -c "import sys, json; print(json.load(sys.stdin)['data']['token'])" 2>/dev/null) + +if [ -z "$TOKEN" ]; then + echo "❌ 获取token失败" + exit 1 +fi + +echo "✅ Token获取成功" +echo "" + +# 测试1: 不带门店筛选 +echo "2. 测试1: 不带门店筛选..." +RESPONSE1=$(curl -s -X POST "http://localhost:2011/api/Extend/lqstatistics/GetGoddessCardMembers" \ + -H "Authorization: $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "PageIndex": 1, + "PageSize": 5 + }') + +echo "$RESPONSE1" | python3 -c " +import sys, json +try: + data = json.load(sys.stdin) + if data.get('code') == 200: + total = data.get('data', {}).get('pagination', {}).get('total', 0) + count = len(data.get('data', {}).get('list', [])) + print(f'✅ 成功 - 总记录数: {total}, 当前页记录数: {count}') + else: + print(f'❌ 失败 - Code: {data.get(\"code\")}, Msg: {data.get(\"msg\")}') +except Exception as e: + print(f'❌ 解析失败: {e}') + print(sys.stdin.read()) +" 2>/dev/null || echo "$RESPONSE1" | head -10 +echo "" + +# 获取一个门店ID用于测试 +echo "3. 获取门店ID..." +STORE_ID=$(curl -s -X GET "http://localhost:2011/api/Extend/lqmdxx?currentPage=1&pageSize=1" \ + -H "Authorization: $TOKEN" | \ + python3 -c "import sys, json; data = json.load(sys.stdin); print(data.get('data', {}).get('list', [{}])[0].get('id', ''))" 2>/dev/null) + +if [ -z "$STORE_ID" ]; then + echo "❌ 获取门店ID失败" + exit 1 +fi + +echo "✅ 门店ID: $STORE_ID" +echo "" + +# 测试2: 带单个门店筛选 +echo "4. 测试2: 带单个门店筛选 (StoreId)..." +RESPONSE2=$(curl -s -X POST "http://localhost:2011/api/Extend/lqstatistics/GetGoddessCardMembers" \ + -H "Authorization: $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"PageIndex\": 1, + \"PageSize\": 5, + \"StoreId\": \"$STORE_ID\" + }") + +echo "$RESPONSE2" | python3 -c " +import sys, json +try: + data = json.load(sys.stdin) + if data.get('code') == 200: + total = data.get('data', {}).get('pagination', {}).get('total', 0) + count = len(data.get('data', {}).get('list', [])) + stores = set([m.get('storeName', '') for m in data.get('data', {}).get('list', [])]) + print(f'✅ 成功 - 总记录数: {total}, 当前页记录数: {count}') + if stores: + print(f' 门店列表: {stores}') + else: + print(f'❌ 失败 - Code: {data.get(\"code\")}, Msg: {data.get(\"msg\")}') +except Exception as e: + print(f'❌ 解析失败: {e}') + print(sys.stdin.read()) +" 2>/dev/null || echo "$RESPONSE2" | head -10 +echo "" + +# 测试3: 带多个门店筛选 +echo "5. 测试3: 带多个门店筛选 (StoreIds)..." +STORE_ID2=$(curl -s -X GET "http://localhost:2011/api/Extend/lqmdxx?currentPage=1&pageSize=2" \ + -H "Authorization: $TOKEN" | \ + python3 -c "import sys, json; data = json.load(sys.stdin); stores = data.get('data', {}).get('list', []); print(stores[1].get('id', '') if len(stores) > 1 else '')" 2>/dev/null) + +if [ -n "$STORE_ID2" ]; then + RESPONSE3=$(curl -s -X POST "http://localhost:2011/api/Extend/lqstatistics/GetGoddessCardMembers" \ + -H "Authorization: $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"PageIndex\": 1, + \"PageSize\": 5, + \"StoreIds\": [\"$STORE_ID\", \"$STORE_ID2\"] + }") + + echo "$RESPONSE3" | python3 -c " +import sys, json +try: + data = json.load(sys.stdin) + if data.get('code') == 200: + total = data.get('data', {}).get('pagination', {}).get('total', 0) + count = len(data.get('data', {}).get('list', [])) + stores = set([m.get('storeName', '') for m in data.get('data', {}).get('list', [])]) + print(f'✅ 成功 - 总记录数: {total}, 当前页记录数: {count}') + if stores: + print(f' 门店列表: {stores}') + else: + print(f'❌ 失败 - Code: {data.get(\"code\")}, Msg: {data.get(\"msg\")}') +except Exception as e: + print(f'❌ 解析失败: {e}') + print(sys.stdin.read()) +" 2>/dev/null || echo "$RESPONSE3" | head -10 +else + echo "⚠️ 只有一个门店,跳过多门店筛选测试" +fi +echo "" + +echo "==========================================" +echo "测试完成" +echo "==========================================" + diff --git a/test_tianwang_api.py b/test_tianwang_api.py index 751945e..2530dd2 100644 --- a/test_tianwang_api.py +++ b/test_tianwang_api.py @@ -84,3 +84,4 @@ print(f"\n教育一部+教育二部合计 BillingPerformance: {total2}") + diff --git a/test_update_billing_info.sh b/test_update_billing_info.sh new file mode 100755 index 0000000..d677dfd --- /dev/null +++ b/test_update_billing_info.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +# 测试修改开单信息接口 +# 接口地址: PUT /api/Extend/lqkdkdjlb/UpdateBillingInfo + +echo "==========================================" +echo "测试修改开单信息接口" +echo "==========================================" + +# 获取token +echo "1. 获取登录token..." +TOKEN=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e" | \ + python3 -c "import sys, json; print(json.load(sys.stdin)['data']['token'])" 2>/dev/null) + +if [ -z "$TOKEN" ]; then + echo "❌ 获取token失败" + exit 1 +fi + +echo "✅ Token获取成功" +echo "" + +# 获取一个有效的开单记录ID +echo "2. 获取开单记录ID..." +BILLING_ID=$(curl -s -X GET "http://localhost:2011/api/Extend/lqkdkdjlb?currentPage=1&pageSize=1" \ + -H "Authorization: $TOKEN" | \ + python3 -c "import sys, json; data = json.load(sys.stdin); print(data.get('data', {}).get('list', [{}])[0].get('id', ''))" 2>/dev/null) + +if [ -z "$BILLING_ID" ]; then + echo "❌ 获取开单记录ID失败" + exit 1 +fi + +echo "✅ 开单记录ID: $BILLING_ID" +echo "" + +# 测试1: 只更新备注和简介 +echo "3. 测试1: 只更新备注和简介..." +RESPONSE1=$(curl -s -X PUT "http://localhost:2011/api/Extend/lqkdkdjlb/UpdateBillingInfo" \ + -H "Authorization: $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"id\": \"$BILLING_ID\", + \"Bz\": \"测试备注信息 - $(date +%Y-%m-%d\ %H:%M:%S)\", + \"Jj\": \"测试简介信息 - $(date +%Y-%m-%d\ %H:%M:%S)\" + }") + +echo "$RESPONSE1" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE1" +echo "" + +# 测试2: 只更新备注 +echo "4. 测试2: 只更新备注..." +RESPONSE2=$(curl -s -X PUT "http://localhost:2011/api/Extend/lqkdkdjlb/UpdateBillingInfo" \ + -H "Authorization: $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"id\": \"$BILLING_ID\", + \"Bz\": \"只更新备注 - $(date +%Y-%m-%d\ %H:%M:%S)\" + }") + +echo "$RESPONSE2" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE2" +echo "" + +# 测试3: 只更新简介 +echo "5. 测试3: 只更新简介..." +RESPONSE3=$(curl -s -X PUT "http://localhost:2011/api/Extend/lqkdkdjlb/UpdateBillingInfo" \ + -H "Authorization: $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"id\": \"$BILLING_ID\", + \"Jj\": \"只更新简介 - $(date +%Y-%m-%d\ %H:%M:%S)\" + }") + +echo "$RESPONSE3" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE3" +echo "" + +echo "==========================================" +echo "测试完成" +echo "==========================================" + diff --git a/用户画像数据清单.md b/用户画像数据清单.md new file mode 100644 index 0000000..062c991 --- /dev/null +++ b/用户画像数据清单.md @@ -0,0 +1,77 @@ +# 用户画像数据清单(单个会员视角) + +> 目的:快速落地单个会员画像,可直接映射到前端“会员画像”页面。优先复用现有接口与字段,后续补充按需扩展。 + +## 一、基础档案 +- 会员ID、姓名、手机号 +- 所属门店(gsmd + 门店名)、渠道/来源(khly) +- 创建时间、首次到店时间、最后到店时间(firstVisitTime, lastVisitTime) +- 会员类型标识:生美/医美/科技部/教育部(及标记时间) +- 消费等级(consumeLevel)、消费等级更新时间(consumeLevelUpdateTime) +- 睡眠天数(sleepDays),睡眠开始时间(sleepStartTime) +- 拓客人员、推荐人、主/副健康师 +- 备注 + +## 二、账户资产 / 权益 +- 剩余权益总金额(RemainingRightsAmount) +- 剩余权益明细(`GetMemberRemainingItems`): + - 品项ID/名称、单价、来源类型(SourceType) + - 总购买数量、已消费数量、已退卡数量、储扣数量、剩余数量 + - 剩余价值:单价 * 剩余数量(前端可计算汇总) +- 权益结构:按来源/品项分类的剩余占比(前端聚合) +- 未拆明细金额占比(如需):整单实付 - 明细合计(用于提示权益偏大风险) + +## 三、消费与开单行为 +- 累计开单金额(totalBillingAmount) +- 累计消耗金额(totalConsumeAmount) +- 退卡总金额(totalRefundAmount,如需额外查询) +- 最近一次开单日期、最近一次消耗日期(可由开单/耗卡记录推导) +- 开单/消耗频次(如需:最近30/60/90天内次数) +- 订单类型:首单/升单(已有获取客户订单类型接口) + +## 四、活跃度与到店 +- 睡眠分层:≤3天活跃,4-59天常到店,60-89/90-179/180-359/360+天沉睡 +- 到店频次:总到店次数、最近 N 天到店次数(可用消费或预约记录统计) +- 平均到店间隔(基于首次/最后到店与到店次数估算) + +## 五、品项偏好 +- 购买 Top N(按累计购买金额/次数) +- 剩余 Top N(按剩余价值/剩余次数) +- 来源类型分布(购买/赠送/活动等) + +## 六、门店与人员关联 +- 所属门店的业绩对比:门店消费/开单业绩(可用 store-consume-performance / store-statistics) +- 服务人员关联:主/副健康师的业绩对比(可用 tech-performance / department-consume-performance 做参考) +- 拓客/推荐链路:拓客人、推荐人、渠道 + +## 七、成长与分层 +- 消费等级(0-5)与更新时间 +- 累计消费区间(可映射标签:高净值/中等/低) +- 成长轨迹:近12个月消费趋势(可用业务统计接口按月聚合) + +## 八、风险与预警 +- 高剩余权益 + 长期未到店(remainingRightsAmount 高 & lastVisitTime 久远) +- 睡眠预警:sleepDays > 阈值 +- 明细缺失预警:实付总额远大于品项权益合计 +- 权益即将过期(如有到期规则,可扩展) + +## 九、可直接复用的接口 +- 会员基础/累计字段:`/api/Extend/LqKhxx/get-list`、`/api/Extend/LqKhxx/GetInfo/{id}` +- 权益明细:`/api/Extend/lqkhxx/GetMemberRemainingItems` +- 门店/部门/技术业绩(对比参考): + - `/api/Extend/LqStatistics/get-store-consume-performance-statistics-list` + - `/api/Extend/LqStatistics/get-department-consume-performance-statistics-list` + - `/api/Extend/LqStatistics/get-tech-performance-statistics-list` +- 门店邀约/预约/开单/消耗:`/api/Extend/lqstatistics/get-store-statistics-list` +- 线索/拓客:`/api/Extend/lqstatistics/get-lead-customer-statistics-list` +- 全局仪表盘(会员活跃、类型分布等参考):`/api/Extend/LqReport/get-dashboard-data` + +## 十、前端呈现建议 +- 页头:基础档案 + 核心指标(剩余权益、消费等级、睡眠天数、最近到店) +- 权益资产:剩余权益总额 + 明细表 + 剩余结构饼图 +- 行为概览:近 30/60/90 天开单/消耗次数与金额,最近一次开单/消耗时间 +- 活跃/到店:睡眠分层、到店频次、平均间隔 +- 品项偏好:购买/剩余 Top N,来源类型分布 +- 关联与对比:门店均值对比,服务人员业绩对比 +- 风险提示:高余额未到店、明细缺失、沉睡预警 + -- libgit2 0.21.4