完善生产异常维修与备件管理功能

This commit is contained in:
2026-09-01 19:17:09 +08:00
parent 02a429af66
commit 32f1486f5e
49 changed files with 1765 additions and 121 deletions
@@ -49,7 +49,8 @@ class RepairRecordController extends Crud
];
$order = $order ?: 'repaired_at DESC, id DESC';
$total = $this->service->getCount($where);
$list = $this->service->selectList($where, $field, $page, $limit, $order, [], false);
// 与总数查询保持一致,启用关键词搜索字段。
$list = $this->service->selectList($where, $field, $page, $limit, $order, [], true);
return call_user_func([$this, $methods[$format] ?? 'formatNormal'], $list, $total);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
@@ -40,6 +40,7 @@ final class DataEntryController extends Base
{
protected ?BaseValidate $validate = null;
protected ?BaseValidate $productRecordValidate = null;
private ?array $repairFlowCache = null;
public function __construct(
protected CurrentMember $currentMember,
@@ -124,6 +125,61 @@ final class DataEntryController extends Base
]);
}
#[OA\Get(path: '/produce/material/options', summary: '获取维修备件选项', tags: ['produce-web'])]
#[DataResponse(description: '获取成功')]
#[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')]
public function materialOptions(Request $request): Response
{
$productCode = trim((string) $request->input('product_code', ''));
$productName = trim((string) $request->input('product_name', ''));
$categoryId = (int) $request->input('category_id', 0);
$page = max(1, (int) $request->input('page', 1));
$limit = min(50, max(1, (int) $request->input('limit', 10)));
$query = Material::query()->with('category:id,category_name');
$query->when($productCode !== '', static fn ($builder) => $builder->where('product_code', 'like', "%{$productCode}%"));
$query->when($productName !== '', static fn ($builder) => $builder->where('product_name', 'like', "%{$productName}%"));
$query->when($categoryId > 0, static fn ($builder) => $builder->where('category_id', $categoryId));
$usageCounts = $this->materialUsageCounts();
$materials = $query->get(['id', 'category_id', 'product_code', 'product_name'])
->sort(function (Material $left, Material $right) use ($usageCounts): int {
$leftCount = $usageCounts[(string) $left->product_code] ?? 0;
$rightCount = $usageCounts[(string) $right->product_code] ?? 0;
return $leftCount === $rightCount
? strcmp((string) $left->product_code, (string) $right->product_code)
: $rightCount <=> $leftCount;
});
$total = $materials->count();
$items = $materials->forPage($page, $limit)
->map(static fn (Material $material): array => [
'id' => (string) $material->id,
'category_id' => (string) $material->category_id,
'category_name' => (string) $material->category_name,
'product_code' => (string) $material->product_code,
'product_name' => (string) $material->product_name,
])
->values()
->all();
return Json::success('获取成功', compact('items', 'total', 'page', 'limit'));
}
private function materialUsageCounts(): array
{
$counts = [];
RepairRecord::query()->without('exception')->whereNotNull('parts')->get(['parts'])->each(function (RepairRecord $record) use (&$counts): void {
foreach ((array) $record->parts as $part) {
$code = trim((string) ($part['product_code'] ?? ''));
if ($code === '') {
continue;
}
$counts[$code] = ($counts[$code] ?? 0) + max(1, (int) ($part['quantity'] ?? 1));
}
});
return $counts;
}
#[OA\Get(path: '/produce/scanapp/material-category', summary: '获取扫码端物料分类选项', tags: ['produce-web'])]
#[DataResponse(description: '获取成功')]
#[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')]
@@ -644,7 +700,7 @@ final class DataEntryController extends Base
$query = match ($recordType) {
'scan' => ScanData::query()->with(['process', 'step', 'status']),
'product' => ProductRecord::query()->with(['process', 'step', 'status']),
'exception' => ExceptionRecord::query(),
'exception' => ExceptionRecord::query()->with(['process', 'step']),
'repair' => RepairRecord::query()->with('exception'),
};
@@ -731,7 +787,7 @@ final class DataEntryController extends Base
ProductRecord::query()->with(['process', 'step', 'status'])->where('process_order_no', $orderNo)->get()->each(function (ProductRecord $item) use ($timeline): void {
$timeline->push($this->formatTimelineRecord('product', $item, (int) $item->created_at));
});
ExceptionRecord::query()->where('order_no', $orderNo)->get()->each(function (ExceptionRecord $item) use ($timeline): void {
ExceptionRecord::query()->with(['process', 'step'])->where('order_no', $orderNo)->get()->each(function (ExceptionRecord $item) use ($timeline): void {
$timeline->push($this->formatTimelineRecord('exception', $item, (int) $item->reported_at));
});
RepairRecord::query()->with('exception')->where('order_no', $orderNo)->get()->each(function (RepairRecord $item) use ($timeline): void {
@@ -1063,6 +1119,22 @@ final class DataEntryController extends Base
return Json::fail('用户编码不存在');
}
$config = $this->getConfigGroupData('basic_setting');
$processId = (int) ($config['exception_process_id'] ?? 0);
$stepId = (int) ($config['exception_step_id'] ?? 0);
if ($processId <= 0 || $stepId <= 0) {
return Json::fail('异常提报工序步骤尚未配置,请联系管理员');
}
$step = ProcessStep::query()
->whereKey($stepId)
->where('process_id', $processId)
->where('status', 1)
->first();
if (!$step) {
return Json::fail('异常提报工序步骤配置无效,请联系管理员');
}
$type = ExceptionSetting::query()
->whereKey($typeId)
->where('setting_type', 'type')
@@ -1099,24 +1171,26 @@ final class DataEntryController extends Base
return is_string($image) && preg_match('/^https?:\/\//', $image) === 1;
}));
$record = ExceptionRecord::query()->create([
'order_no' => $orderNo,
'user_code' => $userCode,
'reporter' => (string) ($member->nickname ?: $member->username),
'reporter_id' => (int) $member->id,
'type_id' => $type->id,
'type_code' => $type->code,
'type_name' => $type->name,
'reason_id' => $reason->id,
'reason_code' => $reason->code,
'reason_name' => $reason->name,
'issue_id' => $issue?->id,
'issue_code' => $issue?->code,
'issue_name' => $issue?->name,
'images' => $images,
'remark' => trim((string) ($data['remark'] ?? '')),
'status' => 0,
'reported_at' => time(),
]);
'order_no' => $orderNo,
'process_id' => $processId,
'step_id' => $stepId,
'user_code' => $userCode,
'reporter' => (string) ($member->nickname ?: $member->username),
'reporter_id' => (int) $member->id,
'type_id' => $type->id,
'type_code' => $type->code,
'type_name' => $type->name,
'reason_id' => $reason->id,
'reason_code' => $reason->code,
'reason_name' => $reason->name,
'issue_id' => $issue?->id,
'issue_code' => $issue?->code,
'issue_name' => $issue?->name,
'images' => $images,
'remark' => trim((string) ($data['remark'] ?? '')),
'status' => 0,
'reported_at' => time(),
]);
return Json::success('提交成功', ['id' => $record->id]);
}
@@ -1223,13 +1297,13 @@ final class DataEntryController extends Base
'record_type_name' => '异常提报',
'order_no' => (string) $item->order_no,
'product_code' => '',
'process_name' => '',
'step_name' => '',
'process_name' => (string) $item->process_name,
'step_name' => (string) $item->step_name,
'status_name' => (string) $item->status_name,
'result_name' => (string) $item->type_name,
'operator' => (string) $item->reporter,
'user_code' => (string) $item->user_code,
'quantity' => null,
'quantity' => 1,
'exception_reason' => (string) $item->reason_name,
'exception_content' => (string) $item->issue_name,
'event_at' => (int) $item->reported_at,
@@ -1242,13 +1316,13 @@ final class DataEntryController extends Base
'record_type_name' => '维修记录',
'order_no' => (string) $item->order_no,
'product_code' => (string) $item->product_code,
'process_name' => '',
'step_name' => '',
'process_name' => $this->getRepairFlowNodeNames()['process_name'],
'step_name' => $this->getRepairFlowNodeNames()['step_name'],
'status_name' => '已维修',
'result_name' => (string) $item->repair_result_name,
'operator' => (string) $item->operator,
'user_code' => '',
'quantity' => null,
'user_code' => (string) ($item->exception?->user_code ?? ''),
'quantity' => 1,
'material_category' => (string) ($item->category_name ?: $item->category_names),
'fault_description' => (string) ($item->fault_description_name ?: $item->fault_description),
'event_at' => (int) ($item->repaired_at ?: $item->created_at),
@@ -1258,6 +1332,28 @@ final class DataEntryController extends Base
};
}
/** 获取维修记录配置的工序与步骤名称。 */
private function getRepairFlowNodeNames(): array
{
if ($this->repairFlowCache !== null) {
return $this->repairFlowCache;
}
$basic = $this->getConfigGroupData('basic_setting');
$flow = $this->getConfigGroupData('repair_flow');
$processId = (int) ($basic['repair_process_id'] ?? 0);
$stepId = (int) ($basic['repair_step_id'] ?? 0);
$processId = $processId > 0 ? $processId : (int) ($flow['repair_process_id'] ?? 0);
$stepId = $stepId > 0 ? $stepId : (int) ($flow['repair_step_id'] ?? 0);
$process = $processId > 0 ? Process::query()->withTrashed()->find($processId) : null;
$step = $stepId > 0 ? ProcessStep::query()->withTrashed()->find($stepId) : null;
return $this->repairFlowCache = [
'process_name' => (string) ($process?->process_name ?? ''),
'step_name' => (string) ($step?->step_name ?? ''),
];
}
private function formatTimelineRecord(string $recordType, object $item, int $eventAt): array
{
$record = $this->formatQueryRecord($recordType, $item);
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace plugin\produce\app\model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ExceptionRecord extends BaseModel
{
protected $table = 'produce_exception_record';
@@ -22,6 +24,8 @@ class ExceptionRecord extends BaseModel
protected $fillable = [
'id',
'order_no',
'process_id',
'step_id',
'user_code',
'reporter',
'reporter_id',
@@ -50,6 +54,8 @@ class ExceptionRecord extends BaseModel
protected $casts = [
'images' => 'array',
'process_id' => 'integer',
'step_id' => 'integer',
'status' => 'integer',
'handled_at' => 'integer',
'reported_at' => 'integer',
@@ -60,6 +66,26 @@ class ExceptionRecord extends BaseModel
'deleted_at' => 'integer',
];
public function process(): BelongsTo
{
return $this->belongsTo(Process::class, 'process_id', 'id')->withTrashed();
}
public function step(): BelongsTo
{
return $this->belongsTo(ProcessStep::class, 'step_id', 'id')->withTrashed();
}
public function getProcessNameAttribute(): string
{
return (string) ($this->process?->process_name ?? '');
}
public function getStepNameAttribute(): string
{
return (string) ($this->step?->step_name ?? '');
}
public function getReportedDateAttribute(): ?string
{
return $this->reported_at ? date('Y-m-d H:i:s', (int) $this->reported_at) : null;
@@ -53,6 +53,11 @@ class RepairRecordService extends BaseService
private function prepareData(array $data): array
{
foreach (['category_ids', 'parts', 'images', 'operation_fault_steps', 'responsible_operators'] as $field) {
if (isset($data[$field]) && is_string($data[$field])) {
$data[$field] = $this->decodeArray($data[$field], $field);
}
}
$hasCategoryIds = array_key_exists('category_ids', $data);
$categoryIds = array_values(array_unique(array_filter(array_map(static fn ($id): string => trim((string) $id), (array) ($data['category_ids'] ?? [])), static fn (string $id): bool => $id !== '')));
if ($hasCategoryIds) {
@@ -100,12 +105,28 @@ class RepairRecordService extends BaseService
$data['category_names'] = [$data['category_name']];
$data['fault_description'] = $fault->description;
}
if (isset($data['parts']) && is_string($data['parts'])) {
$data['parts'] = json_decode($data['parts'], true, 512, JSON_THROW_ON_ERROR);
}
return $data;
}
/**
* 将 JSON 字符串转换为数组,避免前端表单提交字符串导致保存失败。
*/
private function decodeArray(string $value, string $field): ?array
{
if (trim($value) === '') {
return null;
}
try {
$decoded = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
throw new \InvalidArgumentException(sprintf('%s 格式不正确', $field), 0, $e);
}
if (!is_array($decoded)) {
throw new \InvalidArgumentException(sprintf('%s 格式不正确', $field));
}
return $decoded;
}
private function changeFaultUsage(int|string|null $faultId, int $delta): void
{
if (!$faultId) {
@@ -5,30 +5,39 @@ declare(strict_types=1);
namespace plugin\produce\app\validate;
use core\base\BaseValidate;
use Illuminate\Validation\Rule;
use plugin\produce\app\model\RepairRecord;
class RepairRecordValidate extends BaseValidate
{
public function rules(): array
{
$id = request()->route?->param('id');
return [
'repair_no' => ['required', 'max:100'],
'order_no' => ['max:100'],
'product_code' => ['max:100'],
'product_name' => ['max:200'],
'category_id' => ['integer'],
'category_ids' => ['array'],
'repair_no' => ['required', 'max:100', Rule::unique(RepairRecord::class, 'repair_no')->ignore($id, 'id')],
'order_no' => ['nullable', 'max:100'],
'product_code' => ['nullable', 'max:100'],
'product_name' => ['nullable', 'max:200'],
'category_id' => ['nullable', 'integer'],
'category_ids' => ['nullable', 'array'],
'category_ids.*' => ['integer'],
'fault_description_id' => ['integer'],
'repair_result' => ['max:50'],
'operator' => ['max:100'],
'repaired_at' => ['integer'],
'remark' => ['max:1000'],
'fault_description_id' => ['nullable', 'integer'],
'repair_result' => ['nullable', 'max:50'],
'parts' => ['nullable'],
'images' => ['nullable'],
'operation_fault_steps' => ['nullable'],
'responsible_operators' => ['nullable'],
'operator' => ['nullable', 'max:100'],
'repaired_at' => ['nullable', 'integer'],
'remark' => ['nullable', 'max:1000'],
];
}
protected array $message = [
'repair_no.required' => '维修单号不能为空',
'repair_no.max' => '维修单号不能超过100个字符',
'repair_no.unique' => '维修单号已存在',
'order_no.max' => '订单号不能超过100个字符',
'product_code.max' => '商品编码不能超过100个字符',
'product_name.max' => '商品名称不能超过200个字符',