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,23 @@
|
||||
# 应用配置
|
||||
APP_DEBUG=true
|
||||
APP_ENV=local
|
||||
|
||||
# 数据库配置
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=x_server_mobile
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
|
||||
# Redis 配置
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DATABASE=0
|
||||
|
||||
# JWT 配置
|
||||
JWT_SECRET=your-secret-key-change-in-production
|
||||
|
||||
# 飞书配置
|
||||
FEISHU_APP_ID=
|
||||
FEISHU_APP_SECRET=
|
||||
@@ -0,0 +1,34 @@
|
||||
FROM php:8.5-cli
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
unzip \
|
||||
git \
|
||||
libzip-dev \
|
||||
libpng-dev \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install PHP extensions
|
||||
RUN docker-php-ext-install zip pdo pdo_mysql bcmath pcntl
|
||||
|
||||
# Install Composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy composer files
|
||||
COPY composer.json composer.lock* ./
|
||||
|
||||
# Install dependencies
|
||||
RUN composer install --no-interaction --optimize-autoloader
|
||||
|
||||
# Copy application files
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8787
|
||||
|
||||
# Start webman in foreground mode
|
||||
CMD ["php", "start.php", "start"]
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "x-server/mobile-backend",
|
||||
"description": "Enterprise Tool System Backend",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"workerman/webman": "^2.0",
|
||||
"workerman/webman-framework": "^2.0",
|
||||
"illuminate/database": "^10.0",
|
||||
"illuminate/pagination": "^10.0",
|
||||
"illuminate/redis": "^10.0",
|
||||
"illuminate/events": "^10.0",
|
||||
"firebase/php-jwt": "^6.10",
|
||||
"vlucas/phpdotenv": "^5.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.5",
|
||||
"phpunit/phpunit": "^10.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-package-install": [
|
||||
"support\\Plugin::install"
|
||||
],
|
||||
"post-package-update": [
|
||||
"support\\Plugin::install"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"audit": {
|
||||
"ignore": {
|
||||
"firebase/php-jwt": "PKSA-y2cr-5h3j-g3ys"
|
||||
}
|
||||
},
|
||||
"allow-plugins": {
|
||||
"*": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+4750
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'debug' => getenv('APP_DEBUG') ?: true,
|
||||
'default_timezone' => 'Asia/Shanghai',
|
||||
'request_class' => support\Request::class,
|
||||
'public_path' => base_path() . '/public',
|
||||
'runtime_path' => base_path() . '/runtime',
|
||||
'controller_suffix' => 'Controller',
|
||||
'controller_reuse' => true,
|
||||
];
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
return new \Illuminate\Container\Container();
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'default' => 'mysql',
|
||||
'connections' => [
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'host' => getenv('DB_HOST') ?: 'mysql',
|
||||
'port' => getenv('DB_PORT') ?: 3306,
|
||||
'database' => getenv('DB_DATABASE') ?: 'x_server_mobile',
|
||||
'username' => getenv('DB_USERNAME') ?: 'root',
|
||||
'password' => getenv('DB_PASSWORD') ?: '123456',
|
||||
'unix_socket' => getenv('DB_SOCKET') ?: '',
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => [
|
||||
\PDO::ATTR_TIMEOUT => 5,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
use support\Log;
|
||||
|
||||
return [
|
||||
'default' => [
|
||||
'handlers' => [
|
||||
[
|
||||
'class' => Monolog\Handler\StreamHandler::class,
|
||||
'constructor' => [
|
||||
runtime_path() . '/logs/workerman.log',
|
||||
Monolog\Logger::DEBUG,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
return [
|
||||
'enable' => true,
|
||||
|
||||
'build_dir' => BASE_PATH . DIRECTORY_SEPARATOR . 'build',
|
||||
|
||||
'phar_filename' => 'webman.phar',
|
||||
|
||||
'phar_format' => Phar::PHAR, // Phar archive format: Phar::PHAR, Phar::TAR, Phar::ZIP
|
||||
|
||||
'phar_compression' => Phar::NONE, // Compression method for Phar archive: Phar::NONE, Phar::GZ, Phar::BZ2
|
||||
|
||||
'bin_filename' => 'webman.bin',
|
||||
|
||||
'signature_algorithm'=> Phar::SHA256, //set the signature algorithm for a phar and apply it. The signature algorithm must be one of Phar::MD5, Phar::SHA1, Phar::SHA256, Phar::SHA512, or Phar::OPENSSL.
|
||||
|
||||
'private_key_file' => '', // The file path for certificate or OpenSSL private key file.
|
||||
|
||||
'exclude_pattern' => '#^(?!.*(composer.json|/.github/|/.idea/|/.git/|/.setting/|/runtime/|/vendor-bin/|/build/|/vendor/webman/admin/))(.*)$#',
|
||||
|
||||
'exclude_files' => [
|
||||
'.env', 'LICENSE', 'composer.json', 'composer.lock', 'start.php', 'webman.phar', 'webman.bin'
|
||||
],
|
||||
|
||||
'custom_ini' => '
|
||||
memory_limit = 256M
|
||||
',
|
||||
];
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'default' => [
|
||||
'host' => getenv('REDIS_HOST') ?: 'redis',
|
||||
'port' => getenv('REDIS_PORT') ?: 6379,
|
||||
'password' => getenv('REDIS_PASSWORD') ?: null,
|
||||
'database' => getenv('REDIS_DATABASE') ?: 0,
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use Webman\Route;
|
||||
|
||||
// API 路由
|
||||
Route::group('/api/v1', function () {
|
||||
// 认证路由
|
||||
Route::post('/auth/login', [App\Controller\AuthController::class, 'login']);
|
||||
Route::post('/auth/feishu-login', [App\Controller\AuthController::class, 'feishuLogin']);
|
||||
|
||||
// 用户路由(需要认证)
|
||||
Route::group(function () {
|
||||
Route::get('/user/info', [App\Controller\UserController::class, 'info']);
|
||||
Route::get('/user/menu', [App\Controller\UserController::class, 'menu']);
|
||||
Route::get('/user/companies', [App\Controller\UserController::class, 'companies']);
|
||||
Route::post('/user/switch-company', [App\Controller\UserController::class, 'switchCompany']);
|
||||
});
|
||||
|
||||
// 审批路由
|
||||
Route::group(function () {
|
||||
Route::get('/approval/list', [App\Controller\ApprovalController::class, 'list']);
|
||||
Route::get('/approval/detail/{id}', [App\Controller\ApprovalController::class, 'detail']);
|
||||
Route::post('/approval/submit', [App\Controller\ApprovalController::class, 'submit']);
|
||||
Route::post('/approval/approve/{id}', [App\Controller\ApprovalController::class, 'approve']);
|
||||
Route::post('/approval/reject/{id}', [App\Controller\ApprovalController::class, 'reject']);
|
||||
Route::post('/approval/cancel/{id}', [App\Controller\ApprovalController::class, 'cancel']);
|
||||
});
|
||||
|
||||
// 出差审批路由
|
||||
Route::group(function () {
|
||||
Route::get('/business-trip/list', [App\Controller\BusinessTripController::class, 'list']);
|
||||
Route::get('/business-trip/detail/{id}', [App\Controller\BusinessTripController::class, 'detail']);
|
||||
Route::post('/business-trip/apply', [App\Controller\BusinessTripController::class, 'apply']);
|
||||
Route::put('/business-trip/{id}', [App\Controller\BusinessTripController::class, 'update']);
|
||||
});
|
||||
});
|
||||
|
||||
// 默认路由
|
||||
Route::fallback(function () {
|
||||
return json(['code' => 404, 'message' => 'Not Found']);
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
/**
|
||||
* Webman server configuration
|
||||
*/
|
||||
use support\Log;
|
||||
|
||||
return [
|
||||
'listen' => 'http://0.0.0.0:8787',
|
||||
'transport' => 'tcp',
|
||||
'context' => [],
|
||||
'name' => 'webman',
|
||||
'count' => (int)shell_exec('nproc') ?: 4,
|
||||
'user' => '',
|
||||
'group' => '',
|
||||
'reusePort' => false,
|
||||
'event_loop' => '',
|
||||
'stop_timeout' => 2,
|
||||
'pid_file' => runtime_path() . '/webman.pid',
|
||||
'max_request' => 1000000,
|
||||
'stdout_file' => runtime_path() . '/logs/stdout.log',
|
||||
'log_file' => runtime_path() . '/logs/workerman.log',
|
||||
'max_package_size' => 10 * 1024 * 1024,
|
||||
];
|
||||
@@ -0,0 +1,196 @@
|
||||
-- 企业内部工具系统 - 数据库初始化脚本
|
||||
-- 数据库: x_server_mobile
|
||||
|
||||
-- =====================================
|
||||
-- 1. 公司表(飞书应用配置)
|
||||
-- =====================================
|
||||
CREATE TABLE IF NOT EXISTS `company` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '公司ID',
|
||||
`name` VARCHAR(100) NOT NULL COMMENT '公司名称',
|
||||
`feishu_app_id` VARCHAR(100) DEFAULT NULL COMMENT '飞书应用ID',
|
||||
`feishu_app_secret` VARCHAR(255) DEFAULT NULL COMMENT '飞书应用密钥',
|
||||
`feishu_tenant_key` VARCHAR(100) DEFAULT NULL COMMENT '飞书租户Key',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1=启用,0=禁用',
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_feishu_app_id` (`feishu_app_id`),
|
||||
KEY `idx_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='公司表';
|
||||
|
||||
-- =====================================
|
||||
-- 2. 用户表(全局用户)
|
||||
-- =====================================
|
||||
CREATE TABLE IF NOT EXISTS `user` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID',
|
||||
`company_id` BIGINT UNSIGNED NOT NULL COMMENT '所属公司ID',
|
||||
`feishu_user_id` VARCHAR(100) DEFAULT NULL COMMENT '飞书用户ID',
|
||||
`feishu_union_id` VARCHAR(100) DEFAULT NULL COMMENT '飞书UnionID(跨应用唯一)',
|
||||
`name` VARCHAR(50) NOT NULL COMMENT '用户名',
|
||||
`email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
|
||||
`phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
|
||||
`avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1=正常,0=禁用',
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_company_id` (`company_id`),
|
||||
KEY `idx_feishu_user_id` (`feishu_user_id`),
|
||||
KEY `idx_feishu_union_id` (`feishu_union_id`),
|
||||
KEY `idx_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
|
||||
|
||||
-- =====================================
|
||||
-- 3. 用户与飞书应用关联表
|
||||
-- =====================================
|
||||
CREATE TABLE IF NOT EXISTS `user_feishu_binding` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
|
||||
`company_id` BIGINT UNSIGNED NOT NULL COMMENT '公司ID',
|
||||
`feishu_open_id` VARCHAR(100) DEFAULT NULL COMMENT '飞书OpenID',
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_user_company` (`user_id`, `company_id`),
|
||||
KEY `idx_feishu_open_id` (`feishu_open_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户飞书应用绑定表';
|
||||
|
||||
-- =====================================
|
||||
-- 4. 审批流程配置表
|
||||
-- =====================================
|
||||
CREATE TABLE IF NOT EXISTS `approval_flow` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '流程ID',
|
||||
`business_type` VARCHAR(50) NOT NULL COMMENT '业务类型标识(如:business-trip, purchase)',
|
||||
`name` VARCHAR(100) NOT NULL COMMENT '流程名称',
|
||||
`description` VARCHAR(255) DEFAULT NULL COMMENT '流程描述',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1=启用,0=禁用',
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_business_type` (`business_type`),
|
||||
KEY `idx_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批流程配置表';
|
||||
|
||||
-- =====================================
|
||||
-- 5. 审批流程节点表
|
||||
-- =====================================
|
||||
CREATE TABLE IF NOT EXISTS `approval_node` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '节点ID',
|
||||
`flow_id` BIGINT UNSIGNED NOT NULL COMMENT '流程ID',
|
||||
`type` ENUM('approver', 'and_approve', 'or_approve', 'notify') NOT NULL COMMENT '节点类型',
|
||||
`name` VARCHAR(100) NOT NULL COMMENT '节点名称',
|
||||
`order` INT NOT NULL DEFAULT 0 COMMENT '节点顺序',
|
||||
`approvers` JSON DEFAULT NULL COMMENT '审批人列表(JSON数组)',
|
||||
`conditions` JSON DEFAULT NULL COMMENT '条件判断(JSON)',
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_flow_id` (`flow_id`),
|
||||
KEY `idx_order` (`order`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批流程节点表';
|
||||
|
||||
-- =====================================
|
||||
-- 6. 审批单表
|
||||
-- =====================================
|
||||
CREATE TABLE IF NOT EXISTS `approval_order` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '审批单ID',
|
||||
`business_type` VARCHAR(50) NOT NULL COMMENT '业务类型',
|
||||
`business_id` BIGINT UNSIGNED NOT NULL COMMENT '业务单据ID',
|
||||
`title` VARCHAR(200) NOT NULL COMMENT '审批标题',
|
||||
`applicant_id` BIGINT UNSIGNED NOT NULL COMMENT '申请人ID',
|
||||
`status` ENUM('draft', 'pending', 'approved', 'rejected', 'cancelled') NOT NULL DEFAULT 'draft' COMMENT '审批状态',
|
||||
`current_nodes` JSON DEFAULT NULL COMMENT '当前待审批节点ID列表',
|
||||
`flow_id` BIGINT UNSIGNED NOT NULL COMMENT '使用的流程ID',
|
||||
`form_data` JSON DEFAULT NULL COMMENT '业务表单数据(JSON)',
|
||||
`attachments` JSON DEFAULT NULL COMMENT '附件列表(JSON)',
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_business` (`business_type`, `business_id`),
|
||||
KEY `idx_applicant` (`applicant_id`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批单表';
|
||||
|
||||
-- =====================================
|
||||
-- 7. 审批记录表(审批轨迹)
|
||||
-- =====================================
|
||||
CREATE TABLE IF NOT EXISTS `approval_record` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '记录ID',
|
||||
`order_id` BIGINT UNSIGNED NOT NULL COMMENT '审批单ID',
|
||||
`node_id` BIGINT UNSIGNED NOT NULL COMMENT '节点ID',
|
||||
`node_type` VARCHAR(20) NOT NULL COMMENT '节点类型',
|
||||
`approver_id` BIGINT UNSIGNED NOT NULL COMMENT '审批人ID',
|
||||
`action` ENUM('submit', 'approve', 'reject', 'cancel') NOT NULL COMMENT '操作类型',
|
||||
`comment` VARCHAR(500) DEFAULT NULL COMMENT '审批意见',
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_order_id` (`order_id`),
|
||||
KEY `idx_approver_id` (`approver_id`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批记录表';
|
||||
|
||||
-- =====================================
|
||||
-- 8. 出差申请表
|
||||
-- =====================================
|
||||
CREATE TABLE IF NOT EXISTS `business_trip` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '出差ID',
|
||||
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '申请人ID',
|
||||
`reason` VARCHAR(500) NOT NULL COMMENT '出差事由',
|
||||
`start_date` DATE NOT NULL COMMENT '开始日期',
|
||||
`end_date` DATE NOT NULL COMMENT '结束日期',
|
||||
`destination` VARCHAR(200) NOT NULL COMMENT '目的地',
|
||||
`estimated_cost` DECIMAL(10,2) DEFAULT 0.00 COMMENT '预计费用',
|
||||
`status` ENUM('draft', 'pending', 'approved', 'rejected', 'cancelled') NOT NULL DEFAULT 'draft' COMMENT '状态',
|
||||
`approval_order_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '关联审批单ID',
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_dates` (`start_date`, `end_date`),
|
||||
KEY `idx_approval_order_id` (`approval_order_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='出差申请表';
|
||||
|
||||
-- =====================================
|
||||
-- 9. 出差行程明细表
|
||||
-- =====================================
|
||||
CREATE TABLE IF NOT EXISTS `business_trip_itinerary` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '行程ID',
|
||||
`business_trip_id` BIGINT UNSIGNED NOT NULL COMMENT '出差申请ID',
|
||||
`date` DATE NOT NULL COMMENT '行程日期',
|
||||
`departure` VARCHAR(100) DEFAULT NULL COMMENT '出发地',
|
||||
`destination` VARCHAR(100) NOT NULL COMMENT '目的地',
|
||||
`transport` VARCHAR(50) DEFAULT NULL COMMENT '交通方式',
|
||||
`accommodation` VARCHAR(100) DEFAULT NULL COMMENT '住宿安排',
|
||||
`remark` VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_business_trip_id` (`business_trip_id`),
|
||||
KEY `idx_date` (`date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='出差行程明细表';
|
||||
|
||||
-- =====================================
|
||||
-- 初始化数据
|
||||
-- =====================================
|
||||
|
||||
-- 插入默认公司
|
||||
INSERT INTO `company` (`id`, `name`, `status`) VALUES
|
||||
(1, '示例公司', 1)
|
||||
ON DUPLICATE KEY UPDATE `name` = VALUES(`name`);
|
||||
|
||||
-- 插入默认用户
|
||||
INSERT INTO `user` (`id`, `company_id`, `name`, `email`, `status`) VALUES
|
||||
(1, 1, '管理员', 'admin@example.com', 1)
|
||||
ON DUPLICATE KEY UPDATE `name` = VALUES(`name`);
|
||||
|
||||
-- 插入默认出差审批流程
|
||||
INSERT INTO `approval_flow` (`id`, `business_type`, `name`, `status`) VALUES
|
||||
(1, 'business-trip', '默认出差审批流程', 1)
|
||||
ON DUPLICATE KEY UPDATE `name` = VALUES(`name`);
|
||||
|
||||
-- 插入默认审批节点
|
||||
INSERT INTO `approval_node` (`flow_id`, `type`, `name`, `order`, `approvers`) VALUES
|
||||
(1, 'approver', '部门主管审批', 1, '[]'),
|
||||
(1, 'approver', '人事审批', 2, '[]')
|
||||
ON DUPLICATE KEY UPDATE `name` = VALUES(`name`);
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* English translation file
|
||||
*/
|
||||
return [
|
||||
'app' => [
|
||||
'name' => 'Enterprise Tool System',
|
||||
'title' => 'Enterprise Internal Tool Platform',
|
||||
],
|
||||
'common' => [
|
||||
'success' => 'Operation successful',
|
||||
'error' => 'Operation failed',
|
||||
'confirm' => 'Confirm',
|
||||
'cancel' => 'Cancel',
|
||||
'submit' => 'Submit',
|
||||
'save' => 'Save',
|
||||
'delete' => 'Delete',
|
||||
'edit' => 'Edit',
|
||||
'search' => 'Search',
|
||||
'refresh' => 'Refresh',
|
||||
'back' => 'Back',
|
||||
'close' => 'Close',
|
||||
'loading' => 'Loading...',
|
||||
'no_data' => 'No data',
|
||||
'operation' => 'Operation',
|
||||
'status' => 'Status',
|
||||
'create_time' => 'Create Time',
|
||||
'update_time' => 'Update Time',
|
||||
'remark' => 'Remark',
|
||||
],
|
||||
'auth' => [
|
||||
'login_success' => 'Login successful',
|
||||
'login_failed' => 'Login failed',
|
||||
'username_required' => 'Username is required',
|
||||
'password_required' => 'Password is required',
|
||||
'invalid_credentials' => 'Invalid username or password',
|
||||
'token_expired' => 'Session expired, please login again',
|
||||
'unauthorized' => 'Unauthorized, please login first',
|
||||
],
|
||||
'user' => [
|
||||
'not_found' => 'User not found',
|
||||
'info_updated' => 'Profile updated successfully',
|
||||
'company_switched' => 'Company switched successfully',
|
||||
],
|
||||
'approval' => [
|
||||
'submit_success' => 'Approval submitted successfully',
|
||||
'approve_success' => 'Approved successfully',
|
||||
'reject_success' => 'Rejected successfully',
|
||||
'cancel_success' => 'Cancelled successfully',
|
||||
'not_found' => 'Approval not found',
|
||||
'cannot_approve' => 'Cannot approve in current status',
|
||||
'cannot_cancel' => 'Cannot cancel in current status',
|
||||
'not_owner' => 'Only the applicant can cancel',
|
||||
'status' => [
|
||||
'pending' => 'Pending',
|
||||
'approved' => 'Approved',
|
||||
'rejected' => 'Rejected',
|
||||
'cancelled' => 'Cancelled',
|
||||
],
|
||||
],
|
||||
'business_trip' => [
|
||||
'submit_success' => 'Business trip submitted successfully',
|
||||
'update_success' => 'Business trip updated successfully',
|
||||
'not_found' => 'Business trip not found',
|
||||
],
|
||||
'error' => [
|
||||
'network_error' => 'Network error, please try again later',
|
||||
'server_error' => 'Server error',
|
||||
'not_found' => 'Page not found',
|
||||
'unknown_error' => 'Unknown error',
|
||||
'params_error' => 'Parameter error',
|
||||
'validation_error' => 'Validation failed',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Traducción al español (Spanish translation)
|
||||
*/
|
||||
return [
|
||||
'app' => [
|
||||
'name' => 'Sistema Empresarial',
|
||||
'title' => 'Plataforma de Herramientas Empresariales',
|
||||
],
|
||||
'common' => [
|
||||
'success' => 'Operación exitosa',
|
||||
'error' => 'Operación fallida',
|
||||
'confirm' => 'Confirmar',
|
||||
'cancel' => 'Cancelar',
|
||||
'submit' => 'Enviar',
|
||||
'save' => 'Guardar',
|
||||
'delete' => 'Eliminar',
|
||||
'edit' => 'Editar',
|
||||
'search' => 'Buscar',
|
||||
'refresh' => 'Actualizar',
|
||||
'back' => 'Atrás',
|
||||
'close' => 'Cerrar',
|
||||
'loading' => 'Cargando...',
|
||||
'no_data' => 'Sin datos',
|
||||
'operation' => 'Operación',
|
||||
'status' => 'Estado',
|
||||
'create_time' => 'Fecha de creación',
|
||||
'update_time' => 'Fecha de actualización',
|
||||
'remark' => 'Observación',
|
||||
],
|
||||
'auth' => [
|
||||
'login_success' => 'Inicio de sesión exitoso',
|
||||
'login_failed' => 'Error de inicio de sesión',
|
||||
'username_required' => 'Se requiere usuario',
|
||||
'password_required' => 'Se requiere contraseña',
|
||||
'invalid_credentials' => 'Usuario o contraseña inválidos',
|
||||
'token_expired' => 'Sesión expirada, inicie sesión nuevamente',
|
||||
'unauthorized' => 'No autorizado, inicie sesión primero',
|
||||
],
|
||||
'user' => [
|
||||
'not_found' => 'Usuario no encontrado',
|
||||
'info_updated' => 'Perfil actualizado exitosamente',
|
||||
'company_switched' => 'Empresa cambiada exitosamente',
|
||||
],
|
||||
'approval' => [
|
||||
'submit_success' => 'Aprobación enviada exitosamente',
|
||||
'approve_success' => 'Aprobado exitosamente',
|
||||
'reject_success' => 'Rechazado exitosamente',
|
||||
'cancel_success' => 'Cancelado exitosamente',
|
||||
'not_found' => 'Aprobación no encontrada',
|
||||
'cannot_approve' => 'No se puede aprobar en estado actual',
|
||||
'cannot_cancel' => 'No se puede cancelar en estado actual',
|
||||
'not_owner' => 'Solo el solicitante puede cancelar',
|
||||
'status' => [
|
||||
'pending' => 'Pendiente',
|
||||
'approved' => 'Aprobado',
|
||||
'rejected' => 'Rechazado',
|
||||
'cancelled' => 'Cancelado',
|
||||
],
|
||||
],
|
||||
'business_trip' => [
|
||||
'submit_success' => 'Viaje enviado exitosamente',
|
||||
'update_success' => 'Viaje actualizado exitosamente',
|
||||
'not_found' => 'Viaje no encontrado',
|
||||
],
|
||||
'error' => [
|
||||
'network_error' => 'Error de red, inténtelo más tarde',
|
||||
'server_error' => 'Error del servidor',
|
||||
'not_found' => 'Página no encontrada',
|
||||
'unknown_error' => 'Error desconocido',
|
||||
'params_error' => 'Error de parámetros',
|
||||
'validation_error' => 'Validación fallida',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Traduction française (French translation)
|
||||
*/
|
||||
return [
|
||||
'app' => [
|
||||
'name' => 'Système d\'Entreprise',
|
||||
'title' => 'Plateforme d\'Outils Entreprise',
|
||||
],
|
||||
'common' => [
|
||||
'success' => 'Opération réussie',
|
||||
'error' => 'Opération échouée',
|
||||
'confirm' => 'Confirmer',
|
||||
'cancel' => 'Annuler',
|
||||
'submit' => 'Soumettre',
|
||||
'save' => 'Enregistrer',
|
||||
'delete' => 'Supprimer',
|
||||
'edit' => 'Modifier',
|
||||
'search' => 'Rechercher',
|
||||
'refresh' => 'Actualiser',
|
||||
'back' => 'Retour',
|
||||
'close' => 'Fermer',
|
||||
'loading' => 'Chargement...',
|
||||
'no_data' => 'Aucune donnée',
|
||||
'operation' => 'Opération',
|
||||
'status' => 'Statut',
|
||||
'create_time' => 'Date de création',
|
||||
'update_time' => 'Date de mise à jour',
|
||||
'remark' => 'Remarque',
|
||||
],
|
||||
'auth' => [
|
||||
'login_success' => 'Connexion réussie',
|
||||
'login_failed' => 'Échec de la connexion',
|
||||
'username_required' => 'Nom d\'utilisateur requis',
|
||||
'password_required' => 'Mot de passe requis',
|
||||
'invalid_credentials' => 'Nom d\'utilisateur ou mot de passe invalide',
|
||||
'token_expired' => 'Session expirée, veuillez vous reconnecter',
|
||||
'unauthorized' => 'Non autorisé, veuillez vous connecter d\'abord',
|
||||
],
|
||||
'user' => [
|
||||
'not_found' => 'Utilisateur introuvable',
|
||||
'info_updated' => 'Profil mis à jour avec succès',
|
||||
'company_switched' => 'Entreprise changée avec succès',
|
||||
],
|
||||
'approval' => [
|
||||
'submit_success' => 'Approbation soumise avec succès',
|
||||
'approve_success' => 'Approuvé avec succès',
|
||||
'reject_success' => 'Rejeté avec succès',
|
||||
'cancel_success' => 'Annulé avec succès',
|
||||
'not_found' => 'Approbation introuvable',
|
||||
'cannot_approve' => 'Impossible d\'approuver dans cet état',
|
||||
'cannot_cancel' => 'Impossible d\'annuler dans cet état',
|
||||
'not_owner' => 'Seul le demandeur peut annuler',
|
||||
'status' => [
|
||||
'pending' => 'En attente',
|
||||
'approved' => 'Approuvé',
|
||||
'rejected' => 'Rejeté',
|
||||
'cancelled' => 'Annulé',
|
||||
],
|
||||
],
|
||||
'business_trip' => [
|
||||
'submit_success' => 'Déplacement soumis avec succès',
|
||||
'update_success' => 'Déplacement mis à jour avec succès',
|
||||
'not_found' => 'Déplacement introuvable',
|
||||
],
|
||||
'error' => [
|
||||
'network_error' => 'Erreur réseau, veuillez réessayer plus tard',
|
||||
'server_error' => 'Erreur du serveur',
|
||||
'not_found' => 'Page introuvable',
|
||||
'unknown_error' => 'Erreur inconnue',
|
||||
'params_error' => 'Erreur de paramètres',
|
||||
'validation_error' => 'Validation échouée',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Русский перевод (Russian translation)
|
||||
*/
|
||||
return [
|
||||
'app' => [
|
||||
'name' => 'Корпоративная система',
|
||||
'title' => 'Корпоративная платформа инструментов',
|
||||
],
|
||||
'common' => [
|
||||
'success' => 'Операция выполнена успешно',
|
||||
'error' => 'Операция не удалась',
|
||||
'confirm' => 'Подтвердить',
|
||||
'cancel' => 'Отмена',
|
||||
'submit' => 'Отправить',
|
||||
'save' => 'Сохранить',
|
||||
'delete' => 'Удалить',
|
||||
'edit' => 'Редактировать',
|
||||
'search' => 'Поиск',
|
||||
'refresh' => 'Обновить',
|
||||
'back' => 'Назад',
|
||||
'close' => 'Закрыть',
|
||||
'loading' => 'Загрузка...',
|
||||
'no_data' => 'Нет данных',
|
||||
'operation' => 'Операция',
|
||||
'status' => 'Статус',
|
||||
'create_time' => 'Время создания',
|
||||
'update_time' => 'Время обновления',
|
||||
'remark' => 'Примечание',
|
||||
],
|
||||
'auth' => [
|
||||
'login_success' => 'Вход выполнен успешно',
|
||||
'login_failed' => 'Ошибка входа',
|
||||
'username_required' => 'Имя пользователя обязательно',
|
||||
'password_required' => 'Пароль обязателен',
|
||||
'invalid_credentials' => 'Неверное имя пользователя или пароль',
|
||||
'token_expired' => 'Сессия истекла, войдите снова',
|
||||
'unauthorized' => 'Не авторизован, войдите сначала',
|
||||
],
|
||||
'user' => [
|
||||
'not_found' => 'Пользователь не найден',
|
||||
'info_updated' => 'Профиль обновлен успешно',
|
||||
'company_switched' => 'Компания изменена успешно',
|
||||
],
|
||||
'approval' => [
|
||||
'submit_success' => 'Согласование отправлено успешно',
|
||||
'approve_success' => 'Согласовано успешно',
|
||||
'reject_success' => 'Отклонено успешно',
|
||||
'cancel_success' => 'Отменено успешно',
|
||||
'not_found' => 'Согласование не найдено',
|
||||
'cannot_approve' => 'Невозможно согласовать в текущем статусе',
|
||||
'cannot_cancel' => 'Невозможно отменить в текущем статусе',
|
||||
'not_owner' => 'Только заявитель может отменить',
|
||||
'status' => [
|
||||
'pending' => 'Ожидает',
|
||||
'approved' => 'Согласовано',
|
||||
'rejected' => 'Отклонено',
|
||||
'cancelled' => 'Отменено',
|
||||
],
|
||||
],
|
||||
'business_trip' => [
|
||||
'submit_success' => 'Командировка отправлена успешно',
|
||||
'update_success' => 'Командировка обновлена успешно',
|
||||
'not_found' => 'Командировка не найдена',
|
||||
],
|
||||
'error' => [
|
||||
'network_error' => 'Ошибка сети, попробуйте позже',
|
||||
'server_error' => 'Ошибка сервера',
|
||||
'not_found' => 'Страница не найдена',
|
||||
'unknown_error' => 'Неизвестная ошибка',
|
||||
'params_error' => 'Ошибка параметров',
|
||||
'validation_error' => 'Ошибка проверки данных',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 中文翻译文件
|
||||
*/
|
||||
return [
|
||||
'app' => [
|
||||
'name' => '企业工具系统',
|
||||
'title' => '企业内部工具平台',
|
||||
],
|
||||
'common' => [
|
||||
'success' => '操作成功',
|
||||
'error' => '操作失败',
|
||||
'confirm' => '确认',
|
||||
'cancel' => '取消',
|
||||
'submit' => '提交',
|
||||
'save' => '保存',
|
||||
'delete' => '删除',
|
||||
'edit' => '编辑',
|
||||
'search' => '搜索',
|
||||
'refresh' => '刷新',
|
||||
'back' => '返回',
|
||||
'close' => '关闭',
|
||||
'loading' => '加载中...',
|
||||
'no_data' => '暂无数据',
|
||||
'operation' => '操作',
|
||||
'status' => '状态',
|
||||
'create_time' => '创建时间',
|
||||
'update_time' => '更新时间',
|
||||
'remark' => '备注',
|
||||
],
|
||||
'auth' => [
|
||||
'login_success' => '登录成功',
|
||||
'login_failed' => '登录失败',
|
||||
'username_required' => '用户名不能为空',
|
||||
'password_required' => '密码不能为空',
|
||||
'invalid_credentials' => '用户名或密码错误',
|
||||
'token_expired' => '登录已过期,请重新登录',
|
||||
'unauthorized' => '未授权,请先登录',
|
||||
],
|
||||
'user' => [
|
||||
'not_found' => '用户不存在',
|
||||
'info_updated' => '个人信息更新成功',
|
||||
'company_switched' => '公司切换成功',
|
||||
],
|
||||
'approval' => [
|
||||
'submit_success' => '审批提交成功',
|
||||
'approve_success' => '审批通过',
|
||||
'reject_success' => '已拒绝',
|
||||
'cancel_success' => '已撤销',
|
||||
'not_found' => '审批单不存在',
|
||||
'cannot_approve' => '当前状态无法审批',
|
||||
'cannot_cancel' => '当前状态无法撤销',
|
||||
'not_owner' => '只有申请人可以撤销',
|
||||
'status' => [
|
||||
'pending' => '待审批',
|
||||
'approved' => '已通过',
|
||||
'rejected' => '已拒绝',
|
||||
'cancelled' => '已撤销',
|
||||
],
|
||||
],
|
||||
'business_trip' => [
|
||||
'submit_success' => '出差申请提交成功',
|
||||
'update_success' => '出差申请更新成功',
|
||||
'not_found' => '出差申请不存在',
|
||||
],
|
||||
'error' => [
|
||||
'network_error' => '网络错误,请稍后重试',
|
||||
'server_error' => '服务器错误',
|
||||
'not_found' => '页面不存在',
|
||||
'unknown_error' => '未知错误',
|
||||
'params_error' => '参数错误',
|
||||
'validation_error' => '数据验证失败',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
chdir(__DIR__);
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
support\App::run();
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace support;
|
||||
|
||||
use Webman\App;
|
||||
use Workerman\Worker;
|
||||
|
||||
class Bootstrap
|
||||
{
|
||||
public static function start(?Worker $worker): void
|
||||
{
|
||||
// 应用启动时的初始化
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 'on');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
use Dotenv\Dotenv;
|
||||
use support\Log;
|
||||
use Webman\Bootstrap;
|
||||
use Webman\Config;
|
||||
use Webman\Middleware;
|
||||
use Webman\Route;
|
||||
use Webman\Util;
|
||||
use Workerman\Events\Select;
|
||||
use Workerman\Worker;
|
||||
|
||||
$worker = $worker ?? null;
|
||||
|
||||
if (empty(Worker::$eventLoopClass)) {
|
||||
Worker::$eventLoopClass = Select::class;
|
||||
}
|
||||
|
||||
set_error_handler(function ($level, $message, $file = '', $line = 0) {
|
||||
if (error_reporting() & $level) {
|
||||
throw new ErrorException($message, 0, $level, $file, $line);
|
||||
}
|
||||
});
|
||||
|
||||
if ($worker) {
|
||||
register_shutdown_function(function ($startTime) {
|
||||
if (time() - $startTime <= 0.1) {
|
||||
sleep(1);
|
||||
}
|
||||
}, time());
|
||||
}
|
||||
|
||||
if (class_exists('Dotenv\Dotenv') && file_exists(base_path(false) . '/.env')) {
|
||||
if (method_exists('Dotenv\Dotenv', 'createUnsafeMutable')) {
|
||||
Dotenv::createUnsafeMutable(base_path(false))->load();
|
||||
} else {
|
||||
Dotenv::createMutable(base_path(false))->load();
|
||||
}
|
||||
}
|
||||
|
||||
Config::clear();
|
||||
support\App::loadAllConfig(['route']);
|
||||
if ($timezone = config('app.default_timezone')) {
|
||||
date_default_timezone_set($timezone);
|
||||
}
|
||||
|
||||
foreach (config('autoload.files', []) as $file) {
|
||||
include_once $file;
|
||||
}
|
||||
foreach (config('plugin', []) as $firm => $projects) {
|
||||
foreach ($projects as $name => $project) {
|
||||
if (!is_array($project)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($project['autoload']['files'] ?? [] as $file) {
|
||||
include_once $file;
|
||||
}
|
||||
}
|
||||
foreach ($projects['autoload']['files'] ?? [] as $file) {
|
||||
include_once $file;
|
||||
}
|
||||
}
|
||||
|
||||
Middleware::load(config('middleware', []));
|
||||
foreach (config('plugin', []) as $firm => $projects) {
|
||||
foreach ($projects as $name => $project) {
|
||||
if (!is_array($project) || $name === 'static') {
|
||||
continue;
|
||||
}
|
||||
Middleware::load($project['middleware'] ?? []);
|
||||
}
|
||||
Middleware::load($projects['middleware'] ?? [], $firm);
|
||||
if ($staticMiddlewares = config("plugin.$firm.static.middleware")) {
|
||||
Middleware::load(['__static__' => $staticMiddlewares], $firm);
|
||||
}
|
||||
}
|
||||
Middleware::load(['__static__' => config('static.middleware', [])]);
|
||||
|
||||
foreach (config('bootstrap', []) as $className) {
|
||||
if (!class_exists($className)) {
|
||||
$log = "Warning: Class $className setting in config/bootstrap.php not found\r\n";
|
||||
echo $log;
|
||||
Log::error($log);
|
||||
continue;
|
||||
}
|
||||
/** @var Bootstrap $className */
|
||||
$className::start($worker);
|
||||
}
|
||||
|
||||
foreach (config('plugin', []) as $firm => $projects) {
|
||||
foreach ($projects as $name => $project) {
|
||||
if (!is_array($project)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($project['bootstrap'] ?? [] as $className) {
|
||||
if (!class_exists($className)) {
|
||||
$log = "Warning: Class $className setting in config/plugin/$firm/$name/bootstrap.php not found\r\n";
|
||||
echo $log;
|
||||
Log::error($log);
|
||||
continue;
|
||||
}
|
||||
/** @var Bootstrap $className */
|
||||
$className::start($worker);
|
||||
}
|
||||
}
|
||||
foreach ($projects['bootstrap'] ?? [] as $className) {
|
||||
/** @var string $className */
|
||||
if (!class_exists($className)) {
|
||||
$log = "Warning: Class $className setting in plugin/$firm/config/bootstrap.php not found\r\n";
|
||||
echo $log;
|
||||
Log::error($log);
|
||||
continue;
|
||||
}
|
||||
/** @var Bootstrap $className */
|
||||
$className::start($worker);
|
||||
}
|
||||
}
|
||||
|
||||
$directory = base_path() . '/plugin';
|
||||
$paths = [config_path()];
|
||||
foreach (Util::scanDir($directory) as $path) {
|
||||
if (is_dir($path = "$path/config")) {
|
||||
$paths[] = $path;
|
||||
}
|
||||
}
|
||||
Route::load($paths);
|
||||
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Webman\Config;
|
||||
use Webman\Console\Command;
|
||||
use Webman\Console\Util;
|
||||
use support\Container;
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
if (!Phar::running()) {
|
||||
chdir(__DIR__);
|
||||
}
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
if (!$appConfigFile = config_path('app.php')) {
|
||||
throw new RuntimeException('Config file not found: app.php');
|
||||
}
|
||||
|
||||
if (class_exists(Dotenv::class) && file_exists(run_path('.env'))) {
|
||||
if (method_exists(Dotenv::class, 'createUnsafeImmutable')) {
|
||||
Dotenv::createUnsafeImmutable(run_path())->load();
|
||||
} else {
|
||||
Dotenv::createMutable(run_path())->load();
|
||||
}
|
||||
}
|
||||
|
||||
$appConfig = require $appConfigFile;
|
||||
if ($timezone = $appConfig['default_timezone'] ?? '') {
|
||||
date_default_timezone_set($timezone);
|
||||
}
|
||||
|
||||
if ($errorReporting = $appConfig['error_reporting'] ?? '') {
|
||||
error_reporting($errorReporting);
|
||||
}
|
||||
|
||||
if (!in_array($argv[1] ?? '', ['start', 'restart', 'stop', 'status', 'reload', 'connections'])) {
|
||||
require_once __DIR__ . '/support/bootstrap.php';
|
||||
} else {
|
||||
if (class_exists('Support\App')) {
|
||||
Support\App::loadAllConfig(['route']);
|
||||
} else {
|
||||
Config::reload(config_path(), ['route', 'container']);
|
||||
}
|
||||
}
|
||||
|
||||
$cli = new Command();
|
||||
$cli->setName('webman cli');
|
||||
$cli->installInternalCommands();
|
||||
if (is_dir($command_path = Util::guessPath(app_path(), '/command', true))) {
|
||||
$cli->installCommands($command_path);
|
||||
}
|
||||
|
||||
foreach (config('plugin', []) as $firm => $projects) {
|
||||
if (isset($projects['app'])) {
|
||||
foreach (['', '/app'] as $app) {
|
||||
if ($command_str = Util::guessPath(base_path() . "/plugin/$firm{$app}", 'command')) {
|
||||
$command_path = base_path() . "/plugin/$firm{$app}/$command_str";
|
||||
$cli->installCommands($command_path, "plugin\\$firm" . str_replace('/', '\\', $app) . "\\$command_str");
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($projects as $name => $project) {
|
||||
if (!is_array($project)) {
|
||||
continue;
|
||||
}
|
||||
$project['command'] ??= [];
|
||||
array_walk($project['command'], [$cli, 'createCommandInstance']);
|
||||
}
|
||||
}
|
||||
|
||||
$cli->run();
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
/**
|
||||
* Start file for windows
|
||||
*/
|
||||
chdir(__DIR__);
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use Dotenv\Dotenv;
|
||||
use support\App;
|
||||
use Workerman\Worker;
|
||||
|
||||
ini_set('display_errors', 'on');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
if (class_exists('Dotenv\Dotenv') && file_exists(base_path() . '/.env')) {
|
||||
if (method_exists('Dotenv\Dotenv', 'createUnsafeImmutable')) {
|
||||
Dotenv::createUnsafeImmutable(base_path())->load();
|
||||
} else {
|
||||
Dotenv::createMutable(base_path())->load();
|
||||
}
|
||||
}
|
||||
|
||||
App::loadAllConfig(['route']);
|
||||
|
||||
$errorReporting = config('app.error_reporting');
|
||||
if (isset($errorReporting)) {
|
||||
error_reporting($errorReporting);
|
||||
}
|
||||
|
||||
$runtimeProcessPath = runtime_path() . DIRECTORY_SEPARATOR . '/windows';
|
||||
$paths = [
|
||||
$runtimeProcessPath,
|
||||
runtime_path('logs'),
|
||||
runtime_path('views')
|
||||
];
|
||||
foreach ($paths as $path) {
|
||||
if (!is_dir($path)) {
|
||||
mkdir($path, 0777, true);
|
||||
}
|
||||
}
|
||||
|
||||
$processFiles = [];
|
||||
if (config('server.listen')) {
|
||||
$processFiles[] = __DIR__ . DIRECTORY_SEPARATOR . 'start.php';
|
||||
}
|
||||
foreach (config('process', []) as $processName => $config) {
|
||||
$processFiles[] = write_process_file($runtimeProcessPath, $processName, '');
|
||||
}
|
||||
|
||||
foreach (config('plugin', []) as $firm => $projects) {
|
||||
foreach ($projects as $name => $project) {
|
||||
if (!is_array($project)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($project['process'] ?? [] as $processName => $config) {
|
||||
$processFiles[] = write_process_file($runtimeProcessPath, $processName, "$firm.$name");
|
||||
}
|
||||
}
|
||||
foreach ($projects['process'] ?? [] as $processName => $config) {
|
||||
$processFiles[] = write_process_file($runtimeProcessPath, $processName, $firm);
|
||||
}
|
||||
}
|
||||
|
||||
function write_process_file($runtimeProcessPath, $processName, $firm): string
|
||||
{
|
||||
$processParam = $firm ? "plugin.$firm.$processName" : $processName;
|
||||
$configParam = $firm ? "config('plugin.$firm.process')['$processName']" : "config('process')['$processName']";
|
||||
$fileContent = <<<EOF
|
||||
<?php
|
||||
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
use Workerman\Worker;
|
||||
use Workerman\Connection\TcpConnection;
|
||||
use Webman\Config;
|
||||
use support\App;
|
||||
|
||||
ini_set('display_errors', 'on');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
if (is_callable('opcache_reset')) {
|
||||
opcache_reset();
|
||||
}
|
||||
|
||||
if (!\$appConfigFile = config_path('app.php')) {
|
||||
throw new RuntimeException('Config file not found: app.php');
|
||||
}
|
||||
\$appConfig = require \$appConfigFile;
|
||||
if (\$timezone = \$appConfig['default_timezone'] ?? '') {
|
||||
date_default_timezone_set(\$timezone);
|
||||
}
|
||||
|
||||
App::loadAllConfig(['route']);
|
||||
|
||||
worker_start('$processParam', $configParam);
|
||||
|
||||
if (DIRECTORY_SEPARATOR != "/") {
|
||||
Worker::\$logFile = config('server')['log_file'] ?? Worker::\$logFile;
|
||||
TcpConnection::\$defaultMaxPackageSize = config('server')['max_package_size'] ?? 10*1024*1024;
|
||||
}
|
||||
|
||||
Worker::runAll();
|
||||
|
||||
EOF;
|
||||
$processFile = $runtimeProcessPath . DIRECTORY_SEPARATOR . "start_$processParam.php";
|
||||
file_put_contents($processFile, $fileContent);
|
||||
return $processFile;
|
||||
}
|
||||
|
||||
if ($monitorConfig = config('process.monitor.constructor')) {
|
||||
$monitorHandler = config('process.monitor.handler');
|
||||
$monitor = new $monitorHandler(...array_values($monitorConfig));
|
||||
}
|
||||
|
||||
function popen_processes($processFiles)
|
||||
{
|
||||
$cmd = '"' . PHP_BINARY . '" ' . implode(' ', $processFiles);
|
||||
$descriptorspec = [STDIN, STDOUT, STDOUT];
|
||||
$resource = proc_open($cmd, $descriptorspec, $pipes, null, null, ['bypass_shell' => true]);
|
||||
if (!$resource) {
|
||||
exit("Can not execute $cmd\r\n");
|
||||
}
|
||||
return $resource;
|
||||
}
|
||||
|
||||
$resource = popen_processes($processFiles);
|
||||
echo "\r\n";
|
||||
while (1) {
|
||||
sleep(1);
|
||||
if (!empty($monitor) && $monitor->checkAllFilesChange()) {
|
||||
$status = proc_get_status($resource);
|
||||
$pid = $status['pid'];
|
||||
shell_exec("taskkill /F /T /PID $pid");
|
||||
proc_close($resource);
|
||||
$resource = popen_processes($processFiles);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user