fix(employees): preserve age across birthday boundaries
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
# 员工年龄反算出生日期偏差一年
|
||||||
|
|
||||||
|
日期:2026-07-18
|
||||||
|
文档路径:document/development/2026-07-18/dev-logs/bugs/employee-age-birthdate-conversion.md
|
||||||
|
|
||||||
|
## 修复记录
|
||||||
|
|
||||||
|
- 15:40:记录 bug 修复:员工生日在当年尚未到来时,编辑年龄会反算出少一年的年龄。
|
||||||
|
- Git 提交检查:`git fetch --all --prune` 成功;upstream `origin/main` 无新提交;本地 ahead 19 条,最新包括 `07241b46 fix(docker): manage local postgres in default compose`、`787bc3a4 feat(platform): close AI expense value loop`、`242d68c3 feat(approval): add task workflow and waiver decisions`,另有 16 条。
|
||||||
|
- 修改:`employeeManagementModel.js` 在目标生日尚未到来时向前校正出生年份,并为年龄与出生日期转换函数增加可注入的参考日期;新增 `employee-management-age.test.mjs` 覆盖未过生日和已过生日两个边界。
|
||||||
|
- 操作:在 `local-x-financial-linux` 容器内运行员工年龄、员工历史和规则权限定向测试,并执行 Vite 生产构建与 `git diff --check`。
|
||||||
|
- 验证:以 2026-07-18 为参考日期,30 岁且生日为 12 月 31 日时正确生成 `1995-12-31`,回算年龄仍为 30;定向前端测试 9 项全部通过,前端全量测试 `820/820` 通过,Vite 生产构建成功,差异格式检查通过。
|
||||||
|
- 影响:员工管理中直接修改年龄后,保存与重新加载不再把员工年龄静默减一岁。
|
||||||
|
- 16:03:补齐 0 岁与闰日出生日期边界。
|
||||||
|
- Git 提交检查:沿用本轮 15:54 拉取检查;upstream `origin/main` 无新提交,本地 ahead 19 条。
|
||||||
|
- 修改:出生年按参考日中生日是否已过推导;0 岁且生日未到时使用上一出生年度,避免生成未来日期;2 月 29 日跨非闰年统一按 2 月 28 日计算和保存。
|
||||||
|
- 验证:参考日 2026-07-18、0 岁、保留 12 月 31 日时生成 `2025-12-31` 并回算 0 岁;闰日跨到非闰年生成合法 `2001-02-28` 并回算 23 岁;定向测试 22 项、前端全量测试 `823/823`、生产构建均通过。
|
||||||
|
- 影响:新生儿档案和闰日生日员工的编辑结果保持合法、可保存且年龄一致。
|
||||||
@@ -303,21 +303,34 @@ export function resolveOrganizationOptions(metaOrganizations) {
|
|||||||
.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'))
|
.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateAgeFromDate(dateString) {
|
export function calculateAgeFromDate(dateString, referenceDate = new Date()) {
|
||||||
if (!dateString) {
|
if (!isValidIsoDate(dateString) || !isValidReferenceDate(referenceDate)) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const birthDate = new Date(`${dateString}T00:00:00`)
|
const [birthYearText, birthMonthText, birthDayText] = dateString.split('-')
|
||||||
if (Number.isNaN(birthDate.getTime())) {
|
const birthYear = Number.parseInt(birthYearText, 10)
|
||||||
|
const birthMonth = Number.parseInt(birthMonthText, 10)
|
||||||
|
const birthDay = Number.parseInt(birthDayText, 10)
|
||||||
|
const referenceYear = referenceDate.getFullYear()
|
||||||
|
const referenceMonth = referenceDate.getMonth() + 1
|
||||||
|
const referenceDay = referenceDate.getDate()
|
||||||
|
const referenceDateNumber = referenceYear * 10000 + referenceMonth * 100 + referenceDay
|
||||||
|
const birthDateNumber = birthYear * 10000 + birthMonth * 100 + birthDay
|
||||||
|
|
||||||
|
if (birthDateNumber > referenceDateNumber) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const today = new Date()
|
const [birthdayMonth, birthdayDay] = normalizeBirthdayForYear(
|
||||||
let age = today.getFullYear() - birthDate.getFullYear()
|
birthMonth,
|
||||||
|
birthDay,
|
||||||
|
referenceYear
|
||||||
|
)
|
||||||
|
let age = referenceYear - birthYear
|
||||||
const hasBirthdayPassed =
|
const hasBirthdayPassed =
|
||||||
today.getMonth() > birthDate.getMonth() ||
|
referenceMonth > birthdayMonth ||
|
||||||
(today.getMonth() === birthDate.getMonth() && today.getDate() >= birthDate.getDate())
|
(referenceMonth === birthdayMonth && referenceDay >= birthdayDay)
|
||||||
|
|
||||||
if (!hasBirthdayPassed) {
|
if (!hasBirthdayPassed) {
|
||||||
age -= 1
|
age -= 1
|
||||||
@@ -326,31 +339,56 @@ export function calculateAgeFromDate(dateString) {
|
|||||||
return age >= 0 ? String(age) : ''
|
return age >= 0 ? String(age) : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateBirthDateFromAge(ageValue, existingBirthDate = '') {
|
export function calculateBirthDateFromAge(
|
||||||
|
ageValue,
|
||||||
|
existingBirthDate = '',
|
||||||
|
referenceDate = new Date()
|
||||||
|
) {
|
||||||
const age = Number.parseInt(String(ageValue ?? '').trim(), 10)
|
const age = Number.parseInt(String(ageValue ?? '').trim(), 10)
|
||||||
if (Number.isNaN(age) || age < 0 || age > 120) {
|
if (Number.isNaN(age) || age < 0 || age > 120) {
|
||||||
return existingBirthDate || ''
|
return existingBirthDate || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const today = new Date()
|
if (!isValidReferenceDate(referenceDate)) {
|
||||||
let month = '01'
|
return existingBirthDate || ''
|
||||||
let day = '01'
|
}
|
||||||
|
|
||||||
|
let month = 1
|
||||||
|
let day = 1
|
||||||
|
|
||||||
if (existingBirthDate && isValidIsoDate(existingBirthDate)) {
|
if (existingBirthDate && isValidIsoDate(existingBirthDate)) {
|
||||||
const [, monthText, dayText] = existingBirthDate.split('-')
|
const [, monthText, dayText] = existingBirthDate.split('-')
|
||||||
month = monthText
|
month = Number.parseInt(monthText, 10)
|
||||||
day = dayText
|
day = Number.parseInt(dayText, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
let birthYear = today.getFullYear() - age
|
const referenceYear = referenceDate.getFullYear()
|
||||||
let candidate = `${birthYear}-${month}-${day}`
|
const referenceMonth = referenceDate.getMonth() + 1
|
||||||
|
const referenceDay = referenceDate.getDate()
|
||||||
|
const [birthdayMonth, birthdayDay] = normalizeBirthdayForYear(month, day, referenceYear)
|
||||||
|
const hasBirthdayPassed =
|
||||||
|
referenceMonth > birthdayMonth ||
|
||||||
|
(referenceMonth === birthdayMonth && referenceDay >= birthdayDay)
|
||||||
|
const birthYear = referenceYear - age - (hasBirthdayPassed ? 0 : 1)
|
||||||
|
const [birthMonth, birthDay] = normalizeBirthdayForYear(month, day, birthYear)
|
||||||
|
|
||||||
if (Number(calculateAgeFromDate(candidate)) > age) {
|
return `${birthYear}-${padDatePart(birthMonth)}-${padDatePart(birthDay)}`
|
||||||
birthYear -= 1
|
}
|
||||||
candidate = `${birthYear}-${month}-${day}`
|
|
||||||
|
function isLeapYear(year) {
|
||||||
|
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidReferenceDate(value) {
|
||||||
|
return typeof value?.getTime === 'function' && !Number.isNaN(value.getTime())
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBirthdayForYear(month, day, year) {
|
||||||
|
if (month === 2 && day === 29 && !isLeapYear(year)) {
|
||||||
|
return [2, 28]
|
||||||
}
|
}
|
||||||
|
|
||||||
return candidate
|
return [month, day]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function matchKeyword(employee, keyword) {
|
export function matchKeyword(employee, keyword) {
|
||||||
|
|||||||
44
web/tests/employee-management-age.test.mjs
Normal file
44
web/tests/employee-management-age.test.mjs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
import {
|
||||||
|
calculateAgeFromDate,
|
||||||
|
calculateBirthDateFromAge,
|
||||||
|
isValidIsoDate
|
||||||
|
} from '../src/views/scripts/employeeManagementModel.js'
|
||||||
|
|
||||||
|
test('根据年龄反算未到生日的出生年份不会少一年', () => {
|
||||||
|
const referenceDate = new Date('2026-07-18T00:00:00')
|
||||||
|
const birthDate = calculateBirthDateFromAge('30', '1990-12-31', referenceDate)
|
||||||
|
|
||||||
|
assert.equal(birthDate, '1995-12-31')
|
||||||
|
assert.equal(calculateAgeFromDate(birthDate, referenceDate), '30')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('根据年龄反算已过生日的出生年份保持当年差值', () => {
|
||||||
|
const referenceDate = new Date('2026-07-18T00:00:00')
|
||||||
|
const birthDate = calculateBirthDateFromAge('30', '1990-01-01', referenceDate)
|
||||||
|
|
||||||
|
assert.equal(birthDate, '1996-01-01')
|
||||||
|
assert.equal(calculateAgeFromDate(birthDate, referenceDate), '30')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('零岁且生日月日尚未到来时不会生成未来出生日期', () => {
|
||||||
|
const referenceDate = new Date('2026-07-18T00:00:00')
|
||||||
|
const birthDate = calculateBirthDateFromAge('0', '2000-12-31', referenceDate)
|
||||||
|
|
||||||
|
assert.equal(birthDate, '2025-12-31')
|
||||||
|
assert.equal(isValidIsoDate(birthDate), true)
|
||||||
|
assert.equal(new Date(`${birthDate}T00:00:00`) <= referenceDate, true)
|
||||||
|
assert.equal(calculateAgeFromDate(birthDate, referenceDate), '0')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('闰日生日跨非闰年时使用二月二十八日并保持年龄回算一致', () => {
|
||||||
|
const referenceDate = new Date('2024-03-01T00:00:00')
|
||||||
|
const birthDate = calculateBirthDateFromAge('23', '2000-02-29', referenceDate)
|
||||||
|
|
||||||
|
assert.equal(birthDate, '2001-02-28')
|
||||||
|
assert.equal(isValidIsoDate(birthDate), true)
|
||||||
|
assert.equal(calculateAgeFromDate(birthDate, referenceDate), '23')
|
||||||
|
assert.equal(calculateAgeFromDate('2000-02-29', new Date('2023-02-28T00:00:00')), '23')
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user