feat: add multi-company Feishu integration and data migration

- Add companies table enhancements (abbr, tenant_key, feishu_enabled)
- Add users table enhancements (feishu fields, sn, report_to, etc.)
- Create company_feishu_apps table for multi-app per company
- Create user_sn_pool table for user number management
- Update FeishuService to support multiple Feishu apps per company
- Add data migration script (002_merge_old_data.sql)
- Add import script (scripts/import_old_data.php)
- Update User and Company models with new scopes and relationships
- Add CompanyFeishuApp model
- Add Git workflow documentation (GIT_WORKFLOW.md)

This supports:
- Multi-company architecture with Feishu integration
- Multiple Feishu apps per company (X-Server, Ticket, CRM)
- User number assignment system
- Data migration from old project

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-02-24 18:12:18 +08:00
parent 8d78e18267
commit 1f6d346f6a
8 changed files with 875 additions and 35 deletions
+4
View File
@@ -48,3 +48,7 @@ docker-volumes/
# Lock files (optional - uncomment if you want to exclude)
# package-lock.json
# composer.lock
# Large SQL files (import data files)
*.sql
!database/migrations/*.sql
+171
View File
@@ -0,0 +1,171 @@
# Git 工作流
## 分支策略
采用 Git Flow 简化版工作流:
### 主要分支
| 分支 | 用途 | 说明 |
|------|------|------|
| `master` | 生产环境 | 保持稳定,只接受从 develop 合并的代码 |
| `develop` | 开发环境 | 主开发分支,包含最新的开发功能 |
| `feature/*` | 功能分支 | 从 develop 分支,开发完成后合并回 develop |
| `hotfix/*` | 热修复分支 | 从 master 分支,修复后合并到 master 和 develop |
## 分支命名规范
### 功能分支
```
feature/功能名称
例如:feature/i18n-support
```
### 热修复分支
```
hotfix/问题描述
例如:hotfix/login-error
```
## 工作流程
### 1. 开发新功能
```bash
# 从 develop 创建功能分支
git checkout develop
git pull origin develop
git checkout -b feature/your-feature-name
# 开发并提交
git add .
git commit -m "feat: add your feature"
# 推送到远程
git push -u origin feature/your-feature-name
# 完成后合并到 develop
git checkout develop
git merge feature/your-feature-name
git push origin develop
# 删除功能分支(可选)
git branch -d feature/your-feature-name
```
### 2. 修复紧急问题
```bash
# 从 master 创建热修复分支
git checkout master
git pull origin master
git checkout -b hotfix/your-fix-name
# 修复并提交
git add .
git commit -m "fix: describe the fix"
# 推送到远程
git push -u origin hotfix/your-fix-name
# 合并到 master 和 develop
git checkout master
git merge hotfix/your-fix-name
git push origin master
git checkout develop
git merge hotfix/your-fix-name
git push origin develop
```
### 3. 发布到生产
```bash
# 将 develop 合并到 master
git checkout master
git merge develop
git push origin master
# 打标签
git tag -a v1.0.0 -m "Release version 1.0.0"
git push origin v1.0.0
```
## 提交信息规范
使用 Conventional Commits 规范:
| 类型 | 说明 | 示例 |
|------|------|------|
| `feat` | 新功能 | `feat: add user login` |
| `fix` | 修复bug | `fix: correct i18n loading` |
| `docs` | 文档变更 | `docs: update README` |
| `style` | 代码格式 | `style: format code` |
| `refactor` | 重构 | `refactor: simplify API` |
| `test` | 测试 | `test: add unit tests` |
| `chore` | 构建/工具 | `chore: update dependencies` |
格式:
```
<type>(<scope>): <subject>
<body>
<footer>
```
## 当前分支状态
| 分支 | 状态 | 说明 |
|------|------|------|
| `master` | ✅ 稳定 | 生产代码(含5语言支持) |
| `develop` | ✅ 活跃 | 主开发分支(含5语言支持) |
## 常用命令
```bash
# 查看所有分支
git branch -a
# 查看当前分支
git branch --show-current
# 切换分支
git checkout <branch-name>
# 创建并切换到新分支
git checkout -b <new-branch>
# 删除本地分支
git branch -d <branch-name>
# 删除远程分支
git push origin --delete <branch-name>
# 查看分支差异
git diff develop
# 查看分支树
git log --graph --oneline --all
```
## 发布流程
1. **开发阶段**:在 `develop` 分支进行开发
2. **测试阶段**:创建 `release` 分支进行测试
3. **发布阶段**:合并到 `master` 并打标签
4. **维护阶段**:使用 `hotfix` 分支修复紧急问题
## 注意事项
1. **永远不要直接在 master 分支开发**
2. **功能分支必须从 develop 分支创建**
3. **提交前先拉取最新代码**
4. **保持提交信息清晰规范**
5. **及时删除已合并的功能分支**
## 远程仓库
- 仓库地址:http://219.157.255.213:25311/xinyouhe/x-server-mobile.git
- 默认分支:develop(建议设置)
- Pull Request:http://219.157.255.213:25311/xinyouhe/x-server-mobile/compare/master...develop
+54 -13
View File
@@ -6,25 +6,32 @@ use support\Model;
class Company extends Model
{
protected $table = 'company';
protected $table = 'companies';
protected $fillable = [
'name',
'feishu_app_id',
'feishu_app_secret',
'feishu_tenant_key',
'abbr',
'tenant_key',
'sort',
'status',
'feishu_enabled',
];
protected $casts = [
'sort' => 'integer',
'status' => 'integer',
'feishu_enabled' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
protected $hidden = [
'feishu_app_secret',
];
/**
* 关联用户
*/
public function users()
{
return $this->hasMany(User::class, 'company_id');
}
/**
* 获取启用的公司
@@ -35,18 +42,52 @@ class Company extends Model
}
/**
* 根据飞书租户Key查找
* 根据租户Key查找
*/
public function scopeByFeishuTenantKey($query, string $tenantKey)
public function scopeByTenantKey($query, string $tenantKey)
{
return $query->where('feishu_tenant_key', $tenantKey);
return $query->where('tenant_key', $tenantKey);
}
/**
* 根据飞书AppID查找
* 根据缩写查找
*/
public function scopeByFeishuAppId($query, string $appId)
public function scopeByAbbr($query, string $abbr)
{
return $query->where('feishu_app_id', $appId);
return $query->where('abbr', $abbr);
}
/**
* 搜索公司
*/
public function scopeSearch($query, ?string $keyword)
{
if (!$keyword) {
return $query;
}
return $query->where(function ($q) use ($keyword) {
$q->where('name', 'like', "%{$keyword}%")
->orWhere('abbr', 'like', "%{$keyword}%");
});
}
/**
* 获取飞书应用配置
*/
public function feishuApps()
{
return $this->hasMany(CompanyFeishuApp::class, 'company_id');
}
/**
* 获取默认飞书应用(X-Server)
*/
public function getDefaultFeishuApp()
{
return $this->hasOne(CompanyFeishuApp::class, 'company_id')
->where('system_id', 1)
->where('status', 1)
->orderBy('sort');
}
}
+91
View File
@@ -0,0 +1,91 @@
<?php
namespace App\Model;
use support\Model;
class CompanyFeishuApp extends Model
{
protected $table = 'company_feishu_apps';
protected $fillable = [
'company_id',
'system_id',
'name',
'tenant_key',
'app_id',
'app_secret',
'encrypt_key',
'verification_token',
'base_uri',
'callback_url',
'mobile_callback_url',
'scopes',
'state',
'sort',
'status',
];
protected $casts = [
'company_id' => 'integer',
'system_id' => 'integer',
'sort' => 'integer',
'status' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
protected $hidden = [
'app_secret',
'encrypt_key',
'verification_token',
];
/**
* 关联公司
*/
public function company()
{
return $this->belongsTo(Company::class, 'company_id');
}
/**
* 获取启用的应用
*/
public function scopeActive($query)
{
return $query->where('status', 1);
}
/**
* 根据系统ID查找
*/
public function scopeBySystemId($query, int $systemId)
{
return $query->where('system_id', $systemId);
}
/**
* 获取X-Server应用
*/
public function scopeXServer($query)
{
return $query->where('system_id', 1);
}
/**
* 获取工单应用
*/
public function scopeTicket($query)
{
return $query->where('system_id', 2);
}
/**
* 获取CRM应用
*/
public function scopeCrm($query)
{
return $query->where('system_id', 3);
}
}
+81 -7
View File
@@ -6,26 +6,42 @@ use support\Model;
class User extends Model
{
protected $table = 'user';
protected $table = 'users';
protected $fillable = [
'company_id',
'feishu_user_id',
'feishu_union_id',
'sn',
'report_to',
'name',
'avatar',
'sex',
'email',
'phone',
'avatar',
'password',
'status',
'user_type',
'union_id',
'feishu_user_id',
'feishu_open_id',
'tenant_key',
'position',
'sign_img',
];
protected $casts = [
'company_id' => 'integer',
'report_to' => 'integer',
'sex' => 'integer',
'status' => 'integer',
'user_type' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
protected $hidden = [
'password',
];
/**
* 关联公司
*/
@@ -34,6 +50,22 @@ class User extends Model
return $this->belongsTo(Company::class, 'company_id');
}
/**
* 上级用户
*/
public function parent()
{
return $this->belongsTo(User::class, 'report_to');
}
/**
* 下级用户
*/
public function children()
{
return $this->hasMany(User::class, 'report_to');
}
/**
* 获取启用的用户
*/
@@ -42,19 +74,61 @@ class User extends Model
return $query->where('status', 1);
}
/**
* 根据公司ID查找
*/
public function scopeByCompany($query, int $companyId)
{
return $query->where('company_id', $companyId);
}
/**
* 根据飞书用户ID查找
*/
public function scopeByFeishuUserId($query, string $feishuUserId)
public function scopeByFeishuUserId($query, ?string $feishuUserId)
{
if ($feishuUserId === null) {
return $query;
}
return $query->where('feishu_user_id', $feishuUserId);
}
/**
* 根据飞书 UnionID 查找
*/
public function scopeByFeishuUnionId($query, string $feishuUnionId)
public function scopeByFeishuUnionId($query, ?string $feishuUnionId)
{
return $query->where('feishu_union_id', $feishuUnionId);
if ($feishuUnionId === null) {
return $query;
}
return $query->where('union_id', $feishuUnionId);
}
/**
* 根据OpenID查找
*/
public function scopeByFeishuOpenId($query, ?string $openId)
{
if ($openId === null) {
return $query;
}
return $query->where('feishu_open_id', $openId);
}
/**
* 搜索用户
*/
public function scopeSearch($query, ?string $keyword)
{
if (!$keyword) {
return $query;
}
return $query->where(function ($q) use ($keyword) {
$q->where('name', 'like', "%{$keyword}%")
->orWhere('email', 'like', "%{$keyword}%")
->orWhere('phone', 'like', "%{$keyword}%")
->orWhere('sn', 'like', "%{$keyword}%");
});
}
}
+70 -15
View File
@@ -4,12 +4,14 @@ namespace App\Service;
use App\Model\Company;
use App\Model\User;
use App\Util\Db;
use support\Cache;
class FeishuService
{
private ?string $accessToken = null;
private ?int $companyId = null;
private ?int $appId = null;
/**
* 设置当前操作的公司
@@ -21,12 +23,72 @@ class FeishuService
return $this;
}
/**
* 设置飞书应用ID(支持多应用)
*/
public function setApp(int $appId): self
{
$this->appId = $appId;
$this->accessToken = null;
return $this;
}
/**
* 根据 tenant_key 获取公司配置
*/
public function getCompanyByTenantKey(string $tenantKey): ?Company
{
return Company::active()->byFeishuTenantKey($tenantKey)->first();
return Company::active()->byTenantKey($tenantKey)->first();
}
/**
* 获取公司的飞书应用配置
*/
private function getFeishuApp(int $appId): array
{
$sql = "SELECT * FROM company_feishu_apps WHERE id = ? AND status = 1";
$stmt = Db::connection()->getPdo()->prepare($sql);
$stmt->execute([$appId]);
$app = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$app) {
throw new \Exception('飞书应用配置不存在');
}
return $app;
}
/**
* 获取公司的默认飞书应用
*/
private function getDefaultFeishuApp(int $companyId): array
{
$sql = "SELECT * FROM company_feishu_apps WHERE company_id = ? AND system_id = 1 AND status = 1 ORDER BY sort ASC LIMIT 1";
$stmt = Db::connection()->getPdo()->prepare($sql);
$stmt->execute([$companyId]);
$app = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$app) {
throw new \Exception('未配置飞书应用');
}
return $app;
}
/**
* 获取飞书应用配置(优先使用指定应用,否则使用默认应用)
*/
private function getAppConfig(): array
{
if (!$this->companyId) {
throw new \Exception('未设置公司ID');
}
if ($this->appId) {
return $this->getFeishuApp($this->appId);
}
return $this->getDefaultFeishuApp($this->companyId);
}
/**
@@ -38,14 +100,10 @@ class FeishuService
return $this->accessToken;
}
if (!$this->companyId) {
throw new \Exception('未设置公司ID');
}
$company = Company::findOrFail($this->companyId);
$app = $this->getAppConfig();
// 从缓存获取
$cacheKey = "feishu:access_token:{$company->id}";
$cacheKey = "feishu:access_token:{$app['id']}";
$token = Cache::get($cacheKey);
if ($token) {
@@ -55,8 +113,8 @@ class FeishuService
// 请求新的 token
$response = $this->httpPost('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', [
'app_id' => $company->feishu_app_id,
'app_secret' => $company->feishu_app_secret,
'app_id' => $app['app_id'],
'app_secret' => $app['app_secret'],
]);
if ($response['code'] !== 0) {
@@ -78,17 +136,14 @@ class FeishuService
*/
public function getUserInfo(string $authCode): array
{
if (!$this->companyId) {
throw new \Exception('未设置公司ID');
}
$app = $this->getAppConfig();
$token = $this->getAccessToken();
// 获取用户 access_token
$response = $this->httpPost('https://open.feishu.cn/open-apis/authen/v1/oidc/access_token', [
'grant_type' => 'authorization_code',
'client_id' => Company::findOrFail($this->companyId)->feishu_app_id,
'client_secret' => Company::findOrFail($this->companyId)->feishu_app_secret,
'client_id' => $app['app_id'],
'client_secret' => $app['app_secret'],
'code' => $authCode,
], [
'Authorization: Bearer ' . $token,
@@ -0,0 +1,134 @@
-- 合并旧项目数据到新项目
-- 执行前请确保已备份数据库
-- 1. 更新 companies 表,添加飞书相关字段
ALTER TABLE `companies`
ADD COLUMN `abbr` varchar(50) NOT NULL DEFAULT '' COMMENT '公司缩写' AFTER `name`,
ADD COLUMN `tenant_key` varchar(36) NOT NULL DEFAULT '' COMMENT '飞书企业标识' AFTER `abbr`,
ADD COLUMN `sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序' AFTER `tenant_key`,
ADD COLUMN `feishu_enabled` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否启用飞书';
-- 2. 更新 users 表,添加飞书相关字段
ALTER TABLE `users`
ADD COLUMN `sn` varchar(36) NOT NULL DEFAULT '' COMMENT '用户编号' AFTER `id`,
ADD COLUMN `report_to` bigint(20) UNSIGNED NULL DEFAULT 0 COMMENT '上级用户ID' AFTER `sn`,
ADD COLUMN `avatar` varchar(255) NULL DEFAULT NULL COMMENT '头像' AFTER `phone`,
ADD COLUMN `sex` tinyint(2) NULL DEFAULT NULL COMMENT '性别:1:男,2:女' AFTER `avatar`,
ADD COLUMN `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态' AFTER `sex`,
ADD COLUMN `user_type` tinyint(2) NOT NULL DEFAULT 1 COMMENT '用户类型 1内部2外部' AFTER `status`,
ADD COLUMN `union_id` varchar(60) NULL DEFAULT NULL COMMENT 'ISV的唯一标识' AFTER `user_type`,
ADD COLUMN `feishu_user_id` varchar(60) NULL DEFAULT NULL COMMENT '飞书user_id' AFTER `union_id`,
ADD COLUMN `feishu_open_id` varchar(60) NULL DEFAULT NULL COMMENT '飞书open_id' AFTER `feishu_user_id`,
ADD COLUMN `tenant_key` varchar(60) NULL DEFAULT NULL COMMENT '当前企业标识' AFTER `feishu_open_id`,
ADD COLUMN `position` varchar(128) NULL DEFAULT NULL COMMENT '职位' AFTER `tenant_key`,
ADD COLUMN `sign_img` varchar(255) NULL DEFAULT NULL COMMENT '签名图片' AFTER `position`;
-- 3. 创建公司飞书应用配置表
CREATE TABLE IF NOT EXISTS `company_feishu_apps` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
`company_id` int(11) NOT NULL DEFAULT 0 COMMENT '公司ID',
`system_id` int(11) NOT NULL DEFAULT 0 COMMENT '系统ID:1=X-Server, 2=工单, 3=CRM_PC',
`name` varchar(90) NOT NULL DEFAULT '' COMMENT '应用名称',
`tenant_key` varchar(36) NOT NULL DEFAULT '' COMMENT '企业标识',
`app_id` varchar(60) NOT NULL DEFAULT '' COMMENT '飞书应用ID',
`app_secret` varchar(150) NOT NULL DEFAULT '' COMMENT '飞书应用密钥',
`encrypt_key` varchar(60) NULL DEFAULT NULL COMMENT '事件加密密钥',
`verification_token` varchar(60) NULL DEFAULT NULL COMMENT '事件验证令牌',
`base_uri` varchar(255) NOT NULL DEFAULT 'https://open.feishu.cn/open-apis/' COMMENT '飞书API基础地址',
`callback_url` varchar(255) NULL DEFAULT NULL COMMENT 'PC端回调地址',
`mobile_callback_url` varchar(255) NULL DEFAULT NULL COMMENT '移动端回调地址',
`scopes` text NULL COMMENT '授权范围',
`state` varchar(255) NULL DEFAULT NULL COMMENT '携带参数',
`sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序',
`status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态:1启用,0禁用',
`created_at` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间',
`updated_at` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_company_id` (`company_id`) USING BTREE,
KEY `idx_system_id` (`system_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='公司飞书应用配置';
-- 4. 创建用户编号池表
CREATE TABLE IF NOT EXISTS `user_sn_pool` (
`sn` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '用户编号',
`user_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '分配的用户ID',
`status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '状态:0未分配,1已分配',
`created_at` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间',
PRIMARY KEY (`sn`) USING BTREE,
KEY `idx_user_id` (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户编号池';
-- 5. 插入初始编号池数据(从100000到199999)
INSERT IGNORE INTO `user_sn_pool` (`sn`, `status`, `created_at`)
SELECT
100000 + n.n AS sn,
0 AS status,
UNIX_TIMESTAMP() AS created_at
FROM (
SELECT 0 AS n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION
SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9
) t
CROSS JOIN (
SELECT 0 AS n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION
SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9
) t1
CROSS JOIN (
SELECT 0 AS n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
) t2;
-- 6. 创建迁移数据临时表(用于从旧数据迁移)
CREATE TABLE IF NOT EXISTS `_migration_users` (
`id` bigint(20) UNSIGNED NOT NULL,
`sn` varchar(36) NOT NULL DEFAULT '',
`report_to` bigint(20) UNSIGNED NULL DEFAULT 0,
`name` varchar(60) NOT NULL DEFAULT '',
`avatar` varchar(255) NULL DEFAULT NULL,
`sex` tinyint(2) NULL DEFAULT NULL,
`password` varchar(255) NOT NULL DEFAULT '',
`phone` varchar(22) NOT NULL DEFAULT '',
`email` varchar(90) NOT NULL DEFAULT '',
`corp_id` int(11) NOT NULL DEFAULT 0,
`status` tinyint(1) NOT NULL DEFAULT 1,
`user_type` tinyint(2) NOT NULL DEFAULT 1,
`union_id` varchar(60) NULL DEFAULT NULL,
`user_id` varchar(60) NULL DEFAULT NULL,
`open_id` varchar(60) NULL DEFAULT NULL,
`tenant_key` varchar(60) NULL DEFAULT NULL,
`position` varchar(128) NULL DEFAULT NULL,
`sign_img` varchar(255) NULL DEFAULT NULL,
`created_at` int(11) NOT NULL DEFAULT 0,
`updated_at` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `_migration_corp` (
`id` int(11) UNSIGNED NOT NULL,
`name` varchar(255) NOT NULL DEFAULT '',
`abbr` enum('hesu','xinyouhe','paika','simate','mbaf') NOT NULL DEFAULT 'hesu',
`tenant_key` varchar(36) NOT NULL DEFAULT '',
`sort` int(11) NOT NULL DEFAULT 0,
`status` tinyint(1) NOT NULL DEFAULT 1,
`created_at` int(11) NOT NULL DEFAULT 0,
`updated_at` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `_migration_corp_feishu_app` (
`id` int(11) UNSIGNED NOT NULL,
`corp_id` int(11) NOT NULL DEFAULT 0,
`system_id` int(11) NOT NULL DEFAULT 0,
`name` varchar(90) NOT NULL DEFAULT '',
`tenant_key` varchar(36) NOT NULL DEFAULT '',
`app_id` varchar(60) NOT NULL DEFAULT '',
`app_secret` varchar(150) NOT NULL DEFAULT '',
`base_uri` varchar(255) NOT NULL DEFAULT '',
`r_url` varchar(255) NULL DEFAULT NULL,
`m_url` varchar(255) NULL DEFAULT '',
`scopes` text NULL,
`state` varchar(255) NULL DEFAULT NULL,
`sort` int(11) NOT NULL DEFAULT 0,
`status` tinyint(1) NOT NULL DEFAULT 1,
`created_at` int(11) NOT NULL DEFAULT 0,
`updated_at` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+270
View File
@@ -0,0 +1,270 @@
<?php
/**
* 数据迁移脚本
* 从旧项目导入用户、公司和飞书应用数据
*
* 使用方法:
* php scripts/import_old_data.php /path/to/fdesk.sql
*/
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../app/Util/Db.php';
use App\Util\Db;
class DataImporter
{
private $pdo;
private $sourcePdo = null;
public function __construct()
{
// 初始化数据库连接
Db::init();
$this->pdo = Db::connection()->getPdo();
}
/**
* 从SQL文件导入数据
*/
public function importFromSqlFile(string $sqlFile): void
{
echo "开始从 SQL 文件导入数据...\n";
// 创建临时数据库用于导入旧数据
$tempDbName = 'temp_fdesk_import';
try {
// 连接到MySQL服务器(不指定数据库)
$dsn = sprintf(
'mysql:host=%s;port=%s;charset=utf8mb4',
getenv('DB_HOST') ?: 'mysql',
getenv('DB_PORT') ?: '3306'
);
$rootPdo = new PDO($dsn, 'root', getenv('DB_PASSWORD') ?: '123456');
$rootPdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 删除临时数据库(如果存在)
$rootPdo->exec("DROP DATABASE IF EXISTS `{$tempDbName}`");
// 创建临时数据库
$rootPdo->exec("CREATE DATABASE `{$tempDbName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
echo "✓ 临时数据库创建成功\n";
// 导入SQL文件到临时数据库
$sql = file_get_contents($sqlFile);
if ($sql === false) {
throw new Exception("无法读取 SQL 文件");
}
// 移除SQL文件中的创建数据库语句
$sql = preg_replace('/CREATE DATABASE.*?;/is', '', $sql);
$sql = preg_replace('/USE .*?;/is', "USE `{$tempDbName}`;", $sql);
$rootPdo->exec($sql);
echo "✓ SQL 文件导入临时数据库成功\n";
// 连接到临时数据库
$this->sourcePdo = new PDO(
$dsn . ';dbname=' . $tempDbName,
'root',
getenv('DB_PASSWORD') ?: '123456'
);
$this->sourcePdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 开始迁移数据
$this->migrateData();
// 清理临时数据库
$rootPdo->exec("DROP DATABASE IF EXISTS `{$tempDbName}`");
echo "✓ 临时数据库已清理\n";
echo "\n数据迁移完成!\n";
} catch (Exception $e) {
echo "✗ 导入失败: " . $e->getMessage() . "\n";
// 清理临时数据库
if (isset($rootPdo)) {
$rootPdo->exec("DROP DATABASE IF EXISTS `{$tempDbName}`");
}
throw $e;
}
}
/**
* 迁移数据
*/
private function migrateData(): void
{
$this->migrateCompanies();
$this->migrateFeishuApps();
$this->migrateUsers();
}
/**
* 迁移公司数据
*/
private function migrateCompanies(): void
{
echo "\n迁移公司数据...\n";
$stmt = $this->sourcePdo->query("SELECT * FROM corp");
$companies = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($companies as $company) {
$sql = "INSERT INTO companies
(id, name, abbr, tenant_key, sort, status, created_at, updated_at, feishu_enabled)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
abbr = VALUES(abbr),
tenant_key = VALUES(tenant_key)
";
try {
$this->pdo->prepare($sql)->execute([
$company['id'],
$company['name'],
$company['abbr'],
$company['tenant_key'],
$company['sort'],
$company['status'],
$company['created_at'],
$company['updated_at'],
]);
echo " ✓ {$company['name']} (ID: {$company['id']})\n";
} catch (Exception $e) {
echo " ✗ {$company['name']} 导入失败: " . $e->getMessage() . "\n";
}
}
}
/**
* 迁移飞书应用配置
*/
private function migrateFeishuApps(): void
{
echo "\n迁移飞书应用配置...\n";
$stmt = $this->sourcePdo->query("SELECT * FROM corp_feishu_app");
$apps = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($apps as $app) {
$sql = "INSERT INTO company_feishu_apps
(id, company_id, system_id, name, tenant_key, app_id, app_secret,
base_uri, callback_url, mobile_callback_url, scopes, state, sort, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
app_id = VALUES(app_id),
app_secret = VALUES(app_secret)
";
try {
$this->pdo->prepare($sql)->execute([
$app['id'],
$app['corp_id'],
$app['system_id'],
$app['name'],
$app['tenant_key'],
$app['app_id'],
$app['app_secret'],
$app['base_uri'],
$app['r_url'],
$app['m_url'],
$app['scopes'],
$app['state'],
$app['sort'],
$app['status'],
$app['created_at'],
$app['updated_at'],
]);
echo " ✓ {$app['name']} - 公司ID: {$app['corp_id']}\n";
} catch (Exception $e) {
echo " ✗ {$app['name']} 导入失败: " . $e->getMessage() . "\n";
}
}
}
/**
* 迁移用户数据
*/
private function migrateUsers(): void
{
echo "\n迁移用户数据...\n";
$stmt = $this->sourcePdo->query("SELECT * FROM `user` LIMIT 1000");
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($users as $user) {
// 检查用户是否已存在
$checkSql = "SELECT id FROM users WHERE id = ?";
$checkStmt = $this->pdo->prepare($checkSql);
$checkStmt->execute([$user['id']]);
if ($checkStmt->fetch()) {
echo " ⊘ {$user['name']} (ID: {$user['id']}) - 已存在,跳过\n";
continue;
}
$sql = "INSERT INTO users
(id, sn, report_to, name, avatar, sex, password, phone, email, company_id,
status, user_type, union_id, feishu_user_id, feishu_open_id, tenant_key,
position, sign_img, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
";
try {
$this->pdo->prepare($sql)->execute([
$user['id'],
$user['sn'],
$user['report_to'],
$user['name'],
$user['avatar'],
$user['sex'],
$user['password'],
$user['phone'],
$user['email'],
$user['corp_id'],
$user['status'],
$user['user_type'],
$user['union_id'],
$user['user_id'],
$user['open_id'],
$user['tenant_key'],
$user['position'],
$user['sign_img'],
$user['created_at'],
$user['updated_at'],
]);
echo " ✓ {$user['name']} (ID: {$user['id']}, {$user['email']})\n";
} catch (Exception $e) {
echo " ✗ {$user['name']} 导入失败: " . $e->getMessage() . "\n";
}
}
}
}
// 主执行
if ($argc < 2) {
echo "用法: php " . basename(__FILE__) . " <fdesk.sql文件路径>\n";
echo "示例: php " . basename(__FILE__) . " /www/x-server-mobile/fdesk.sql\n";
exit(1);
}
$sqlFile = $argv[1];
if (!file_exists($sqlFile)) {
echo "错误: SQL 文件不存在: {$sqlFile}\n";
exit(1);
}
try {
$importer = new DataImporter();
$importer->importFromSqlFile($sqlFile);
} catch (Exception $e) {
echo "\n错误: " . $e->getMessage() . "\n";
echo "堆栈: " . $e->getTraceAsString() . "\n";
exit(1);
}