From 679e61057b7eebd9f925b4ab0106263529d7b7b9 Mon Sep 17 00:00:00 2001 From: long <452591453@qq.com> Date: Fri, 22 May 2026 17:01:58 +0800 Subject: [PATCH] =?UTF-8?q?fix(stats/self=5Finput):=20=E5=94=AF=E4=B8=80?= =?UTF-8?q?=E7=B4=A2=E5=BC=95=E5=90=AB=20delete=5Ftime=EF=BC=8C=E8=BD=AF?= =?UTF-8?q?=E5=88=A0=E5=90=8E=E5=8F=AF=E9=87=8D=E6=96=B0=E5=BD=95=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 去掉「同日+同渠道」全局唯一,回到按录入人判重;唯一索引补 delete_time 避免软删占键导致 1062;错误提示带冲突记录 ID,迁移脚本兼容多次执行。 Co-authored-by: Cursor --- .../logic/stats/PersonalAccountCostLogic.php | 11 ++++-- .../logic/stats/PersonalStatsScopeTrait.php | 33 +++++++++++++--- .../logic/stats/PersonalYejiLogic.php | 11 ++++-- .../alter_personal_unique_key.sql | 38 +++++++++++++------ .../create_personal_account_cost_table.sql | 2 +- .../create_personal_yeji_table.sql | 2 +- 6 files changed, 71 insertions(+), 26 deletions(-) diff --git a/server/app/adminapi/logic/stats/PersonalAccountCostLogic.php b/server/app/adminapi/logic/stats/PersonalAccountCostLogic.php index efab4c18..5929408c 100644 --- a/server/app/adminapi/logic/stats/PersonalAccountCostLogic.php +++ b/server/app/adminapi/logic/stats/PersonalAccountCostLogic.php @@ -22,8 +22,9 @@ class PersonalAccountCostLogic extends BaseLogic return false; } - if (self::isCostDuplicate($adminId, $costDate, $mediaSource)) { - self::setError('您在该日期下已录入过该渠道的账户消耗,请直接编辑'); + $dupId = self::findCostDuplicateId($adminId, $costDate, $mediaSource); + if ($dupId > 0) { + self::setError("您在 {$costDate} 已录入过【{$mediaSource}】账户消耗(记录#{$dupId}),请直接编辑该记录"); return false; } @@ -42,7 +43,11 @@ class PersonalAccountCostLogic extends BaseLogic return true; } catch (\Throwable $e) { - self::setError($e->getMessage()); + if (self::isUniqueConstraintViolation($e)) { + self::setError('该日期下该渠道的账户消耗已存在(唯一索引冲突),请刷新列表后直接编辑'); + } else { + self::setError($e->getMessage()); + } return false; } diff --git a/server/app/adminapi/logic/stats/PersonalStatsScopeTrait.php b/server/app/adminapi/logic/stats/PersonalStatsScopeTrait.php index 6fc505bf..63e8aa75 100644 --- a/server/app/adminapi/logic/stats/PersonalStatsScopeTrait.php +++ b/server/app/adminapi/logic/stats/PersonalStatsScopeTrait.php @@ -201,31 +201,52 @@ trait PersonalStatsScopeTrait /** * 同一录入人 + 同一天 + 同一渠道唯一(不同录入人可同日同渠道各录一条)。 + * 命中返回冲突记录 ID,未命中返回 0。 */ - protected static function isYejiDuplicate(int $creatorId, string $yejiDate, string $mediaSource, int $excludeId = 0): bool + protected static function findYejiDuplicateId(int $creatorId, string $yejiDate, string $mediaSource, int $excludeId = 0): int { $query = PersonalYeji::where('creator_id', $creatorId) ->where('yeji_date', $yejiDate) - ->where('media_source', $mediaSource); + ->where('media_source', $mediaSource) + ->whereNull('delete_time'); if ($excludeId > 0) { $query->where('id', '<>', $excludeId); } - return $query->count() > 0; + return (int) ($query->value('id') ?? 0); + } + + protected static function isYejiDuplicate(int $creatorId, string $yejiDate, string $mediaSource, int $excludeId = 0): bool + { + return self::findYejiDuplicateId($creatorId, $yejiDate, $mediaSource, $excludeId) > 0; } /** * 同一录入人 + 同一天 + 同一渠道唯一(不同录入人可同日同渠道各录一条)。 */ - protected static function isCostDuplicate(int $creatorId, string $costDate, string $mediaSource, int $excludeId = 0): bool + protected static function findCostDuplicateId(int $creatorId, string $costDate, string $mediaSource, int $excludeId = 0): int { $query = PersonalAccountCost::where('creator_id', $creatorId) ->where('cost_date', $costDate) - ->where('media_source', $mediaSource); + ->where('media_source', $mediaSource) + ->whereNull('delete_time'); if ($excludeId > 0) { $query->where('id', '<>', $excludeId); } - return $query->count() > 0; + return (int) ($query->value('id') ?? 0); + } + + protected static function isCostDuplicate(int $creatorId, string $costDate, string $mediaSource, int $excludeId = 0): bool + { + return self::findCostDuplicateId($creatorId, $costDate, $mediaSource, $excludeId) > 0; + } + + /** + * MySQL 唯一索引冲突 1062 兜底转友好提示(避免裸 SQL 异常)。 + */ + protected static function isUniqueConstraintViolation(\Throwable $e): bool + { + return (int) $e->getCode() === 23000 || str_contains($e->getMessage(), '1062'); } } diff --git a/server/app/adminapi/logic/stats/PersonalYejiLogic.php b/server/app/adminapi/logic/stats/PersonalYejiLogic.php index 170c076b..53413ddd 100644 --- a/server/app/adminapi/logic/stats/PersonalYejiLogic.php +++ b/server/app/adminapi/logic/stats/PersonalYejiLogic.php @@ -22,8 +22,9 @@ class PersonalYejiLogic extends BaseLogic return false; } - if (self::isYejiDuplicate($adminId, $yejiDate, $mediaSource)) { - self::setError('您在该日期下已录入过该渠道的业绩,请直接编辑'); + $dupId = self::findYejiDuplicateId($adminId, $yejiDate, $mediaSource); + if ($dupId > 0) { + self::setError("您在 {$yejiDate} 已录入过【{$mediaSource}】业绩(记录#{$dupId}),请直接编辑该记录"); return false; } @@ -49,7 +50,11 @@ class PersonalYejiLogic extends BaseLogic return true; } catch (\Throwable $e) { - self::setError($e->getMessage()); + if (self::isUniqueConstraintViolation($e)) { + self::setError('该日期下该渠道的业绩已存在(唯一索引冲突),请刷新列表后直接编辑'); + } else { + self::setError($e->getMessage()); + } return false; } diff --git a/server/sql/1.9.20260521/alter_personal_unique_key.sql b/server/sql/1.9.20260521/alter_personal_unique_key.sql index 08ca4da3..1e6d26cf 100644 --- a/server/sql/1.9.20260521/alter_personal_unique_key.sql +++ b/server/sql/1.9.20260521/alter_personal_unique_key.sql @@ -1,10 +1,15 @@ --- 自录业绩/账户消耗:确保唯一索引为「同录入人 + 同日 + 同渠道」。 --- 可重复执行:不存在则跳过删除;目标索引已存在则跳过添加。 +-- 自录业绩/账户消耗:把唯一索引补上 delete_time,使软删后可重新录入。 -- --- 适用场景: --- 1) 曾改为 uk_date_media(全局唯一)的环境 → 删除 uk_date_media,恢复 uk_creator_date_media --- 2) 一直是 uk_creator_date_media 的环境 → 本脚本全部跳过,不会报 1091 --- 3) 尚无唯一索引的环境 → 直接添加 uk_creator_date_media +-- 背景: +-- 旧索引 uk_creator_date_media = (creator_id, date, media_source) 不含 delete_time, +-- 软删的历史记录会继续占用唯一键,导致同账号同日同渠道再次新增触发 MySQL 1062。 +-- +-- 新索引 uk_creator_date_media = (creator_id, date, media_source, delete_time) +-- MySQL 中 NULL ≠ NULL: +-- - 活跃记录 delete_time = NULL,最多 1 条(行为不变,仍唯一) +-- - 软删后 delete_time 变成时间戳,与新插入的 NULL 不冲突 → 允许重新录入 +-- +-- 可重复执行:先检查再操作,避免 1091/1061 报错。 -- ========== 查看当前索引(可选) ========== SELECT @@ -19,6 +24,7 @@ GROUP BY TABLE_NAME, INDEX_NAME ORDER BY TABLE_NAME, INDEX_NAME; -- ========== zyt_personal_yeji ========== +-- 清理可能存在的旧版「全局唯一」索引 SET @sql = IF( (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() @@ -31,18 +37,23 @@ PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; +-- 删除旧的 3 列唯一索引(若存在) SET @sql = IF( (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'zyt_personal_yeji' - AND INDEX_NAME = 'uk_creator_date_media') = 0, - 'ALTER TABLE `zyt_personal_yeji` ADD UNIQUE KEY `uk_creator_date_media` (`creator_id`, `yeji_date`, `media_source`)', - 'SELECT ''zyt_personal_yeji: uk_creator_date_media 已存在,跳过添加'' AS message' + AND INDEX_NAME = 'uk_creator_date_media') > 0, + 'ALTER TABLE `zyt_personal_yeji` DROP INDEX `uk_creator_date_media`', + 'SELECT ''zyt_personal_yeji: 无 uk_creator_date_media,跳过删除'' AS message' ); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; +-- 添加 4 列唯一索引(包含 delete_time) +ALTER TABLE `zyt_personal_yeji` + ADD UNIQUE KEY `uk_creator_date_media` (`creator_id`, `yeji_date`, `media_source`, `delete_time`); + -- ========== zyt_personal_account_cost ========== SET @sql = IF( (SELECT COUNT(*) FROM information_schema.STATISTICS @@ -60,10 +71,13 @@ SET @sql = IF( (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'zyt_personal_account_cost' - AND INDEX_NAME = 'uk_creator_date_media') = 0, - 'ALTER TABLE `zyt_personal_account_cost` ADD UNIQUE KEY `uk_creator_date_media` (`creator_id`, `cost_date`, `media_source`)', - 'SELECT ''zyt_personal_account_cost: uk_creator_date_media 已存在,跳过添加'' AS message' + AND INDEX_NAME = 'uk_creator_date_media') > 0, + 'ALTER TABLE `zyt_personal_account_cost` DROP INDEX `uk_creator_date_media`', + 'SELECT ''zyt_personal_account_cost: 无 uk_creator_date_media,跳过删除'' AS message' ); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +ALTER TABLE `zyt_personal_account_cost` + ADD UNIQUE KEY `uk_creator_date_media` (`creator_id`, `cost_date`, `media_source`, `delete_time`); diff --git a/server/sql/1.9.20260521/create_personal_account_cost_table.sql b/server/sql/1.9.20260521/create_personal_account_cost_table.sql index da0b07c5..e983707e 100644 --- a/server/sql/1.9.20260521/create_personal_account_cost_table.sql +++ b/server/sql/1.9.20260521/create_personal_account_cost_table.sql @@ -13,7 +13,7 @@ CREATE TABLE IF NOT EXISTS `zyt_personal_account_cost` ( `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', `delete_time` int(11) unsigned DEFAULT NULL COMMENT '删除时间', PRIMARY KEY (`id`), - UNIQUE KEY `uk_creator_date_media` (`creator_id`, `cost_date`, `media_source`), + UNIQUE KEY `uk_creator_date_media` (`creator_id`, `cost_date`, `media_source`, `delete_time`), KEY `idx_cost_date` (`cost_date`), KEY `idx_creator_id` (`creator_id`), KEY `idx_dept_id` (`dept_id`) diff --git a/server/sql/1.9.20260521/create_personal_yeji_table.sql b/server/sql/1.9.20260521/create_personal_yeji_table.sql index 23a50b7c..c76a435e 100644 --- a/server/sql/1.9.20260521/create_personal_yeji_table.sql +++ b/server/sql/1.9.20260521/create_personal_yeji_table.sql @@ -20,7 +20,7 @@ CREATE TABLE IF NOT EXISTS `zyt_personal_yeji` ( `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', `delete_time` int(11) unsigned DEFAULT NULL COMMENT '删除时间', PRIMARY KEY (`id`), - UNIQUE KEY `uk_creator_date_media` (`creator_id`, `yeji_date`, `media_source`), + UNIQUE KEY `uk_creator_date_media` (`creator_id`, `yeji_date`, `media_source`, `delete_time`), KEY `idx_yeji_date` (`yeji_date`), KEY `idx_creator_id` (`creator_id`), KEY `idx_dept_id` (`dept_id`)