Vue3 + Element Plus 项目中日期时间处理的最佳实践与数据库设计规范

Vue3 + Element Plus 项目中日期时间处理的最佳实践与数据库设计规范

前言

在开发企业级管理系统过程中,遇到了一个看似简单但实际复杂的问题:日期时间处理。不同模块使用不同的日期格式,前后端交互时出现格式不匹配,数据库设计也不够规范。经过深入分析和实践,总结出了一套完整的解决方案。

🚨 问题分析

1. 前端日期格式不统一

在我们的项目中,不同模块使用了不同的日期格式:

vue 复制代码
<!-- 订单管理模块 -->
<el-date-picker
  v-model="formData.orderDate"
  type="date"
  value-format="YYYY-MM-DD"
/>

<!-- 发货管理模块 -->
<el-date-picker
  v-model="queryParams.shippingDate"
  type="daterange"
  value-format="YYYY-MM-DD HH:mm:ss"
  :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
/>

<!-- 用户管理模块 -->
<el-date-picker
  v-model="formData.registerDate"
  type="date"
  value-format="x"  <!-- 时间戳格式 -->
/>

2. 表格显示格式混乱

vue 复制代码
<!-- 不同表格使用不同的格式化函数 -->
<el-table-column 
  label="订单日期" 
  :formatter="dateFormatter2"  <!-- YYYY-MM-DD -->
/>

<el-table-column 
  label="创建时间" 
  :formatter="dateFormatter"    <!-- YYYY-MM-DD HH:mm:ss -->
/>

3. 前后端交互问题

typescript 复制代码
// 前端发送格式
const data = {
  orderDate: "2024-01-15",  // yyyy-MM-dd
  deliveryDate: "2024-01-15 14:30:00"  // yyyy-MM-dd HH:mm:ss
}

// 后端期望格式不一致,导致解析错误

🎯 解决方案

1. 统一日期格式标准

首先,建立统一的日期格式标准:

typescript 复制代码
// utils/dateConstants.ts
export const DATE_FORMATS = {
  DATE_ONLY: 'YYYY-MM-DD',           // 仅日期
  DATETIME: 'YYYY-MM-DD HH:mm:ss',   // 日期时间
  TIMESTAMP: 'x',                    // 时间戳
  ISO: 'YYYY-MM-DDTHH:mm:ss.SSSZ'    // ISO格式
}

// 使用场景定义
export const DATE_USAGE = {
  // 业务日期字段(只需要日期)
  BUSINESS_DATE: DATE_FORMATS.DATE_ONLY,
  // 系统时间字段(需要精确到秒)
  SYSTEM_TIME: DATE_FORMATS.DATETIME,
  // 时间戳字段
  TIMESTAMP: DATE_FORMATS.TIMESTAMP
}

2. 创建统一的日期处理工具

typescript 复制代码
// utils/dateHandler.ts
import dayjs from 'dayjs'
import { DATE_FORMATS } from './dateConstants'

export class DateHandler {
  /**
   * 统一日期格式化
   * @param date 日期值
   * @param format 格式类型
   * @returns 格式化后的字符串
   */
  static formatDate(date: any, format: string = DATE_FORMATS.DATE_ONLY): string {
    if (!date) return ''
    return dayjs(date).format(format)
  }
  
  /**
   * 统一日期范围处理
   * @param dates 日期范围数组
   * @param format 格式类型
   * @returns 格式化后的日期范围
   */
  static formatDateRange(dates: [string, string], format: string = DATE_FORMATS.DATE_ONLY): [string, string] {
    if (!dates || dates.length !== 2) return ['', '']
    return [dayjs(dates[0]).format(format), dayjs(dates[1]).format(format)]
  }
  
  /**
   * 统一日期验证
   * @param date 日期值
   * @returns 是否有效
   */
  static isValidDate(date: any): boolean {
    return dayjs(date).isValid()
  }
  
  /**
   * 获取日期范围查询参数
   * @param dateRange 日期范围
   * @returns 查询参数
   */
  static getDateRangeQuery(dateRange: [string, string]): { startTime: string, endTime: string } {
    if (!dateRange || dateRange.length !== 2) {
      return { startTime: '', endTime: '' }
    }
    
    return {
      startTime: dayjs(dateRange[0]).startOf('day').format(DATE_FORMATS.DATETIME),
      endTime: dayjs(dateRange[1]).endOf('day').format(DATE_FORMATS.DATETIME)
    }
  }
}

3. 统一表格日期格式化

typescript 复制代码
// utils/tableFormatters.ts
import { DateHandler } from './dateHandler'
import { DATE_FORMATS } from './dateConstants'

export const tableDateFormatters = {
  /**
   * 仅显示日期
   */
  dateOnly: (row: any, column: any, cellValue: any): string => {
    return DateHandler.formatDate(cellValue, DATE_FORMATS.DATE_ONLY) || '-'
  },
  
  /**
   * 显示日期时间
   */
  dateTime: (row: any, column: any, cellValue: any): string => {
    return DateHandler.formatDate(cellValue, DATE_FORMATS.DATETIME) || '-'
  },
  
  /**
   * 相对时间显示
   */
  relative: (row: any, column: any, cellValue: any): string => {
    if (!cellValue) return '-'
    return dayjs(cellValue).fromNow()
  }
}

4. 统一日期选择器配置

vue 复制代码
<!-- components/DatePicker/index.vue -->
<template>
  <div class="date-picker-wrapper">
    <!-- 仅日期选择 -->
    <el-date-picker
      v-if="type === 'date'"
      v-model="modelValue"
      type="date"
      :value-format="DATE_FORMATS.DATE_ONLY"
      :placeholder="placeholder"
      :disabled="disabled"
      @change="handleChange"
    />
    
    <!-- 日期时间选择 -->
    <el-date-picker
      v-else-if="type === 'datetime'"
      v-model="modelValue"
      type="datetime"
      :value-format="DATE_FORMATS.DATETIME"
      :placeholder="placeholder"
      :disabled="disabled"
      @change="handleChange"
    />
    
    <!-- 日期范围选择 -->
    <el-date-picker
      v-else-if="type === 'daterange'"
      v-model="modelValue"
      type="daterange"
      :value-format="DATE_FORMATS.DATE_ONLY"
      :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
      :start-placeholder="startPlaceholder"
      :end-placeholder="endPlaceholder"
      :disabled="disabled"
      @change="handleChange"
    />
  </div>
</template>

<script setup lang="ts">
import { DATE_FORMATS } from '@/utils/dateConstants'

interface Props {
  modelValue: string | [string, string]
  type: 'date' | 'datetime' | 'daterange'
  placeholder?: string
  startPlaceholder?: string
  endPlaceholder?: string
  disabled?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  placeholder: '请选择日期',
  startPlaceholder: '开始日期',
  endPlaceholder: '结束日期',
  disabled: false
})

const emit = defineEmits<{
  'update:modelValue': [value: string | [string, string]]
  'change': [value: string | [string, string]]
}>()

const handleChange = (value: string | [string, string]) => {
  emit('update:modelValue', value)
  emit('change', value)
}
</script>

🗄️ 数据库设计规范

1. 时间字段类型选择

sql 复制代码
-- ✅ 推荐:系统字段使用 timestamp(6)
"create_time" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"update_time" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,

-- ✅ 推荐:业务时间字段使用 timestamp(6) 
"delivery_date" timestamp(6),
"shipping_date" timestamp(6),
"processing_time" timestamp(6),

-- ✅ 推荐:纯日期业务字段使用 date
"order_date" date,
"effective_date" date,
"expire_date" date,

2. 完整的表结构设计

sql 复制代码
-- 订单表
CREATE TABLE "sys_order" (
  "id" int8 NOT NULL DEFAULT nextval('sys_order_id_seq'::regclass),
  "order_no" varchar(50) NOT NULL,
  "order_name" varchar(100) NOT NULL,
  "order_type" int2 NOT NULL,
  "status" int2 NOT NULL DEFAULT 0,
  
  -- 业务日期字段(只需要日期)
  "order_date" date,
  "effective_date" date,
  "expire_date" date,
  
  -- 业务金额字段
  "order_amount" numeric(15,2) DEFAULT 0,
  "paid_amount" numeric(15,2) DEFAULT 0,
  "unpaid_amount" numeric(15,2) DEFAULT 0,
  
  -- 系统字段
  "creator" varchar(64) DEFAULT '',
  "create_time" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
  "updater" varchar(64) DEFAULT '',
  "update_time" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
  "deleted" bool NOT NULL DEFAULT false,
  "tenant_id" int8 NOT NULL DEFAULT 0,
  
  CONSTRAINT "sys_order_pkey" PRIMARY KEY ("id")
);

-- 发货记录表
CREATE TABLE "sys_shipping_record" (
  "id" int8 NOT NULL DEFAULT nextval('sys_shipping_record_id_seq'::regclass),
  "order_id" int8 NOT NULL,
  "shipping_no" varchar(50) NOT NULL,
  
  -- 业务时间字段(需要精确到秒)
  "shipping_date" timestamp(6) NOT NULL,
  "delivery_date" timestamp(6),
  "arrival_date" timestamp(6),
  
  -- 业务数据字段
  "shipping_quantity" int4 DEFAULT 0,
  "logistics_company" varchar(100),
  "logistics_no" varchar(100),
  
  -- 系统字段
  "creator" varchar(64) DEFAULT '',
  "create_time" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
  "updater" varchar(64) DEFAULT '',
  "update_time" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
  "deleted" bool NOT NULL DEFAULT false,
  "tenant_id" int8 NOT NULL DEFAULT 0,
  
  CONSTRAINT "sys_shipping_record_pkey" PRIMARY KEY ("id")
);

3. 索引优化

sql 复制代码
-- 为常用查询字段添加索引
CREATE INDEX "idx_sys_order_order_date" ON "sys_order" ("order_date");
CREATE INDEX "idx_sys_order_create_time" ON "sys_order" ("create_time");
CREATE INDEX "idx_sys_order_status" ON "sys_order" ("status");

CREATE INDEX "idx_sys_shipping_shipping_date" ON "sys_shipping_record" ("shipping_date");
CREATE INDEX "idx_sys_shipping_order_id" ON "sys_shipping_record" ("order_id");

后端Java实体类设计

1. 实体类映射

java 复制代码
// Order.java
@Data
@TableName("sys_order")
public class OrderDO {
    
    @TableId(type = IdType.AUTO)
    private Long id;
    
    private String orderNo;
    private String orderName;
    private Integer orderType;
    private Integer status;
    
    // 业务日期字段 - 对应数据库 date 类型
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate orderDate;
    
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate effectiveDate;
    
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate expireDate;
    
    // 业务金额字段
    private BigDecimal orderAmount;
    private BigDecimal paidAmount;
    private BigDecimal unpaidAmount;
    
    // 系统字段 - 对应数据库 timestamp(6) 类型
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    
    private String creator;
    private String updater;
    private Boolean deleted;
    private Long tenantId;
}

// ShippingRecord.java
@Data
@TableName("sys_shipping_record")
public class ShippingRecordDO {
    
    @TableId(type = IdType.AUTO)
    private Long id;
    
    private Long orderId;
    private String shippingNo;
    
    // 业务时间字段 - 对应数据库 timestamp(6) 类型
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime shippingDate;
    
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime deliveryDate;
    
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime arrivalDate;
    
    // 业务数据字段
    private Integer shippingQuantity;
    private String logisticsCompany;
    private String logisticsNo;
    
    // 系统字段
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    
    private String creator;
    private String updater;
    private Boolean deleted;
    private Long tenantId;
}

2. VO类设计

java 复制代码
// OrderVO.java
@Data
public class OrderVO {
    
    private Long id;
    private String orderNo;
    private String orderName;
    private Integer orderType;
    private Integer status;
    
    // 业务日期字段
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate orderDate;
    
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate effectiveDate;
    
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate expireDate;
    
    // 业务金额字段
    private BigDecimal orderAmount;
    private BigDecimal paidAmount;
    private BigDecimal unpaidAmount;
    
    // 系统字段
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    
    private String creatorName;
    private String updaterName;
}

🚀 实际应用示例

1. 订单表单组件

vue 复制代码
<!-- OrderForm.vue -->
<template>
  <Dialog :title="dialogTitle" v-model="dialogVisible" width="1400px">
    <el-form ref="formRef" :model="formData" :rules="formRules">
      <!-- 订单基本信息 -->
      <el-row :gutter="20">
        <el-col :span="6">
          <el-form-item label="订单日期" prop="orderDate">
            <DatePicker
              v-model="formData.orderDate"
              type="date"
              placeholder="请选择订单日期"
            />
          </el-form-item>
        </el-col>
        <el-col :span="6">
          <el-form-item label="到期日期" prop="expireDate">
            <DatePicker
              v-model="formData.expireDate"
              type="date"
              placeholder="请选择到期日期"
            />
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>
  </Dialog>
</template>

<script setup lang="ts">
import { DateHandler } from '@/utils/dateHandler'
import { DATE_FORMATS } from '@/utils/dateConstants'

const formData = ref({
  orderDate: '',
  expireDate: '',
  // ... 其他字段
})

// 提交表单时的数据处理
const submitForm = async () => {
  // 校验表单
  await formRef.value.validate()
  
  // 处理日期字段
  const data = {
    ...formData.value,
    // 确保日期格式正确
    orderDate: DateHandler.formatDate(formData.value.orderDate, DATE_FORMATS.DATE_ONLY),
    expireDate: DateHandler.formatDate(formData.value.expireDate, DATE_FORMATS.DATE_ONLY),
  }
  
  // 提交到后端
  if (formType.value === 'create') {
    await OrderApi.createOrder(data)
  } else {
    await OrderApi.updateOrder(data)
  }
}
</script>

2. 列表查询组件

vue 复制代码
<!-- OrderList.vue -->
<template>
  <ContentWrap>
    <el-form :model="queryParams" ref="queryFormRef" :inline="true">
      <el-form-item label="订单日期" prop="orderDate">
        <DatePicker
          v-model="queryParams.orderDate"
          type="daterange"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
        />
      </el-form-item>
    </el-form>
    
    <el-table :data="list" :stripe="true">
      <el-table-column 
        label="订单日期" 
        prop="orderDate" 
        :formatter="tableDateFormatters.dateOnly"
      />
      <el-table-column 
        label="创建时间" 
        prop="createTime" 
        :formatter="tableDateFormatters.dateTime"
      />
    </el-table>
  </ContentWrap>
</template>

<script setup lang="ts">
import { DateHandler } from '@/utils/dateHandler'
import { tableDateFormatters } from '@/utils/tableFormatters'

const queryParams = reactive({
  orderDate: [] as [string, string],
  // ... 其他查询参数
})

// 查询列表
const getList = async () => {
  const params = {
    ...queryParams,
    // 处理日期范围查询
    ...DateHandler.getDateRangeQuery(queryParams.orderDate)
  }
  
  const data = await OrderApi.getOrderPage(params)
  list.value = data.list || []
  total.value = data.total || 0
}
</script>

📊 数据迁移方案

1. 现有数据迁移脚本

sql 复制代码
-- 迁移订单表的订单日期字段
-- 步骤1:添加新字段
ALTER TABLE "sys_order" 
ADD COLUMN "order_date_new" date;

-- 步骤2:迁移数据
UPDATE "sys_order" 
SET "order_date_new" = "order_date"::date 
WHERE "order_date" IS NOT NULL AND "order_date" != '';

-- 步骤3:删除旧字段
ALTER TABLE "sys_order" 
DROP COLUMN "order_date";

-- 步骤4:重命名新字段
ALTER TABLE "sys_order" 
RENAME COLUMN "order_date_new" TO "order_date";

-- 步骤5:添加约束
ALTER TABLE "sys_order" 
ALTER COLUMN "order_date" SET NOT NULL;

2. 兼容性处理

java 复制代码
// 在实体类中添加兼容性处理
@Data
public class OrderDO {
    
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate orderDate;
    
    // 兼容性方法:处理历史数据
    @JsonSetter
    public void setOrderDate(String dateStr) {
        if (StringUtils.isNotBlank(dateStr)) {
            try {
                this.orderDate = LocalDate.parse(dateStr);
            } catch (Exception e) {
                // 处理格式不正确的历史数据
                log.warn("Invalid date format: {}", dateStr);
                this.orderDate = null;
            }
        }
    }
}

⚡ 性能优化建议

1. 查询优化

sql 复制代码
-- 使用日期范围查询时,确保索引生效
SELECT * FROM sys_order 
WHERE order_date BETWEEN '2024-01-01' AND '2024-12-31'
  AND status = 1;

-- 时间戳查询优化
SELECT * FROM sys_shipping_record 
WHERE shipping_date >= '2024-01-01 00:00:00' 
  AND shipping_date <= '2024-12-31 23:59:59'
  AND order_id = 123;

2. 分区表设计

sql 复制代码
-- 对于大数据量的时间相关表,考虑按时间分区
CREATE TABLE sys_shipping_record_2024 PARTITION OF sys_shipping_record
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

CREATE TABLE sys_shipping_record_2025 PARTITION OF sys_shipping_record
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');

🎯 实施效果

1. 开发效率提升

  • 统一标准:所有开发者使用相同的日期处理规范
  • 减少错误:避免格式不匹配导致的问题
  • 代码复用:统一的工具类和组件

2. 用户体验改善

  • 界面一致:所有页面的日期选择器行为一致
  • 数据准确:避免时区转换导致的时间错误
  • 查询高效:优化的索引和查询语句

3. 维护成本降低

  • 代码清晰:统一的命名和格式规范
  • 问题定位:标准化的错误处理
  • 扩展容易:新功能可以复用现有规范

🔮 未来改进方向

1. 技术升级

  • 时区支持:考虑多时区业务场景
  • 国际化:支持不同地区的日期格式
  • 性能优化:大数据量下的查询优化

2. 功能扩展

  • 日期计算:业务日期的自动计算
  • 提醒功能:基于日期的业务提醒
  • 报表统计:时间维度的数据分析

总结

通过建立统一的日期处理规范,我们解决了以下问题:

  1. 格式不统一:建立了标准化的日期格式
  2. 交互问题:规范了前后端数据交互
  3. 数据库设计:优化了时间字段的类型选择
  4. 开发效率:提供了可复用的工具和组件

这套方案不仅解决了当前的问题,也为未来的功能扩展打下了良好的基础。建议在项目中逐步实施,先从核心模块开始,然后推广到整个系统。

相关推荐
白鹭6 小时前
MySQL源码部署(rhel7)
数据库·mysql
666和7776 小时前
Struts2 工作总结
java·数据库
还听珊瑚海吗6 小时前
SpringMVC(一)
数据库
星期天要睡觉8 小时前
MySQL 综合练习
数据库·mysql
Y4090018 小时前
数据库基础知识——聚合函数、分组查询
android·数据库
JosieBook9 小时前
【数据库】MySQL 数据库创建存储过程及使用场景详解
数据库·mysql
处女座_三月9 小时前
改 TDengine 数据库的时间写入限制
数据库·sql·mysql