feat(后台管理): 实体管理列表统一展示创建时间/更新时间
用户管理、菜单管理、不符合ASIN、数据去重总数据、查询ASIN、最低价ASIN、 商品类目、密钥管理共 8 个实体管理列表补齐两列。分组管理/店铺密钥/店铺管理 此前已带创建+修改时间,任务列表与统计报表(撞款监控、密钥用量、日志、 记录与版本)不含实体更新语义,均未改动。 关键点——时间列必须由数据库维护,否则新列是假的: 这些表的更新走 selectById → 改字段 → updateById,实体带着读出的旧 updated_at 一起写回。MySQL 规则是「UPDATE 显式给某列赋值时不触发该列的 ON UPDATE 自动更新」,不禁写就会把旧值写回去,更新时间永远冻结在首次写入 时刻。按 V125(biz_file_result)既有样板,给 7 个实体标注 @TableField(insertStrategy=NEVER, updateStrategy=NEVER)。 - V131:users / columns 补 updated_at(幂等 ADD COLUMN,仿 V125 写法)。 存量行被回填为迁移执行时刻,非真实历史变更时间(历史上无记录,无法还原) - 实体/VO:AdminUserEntity、PermissionMenuEntity、InvalidAsinDataEntity、 DedupeTotalDataEntity、ProductCategoryEntity、QueryAsinEntity、 SkipPriceAsinEntity 加/改写 updatedAt;AdminUserItemVo、PermissionMenuItemVo、 InvalidAsinDataItemVo、DedupeTotalDataItemVo 补 updatedAt; AdminUserSecretRowVo 补 createdAt(行级首次配置时间 = 三模块最早) - 查询ASIN/最低价ASIN 后端 VO 与前端 model 本就有两字段,仅补渲染 - 前端 8 页表格加列,同步修正空态/加载行的 colspan(手写表格,不同步会错位) - 测试:align-query-asin / align-skip-price 原断言「不允许有更新时间列」 (像素复刻旧版),按新需求改为断言两列存在;新增 e2e list-time-columns 覆盖 8 页表头与真实时间值渲染
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package com.nanri.aiimage.common.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
@@ -24,5 +25,13 @@ public class AdminUserEntity {
|
||||
private Long createdById;
|
||||
@TableField("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
/**
|
||||
* 更新时间(V131 新增):由数据库维护(DEFAULT CURRENT_TIMESTAMP + ON UPDATE CURRENT_TIMESTAMP)。
|
||||
*
|
||||
* <p>禁止应用显式写:MySQL 在 UPDATE 语句显式给该列赋值时不会触发自动更新,
|
||||
* 而本表写回多为 selectById → 改字段 → updateById(实体带着旧值),一旦写回就会冻结更新时间。
|
||||
*/
|
||||
@TableField(value = "updated_at", insertStrategy = FieldStrategy.NEVER, updateStrategy = FieldStrategy.NEVER)
|
||||
private LocalDateTime updatedAt;
|
||||
private String machine;
|
||||
}
|
||||
|
||||
+2
@@ -16,6 +16,8 @@ public class AdminUserItemVo {
|
||||
private String creatorUsername;
|
||||
@JsonProperty("created_at")
|
||||
private String createdAt;
|
||||
@JsonProperty("updated_at")
|
||||
private String updatedAt;
|
||||
@JsonProperty("pinyin_abbr")
|
||||
private String pinyinAbbr;
|
||||
}
|
||||
|
||||
+2
@@ -338,6 +338,8 @@ public class AdminUserService {
|
||||
vo.setCreatorUsername(creatorId == null ? "" : creatorMap.getOrDefault(creatorId, ""));
|
||||
LocalDateTime createdAt = entity.getCreatedAt();
|
||||
vo.setCreatedAt(createdAt == null ? "" : createdAt.format(CREATED_AT_FORMATTER));
|
||||
LocalDateTime updatedAt = entity.getUpdatedAt();
|
||||
vo.setUpdatedAt(updatedAt == null ? "" : updatedAt.format(CREATED_AT_FORMATTER));
|
||||
vo.setPinyinAbbr(PinyinAbbrUtil.abbr(entity.getUsername()));
|
||||
return vo;
|
||||
}
|
||||
|
||||
+9
@@ -1,6 +1,8 @@
|
||||
package com.nanri.aiimage.modules.dedupe.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
@@ -19,5 +21,12 @@ public class DedupeTotalDataEntity {
|
||||
private Long uploaderUserId;
|
||||
private String uploaderUsername;
|
||||
private LocalDateTime createdAt;
|
||||
/**
|
||||
* 更新时间:由数据库维护(DEFAULT CURRENT_TIMESTAMP + ON UPDATE CURRENT_TIMESTAMP)。
|
||||
*
|
||||
* <p>禁止应用显式写:MySQL 在 UPDATE 语句显式给该列赋值时不会触发自动更新,
|
||||
* 而本表写回为 selectById → 改字段 → updateById(实体带着旧值),一旦写回就会冻结更新时间。
|
||||
*/
|
||||
@TableField(value = "updated_at", insertStrategy = FieldStrategy.NEVER, updateStrategy = FieldStrategy.NEVER)
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
+3
@@ -32,4 +32,7 @@ public class DedupeTotalDataItemVo {
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
+1
@@ -1398,6 +1398,7 @@ public class DedupeTotalDataService {
|
||||
vo.setUploaderUserId(entity.getUploaderUserId());
|
||||
vo.setUsername(entity.getUploaderUsername());
|
||||
vo.setCreatedAt(entity.getCreatedAt());
|
||||
vo.setUpdatedAt(entity.getUpdatedAt());
|
||||
return vo;
|
||||
}
|
||||
|
||||
|
||||
+9
@@ -1,6 +1,8 @@
|
||||
package com.nanri.aiimage.modules.invalidasin.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
@@ -18,5 +20,12 @@ public class InvalidAsinDataEntity {
|
||||
private Long groupId;
|
||||
private String recordSource;
|
||||
private LocalDateTime createdAt;
|
||||
/**
|
||||
* 更新时间:由数据库维护(DEFAULT CURRENT_TIMESTAMP + ON UPDATE CURRENT_TIMESTAMP)。
|
||||
*
|
||||
* <p>禁止应用显式写:MySQL 在 UPDATE 语句显式给该列赋值时不会触发自动更新,
|
||||
* 而本表写回为 selectById → 改字段 → updateById(实体带着旧值),一旦写回就会冻结更新时间。
|
||||
*/
|
||||
@TableField(value = "updated_at", insertStrategy = FieldStrategy.NEVER, updateStrategy = FieldStrategy.NEVER)
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
+3
@@ -29,4 +29,7 @@ public class InvalidAsinDataItemVo {
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
+1
@@ -245,6 +245,7 @@ public class InvalidAsinDataService {
|
||||
vo.setGroupName(groupName == null ? "" : groupName);
|
||||
vo.setRecordSource(isManualRecord(entity) ? RECORD_SOURCE_MANUAL : RECORD_SOURCE_AUTO);
|
||||
vo.setCreatedAt(entity.getCreatedAt());
|
||||
vo.setUpdatedAt(entity.getUpdatedAt());
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -1,5 +1,6 @@
|
||||
package com.nanri.aiimage.modules.permission.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
@@ -22,4 +23,12 @@ public class PermissionMenuEntity {
|
||||
private String routePath;
|
||||
private Integer sortOrder;
|
||||
private LocalDateTime createdAt;
|
||||
/**
|
||||
* 更新时间(V131 新增):由数据库维护(DEFAULT CURRENT_TIMESTAMP + ON UPDATE CURRENT_TIMESTAMP)。
|
||||
*
|
||||
* <p>禁止应用显式写:MySQL 在 UPDATE 语句显式给该列赋值时不会触发自动更新,
|
||||
* 而本表写回多为 selectById → 改字段 → updateById(实体带着旧值),一旦写回就会冻结更新时间。
|
||||
*/
|
||||
@TableField(value = "updated_at", insertStrategy = FieldStrategy.NEVER, updateStrategy = FieldStrategy.NEVER)
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
+2
@@ -24,6 +24,8 @@ public class PermissionMenuItemVo {
|
||||
private Integer sortOrder;
|
||||
@JsonProperty("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
@JsonProperty("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* 当前操作者能否把该菜单授予他人。仅授权树接口(list)填充;菜单 CRUD 回显等
|
||||
|
||||
+1
@@ -1160,6 +1160,7 @@ public class PermissionMenuService {
|
||||
vo.setRoutePath(entity.getRoutePath());
|
||||
vo.setSortOrder(entity.getSortOrder());
|
||||
vo.setCreatedAt(entity.getCreatedAt());
|
||||
vo.setUpdatedAt(entity.getUpdatedAt());
|
||||
return vo;
|
||||
}
|
||||
|
||||
|
||||
+8
-1
@@ -1,5 +1,6 @@
|
||||
package com.nanri.aiimage.modules.shopkey.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
@@ -39,6 +40,12 @@ public class QueryAsinEntity {
|
||||
@TableField("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@TableField("updated_at")
|
||||
/**
|
||||
* 更新时间:由数据库维护(DEFAULT CURRENT_TIMESTAMP + ON UPDATE CURRENT_TIMESTAMP)。
|
||||
*
|
||||
* <p>禁止应用显式写:本表更新走 selectById → 改字段 → updateById(实体带着旧值),
|
||||
* 显式写回旧值会触发 MySQL 的「显式赋值不自动更新」规则,把更新时间冻结在首次写入时刻。
|
||||
*/
|
||||
@TableField(value = "updated_at", insertStrategy = FieldStrategy.NEVER, updateStrategy = FieldStrategy.NEVER)
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
+7
-1
@@ -56,6 +56,12 @@ public class SkipPriceAsinEntity {
|
||||
@TableField("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@TableField("updated_at")
|
||||
/**
|
||||
* 更新时间:由数据库维护(DEFAULT CURRENT_TIMESTAMP + ON UPDATE CURRENT_TIMESTAMP)。
|
||||
*
|
||||
* <p>禁止应用显式写:本表更新走 selectById → 改字段 → updateById(实体带着旧值),
|
||||
* 显式写回旧值会触发 MySQL 的「显式赋值不自动更新」规则,把更新时间冻结在首次写入时刻。
|
||||
*/
|
||||
@TableField(value = "updated_at", insertStrategy = FieldStrategy.NEVER, updateStrategy = FieldStrategy.NEVER)
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
+3
@@ -37,6 +37,9 @@ public class AdminUserSecretRowVo {
|
||||
@Schema(description = "行级状态说明")
|
||||
private String statusMessage;
|
||||
|
||||
@Schema(description = "首次配置时间(三模块中最早)")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Schema(description = "最近更新时间(三模块中最晚)")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
+14
@@ -41,6 +41,7 @@ import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -607,6 +608,7 @@ public class UserApiSecretService implements UserSecretCleanupPort {
|
||||
vo.setStatus(summarizeRowStatus(modules));
|
||||
vo.setStatusMessage(summarizeRowMessage(modules, vo.getStatus()));
|
||||
vo.setUpdatedAt(latestUpdatedAt(modules));
|
||||
vo.setCreatedAt(earliestCreatedAt(moduleRows.values()));
|
||||
AdminUserEntity user = adminUserMapper.selectById(userId);
|
||||
vo.setUsername(user == null ? "" : user.getUsername());
|
||||
vo.setGroups(List.of());
|
||||
@@ -673,6 +675,18 @@ public class UserApiSecretService implements UserSecretCleanupPort {
|
||||
return latest;
|
||||
}
|
||||
|
||||
/** 行级创建时间:该用户已配置模块中最早的一条 created_at;一条都没有则为 null。 */
|
||||
private LocalDateTime earliestCreatedAt(Collection<UserApiSecretEntity> rows) {
|
||||
LocalDateTime earliest = null;
|
||||
for (UserApiSecretEntity row : rows) {
|
||||
LocalDateTime value = row.getCreatedAt();
|
||||
if (value != null && (earliest == null || value.isBefore(earliest))) {
|
||||
earliest = value;
|
||||
}
|
||||
}
|
||||
return earliest;
|
||||
}
|
||||
|
||||
private void upsert(Long userId, String moduleKey, String plainValue, String source) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
UserApiSecretEntity existing = selectOne(userId, moduleKey);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
-- V131: users(用户)、columns(后台菜单) 两表增加 updated_at 列
|
||||
--
|
||||
-- 背景:后台「用户管理」「菜单管理」列表要展示「创建时间 + 更新时间」,但这两张历史表
|
||||
-- 建表时只落了一个 created_at,更新时间无从取值。补一列由数据库维护的 updated_at
|
||||
-- (DEFAULT CURRENT_TIMESTAMP + ON UPDATE CURRENT_TIMESTAMP),应用侧不写它。
|
||||
--
|
||||
-- 与 V125 同样的取舍:列交给数据库维护,Java 实体上标注
|
||||
-- insertStrategy=NEVER / updateStrategy=NEVER,保证 MyBatis-Plus 永不显式写。
|
||||
-- 这一步是必要的——本项目的写回模式多为 selectById → 改字段 → updateById(实体带着旧值),
|
||||
-- 而 MySQL 的规则是「UPDATE 语句显式给某列赋值时不触发该列的自动更新」,
|
||||
-- 不禁写就会把读出来的旧值写回去,更新时间会一直停在第一次写入的值。
|
||||
--
|
||||
-- 存量行:ADD COLUMN 的 DEFAULT CURRENT_TIMESTAMP 会把已有行填成迁移执行时刻,
|
||||
-- 不是真实的历史变更时间(历史上也没有记录,无法还原),属已知取舍。
|
||||
--
|
||||
-- 风险:两表均小(users 百余行、columns 数十行),ADD COLUMN 秒级完成。
|
||||
-- 回滚:ALTER TABLE `users` DROP COLUMN updated_at; ALTER TABLE `columns` DROP COLUMN updated_at;
|
||||
|
||||
SET @db_name = DATABASE();
|
||||
|
||||
-- users(用户表)
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'users' AND COLUMN_NAME = 'updated_at'
|
||||
);
|
||||
SET @sql := IF(@col_exists = 0,
|
||||
'ALTER TABLE `users` ADD COLUMN `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT ''更新时间'' AFTER `created_at`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- columns(后台菜单表)
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'columns' AND COLUMN_NAME = 'updated_at'
|
||||
);
|
||||
SET @sql := IF(@col_exists = 0,
|
||||
'ALTER TABLE `columns` ADD COLUMN `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT ''更新时间'' AFTER `created_at`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
Reference in New Issue
Block a user