把一套传统的 PHP + MySQL IPTV 管理系统,完整迁移到 Cloudflare Workers + D1 边缘计算平台,实现免服务器、全球加速、零运维成本。
📖 前言
传统的 IPTV 管理系统(比如”群晖IPTV”)一般基于 PHP + MySQL 架构,需要:
- 一台 VPS 服务器
- 安装宝塔面板或 LNMP 环境
- 定期维护(更新系统、备份数据、续费服务器)
这套方案的问题:
- ❌ 成本高:VPS 每月至少几十块钱
- ❌ 运维麻烦:系统更新、安全补丁、备份都要管
- ❌ 访问慢:单点服务器,跨地区访问延迟大
- ❌ 容易被打:暴露 IP,被 DDoS 就崩
替代方案:Cloudflare Workers + D1
- ✅ 几乎零成本:Workers 每天 10 万次免费请求,D1 有 5GB 免费存储
- ✅ 全球加速:Cloudflare 全球 300+ 节点,用户就近访问
- ✅ 免运维:不需要管服务器,Cloudflare 全托管
- ✅ 免备案:Workers 默认域名不需要备案(自定义域名看情况)
- ✅ 自动 HTTPS:无需配置证书
代价:
- 需要把 PHP 代码重写成 TypeScript
- 数据库从 MySQL 换成 SQLite(语法有差异)
- 一些 PHP 特性(
gzcompress、openssl_encrypt)需要用 JS 替代
本文完整记录了这次迁移的全过程,供有类似需求的朋友参考。
🏗️ 一、准备工作
1.1 账号和工具
| 名称 | 用途 | 获取方式 |
|---|---|---|
| Cloudflare 账号 | 托管 Workers、D1、自定义域名 | https://dash.cloudflare.com |
| Node.js(≥ 18) | 本地开发和部署 | https://nodejs.org |
| Wrangler CLI | Cloudflare 官方命令行工具 | npm install -g wrangler |
| 代码编辑器 | VS Code 或其他 | https://code.visualstudio.com |
1.2 原系统文件
从原 PHP 项目里你需要准备:
- 源代码(
login.php、data.php、admin.php等) - 数据库导出(
.sql文件,MySQL dump) - 静态资源(
views/css/、views/js/、views/fonts/、views/images/)
🚀 二、创建 Worker 项目
2.1 初始化项目
mkdir iptv-worker
cd iptv-worker
npm init -y
npm install -D wrangler typescript @cloudflare/workers-types
npm install crypto-js
2.2 项目结构
iptv-worker/
├── src/
│ ├── index.ts # Worker 入口
│ ├── crypto.ts # AES + MD5 工具
│ ├── db.ts # D1 数据库封装
│ ├── handlers/
│ │ ├── clientLogin.ts # 客户端登录
│ │ ├── clientData.ts # 客户端数据
│ │ ├── clientMisc.ts # 版本/背景图/IP 查询
│ │ ├── adminAuth.ts # 后台登录
│ │ ├── adminDashboard.ts # 后台首页
│ │ ├── adminUsers.ts # 用户管理
│ │ ├── adminAuthor.ts # 授权管理
│ │ ├── adminException.ts # 异常管理
│ │ ├── adminMeal.ts # 套餐管理
│ │ ├── adminOrder.ts # 订单管理
│ │ ├── adminEpg.ts # EPG 管理
│ │ ├── adminMovie.ts # 点播管理
│ │ ├── adminChannel.ts # 频道管理
│ │ └── adminSys.ts # 系统设置
│ └── html/
│ ├── adminLogin.ts # 登录页 HTML
│ └── adminLayout.ts # 后台通用布局
├── public/ # 静态资源
│ ├── favicon.ico
│ └── views/
│ ├── css/
│ ├── js/
│ ├── fonts/
│ └── images/
├── schema.sql # D1 初始化 SQL
├── convert.js # MySQL → SQLite 转换脚本
├── wrangler.toml # Wrangler 配置
├── tsconfig.json
└── package.json
2.3 wrangler.toml 配置
name = "iptv-worker"
main = "src/index.ts"
compatibility_date = "2024-09-23"
[[d1_databases]]
binding = "DB"
database_name = "iptv_db"
database_id = "你的数据库ID"
[vars]
PANEL_MD5_KEY = "tvkey_"
[assets]
directory = "./public"
binding = "ASSETS"
💾 三、D1 数据库
3.1 创建 D1 数据库
npx wrangler d1 create iptv_db
输出会给出 database_id,把它填到 wrangler.toml 里。
3.2 表结构设计
原 MySQL 的 11 张表:
| 表名 | 用途 |
|---|---|
iptv_admin |
管理员账号 |
iptv_adminrec |
后台操作日志 |
iptv_category |
频道分类 |
iptv_channels |
频道源列表 |
iptv_config |
系统配置项 |
iptv_epg |
电子节目单 |
iptv_meals |
套餐 |
iptv_payment |
订单 |
iptv_users |
用户 |
qhtv_movie |
点播接口 |
eztv_movie |
备用点播接口 |
关键差异(MySQL vs SQLite):
| MySQL | SQLite |
|---|---|
INT(11) AUTO_INCREMENT |
INTEGER PRIMARY KEY AUTOINCREMENT |
VARCHAR(255) |
TEXT |
ENGINE=InnoDB |
无 |
DEFAULT CHARSET=utf8 |
无 |
反引号 `table` |
双引号 "table" 或不加 |
建表 SQL 放在 schema.sql 里,执行:
npx wrangler d1 execute iptv_db --remote --file=./schema.sql
🔄 四、数据迁移(MySQL → SQLite)
这是最繁琐的一步,核心是把 MySQL dump 转换成 SQLite 兼容格式。
4.1 转换脚本 convert.js
写一个 Node.js 脚本,处理以下转换:
const fs = require('fs');
const content = fs.readFileSync(process.argv[2], 'utf8');
// 1. 输出建表语句(手动维护 tableCreates 对象)
// 2. 逐行解析 INSERT INTO 语句
// 3. 处理转义符(\' → '',\" → "")
// 4. 拆分超长 INSERT(避免 SQLITE_TOOBIG)
// 5. 去掉 MySQL 特有语法
关键处理点:
| 问题 | 解决方案 |
|---|---|
转义符 \' |
替换为 ''(SQLite 用双引号转义) |
| 大 INSERT 超限 | 按 500 行拆成小 INSERT |
| 列数不匹配 | 检查原表和 SQLite 表定义是否一致 |
| 中文乱码 | 用 UTF-8 读取文件 |
4.2 导入 D1
# 清空旧数据
npx wrangler d1 execute iptv_db --remote --command="DELETE FROM iptv_admin; DELETE FROM iptv_users; ..."
# 导入新数据
npx wrangler d1 execute iptv_db --remote --file=./d1_import.sql
避坑指南:
- 一次导入失败会全部回滚,所以任何一条 SQL 出错都会导致整体失败
- 遇到
SQLITE_TOOBIG就拆 INSERT - 遇到
UNIQUE constraint failed就先清空表 - 遇到
no such table就先建表
🔐 五、核心加密逻辑
原 PHP 用了 AES-128-ECB 和 MD5,需要完整迁移到 JS。
5.1 MD5 实现
不要自己手写,用 crypto-js:
import MD5 from 'crypto-js/md5';
export function md5(input: string): string {
return MD5(input).toString();
}
踩坑记录:Web Crypto API 不支持 MD5,crypto.subtle.digest('MD5', ...) 会报错。必须用 crypto-js 或 js-md5。
5.2 AES-128-ECB 加密
Web Crypto 只有 AES-CBC,没有 AES-ECB。用 CBC + 全零 IV + 单块加密模拟 ECB:
export async function aesEncrypt(plaintext: string, key: string): Promise<string> {
const keyObj = await importKey(key);
const padded = pkcs7Pad(encoder.encode(plaintext));
const blockSize = 16;
const result = new Uint8Array(padded.length);
const zeroIV = new Uint8Array(blockSize);
for (let i = 0; i < padded.length; i += blockSize) {
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-CBC', iv: zeroIV },
keyObj,
padded.slice(i, i + blockSize)
);
result.set(new Uint8Array(encrypted).slice(0, blockSize), i);
}
return btoa(String.fromCharCode(...result));
}
验证对齐方法:用 PHP 和 Node 分别加密同一段数据,结果必须完全一致。
🎨 六、后台管理系统
6.1 后台页面结构
原来 PHP 用 .php 文件混排 HTML 和逻辑,迁移后改成前后端分离:
- 后端:Worker 输出 HTML 字符串(服务端渲染)
- 前端:HTML 里用原生 JS
fetch调用 API
6.2 通用布局
后台有 10 个页面,全都套用同一个布局(adminLayout.ts),只需要替换中间 <main> 内容。
export function adminLayout(
active: string, // 当前激活的菜单
title: string, // 页面标题
content: string, // 主内容 HTML
username: string, // 登录用户名
dateNow: string // 当前时间
): string {
// 返回完整 HTML 骨架
}
6.3 后台登录(JWT)
原 PHP 用 Session,Workers 是无状态的,改用 JWT (JSON Web Token) 存到 Cookie。
// 登录成功后生成 JWT
const token = await createToken(username, panelKey, remember ? 7 * 86400 : 86400);
// 通过 Set-Cookie 下发
'Set-Cookie': `admin_token=${token}; Path=/; Max-Age=${maxAge}; HttpOnly; SameSite=Lax; Secure`
6.4 后台的 10 个页面
| 页面 | 路径 | 功能 |
|---|---|---|
| 首页 | /admin |
8 个统计卡片 + 频道分类统计 |
| 授权 | /admin/author |
待授权用户列表、批量授权 |
| 用户 | /admin/user |
已授权用户、套餐修改、到期管理 |
| 异常 | /admin/exception |
VPN/设备 ID 更换用户管理 |
| 套餐 | /admin/meal |
套餐增删改 |
| 订单 | /admin/order |
订单列表(只读) |
| EPG | /admin/epg |
EPG 增删改、绑定频道 |
| 点播 | /admin/movie |
点播接口管理 |
| 频道 | /admin/channel |
分类管理 + 频道编辑 |
| 系统 | /admin/sys |
6 个子配置页 |
📱 七、客户端接口
App 需要 5 个核心接口,全都用 AES 加密返回:
| 接口 | 方法 | 作用 |
|---|---|---|
/login.php |
POST | 登录,返回用户状态、套餐、加密的 dataurl |
/data.php |
POST | 返回频道列表(AES 加密) |
/getver.php |
GET | 返回版本信息(JSON) |
/bg.php |
GET | 返回随机背景图 URL |
/getIpInfo.php |
GET | 返回 IP 归属地 |
7.1 登录接口逻辑
1. 从请求体解析 JSON(mac、androidid、model 等)
2. 校验 MAC 是否合法
3. 查询 iptv_users 表
- 存在 → 更新 ip、region、lasttime
- 不存在 → 创建试用账号(根据 trialdays 配置)
4. 检查套餐内容
5. 组装响应 JSON
6. 用 md5 计算 AES 密钥
7. AES 加密返回
7.2 数据接口逻辑
1. 从请求体解析 JSON(androidid、mac、rand)
2. 查询用户,得到 meal
3. 查询套餐的 content(下划线分隔的分类名)
4. 遍历分类,查询每个分类下的频道
5. 按频道名分组(同名频道多个源)
6. 组装 JSON
7. 用 md5 计算 AES 密钥(含 rand 参数)
8. AES 加密返回
7.3 关键的加解密一致性
客户端(App)和服务器(Worker)必须用完全一致的算法:
- 相同密钥推导逻辑:
md5(md5(签名+应用名+包名+固定串) + 应用名 + 包名 + rand) - 相同的 AES 模式(AES-128-ECB)
- 相同的字符替换(PHP 里做了
f/b、t/y互换)
测试方法:把 App 端的加密逻辑用 Node 重写一遍,两边结果对比。
⚙️ 八、静态资源托管
8.1 Cloudflare Assets
在 wrangler.toml 里配置:
[assets]
directory = "./public"
binding = "ASSETS"
Wrangler 会自动把 public/ 下的文件作为静态资源托管,访问路径就是文件相对路径。
8.2 关键:字体文件
踩坑:materialdesignicons.min.css 里用 @font-face 引用了 ../fonts/materialdesignicons.woff2,如果只拷贝 CSS 没拷 fonts,所有图标会显示为方框。
完整拷贝:views/css/、views/js/、views/fonts/、views/images/ 全部要放到 public/views/ 下。
🌐 九、域名和路由
9.1 默认 workers.dev 域名
部署后自动生成 xxx.workers.dev 域名,可以立即使用。
9.2 绑定自定义域名
在 Cloudflare Dashboard 里给 Worker 绑定自定义域名(比如 iptv.example.com),需要:
- 域名 NS 指向 Cloudflare
- Worker → Settings → Triggers → Custom Domains → 添加
好处:
- 更短的 URL
- 可以开启 CDN 缓存
- 可以单独配置防火墙规则
🧪 十、测试清单
10.1 后台
- [ ] 登录页正常显示(有图标、有样式)
- [ ] admin/123456 能登录
- [ ] 首页 8 个统计卡片数字正确
- [ ] 用户列表能翻页、能搜索
- [ ] 授权页能批量授权
- [ ] 频道页能增加分类、编辑频道
- [ ] 系统设置能修改配置
- [ ] 修改密码后能用新密码登录
10.2 客户端
- [ ]
/login.php返回加密字符串 - [ ]
/data.php返回加密字符串(长度正常) - [ ]
/getver.php返回 JSON - [ ]
/bg.php返回图片 URL - [ ]
/getIpInfo.php?ip=myip返回归属地
10.3 App 端
- [ ] App 能连上服务器
- [ ] 能登录成功
- [ ] 能看到频道列表
- [ ] 能播放直播源
🐛 十一、常见问题汇总
| 问题 | 原因 | 解决方案 |
|---|---|---|
Could not resolve "crypto-js/md5" |
未安装依赖 | npm install crypto-js |
no such table: iptv_admin |
数据库未初始化 | 执行 schema.sql |
no such column: secret_key |
表结构不匹配 | ALTER TABLE iptv_admin ADD COLUMN secret_key TEXT |
SQLITE_TOOBIG |
单条 SQL 太长 | 拆分 INSERT(每 500 行) |
UNIQUE constraint failed |
主键冲突 | 先 DELETE FROM 清空 |
unrecognized token: "\" |
MySQL 转义符 | 替换 \' 为 '' |
| MD5 结果不一致 | 算法实现不同 | 用 crypto-js |
| 图标显示方框 | 字体文件缺失 | 拷贝 views/fonts/ |
$.cookie is not a function |
jquery.cookie 缺失 | 引入 CDN |
| 登录后跳转”页面开发中” | 路由未实现 | 补 handleXXX 路由 |
💰 十二、成本分析
| 项目 | 免费额度 | 本系统预估用量 | 是否够用 |
|---|---|---|---|
| Workers 请求 | 10 万次/天 | 几百-几千次 | ✅ |
| Workers CPU | 10ms/请求 | 5-20ms | ✅ |
| D1 读取 | 500 万行/天 | 几万行 | ✅ |
| D1 写入 | 10 万行/天 | 几千行 | ✅ |
| D1 存储 | 5GB | 几十 MB | ✅ |
| Assets 带宽 | 无限 | - | ✅ |
结论:个人/小规模运营完全免费。除非用户量上千、每天几十万请求,才需要考虑付费(Workers Paid $5/月起)。
🎯 十三、总结
迁移的核心工作
- PHP → TypeScript:所有业务逻辑重写
- MySQL → SQLite:表结构、SQL 语法微调
- Session → JWT:无状态登录
- OpenSSL → Web Crypto:加解密重写
- 服务端渲染 → 混合渲染:HTML 用 JS 动态生成
迁移的优势
- ✅ 零服务器:不用买 VPS,不用管系统
- ✅ 全球加速:300+ 节点就近服务
- ✅ 自动扩容:流量突增不宕机
- ✅ 免运维:不用打补丁、不用备份(Cloudflare 有备份机制)
- ✅ 自动 HTTPS:证书自动续期
迁移的挑战
- ❌ 冷启动:第一次部署需要 2-3 小时
- ❌ 调试麻烦:Workers 环境不能像 PHP 一样直接改文件
- ❌ PHP 特性缺失:
gzcompress、openssl_encrypt需要替代
适合的场景
- ✅ IPTV、直播、点播系统
- ✅ API 网关、Webhook 转发
- ✅ 小型后台管理系统
- ✅ 反代、转发、限流
不适合的场景
- ❌ 需要大量文件读写
- ❌ 长连接(WebSocket 虽有但受限)
- ❌ 复杂的图片处理(需要配合 R2)
📚 参考资源
作者注:本文基于真实的 IPTV 系统迁移项目整理,所有代码片段都经过实际验证。如果你也在做类似的迁移,希望这份文档能帮你少踩坑。
最后更新:2026-09-25

💬 评论区 0