fix(stats/self_input): 唯一索引含 delete_time,软删后可重新录入

去掉「同日+同渠道」全局唯一,回到按录入人判重;唯一索引补 delete_time 避免软删占键导致 1062;错误提示带冲突记录 ID,迁移脚本兼容多次执行。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-22 17:01:58 +08:00
co-authored by Cursor
parent 003810114b
commit 679e61057b
6 changed files with 71 additions and 26 deletions
@@ -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) {
if (self::isUniqueConstraintViolation($e)) {
self::setError('该日期下该渠道的账户消耗已存在(唯一索引冲突),请刷新列表后直接编辑');
} else {
self::setError($e->getMessage());
}
return false;
}
@@ -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');
}
}
@@ -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) {
if (self::isUniqueConstraintViolation($e)) {
self::setError('该日期下该渠道的业绩已存在(唯一索引冲突),请刷新列表后直接编辑');
} else {
self::setError($e->getMessage());
}
return false;
}
@@ -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`);
@@ -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`)
@@ -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`)