CRM系统签到列表数据重复BUG排查与修复(SQL Server存储过程)
CRM系统签到列表数据重复BUG
排查与修复(SQL Server存储过程)
问题现象
任我行协同CRM系统中,工作轨迹模块的签到列表出现数据重复:
- 同一个客户出现2 次
- 同一签到记录显示2 条
- 关联的"外勤签到转日程"也重复
以用户"XXX"为例,2026-05-21 的签到数据全部翻倍显示。
排查过程
第一步:定位查询来源
签到列表的数据来自存储过程spCRM_SelectSignInList,该过程使用UNION ALL合并两个子查询:
签到子查询 (CRM_SignIn, ObjectType=77) UNION ALL 日程子查询 (CRM_Schedule, ObjectType=15)第二步:发现 BUG1 — 自动生成日程混入
签到时如果IsRollSchedule=True,系统会在CRM_Schedule表自动创建一条"外勤签到转日程-外勤签到"记录,标记CoObjectType='SignIn'。
结果:同一个行为在列表中出现了两次——第1次来自签到子查询(ObjType=77),第2次来自日程子查询(ObjType=15)。数据库中此类自动生成日程共5,645 条。
修复 v1:在日程子查询 WHERE 条件中排除签到自动生成的日程:
AND (a.CoObjectType IS NULL OR a.CoObjectType <> 'SignIn')CoObjectType IS NULL 兼容普通日程(非签到转来的),<> 'SignIn' 排除签到自动生成的日程。
第三步:v1 部署后仍重复!
用户反馈修复后仍然重复。进一步分析发现:重复的全部是 ObjectType=77(签到本身),不是日程。说明签到子查询内部还有问题。
第四步:发现 BUG2 — 头像 LEFT JOIN 笛卡尔积
签到子查询用LEFT JOIN CRM_UploadFile取员工头像:
-- 原代码(有BUG) LEFT JOIN CRM_UploadFile d ON d.CoObjectTypeID = a.Creator AND d.CoObject = 'HeadPicture'查询数据库发现:XXX(Creator=0000000004)在 CRM_UploadFile 表中有 2 个头像记录(ID=32351 和 32352,均于2020年上传)。LEFT JOIN 时每条签到匹配到 2 个头像 → 输出 2 行。
| 签到 ID | 描述 | 匹配头像数 | 输出行数 |
|---|---|---|---|
| 6365 | 测试 | 2 | 2 ❌ |
| 6366 | 嘉澜 | 2 | 2 ❌ |
| 6363 | (ZXX) | 1 | 1 ✅ |
修复 v2:用OUTER APPLY (SELECT TOP 1 ...)替代LEFT JOIN,只取最新头像:
-- 修复后 OUTER APPLY ( SELECT TOP 1 FilePath FROM CRM_UploadFile WHERE CoObjectTypeID = a.Creator AND CoObject = 'HeadPicture' ORDER BY ID DESC ) dORDER BY ID DESC取最新上传的头像,TOP 1保证只返回一行,杜绝笛卡尔积。
根因总结
这是两层笛卡尔积叠加导致的重复:
| 层级 | 根因 | 影响机制 |
|---|---|---|
| BUG1(外层) | UNION ALL 把自动生成日程也查出来 | 签到 + 日程 = 每条行为出现 2 次 |
| BUG2(内层) | 头像 LEFT JOIN 一人多头像 | 胡玉发 2 个头像 → 每个签到 ×2 |
极端情况:IsRollSchedule=True + 有 2 个头像 → 单条签到显示4 次。
完整修复脚本
-- ============================================= -- 修复:工作轨迹签到列表重复BUG(完整版 v2) -- 日期:2026-05-21 -- BUG1:UNION ALL 导致签到自动生成的日程重复 -- BUG2:LEFT JOIN CRM_UploadFile 头像笛卡尔积 -- ============================================= USE [grasp_crm04] GO ALTER PROCEDURE [dbo].[spCRM_SelectSignInList] ( @CoScheduleTypeId VARCHAR(30), @Creator VARCHAR(30), @CurrentUser VARCHAR(30), @Keywords NVARCHAR(50), @Department VARCHAR(30), @UserGroup INT, @BeginDate DATETIME, @EndDate DATETIME, @SignInRange INT, @Flag INT = 0, @pageIndex INT, @pageSize INT, @WorkTraceSignOut INT, @TotalCount INT OUTPUT ) AS BEGIN DECLARE @sql NVARCHAR(max), @filter NVARCHAR(max), @child NVARCHAR(max) SET @child = '' SET @TotalCount = 0 DECLARE @startRow INT, @endRow INT SET @startRow = (@pageIndex - 1) * @pageSize + 1 SET @endRow = @startRow + @pageSize - 1 -- 权限控制 DECLARE @bitEmployeeIsAdmin BIT, @bitEmployeeIsManager BIT DECLARE @bitViewPersonalLimit BIT, @bitViewDepartmentLimit BIT DECLARE @isNone BIT, @chvEmployeeDepartment VARCHAR(30) SELECT @bitEmployeeIsAdmin = isAdmin, @bitEmployeeIsManager = isAll, @bitViewPersonalLimit = isSelf, @bitViewDepartmentLimit = isDep, @isNone = isNone, @chvEmployeeDepartment = Department FROM fn_getUserViewRightRange(@CurrentUser, 'WorkTraceView') IF @isNone = 1 BEGIN SELECT * FROM CRM_SignIn WHERE [ID] = 0 RETURN END DECLARE @sortfilter VARCHAR(100) IF @Flag = 0 SET @sortfilter = 'a.CreateDate desc' ELSE SET @sortfilter = 'a.CreateDate asc' DECLARE @employeewhere VARCHAR(2000) SET @employeewhere = ' 1=1 ' IF @bitEmployeeIsAdmin <> 1 AND @bitEmployeeIsManager <> 1 BEGIN IF @bitViewDepartmentLimit = 1 SET @employeewhere = @employeewhere + ' and (ISNULL(b.[Department],''001'') IN (SELECT Department FROM dbo.CRM_WorkTraceVisibleDepartments WHERE [User]=''' + @CurrentUser + ''') Or b.[TypeID] =''' + @CurrentUser + ''')' ELSE IF @bitViewPersonalLimit = 1 SET @employeewhere = @employeewhere + ' and b.[TypeID] =''' + @CurrentUser + '''' ELSE SET @employeewhere = @employeewhere + ' and 1=2 ' END SET @filter = ' where 1=1 ' IF ISNULL(@CoScheduleTypeId, '') <> '' SET @filter = @filter + ' and CoScheduleTypeId =''' + @CoScheduleTypeId + ''' ' IF ISNULL(@Keywords, '') <> '' SET @filter = @filter + ' and (a.Description like @Keywords or a.Address like @Keywords) ' IF ISNULL(@Creator, '') <> '' SET @filter = @filter + ' and a.Creator=@Creator ' ELSE SET @employeewhere = @employeewhere + ' and b.IsActive=1 ' IF ISNULL(@BeginDate, '') <> '' SET @filter = @filter + ' and a.CreateDate>=@BeginDate ' IF ISNULL(@EndDate, '') <> '' SET @filter = @filter + ' and a.CreateDate<=@EndDate ' DECLARE @tempsql VARCHAR(200) SET @tempsql = '' IF @WorkTraceSignOut = 0 SET @tempsql = ' and a.id not in(SELECT distinct CoSignInID FROM dbo.CRM_SignOut) ' IF @WorkTraceSignOut = 1 SET @tempsql = ' and a.id in(SELECT distinct CoSignInID FROM dbo.CRM_SignOut) ' IF ISNULL(@UserGroup, 0) <> 0 BEGIN DECLARE @groupUser VARCHAR(max) SELECT @groupUser = ReceiverTypeID FROM CRM_MsgReceiverGroup WHERE id = @UserGroup SET @filter = @filter + ' and a.Creator in(select a from dbo.f_split(''' + @groupUser + ''','','')) ' END IF ISNULL(@Department, '') <> '' SET @filter = @filter + ' and a.Creator in(SELECT TypeID FROM dbo.CRM_Employee WHERE Department like @Department+''%'' OR Department2 like @Department+''%'' OR Department3 like @Department+''%'') ' -- ============ 签到子查询 ============ IF @SignInRange & 2 = 2 SET @child = @child + ' select a.ID, a.Creator, a.CreateDate, a.Description, a.ReplyAmount, a.SupportAmount, a.Longitude, a.Latitude, a.Address, b.Name as CreatorName, c.ID as SupportID, d.FilePath AS HeadPhoto, cast(a.ID as varchar(30)) TypeID, ObjectType=77, b.Department, Style=1, a.IsRollSchedule, a.IsRollAttendance, a.ModifyDate, a.CheckStatus, a.ScheduleStyle, a.CoScheduleTypeId from CRM_SignIn a inner join CRM_Employee b on b.TypeID=a.Creator -- [修复 BUG2] OUTER APPLY 避免多头像笛卡尔积 OUTER APPLY ( SELECT TOP 1 FilePath FROM CRM_UploadFile WHERE CoObjectTypeID=a.Creator AND CoObject=''HeadPicture'' ORDER BY ID DESC ) d LEFT JOIN dbo.CRM_CoEvaluate c ON c.CoObjectTypeID=a.ID and c.CreateTypeID=@CurrentUser and c.CoObject=''SignIn'' where ' + @employeewhere + @tempsql IF @SignInRange & 3 = 3 SET @child = @child + ' union all ' -- ============ 日程子查询 ============ IF @SignInRange & 1 = 1 SET @child = @child + ' SELECT a.ID, a.CreatorTypeID Creator, a.CreateDate, a.[Subject] Description, f.ReplyAmount, e.SupportAmount, a.Longitude, a.Latitude, a.AdressName Address, b.Name CreatorName, c.ID as SupportID, d.FilePath AS HeadPhoto, a.TypeID, ObjectType=15, a.Department, a.Style, 0 as IsRollSchedule, 0 as IsRollAttendance, a.ModifyDate, '''' as CheckStatus, 0 as ScheduleStyle, '''' as CoScheduleTypeId FROM dbo.CRM_Schedule a INNER JOIN dbo.CRM_Employee b ON b.TypeID=a.CreatorTypeID -- [修复 BUG2] OUTER APPLY 避免多头像笛卡尔积 OUTER APPLY ( SELECT TOP 1 FilePath FROM CRM_UploadFile WHERE CoObjectTypeID=a.CreatorTypeID AND CoObject=''HeadPicture'' ORDER BY ID DESC ) d LEFT JOIN dbo.CRM_CoEvaluate c ON c.CoObjectTypeID=a.TypeID and c.CreateTypeID=@CurrentUser and c.CoObject=''Schedule'' LEFT JOIN ( SELECT COUNT(0) SupportAmount, CoObjectTypeID FROM CRM_CoEvaluate WHERE CoObject=''Schedule'' and Evaluate=''Praise'' GROUP BY CoObjectTypeID ) e ON e.CoObjectTypeID=a.TypeID left join ( SELECT COUNT(0) ReplyAmount, CoScheduleTypeID FROM dbo.CRM_SchedulePostil GROUP BY CoScheduleTypeID ) f on f.CoScheduleTypeID=a.TypeID WHERE ISNULL(a.Longitude,0)<>0 AND ISNULL(a.Latitude,0)<>0 -- [修复 BUG1] 排除签到自动生成的日程 AND (a.CoObjectType IS NULL OR a.CoObjectType <> ''SignIn'') and ' + @employeewhere -- 总数查询 SET @sql = 'select @TotalCount=count(0) from ( ' + @child + ' ) as a ' + @filter EXECUTE sp_executesql @sql, N'@Creator varchar(30),@CurrentUser varchar(30), @chvEmployeeDepartment varchar(30), @Keywords NVARCHAR(50),@BeginDate datetime, @EndDate datetime,@Department varchar(30), @TotalCount INT OUTPUT', @Creator, @CurrentUser, @chvEmployeeDepartment, @Keywords, @BeginDate, @EndDate, @Department, @TotalCount OUTPUT IF @pageIndex = -1 BEGIN SET @sql = ' select * from ( select ROW_NUMBER() OVER( ORDER BY a.Creator asc,' + @sortfilter + ' ) AS rowNum, ID, Creator, CreateDate, Description, ReplyAmount, SupportAmount, Longitude, Latitude, Address, CreatorName, SupportID, HeadPhoto, TypeID, ObjectType, Department, Style, IsRollSchedule, IsRollAttendance, ModifyDate, CheckStatus, ScheduleStyle, CoScheduleTypeId from ( ' + @child + ' ) as a ' + @filter + ' ) as temp ' EXECUTE sp_executesql @sql, N'@Creator varchar(30),@CurrentUser varchar(30), @chvEmployeeDepartment varchar(30), @Keywords NVARCHAR(50),@BeginDate datetime, @EndDate datetime,@Department varchar(30), @startRow int,@endRow int', @Creator, @CurrentUser, @chvEmployeeDepartment, @Keywords, @BeginDate, @EndDate, @Department, @startRow, @endRow END ELSE BEGIN SET @sql = ' select * from ( select ROW_NUMBER() OVER( ORDER BY a.Creator asc,' + @sortfilter + ' ) AS rowNum, ID, Creator, CreateDate, Description, ReplyAmount, SupportAmount, Longitude, Latitude, Address, CreatorName, SupportID, HeadPhoto, TypeID, ObjectType, Department, Style, IsRollSchedule, IsRollAttendance, ModifyDate, CheckStatus, ScheduleStyle, CoScheduleTypeId from ( ' + @child + ' ) as a ' + @filter + ' ) as temp where temp.rowNum >= @startRow and temp.rowNum <= @endRow' EXECUTE sp_executesql @sql, N'@Creator varchar(30),@CurrentUser varchar(30), @chvEmployeeDepartment varchar(30), @Keywords NVARCHAR(50),@BeginDate datetime, @EndDate datetime,@Department varchar(30), @startRow int,@endRow int', @Creator, @CurrentUser, @chvEmployeeDepartment, @Keywords, @BeginDate, @EndDate, @Department, @startRow, @endRow END END GO修复效果对比
| 指标 | 修复前 | 修复后 |
|---|---|---|
| 胡玉发签到数 | 4条(2签到 × 2头像) | 2条✅ |
| ObjType 分布 | 混有 77 和 15 | 全部 77✅ |
| 日程重复 | 有 | 无✅ |
经验总结
排查 SQL 数据重复问题时,建议按以下顺序检查:
- UNION / UNION ALL是否可能产生重复行
- LEFT JOIN 关联字段是否唯一——头像表、附件表、评价表最常见多对一
- 源表本身是否有重复数据
- 多对多中间表(如协同人表)是否被 JOIN 进来导致膨胀
本次排查关键教训:BUG1(UNION ALL)容易发现,BUG2(头像笛卡尔积)比较隐蔽——因为只有头像数量 > 1 的员工才受影响,其他员工看起来一切正常。排查时要注意缩小范围:先看 ObjType 分布判断是哪层出问题,再从 JOIN 链条逐一检查关联唯一性。
