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:
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user