1. 项目背景与核心价值
去年给本地一所高校开发在线投票系统时,我选择了ThinkPHP+Vue这个技术组合。这种前后端分离的架构特别适合需要快速迭代的校园应用场景——教务处要频繁组织各种评选活动,从学生会选举到优秀教师评比,每个活动都有独特的投票规则和展示需求。
传统PHP套模板的方式在应对这种多变需求时显得力不从心,而全栈JavaScript又对学校现有技术栈冲击太大。ThinkPHP6的轻量级API开发能力配合Vue3的组件化前端,既保留了PHP部署简单的优势,又获得了现代前端开发的灵活性。实测下来,从需求变更到上线平均能缩短40%工期。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术架构设计解析
2.1 后端服务层搭建
选用ThinkPHP6.0.13版本,主要考虑其:
- 内置JWT组件简化接口鉴权
- 完善的数据库迁移工具
- 符合PSR规范的中间件体系
典型控制器代码结构:
php复制class VoteController
{
#[Route('/api/vote')]
public function create(VoteRequest $request)
{
$vote = Vote::create($request->validated());
return json($vote->toArray());
}
}
数据库设计特别注意了投票事务特性:
sql复制CREATE TABLE `votes` (
`id` bigint unsigned PRIMARY KEY AUTO_INCREMENT,
`user_id` bigint NOT NULL COMMENT '投票人',
`item_id` bigint NOT NULL COMMENT '候选项目',
`activity_id` bigint NOT NULL COMMENT '所属活动',
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `user_activity_unique` (`user_id`,`activity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
2.2 前端工程化实践
采用Vue3+TypeScript组合,通过以下配置提升开发体验:
javascript复制// vite.config.ts
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
// 处理ThinkPHP模板标签冲突
delimiters: ['${', '}']
}
}
})
],
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
})
典型投票组件设计:
vue复制<script setup lang="ts">
const props = defineProps<{
candidates: Candidate[]
maxSelect: number
}>()
const selected = ref<number[]>([])
</script>
<template>
<div class="grid grid-cols-3 gap-4">
<div
v-for="item in candidates"
:class="['card', selected.includes(item.id) ? 'border-blue-500' : '']"
@click="handleSelect(item.id)"
>
<img :src="item.avatar" class="w-20 h-20 rounded-full">
<h3>{{ item.name }}</h3>
</div>
</div>
</template>
3. 核心业务逻辑实现
3.1 防刷票机制设计
采用三级防御策略:
- 基础层:IP限流(Nginx配置)
nginx复制limit_req_zone $binary_remote_addr zone=voteapi:10m rate=5r/s;
location /api/vote {
limit_req zone=voteapi burst=10 nodelay;
try_files $uri $uri/ /index.php?$query_string;
}
- 业务层:分布式锁防重复提交
php复制use think\facade\Cache;
$lockKey = "vote_lock:{$userId}:{$activityId}";
if (!Cache::store('redis')->set($lockKey, 1, ['nx', 'ex' => 10])) {
throw new Exception('操作过于频繁');
}
- 数据层:事务保证一致性
php复制Db::transaction(function() use ($data) {
$vote = Vote::create($data);
Candidate::where('id', $data['item_id'])
->inc('votes_count')
->update();
});
3.2 实时统计展示方案
通过WebSocket实现双通道更新:
javascript复制// 前端建立连接
const socket = new WebSocket(`wss://${location.host}/ws/vote`)
socket.onmessage = (event) => {
const data = JSON.parse(event.data)
if (data.type === 'stats_update') {
updateChart(data.payload)
}
}
后端使用Workerman处理推送:
php复制$worker = new Worker('websocket://0.0.0.0:2345');
$worker->onMessage = function($connection, $data) {
$activityId = json_decode($data)->activity_id;
$stats = Db::name('votes')
->where('activity_id', $activityId)
->group('item_id')
->field('item_id,count(*) as count')
->select();
$connection->send(json_encode([
'type' => 'stats_update',
'payload' => $stats
]));
};
4. 部署与性能优化
4.1 生产环境配置
Nginx关键配置优化:
nginx复制location ~ \.php$ {
fastcgi_buffer_size 128k;
fastcgi_buffers 256 16k;
fastcgi_busy_buffers_size 256k;
# 静态资源缓存
location ~* \.(jpg|png|css|js)$ {
expires 30d;
add_header Cache-Control "public";
}
}
ThinkPHP生产配置建议:
php复制// config/cache.php
return [
'default' => 'redis',
'stores' => [
'redis' => [
'type' => 'redis',
'host' => '127.0.0.1',
'port' => 6379,
'password' => '',
'select' => 1,
'timeout' => 0,
'persistent' => true
]
]
];
4.2 前端性能调优
按需加载策略:
javascript复制// 路由配置中
const routes = [
{
path: '/results',
component: () => import('./views/Results.vue'),
meta: { preload: true } // 提前预加载
}
]
关键CSS内联处理:
html复制<head>
<style>
.vote-button {
@apply bg-blue-500 text-white py-2 px-4 rounded;
}
</style>
</head>
5. 踩坑经验实录
5.1 跨域会话保持问题
开发环境遇到的典型问题:
- 现象:登录状态在接口请求时丢失
- 根因:SameSite Cookie策略限制
- 解决方案:
php复制// config/cookie.php
return [
'secure' => false, // 开发环境关闭
'samesite' => 'None', // 允许跨站
'httponly' => true
];
5.2 大屏展示内存泄漏
数据看板常见问题:
- 现象:长时间运行后浏览器卡顿
- 排查:Chrome Performance面板显示图表实例未销毁
- 修复方案:
vue复制<script setup>
import { onUnmounted } from 'vue'
const chart = initChart()
onUnmounted(() => {
chart.dispose() // 清理ECharts实例
})
</script>
5.3 高并发投票场景
压力测试暴露的问题:
- 现象:MySQL出现大量死锁
- 优化方案:
- 改用Redis原子计数器预处理
php复制$redis->incr("activity:{$activityId}:item:{$itemId}");
- 定时任务异步持久化
php复制$schedule->call(function(){
$keys = $redis->keys('activity:*:item:*');
foreach($keys as $key) {
$count = $redis->get($key);
Db::name('candidates')
->where('id', substr($key, -1))
->update(['votes_count' => $count]);
}
})->everyMinute();
6. 扩展功能建议
6.1 可视化投票分析
集成ECharts实现:
javascript复制const renderChart = (data) => {
const option = {
dataset: { source: data },
xAxis: { type: 'category' },
yAxis: {},
series: [{ type: 'bar' }]
}
chart.setOption(option)
}
6.2 微信小程序端适配
Uniapp跨端方案:
javascript复制// 条件编译处理差异
// #ifdef MP-WEIXIN
wx.login({
success(res) {
store.commit('SET_WXCODE', res.code)
}
})
// #endif
6.3 自动化测试方案
PHPUnit测试用例示例:
php复制class VoteTest extends TestCase
{
public function testDuplicateVote()
{
$this->post('/api/vote', ['user_id' => 1, 'item_id' => 5]);
$response = $this->post('/api/vote', ['user_id' => 1, 'item_id' => 5]);
$response->assertStatus(422);
}
}
