Commit 7b15acce33409eba8e92f3fb41f1595387daa710
1 parent
7718b4dc
feat: 添加拓客活动漏斗统计和门店顾客详情接口
- 新增GetFunnelStatistics接口:获取按门店分组的漏斗统计数据 - 新增GetOverallFunnelStatistics接口:获取总体漏斗统计数据 - 新增GetStoreCustomerDetails接口:获取门店顾客详情(不分页) - 新增GetStoreCustomerDetailsPaged接口:获取门店顾客详情(分页) - 修正SQL字段名:将yy.F_AppointmentTime改为yy.yysj - 支持拓客数量、预约人数、耗卡人数、耗卡金额统计 - 支持预约转化率和耗卡转化率计算 - 支持按拓客时间倒序排列
Showing
1 changed file
with
239 additions
and
2 deletions
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 | } |