很好,我们继续进行插件开发的第三步:URL Rewrite 支持语言前缀。
这是整个插件中最核心的一部分,它将实现:
🎯 目标- 用户访问 /es/、/en/category/... 这类 URL 时,正确加载对应的页面(不再报 Nginx 404)
- 所有前台生成的链接自动带上语言前缀
- 保留 OpenCart 原有 SEO URL 支持(如 category/product slug)
- 未来为每个 URL 自动添加 <link rel="alternate" hreflang="..."> 做好准备
✅ 本阶段将修改/添加的文件(共 2 个):[td]序号 | 文件路径 | 文件类型 | 说明 | ④ | catalog/controller/startup/seo_url_lang_prefix.php | 新建 | 替代默认 seo_url 的语言感知版本 | ⑤ | catalog/controller/startup/router.php | 修改 | 注入我们自定义的语言前缀路由器 |
📁 ④ 新建文件:catalog/controller/startup/seo_url_lang_prefix.php
<?php
class ControllerStartupSeoUrlLangPrefix extends Controller {
public function index() {
// 加载默认 SEO URL 解析器
if ($this->config->get('config_seo_url')) {
$this->url->addRewrite($this);
}
// 获取当前 URL 请求路径
$route = '';
if (isset($this->request->server['REQUEST_URI'])) {
$route = ltrim(parse_url($this->request->server['REQUEST_URI'], PHP_URL_PATH), '/');
}
$parts = explode('/', $route);
// 检测语言前缀(如 /en/、/es/)
$languages = array_column($this->model_localisation_language->getLanguages(), 'code');
$language_code = isset($parts[0]) ? $parts[0] : '';
if (in_array($language_code, $languages)) {
$this->session->data['language'] = $language_code;
$this->config->set('config_language', $language_code);
// 去掉语言前缀后重新拼接 URI
array_shift($parts);
$new_path = implode('/', $parts);
// 设置修改后的 REQUEST_URI
$_SERVER['REQUEST_URI'] = '/' . $new_path;
$this->request->server['REQUEST_URI'] = '/' . $new_path;
}
}
// 生成链接时自动加上语言前缀
public function rewrite($link) {
$url_info = parse_url(str_replace('&', '&', $link));
if (!isset($url_info['query'])) return $link;
parse_str($url_info['query'], $data);
if (!isset($data['route'])) return $link;
$route = $data['route'];
unset($data['route']);
$seo_url = '';
// 可在此处添加你的 slug 映射逻辑(如 category_id => slug)
// 构造基础 URL
$url = $url_info['scheme'] . '://' . $url_info['host'];
// 获取语言前缀
$language = $this->config->get('config_language');
$seo_url .= '/' . $language;
// 添加 slug(可根据 route 进一步丰富)
if ($route == 'common/home') {
// do nothing
} else {
$seo_url .= '/' . str_replace('.', '/', $route);
}
if ($data) {
$seo_url .= '?' . http_build_query($data);
}
return $url . $seo_url;
}
}
📁 ⑤ 修改文件:catalog/controller/startup/router.php
在文件中查找这段(大概在 index() 函数中):
if ($this->config->get('config_seo_url')) {
$this->url->addRewrite(new SeoUrl($this->registry));}
将其替换为:
if ($this->config->get('config_seo_url')) {
// 替换为我们支持语言前缀的 SEO URL 重写
$this->url->addRewrite(new ControllerStartupSeoUrlLangPrefix($this->registry));
}
💡 注意:你也可以通过在 admin > System > Settings > Server 中启用 Use SEO URL 选项来激活重写。
🧪 部署后检查
完成上述两个文件后,按如下步骤检查:
- ✅ 清除 OC 缓存(Admin > Dashboard > Gear Icon)
- ✅ 访问 https://oc.indmu.com/es/ 应能正常进入首页,语言为西班牙语
- ✅ 所有生成的链接都带有 /es/ 前缀
- ✅ 访问 /en/category/category-name 应指向英文分类页(你之后可以设定 slug)
是否继续进入下一步:支持 SEO keyword 映射 slug(category/product)?
还是你先测试上述功能?我也可以为你生成这个步骤的完整压缩包。
反馈:
我已经完成了第四步,新建了文件catalog/controller/startup/seo_url_lang_prefix.php,
在试图完成第五步的时候,
修改文件:catalog/controller/startup/router.php
在文件中查找这段(大概在 index() 函数中):其实没有找到:
if ($this->config->get('config_seo_url')) {
$this->url->addRewrite(new SeoUrl($this->registry));
}
我发给你完整的代码:
opencart 3.0.3.9
catalog/controller/startup/router.php:
<?php
class ControllerStartupRouter extends Controller {
public function index() {
// Route
if (isset($this->request->get['route']) && $this->request->get['route'] != 'startup/router') {
$route = $this->request->get['route'];
} else {
$route = $this->config->get('action_default');
}
// Sanitize the call
$route = preg_replace('/[^a-zA-Z0-9_\/]/', '', (string)$route);
// Trigger the pre events
$result = $this->event->trigger('controller/' . $route . '/before', array(&$route, &$data));
if (!is_null($result)) {
return $result;
}
// We dont want to use the loader class as it would make an controller callable.
$action = new Action($route);
// Any output needs to be another Action object.
$output = $action->execute($this->registry);
// Trigger the post events
$result = $this->event->trigger('controller/' . $route . '/after', array(&$route, &$data, &$output));
if (!is_null($result)) {
return $result;
}
return $output;
}
}
非常好!你的 router.php 是 OpenCart 3.0.3.9 的默认结构,它没有默认添加 seo_url 重写器,而是通过 事件系统(Events)在系统初始化阶段注入 URL 重写逻辑。
这意味着我们不需要在 router.php 中修改任何代码。
我们应该以插件形式注册自定义的重写类,并确保它在 startup 流程中被加载。
✅ 正确的做法:将我们自定义的语言前缀支持插件,注册为 startup 类型你只需要做两件事: 📌 第一步:编辑文件catalog/controller/startup/seo_url_lang_prefix.php
将开头这部分:
class ControllerStartupSeoUrlLangPrefix extends Controller { public function index() {
修改为:
class ControllerStartupSeoUrlLangPrefix extends Controller { public function index() { // 如果启用了 SEO URL 功能 if ($this->config->get('config_seo_url')) { $this->url->addRewrite($this); // 注册当前类为 URL 重写器 } // 接下来的逻辑...你应该已经这样写了,无需再动。
📌 第二步:在后台注册插件(一次性)- 登录后台 admin
- 进入菜单:Extensions > Extensions
- 在类型下拉中选择:Modules
- 点击右上角的 + Install,选择你刚刚创建的 seo_url_lang_prefix(需要你制作成 OCMOD 安装包,或者我们现在可以手动模拟)
- 启用该插件
⚠️ 但是你现在没有安装界面,也没做 OCMOD 包怎么办?
我们就手动模拟加载这个文件到启动流程中。
🛠 手动注册 startup(模拟插件注入)
打开文件:
catalog/controller/startup/startup.php
这个文件会按顺序加载所有 startup 组件,我们可以手动添加我们的语言前缀重写器。
查找这一段(大约在底部):
$this->load->controller('startup/seo_url');
并在它前面添加这一行:
$this->load->controller('startup/seo_url_lang_prefix');
完整结构大概如下:
// 语言前缀重写器
$this->load->controller('startup/seo_url_lang_prefix');
// 原始的 SEO URL(可保留)
$this->load->controller('startup/seo_url');
🔁 说明:为什么不改 router.php
因为 OpenCart 的 URL 重写系统是通过 $this->url->addRewrite(...) 注册一个实现了 rewrite($link) 方法的类,然后系统会自动在生成链接时调用。默认系统就是这么通过 startup/seo_url 注册重写逻辑的。
你写的 seo_url_lang_prefix.php 只要被加载,并调用 $this->url->addRewrite($this),就会参与 URL 重写逻辑,无需修改 router.php。
✅ 现在你应该这样做:
- 将 startup.php 添加一行:加载 startup/seo_url_lang_prefix
- 清缓存
- 访问:
- https://oc.indmu.com/en/
- https://oc.indmu.com/es/
如果你的设置没问题,URL 应该不再报 404,并且语言前缀自动设置成功。
是否需要我现在打包这个 OCMOD 安装插件?还是你想先手动验证成功?
我想手动验证
很好!我们现在就手动验证是否成功。请按以下步骤操作并确认每一步是否完成:
✅ 手动测试语言前缀支持插件(不依赖 OCMOD)
🔧 第一步:确认已有文件
你之前已创建以下文件:
[td]步骤 | 路径 | 文件名 | 1 | catalog/controller/startup/ | seo_url_lang_prefix.php | 2 | catalog/language/en-gb/ | startup/seo_url_lang_prefix.php(可选) | 3 | catalog/model/ | 可能还未建立,但暂不需要 | 我们只需要保证第1个控制器文件存在即可。
🧠 第二步:修改 startup.php 手动加载语言前缀插件
打开这个文件:
catalog/controller/startup/startup.php
在靠近底部的这一段:
$this->load->controller('startup/seo_url');上方加入:
$this->load->controller('startup/seo_url_lang_prefix');现在最后一部分应该像这样:
$this->load->controller('startup/seo_url_lang_prefix');$this->load->controller('startup/seo_url');$this->load->controller('startup/sass'); // 如果你有这个保存后上传覆盖。
🧹 第三步:清除缓存清除 OpenCart 的缓存:
- 登录后台
- 进入 Dashboard
- 点击右上角的齿轮图标(⚙️)→ 刷新修改和主题缓存
或者你也可以手动清空这些文件夹中的缓存文件:
- /system/storage/cache/
- /system/storage/modification/
🔍 第四步:测试前台访问尝试访问以下链接:
如果你设置语言(西班牙语)正确,并安装了至少一个商品或分类页面,它们应该会自动进入西班牙语状态。
还可以测试生成链接是否自动加前缀,例如首页 logo 链接、分类链接是否变成 /es/xxx。
🧪 第五步:如果页面仍然 404?请检查以下内容:
[td]检查项 | 如何确认 | 🔁 是否启用了 SEO URL 功能 | 后台 → System → Settings → Edit Store → Server → Use SEO URLs = ✅ Yes | 📜 catalog/.htaccess 是否存在 | 使用 Nginx 则不需要,但你必须配置了重写规则(你已经说你配好了) | 🗂 catalog/controller/startup/seo_url_lang_prefix.php 是否执行 | 可以在代码中加一行 file_put_contents('/tmp/test.log', 'lang_prefix loaded'); 来测试 |
✅ 补充建议:
临时调试方式
你也可以在 seo_url_lang_prefix.php 中 rewrite() 方法顶部写入日志:
public function rewrite($link) {
file_put_contents('/tmp/opencart_debug.log', "Rewrite hook triggered: $link\n", FILE_APPEND);
...
}
上传后访问页面,检查 /tmp/opencart_debug.log 是否有内容,确认你的类是否参与了 URL 重写流程。
做好这些之后,你可以告诉我测试结果。如果成功,我们就进入下一步:让产品页和分类页也实现语言前缀跳转,并加上 hreflang 标签。
反馈
说明,我在一开始运行网站,就设置好了:后台 → System → Settings → Edit Store → Server → Use SEO URLs = ✅ Yes
我在操作第二步的时候,
catalog/controller/startup/startup.php
没有发现这个代码:
$this->load->controller('startup/seo_url');
非常好,你一开始就启用了 SEO URLs 选项,这是正确的前提条件。现在我们来解决你提到的问题:
🧩 问题:
在 catalog/controller/startup/startup.php 中没有找到
$this->load->controller('startup/seo_url');
✅ 解释:OpenCart 3.0.3.9 的 SEO URL 并不是在 startup/startup.php 中加载的,而是在 system/framework.php 中通过 $action 执行控制器链条的。
具体地说,catalog/controller/startup/seo_url.php 会在 startup.php → start('catalog') 执行时被加载,但它是通过配置和事件系统触发的,不是显式写成 $this->load->controller(...)。
✅ 正确做法(OpenCart 3.0.3.9 的兼容方式)
因为 startup.php 没有手动加载任何 startup controller,所以你需要用事件系统注册你的 seo_url_lang_prefix 控制器。
🧪 方法一(推荐):注册事件,在
catalog/controller/startup/startup.php
末尾手动加载
打开并编辑:
catalog/controller/startup/startup.php
在末尾(return true; 之前)添加:
$this->load->controller('startup/seo_url_lang_prefix');
示例:
...// Original startup code
$this->load->controller('startup/startup_other_things');
// Add this before return
$this->load->controller('startup/seo_url_lang_prefix');
return true;
✅ 然后:
🧪 方法二(可选):通过事件系统注册你也可以在后台添加事件(在数据库中),或者通过安装 XML OCMOD,但既然我们现在是测试阶段,建议直接用方法一写死加载逻辑。
🔁 回顾目标
我们要实现的是:
- https://oc.indmu.com/en/ 加载英语首页
- https://oc.indmu.com/es/ 加载西班牙语首页
- 所有链接自动带上 /en/ 或 /es/
- 后续添加 hreflang 和兼容 OptimBlog
你完成以上后,告诉我结果,我们继续下一步。如果还是 404,我会帮你查 URL 重写或语言前缀处理代码是否有问题。
反馈
我在操作你给的方法一($this->load->controller('startup/seo_url_lang_prefix');)之前,发给你两个文件代码,你看了以后,是否还推荐这个方法。 你说“OpenCart 3.0.3.9 的 SEO URL 并不是在 startup/startup.php 中加载的,而是在 system/framework.php 中通过 $action 执行控制器链条的。”
下面的代码是system/framework.php(我没有在里面找到seo url,请解释):
<?php
// Registry
$registry = new Registry();
// Config
$config = new Config();
$config->load('default');
$config->load($application_config);
$registry->set('config', $config);
// Log
$log = new Log($config->get('error_filename'));
$registry->set('log', $log);
date_default_timezone_set($config->get('date_timezone'));
set_error_handler(function($code, $message, $file, $line) use($log, $config) {
// error suppressed with @
if (!(error_reporting() & $code)) {
return false;
}
switch ($code) {
case E_NOTICE:
case E_USER_NOTICE:
$error = 'Notice';
break;
case E_WARNING:
case E_USER_WARNING:
$error = 'Warning';
break;
case E_ERROR:
case E_USER_ERROR:
$error = 'Fatal Error';
break;
default:
$error = 'Unknown';
break;
}
if ($config->get('error_display')) {
echo '<b>' . $error . '</b>: ' . $message . ' in <b>' . $file . '</b> on line <b>' . $line . '</b>';
}
if ($config->get('error_log')) {
$log->write('PHP ' . $error . ': ' . $message . ' in ' . $file . ' on line ' . $line);
}
return true;
});
// Event
$event = new Event($registry);
$registry->set('event', $event);
// Event Register
if ($config->has('action_event')) {
foreach ($config->get('action_event') as $key => $value) {
foreach ($value as $priority => $action) {
$event->register($key, new Action($action), $priority);
}
}
}
// Loader
$loader = new Loader($registry);
$registry->set('load', $loader);
// Request
$registry->set('request', new Request());
// Response
$response = new Response();
$response->addHeader('Content-Type: text/html; charset=utf-8');
$response->setCompression($config->get('config_compression'));
$registry->set('response', $response);
// Database
if ($config->get('db_autostart')) {
$db = new DB($config->get('db_engine'), $config->get('db_hostname'), $config->get('db_username'), $config->get('db_password'), $config->get('db_database'), $config->get('db_port'));
$registry->set('db', $db);
// Sync PHP and DB time zones
$db->query("SET time_zone = '" . $db->escape(date('P')) . "'");
}
// Session
$session = new Session($config->get('session_engine'), $registry);
$registry->set('session', $session);
if ($config->get('session_autostart')) {
/*
We are adding the session cookie outside of the session class as I believe
PHP messed up in a big way handling sessions. Why in the hell is it so hard to
have more than one concurrent session using cookies!
Is it not better to have multiple cookies when accessing parts of the system
that requires different cookie sessions for security reasons.
Also cookies can be accessed via the URL parameters. So why force only one cookie
for all sessions!
*/
if (isset($_COOKIE[$config->get('session_name')])) {
$session_id = $_COOKIE[$config->get('session_name')];
} else {
$session_id = '';
}
$session->start($session_id);
setcookie($config->get('session_name'), $session->getId(), ini_get('session.cookie_lifetime'), ini_get('session.cookie_path'), ini_get('session.cookie_domain'));
}
// Cache
$registry->set('cache', new Cache($config->get('cache_engine'), $config->get('cache_expire')));
// Url
if ($config->get('url_autostart')) {
$registry->set('url', new Url($config->get('site_url'), $config->get('site_ssl')));
}
// Language
$language = new Language($config->get('language_directory'));
$registry->set('language', $language);
// Document
$registry->set('document', new Document());
// Config Autoload
if ($config->has('config_autoload')) {
foreach ($config->get('config_autoload') as $value) {
$loader->config($value);
}
}
// Language Autoload
if ($config->has('language_autoload')) {
foreach ($config->get('language_autoload') as $value) {
$loader->language($value);
}
}
// Library Autoload
if ($config->has('library_autoload')) {
foreach ($config->get('library_autoload') as $value) {
$loader->library($value);
}
}
// Model Autoload
if ($config->has('model_autoload')) {
foreach ($config->get('model_autoload') as $value) {
$loader->model($value);
}
}
// Route
$route = new Router($registry);
// Pre Actions
if ($config->has('action_pre_action')) {
foreach ($config->get('action_pre_action') as $value) {
$route->addPreAction(new Action($value));
}
}
// Dispatch
$route->dispatch(new Action($config->get('action_router')), new Action($config->get('action_error')));
// Output
$response->output();
另一个文件是,catalog/controller/startup/startup.php:
<?php
class ControllerStartupStartup extends Controller {
public function __isset($key) {
// To make sure that calls to isset also support dynamic properties from the registry
// See https://www.php.net/manual/en/la ... ng.php#object.isset
if ($this->registry) {
if ($this->registry->get($key)!==null) {
return true;
}
}
return false;
}
public function index() {
// Store
if ($this->request->server['HTTPS']) {
$query = $this->db->query("SELECT * FROM " . DB_PREFIX . "store WHERE REPLACE(`ssl`, 'www.', '') = '" . $this->db->escape('https://' . str_replace('www.', '', $_SERVER['HTTP_HOST']) . rtrim(dirname($_SERVER['PHP_SELF']), '/.\\') . '/') . "'");
} else {
$query = $this->db->query("SELECT * FROM " . DB_PREFIX . "store WHERE REPLACE(`url`, 'www.', '') = '" . $this->db->escape('http://' . str_replace('www.', '', $_SERVER['HTTP_HOST']) . rtrim(dirname($_SERVER['PHP_SELF']), '/.\\') . '/') . "'");
}
if (isset($this->request->get['store_id'])) {
$this->config->set('config_store_id', (int)$this->request->get['store_id']);
} else if ($query->num_rows) {
$this->config->set('config_store_id', $query->row['store_id']);
} else {
$this->config->set('config_store_id', 0);
}
if (!$query->num_rows) {
$this->config->set('config_url', HTTP_SERVER);
$this->config->set('config_ssl', HTTPS_SERVER);
}
// Settings
$query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "setting` WHERE store_id = '0' OR store_id = '" . (int)$this->config->get('config_store_id') . "' ORDER BY store_id ASC");
foreach ($query->rows as $result) {
if (!$result['serialized']) {
$this->config->set($result['key'], $result['value']);
} else {
$this->config->set($result['key'], json_decode($result['value'], true));
}
}
// Set time zone
if ($this->config->get('config_timezone')) {
date_default_timezone_set($this->config->get('config_timezone'));
// Sync PHP and DB time zones.
$this->db->query("SET time_zone = '" . $this->db->escape(date('P')) . "'");
}
// Theme
$this->config->set('template_cache', $this->config->get('developer_theme'));
// Url
$this->registry->set('url', new Url($this->config->get('config_url'), $this->config->get('config_ssl')));
// Language
$code = '';
$this->load->model('localisation/language');
$languages = $this->model_localisation_language->getLanguages();
if (isset($this->session->data['language'])) {
$code = $this->session->data['language'];
}
if (isset($this->request->cookie['language']) && !array_key_exists($code, $languages)) {
$code = $this->request->cookie['language'];
}
// Language Detection
if (!empty($this->request->server['HTTP_ACCEPT_LANGUAGE']) && !array_key_exists($code, $languages)) {
$detect = '';
$browser_languages = explode(',', $this->request->server['HTTP_ACCEPT_LANGUAGE']);
// Try using local to detect the language
foreach ($browser_languages as $browser_language) {
foreach ($languages as $key => $value) {
if ($value['status']) {
$locale = explode(',', $value['locale']);
if (in_array($browser_language, $locale)) {
$detect = $key;
break 2;
}
}
}
}
if (!$detect) {
// Try using language folder to detect the language
foreach ($browser_languages as $browser_language) {
if (array_key_exists(strtolower($browser_language), $languages)) {
$detect = strtolower($browser_language);
break;
}
}
}
$code = $detect ? $detect : '';
}
if (!array_key_exists($code, $languages)) {
$code = $this->config->get('config_language');
}
if (!isset($this->session->data['language']) || $this->session->data['language'] != $code) {
$this->session->data['language'] = $code;
}
if (!isset($this->request->cookie['language']) || $this->request->cookie['language'] != $code) {
setcookie('language', $code, time() + 60 * 60 * 24 * 30, '/', $this->request->server['HTTP_HOST']);
}
// Overwrite the default language object
$language = new Language($code);
$language->load($code);
$this->registry->set('language', $language);
// Set the config language_id
$this->config->set('config_language_id', $languages[$code]['language_id']);
// Customer
$customer = new Cart\Customer($this->registry);
$this->registry->set('customer', $customer);
// Customer Group
if (isset($this->session->data['customer']) && isset($this->session->data['customer']['customer_group_id'])) {
// For API calls
$this->config->set('config_customer_group_id', $this->session->data['customer']['customer_group_id']);
} elseif ($this->customer->isLogged()) {
// Logged in customers
$this->config->set('config_customer_group_id', $this->customer->getGroupId());
} elseif (isset($this->session->data['guest']) && isset($this->session->data['guest']['customer_group_id'])) {
$this->config->set('config_customer_group_id', $this->session->data['guest']['customer_group_id']);
} else {
$this->config->set('config_customer_group_id', $this->config->get('config_customer_group_id'));
}
// Tracking Code
if (isset($this->request->get['tracking'])) {
setcookie('tracking', $this->request->get['tracking'], time() + 3600 * 24 * 1000, '/');
$this->db->query("UPDATE `" . DB_PREFIX . "marketing` SET clicks = (clicks + 1) WHERE code = '" . $this->db->escape($this->request->get['tracking']) . "'");
}
// Currency
$code = '';
$this->load->model('localisation/currency');
$currencies = $this->model_localisation_currency->getCurrencies();
if (isset($this->session->data['currency'])) {
$code = $this->session->data['currency'];
}
if (isset($this->request->cookie['currency']) && !array_key_exists($code, $currencies)) {
$code = $this->request->cookie['currency'];
}
if (!array_key_exists($code, $currencies)) {
$code = $this->config->get('config_currency');
}
if (!isset($this->session->data['currency']) || $this->session->data['currency'] != $code) {
$this->session->data['currency'] = $code;
}
if (!isset($this->request->cookie['currency']) || $this->request->cookie['currency'] != $code) {
setcookie('currency', $code, time() + 60 * 60 * 24 * 30, '/', $this->request->server['HTTP_HOST']);
}
$this->registry->set('currency', new Cart\Currency($this->registry));
// Tax
$this->registry->set('tax', new Cart\Tax($this->registry));
// PHP v7.4+ validation compatibility.
if (isset($this->session->data['shipping_address']['country_id']) && isset($this->session->data['shipping_address']['zone_id'])) {
$this->tax->setShippingAddress($this->session->data['shipping_address']['country_id'], $this->session->data['shipping_address']['zone_id']);
} elseif ($this->config->get('config_tax_default') == 'shipping') {
$this->tax->setShippingAddress($this->config->get('config_country_id'), $this->config->get('config_zone_id'));
}
if (isset($this->session->data['payment_address']['country_id']) && isset($this->session->data['payment_address']['zone_id'])) {
$this->tax->setPaymentAddress($this->session->data['payment_address']['country_id'], $this->session->data['payment_address']['zone_id']);
} elseif ($this->config->get('config_tax_default') == 'payment') {
$this->tax->setPaymentAddress($this->config->get('config_country_id'), $this->config->get('config_zone_id'));
}
$this->tax->setStoreAddress($this->config->get('config_country_id'), $this->config->get('config_zone_id'));
// Weight
$this->registry->set('weight', new Cart\Weight($this->registry));
// Length
$this->registry->set('length', new Cart\Length($this->registry));
// Cart
$this->registry->set('cart', new Cart\Cart($this->registry));
// Encryption
$this->registry->set('encryption', new Encryption($this->config->get('config_encryption')));
}
}
反馈完毕
非常感谢你分享了完整的 system/framework.php 和 catalog/controller/startup/startup.php 文件内容。你提的问题非常专业,我现在来给你详细解释并结合你的实际代码,判断是否还建议你手动在 startup.php 中添加 seo_url_lang_prefix 控制器。
✅ 结论:是的,仍然建议使用方法一(在 startup.php 添加 $this->load->controller(...)),原因如下: 🔍 一、关于你的疑问:framework.php 没有出现 seo_url 加载?你是对的:在你提供的 system/framework.php 文件中,并没有显式调用 seo_url 或任何 startup controller。这是因为:
- OpenCart 实际通过 config->get('action_pre_action') 获取所有“前置执行”的控制器。
- 它们在下面这一段中执行:
// Pre Actionsif ($config->has('action_pre_action')) { foreach ($config->get('action_pre_action') as $value) { $route->addPreAction(new Action($value)); }}🔁 也就是说:OpenCart 的 SEO URL 是在 action_pre_action 配置项中注册的事件控制器之一,比如:
'action_pre_action' => [ 'startup/session', 'startup/startup', 'startup/seo_url', // 就是它在默认配置里出现 ...]而这个配置通常存在于:
- config.php
- 或者某个启动类 system/config/default.php
你可以打开 system/config/default.php 找找看是否有这一段。
🧩 你的实际问题关键是:你创建了一个新的 startup 控制器:
catalog/controller/startup/seo_url_lang_prefix.php
它是你自己写的,用于识别 /en/ 或 /es/ 前缀,并设置语言。
🔧 但它 没有被注册到 action_pre_action 中,所以不会被自动执行! ✅ 为什么仍推荐用 $this->load->controller(...) 手动加载?
你分享的 catalog/controller/startup/startup.php 明确显示,这是 OpenCart 的启动控制器之一,已经在 action_pre_action 中执行。也就是说:
🔄 把你自己的 controller 加到这个文件末尾,是最安全直接的加载方式!
// 在 startup.php 的 index() 函数最后一行加:
$this->load->controller('startup/seo_url_lang_prefix');
这相当于给它“注册”到了前置流程中。
🔒 方法一的优势[td]方法 | 优点 | 缺点 | 手动添加到 startup.php | 快速、兼容所有版本、便于调试 | 修改核心文件(建议做标记) | 通过 action_pre_action 注册事件 | 符合 OpenCart 原生架构 | 配置繁琐,容易遗漏,不直观 |
✅ 推荐操作步骤- 在 catalog/controller/startup/startup.php 的 index() 函数末尾加上:
$this->load->controller('startup/seo_url_lang_prefix');- 清除缓存(modification 缓存和系统缓存)
- 测试访问:
- https://oc.indmu.com/en/ 是否切到英语首页
- https://oc.indmu.com/es/ 是否切到西语首页
- https://oc.indmu.com/ 是否重定向到默认语言 /en/
🧪 如果你坚持不改核心文件
那就需要通过 catalog/config.php 或 XML 安装事件注册:
$config->set('action_pre_action',
[ 'startup/startup',
'startup/seo_url_lang_prefix',
...
]);
但这样做需要确保顺序没问题,维护成本较高,不推荐在开发初期这么做。
✅ 总结
你当前的 OpenCart 版本中:
- 没有通过 framework.php 直接加载 SEO 控制器
- 也没有自动执行你自定义的语言前缀控制器
因此:
✅ 是的,强烈建议你手动在 startup.php 的末尾添加:
$this->load->controller('startup/seo_url_lang_prefix'); 这样做最简单、最直接、最兼容。
如果你加完以后访问 /en/ 或 /es/ 仍是 404,我们下一步会排查 .htaccess 和 seo_url_lang_prefix 控制器逻辑。准备好告诉我下一步的测试结果即可。
|