CaiWuXiTong/app/Business/CourierSurchargeAuditLogRepository.php
zhoushengchen f40c102a7b 新增快递附加费操作日志 (#227)
## 实际改动
- 为快递附加费报价规则和附加费类型记录新增、更改、启用、停用日志。
- 日志按公司隔离,记录操作人、时间、对象及安全字段的变更前后值,页面展示最近 50 条。
- 报价/类型写入与日志记录使用同一事务;审计表未迁移时保留只读查看并阻止无日志写入。

## 修改范围
- 新增审计日志 Repository、Model 和迁移。
- 修改报价/类型 Repository、页面控制器、现有 VIP 黑金页面和对应 Feature 测试。
- 不修改路由、权限、菜单、上游数据源或生产配置。

## 协作检查
- 已获取最新 `main` 并检查远程任务分支。
- `origin/feature/limeng-shipping-data-filters` 同样修改 `Pages.php`,但其逻辑为独立的发货数据页面,与本任务方法、路由、Repository、表和视图无实际重叠。

## 验证
- `CourierSurchargePageTest`:9 个测试通过。
- `CourierPayableSummaryPagesTest`:14 个测试通过。
- 合计 23 个测试、336 个断言通过。
- 已验证现有报价可见、未迁移警告及写入禁用状态。

## 风险与授权
- 新增 1 张审计日志表,需要随生产发布执行迁移。
- 用户已明确要求主线部署,授权本次迁移与发布。
- 未提交 `.env`、密码、Token、私钥、真实财务数据、生产日志或构建产物。
- 回滚代码时保留审计表及历史记录,不执行破坏性删表。

## 合并前自审
- 已逐文件审核完整 diff,确认无越界修改、重复实现、敏感内容和未处理冲突。
- 使用 Squash merge 合并到最新 `main`,再按标准脚本部署。

Reviewed-on: http://git.1080p.hk/WuYongJie/CaiWuXiTong/pulls/227
2026-09-21 10:42:13 +08:00

149 lines
5.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Business;
use App\Models\Business\CourierSurchargeAuditLog;
use Illuminate\Database\QueryException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Schema;
/**
* 快递附加费维护审计日志的固定入口。
*
* 日志只保存当前公司、操作人和报价/类型的安全字段快照,不保存密码、
* Token、连接串或原始请求内容。
*/
class CourierSurchargeAuditLogRepository
{
/** @var array<string,string> */
private const FIELD_LABELS = [
'courier' => '快递名称',
'scope' => '快递归属',
'reconciler' => '对账人',
'types' => '附加费类型',
'mode' => '结算方式',
'standard' => '计价标准',
'condition' => '生效条件',
'effective_at' => '生效日期',
'remark' => '备注',
'status' => '状态',
'name' => '类型名称',
];
/** @var array<string,string> */
private const ACTION_LABELS = [
CourierSurchargeAuditLog::ACTION_CREATED => '新增',
CourierSurchargeAuditLog::ACTION_UPDATED => '更改',
CourierSurchargeAuditLog::ACTION_ACTIVATED => '启用',
CourierSurchargeAuditLog::ACTION_DEACTIVATED => '停用',
];
/** @var array<string,string> */
private const TARGET_LABELS = [
CourierSurchargeAuditLog::TARGET_QUOTE => '报价规则',
CourierSurchargeAuditLog::TARGET_TYPE => '附加费类型',
];
public function ready(): bool
{
try {
return Schema::hasTable((new CourierSurchargeAuditLog())->getTable());
} catch (QueryException) {
return false;
}
}
/** @param array<string,mixed> $changes */
public function record(
string $action,
string $targetType,
?int $targetId,
string $targetLabel,
array $changes,
int $actorId,
): CourierSurchargeAuditLog {
abort_unless($this->ready(), 503, '附加费操作日志数据表尚未完成迁移。');
abort_unless(array_key_exists($action, self::ACTION_LABELS), 422, '附加费日志动作不合法。');
abort_unless(array_key_exists($targetType, self::TARGET_LABELS), 422, '附加费日志对象不合法。');
return CourierSurchargeAuditLog::query()->create([
'company_id' => company_id(),
'actor_id' => $actorId ?: null,
'action' => $action,
'target_type' => $targetType,
'target_id' => $targetId,
'target_label' => mb_substr(trim($targetLabel), 0, 191),
'changes' => $changes,
]);
}
/**
* @return Collection<int,array<string,mixed>>
*/
public function recent(int $limit = 50): Collection
{
if (! $this->ready()) {
return collect();
}
try {
return CourierSurchargeAuditLog::query()
->with('actor')
->orderByDesc('id')
->limit(max(1, min($limit, 200)))
->get()
->map(fn (CourierSurchargeAuditLog $log): array => [
'id' => $log->id,
'occurred_at' => optional($log->created_at)->format('Y-m-d H:i:s') ?: '',
'actor_name' => $log->actor?->name ?: '系统',
'action' => $log->action,
'action_label' => self::ACTION_LABELS[$log->action] ?? $log->action,
'target_type_label' => self::TARGET_LABELS[$log->target_type] ?? $log->target_type,
'target_label' => $log->target_label,
'changes_label' => $this->changesLabel($log),
])
->values();
} catch (QueryException) {
return collect();
}
}
private function changesLabel(CourierSurchargeAuditLog $log): string
{
$changes = (array) $log->changes;
if ($log->action === CourierSurchargeAuditLog::ACTION_CREATED) {
return '新增记录';
}
if (in_array($log->action, [CourierSurchargeAuditLog::ACTION_ACTIVATED, CourierSurchargeAuditLog::ACTION_DEACTIVATED], true)) {
$before = data_get($changes, 'before.status');
$after = data_get($changes, 'after.status');
return sprintf('状态:%s → %s', $before ?: '—', $after ?: '—');
}
$before = (array) ($changes['before'] ?? []);
$after = (array) ($changes['after'] ?? []);
$parts = [];
foreach (array_unique(array_merge(array_keys($before), array_keys($after))) as $field) {
$old = $this->valueLabel($before[$field] ?? null);
$new = $this->valueLabel($after[$field] ?? null);
if ($old !== $new) {
$parts[] = sprintf('%s:%s → %s', self::FIELD_LABELS[$field] ?? $field, $old, $new);
}
}
return $parts === [] ? '未改变字段' : implode(';', $parts);
}
private function valueLabel(mixed $value): string
{
if (is_array($value)) {
return implode('、', array_map(static fn (mixed $item): string => (string) $item, $value));
}
$value = trim((string) $value);
return $value === '' ? '—' : $value;
}
}