feat: 按天保留系统化待办操作记录 #1069
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 @@
|
||||
<select data-sys-todo-history-action aria-label="按操作筛选">
|
||||
<option value="">全部操作</option>
|
||||
<option value="create">新增</option>
|
||||
<option value="update">修改</option>
|
||||
<option value="update">改动</option>
|
||||
<option value="delete">删除</option>
|
||||
<option value="import">导入</option>
|
||||
</select>
|
||||
<input type="date" data-sys-todo-history-date aria-label="按日期筛选" title="按日期筛选">
|
||||
<input type="search" data-sys-todo-history-keyword placeholder="搜类型、内容、待办事项、变更内容或操作人" aria-label="搜索操作记录">
|
||||
<button type="button" data-sys-todo-history-export>导出记录 CSV</button>
|
||||
<span class="sys-todo-history-meta" data-sys-todo-history-meta></span>
|
||||
<span class="sys-todo-history-meta">从今天开始显示,时间为北京时间,最多显示最近 500 条</span>
|
||||
<span class="sys-todo-history-meta">按北京时间记录并长期保留,可选择日期查看;单次最多显示 500 条</span>
|
||||
</div>
|
||||
<div class="sys-todo-history-scroll">
|
||||
<table class="sys-todo-history-table">
|
||||
@ -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');
|
||||
|
||||
@ -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('<option value="update">改动</option>', 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();
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user