这个内部服务器销售信息管理平台项目源于企业IT资产管理中的实际痛点。在中等规模以上的互联网公司或IDC服务商中,服务器等硬件设备的采购、分配、维护和报废流程往往涉及多个部门协作。传统Excel表格或简单CRM系统难以满足以下需求:
ThinkPHP框架的选择体现了典型的中型业务系统技术选型策略。作为国内广泛使用的PHP框架,其优势在于:
提示:在传统企业IT系统中,约60%的自研管理系统选择PHP+MySQL组合,主要考虑开发效率与运维成本平衡。但需注意ThinkPHP5.1以下版本存在已知安全漏洞,建议使用6.0+版本。
mermaid复制graph TD
A[前端] -->|Vue.js| B(交互组件)
B --> C[Element UI]
D[后端] -->|ThinkPHP 6| E(业务逻辑)
E --> F[MySQL 8.0]
G[基础设施] --> H[Docker容器化]
H --> I[Redis缓存]
(注:根据规范要求,实际输出时应删除此mermaid图表,改为文字描述)
前端采用Vue.js + Element UI组合,实现响应式管理界面。这种选型考虑:
后端核心模块包括:
主要表结构及其关系:
| 表名 | 关键字段 | 关联关系 |
|---|---|---|
| server_assets | id,sn,type_id,spec,status,purchase_date,price | 多对一asset_type |
| asset_type | id,name,spec_template | 一对多server_assets |
| department | id,name,manager | 一对多server_assets |
| maintenance_log | id,asset_id,fault_desc,handler | 多对一server_assets |
注意:status字段建议使用ENUM类型定义('in_stock','in_use','maintenance','scrapped'),比纯数字状态码更易维护
典型代码示例(ThinkPHP控制器逻辑):
php复制public function store(Request $request)
{
$data = $request->only(['sn', 'type_id', 'spec_json']);
// 验证器配置
$validate = Validate::rule([
'sn' => 'require|unique:server_assets',
'type_id' => 'require|number',
'spec_json' => 'require|json'
]);
if (!$validate->check($data)) {
return json($validate->getError(), 400);
}
// 补充系统字段
$data['status'] = 'in_stock';
$data['operator'] = session('user_id');
$data['purchase_date'] = date('Y-m-d');
try {
$asset = ServerAsset::create($data);
// 记录操作日志
SystemLog::create([
'type' => 'asset_add',
'content' => json_encode($data)
]);
return json(['id' => $asset->id]);
} catch (\Exception $e) {
Log::error('Asset add failed: '.$e->getMessage());
return json(['error' => 'DB_ERROR'], 500);
}
}
关键实现要点:
基于状态机的工单流转实现:
php复制class WorkflowEngine
{
private static $transitions = [
'purchase_apply' => [
'from' => ['draft'],
'to' => 'waiting_approval',
'handler' => 'sendApprovalEmail'
],
'approve' => [
'from' => ['waiting_approval'],
'to' => 'approved',
'handler' => 'createFinanceRecord'
]
];
public function process($action, $ticket)
{
if (!isset(self::$transitions[$action])) {
throw new \Exception('Invalid action');
}
$rule = self::$transitions[$action];
if (!in_array($ticket->status, $rule['from'])) {
throw new \Exception('Invalid status transition');
}
DB::transaction(function() use ($rule, $ticket) {
$ticket->status = $rule['to'];
$ticket->save();
if ($rule['handler']) {
$this->{$rule['handler']}($ticket);
}
});
}
}
php.ini关键参数:
ini复制; ThinkPHP生产模式优化
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0 ; 代码更新需手动清除缓存
; 会话配置
session.save_handler = redis
session.save_path = "tcp://redis:6379"
Nginx站点配置要点:
nginx复制location ~ \.php$ {
fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
fastcgi_busy_buffers_size 256k;
# 静态资源缓存
location ~* \.(jpg|png|css|js)$ {
expires 30d;
add_header Cache-Control "public";
}
}
资产列表页的典型性能问题及解决方案:
问题场景:
优化方案:
php复制$types = Cache::remember('asset_types', 3600, function() {
return AssetType::pluck('name', 'id')->toArray();
});
sql复制ALTER TABLE `server_assets`
ADD INDEX `idx_status_type` (`status`, `type_id`),
ADD INDEX `idx_department` (`department_id`);
php复制// 不使用count()获取总数
$paginator = ServerAsset::where($conditions)
->paginate(15, ['*'], 'page', $page);
// 前端显示"约XXX条"代替精确计数
ThinkPHP安全防护三板斧:
php复制class XssGuard
{
public function handle($request, Closure $next)
{
$input = $request->all();
array_walk_recursive($input, function(&$item) {
$item = htmlspecialchars($item, ENT_QUOTES);
});
$request->merge($input);
return $next($request);
}
}
php复制// 错误示范
Db::query("SELECT * FROM table WHERE id=".$id);
// 正确做法
Db::query("SELECT * FROM table WHERE id=?", [$id]);
{__token__}php复制protected $rule = [
'__token__' => 'token'
];
关键审计要素记录:
php复制SystemLog::create([
'user_id' => Session::get('user_id'),
'action' => 'asset_delete',
'ip' => request()->ip(),
'user_agent' => request()->server('HTTP_USER_AGENT'),
'before_data' => json_encode($asset->toArray()),
'request_params' => json_encode(request()->except(['password']))
]);
重要:审计日志需要定期归档(建议按季度),原始记录不可删除,只做标记废弃
消息通知对接示例:
php复制class WeChatNotifier
{
public static function send($to, $content)
{
$client = new \GuzzleHttp\Client();
$response = $client->post('https://qyapi.weixin.qq.com/cgi-bin/message/send', [
'query' => ['access_token' => $this->getToken()],
'json' => [
'touser' => $to,
'msgtype' => 'text',
'agentid' => config('wechat.agent_id'),
'text' => ['content' => $content],
'safe' => 0
]
]);
return json_decode($response->getBody(), true);
}
private function getToken()
{
return Cache::remember('wechat_token', 7000, function() {
// 获取token逻辑
});
}
}
使用ECharts实现的关键代码:
javascript复制// 资产状态分布饼图
function initStatusChart() {
const chart = echarts.init(document.getElementById('status-chart'));
axios.get('/api/asset/stats').then(response => {
chart.setOption({
tooltip: { trigger: 'item' },
series: [{
type: 'pie',
data: response.data.map(item => ({
value: item.count,
name: item.status
})),
roseType: 'radius'
}]
});
});
}
/health接口实现示例:
php复制public function health()
{
$checks = [
'database' => $this->checkDatabase(),
'redis' => $this->checkRedis(),
'storage' => disk_free_space('/') > 1073741824 // 剩余1GB
];
$status = in_array(false, $checks) ? 503 : 200;
return json([
'status' => $status,
'details' => $checks
], $status);
}
推荐ELK栈配置:
yaml复制filebeat.inputs:
- type: log
paths:
- "/var/www/storage/logs/*.log"
fields:
app: asset_management
output.logstash:
hosts: ["logstash:5044"]
ruby复制filter {
grok {
match => { "message" => "\[%{TIMESTAMP_ISO8601:timestamp}\] %{WORD:env}\.%{LOGLEVEL:level}: %{GREEDYDATA:message}" }
}
date {
match => [ "timestamp", "ISO8601" ]
}
}
php复制use Endroid\QrCode\QrCode;
$qrCode = new QrCode($assetUrl);
header('Content-Type: '.$qrCode->getContentType());
echo $qrCode->writeString();
经验之谈:初期建议保持单体架构,当并发请求超过500QPS或团队规模超过10人时再考虑微服务化