## 实际改动 - 为快递附加费报价规则和附加费类型记录新增、更改、启用、停用日志。 - 日志按公司隔离,记录操作人、时间、对象及安全字段的变更前后值,页面展示最近 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
149 lines
5.2 KiB
PHP
149 lines
5.2 KiB
PHP
<?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;
|
||
}
|
||
}
|