From 5b8105ec2b66445cbc353f53ed8a3dcd96f21be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Sat, 19 Sep 2026 15:53:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=90=8E=E5=8F=B0=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E5=AE=9E=E4=BD=93=E7=AE=A1=E7=90=86=E5=88=97=E8=A1=A8=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E5=B1=95=E7=A4=BA=E5=88=9B=E5=BB=BA=E6=97=B6=E9=97=B4?= =?UTF-8?q?/=E6=9B=B4=E6=96=B0=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户管理、菜单管理、不符合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 页表头与真实时间值渲染 --- .../e2e/list-time-columns.spec.ts | 44 +++++ .../scripts/mock-admin-server.mjs | 174 +++++++++++++++++- admin-frontend-vue/src/api/user-secrets.ts | 2 + admin-frontend-vue/src/api/users-model.ts | 2 + .../src/pages/account/MenusPage.vue | 6 +- .../src/pages/account/UserSecretsPage.vue | 8 +- .../src/pages/account/UsersPage.vue | 6 +- .../src/pages/account/menu-manage-model.ts | 3 + .../src/pages/asin/AsinInvalidPage.vue | 6 +- .../src/pages/asin/DedupeRegistryPage.vue | 6 +- .../src/pages/asin/ProductCategoryPage.vue | 17 +- .../src/pages/asin/QueryAsinPage.vue | 9 +- .../src/pages/asin/SkipPricePage.vue | 9 +- .../src/pages/asin/dedupe-total-model.ts | 3 + .../src/pages/asin/product-category-model.ts | 6 + .../pages/invalidasin/invalid-asin-model.ts | 3 + admin-frontend-vue/src/types/admin.ts | 1 + .../tests/align-query-asin.test.ts | 4 +- .../tests/align-skip-price.test.ts | 4 +- .../common/model/entity/AdminUserEntity.java | 9 + .../admin/model/vo/AdminUserItemVo.java | 2 + .../admin/service/AdminUserService.java | 2 + .../model/entity/DedupeTotalDataEntity.java | 9 + .../model/vo/DedupeTotalDataItemVo.java | 3 + .../service/DedupeTotalDataService.java | 1 + .../model/entity/InvalidAsinDataEntity.java | 9 + .../model/vo/InvalidAsinDataItemVo.java | 3 + .../service/InvalidAsinDataService.java | 1 + .../model/entity/PermissionMenuEntity.java | 9 + .../model/vo/PermissionMenuItemVo.java | 2 + .../service/PermissionMenuService.java | 1 + .../shopkey/model/entity/QueryAsinEntity.java | 9 +- .../model/entity/SkipPriceAsinEntity.java | 8 +- .../model/vo/AdminUserSecretRowVo.java | 3 + .../service/UserApiSecretService.java | 14 ++ .../db/V131__users_and_columns_updated_at.sql | 45 +++++ 36 files changed, 418 insertions(+), 25 deletions(-) create mode 100644 admin-frontend-vue/e2e/list-time-columns.spec.ts create mode 100644 backend-java/src/main/resources/db/V131__users_and_columns_updated_at.sql diff --git a/admin-frontend-vue/e2e/list-time-columns.spec.ts b/admin-frontend-vue/e2e/list-time-columns.spec.ts new file mode 100644 index 00000000..31d5d779 --- /dev/null +++ b/admin-frontend-vue/e2e/list-time-columns.spec.ts @@ -0,0 +1,44 @@ +import { expect, test, type Page } from '@playwright/test' + +// 后台管理列表「创建时间 + 更新时间」两列验收(2026-09-19 需求): +// 8 个实体管理列表统一展示创建/更新时间,数据取自各列表 VO。 +// mock 夹具统一给 createdAt=2026-03-01 09:15:00、updatedAt=2026-09-18 16:42:30, +// 页面统一经 formatDateTime 归一为「YYYY-MM-DD HH:mm:ss」。 + +const CREATED = '2026-03-01 09:15:00' +const UPDATED = '2026-09-18 16:42:30' + +const PAGES: Array<{ title: string; path: string; heading: string }> = [ + { title: '用户管理', path: '/admin-vue/account/users', heading: '用户列表' }, + { title: '菜单管理', path: '/admin-vue/account/menus', heading: '菜单' }, + { title: '密钥管理', path: '/admin-vue/account/user-secrets', heading: '用户密钥列表' }, + { title: '不符合ASIN数据', path: '/admin-vue/asin-center/invalid', heading: '品牌数据列表' }, + { title: '数据去重总数据', path: '/admin-vue/asin-center/registry', heading: '数据去重总数据' }, + { title: '查询ASIN', path: '/admin-vue/asin-center/query', heading: '查询' }, + { title: '最低价ASIN', path: '/admin-vue/asin-center/skip-price', heading: '最低价' }, + { title: '商品类目', path: '/admin-vue/asin-center/categories', heading: '商品类目' }, +] + +async function openPage(page: Page, path: string) { + await page.goto(path) + await expect(page.locator('.panel-box table tbody tr').first()).toBeVisible({ timeout: 15000 }) +} + +for (const spec of PAGES) { + test(`${spec.title}:表格展示创建时间与更新时间`, async ({ page }) => { + const errors: string[] = [] + page.on('pageerror', (error) => errors.push(error.message)) + + await openPage(page, spec.path) + + const table = page.locator('.panel-box table').first() + // 表头出现两列 + await expect(table.locator('thead')).toContainText('创建时间') + await expect(table.locator('thead')).toContainText('更新时间') + // 数据行渲染出真实时间值(非空、非占位符) + await expect(table.locator('tbody')).toContainText(CREATED) + await expect(table.locator('tbody')).toContainText(UPDATED) + + expect(errors).toEqual([]) + }) +} diff --git a/admin-frontend-vue/scripts/mock-admin-server.mjs b/admin-frontend-vue/scripts/mock-admin-server.mjs index 51424e8a..45fb6e57 100644 --- a/admin-frontend-vue/scripts/mock-admin-server.mjs +++ b/admin-frontend-vue/scripts/mock-admin-server.mjs @@ -220,13 +220,183 @@ const server = createServer((req, res) => { return json(res, { success: true, data: { - items: [{ id: 1, username: 'admin', role: 'super_admin', creatorUsername: 'system', createdAt: '2026-01-01 00:00:00' }], + items: [{ id: 1, username: 'admin', role: 'super_admin', creatorUsername: 'system', created_at: '2026-03-01 09:15:00', updated_at: '2026-09-18 16:42:30' }], total: 1, }, }) } if (url === '/api/admin/permission-menus') { - return json(res, { success: true, data: { items: [] } }) + // 菜单管理列表:一条根节点,带创建/更新时间(验收时间列)。 + return json(res, { + success: true, + data: [ + { + id: 1, + name: '用户管理', + column_key: 'admin_users', + parent_id: null, + menu_type: 'admin', + route_path: 'account/users', + sort_order: 10, + created_at: '2026-03-01T09:15:00', + updated_at: '2026-09-18T16:42:30', + }, + ], + }) + } + if (url === '/api/admin/invalid-asin-data') { + return json(res, { + success: true, + data: { + items: [ + { + id: 1, + dataValue: 'B0INVALID1', + brand: '测试品牌', + groupId: 1, + groupName: '测试分组', + recordSource: 'MANUAL', + createdAt: '2026-03-01 09:15:00', + updatedAt: '2026-09-18 16:42:30', + }, + ], + total: 1, + page: 1, + pageSize: 10, + }, + }) + } + if (url === '/api/admin/dedupe-total-data') { + return json(res, { + success: true, + data: { + items: [ + { + id: 1, + dataValue: 'B0DEDUPE01', + country: 'DE', + groupId: 1, + groupName: '测试分组', + uploaderUserId: 1, + username: 'admin', + createdAt: '2026-03-01 09:15:00', + updatedAt: '2026-09-18 16:42:30', + }, + ], + total: 1, + page: 1, + pageSize: 10, + }, + }) + } + if (url === '/api/admin/query-asins') { + return json(res, { + success: true, + data: { + items: [ + { + id: 1, + groupId: 1, + groupName: '测试分组', + shopName: '测试店铺', + asinDe: 'B0QUERY001', + asinUk: '', + asinFr: '', + asinIt: '', + asinEs: '', + createdAt: '2026-03-01 09:15:00', + updatedAt: '2026-09-18 16:42:30', + }, + ], + total: 1, + page: 1, + pageSize: 10, + }, + }) + } + if (url === '/api/admin/skip-price-asins') { + return json(res, { + success: true, + data: { + items: [ + { + id: 1, + groupId: 1, + groupName: '测试分组', + shopName: '测试店铺', + asinDe: 'B0SKIP0001', + minimumPriceDe: 9.99, + asinUk: '', + minimumPriceUk: null, + asinFr: '', + minimumPriceFr: null, + asinIt: '', + minimumPriceIt: null, + asinEs: '', + minimumPriceEs: null, + createdAt: '2026-03-01 09:15:00', + updatedAt: '2026-09-18 16:42:30', + }, + ], + total: 1, + page: 1, + pageSize: 10, + }, + }) + } + if (url === '/api/admin/product-categories/children') { + return json(res, { + success: true, + data: { + items: [ + { + id: 1, + parentId: null, + name: '护肤品', + categoryKey: 'skincare', + sortOrder: 10, + description: '测试备注', + isBuiltin: true, + childCount: 0, + level: 0, + path: '护肤品', + createdAt: '2026-03-01 09:15:00', + updatedAt: '2026-09-18 16:42:30', + }, + ], + total: 1, + page: 1, + pageSize: 10, + hasMore: false, + }, + }) + } + if (url === '/api/admin/user-secrets') { + const emptyModule = { moduleKey: '', moduleLabel: '', masked: '', full: '', exists: false, checkStatus: 'unknown', checkCode: '', checkMessage: '', checkLatencyMs: null, checkedAt: null, updatedAt: null } + return json(res, { + success: true, + data: { + items: [ + { + userId: 1, + username: 'admin', + groups: [], + leaderUsername: '', + similarAsin: { ...emptyModule, moduleKey: 'similar-asin', moduleLabel: '货源查询密钥' }, + appearancePatent: { ...emptyModule, moduleKey: 'appearance-patent', moduleLabel: '外观专利密钥' }, + proxy: { ...emptyModule, moduleKey: 'proxy', moduleLabel: '代理设置' }, + status: 'unknown', + statusMessage: '', + createdAt: '2026-03-01 09:15:00', + updatedAt: '2026-09-18 16:42:30', + }, + ], + total: 1, + page: 1, + pageSize: 15, + groupOptions: [], + }, + }) } if (url === '/api/admin/shop-manage-groups') { return json(res, { success: true, data: { items: [] } }) diff --git a/admin-frontend-vue/src/api/user-secrets.ts b/admin-frontend-vue/src/api/user-secrets.ts index f9fa88e7..8a2223e2 100644 --- a/admin-frontend-vue/src/api/user-secrets.ts +++ b/admin-frontend-vue/src/api/user-secrets.ts @@ -30,6 +30,8 @@ export interface AdminUserSecretRow { proxy: AdminUserSecretModule status: string statusMessage: string + /** 首次配置时间(三模块中最早);从未配置为 null。 */ + createdAt: string | null updatedAt: string | null } diff --git a/admin-frontend-vue/src/api/users-model.ts b/admin-frontend-vue/src/api/users-model.ts index 7ac8a2fc..b7ac8681 100644 --- a/admin-frontend-vue/src/api/users-model.ts +++ b/admin-frontend-vue/src/api/users-model.ts @@ -23,6 +23,8 @@ export function toAdminUserItem(raw: unknown): AdminUser | null { if (createdById !== null) item.createdById = createdById const createdAt = text(r.created_at) if (createdAt) item.createdAt = createdAt + const updatedAt = text(r.updated_at) + if (updatedAt) item.updatedAt = updatedAt const creator = text(r.creator_username) if (creator) item.creatorUsername = creator const abbr = text(r.pinyin_abbr) diff --git a/admin-frontend-vue/src/pages/account/MenusPage.vue b/admin-frontend-vue/src/pages/account/MenusPage.vue index dda93750..e268a124 100644 --- a/admin-frontend-vue/src/pages/account/MenusPage.vue +++ b/admin-frontend-vue/src/pages/account/MenusPage.vue @@ -241,6 +241,7 @@ onMounted(loadMenus)
禁止应用显式写:MySQL 在 UPDATE 语句显式给该列赋值时不会触发自动更新, + * 而本表写回多为 selectById → 改字段 → updateById(实体带着旧值),一旦写回就会冻结更新时间。 + */ + @TableField(value = "updated_at", insertStrategy = FieldStrategy.NEVER, updateStrategy = FieldStrategy.NEVER) + private LocalDateTime updatedAt; private String machine; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/admin/model/vo/AdminUserItemVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/admin/model/vo/AdminUserItemVo.java index fb268a91..d0de3c6b 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/admin/model/vo/AdminUserItemVo.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/admin/model/vo/AdminUserItemVo.java @@ -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; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/admin/service/AdminUserService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/admin/service/AdminUserService.java index c8e9f765..a9444ae1 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/admin/service/AdminUserService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/admin/service/AdminUserService.java @@ -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; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/model/entity/DedupeTotalDataEntity.java b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/model/entity/DedupeTotalDataEntity.java index d5908376..3f1de9f9 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/model/entity/DedupeTotalDataEntity.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/model/entity/DedupeTotalDataEntity.java @@ -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)。 + * + *
禁止应用显式写:MySQL 在 UPDATE 语句显式给该列赋值时不会触发自动更新, + * 而本表写回为 selectById → 改字段 → updateById(实体带着旧值),一旦写回就会冻结更新时间。 + */ + @TableField(value = "updated_at", insertStrategy = FieldStrategy.NEVER, updateStrategy = FieldStrategy.NEVER) private LocalDateTime updatedAt; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/model/vo/DedupeTotalDataItemVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/model/vo/DedupeTotalDataItemVo.java index db0fb5e0..b046bd8f 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/model/vo/DedupeTotalDataItemVo.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/model/vo/DedupeTotalDataItemVo.java @@ -32,4 +32,7 @@ public class DedupeTotalDataItemVo { @Schema(description = "创建时间") private LocalDateTime createdAt; + + @Schema(description = "更新时间") + private LocalDateTime updatedAt; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeTotalDataService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeTotalDataService.java index 06d0248b..b6fcff17 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeTotalDataService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeTotalDataService.java @@ -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; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/invalidasin/model/entity/InvalidAsinDataEntity.java b/backend-java/src/main/java/com/nanri/aiimage/modules/invalidasin/model/entity/InvalidAsinDataEntity.java index f2f8d1e3..484351d1 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/invalidasin/model/entity/InvalidAsinDataEntity.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/invalidasin/model/entity/InvalidAsinDataEntity.java @@ -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)。 + * + *
禁止应用显式写:MySQL 在 UPDATE 语句显式给该列赋值时不会触发自动更新, + * 而本表写回为 selectById → 改字段 → updateById(实体带着旧值),一旦写回就会冻结更新时间。 + */ + @TableField(value = "updated_at", insertStrategy = FieldStrategy.NEVER, updateStrategy = FieldStrategy.NEVER) private LocalDateTime updatedAt; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/invalidasin/model/vo/InvalidAsinDataItemVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/invalidasin/model/vo/InvalidAsinDataItemVo.java index bf001754..c6d6c29a 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/invalidasin/model/vo/InvalidAsinDataItemVo.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/invalidasin/model/vo/InvalidAsinDataItemVo.java @@ -29,4 +29,7 @@ public class InvalidAsinDataItemVo { @Schema(description = "创建时间") private LocalDateTime createdAt; + + @Schema(description = "更新时间") + private LocalDateTime updatedAt; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/invalidasin/service/InvalidAsinDataService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/invalidasin/service/InvalidAsinDataService.java index c1732ced..668399a8 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/invalidasin/service/InvalidAsinDataService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/invalidasin/service/InvalidAsinDataService.java @@ -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; } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/entity/PermissionMenuEntity.java b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/entity/PermissionMenuEntity.java index 77377046..fa15ce3b 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/entity/PermissionMenuEntity.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/entity/PermissionMenuEntity.java @@ -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)。 + * + *
禁止应用显式写:MySQL 在 UPDATE 语句显式给该列赋值时不会触发自动更新, + * 而本表写回多为 selectById → 改字段 → updateById(实体带着旧值),一旦写回就会冻结更新时间。 + */ + @TableField(value = "updated_at", insertStrategy = FieldStrategy.NEVER, updateStrategy = FieldStrategy.NEVER) + private LocalDateTime updatedAt; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/vo/PermissionMenuItemVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/vo/PermissionMenuItemVo.java index 6afdbc05..9194dbfa 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/vo/PermissionMenuItemVo.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/vo/PermissionMenuItemVo.java @@ -24,6 +24,8 @@ public class PermissionMenuItemVo { private Integer sortOrder; @JsonProperty("created_at") private LocalDateTime createdAt; + @JsonProperty("updated_at") + private LocalDateTime updatedAt; /** * 当前操作者能否把该菜单授予他人。仅授权树接口(list)填充;菜单 CRUD 回显等 diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuService.java index a2de5031..ab90273b 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuService.java @@ -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; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/QueryAsinEntity.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/QueryAsinEntity.java index 795ec648..b15d0098 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/QueryAsinEntity.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/QueryAsinEntity.java @@ -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)。 + * + *
禁止应用显式写:本表更新走 selectById → 改字段 → updateById(实体带着旧值), + * 显式写回旧值会触发 MySQL 的「显式赋值不自动更新」规则,把更新时间冻结在首次写入时刻。 + */ + @TableField(value = "updated_at", insertStrategy = FieldStrategy.NEVER, updateStrategy = FieldStrategy.NEVER) private LocalDateTime updatedAt; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/SkipPriceAsinEntity.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/SkipPriceAsinEntity.java index cc2e5aa8..333c5b6a 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/SkipPriceAsinEntity.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/SkipPriceAsinEntity.java @@ -56,6 +56,12 @@ public class SkipPriceAsinEntity { @TableField("created_at") private LocalDateTime createdAt; - @TableField("updated_at") + /** + * 更新时间:由数据库维护(DEFAULT CURRENT_TIMESTAMP + ON UPDATE CURRENT_TIMESTAMP)。 + * + *
禁止应用显式写:本表更新走 selectById → 改字段 → updateById(实体带着旧值),
+ * 显式写回旧值会触发 MySQL 的「显式赋值不自动更新」规则,把更新时间冻结在首次写入时刻。
+ */
+ @TableField(value = "updated_at", insertStrategy = FieldStrategy.NEVER, updateStrategy = FieldStrategy.NEVER)
private LocalDateTime updatedAt;
}
diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/usersecret/model/vo/AdminUserSecretRowVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/usersecret/model/vo/AdminUserSecretRowVo.java
index 89ba1805..20233a7c 100644
--- a/backend-java/src/main/java/com/nanri/aiimage/modules/usersecret/model/vo/AdminUserSecretRowVo.java
+++ b/backend-java/src/main/java/com/nanri/aiimage/modules/usersecret/model/vo/AdminUserSecretRowVo.java
@@ -37,6 +37,9 @@ public class AdminUserSecretRowVo {
@Schema(description = "行级状态说明")
private String statusMessage;
+ @Schema(description = "首次配置时间(三模块中最早)")
+ private LocalDateTime createdAt;
+
@Schema(description = "最近更新时间(三模块中最晚)")
private LocalDateTime updatedAt;
}
diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/usersecret/service/UserApiSecretService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/usersecret/service/UserApiSecretService.java
index f629ad74..44951f7a 100644
--- a/backend-java/src/main/java/com/nanri/aiimage/modules/usersecret/service/UserApiSecretService.java
+++ b/backend-java/src/main/java/com/nanri/aiimage/modules/usersecret/service/UserApiSecretService.java
@@ -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