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:
@@ -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
@@ -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
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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}%");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user