基于 Chemex 的资产盘点功能优化

Auth:张老道      Date:2025/12/26      Cat:技术      Word:共599字      Views:19

随着公司 IT 资产数量不断增加,继续使用 Excel 进行资产管理逐渐显得力不从心:数据分散、更新困难、盘点效率低下。 在此背景下,我测试了网上较为常见的 IT 资产管理系统,其中包括被广泛推荐的 GLPI,但在实际使用过程中发现其操作流程相对复杂,学习和维护成本较高,并不完全符合当前公司的使用习惯。

经过对多个系统的对比和测试,最终选择了 Chemex 作为资产管理平台(GitHub 项目地址)。虽然该项目自 2023 年起已停止维护,但整体架构清晰、功能贴近实际需求。 在原有功能基础上,针对实际使用场景进行了多项调整和功能扩展,目前系统已能够较好地支撑日常 IT 资产管理工作。

image-20251226092626147

在实际使用过程中发现,Chemex 原生盘点功能存在一定局限:盘点单创建完成后,只能在网页端手动操作确认盘点,无法配合移动设备进行现场扫码盘点。

为解决该问题,我对二维码资产页面进行了定制化修改,使其能够在盘点项目创建后,通过扫码方式直接完成资产盘点确认,实现移动端实时盘点。

image-20251226092115523

修改三个地方,直接复制粘贴就行

/www/wwwroot/chemex/resources/views/asset_card_device.blade.php

在最后之前插入,其中电话和报修功能,自行删减就行。

<!-- 底部操作栏 -->
<div style="
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    background: #ffffff;
    border-top: 1px solid #ddd;
    z-index: 999;
">
    <div style="
        max-width: 1140px;
        margin: 0 auto;
        padding: 10px 15px;
        display: flex;
        gap: 10px;
    ">
        <!-- 电话 -->
        <a href="tel:13200000000"
           style="
            flex: 1;
            background: #198754;
            color: #fff;
            padding: 12px 0;
            font-size: 16px;
            border-radius: 6px;
            text-decoration: none;
            text-align: center;
           ">
            ☎ 联系IT
        </a>

        <!-- 钉钉流程报修 -->
</a>
 <a href="javascript:void(0)"
   onclick="window.location.href='dingtalk://dingtalkclient/action/openapp?app_id=-4&container_type=work_platform&corpid=ding417bf10d9dfba271acaaa37764f94726&ddtab=true&redirect_type=jump&redirect_url=https%3A%2F%2Faflow.dingtalk.com%2Fdingtalk%2Fmobile%2Fhomepage.htm%3Fbackcontrol%3Dfalse%26corpid%3Dding417bf10d9dfba271acaaa37764f94726%26dd_progress%3Dfalse%26dd_share%3Dfalse%26ddtab%3Dtrue%26showmenu%3Dfalse%23%2Fcustom%3Fpcredirect%3Dself%26processCode%3DPROC-7DE3D1BF-5B09-4F2A-A76F-2C8925C82B9A';"
   style="
            flex: 1;
            background: #0d6efd;
            color: #fff;
            padding: 12px 0;
            font-size: 16px;
            border-radius: 6px;
            text-decoration: none;
            text-align: center;
           ">
            🛠 报修
        </a>

        <!-- 盘点 -->
        <a href="javascript:void(0)" 
           id="inventory-btn"
           onclick="confirmInventory()"
           style="
            flex: 1;
            background: #fd7e14;
            color: #fff;
            padding: 12px 0;
            font-size: 16px;
            border-radius: 6px;
            text-decoration: none;
            text-align: center;
           ">
            ☑︎盘点
        </a>
    </div>
</div>

<script>
// 页面加载完成后检查盘点状态
document.addEventListener("DOMContentLoaded", function() {
    const assetNumber = "{{ $data->asset_number }}";
    const btn = document.querySelector('#inventory-btn');

    fetch(/api/inventory/status/${assetNumber}, {
        method: 'GET',
        headers: {
            'X-Requested-With': 'XMLHttpRequest',
        }
    })
    .then(res => res.json())
    .then(res => {
        if(res.code === 200 && res.data.status === 'completed'){
            if(btn){
                btn.innerText = '已盘点 ✅';
                btn.disabled = true;
                btn.style.opacity = 0.6;
            }
        }
    })
    .catch(() => {
        console.log('无法获取盘点状态');
    });
});

// 原来的 confirmInventory 保持不变
function confirmInventory() {
    if (!confirm('确认该设备已盘点?')) return;

    fetch('/api/inventory/confirm/{{ $data->asset_number }}', {
        method: 'POST',
        headers: {
            'X-Requested-With': 'XMLHttpRequest',
            'X-CSRF-TOKEN': '{{ csrf_token() }}'
        }
    })
    .then(res => res.json())
    .then(res => {
        if(res.code === 200){
            alert('✅ 资产已盘点');
            const btn = document.querySelector('#inventory-btn');
            if(btn){
                btn.innerText = '已盘点 ✅';
                btn.disabled = true;
                btn.style.opacity = 0.6;
            }
        } else {
            alert(res.message || '操作失败');
        }
    })
    .catch(() => alert('网络错误'));
}
</script>

/www/wwwroot/chemex/app/Http/Controllers/AssetController.php

直接替换所有代码即可

<?php

namespace App\Http\Controllers;

use Ace\Uni;
use App\Models\DeviceRecord;
use App\Models\PartRecord;
use App\Models\SoftwareRecord;
use App\Models\CheckTrack;
use Exception;
use Illuminate\Support\Facades\Auth;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class AssetController extends Controller
{
    /**
     * Display a listing of all assets.
     *
     * @return array|JsonResponse
     */
    public function index(): array|JsonResponse
    {
        try {
            $device_records = DeviceRecord::whereNotNull('asset_number')->get()->toArray();
            $part_records = PartRecord::whereNotNull('asset_number')->get()->toArray();
            $software_records = SoftwareRecord::whereNotNull('asset_number')->get()->toArray();

            foreach ($device_records as &$device_record) {
                $device_record['asset_type'] = 'device';
            }
            foreach ($part_records as &$part_record) {
                $part_record['asset_type'] = 'part';
            }
            foreach ($software_records as &$software_record) {
                $software_record['asset_type'] = 'software';
            }

            $records = array_merge($device_records, $part_records, $software_records);

            return Uni::response(200, '查询成功', $records);
        } catch (Exception $exception) {
            return Uni::response(500, $exception->getMessage());
        }
    }

    /**
     * Store a newly created asset.
     */
    public function store(Request $request)
    {
        // TODO: 添加资产逻辑
    }

    /**
     * Display the specified asset.
     *
     * @param string $asset_number
     * @return array|JsonResponse
     */
    public function show(string $asset_number): array|JsonResponse
    {
        [$type, $asset_number] = explode(':', $asset_number);

        switch ($type) {
            case 'part':
                $part = PartRecord::where('asset_number', $asset_number)->first();
                if ($part) {
                    $part->asset_type = 'part';
                    return Uni::response(200, '查询成功', $part);
                }
                break;

            case 'software':
                $software = SoftwareRecord::where('asset_number', $asset_number)->first();
                if ($software) {
                    $software->asset_type = 'software';
                    return Uni::response(200, '查询成功', $software);
                }
                break;

            default:
                $device = DeviceRecord::with([
                    'category', 'vendor', 'admin_user', 'admin_user.department', 'depreciation',
                ])->where('asset_number', $asset_number)->first();

                if ($device) {
                    $device->asset_type = 'device';
                    return Uni::response(200, '查询成功', $device);
                }
        }

        return Uni::response(404, '无法查询到相关信息');
    }

    /**
     * Update the specified asset.
     */
    public function update(Request $request, $id)
    {
        // TODO: 更新资产逻辑
    }

    /**
     * Remove the specified asset.
     */
    public function destroy($id)
    {
        // TODO: 删除资产逻辑
    }

    /**
     * 页面显示资产详情.
     */
    public function assetCardDevice(string $asset_number): View|Factory|Application
    {
        $data = DeviceRecord::where('asset_number', $asset_number)->first();
        if ($data) {
            return view("asset_card_device", ["data" => $data]);
        }
        abort(404);
    }

    /**
     * 确认盘点(将待盘点状态改为已盘点)
     */
    public function confirmInventory(string $asset_number): JsonResponse
    {
        $device = DeviceRecord::where('asset_number', $asset_number)->first();

        if (!$device) {
            return Uni::response(404, '未找到设备');
        }

        $checkTrack = CheckTrack::where('item_id', $device->id)
                        ->where('status', 0) // 0 = 待盘点
                        ->first();

        if (!$checkTrack) {
            return Uni::response(404, '没有找到待盘点任务或已盘点');
        }

        $checkTrack->status = 1; // 已盘点
        $checkTrack->checker = 1;
        $checkTrack->updated_at = now();
        $checkTrack->save();

        return Uni::response(200, '资产已标记为已盘点', [
            'asset_number' => $asset_number,
            'status' => 'completed'
        ]);
    }

    /**
     * 获取资产盘点状态
     */
    public function getInventoryStatus(string $asset_number): JsonResponse
    {
        $device = DeviceRecord::where('asset_number', $asset_number)->first();

        if (!$device) {
            return Uni::response(404, '未找到设备');
        }

        $checkTrack = CheckTrack::where('item_id', $device->id)->first();

        if (!$checkTrack) {
            return Uni::response(404, '未找到盘点记录', ['status' => 'pending']);
        }

        return Uni::response(200, '查询成功', [
            'asset_number' => $asset_number,
            'status' => $checkTrack->status == 0 ? 'pending' : 'completed'
        ]);
    }
}

/www/wwwroot/chemex/routes/api.php

在最后添加

// === 盘点接口 ===
Route::group(['prefix' => 'inventory'], function () {
    // 点击确认盘点
    Route::post('confirm/{asset_number}', [AssetController::class, 'confirmInventory']);

    // 查询资产盘点状态
    Route::get('status/{asset_number}', [AssetController::class, 'getInventoryStatus']);
});

image-20251226093931536

在系统中创建盘点项目后,工作人员可使用手机扫描粘贴在设备上的二维码,直接进入资产页面并完成盘点确认,实现移动端扫码盘点。

《基于 Chemex 的资产盘点功能优化》留言数:1

发表留言