Initial commit: X-Server Mobile enterprise tool system

- Frontend: React 18 + TypeScript + Vite + Ant Design Mobile
- Backend: PHP 8.5 + Webman 2.x + MySQL + Redis
- i18n support: Chinese, English, Russian, Spanish, French
- Approval workflow engine
- Docker development environment
- Feishu integration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-02-24 17:57:25 +08:00
commit 8d78e18267
121 changed files with 18563 additions and 0 deletions
@@ -0,0 +1,134 @@
<?php
namespace App\Controller;
use App\Service\ApprovalService;
use Webman\Http\Request;
use Webman\Http\Response;
class ApprovalController extends BaseController
{
private ApprovalService $approvalService;
public function __construct()
{
$this->approvalService = new ApprovalService();
}
/**
* 获取审批列表
*/
public function list(Request $request): Response
{
$type = $request->input('type', 'my');
$page = (int) $request->input('page', 1);
$pageSize = (int) $request->input('page_size', 20);
$userId = $this->getUserId($request);
if (!$userId) {
return $this->error('用户未登录', 401);
}
$result = $this->approvalService->getList($userId, $type, $page, $pageSize);
return $this->success($result);
}
/**
* 获取审批详情
*/
public function detail(Request $request): Response
{
$id = $request->route('id');
$order = \App\Model\ApprovalOrder::with(['flow', 'applicant', 'records.approver'])
->findOrFail($id);
return $this->success($order);
}
/**
* 提交审批
*/
public function submit(Request $request): Response
{
$data = $request->all();
$userId = $this->getUserId($request);
if (!$userId) {
return $this->error('用户未登录', 401);
}
$data['applicant_id'] = $userId;
try {
$order = $this->approvalService->submit($data);
return $this->success($order);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* 审批通过
*/
public function approve(Request $request): Response
{
$id = $request->route('id');
$comment = $request->post('comment');
$userId = $this->getUserId($request);
if (!$userId) {
return $this->error('用户未登录', 401);
}
try {
$order = $this->approvalService->approve((int) $id, $userId, $comment);
return $this->success($order);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* 审批拒绝
*/
public function reject(Request $request): Response
{
$id = $request->route('id');
$comment = $request->post('comment');
$userId = $this->getUserId($request);
if (!$userId) {
return $this->error('用户未登录', 401);
}
try {
$order = $this->approvalService->reject((int) $id, $userId, $comment);
return $this->success($order);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* 撤销申请
*/
public function cancel(Request $request): Response
{
$id = $request->route('id');
$userId = $this->getUserId($request);
if (!$userId) {
return $this->error('用户未登录', 401);
}
try {
$order = $this->approvalService->cancel((int) $id, $userId);
return $this->success($order);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace App\Controller;
use App\Util\JwtUtil;
use Webman\Http\Request;
use Webman\Http\Response;
class AuthController extends BaseController
{
/**
* 用户登录
*/
public function login(Request $request): Response
{
$username = $request->post('username');
$password = $request->post('password');
// TODO: 实现真实的登录逻辑
if (!$username || !$password) {
return $this->error('用户名或密码不能空');
}
// 模拟用户数据
$user = [
'user_id' => 1,
'username' => $username,
'name' => '测试用户',
'company_id' => 1,
];
$token = JwtUtil::encode($user);
return $this->success([
'token' => $token,
'user' => $user,
]);
}
/**
* 飞书免登
*/
public function feishuLogin(Request $request): Response
{
$authCode = $request->post('auth_code');
$tenantKey = $request->post('tenant_key');
// TODO: 实现飞书免登逻辑
// 1. 根据 tenant_key 获取公司配置
// 2. 使用 auth_code 换取用户信息
// 3. 创建或更新用户
// 4. 生成 JWT Token
return $this->success([
'token' => JwtUtil::encode(['user_id' => 1, 'company_id' => 1]),
'user' => ['id' => 1, 'name' => '飞书用户', 'company_id' => 1],
]);
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace App\Controller;
use Webman\Http\Request;
use Webman\Http\Response;
abstract class BaseController
{
/**
* 成功响应
*/
protected function success($data = null, string $message = 'success'): Response
{
return json([
'code' => 0,
'message' => $message,
'data' => $data,
]);
}
/**
* 错误响应
*/
protected function error(string $message = 'error', int $code = 1, $data = null): Response
{
return json([
'code' => $code,
'message' => $message,
'data' => $data,
]);
}
/**
* 获取当前用户ID
*/
protected function getUserId(Request $request): ?int
{
return $request->userId ?? null;
}
}
@@ -0,0 +1,60 @@
<?php
namespace App\Controller;
use Webman\Http\Request;
use Webman\Http\Response;
class BusinessTripController extends BaseController
{
/**
* 获取出差列表
*/
public function list(Request $request): Response
{
$type = $request->input('type', 'my');
// TODO: 从数据库获取出差列表
return $this->success([
'list' => [],
'total' => 0,
]);
}
/**
* 获取出差详情
*/
public function detail(Request $request): Response
{
$id = $request->route('id');
// TODO: 从数据库获取出差详情
return $this->success([
'id' => $id,
'title' => '测试出差',
]);
}
/**
* 提交出差申请
*/
public function apply(Request $request): Response
{
$data = $request->all();
// TODO: 保存出差数据并创建审批流程
return $this->success();
}
/**
* 更新出差申请
*/
public function update(Request $request): Response
{
$id = $request->route('id');
$data = $request->all();
// TODO: 更新出差数据
return $this->success();
}
}
+62
View File
@@ -0,0 +1,62 @@
<?php
namespace App\Controller;
use Webman\Http\Request;
use Webman\Http\Response;
class UserController extends BaseController
{
/**
* 获取用户信息
*/
public function info(Request $request): Response
{
$userId = $this->getUserId($request);
// TODO: 从数据库获取用户信息
return $this->success([
'id' => $userId,
'name' => '测试用户',
'email' => 'test@example.com',
'company_id' => 1,
]);
}
/**
* 获取用户菜单权限
*/
public function menu(Request $request): Response
{
// TODO: 根据用户权限返回菜单
return $this->success([
['id' => 1, 'name' => '首页', 'path' => '/dashboard'],
['id' => 2, 'name' => '出差审批', 'path' => '/business-trip'],
]);
}
/**
* 获取用户可切换的公司列表
*/
public function companies(Request $request): Response
{
$userId = $this->getUserId($request);
// TODO: 从数据库获取用户关联的公司列表
return $this->success([
['id' => 1, 'name' => '公司A'],
]);
}
/**
* 切换当前公司
*/
public function switchCompany(Request $request): Response
{
$companyId = $request->post('company_id');
// TODO: 更新用户当前公司
return $this->success();
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace App\Model;
use support\Model;
class ApprovalFlow extends Model
{
protected $table = 'approval_flow';
protected $fillable = [
'business_type',
'name',
'description',
'status',
];
protected $casts = [
'status' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* 关联审批节点
*/
public function nodes()
{
return $this->hasMany(ApprovalNode::class, 'flow_id')->orderBy('order');
}
/**
* 获取启用的流程
*/
public function scopeActive($query)
{
return $query->where('status', 1);
}
/**
* 根据业务类型获取流程
*/
public function scopeByBusinessType($query, string $businessType)
{
return $query->where('business_type', $businessType);
}
}
+79
View File
@@ -0,0 +1,79 @@
<?php
namespace App\Model;
use support\Model;
class ApprovalNode extends Model
{
protected $table = 'approval_node';
protected $fillable = [
'flow_id',
'type',
'name',
'order',
'approvers',
'conditions',
];
protected $casts = [
'approvers' => 'array',
'conditions' => 'array',
'order' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* 节点类型常量
*/
const TYPE_APPROVER = 'approver'; // 审批人(单人)
const TYPE_AND_APPROVE = 'and_approve'; // 会签(需所有人通过)
const TYPE_OR_APPROVE = 'or_approve'; // 或签(任一人通过)
const TYPE_NOTIFY = 'notify'; // 抄送(通知用)
/**
* 关联流程
*/
public function flow()
{
return $this->belongsTo(ApprovalFlow::class, 'flow_id');
}
/**
* 获取指定顺序的节点
*/
public function scopeByOrder($query, int $order)
{
return $query->where('order', $order);
}
/**
* 判断是否是审批节点
*/
public function isApprovalNode(): bool
{
return in_array($this->type, [
self::TYPE_APPROVER,
self::TYPE_AND_APPROVE,
self::TYPE_OR_APPROVE,
]);
}
/**
* 判断是否是会签
*/
public function isAndApprove(): bool
{
return $this->type === self::TYPE_AND_APPROVE;
}
/**
* 判断是否是或签
*/
public function isOrApprove(): bool
{
return $this->type === self::TYPE_OR_APPROVE;
}
}
+91
View File
@@ -0,0 +1,91 @@
<?php
namespace App\Model;
use support\Model;
class ApprovalOrder extends Model
{
protected $table = 'approval_order';
protected $fillable = [
'business_type',
'business_id',
'title',
'applicant_id',
'status',
'current_nodes',
'flow_id',
'form_data',
'attachments',
];
protected $casts = [
'current_nodes' => 'array',
'form_data' => 'array',
'attachments' => 'array',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* 审批状态常量
*/
const STATUS_DRAFT = 'draft'; // 草稿
const STATUS_PENDING = 'pending'; // 审批中
const STATUS_APPROVED = 'approved'; // 已通过
const STATUS_REJECTED = 'rejected'; // 已拒绝
const STATUS_CANCELLED = 'cancelled'; // 已撤销
/**
* 关联流程
*/
public function flow()
{
return $this->belongsTo(ApprovalFlow::class, 'flow_id');
}
/**
* 关联申请人
*/
public function applicant()
{
return $this->belongsTo(User::class, 'applicant_id');
}
/**
* 关联审批记录
*/
public function records()
{
return $this->hasMany(ApprovalRecord::class, 'order_id')->orderBy('created_at');
}
/**
* 判断是否可以编辑
*/
public function canEdit(): bool
{
return $this->status === self::STATUS_DRAFT;
}
/**
* 判断是否可以审批
*/
public function canApprove(): bool
{
return $this->status === self::STATUS_PENDING;
}
/**
* 判断是否已结束
*/
public function isEnded(): bool
{
return in_array($this->status, [
self::STATUS_APPROVED,
self::STATUS_REJECTED,
self::STATUS_CANCELLED,
]);
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
namespace App\Model;
use support\Model;
class ApprovalRecord extends Model
{
protected $table = 'approval_record';
protected $fillable = [
'order_id',
'node_id',
'node_type',
'approver_id',
'action',
'comment',
];
protected $casts = [
'created_at' => 'datetime',
];
/**
* 操作类型常量
*/
const ACTION_SUBMIT = 'submit'; // 提交
const ACTION_APPROVE = 'approve'; // 通过
const ACTION_REJECT = 'reject'; // 拒绝
const ACTION_CANCEL = 'cancel'; // 撤销
/**
* 关联审批单
*/
public function order()
{
return $this->belongsTo(ApprovalOrder::class, 'order_id');
}
/**
* 关联审批人
*/
public function approver()
{
return $this->belongsTo(User::class, 'approver_id');
}
/**
* 判断是否是批准操作
*/
public function isApproved(): bool
{
return $this->action === self::ACTION_APPROVE;
}
/**
* 判断是否是拒绝操作
*/
public function isRejected(): bool
{
return $this->action === self::ACTION_REJECT;
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace App\Model;
use support\Model;
class Company extends Model
{
protected $table = 'company';
protected $fillable = [
'name',
'feishu_app_id',
'feishu_app_secret',
'feishu_tenant_key',
'status',
];
protected $casts = [
'status' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
protected $hidden = [
'feishu_app_secret',
];
/**
* 获取启用的公司
*/
public function scopeActive($query)
{
return $query->where('status', 1);
}
/**
* 根据飞书租户Key查找
*/
public function scopeByFeishuTenantKey($query, string $tenantKey)
{
return $query->where('feishu_tenant_key', $tenantKey);
}
/**
* 根据飞书AppID查找
*/
public function scopeByFeishuAppId($query, string $appId)
{
return $query->where('feishu_app_id', $appId);
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace App\Model;
use support\Model;
class User extends Model
{
protected $table = 'user';
protected $fillable = [
'company_id',
'feishu_user_id',
'feishu_union_id',
'name',
'email',
'phone',
'avatar',
'status',
];
protected $casts = [
'company_id' => 'integer',
'status' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* 关联公司
*/
public function company()
{
return $this->belongsTo(Company::class, 'company_id');
}
/**
* 获取启用的用户
*/
public function scopeActive($query)
{
return $query->where('status', 1);
}
/**
* 根据飞书用户ID查找
*/
public function scopeByFeishuUserId($query, string $feishuUserId)
{
return $query->where('feishu_user_id', $feishuUserId);
}
/**
* 根据飞书 UnionID 查找
*/
public function scopeByFeishuUnionId($query, string $feishuUnionId)
{
return $query->where('feishu_union_id', $feishuUnionId);
}
}
+302
View File
@@ -0,0 +1,302 @@
<?php
namespace App\Service;
use App\Model\ApprovalFlow;
use App\Model\ApprovalNode;
use App\Model\ApprovalOrder;
use App\Model\ApprovalRecord;
use App\Model\User;
use Illuminate\Database\DatabaseManager;
use App\Util\Db;
use Illuminate\Database\Connection;
class ApprovalService
{
private Connection $db;
public function __construct()
{
// Initialize database if not already initialized
Db::init();
$this->db = Db::connection();
}
/**
* 提交审批
*/
public function submit(array $data): ApprovalOrder
{
return $this->db->transaction(function () use ($data) {
// 1. 获取审批流程
$flow = ApprovalFlow::active()
->byBusinessType($data['business_type'])
->with('nodes')
->findOrFail($data['flow_id']);
// 2. 创建审批单
$order = ApprovalOrder::create([
'business_type' => $data['business_type'],
'business_id' => $data['business_id'],
'title' => $data['title'],
'applicant_id' => $data['applicant_id'],
'status' => ApprovalOrder::STATUS_PENDING,
'current_nodes' => $this->getFirstNodeIds($flow),
'flow_id' => $flow->id,
'form_data' => $data['form_data'] ?? [],
'attachments' => $data['attachments'] ?? [],
]);
// 3. 记录提交操作
ApprovalRecord::create([
'order_id' => $order->id,
'node_id' => $flow->nodes->first()->id ?? 0,
'node_type' => 'submit',
'approver_id' => $data['applicant_id'],
'action' => ApprovalRecord::ACTION_SUBMIT,
'comment' => $data['comment'] ?? null,
]);
return $order->load(['flow', 'applicant']);
});
}
/**
* 审批通过
*/
public function approve(int $orderId, int $approverId, ?string $comment = null): ApprovalOrder
{
return $this->db->transaction(function () use ($orderId, $approverId, $comment) {
$order = ApprovalOrder::with(['flow.nodes'])->findOrFail($orderId);
if (!$order->canApprove()) {
throw new \Exception('当前状态无法审批');
}
$currentNodeIds = $order->current_nodes ?? [];
$currentNode = ApprovalNode::whereIn('id', $currentNodeIds)->first();
if (!$currentNode) {
throw new \Exception('当前节点不存在');
}
// 记录审批操作
ApprovalRecord::create([
'order_id' => $order->id,
'node_id' => $currentNode->id,
'node_type' => $currentNode->type,
'approver_id' => $approverId,
'action' => ApprovalRecord::ACTION_APPROVE,
'comment' => $comment,
]);
// 判断节点是否完成
if ($this->isNodeCompleted($order->id, $currentNode->id)) {
// 移动到下一节点
$nextNodes = $this->getNextNodes($order->flow, $currentNode->order);
if (empty($nextNodes)) {
// 审批完成
$order->status = ApprovalOrder::STATUS_APPROVED;
$order->current_nodes = [];
} else {
$order->current_nodes = collect($nextNodes)->pluck('id')->toArray();
}
$order->save();
}
return $order->fresh(['flow', 'applicant', 'records']);
});
}
/**
* 审批拒绝
*/
public function reject(int $orderId, int $approverId, ?string $comment = null): ApprovalOrder
{
return $this->db->transaction(function () use ($orderId, $approverId, $comment) {
$order = ApprovalOrder::findOrFail($orderId);
if (!$order->canApprove()) {
throw new \Exception('当前状态无法审批');
}
$currentNodeIds = $order->current_nodes ?? [];
$currentNode = ApprovalNode::whereIn('id', $currentNodeIds)->first();
// 记录拒绝操作
ApprovalRecord::create([
'order_id' => $order->id,
'node_id' => $currentNode->id ?? 0,
'node_type' => $currentNode->type ?? 'unknown',
'approver_id' => $approverId,
'action' => ApprovalRecord::ACTION_REJECT,
'comment' => $comment,
]);
// 更新状态为已拒绝
$order->status = ApprovalOrder::STATUS_REJECTED;
$order->current_nodes = [];
$order->save();
return $order->fresh(['flow', 'applicant', 'records']);
});
}
/**
* 撤销申请
*/
public function cancel(int $orderId, int $userId): ApprovalOrder
{
$order = ApprovalOrder::findOrFail($orderId);
if ($order->applicant_id !== $userId) {
throw new \Exception('只有申请人可以撤销');
}
if (!$order->canEdit() && !$order->canApprove()) {
throw new \Exception('当前状态无法撤销');
}
// 记录撤销操作
ApprovalRecord::create([
'order_id' => $order->id,
'node_id' => 0,
'node_type' => 'cancel',
'approver_id' => $userId,
'action' => ApprovalRecord::ACTION_CANCEL,
'comment' => '申请人撤销',
]);
$order->status = ApprovalOrder::STATUS_CANCELLED;
$order->current_nodes = [];
$order->save();
return $order->fresh(['flow', 'applicant', 'records']);
}
/**
* 获取审批列表
*/
public function getList(int $userId, string $type = 'my', int $page = 1, int $pageSize = 20): array
{
$query = ApprovalOrder::with(['flow', 'applicant']);
switch ($type) {
case 'my':
$query->where('applicant_id', $userId);
break;
case 'pending':
// 获取用户待审批的订单
$pendingOrderIds = $this->getPendingOrderIds($userId);
$query->whereIn('id', $pendingOrderIds);
break;
case 'approved':
// 获取用户已审批的订单
$approvedOrderIds = ApprovalRecord::where('approver_id', $userId)
->where('action', '!=', ApprovalRecord::ACTION_SUBMIT)
->pluck('order_id')
->unique()
->toArray();
$query->whereIn('id', $approvedOrderIds);
break;
}
$total = $query->count();
$list = $query->orderBy('created_at', 'desc')
->offset(($page - 1) * $pageSize)
->limit($pageSize)
->get();
return [
'list' => $list,
'total' => $total,
'page' => $page,
'page_size' => $pageSize,
];
}
/**
* 获取第一个节点ID列表
*/
private function getFirstNodeIds(ApprovalFlow $flow): array
{
return $flow->nodes
->where('order', 0)
->pluck('id')
->toArray();
}
/**
* 判断节点是否完成
*/
private function isNodeCompleted(int $orderId, int $nodeId): bool
{
$node = ApprovalNode::findOrFail($nodeId);
$records = ApprovalRecord::where('order_id', $orderId)
->where('node_id', $nodeId)
->where('action', ApprovalRecord::ACTION_APPROVE)
->get();
$approvers = $node->approvers ?? [];
if ($node->type === ApprovalNode::TYPE_AND_APPROVE) {
// 会签:需要所有人通过
return count($records) >= count($approvers);
} elseif ($node->type === ApprovalNode::TYPE_OR_APPROVE) {
// 或签:任一人通过即可
return count($records) >= 1;
} else {
// 单人审批:需要一人通过
return count($records) >= 1;
}
}
/**
* 获取下一节点
*/
private function getNextNodes(ApprovalFlow $flow, int $currentOrder): array
{
return $flow->nodes
->where('order', '>', $currentOrder)
->sortBy('order')
->values()
->toArray();
}
/**
* 获取用户待审批的订单ID列表
*/
private function getPendingOrderIds(int $userId): array
{
// 获取用户参与的待审批订单
$pendingOrders = ApprovalOrder::where('status', ApprovalOrder::STATUS_PENDING)
->whereNotNull('current_nodes')
->get();
$result = [];
foreach ($pendingOrders as $order) {
$currentNodeIds = $order->current_nodes ?? [];
$nodes = ApprovalNode::whereIn('id', $currentNodeIds)->get();
foreach ($nodes as $node) {
$approvers = $node->approvers ?? [];
if (in_array($userId, $approvers)) {
// 检查是否已审批
$hasApproved = ApprovalRecord::where('order_id', $order->id)
->where('node_id', $node->id)
->where('approver_id', $userId)
->exists();
if (!$hasApproved) {
$result[] = $order->id;
break;
}
}
}
}
return $result;
}
}
+252
View File
@@ -0,0 +1,252 @@
<?php
namespace App\Service;
use App\Model\Company;
use App\Model\User;
use support\Cache;
class FeishuService
{
private ?string $accessToken = null;
private ?int $companyId = null;
/**
* 设置当前操作的公司
*/
public function setCompany(int $companyId): self
{
$this->companyId = $companyId;
$this->accessToken = null;
return $this;
}
/**
* 根据 tenant_key 获取公司配置
*/
public function getCompanyByTenantKey(string $tenantKey): ?Company
{
return Company::active()->byFeishuTenantKey($tenantKey)->first();
}
/**
* 获取飞书 access_token
*/
public function getAccessToken(): string
{
if ($this->accessToken) {
return $this->accessToken;
}
if (!$this->companyId) {
throw new \Exception('未设置公司ID');
}
$company = Company::findOrFail($this->companyId);
// 从缓存获取
$cacheKey = "feishu:access_token:{$company->id}";
$token = Cache::get($cacheKey);
if ($token) {
$this->accessToken = $token;
return $token;
}
// 请求新的 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,
]);
if ($response['code'] !== 0) {
throw new \Exception('获取飞书 token 失败: ' . ($response['msg'] ?? '未知错误'));
}
$token = $response['tenant_access_token'];
$expire = $response['expire'] ?? 7200;
// 缓存 token(提前5分钟过期)
Cache::set($cacheKey, $token, $expire - 300);
$this->accessToken = $token;
return $token;
}
/**
* 根据 auth_code 获取用户信息
*/
public function getUserInfo(string $authCode): array
{
if (!$this->companyId) {
throw new \Exception('未设置公司ID');
}
$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,
'code' => $authCode,
], [
'Authorization: Bearer ' . $token,
]);
if ($response['code'] !== 0) {
throw new \Exception('获取用户信息失败: ' . ($response['msg'] ?? '未知错误'));
}
return $response['data'] ?? [];
}
/**
* 创建或更新用户
*/
public function createOrUpdateUser(array $feishuData, int $companyId): User
{
$unionId = $feishuData['union_id'] ?? null;
$userId = $feishuData['user_id'] ?? null;
$name = $feishuData['name'] ?? '未知';
$avatar = $feishuData['avatar_url'] ?? null;
$email = $feishuData['email'] ?? null;
if (!$unionId) {
throw new \Exception('缺少 union_id');
}
// 查找或创建用户
$user = User::byFeishuUnionId($unionId)->first();
if (!$user) {
$user = User::create([
'company_id' => $companyId,
'feishu_user_id' => $userId,
'feishu_union_id' => $unionId,
'name' => $name,
'email' => $email,
'avatar' => $avatar,
'status' => 1,
]);
} else {
// 更新用户信息
$user->update([
'feishu_user_id' => $userId,
'name' => $name,
'avatar' => $avatar,
'email' => $email,
]);
}
return $user;
}
/**
* 发送审批消息通知
*/
public function sendApprovalMessage(int $userId, string $title, string $url): bool
{
try {
$user = User::findOrFail($userId);
$company = Company::findOrFail($user->company_id);
$this->setCompany($company->id);
$token = $this->getAccessToken();
// 构建消息内容
$message = [
'msg_type' => 'interactive',
'card' => [
'header' => [
'title' => [
'content' => '审批通知',
'tag' => 'plain_text',
],
'template' => 'blue',
],
'elements' => [
[
'tag' => 'div',
'text' => [
'content' => $title,
'tag' => 'plain_text',
],
],
[
'tag' => 'action',
'actions' => [
[
'tag' => 'button',
'text' => [
'content' => '查看详情',
'tag' => 'plain_text',
],
'type' => 'default',
'url' => $url,
],
],
],
],
],
];
// 发送消息
$response = $this->httpPost(
"https://open.feishu.cn/open-apis/message/v4/send_messages",
[
'receive_id_type' => 'user_id',
'receive_id' => $user->feishu_user_id,
'msg_type' => 'interactive',
'content' => json_encode(['card' => $message['card']]),
],
[
'Authorization: Bearer ' . $token,
]
);
return ($response['code'] ?? -1) === 0;
} catch (\Exception $e) {
// 记录错误但不中断流程
error_log('发送飞书消息失败: ' . $e->getMessage());
return false;
}
}
/**
* HTTP POST 请求
*/
private function httpPost(string $url, array $data, array $headers = []): array
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$defaultHeaders = [
'Content-Type: application/json',
];
curl_setopt($ch, CURLOPT_HTTPHEADER, array_merge($defaultHeaders, $headers));
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new \Exception("HTTP 请求失败: {$httpCode}");
}
$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception('JSON 解析失败');
}
return $result;
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace App\Util;
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Events\Dispatcher;
use Illuminate\Container\Container;
use Illuminate\Database\Eloquent\Model as EloquentModel;
class Db
{
private static ?Capsule $capsule = null;
public static function init(): void
{
if (self::$capsule !== null) {
return;
}
$config = config('database');
self::$capsule = new Capsule();
// Add connection
self::$capsule->addConnection([
'driver' => 'mysql',
'host' => getenv('DB_HOST') ?: '127.0.0.1',
'port' => getenv('DB_PORT') ?: '3306',
'database' => getenv('DB_DATABASE') ?: 'x_server_mobile',
'username' => getenv('DB_USERNAME') ?: 'root',
'password' => getenv('DB_PASSWORD') ?: '',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
]);
// Set event dispatcher
self::$capsule->setEventDispatcher(new Dispatcher(new Container()));
// Make this Capsule instance available globally via static methods
self::$capsule->setAsGlobal();
// Setup the Eloquent ORM
self::$capsule->bootEloquent();
}
public static function connection(?string $name = null): \Illuminate\Database\Connection
{
if (self::$capsule === null) {
self::init();
}
return self::$capsule->connection($name);
}
public static function table(string $table): \Illuminate\Database\Query\Builder
{
return self::connection()->table($table);
}
public static function raw(string $value): \Illuminate\Database\Query\Expression
{
return self::connection()->raw($value);
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace App\Util;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Exception;
class JwtUtil
{
private static string $secret = 'your-secret-key-change-in-production';
private static int $expire = 86400 * 7; // 7 days
/**
* 生成 Token
*/
public static function encode(array $data): string
{
$payload = array_merge($data, [
'iat' => time(),
'exp' => time() + self::$expire,
]);
return JWT::encode($payload, self::$secret, 'HS256');
}
/**
* 解析 Token
*/
public static function decode(string $token): ?array
{
try {
$decoded = JWT::decode($token, new Key(self::$secret, 'HS256'));
return (array) $decoded;
} catch (Exception $e) {
return null;
}
}
/**
* 验证 Token
*/
public static function verify(string $token): bool
{
return self::decode($token) !== null;
}
}
+170
View File
@@ -0,0 +1,170 @@
<?php
namespace App\Util;
/**
* 多语言翻译类
* 支持:中文(zh)、英文(en)、俄语(ru)、西语(es)、法语(fr)
*/
class Lang
{
private static ?array $translations = null;
private static string $locale = 'zh';
private static string $fallbackLocale = 'zh';
/**
* 支持的语言列表
*/
public const SUPPORTED_LOCALES = ['zh', 'en', 'ru', 'es', 'fr'];
/**
* 语言名称映射
*/
public const LOCALE_NAMES = [
'zh' => '简体中文',
'en' => 'English',
'ru' => 'Русский',
'es' => 'Español',
'fr' => 'Français',
];
/**
* 初始化翻译
*/
private static function loadTranslations(): void
{
if (self::$translations !== null) {
return;
}
self::$translations = [];
foreach (self::SUPPORTED_LOCALES as $locale) {
$file = base_path() . "/lang/{$locale}.php";
if (file_exists($file)) {
self::$translations[$locale] = require $file;
}
}
}
/**
* 设置当前语言
*/
public static function setLocale(string $locale): void
{
if (in_array($locale, self::SUPPORTED_LOCALES)) {
self::$locale = $locale;
}
}
/**
* 获取当前语言
*/
public static function getLocale(): string
{
return self::$locale;
}
/**
* 从请求中获取语言
*/
public static function detectFromRequest(): string
{
// 从请求头获取语言
$request = \request();
$acceptLanguage = $request->header('Accept-Language');
if ($acceptLanguage) {
// 解析 Accept-Language 头
$locales = explode(',', $acceptLanguage);
foreach ($locales as $locale) {
$locale = trim(strstr($locale, ';', true));
$locale = substr($locale, 0, 2);
if (in_array($locale, self::SUPPORTED_LOCALES)) {
return $locale;
}
}
}
// 从查询参数获取
$lang = $request->input('lang');
if ($lang && in_array($lang, self::SUPPORTED_LOCALES)) {
return $lang;
}
return self::$fallbackLocale;
}
/**
* 翻译文本
*
* @param string $key 翻译键,使用点号分隔,如 'common.confirm'
* @param array $params 替换参数
* @param string|null $locale 指定语言,null 则使用当前语言
* @return string 翻译后的文本
*/
public static function get(string $key, array $params = [], ?string $locale = null): string
{
self::loadTranslations();
$locale = $locale ?? self::$locale;
// 获取翻译值
$value = self::getTranslationValue($key, $locale);
// 替换参数
if (!empty($params)) {
foreach ($params as $search => $replace) {
$value = str_replace(':' . $search, $replace, $value);
$value = str_replace('{' . $search . '}', $replace, $value);
}
}
return $value;
}
/**
* 获取翻译值
*/
private static function getTranslationValue(string $key, string $locale): string
{
$keys = explode('.', $key);
$value = self::$translations[$locale] ?? [];
foreach ($keys as $k) {
if (isset($value[$k])) {
$value = $value[$k];
} else {
// 尝试使用回退语言
if ($locale !== self::$fallbackLocale) {
return self::getTranslationValue($key, self::$fallbackLocale);
}
return $key;
}
}
return is_string($value) ? $value : $key;
}
/**
* 判断翻译是否存在
*/
public static function has(string $key, ?string $locale = null): bool
{
self::loadTranslations();
$locale = $locale ?? self::$locale;
$keys = explode('.', $key);
$value = self::$translations[$locale] ?? [];
foreach ($keys as $k) {
if (isset($value[$k])) {
$value = $value[$k];
} else {
return false;
}
}
return true;
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace App\Middleware;
use App\Util\JwtUtil;
use Webman\Http\Request;
use Webman\Http\Response;
use support\Log;
class AuthMiddleware
{
public function process(Request $request, callable $handler): Response
{
// 获取 Token
$token = $request->header('Authorization');
$token = str_replace('Bearer ', '', $token);
if (!$token || !JwtUtil::verify($token)) {
return json([
'code' => 401,
'message' => 'Unauthorized',
'data' => null,
], 401);
}
// 解析 Token 并设置用户信息到请求
$payload = JwtUtil::decode($token);
if ($payload) {
$request->userId = $payload['user_id'] ?? null;
$request->companyId = $payload['company_id'] ?? null;
}
return $handler($request);
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Middleware;
use Webman\Http\Request;
use Webman\Http\Response;
class CorsMiddleware
{
public function process(Request $request, callable $handler): Response
{
// 处理 OPTIONS 预检请求
if ($request->method() === 'OPTIONS') {
$response = response('');
} else {
$response = $handler($request);
}
// 添加 CORS 头
$response->withHeader('Access-Control-Allow-Origin', '*')
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
->withHeader('Access-Control-Max-Age', '86400');
return $response;
}
}