Commit 7b15acce33409eba8e92f3fb41f1595387daa710

Authored by “wangming”
1 parent 7718b4dc

feat: 添加拓客活动漏斗统计和门店顾客详情接口

- 新增GetFunnelStatistics接口:获取按门店分组的漏斗统计数据
- 新增GetOverallFunnelStatistics接口:获取总体漏斗统计数据
- 新增GetStoreCustomerDetails接口:获取门店顾客详情(不分页)
- 新增GetStoreCustomerDetailsPaged接口:获取门店顾客详情(分页)
- 修正SQL字段名:将yy.F_AppointmentTime改为yy.yysj
- 支持拓客数量、预约人数、耗卡人数、耗卡金额统计
- 支持预约转化率和耗卡转化率计算
- 支持按拓客时间倒序排列
netcore/src/Modularity/Extend/NCC.Extend/LqTkjlbService.cs
@@ -169,6 +169,8 @@ namespace NCC.Extend.LqTkjlb @@ -169,6 +169,8 @@ namespace NCC.Extend.LqTkjlb
169 { 169 {
170 throw NCCException.Oh("未找到对应的拓客活动用户信息,请确认活动ID和用户ID是否正确"); 170 throw NCCException.Oh("未找到对应的拓客活动用户信息,请确认活动ID和用户ID是否正确");
171 } 171 }
  172 + var MemberNumber = "LQ" + DateTime.Now.ToString("yyyyMMddHHmmssfff");
  173 + var MemberId = YitIdHelper.NextId().ToString();
172 var eventUserInfo = eventUserInfoList.First(); 174 var eventUserInfo = eventUserInfoList.First();
173 // 创建拓客记录 175 // 创建拓客记录
174 var entity = input.Adapt<LqTkjlbEntity>(); 176 var entity = input.Adapt<LqTkjlbEntity>();
@@ -177,16 +179,17 @@ namespace NCC.Extend.LqTkjlb @@ -177,16 +179,17 @@ namespace NCC.Extend.LqTkjlb
177 entity.StoreId = eventUserInfo.StoreId; 179 entity.StoreId = eventUserInfo.StoreId;
178 entity.DepId = eventUserInfo.DepId; 180 entity.DepId = eventUserInfo.DepId;
179 entity.ExpansionTime = DateTime.Now; 181 entity.ExpansionTime = DateTime.Now;
  182 + entity.MemberId = MemberId;
180 var isOk = await _db.Insertable(entity).IgnoreColumns(ignoreNullColumn: true).ExecuteCommandAsync(); 183 var isOk = await _db.Insertable(entity).IgnoreColumns(ignoreNullColumn: true).ExecuteCommandAsync();
181 if (!(isOk > 0)) 184 if (!(isOk > 0))
182 throw NCCException.Oh("创建拓客记录失败"); 185 throw NCCException.Oh("创建拓客记录失败");
183 // 创建客户信息 186 // 创建客户信息
184 LqKhxxEntity MemberInfo = new LqKhxxEntity(); 187 LqKhxxEntity MemberInfo = new LqKhxxEntity();
185 - MemberInfo.Id = YitIdHelper.NextId().ToString(); 188 + MemberInfo.Id = MemberId;
186 MemberInfo.Khmc = entity.CustomerName; 189 MemberInfo.Khmc = entity.CustomerName;
187 MemberInfo.Sjh = input.customerPhone; // 设置手机号 190 MemberInfo.Sjh = input.customerPhone; // 设置手机号
188 MemberInfo.Khlx = MemberTypeEnum.线索.GetHashCode().ToString(); 191 MemberInfo.Khlx = MemberTypeEnum.线索.GetHashCode().ToString();
189 - MemberInfo.Dah = "LQ" + DateTime.Now.ToString("yyyyMMddHHmmssfff"); 192 + MemberInfo.Dah = MemberNumber;
190 MemberInfo.Jdqd = "19.9卡"; 193 MemberInfo.Jdqd = "19.9卡";
191 194
192 //找到input.expansionUserId的用户信息 195 //找到input.expansionUserId的用户信息
@@ -526,5 +529,239 @@ namespace NCC.Extend.LqTkjlb @@ -526,5 +529,239 @@ namespace NCC.Extend.LqTkjlb
526 } 529 }
527 #endregion 530 #endregion
528 531
  532 + #region 漏斗统计
  533 + /// <summary>
  534 + /// 获取拓客活动漏斗统计数据
  535 + /// </summary>
  536 + /// <param name="eventId">活动ID</param>
  537 + /// <returns>漏斗统计数据</returns>
  538 + [HttpGet("GetFunnelStatistics/{eventId}")]
  539 + public async Task<dynamic> GetFunnelStatistics(string eventId)
  540 + {
  541 + try
  542 + {
  543 + var sql = @"
  544 + SELECT
  545 + md.F_Id as store_id,
  546 + md.dm as store_name,
  547 + COUNT(DISTINCT tk.F_Id) as tk_count, -- 拓客数量
  548 + COUNT(DISTINCT CASE WHEN yy.F_Id IS NOT NULL THEN tk.F_CustomerPhone END) as yy_count, -- 预约人数(已确认)
  549 + COUNT(DISTINCT CASE WHEN xh.F_Id IS NOT NULL THEN tk.F_CustomerPhone END) as hk_count, -- 耗卡人数
  550 + COALESCE(SUM(CASE WHEN xh.F_Id IS NOT NULL THEN xh.xfje END), 0) as hk_amount, -- 耗卡金额
  551 + ROUND(
  552 + COUNT(DISTINCT CASE WHEN yy.F_Id IS NOT NULL THEN tk.F_CustomerPhone END) * 100.0 /
  553 + COUNT(DISTINCT tk.F_Id), 2
  554 + ) as yy_conversion_rate, -- 预约转化率(预约人数/拓客数量)
  555 + ROUND(
  556 + COUNT(DISTINCT CASE WHEN xh.F_Id IS NOT NULL THEN tk.F_CustomerPhone END) * 100.0 /
  557 + COUNT(DISTINCT CASE WHEN yy.F_Id IS NOT NULL THEN tk.F_CustomerPhone END), 2
  558 + ) as hk_conversion_rate -- 耗卡转化率(耗卡人数/预约人数)
  559 + FROM lq_tkjlb tk
  560 + JOIN lq_mdxx md ON tk.F_StoreId = md.F_Id
  561 + LEFT JOIN lq_yyjl yy ON tk.F_MemberId = yy.gk
  562 + AND yy.F_Status = '已确认'
  563 + LEFT JOIN lq_xh_hyhk xh ON tk.F_MemberId = xh.hy
  564 + AND xh.F_IsEffective = 1
  565 + WHERE tk.F_EventId = @eventId
  566 + GROUP BY md.F_Id, md.dm
  567 + ORDER BY tk_count DESC";
  568 +
  569 + var result = await _db.Ado.SqlQueryAsync<dynamic>(sql, new { eventId });
  570 +
  571 + return new
  572 + {
  573 + success = true,
  574 + data = result,
  575 + message = "获取漏斗统计数据成功"
  576 + };
  577 + }
  578 + catch (Exception ex)
  579 + {
  580 + throw NCCException.Oh("获取漏斗统计数据失败:" + ex.Message);
  581 + }
  582 + }
  583 +
  584 + /// <summary>
  585 + /// 获取拓客活动总体漏斗统计
  586 + /// </summary>
  587 + /// <param name="eventId">活动ID</param>
  588 + /// <returns>总体漏斗统计数据</returns>
  589 + [HttpGet("GetOverallFunnelStatistics/{eventId}")]
  590 + public async Task<dynamic> GetOverallFunnelStatistics(string eventId)
  591 + {
  592 + try
  593 + {
  594 + var sql = @"
  595 + SELECT
  596 + COUNT(DISTINCT tk.F_Id) as total_tk_count, -- 总拓客数量
  597 + COUNT(DISTINCT CASE WHEN yy.F_Id IS NOT NULL THEN tk.F_CustomerPhone END) as total_yy_count, -- 总预约人数
  598 + COUNT(DISTINCT CASE WHEN xh.F_Id IS NOT NULL THEN tk.F_CustomerPhone END) as total_hk_count, -- 总耗卡人数
  599 + COALESCE(SUM(CASE WHEN xh.F_Id IS NOT NULL THEN xh.xfje END), 0) as total_hk_amount, -- 总耗卡金额
  600 + ROUND(
  601 + COUNT(DISTINCT CASE WHEN yy.F_Id IS NOT NULL THEN tk.F_CustomerPhone END) * 100.0 /
  602 + COUNT(DISTINCT tk.F_Id), 2
  603 + ) as overall_yy_conversion_rate, -- 总体预约转化率
  604 + ROUND(
  605 + COUNT(DISTINCT CASE WHEN xh.F_Id IS NOT NULL THEN tk.F_CustomerPhone END) * 100.0 /
  606 + COUNT(DISTINCT CASE WHEN yy.F_Id IS NOT NULL THEN tk.F_CustomerPhone END), 2
  607 + ) as overall_hk_conversion_rate -- 总体耗卡转化率
  608 + FROM lq_tkjlb tk
  609 + LEFT JOIN lq_yyjl yy ON tk.F_MemberId = yy.gk
  610 + AND yy.F_Status = '已确认'
  611 + LEFT JOIN lq_xh_hyhk xh ON tk.F_MemberId = xh.hy
  612 + AND xh.F_IsEffective = 1
  613 + WHERE tk.F_EventId = @eventId";
  614 +
  615 + var result = await _db.Ado.SqlQueryAsync<dynamic>(sql, new { eventId });
  616 +
  617 + return new
  618 + {
  619 + success = true,
  620 + data = result?.FirstOrDefault(),
  621 + message = "获取总体漏斗统计数据成功"
  622 + };
  623 + }
  624 + catch (Exception ex)
  625 + {
  626 + throw NCCException.Oh("获取总体漏斗统计数据失败:" + ex.Message);
  627 + }
  628 + }
  629 + #endregion
  630 +
  631 + #region 门店顾客详情
  632 + /// <summary>
  633 + /// 获取门店拓客活动顾客详情
  634 + /// </summary>
  635 + /// <param name="eventId">活动ID</param>
  636 + /// <param name="storeId">门店ID</param>
  637 + /// <returns>门店顾客详情列表</returns>
  638 + [HttpGet("GetStoreCustomerDetails/{eventId}/{storeId}")]
  639 + public async Task<dynamic> GetStoreCustomerDetails(string eventId, string storeId)
  640 + {
  641 + try
  642 + {
  643 + var sql = @"
  644 + SELECT
  645 + tk.F_Id as tk_id, -- 拓客记录ID
  646 + tk.F_CustomerPhone as customer_phone, -- 顾客手机号
  647 + tk.F_MemberId as member_id, -- 会员ID
  648 + tk.F_CustomerName as customer_name, -- 顾客姓名
  649 + tk.F_CreateTime as tk_time, -- 拓客时间
  650 + yy.F_Id as yy_id, -- 预约ID
  651 + yy.F_Status as yy_status, -- 预约状态
  652 + yy.F_CreateTime as yy_time, -- 预约时间
  653 + yy.yysj as appointment_time, -- 预约到店时间
  654 + xh.F_Id as xh_id, -- 耗卡ID
  655 + xh.F_CreateTime as xh_time, -- 耗卡时间
  656 + xh.xfje as consume_amount, -- 耗卡金额
  657 + CASE
  658 + WHEN yy.F_Id IS NOT NULL THEN '已预约'
  659 + ELSE '未预约'
  660 + END as appointment_status, -- 预约状态描述
  661 + CASE
  662 + WHEN xh.F_Id IS NOT NULL THEN '已耗卡'
  663 + ELSE '未耗卡'
  664 + END as consume_status -- 耗卡状态描述
  665 + FROM lq_tkjlb tk
  666 + LEFT JOIN lq_yyjl yy ON tk.F_MemberId = yy.gk
  667 + AND yy.F_Status = '已确认'
  668 + LEFT JOIN lq_xh_hyhk xh ON tk.F_MemberId = xh.hy
  669 + AND xh.F_IsEffective = 1
  670 + WHERE tk.F_EventId = @eventId
  671 + AND tk.F_StoreId = @storeId
  672 + ORDER BY tk.F_CreateTime DESC";
  673 +
  674 + var result = await _db.Ado.SqlQueryAsync<dynamic>(sql, new { eventId, storeId });
  675 +
  676 + return new
  677 + {
  678 + success = true,
  679 + data = result,
  680 + message = "获取门店顾客详情成功"
  681 + };
  682 + }
  683 + catch (Exception ex)
  684 + {
  685 + throw NCCException.Oh("获取门店顾客详情失败:" + ex.Message);
  686 + }
  687 + }
  688 +
  689 + /// <summary>
  690 + /// 获取门店拓客活动顾客详情(分页)
  691 + /// </summary>
  692 + /// <param name="eventId">活动ID</param>
  693 + /// <param name="storeId">门店ID</param>
  694 + /// <param name="pageIndex">页码</param>
  695 + /// <param name="pageSize">页大小</param>
  696 + /// <returns>分页的门店顾客详情列表</returns>
  697 + [HttpGet("GetStoreCustomerDetailsPaged/{eventId}/{storeId}")]
  698 + public async Task<dynamic> GetStoreCustomerDetailsPaged(string eventId, string storeId, int pageIndex = 1, int pageSize = 20)
  699 + {
  700 + try
  701 + {
  702 + var sql = @"
  703 + SELECT
  704 + tk.F_Id as tk_id, -- 拓客记录ID
  705 + tk.F_CustomerPhone as customer_phone, -- 顾客手机号
  706 + tk.F_MemberId as member_id, -- 会员ID
  707 + tk.F_CustomerName as customer_name, -- 顾客姓名
  708 + tk.F_CreateTime as tk_time, -- 拓客时间
  709 + yy.F_Id as yy_id, -- 预约ID
  710 + yy.F_Status as yy_status, -- 预约状态
  711 + yy.F_CreateTime as yy_time, -- 预约时间
  712 + yy.yysj as appointment_time, -- 预约到店时间
  713 + xh.F_Id as xh_id, -- 耗卡ID
  714 + xh.F_CreateTime as xh_time, -- 耗卡时间
  715 + xh.xfje as consume_amount, -- 耗卡金额
  716 + CASE
  717 + WHEN yy.F_Id IS NOT NULL THEN '已预约'
  718 + ELSE '未预约'
  719 + END as appointment_status, -- 预约状态描述
  720 + CASE
  721 + WHEN xh.F_Id IS NOT NULL THEN '已耗卡'
  722 + ELSE '未耗卡'
  723 + END as consume_status -- 耗卡状态描述
  724 + FROM lq_tkjlb tk
  725 + LEFT JOIN lq_yyjl yy ON tk.F_MemberId = yy.gk
  726 + AND yy.F_Status = '已确认'
  727 + LEFT JOIN lq_xh_hyhk xh ON tk.F_MemberId = xh.hy
  728 + AND xh.F_IsEffective = 1
  729 + WHERE tk.F_EventId = @eventId
  730 + AND tk.F_StoreId = @storeId
  731 + ORDER BY tk.F_CreateTime DESC
  732 + LIMIT @offset, @pageSize";
  733 +
  734 + var countSql = @"
  735 + SELECT COUNT(*) as total
  736 + FROM lq_tkjlb tk
  737 + WHERE tk.F_EventId = @eventId
  738 + AND tk.F_StoreId = @storeId";
  739 +
  740 + var offset = (pageIndex - 1) * pageSize;
  741 + var result = await _db.Ado.SqlQueryAsync<dynamic>(sql, new { eventId, storeId, offset, pageSize });
  742 + var totalResult = await _db.Ado.SqlQueryAsync<dynamic>(countSql, new { eventId, storeId });
  743 + var total = totalResult?.FirstOrDefault()?.total ?? 0;
  744 +
  745 + return new
  746 + {
  747 + success = true,
  748 + data = new
  749 + {
  750 + list = result,
  751 + total = total,
  752 + pageIndex = pageIndex,
  753 + pageSize = pageSize,
  754 + totalPages = (int)Math.Ceiling((double)total / pageSize)
  755 + },
  756 + message = "获取门店顾客详情成功"
  757 + };
  758 + }
  759 + catch (Exception ex)
  760 + {
  761 + throw NCCException.Oh("获取门店顾客详情失败:" + ex.Message);
  762 + }
  763 + }
  764 + #endregion
  765 +
529 } 766 }
530 } 767 }