最近在团队内部落地了一个基于Uniapp的固定资产借用租赁管理平台,后端同时支持ThinkPHP和Laravel框架。这个项目从技术选型到最终上线踩了不少坑,今天把完整的技术方案和实战经验整理出来,特别适合需要快速搭建资产管理系统的团队参考。
先说说为什么选择这样的技术组合:Uniapp的跨端能力可以同时覆盖微信小程序、H5和App,而后端选择PHP系两大框架则考虑了团队技术栈的兼容性。实际开发中发现,ThinkPHP确实如传闻中那样容易上手,而Laravel在复杂业务流处理上更游刃有余。下面就从架构设计到代码实现,详细拆解这个项目的技术要点。
在项目启动阶段,我们花了三天时间对两个后端框架进行了POC验证。这里分享实测数据:
ThinkPHP 6.2:
Laravel 9.x:
对于中小型项目,如果追求快速上线,建议选择ThinkPHP。我们有个紧急项目用ThinkPHP三天就完成了基础版本开发。而对于需要长期迭代、业务逻辑复杂的系统,Laravel的扩展性和可维护性优势会越来越明显。
采用标准的RESTful API通信模式,前端Uniapp通过封装统一的请求模块与后端交互。这里特别要强调的是接口安全方案的选择:
php复制// Laravel Sanctum 配置示例
'api' => [
'driver' => 'sanctum',
'provider' => 'users',
'expiration' => 60 * 15, // 15分钟过期
'middleware' => [
'throttle:100,1', // 限流配置
'encrypt_cookies',
],
],
固定资产管理的核心是状态追踪,我们设计了7张主表+4张关联表。重点说几个关键设计:
php复制Schema::create('assets', function (Blueprint $table) {
$table->id();
$table->string('name', 100)->comment('资产名称');
$table->enum('type', ['equipment', 'furniture', 'vehicle'])
->comment('避免使用数字枚举,提高可读性');
$table->decimal('price', 10, 2)->nullable()
->comment('建议保留两位小数');
$table->date('purchase_date')->index()
->comment('必须建立索引,统计报表常用字段');
// 状态字段设计在下节专门讲解
});
资产流转的核心是状态管理,我们对比了三种实现方式:
php复制$table->enum('status', [
'available', // 可借用
'pending', // 审批中
'borrowed', // 已借出
'maintenance', // 维修中
'scrapped' // 已报废
])->default('available');
优点:简单直接 缺点:业务规则分散
php复制class AssetStateContext {
private $state;
public function setState(AssetState $state) {
$this->state = $state;
}
public function handleRequest() {
$this->state->handleRequest($this);
}
}
class AvailableState implements AssetState {
public function handleRequest($context) {
// 处理可用状态下的请求
$context->setState(new PendingState());
}
}
bash复制composer require symfony/workflow
配置示例:
yaml复制# config/packages/workflow.yaml
workflows:
asset:
type: 'state_machine'
initial_marking: available
places:
- available
- pending
- borrowed
- maintenance
- scrapped
transitions:
request:
from: available
to: pending
approve:
from: pending
to: borrowed
reject:
from: pending
to: available
实测发现状态机方案虽然前期配置复杂,但后期维护成本低,特别适合审批流程可能变更的场景。
Uniapp的网络请求需要特殊处理,分享几个实用技巧:
javascript复制// utils/http.js
const http = {
async request(options) {
// 自动添加token
if (!options.header) options.header = {}
options.header['Authorization'] = store.state.token
// 超时设置
options.timeout = options.timeout || 15000
return new Promise((resolve, reject) => {
// 添加请求标记用于取消请求
const requestTask = uni.request({
...options,
success: (res) => {
if (res.statusCode >= 400) {
this._handleError(res)
reject(res)
} else {
resolve(res.data)
}
},
fail: (err) => {
this._handleError(err)
reject(err)
}
})
// 存储请求任务
this._addRequestTask(requestTask)
})
},
_handleError(error) {
// 统一错误处理逻辑
if (error.statusCode === 401) {
uni.navigateTo({ url: '/pages/login' })
}
}
}
javascript复制let pendingRequests = []
http._addRequestTask = (task) => {
pendingRequests.push(task)
}
http.cancelPendingRequests = () => {
pendingRequests.forEach(task => {
task.abort()
})
pendingRequests = []
}
// 在页面onUnload时调用
onUnload() {
http.cancelPendingRequests()
}
vue复制<template>
<image
:src="placeholder"
:data-src="realSrc"
@load="handleLoad"
lazy-load
/>
</template>
<script>
export default {
data() {
return {
loaded: false
}
},
methods: {
handleLoad() {
if (this.loaded) return
const observer = uni.createIntersectionObserver(this)
observer.relativeToViewport()
.observe('.lazy-img', (res) => {
if (res.intersectionRatio > 0) {
this.loaded = true
this.$el.src = this.$el.dataset.src
observer.disconnect()
}
})
}
}
}
</script>
javascript复制// 改进后的分页加载逻辑
async loadMore() {
if (this.loading || this.finished) return
this.loading = true
try {
const res = await api.getAssets({
page: this.page + 1,
pageSize: 10
})
// 使用Vue.set确保响应式更新
this.list = [...this.list, ...res.data]
if (res.data.length < 10) {
this.finished = true
} else {
this.page++
}
} finally {
this.loading = false
}
}
根据客户需求,我们实现了三种审批模式:
数据库设计关键字段:
php复制Schema::create('approval_flows', function (Blueprint $table) {
$table->id();
$table->foreignId('asset_id')->constrained();
$table->enum('type', ['linear', 'and', 'or']);
$table->json('approvers'); // 存储审批人ID数组
$table->json('current_approvers'); // 当前待审批人
$table->timestamps();
});
采用多通道通知方案:
消息队列处理代码:
php复制// Laravel Job示例
class SendApprovalNotification implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public ApprovalFlow $flow,
public User $approver
) {}
public function handle()
{
// 优先尝试小程序通知
try {
$this->sendWeappMsg();
} catch (\Exception $e) {
Log::error('小程序通知失败', ['error' => $e]);
// 降级为企业微信通知
$this->sendWorkWechatMsg();
}
}
private function sendWeappMsg()
{
$weapp = app('weapp');
$weapp->sendTemplateMessage([
'touser' => $this->approver->weapp_openid,
'template_id' => config('weapp.templates.approval'),
'data' => [
'thing1' => ['value' => $this->flow->asset->name],
'time2' => ['value' => $this->flow->created_at]
]
]);
}
}
经过压力测试,推荐以下最低配置:
开发环境:
生产环境(100人团队):
关键服务配置:
nginx复制# Nginx优化配置
http {
keepalive_timeout 30;
client_max_body_size 20m;
# 静态资源缓存
server {
location ~* \.(jpg|png|gif|js|css)$ {
expires 30d;
add_header Cache-Control "public";
}
}
}
我们采用Prometheus+Grafana搭建监控系统,重点监控指标:
告警规则示例:
yaml复制# prometheus/rules.yml
groups:
- name: business.rules
rules:
- alert: HighRejectionRate
expr: sum(rate(approval_rejected_total[5m])) by (asset_type) / sum(rate(approval_requests_total[5m])) by (asset_type) > 0.3
for: 10m
labels:
severity: warning
annotations:
summary: "High rejection rate for {{ $labels.asset_type }}"
description: "Rejection rate is {{ $value }}"
上线后遇到的典型问题及解决方案:
php复制// 优化前
$assets = Asset::all();
foreach ($assets as $asset) {
$asset->borrowLogs; // 每次循环都查询数据库
}
// 优化后
$assets = Asset::with(['borrowLogs' => function($query) {
$query->where('status', 'borrowed');
}])->get();
php复制DB::transaction(function() {
$asset = Asset::lockForUpdate()->find($id);
// 处理审批逻辑
});
php复制// Laravel中间件
public function handle($request, Closure $next)
{
$key = 'api_limit:' . $request->ip();
$limit = 100; // 每分钟100次
$current = Redis::command('LLEN', [$key]);
if ($current >= $limit) {
return response()->json(['error' => 'Too many requests'], 429);
}
Redis::command('LPUSH', [$key, time()]);
Redis::command('EXPIRE', [$key, 60]);
return $next($request);
}
php复制class AssetPolicy
{
public function view(User $user, Asset $asset)
{
return $user->department_id === $asset->department_id;
}
}
实现原理:
核心代码:
php复制// 每天凌晨执行检查
$schedule->command('check:asset-alerts')
->dailyAt('00:05')
->runInBackground();
// 命令处理逻辑
$criticalAssets = Asset::whereColumn('current_value', '<=', 'alert_threshold')
->where('alert_enabled', true)
->get();
foreach ($criticalAssets as $asset) {
AlertDispatcher::dispatch($asset);
}
实现方案:
php复制use Endroid\QrCode\QrCode;
$qrCode = new QrCode(route('assets.show', $asset));
$qrCode->setSize(300);
header('Content-Type: '.$qrCode->getContentType());
echo $qrCode->writeString();
前端扫码处理:
javascript复制uni.scanCode({
success: (res) => {
const assetId = this.parseUrl(res.result)
uni.navigateTo({
url: `/pages/asset/detail?id=${assetId}`
})
}
})
经过三个月的开发和迭代,系统目前稳定支持500+固定资产的管理。几个关键数据:
后续演进方向:
技术债待解决:
这个项目给我的最大启示是:技术选型没有绝对的好坏,关键是匹配团队和业务的实际需求。ThinkPHP和Laravel在这个项目中各展所长,最终都很好地支撑了业务发展。