Browse Source

多语言 设置UTC时间 根据header返回区域时间

hr~ 2 days ago
parent
commit
9006c50963

+ 56 - 0
product-common/src/main/java/com/poyee/config/JacksonConfig.java

@@ -0,0 +1,56 @@
+package com.poyee.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * 全局 Jackson 配置
+ * <p>
+ * 注册时区感知序列化器 {@link LocalDateTimeZoneSerializerConfig}:
+ * 所有响应中的 {@link LocalDateTime} 字段将根据请求头 {@code Accept-Language}
+ * 自动从 UTC 转换为客户端对应的区域时间后输出。
+ * </p>
+ *
+ * <p>
+ * 注意:不使用 {@code JavaTimeModule} 注册自定义序列化器,
+ * 因为 Spring Boot 自动配置的 {@code Jackson2ObjectMapperBuilder} 已注册过 {@code JavaTimeModule},
+ * Jackson 的模块去重机制会导致第二次注册同类型模块被静默跳过。
+ * 改用独立命名的 {@code SimpleModule} 可绕过去重检查,确保自定义序列化器生效。
+ * </p>
+ */
+@Configuration
+public class JacksonConfig {
+
+    private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
+
+    @Bean
+    public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
+        // 保留 Spring Boot 自动配置的所有默认模块,在此基础上叠加
+        ObjectMapper mapper = builder.createXmlMapper(false).build();
+
+        // 使用 SimpleModule(独立类名)注册,绕过 Jackson 对 JavaTimeModule 的去重跳过机制
+        SimpleModule overrideModule = new SimpleModule("LocalDateTimeZoneOverride");
+
+        // 序列化:UTC LocalDateTime → 客户端时区时间(根据 Accept-Language 自动转换)
+        overrideModule.addSerializer(LocalDateTime.class, new LocalDateTimeZoneSerializerConfig());
+
+        // 反序列化:客户端传入时间字符串 → LocalDateTime(业务层保证传入 UTC)
+        overrideModule.addDeserializer(
+                LocalDateTime.class,
+                new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_PATTERN))
+        );
+
+        mapper.registerModule(overrideModule);
+        // 禁用将时间序列化为时间戳
+        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+        return mapper;
+    }
+}

+ 60 - 0
product-common/src/main/java/com/poyee/config/LocalDateTimeZoneSerializerConfig.java

@@ -0,0 +1,60 @@
+package com.poyee.config;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+import com.poyee.utils.LocaleTimeZoneUtil;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * 时区感知的 LocalDateTime Jackson 序列化器
+ * <p>
+ * 数据库存储的是 UTC 时间({@link LocalDateTime} 不带时区信息),
+ * 本序列化器在序列化响应时,从请求头 {@code Accept-Language} 推断客户端所在时区,
+ * 将 UTC 时间转换为对应的区域时间后输出。
+ * </p>
+ *
+ * <pre>
+ * 示例:
+ *   DB UTC时间:   2026-03-20 06:00:00
+ *   Accept-Language: zh-CN
+ *   输出时间:      2026-03-20 14:00:00   (UTC+8)
+ *
+ *   Accept-Language: en-US
+ *   输出时间:      2026-03-20 02:00:00   (UTC-4, 夏令时)
+ * </pre>
+ */
+public class LocalDateTimeZoneSerializerConfig extends StdSerializer<LocalDateTime> {
+
+    private static final DateTimeFormatter FORMATTER =
+            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    public LocalDateTimeZoneSerializerConfig() {
+        super(LocalDateTime.class);
+    }
+
+    @Override
+    public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider provider)
+            throws IOException {
+        if (value == null) {
+            gen.writeNull();
+            return;
+        }
+        // 1. 将数据库中的 LocalDateTime 视为 UTC 时间
+        ZonedDateTime utcDateTime = value.atZone(ZoneId.of("UTC"));
+
+        // 2. 根据请求 Accept-Language 解析目标时区
+        ZoneId targetZone = LocaleTimeZoneUtil.resolveZoneId();
+
+        // 3. 转换到目标时区
+        ZonedDateTime localDateTime = utcDateTime.withZoneSameInstant(targetZone);
+
+        // 4. 格式化输出
+        gen.writeString(localDateTime.format(FORMATTER));
+    }
+}

+ 2 - 2
product-common/src/main/java/com/poyee/domain/ProductFastSale.java

@@ -58,9 +58,9 @@ public class ProductFastSale implements Serializable {
     /**
      * 创建人
      */
-    private Long createBy;
+    private String createBy;
     private LocalDateTime createTime;
-    private Long updateBy;
+    private String updateBy;
     private LocalDateTime updateTime;
 
     /**

+ 2 - 2
product-common/src/main/java/com/poyee/domain/ProductInfo.java

@@ -99,7 +99,7 @@ public class ProductInfo implements Serializable {
     /**
      * 创建人
      */
-    private Long createBy;
+    private String createBy;
 
     /**
      * 创建时间
@@ -109,7 +109,7 @@ public class ProductInfo implements Serializable {
     /**
      * 修改人
      */
-    private Long updateBy;
+    private String updateBy;
 
     /**
      * 修改时间

+ 4 - 0
product-common/src/main/java/com/poyee/res/ProductDetailRes.java

@@ -7,6 +7,8 @@ import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
+import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.util.List;
 
 @Data
@@ -41,4 +43,6 @@ public class ProductDetailRes {
     private Integer sort;
     @ApiModelProperty("配置")
     private String productProperties;
+    @ApiModelProperty("创建时间")
+    private LocalDateTime createTime;
 }

+ 184 - 0
product-common/src/main/java/com/poyee/utils/LocaleTimeZoneUtil.java

@@ -0,0 +1,184 @@
+package com.poyee.utils;
+
+import cn.hutool.core.date.LocalDateTimeUtil;
+import cn.hutool.core.util.StrUtil;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 多语言 → 时区工具类
+ * <p>
+ * 根据请求头 Accept-Language 推断对应的时区,
+ * 用于将数据库中存储的 UTC 时间转换为客户端所在区域的本地时间。
+ * </p>
+ */
+public class LocaleTimeZoneUtil {
+
+    /** 默认时区:UTC */
+    public static final ZoneId UTC = ZoneId.of("UTC");
+
+    /**
+     * 语言标签 → 时区映射表
+     * 优先精确匹配(language-COUNTRY),其次匹配语言前缀(language)
+     */
+    private static final Map<String, ZoneId> LOCALE_ZONE_MAP = new HashMap<>();
+
+    static {
+        // 中文
+        LOCALE_ZONE_MAP.put("zh-CN", ZoneId.of("Asia/Shanghai"));   // 简体中文 → 北京时间 UTC+8
+        LOCALE_ZONE_MAP.put("zh-TW", ZoneId.of("Asia/Taipei"));     // 繁体中文(台湾) → UTC+8
+        LOCALE_ZONE_MAP.put("zh-HK", ZoneId.of("Asia/Hong_Kong"));  // 繁体中文(香港) → UTC+8
+        LOCALE_ZONE_MAP.put("zh-MO", ZoneId.of("Asia/Macau"));      // 繁体中文(澳门) → UTC+8
+        LOCALE_ZONE_MAP.put("zh-SG", ZoneId.of("Asia/Singapore"));  // 简体中文(新加坡) → UTC+8
+        LOCALE_ZONE_MAP.put("zh",    ZoneId.of("Asia/Shanghai"));   // 纯 zh → 默认北京时间
+
+        // 英文
+        LOCALE_ZONE_MAP.put("en-US", ZoneId.of("America/New_York")); // 美国东部 UTC-5/4
+        LOCALE_ZONE_MAP.put("en-GB", ZoneId.of("Europe/London"));    // 英国 UTC+0/1
+        LOCALE_ZONE_MAP.put("en-AU", ZoneId.of("Australia/Sydney")); // 澳大利亚东部 UTC+10/11
+        LOCALE_ZONE_MAP.put("en-CA", ZoneId.of("America/Toronto"));  // 加拿大东部
+        LOCALE_ZONE_MAP.put("en-SG", ZoneId.of("Asia/Singapore"));   // 新加坡英文
+        LOCALE_ZONE_MAP.put("en-HK", ZoneId.of("Asia/Hong_Kong"));   // 香港英文
+        LOCALE_ZONE_MAP.put("en",    ZoneId.of("UTC"));              // 纯 en → UTC
+
+        // 日文
+        LOCALE_ZONE_MAP.put("ja-JP", ZoneId.of("Asia/Tokyo"));       // 日本 UTC+9
+        LOCALE_ZONE_MAP.put("ja",    ZoneId.of("Asia/Tokyo"));
+
+        // 韩文
+        LOCALE_ZONE_MAP.put("ko-KR", ZoneId.of("Asia/Seoul"));       // 韩国 UTC+9
+        LOCALE_ZONE_MAP.put("ko",    ZoneId.of("Asia/Seoul"));
+
+        // 德文
+        LOCALE_ZONE_MAP.put("de-DE", ZoneId.of("Europe/Berlin"));    // 德国 UTC+1/2
+        LOCALE_ZONE_MAP.put("de",    ZoneId.of("Europe/Berlin"));
+
+        // 法文
+        LOCALE_ZONE_MAP.put("fr-FR", ZoneId.of("Europe/Paris"));     // 法国 UTC+1/2
+        LOCALE_ZONE_MAP.put("fr",    ZoneId.of("Europe/Paris"));
+
+        // 西班牙文
+        LOCALE_ZONE_MAP.put("es-ES", ZoneId.of("Europe/Madrid"));    // 西班牙 UTC+1/2
+        LOCALE_ZONE_MAP.put("es-MX", ZoneId.of("America/Mexico_City")); // 墨西哥
+        LOCALE_ZONE_MAP.put("es",    ZoneId.of("Europe/Madrid"));
+
+        // 葡萄牙文
+        LOCALE_ZONE_MAP.put("pt-BR", ZoneId.of("America/Sao_Paulo")); // 巴西 UTC-3
+        LOCALE_ZONE_MAP.put("pt-PT", ZoneId.of("Europe/Lisbon"));     // 葡萄牙 UTC+0/1
+        LOCALE_ZONE_MAP.put("pt",    ZoneId.of("Europe/Lisbon"));
+
+        // 阿拉伯文
+        LOCALE_ZONE_MAP.put("ar-SA", ZoneId.of("Asia/Riyadh"));      // 沙特 UTC+3
+        LOCALE_ZONE_MAP.put("ar-AE", ZoneId.of("Asia/Dubai"));       // 迪拜 UTC+4
+        LOCALE_ZONE_MAP.put("ar",    ZoneId.of("Asia/Riyadh"));
+
+        // 印地文
+        LOCALE_ZONE_MAP.put("hi-IN", ZoneId.of("Asia/Kolkata"));     // 印度 UTC+5:30
+        LOCALE_ZONE_MAP.put("hi",    ZoneId.of("Asia/Kolkata"));
+
+        // 俄文
+        LOCALE_ZONE_MAP.put("ru-RU", ZoneId.of("Europe/Moscow"));    // 莫斯科 UTC+3
+        LOCALE_ZONE_MAP.put("ru",    ZoneId.of("Europe/Moscow"));
+
+        // 马来文
+        LOCALE_ZONE_MAP.put("ms-MY", ZoneId.of("Asia/Kuala_Lumpur")); // 马来西亚 UTC+8
+        LOCALE_ZONE_MAP.put("ms",    ZoneId.of("Asia/Kuala_Lumpur"));
+
+        // 泰文
+        LOCALE_ZONE_MAP.put("th-TH", ZoneId.of("Asia/Bangkok"));     // 泰国 UTC+7
+        LOCALE_ZONE_MAP.put("th",    ZoneId.of("Asia/Bangkok"));
+
+        // 越南文
+        LOCALE_ZONE_MAP.put("vi-VN", ZoneId.of("Asia/Ho_Chi_Minh")); // 越南 UTC+7
+        LOCALE_ZONE_MAP.put("vi",    ZoneId.of("Asia/Ho_Chi_Minh"));
+
+        // 印尼文
+        LOCALE_ZONE_MAP.put("id-ID", ZoneId.of("Asia/Jakarta"));     // 印尼西部 UTC+7
+        LOCALE_ZONE_MAP.put("id",    ZoneId.of("Asia/Jakarta"));
+    }
+
+    /**
+     * 从当前 HTTP 请求的 Accept-Language 头部解析对应时区。
+     * <ul>
+     *   <li>优先精确匹配,如 {@code zh-CN} → {@code Asia/Shanghai}</li>
+     *   <li>其次匹配语言前缀,如 {@code zh} → {@code Asia/Shanghai}</li>
+     *   <li>均未匹配时返回 {@code UTC}</li>
+     * </ul>
+     *
+     * @return 解析出的 {@link ZoneId},默认 UTC
+     */
+    public static ZoneId resolveZoneId() {
+        ServletRequestAttributes attributes =
+                (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        if (attributes == null) {
+            return UTC;
+        }
+        HttpServletRequest request = attributes.getRequest();
+        String acceptLanguage = request.getHeader("Accept-Language");
+        return parseZoneId(acceptLanguage);
+    }
+
+
+    /**
+     * 根据 Accept-Language 字符串解析时区(可单独测试)。
+     *
+     * @param acceptLanguage HTTP Accept-Language 头部值,如 "zh-CN,zh;q=0.9,en;q=0.8"
+     * @return 匹配的 {@link ZoneId},未匹配返回 UTC
+     */
+    /**
+     * 获取当前 UTC 时间,用于统一存入数据库。
+     * <p>
+     * 全项目保存时间字段统一调用此方法,避免因服务器时区配置不同导致存入非 UTC 时间。
+     * </p>
+     * <pre>
+     * // 推荐用法(替代 LocalDateTime.now())
+     * entity.setCreateTime(LocaleTimeZoneUtil.nowUtc());
+     * entity.setUpdateTime(LocaleTimeZoneUtil.nowUtc());
+     * </pre>
+     *
+     * @return 当前 UTC 时间的 {@link LocalDateTime}
+     */
+    public static LocalDateTime nowUtc() {
+        return LocalDateTime.now(ZoneOffset.UTC);
+    }
+
+    public static ZoneId parseZoneId(String acceptLanguage) {
+        if (StrUtil.isBlank(acceptLanguage)) {
+            return UTC;
+        }
+        // Accept-Language 格式:zh-CN,zh;q=0.9,en-US;q=0.8
+        // 按逗号拆分,取第一个语言标签(优先级最高)
+        String[] languages = acceptLanguage.split(",");
+        for (String lang : languages) {
+            // 去掉权重部分,如 "zh-CN;q=0.9" → "zh-CN"
+            String tag = lang.split(";")[0].trim();
+            if (StrUtil.isBlank(tag)) {
+                continue;
+            }
+            // 统一格式:zh_CN → zh-CN
+            tag = tag.replace("_", "-");
+
+            // 1. 精确匹配
+            ZoneId zoneId = LOCALE_ZONE_MAP.get(tag);
+            if (zoneId != null) {
+                return zoneId;
+            }
+            // 2. 匹配语言前缀(取 "-" 之前部分)
+            if (tag.contains("-")) {
+                String prefix = tag.substring(0, tag.indexOf("-"));
+                zoneId = LOCALE_ZONE_MAP.get(prefix);
+                if (zoneId != null) {
+                    return zoneId;
+                }
+            }
+        }
+        return UTC;
+    }
+}

+ 20 - 13
product-web/src/main/java/com/poyee/facade/impl/ProductInfoFacade.java

@@ -1,6 +1,7 @@
 package com.poyee.facade.impl;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.date.LocalDateTimeUtil;
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.BooleanUtil;
 import cn.hutool.core.util.IdUtil;
@@ -27,12 +28,12 @@ import com.poyee.res.ProductFastSaleRes;
 import com.poyee.res.ProductListRes;
 import com.poyee.service.*;
 import com.poyee.utils.I18nUtil;
+import com.poyee.utils.LocaleTimeZoneUtil;
 import com.poyee.utils.ServletUtils;
 import lombok.AllArgsConstructor;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
 
-import java.time.LocalDateTime;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
@@ -118,6 +119,7 @@ public class ProductInfoFacade implements IProductInfoFacade {
                     .defaultImage(productMedia.getMainImage())
                     .productProperties(productInfo.getProductProperties())
                     .sort(productInfo.getSort())
+                    .createTime(productInfo.getCreateTime())
                     .productDesc(productInfo.getProductDesc())
                     .otherImages(otherImages).build();
         } else {
@@ -140,6 +142,8 @@ public class ProductInfoFacade implements IProductInfoFacade {
         validateReviewStatus(productInfo, productReviewReq.getStatus());
         sysDictDataService.validateOptions(ProductConstant.DICT_KEY_PRODUCT_STATUS, StrUtil.toString(productReviewReq.getStatus()));
         productInfo.setStatus(productReviewReq.getStatus());
+        productInfo.setUpdateBy(Objects.nonNull(ServletUtils.getCurrentUser()) ? ServletUtils.getCurrentUser().getUsername() : null);
+        productInfo.setUpdateTime(LocaleTimeZoneUtil.nowUtc());
         return productInfoService.updateById(productInfo);
     }
 
@@ -161,16 +165,15 @@ public class ProductInfoFacade implements IProductInfoFacade {
 
         productInfo.setIdleFlag(BooleanUtil.isTrue(idleAttributeReq.getIdleFlag()) ? StatusEnum.TRUE.getCode() : StatusEnum.FALSE.getCode());
         ProductPermission productPermission = productPermissionService.lambdaQuery().eq(ProductPermission::getRelationSku, productInfo.getSku()).one();
-        LocalDateTime utcNow = LocalDateTime.now(java.time.ZoneOffset.UTC);
         if (Objects.isNull(productPermission)) {
             productPermission = ProductPermission.builder()
                     .permission(idleAttributeReq.getPermission())
-                    .createTme(utcNow)
+                    .createTme(LocaleTimeZoneUtil.nowUtc())
                     .relationSku(productInfo.getSku()).build();
             productPermissionService.save(productPermission);
         } else {
             productPermission.setPermission(idleAttributeReq.getPermission());
-            productPermission.setUpdateTime(utcNow);
+            productPermission.setUpdateTime(LocaleTimeZoneUtil.nowUtc());
             productPermissionService.updateById(productPermission);
         }
         return this.productInfoService.updateById(productInfo);
@@ -209,8 +212,8 @@ public class ProductInfoFacade implements IProductInfoFacade {
                     .recycleFlag(BooleanUtil.isTrue(fastSaleReq.getRecycleFlag()) ? StatusEnum.TRUE.getCode() : StatusEnum.FALSE.getCode())
                     .recycleInventory(fastSaleReq.getRecycleInventory())
                     .saleTime(fastSaleReq.getSaleTime())
-                    .createBy(Objects.nonNull(ServletUtils.getCurrentUser()) ? ServletUtils.getCurrentUser().getId() : null)
-                    .createTime(LocalDateTime.now())
+                    .createBy(Objects.nonNull(ServletUtils.getCurrentUser()) ? ServletUtils.getCurrentUser().getUsername() : null)
+                    .createTime(LocaleTimeZoneUtil.nowUtc())
                     .build();
             this.productFastSaleService.save(newFastSale);
             productInfo.setFastSaleFlag(StatusEnum.TRUE.getCode());
@@ -220,8 +223,8 @@ public class ProductInfoFacade implements IProductInfoFacade {
             existingFastSale.setRecycleFlag(BooleanUtil.isTrue(fastSaleReq.getRecycleFlag()) ? StatusEnum.TRUE.getCode() : StatusEnum.FALSE.getCode());
             existingFastSale.setRecycleInventory(fastSaleReq.getRecycleInventory());
             existingFastSale.setSaleTime(fastSaleReq.getSaleTime());
-            existingFastSale.setUpdateBy(Objects.nonNull(ServletUtils.getCurrentUser()) ? ServletUtils.getCurrentUser().getId() : null);
-            existingFastSale.setUpdateTime(LocalDateTime.now());
+            existingFastSale.setUpdateBy(Objects.nonNull(ServletUtils.getCurrentUser()) ? ServletUtils.getCurrentUser().getUsername() : null);
+            existingFastSale.setUpdateTime(LocaleTimeZoneUtil.nowUtc());
             return this.productFastSaleService.updateById(existingFastSale);
         }
     }
@@ -232,7 +235,6 @@ public class ProductInfoFacade implements IProductInfoFacade {
         ProductTypeEnum productTypeEnum = getByCode(productInfoAddReq.getProductType());
         sysDictDataService.validateOptions(ProductConstant.DICT_KEY_PRODUCT_TYPE, StrUtil.toString(productInfoAddReq.getProductType()));
         sysDictDataService.validateOptions(ProductConstant.DICT_KEY_LABEL, StrUtil.toString(productInfoAddReq.getLabelKey()));
-
         String sku = SKU_PREFIX + IdUtil.getSnowflakeNextId() % 1000000000000000000L;
         ProductInfo productInfo;
         switch (productTypeEnum) {
@@ -243,13 +245,13 @@ public class ProductInfoFacade implements IProductInfoFacade {
                         .manufacturer(productInfoAddReq.getManufacturer())
                         .sets(productInfoAddReq.getSets())
                         .setsVersion(productInfoAddReq.getSetsVersion())
-                        .createTime(LocalDateTime.now(java.time.ZoneOffset.UTC))
+                        .createTime(LocaleTimeZoneUtil.nowUtc())
                         .relationSku(sku)
                         .build();
                 ProductMedia productMedia = ProductMedia.builder()
                         .mainImage(productInfoAddReq.getDefaultImage())
                         .relationSku(sku)
-                        .createTime(LocalDateTime.now(java.time.ZoneOffset.UTC))
+                        .createTime(LocaleTimeZoneUtil.nowUtc())
                         .otherImages(StrUtil.join(",", productInfoAddReq.getOtherImages()))
                         .build();
                 productInfo = ProductInfo.builder()
@@ -261,7 +263,8 @@ public class ProductInfoFacade implements IProductInfoFacade {
                         .relationCode(productInfoAddReq.getRelationCode())
                         .productProperties(productInfoAddReq.getProductProperties())
                         .sort(productInfoAddReq.getSort())
-                        .createTime(LocalDateTime.now(java.time.ZoneOffset.UTC))
+                        .createBy(Objects.nonNull(ServletUtils.getCurrentUser()) ? ServletUtils.getCurrentUser().getUsername() : null)
+                        .createTime(LocaleTimeZoneUtil.nowUtc())
                         .build();
                 this.productInfoService.save(productInfo);
                 this.cardAttributeService.save(cardAttribute);
@@ -275,10 +278,11 @@ public class ProductInfoFacade implements IProductInfoFacade {
                         .productType(productInfoAddReq.getProductType())
                         .label(productInfoAddReq.getLabelKey())
                         .sort(productInfoAddReq.getSort())
+                        .createBy(Objects.nonNull(ServletUtils.getCurrentUser()) ? ServletUtils.getCurrentUser().getUsername() : null)
                         .productProperties(productInfoAddReq.getProductProperties())
                         .productDesc(productInfoAddReq.getProductDesc())
                         .relationCode(productInfoAddReq.getRelationCode())
-                        .createTime(LocalDateTime.now(java.time.ZoneOffset.UTC))
+                        .createTime(LocaleTimeZoneUtil.nowUtc())
                         .build();
                 this.productInfoService.save(productInfo);
                 break;
@@ -308,6 +312,7 @@ public class ProductInfoFacade implements IProductInfoFacade {
         if (Objects.nonNull(productInfoEditReq.getSort())) {
             productInfo.setSort(productInfoEditReq.getSort());
         }
+        productInfo.setUpdateTime(LocaleTimeZoneUtil.nowUtc());
         this.productInfoService.updateById(productInfo);
         if (StrUtil.isNotBlank(productInfoEditReq.getDefaultImage()) ||
                 CollUtil.isNotEmpty(productInfoEditReq.getOtherImages())) {
@@ -318,6 +323,7 @@ public class ProductInfoFacade implements IProductInfoFacade {
                 productMedia = ProductMedia.builder()
                         .relationSku(productInfoEditReq.getSku())
                         .mainImage(productInfoEditReq.getDefaultImage())
+                        .createTime(LocaleTimeZoneUtil.nowUtc())
                         .otherImages(CollUtil.join(productInfoEditReq.getOtherImages(), ","))
                         .build();
                 this.productMediaService.save(productMedia);
@@ -328,6 +334,7 @@ public class ProductInfoFacade implements IProductInfoFacade {
                 if (CollUtil.isNotEmpty(productInfoEditReq.getOtherImages())) {
                     productMedia.setOtherImages(CollUtil.join(productInfoEditReq.getOtherImages(), ","));
                 }
+                productMedia.setUpdateTime(LocaleTimeZoneUtil.nowUtc());
                 this.productMediaService.updateById(productMedia);
             }
         }

+ 4 - 1
product-web/src/main/java/com/poyee/facade/impl/ProductItemFacade.java

@@ -21,6 +21,7 @@ import com.poyee.res.ProductItemListRes;
 import com.poyee.service.ProductInfoService;
 import com.poyee.service.ProductItemService;
 import com.poyee.service.SysDictDataService;
+import com.poyee.utils.LocaleTimeZoneUtil;
 import com.poyee.utils.ServletUtils;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -87,7 +88,7 @@ public class ProductItemFacade implements IProductItemFacade {
                 .frozenInventory(0)
                 .sales(0)
                 .publisher(Objects.nonNull(ServletUtils.getCurrentUser()) ? ServletUtils.getCurrentUser().getUsername() : null)
-                .createTime(LocalDateTime.now(java.time.ZoneOffset.UTC))
+                .createTime(LocaleTimeZoneUtil.nowUtc())
                 .build();
         return this.productItemService.save(productItem);
     }
@@ -113,6 +114,8 @@ public class ProductItemFacade implements IProductItemFacade {
         if (Objects.nonNull(productItemEditReq.getInventory())) {
             productItem.setInventory(productItemEditReq.getInventory());
         }
+        productItem.setUpdateTime(LocaleTimeZoneUtil.nowUtc());
+        productItem.setUpdateBy(Objects.nonNull(ServletUtils.getCurrentUser()) ? ServletUtils.getCurrentUser().getUsername() : null);
         return this.productItemService.updateById(productItem);
     }