基于 Chemex 的资产盘点功能优化 | 张老道的博客
随着公司 IT 资产数量不断增加,继续使用 Excel 进行资产管理逐渐显得力不从心:数据分散、更新困难、盘点效率低下。
在此背景下,我测试了网上较为常见的 IT 资产管理系统,其中包括被广泛推荐的 GLPI ,但在实际使用过程中发现其操作流程相对复杂,学习和维护成本较高,并不完全符合当前公司的使用习惯。
经过对多个系统的对比和测试,最终选择了 Chemex 作为资产管理平台(GitHub 项目地址 )。虽然该项目自 2023 年起已停止维护,但整体架构清晰、功能贴近实际需求。
在原有功能基础上,针对实际使用场景进行了多项调整和功能扩展,目前系统已能够较好地支撑日常 IT 资产管理工作。
在实际使用过程中发现,Chemex 原生盘点功能存在一定局限:盘点单创建完成后,只能在网页端手动操作确认盘点,无法配合移动设备进行现场扫码盘点。
为解决该问题,我对二维码资产页面进行了定制化修改,使其能够在盘点项目创建后,通过扫码方式直接完成资产盘点确认,实现移动端实时盘点。
修改三个地方,直接复制粘贴就行
/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']);
});
在系统中创建盘点项目后,工作人员可使用手机扫描粘贴在设备上的二维码,直接进入资产页面并完成盘点确认,实现移动端扫码盘点。
Post navigation
← 锐捷多端口映射问题终于得到了解决
创建盘点单,需要关联administrator用户,因为默认插入的是ID=1,或者修改盘点人对应的ID号。