yxh
7 天以前 68ff33beff11310e830b0615b5a81f83fad0f683
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
package cn.lihu.jh.module.ecg.service.appointment;
 
import cn.lihu.jh.framework.common.util.object.BeanUtils;
import cn.lihu.jh.module.ecg.controller.admin.appointment.vo.AppointmentSaveReqVO;
import cn.lihu.jh.module.ecg.dal.dataobject.appointment.AppointmentDO;
import org.junit.Test;
 
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.text.SimpleDateFormat;
 
import static org.junit.Assert.*;
 
/**
 * 锁定 {@code register_date}「时间被截断为 00:00:00」的成因与修复。
 *
 * <h3>现象</h3>
 * 2026-09-27 起 {@code appointment.register_date} 大量变成 {@code yyyy-MM-dd 00:00:00},
 * 而 09-26 之前是 {@code 2026-09-26 16:58:53} 这样的完整时间。
 *
 * <h3>成因(经实验确定)</h3>
 * {@code java.sql.Date} 是**只表示日期**的 JDBC 类型;把它写入 {@code DATETIME} 列时,
 * 时间部分必然变成 {@code 00:00:00}。实测(同连接串、同列):
 * <pre>
 *   setObject(java.util.Date)  → 2026-09-26 16:58:53    保留
 *   setObject(java.sql.Date)   → 2026-09-26 00:00:00    丢弃
 * </pre>
 * 而 {@code AppointmentSaveReqVO}/{@code AppointmentRespVO}/{@code AppointmentPageReqVO}
 * 原先都把 {@code registerDate}/{@code doctorTime} 声明为 {@code java.sql.Date}
 * (经 {@code import java.sql.Date;}),对应的 {@code AppointmentDO} 却是
 * {@code java.util.Date} —— 类型不一致导致往返过程中时间丢失。
 *
 * <h3>修复</h3>
 * 三个 VO 统一改为 {@code java.util.Date}。本测试锁定「类型一致」这一约束。
 */
public class RegisterDateTruncationTest {
 
    private static final String SAMPLE = "2026-09-26 16:58:53";
 
    /** 生产/测试库连接串(与 application-local.yaml 一致) */
    private static final String JDBC_URL =
            "jdbc:mysql://47.93.127.38:3308/lihu?useSSL=false&serverTimezone=Asia/Shanghai"
          + "&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true";
 
    /**
     * 核心约束:VO 与 DO 的 registerDate/doctorTime 必须是**同一种** Date 类型,
     * 且都不是 {@code java.sql.Date}(否则时间会被丢弃)。
     */
    @Test
    public void testRegisterDateTypes_shouldBeConsistentAndNotSqlDate() throws Exception {
        Class<?> voReg = AppointmentSaveReqVO.class.getDeclaredField("registerDate").getType();
        Class<?> voDoc = AppointmentSaveReqVO.class.getDeclaredField("doctorTime").getType();
        Class<?> doReg = AppointmentDO.class.getDeclaredField("registerDate").getType();
        Class<?> doDoc = AppointmentDO.class.getDeclaredField("doctorTime").getType();
 
        System.out.println("[类型] VO.registerDate = " + voReg.getName());
        System.out.println("[类型] VO.doctorTime  = " + voDoc.getName());
        System.out.println("[类型] DO.registerDate = " + doReg.getName());
        System.out.println("[类型] DO.doctorTime  = " + doDoc.getName());
 
        assertNotEquals("VO.registerDate 不得为 java.sql.Date(会丢弃时分秒)",
                "java.sql.Date", voReg.getName());
        assertNotEquals("VO.doctorTime 不得为 java.sql.Date",
                "java.sql.Date", voDoc.getName());
        assertEquals("VO 与 DO 的 registerDate 类型必须一致",
                doReg.getName(), voReg.getName());
        assertEquals("VO 与 DO 的 doctorTime 类型必须一致",
                doDoc.getName(), voDoc.getName());
    }
 
    /** 往返转换不得丢失时间 */
    @Test
    public void testDoToVoToDo_keepsTime() throws Exception {
        AppointmentDO source = new AppointmentDO();
        source.setApplyNo("TEST_TRUNC_1");
        source.setRegisterDate(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(SAMPLE));
        source.setDoctorTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(SAMPLE));
 
        AppointmentSaveReqVO vo = BeanUtils.toBean(source, AppointmentSaveReqVO.class);
        AppointmentDO back = BeanUtils.toBean(vo, AppointmentDO.class);
 
        String reg = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(back.getRegisterDate());
        String doc = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(back.getDoctorTime());
        System.out.println("[往返] registerDate = " + reg + "  doctorTime = " + doc);
 
        assertEquals("registerDate 经 VO 往返后不应丢失时间", SAMPLE, reg);
        assertEquals("doctorTime 经 VO 往返后不应丢失时间", SAMPLE, doc);
    }
 
    /**
     * 复现实验(需要测试库可访问):证明「java.sql.Date → DATETIME 列」必然截断。
     * <p>若数据库不可达则跳过(不影响其它断言的价值)。
     */
    @Test
    public void testJdbcWrite_sqlDateTruncates_utilDateKeeps() throws Exception {
        Connection c = null;
        try {
            c = DriverManager.getConnection(JDBC_URL, "smartor", "Smartor.2016");
        } catch (Exception e) {
            System.out.println("[跳过] 数据库不可达: " + e.getMessage());
            return;
        }
        try {
            c.setAutoCommit(false);
            try (Statement s = c.createStatement()) {
                s.execute("DROP TABLE IF EXISTS _probe_regdate_test");
                s.execute("CREATE TABLE _probe_regdate_test (id INT PRIMARY KEY AUTO_INCREMENT, "
                        + "d_util DATETIME NULL, d_sql DATETIME NULL)");
            }
            java.util.Date utilDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(SAMPLE);
            java.sql.Date sqlDate = new java.sql.Date(utilDate.getTime());
            try (PreparedStatement ps = c.prepareStatement(
                    "INSERT INTO _probe_regdate_test (d_util, d_sql) VALUES (?,?)")) {
                ps.setObject(1, utilDate);
                ps.setObject(2, sqlDate);
                ps.executeUpdate();
            }
            try (Statement s = c.createStatement();
                 ResultSet r = s.executeQuery("SELECT d_util, d_sql FROM _probe_regdate_test")) {
                assertTrue(r.next());
                String util = r.getString(1);
                String sql = r.getString(2);
                System.out.println("[实验] java.util.Date → " + util);
                System.out.println("[实验] java.sql.Date  → " + sql);
 
                assertEquals("java.util.Date 应保留时分秒", SAMPLE, util);
                assertEquals("java.sql.Date 必然丢成 00:00:00",
                        "2026-09-26 00:00:00", sql);
            }
            try (Statement s = c.createStatement()) { s.execute("DROP TABLE _probe_regdate_test"); }
            c.rollback();
        } finally {
            if (c != null) { try { c.close(); } catch (Exception ignore) { } }
        }
    }
}