diff --git a/app/Business/SystemizationTodoAuditLogger.php b/app/Business/SystemizationTodoAuditLogger.php
index 87c612d0..d91de3cd 100644
--- a/app/Business/SystemizationTodoAuditLogger.php
+++ b/app/Business/SystemizationTodoAuditLogger.php
@@ -17,7 +17,7 @@ class SystemizationTodoAuditLogger
public const ACTION_LABELS = [
'create' => '新增',
- 'update' => '修改',
+ 'update' => '改动',
'delete' => '删除',
'import' => '导入',
];
@@ -149,18 +149,6 @@ class SystemizationTodoAuditLogger
}
DB::table(self::TABLE)->insert($rows);
- $companyIds = array_values(array_unique(array_column($rows, 'company_id')));
- foreach ($companyIds as $companyId) {
- $maxId = (int) DB::table(self::TABLE)
- ->where('company_id', $companyId)
- ->max('id');
- if ($maxId > self::HISTORY_LIMIT) {
- DB::table(self::TABLE)
- ->where('company_id', $companyId)
- ->where('id', '<=', $maxId - self::HISTORY_LIMIT)
- ->delete();
- }
- }
}
/**
@@ -170,6 +158,7 @@ class SystemizationTodoAuditLogger
int $companyId,
int $limit = self::HISTORY_LIMIT,
?string $from = null,
+ ?string $date = null,
): array
{
if (! $this->tableReady()) {
@@ -179,6 +168,10 @@ class SystemizationTodoAuditLogger
return DB::table(self::TABLE)
->where('company_id', $companyId)
->when($from !== null && $from !== '', fn ($query) => $query->where('at', '>=', $from))
+ ->when($date !== null && $date !== '', function ($query) use ($date): void {
+ $query->where('at', '>=', $date . ' 00:00:00')
+ ->where('at', '<', Carbon::parse($date)->addDay()->toDateString() . ' 00:00:00');
+ })
->orderByDesc('id')
->limit(max(1, min($limit, self::HISTORY_LIMIT)))
->get()
diff --git a/app/Http/Controllers/Business/SystemizationTodos.php b/app/Http/Controllers/Business/SystemizationTodos.php
index c6f5f3aa..e571b7fb 100644
--- a/app/Http/Controllers/Business/SystemizationTodos.php
+++ b/app/Http/Controllers/Business/SystemizationTodos.php
@@ -1509,20 +1509,21 @@ class SystemizationTodos extends Controller
]);
}
- public function history(): JsonResponse
+ public function history(Request $request): JsonResponse
{
- $from = $this->historyStartAt();
+ $date = $this->historyDate($request);
$events = $this->audit->history(
(int) company_id(),
SystemizationTodoAuditLogger::HISTORY_LIMIT,
- $from,
+ null,
+ $date,
);
return response()->json([
'events' => $events,
'total' => count($events),
'limit' => SystemizationTodoAuditLogger::HISTORY_LIMIT,
- 'from' => $from,
+ 'date' => $date,
]);
}
@@ -1569,12 +1570,13 @@ class SystemizationTodos extends Controller
{
$action = trim((string) $request->query('action', ''));
$keyword = trim((string) $request->query('keyword', ''));
- $from = $this->historyStartAt();
+ $date = $this->historyDate($request);
$rows = array_values(array_filter(
$this->audit->history(
(int) company_id(),
SystemizationTodoAuditLogger::HISTORY_LIMIT,
- $from,
+ null,
+ $date,
),
fn (array $row): bool => $this->historyMatches($row, $action, $keyword),
));
@@ -1601,9 +1603,13 @@ class SystemizationTodos extends Controller
]);
}
- private function historyStartAt(): string
+ private function historyDate(Request $request): string
{
- return $this->audit->platformNow()->startOfDay()->toDateTimeString();
+ $data = $request->validate([
+ 'date' => ['nullable', 'date_format:Y-m-d'],
+ ]);
+
+ return trim((string) ($data['date'] ?? ''));
}
private function historyMatches(array $row, string $action, string $keyword): bool
diff --git a/resources/views/business/pages/systemization-todos.blade.php b/resources/views/business/pages/systemization-todos.blade.php
index e76eb571..7e641a84 100644
--- a/resources/views/business/pages/systemization-todos.blade.php
+++ b/resources/views/business/pages/systemization-todos.blade.php
@@ -295,6 +295,7 @@
.sys-todo-history-tools { display:flex; flex-wrap:wrap; align-items:center; gap:.5rem; margin-bottom:.75rem; }
.sys-todo-history-tools select, .sys-todo-history-tools input { min-height:2.1rem; padding:0 .6rem; border:1px solid var(--sys-border); border-radius:.45rem; background:#fff; color:#171717; font:inherit; font-size:.78rem; }
.sys-todo-history-tools input { flex:1 1 16rem; min-width:12rem; }
+ .sys-todo-history-tools input[type="date"] { flex:0 0 9.8rem; min-width:9.8rem; }
.sys-todo-history-tools button { min-height:2.1rem; padding:0 .75rem; border:1px solid var(--sys-border); border-radius:.45rem; background:#fff; color:#171717; font:inherit; font-size:.78rem; cursor:pointer; }
.sys-todo-history-tools button:hover { border-color:var(--sys-gold); background:var(--sys-gold-soft); }
.sys-todo-history-meta { color:#666; font-size:.72rem; }
@@ -311,6 +312,7 @@
.sys-todo-history-tag--create,.sys-todo-history-tag--import { border-color:#bad0ac; background:#f2f8ed; color:#53793e; }
.sys-todo-history-tag--update { border-color:#dbbf7a; background:#fff7df; color:#935f07; }
.sys-todo-history-tag--delete { border-color:#e7c3c3; background:#fff3f3; color:#b80000; }
+ .sys-todo-history-table__day td { padding:.46rem .65rem; border-bottom:1px solid #d8cbb7; background:#f8f2e7; color:#594a35; font-weight:600; }
.sys-todo-history-item-title { margin-top:.2rem; color:#595959; font-size:.72rem; }
.sys-todo-history-changes { margin:0; padding-left:1rem; }
.sys-todo-history-changes li + li { margin-top:.22rem; }
@@ -1091,14 +1093,15 @@
+
- 从今天开始显示,时间为北京时间,最多显示最近 500 条
+ 按北京时间记录并长期保留,可选择日期查看;单次最多显示 500 条
@@ -1421,6 +1424,7 @@
var groupNote = groupDialog.querySelector('[data-sys-todo-group-note]');
var historyDialog = document.getElementById('sysTodoHistoryDialog');
var historyAction = historyDialog.querySelector('[data-sys-todo-history-action]');
+ var historyDate = historyDialog.querySelector('[data-sys-todo-history-date]');
var historyKeyword = historyDialog.querySelector('[data-sys-todo-history-keyword]');
var historyExport = historyDialog.querySelector('[data-sys-todo-history-export]');
var historyMeta = historyDialog.querySelector('[data-sys-todo-history-meta]');
@@ -2374,7 +2378,24 @@
function renderHistory() {
var events = historyFilteredEvents();
historyBody.replaceChildren();
+ var dayCounts = {};
events.forEach(function (event) {
+ var day = String(event.at || '').slice(0, 10);
+ dayCounts[day] = (dayCounts[day] || 0) + 1;
+ });
+ var renderedDay = '';
+ events.forEach(function (event) {
+ var day = String(event.at || '').slice(0, 10);
+ if (day !== renderedDay) {
+ renderedDay = day;
+ var dayRow = document.createElement('tr');
+ dayRow.className = 'sys-todo-history-table__day';
+ var dayCell = document.createElement('td');
+ dayCell.colSpan = 5;
+ dayCell.textContent = (day || '日期未知') + ' · ' + dayCounts[day] + ' 条';
+ dayRow.appendChild(dayCell);
+ historyBody.appendChild(dayRow);
+ }
var row = document.createElement('tr');
var at = document.createElement('td');
@@ -2415,10 +2436,11 @@
historyEmpty.hidden = events.length > 0;
historyEmpty.textContent = historyEvents.length
? '没有符合条件的操作记录。'
- : '暂无操作记录;从本次功能上线后的第一次新增、修改、删除或导入开始记录。';
+ : '暂无操作记录;从本次功能上线后的第一次新增、改动、删除或导入开始记录。';
historyMeta.textContent = '显示 ' + events.length + ' / ' + historyEvents.length + ' 条';
var params = new URLSearchParams();
if (historyAction.value) params.set('action', historyAction.value);
+ if (historyDate.value) params.set('date', historyDate.value);
if (historyKeyword.value.trim()) params.set('keyword', historyKeyword.value.trim());
historyExport.setAttribute(
'data-export-url',
@@ -2428,7 +2450,9 @@
function loadHistory() {
historyMeta.textContent = '正在读取…';
- fetch(routes.history, {
+ var params = new URLSearchParams();
+ if (historyDate.value) params.set('date', historyDate.value);
+ fetch(routes.history + (params.toString() ? '?' + params.toString() : ''), {
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
}).then(function (response) {
return response.json().catch(function () { return {}; }).then(function (data) {
@@ -2580,6 +2604,7 @@
if (event.target === historyDialog) closeHistoryDialog();
});
historyAction.addEventListener('change', renderHistory);
+ historyDate.addEventListener('change', loadHistory);
historyKeyword.addEventListener('input', renderHistory);
historyExport.addEventListener('click', function () {
var url = historyExport.getAttribute('data-export-url');
diff --git a/tests/Feature/Business/SystemizationTodosTest.php b/tests/Feature/Business/SystemizationTodosTest.php
index bf968074..ccaee28b 100644
--- a/tests/Feature/Business/SystemizationTodosTest.php
+++ b/tests/Feature/Business/SystemizationTodosTest.php
@@ -47,6 +47,9 @@ class SystemizationTodosTest extends FeatureTestCase
->assertSee('data-sys-todo-history-open', false)
->assertSee('id="sysTodoHistoryDialog"', false)
->assertSee('data-sys-todo-history-export', false)
+ ->assertSee('data-sys-todo-history-date', false)
+ ->assertSee('', false)
+ ->assertSeeText('按北京时间记录并长期保留')
->assertSee('data-sys-todo-inline-add', false)
->assertSee('data-sys-todo-col-resizer', false)
->assertSee('data-date-picker', false)
@@ -2051,7 +2054,7 @@ class SystemizationTodosTest extends FeatureTestCase
route('business.systemization-todos.history.export') . '?action=update'
)->streamedContent();
$this->assertStringContainsString('时间,操作,类型,内容,待办事项,变更内容,操作人', $csv);
- $this->assertStringContainsString('修改,收入,操作记录测试,修改后的待办', $csv);
+ $this->assertStringContainsString('改动,收入,操作记录测试,修改后的待办', $csv);
$this->assertStringNotContainsString('删除前:', $csv);
$this->assertStringNotContainsString('新增:', $csv);
}
@@ -2115,7 +2118,7 @@ class SystemizationTodosTest extends FeatureTestCase
$this->assertSoftDeleted('business_systemization_todos', ['id' => $todo->id]);
}
- public function test_operation_history_starts_from_today(): void
+ public function test_operation_history_keeps_and_filters_records_across_days(): void
{
$this->loginAs();
@@ -2131,28 +2134,44 @@ class SystemizationTodosTest extends FeatureTestCase
])->assertRedirect();
$this->travelTo(Carbon::parse('2026-10-28 09:00:00', 'Asia/Shanghai'));
- $this->post(route('business.systemization-todos.store'), [
+ $todo = SystemizationTodo::query()->firstOrFail();
+ $this->patch(route('business.systemization-todos.update', ['todo' => $todo->id]), [
'category' => '收入',
'section' => '操作记录日期',
- 'title' => '今天记录',
+ 'title' => '今天改动记录',
'owner' => '晨曦',
'assignee' => '栎阳',
+ 'start_date' => '2026-10-27',
+ 'due_date' => '2026-10-29',
'status' => SystemizationTodo::STATUS_INCOMPLETE,
])->assertRedirect();
$history = $this->getJson(route('business.systemization-todos.history'))
->assertOk()
- ->assertJsonPath('from', '2026-10-28 00:00:00')
+ ->assertJsonPath('date', '')
->json();
$titles = array_column($history['events'], 'title');
- $this->assertContains('今天记录', $titles);
- $this->assertNotContains('昨天记录', $titles);
+ $this->assertContains('今天改动记录', $titles);
+ $this->assertContains('昨天记录', $titles);
+ $this->assertSame(['改动', '新增'], array_column($history['events'], 'action_label'));
+
+ $yesterday = $this->getJson(route('business.systemization-todos.history') . '?date=2026-10-27')
+ ->assertOk()
+ ->assertJsonPath('date', '2026-10-27')
+ ->json();
+ $this->assertSame(['create'], array_column($yesterday['events'], 'action'));
+ $this->assertSame(['昨天记录'], array_column($yesterday['events'], 'title'));
+
+ $this->withExceptionHandling()
+ ->getJson(route('business.systemization-todos.history') . '?date=2026-02-30')
+ ->assertUnprocessable();
$csv = $this->get(route('business.systemization-todos.history.export'))
->streamedContent();
- $this->assertStringContainsString('今天记录', $csv);
- $this->assertStringNotContainsString('昨天记录', $csv);
+ $this->assertStringContainsString('昨天记录', $csv);
+ $this->assertStringContainsString('今天改动记录', $csv);
+ $this->assertStringContainsString('改动', $csv);
} finally {
$this->travelBack();
}