完善生产异常维修与备件管理功能
This commit is contained in:
@@ -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个字符',
|
||||
|
||||
Reference in New Issue
Block a user