1. 为什么今天还要亲手装禅道?这不只是“搭个系统”那么简单
禅道项目管理软件,这个名字在研发团队的日常沟通里出现频率高得有点意外——它不像某些云服务那样自带光环,也没有SaaS平台那种开箱即用的 slick 感,但凡经历过3次以上迭代复盘、5轮以上需求对齐、2次以上线上事故回溯的团队,几乎都会在某个深夜的站会上听到一句:“要不,我们把需求和Bug都搬到禅道里跑一遍?”这不是跟风,而是真实踩坑后形成的肌肉记忆。
我接触禅道最早是在某高校实验室带学生做毕业设计管理系统时。当时用的是第三方托管的轻量版,结果某天凌晨两点,学生提交的17个测试用例突然全部丢失,后台日志只显示“数据库连接超时”,而服务商回复是“例行维护,预计4小时恢复”。那一晚我们手动导出Excel、核对Git提交记录、重新录入任务状态,折腾到天亮。从那以后,我养成了一个铁律:凡是核心协作流经的系统,必须掌握全链路部署权。禅道恰恰是少数几个把“可私有化、可审计、可追溯”刻进架构基因里的国产开源项目管理工具——它不追求炫酷UI,但每个字段都有明确的业务语义;它不堆砌AI功能,但每个工作流节点都支持自定义触发条件;它不强调“低代码”,却用最朴素的PHP+MySQL组合,实现了从需求→任务→Bug→发布→文档的闭环追踪。
标题里说的“3种方法”,不是为了凑数,而是对应三类真实场景:第一种是开发环境快速验证(Docker一键拉起),适合刚接触敏捷流程的产品经理或测试同学,5分钟内就能看到看板、燃尽图、版本计划的真实形态;第二种是生产环境稳态部署(源码编译+LNMP),面向中小技术团队的运维负责人,要求你理解Nginx重写规则为何必须加/zentao/前缀、MySQL字符集为何必须设为utf8mb4、PHP扩展中pdo_mysql和gd缺一不可;第三种是离线信创环境适配(ARM64+达梦数据库桥接),这是最近两年越来越多某国企下属研究院提出的需求,涉及PHP源码层的ODBC驱动替换、SQL语法兼容性补丁、以及国产中间件的session共享机制改造。这三种路径背后,其实是三种不同的“控制粒度”:容器化给你时间维度的控制(随时回滚镜像),源码部署给你配置维度的控制(改一行.htaccess就能切换多语言),信创适配则给你数据主权维度的控制(所有日志落盘路径、加密算法、审计接口全由你定义)。
所以这篇指南不会教你“点击下一步安装成功”,而是带你拆开禅道的每一颗螺丝:为什么它的权限模型用RBAC+ABAC混合设计?为什么测试用例导入模板里第7列必须是“前置条件”而非“预期结果”?为什么升级包解压后www/目录下会多出一个.upgrade.lock文件?这些细节不是冷知识,而是你在实际使用中遇到“权限继承失效”“用例批量导入失败”“升级后首页白屏”等问题时,唯一能帮你快速定位的线索。接下来的内容,我会以一个完整上线周期为脉络,从环境准备、核心配置、数据迁移、到日常巡检,把这三套部署方案的底层逻辑、实操陷阱、验证要点全部摊开讲透。
2. 三种部署路径的本质差异与选型决策树
2.1 Docker部署:快是唯一目标,但“快”有严格前提
Docker部署常被误读为“最简单”的方式,其实恰恰相反——它对使用者的抽象能力要求最高。你不需要懂PHP怎么加载zentao/config.php,但必须清楚docker-compose.yml里volumes挂载的/app/www/路径,到底映射到宿主机哪个物理位置;你不用配置Nginx虚拟主机,但得明白-p 8080:80这个端口映射,意味着外部请求必须通过http://localhost:8080/zentao访问(注意末尾的/zentao,这是禅道硬编码的入口路径,不是Nginx别名)。
我试过用docker run -d -p 8080:80 --name zentao easysoft/zentao这条命令启动官方镜像,表面看一切正常,但第二天就发现所有上传的附件都消失了。排查后发现,官方镜像默认把/app/www/data/目录设为临时卷(tmpfs),容器重启后自动清空。这个问题在Docker Hub页面的README.md里用小号字体写着:“Production use requires bind mount for /app/www/data”,但90%的新手根本不会点开看。后来我改成这样:
docker run -d \ --name zentao \ -p 8080:80 \ -v $(pwd)/zentao-data:/app/www/data \ -v $(pwd)/zentao-config:/app/www/zentao/config \ easysoft/zentao这里有两个关键点:第一,zentao-data必须是宿主机上的绝对路径,相对路径在某些Docker Desktop版本里会静默失败;第二,zentao-config挂载的是整个config目录,而不是单个my.php文件——因为禅道启动时会动态生成common.php和db.php,如果只挂载my.php,后续升级时新生成的配置文件会被覆盖。
提示:Docker部署最适合三类人:需要快速演示给客户看的售前工程师、正在学习Scrum流程的产品新人、以及负责CI/CD流水线集成的DevOps同学。它不适合长期承载核心业务数据,因为官方镜像默认关闭了OPcache和APCu缓存,高并发下响应延迟会明显升高(实测100并发用户时平均响应时间从320ms升至1.8s)。
2.2 源码部署:掌控力最强,但每一步都是选择题
源码部署不是“下载zip包解压到htdocs”这么简单。禅道从9.0版本开始强制要求PHP 7.2+,而很多CentOS 7服务器默认还是PHP 5.4。我见过最典型的翻车现场是:运维同学用yum install php装完,执行php -v显示7.4,但浏览器访问却报错“Your PHP version is too low”。原因在于Apache用的是/etc/httpd/modules/libphp5.so,而CLI用的是/usr/bin/php,两者指向不同版本。解决方法必须同时更新Apache的PHP模块:
# 先卸载旧模块 sudo yum remove php php-common # 安装SCL源(Software Collections) sudo yum install centos-release-scl # 安装PHP 7.4及Apache模块 sudo yum install rh-php74 rh-php74-php rh-php74-php-common rh-php74-php-gd rh-php74-php-mbstring rh-php74-php-mysqlnd rh-php74-php-pdo rh-php74-php-xml # 启用新模块 sudo ln -sf /opt/rh/rh-php74/root/usr/lib64/httpd/modules/libphp7.so /etc/httpd/modules/libphp.so这个过程暴露了源码部署的核心矛盾:你不是在安装一个软件,而是在协调一套生态。MySQL版本也卡得很死——禅道12.x要求MySQL 5.7.13+或8.0.11+,但8.0.20之后默认启用caching_sha2_password认证插件,而禅道的PDO连接字符串不支持该插件。解决方案只能是降级认证方式:
ALTER USER 'zentao'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password'; FLUSH PRIVILEGES;更隐蔽的坑在Nginx配置。禅道的URL重写规则要求所有请求都转发到index.php,但很多教程直接抄try_files $uri $uri/ /index.php?$query_string;,这会导致/zentao/api.php/v1/projects这类API路由404。正确写法必须显式匹配/zentao/前缀:
location /zentao/ { try_files $uri $uri/ /zentao/index.php?$query_string; } location ~ ^/zentao/(?:\.htaccess|\.git|\.svn|\.project|LICENSE|README.md) { deny all; }注意:源码部署的“掌控力”体现在三个可调参数上:
config/my.php里的$config->request->fix控制URL美化开关(设为false可绕过重写规则)、$config->file->uploadMaxSize限制附件大小(单位是MB,不是字节)、$config->db->persist决定是否启用持久连接(生产环境建议true,减少TCP握手开销)。这三个参数在Docker镜像里是写死的,只有源码部署才能动态调整。
2.3 信创环境部署:不是换个数据库就行,而是重构数据契约
最近半年,我帮三家某省属交通集团下属信息公司做了禅道信创适配。他们提的需求很具体:“必须支持达梦DM8、必须运行在鲲鹏920芯片、必须通过等保三级测评”。听起来只是换数据库,实际要动的远不止config/db.php。达梦数据库不支持MySQL的INSERT ... ON DUPLICATE KEY UPDATE语法,而禅道的story表插入逻辑大量依赖此特性。我们不得不在module/story/model.php里重写create()方法,用MERGE INTO替代:
// 原MySQL写法 $sql = "INSERT INTO " . $this->table . " (title, product, ... ) VALUES (?, ?, ...) ON DUPLICATE KEY UPDATE ..."; // 达梦适配写法 $sql = "MERGE INTO " . $this->table . " t USING (SELECT ? AS title, ? AS product, ... FROM DUAL) s ON (t.product = s.product AND t.title = s.title) WHEN MATCHED THEN UPDATE SET t.status = s.status, ... WHEN NOT MATCHED THEN INSERT (title, product, ...) VALUES (s.title, s.product, ...)";更大的挑战在全文检索。禅道默认用MySQL的MATCH AGAINST实现需求搜索,而达梦的CONTAINS函数语法完全不同,且不支持中文分词。我们最终采用折中方案:在达梦侧建物化视图,把story.title和story.spec字段拼接成fulltext_content,再用达梦内置的CTXSYS.CONTEXT索引类型创建上下文索引。但这导致新建需求后,搜索结果有最多30秒延迟(物化视图刷新间隔)。
ARM64架构的坑更底层。禅道依赖的php-zip扩展在ARM64上编译时,libzip库的zip_source_buffer函数存在内存对齐问题,会导致附件解压后损坏。解决方案是升级libzip到1.8.0+,并添加编译参数-march=armv8-a+crypto启用硬件加密指令集。这个细节在任何公开文档里都找不到,是我们用gdb调试php进程core dump时逐行跟踪汇编指令才定位到的。
实操心得:信创适配不是技术验证,而是合规验证。某次验收时,测评机构要求提供“所有SQL语句的执行计划截图”,我们不得不在达梦管理工具里手动执行每个禅道页面的后台SQL(共137条),并证明每条都走了索引。这种工作量,远超代码修改本身。
3. 核心配置项深度解析与避坑指南
3.1 权限体系:RBAC与ABAC的混合实战
禅道的权限模型常被简化为“角色+分组”,但真正决定数据可见性的,是三层过滤器的叠加:角色权限(Role Permission)→ 分组权限(Group Permission)→ 对象权限(Object Permission)。举个典型场景:某测试组长创建了一个“支付模块回归测试”项目,他希望组员A能看到所有Bug,但组员B只能看到自己提交的Bug。如果只在“分组权限”里设置“Bug查看权限”,两人会看到完全相同的数据——因为对象权限(即Bug所属的项目/产品/模块)才是最终裁决者。
我画了个简化的权限决策树:
- 用户登录后,系统先查
usergroup表获取其所属分组ID列表; - 再查
grouppriv表,确认该分组对bug模块是否有view权限(这是RBAC层); - 如果有,则进入ABAC层:检查当前Bug的
product字段值是否在用户account表的products字段里(逗号分隔的ID列表); - 若不在,则继续检查
project字段是否在用户projects字段里; - 最后检查
module字段是否在用户modules字段里(这个字段存储的是模块ID路径,如1.5.12表示三级模块)。
这个链条里最容易被忽略的是第3步。很多团队把测试人员统一加到“测试组”,然后在grouppriv里开通bug.view,结果发现所有人能看到所有Bug。根本原因是account.products字段为空——禅道不会自动把用户加入其参与的项目所关联的产品。解决方案必须手动执行SQL:
UPDATE `zt_user` u JOIN `zt_projectproduct` pp ON u.account = 'testuser' AND pp.project = 123 SET u.products = CONCAT(IFNULL(u.products,''), ',123') WHERE u.account = 'testuser';注意:
products字段是逗号分隔的字符串,不是JSON数组。所以拼接时必须用CONCAT而非JSON_SET,否则会导致权限校验失败。这个设计虽然原始,但保证了MySQL 5.6+的兼容性。
3.2 工作流引擎:状态机不是静态配置,而是动态脚本
禅道的工作流看似在后台“流程定制”里点点鼠标就能完成,实际上每个状态跳转都绑定着PHP脚本。比如从“激活”到“已解决”的转换,不仅更新status字段,还会触发bugModel::processResolve()方法,该方法内部会:
- 检查
assignedTo是否为空(为空则强制指派给当前操作人); - 调用
mail->send()发送通知邮件(邮件模板在lang/zh-cn/mail.php里定义); - 更新
lastEditedDate和lastEditedBy字段; - 如果开启“解决后自动关闭”,则再调用
close()方法。
我遇到过最棘手的问题是:某客户要求“当Bug解决时,自动创建一条关联的测试任务”。标准做法是在bugModel::processResolve()末尾加$this->loadModel('task')->create(...),但这样会导致事务不一致——如果任务创建失败,Bug状态已变更为“已解决”,数据就错乱了。最终方案是改用数据库触发器:
DELIMITER $$ CREATE TRIGGER after_bug_resolve AFTER UPDATE ON zt_bug FOR EACH ROW BEGIN IF OLD.status != 'resolved' AND NEW.status = 'resolved' THEN INSERT INTO zt_task (project, module, story, title, type, status, assignedTo, openedBy, openedDate) VALUES (NEW.project, NEW.module, NEW.story, CONCAT('验证Bug#', NEW.id), 'test', 'wait', NEW.assignedTo, NEW.lastEditedBy, NOW()); END IF; END$$ DELIMITER ;这个触发器比PHP脚本更可靠,因为和Bug更新在同一个事务里。但它带来了新问题:禅道的bug表没有lastEditedBy字段,触发器里要用OLD.assignedTo代替。这种细节,只有读过module/bug/model.php源码才能意识到。
3.3 API集成:v1接口不是RESTful,而是RPC风格
禅道的API文档里写着“符合REST规范”,但实际调用会发现:所有POST请求的body必须是application/x-www-form-urlencoded格式,而不是application/json;所有返回数据都包裹在{"status":"success","data":{...}}结构里,而不是直接返回资源对象。这是因为禅道API本质是PHP的jsonrpc封装,不是真正的REST。
比如创建需求的API:
curl -X POST "http://zentao.example.com/zentao/api.php/v1/stories" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "product=1" \ -d "title=用户登录支持指纹识别" \ -d "type=story" \ -d "status=active" \ -d "mailto=test@example.com"这里mailto参数不是发邮件的收件人,而是“通知哪些人”,值必须是逗号分隔的用户名(如zhangsan,lisi),不是邮箱地址。如果填错,API会静默忽略,但返回status: success,让人误以为成功。
更隐蔽的是Token机制。禅道API不支持Bearer Token,而是用token参数传递,且Token有效期只有2小时。Token生成方式是:md5(account + password + timestamp),其中timestamp是毫秒级时间戳。这意味着前端不能缓存Token,每次请求都要重新计算。我们曾因前端JS里用了Date.now()但后端用microtime(true)导致毫秒差超过1秒,Token校验失败。
实操技巧:调试API时,务必在请求头里加
X-Request-ID: debug-$(date +%s),然后在禅道日志www/data/log/里搜索这个ID,能看到完整的SQL执行轨迹和参数绑定值。这是比var_dump()更精准的调试方式。
4. 数据迁移与日常运维实战手册
4.1 从旧版本平滑升级:不要相信“一键升级”
禅道官网提供的升级包,本质是增量SQL脚本集合。我处理过最惊险的一次升级是从8.2到12.5,跨度4个大版本。官方文档说“备份数据库后执行升级脚本即可”,但实际执行时,在zentao_12.sql里发现一条ALTER TABLE zt_story ADD COLUMNversionINT DEFAULT 1语句,而我们的zt_story表已有version字段(是之前自行添加的)。MySQL报错Duplicate column name 'version',升级中断。
解决方案不是跳过这行SQL,而是先手动检查所有新增字段是否已存在:
SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='zentao' AND TABLE_NAME='zt_story' AND COLUMN_NAME IN ('version','order','deleted');然后编辑zentao_12.sql,把ADD COLUMN替换成MODIFY COLUMN(如果类型不同)或直接删除(如果已存在)。这个过程需要对照每个版本的changelog,确认字段含义是否一致——比如9.0版本的order字段是排序权重,而12.0版本的order是拖拽排序序号,两者不能简单覆盖。
关键提醒:升级前必须停用所有定时任务。禅道的
cron脚本(如zentao/cron.php)会在后台持续扫描zt_todo表,如果升级过程中表结构变更,可能导致脚本崩溃并锁住zt_todo表,进而阻塞所有新任务创建。正确流程是:systemctl stop httpd && php zentao/cron.php --stop && 执行升级 && systemctl start httpd。
4.2 日常巡检清单:5分钟定位90%的线上问题
我把禅道生产环境的日常巡检固化为5个必查项,每天早上9点自动执行:
附件完整性检查
运行find /path/to/zentao/www/data/upload -type f -size -1k | wc -l,统计空文件数量。禅道上传组件在磁盘满时会创建0字节文件,导致用户看到“上传成功”但实际附件丢失。数据库连接池健康度
执行mysqladmin -u zentao -p ext | grep Threads_connected,如果持续高于50,说明PHP-FPM进程没释放连接。需检查config/my.php里$config->db->persist是否为true,并确认max_connections设置合理(建议设为200)。索引碎片率
对zt_story、zt_bug、zt_task三张大表执行SHOW INDEX FROM zt_story,观察Cardinality值是否接近Rows。如果低于50%,说明索引失效,需OPTIMIZE TABLE zt_story。日志轮转状态
检查www/data/log/目录下error.log是否按日期分割(如error.log.20231001)。如果只有error.log一个文件且大于100MB,说明logrotate配置错误,需检查/etc/logrotate.d/zentao。缓存命中率
在PHPinfo页面查看opcache.hits/opcache.misses比值。低于5:1说明OPcache配置不足,需调大opcache.memory_consumption(建议256M)和opcache.max_accelerated_files(建议20000)。
独家技巧:在
www/目录下创建health.php,内容为:<?php $db = new mysqli('localhost','zentao','pass','zentao'); echo $db->ping() ? 'DB: OK' : 'DB: FAIL'; echo file_exists('/path/to/zentao/www/data/upload') ? ' UPLOAD: OK' : ' UPLOAD: FAIL'; ?>然后用
curl http://zentao.example.com/zentao/health.php一分钟内完成基础健康检查。
4.3 故障应急响应:当首页白屏时,先看这三个文件
禅道首页白屏是最高频故障,90%的原因集中在以下三个文件:
www/zentao/config/my.php:检查$config->request->urlMode是否为0(PATH_INFO模式)或1(GET参数模式)。如果Nginx用的是PATH_INFO但配置里设为1,就会白屏。修正后需清空www/data/tmp/目录。www/data/tmp/目录权限:禅道会在此目录生成缓存文件,如果www/data/tmp/属主不是Web服务器用户(如apache或www-data),会导致require_once失败。执行chown -R www-data:www-data www/data/tmp/即可。www/目录下的.htaccess:Apache环境下,如果.htaccess被意外删除或内容被清空,会导致所有URL重写失效,表现为除首页外其他页面404。解决方案是复制www/zentao/.htaccess到www/目录,并确保AllowOverride All在Apache虚拟主机配置里启用。
我整理了一个白屏故障速查表:
| 现象 | 可能原因 | 验证命令 | 解决方案 |
|---|---|---|---|
| 首页白屏,其他页面正常 | my.php中urlMode配置错误 | grep urlMode www/zentao/config/my.php | 改为0并清空tmp/ |
| 所有页面白屏,错误日志无记录 | tmp/目录权限错误 | ls -ld www/data/tmp/ | chown www-data:www-data www/data/tmp/ |
| 首页显示“Database connection failed” | MySQL服务未启动或密码错误 | mysql -u zentao -p -h localhost zentao | 检查config/db.php并重启MySQL |
经验之谈:每次修改配置后,不要直接刷新浏览器,而是先执行
php -l www/zentao/config/my.php检查PHP语法。禅道的配置文件是PHP代码,语法错误会导致整个应用崩溃,且错误提示被错误处理器捕获后显示为白屏。
5. 常见问题与排查技巧实录
5.1 “无法上传附件”问题的七层穿透分析
这个问题看似简单,实则涉及七层系统栈。我按排查顺序列出真实案例:
第一层:浏览器控制台
打开F12,切换到Network标签,上传时观察upload.php请求。如果状态码是413 Request Entity Too Large,说明Nginx限制了请求体大小。解决方案:在nginx.conf里加client_max_body_size 100m;
第二层:PHP配置
如果请求发出但无响应,检查php.ini:upload_max_filesize = 100M和post_max_size = 100M必须同时设置,且post_max_size要大于upload_max_filesize(因为POST数据包含文件和其他字段)。
第三层:禅道配置
在后台“后台-系统-全局-文件设置”里,最大上传尺寸必须小于等于PHP的upload_max_filesize,否则禅道前端会拦截。这个值存在config/my.php的$config->file->uploadMaxSize里。
第四层:Linux内核参数
在高并发上传场景下,net.core.somaxconn(默认128)可能成为瓶颈。执行sysctl -w net.core.somaxconn=1024并写入/etc/sysctl.conf。
第五层:SELinux策略
CentOS 7默认开启SELinux,会阻止Apache写入www/data/upload/。执行setsebool -P httpd_can_network_connect 1和chcon -R -t httpd_sys_rw_content_t www/data/upload/。
第六层:磁盘inode耗尽df -i查看inode使用率。禅道每上传一个文件会创建多个inode(原文件、缩略图、水印图),如果inode用尽,touch test.txt都会失败。解决方案是清理www/data/tmp/里的过期缓存。
第七层:达梦数据库LOB段
信创环境中,达梦的LOB段空间不足会导致上传失败。执行SELECT * FROM V$TABLESPACE WHERE NAME='ZENTAO_LOB';,如果FREE_SIZE为0,需扩容ALTER TABLESPACE ZENTAO_LOB ADD DATAFILE '/dm8/data/zentao/lob02.dbf' SIZE 1024;
排查口诀:“看浏览器→查PHP→审禅道→调内核→关SELinux→清inode→扩LOB”。按这个顺序,95%的上传问题能在10分钟内定位。
5.2 “搜索功能失效”的元凶:MySQL全文索引的隐式陷阱
禅道的需求、Bug、任务搜索依赖MySQL的FULLTEXT索引。但很多团队升级MySQL到8.0后,搜索突然变慢甚至返回空结果。根本原因是MySQL 8.0默认的ngram分词器对中文支持不完善——它把“用户登录”分成“用户”“登录”两个词,但不会生成“用户登录”这个二元组。
验证方法:执行SELECT MATCH(title) AGAINST('用户登录' IN NATURAL LANGUAGE MODE) FROM zt_story LIMIT 1;,如果返回0,说明索引未生效。
解决方案不是换分词器,而是重建索引并指定最小词长:
-- 删除旧索引 ALTER TABLE zt_story DROP INDEX ft_title; -- 创建新索引,最小词长设为2(中文常用词多为2字) ALTER TABLE zt_story ADD FULLTEXT INDEX ft_title (title) WITH PARSER ngram; SET GLOBAL ngram_token_size = 2;但要注意:ngram_token_size是全局变量,修改后需重启MySQL服务才能生效。而且这个设置会影响所有数据库,不是禅道独享。
独家发现:禅道的搜索框支持布尔模式,输入
+用户 +登录(加号表示必须包含)比自然语言模式更精准。这个技巧在后台文档里从未提及,但实测在10万条数据中查询速度提升3倍。
5.3 “定时任务不执行”的五维诊断法
禅道的cron.php脚本负责每日备份、过期任务清理、邮件队列发送。但很多团队反馈“设置了每天9点执行,但邮件从不发送”。我总结出五维诊断法:
维度一:脚本可执行性php -l zentao/cron.php检查语法,php zentao/cron.php --help看是否输出帮助信息。如果报错“Class 'dao' not found”,说明zentao/common/路径未正确引入。
维度二:Crontab环境隔离
Linux的crontab默认PATH是/usr/bin:/bin,不包含PHP的/usr/local/bin。解决方案是写绝对路径:0 9 * * * /usr/local/bin/php /var/www/zentao/cron.php >> /var/log/zentao-cron.log 2>&1
维度三:Web服务器用户权限
如果用www-data用户执行crontab,但cron.php里写的日志路径是/root/zentao.log,会因权限拒绝写入。统一用/var/log/zentao/目录,并chown www-data:www-data /var/log/zentao/
维度四:数据库连接超时cron.php执行时间长(如备份可能耗时5分钟),MySQL的wait_timeout默认28800秒(8小时),但有些云数据库设为60秒。解决方案是在config/db.php里加'options' => [PDO::ATTR_TIMEOUT => 300]
维度五:邮件队列锁机制
禅道用zt_mailqueue表的status字段控制发送状态,如果某次发送中断,status=doing的记录会一直存在,阻塞后续队列。手动执行UPDATE zt_mailqueue SET status='wait' WHERE status='doing';
实操心得:在
cron.php开头加file_put_contents('/tmp/cron-start.log', date('Y-m-d H:i:s') . "\n", FILE_APPEND);,可以确认脚本是否真的被触发。很多问题根源是crontab根本没运行,而不是脚本有问题。
6. 从部署到演进:我的三年禅道实践体感
第一次部署禅道是在2021年,用U盘拷贝源码到客户内网服务器,全程手工编译PHP扩展,光gd库就重装了7次。那时候觉得“能跑起来就是胜利”,连phpinfo()页面都不敢关,生怕一刷新就白屏。
到了2022年,Docker成为标配。我写了自动化脚本,./deploy.sh --env prod --domain zentao.internal一键生成docker-compose.yml,还集成了healthcheck探针。但很快发现,容器化让问题更难定位——当curl http://zentao:8080返回502时,你得依次检查Nginx容器日志、PHP容器日志、MySQL容器日志,三层日志分散在不同地方,比传统LAMP还费劲。
2023年的转折点是信创适配。当第一次在鲲鹏服务器上跑通达梦数据库时,我意识到禅道的价值不在功能多强大,而在于它的“可驯服性”。它的代码没有过度抽象,每个模块职责清晰;它的配置没有魔法开关,所有参数都有明确注释;它的错误提示不兜圈子,Error: SQLSTATE[HY000]: General error: 1364 Field 'xxx' doesn't have a default value直接告诉你缺哪个字段。
现在回头看,部署禅道的过程,本质上是在训练一种系统思维:你必须同时理解HTTP协议的重写规则、MySQL的事务隔离级别、Linux的文件权限模型、PHP的OPcache内存管理。它不像某些SaaS工具,把所有复杂性封装成“联系客服”,而是把复杂性摊开,逼你直面每一个技术决策的后果。
最后分享一个小技巧:禅道的www/data/backup/目录不仅是数据库备份存放地,还是它的“配置快照中心”。每次升级前,我会把整个www/目录打包,同时备份www/data/backup/里的最新SQL文件。这样即使升级失败,也能在5分钟内回退到上一个稳定版本——不是靠Git回滚代码,而是靠数据库还原+文件覆盖。这种“土办法”的可靠性,远超任何自动化工具。
这个过程教会我的最重要一件事是:真正的敏捷,不是追求更快的交付速度,而是拥有随时叫停、随时回退、随时重构的底气。而禅道,就是那个给你这份底气的工具。