fix: 修复页面间状态丢失问题并优化选择页面

- 修复选择审批人后再选择抄送人导致状态丢失的问题
- 使用独立的 sessionStorage key (selectedApprovers, selectedCc)
- 组件每次挂载时从 sessionStorage 恢复状态
- 只在提交/保存成功后清除 sessionStorage
- 优化人员选择页面布局和样式
- 更新 CLAUDE.md 文档记录问题和解决方案

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-02-25 15:22:12 +08:00
parent 2d39357cc3
commit 09d4cb8054
51 changed files with 2944 additions and 647 deletions
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace App\Bootstrap;
use App\Util\Db;
use Webman\Bootstrap;
class Database implements Bootstrap
{
public static function start(?\Workerman\Worker $worker): void
{
// 初始化数据库连接
Db::init();
}
}
@@ -0,0 +1,99 @@
<?php
namespace App\Controller;
use App\Service\ApprovalService;
use Webman\Http\Request;
use Webman\Http\Response;
class ApprovalController extends BaseController
{
private ApprovalService $service;
public function __construct()
{
$this->service = new ApprovalService();
}
/**
* 获取审批列表
*/
public function list(Request $request): Response
{
$userId = $this->getUserId($request);
$type = $request->get('type', 'my');
$page = (int) $request->get('page', 1);
$pageSize = (int) $request->get('page_size', 20);
$result = $this->service->getList($userId, $type, $page, $pageSize);
return $this->success($result);
}
/**
* 获取审批详情
*/
public function detail(Request $request): Response
{
$id = (int) $request->route('id');
$approval = \App\Model\ApprovalOrder::with(['flow', 'applicant', 'records'])
->findOrFail($id);
return $this->success($approval);
}
/**
* 审批通过
*/
public function approve(Request $request): Response
{
$userId = $this->getUserId($request);
$id = (int) $request->route('id');
$data = $request->post();
$data = $data ?? [];
$comment = $data['comment'] ?? null;
try {
$approval = $this->service->approve($id, $userId, $comment);
return $this->success($approval, '已通过');
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* 审批拒绝
*/
public function reject(Request $request): Response
{
$userId = $this->getUserId($request);
$id = (int) $request->route('id');
$data = $request->post();
$data = $data ?? [];
$comment = $data['comment'] ?? null;
try {
$approval = $this->service->reject($id, $userId, $comment);
return $this->success($approval, '已拒绝');
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* 撤销申请
*/
public function cancel(Request $request): Response
{
$userId = $this->getUserId($request);
$id = (int) $request->route('id');
try {
$approval = $this->service->cancel($id, $userId);
return $this->success($approval, '已撤销');
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
}
+111
View File
@@ -0,0 +1,111 @@
<?php
namespace App\Controller;
use App\Service\FeishuService;
use App\Util\JwtUtil;
use App\Model\User;
use App\Model\Company;
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::first();
if (!$user) {
return $this->error('用户不存在');
}
$token = JwtUtil::encode([
'user_id' => $user->id,
'company_id' => $user->company_id,
'username' => $user->name,
]);
return $this->success([
'token' => $token,
'user' => $user,
]);
}
/**
* 飞书免登
*/
public function feishuLogin(Request $request): Response
{
$authCode = $request->post('auth_code');
$tenantKey = $request->post('tenant_key');
if (!$authCode) {
return $this->error('缺少授权码');
}
try {
$feishuService = new FeishuService();
// 根据 tenant_key 获取公司
$company = $feishuService->getCompanyByTenantKey($tenantKey);
if (!$company) {
return $this->error('公司不存在');
}
// 设置公司并获取用户信息
$feishuService->setCompany($company->id);
$feishuData = $feishuService->getUserInfo($authCode);
// 创建或更新用户
$user = $feishuService->createOrUpdateUser($feishuData, $company->id);
// 生成 JWT Token
$token = JwtUtil::encode([
'user_id' => $user->id,
'company_id' => $user->company_id,
'feishu_user_id' => $user->feishu_user_id,
'union_id' => $user->union_id,
]);
return $this->success([
'token' => $token,
'user' => $user->load('company'),
]);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* 获取当前用户信息
*/
public function me(Request $request): Response
{
$userId = $this->getUserId($request);
if (!$userId) {
return $this->error('未登录', 401);
}
$user = User::with('company')->find($userId);
if (!$user) {
return $this->error('用户不存在');
}
return $this->success($user);
}
}
+74
View File
@@ -0,0 +1,74 @@
<?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
{
// 先检查 request 中是否已注入
if (isset($request->userId)) {
return $request->userId;
}
// 从 Authorization header 获取 token
$token = $request->header('Authorization');
if ($token) {
$token = str_replace('Bearer ', '', $token);
}
if (!$token) {
return null;
}
// 解析 token
$payload = \App\Util\JwtUtil::decode($token);
if (!$payload) {
return null;
}
return (int) ($payload['user_id'] ?? null);
}
/**
* 验证认证并返回用户ID,失败则抛出异常
*/
protected function requireAuth(Request $request): int
{
$userId = $this->getUserId($request);
if (!$userId) {
throw new \Exception('未登录或登录已过期', 401);
}
return $userId;
}
}
@@ -0,0 +1,165 @@
<?php
namespace App\Controller;
use App\Service\BusinessTripService;
use Webman\Http\Request;
use Webman\Http\Response;
class BusinessTripController extends BaseController
{
private BusinessTripService $service;
public function __construct()
{
$this->service = new BusinessTripService();
}
/**
* 获取路由参数
*/
private function getRouteParam(Request $request, string $name, $default = null)
{
$route = $request->route;
if ($route && method_exists($route, 'param')) {
return $route->param($name) ?? $default;
}
return $default;
}
/**
* 获取出差申请列表
*/
public function list(Request $request): Response
{
$userId = $this->getUserId($request);
$type = $request->get('type', 'my');
$page = (int) $request->get('page', 1);
$pageSize = (int) $request->get('page_size', 20);
$result = $this->service->getList($userId, $type, $page, $pageSize);
return $this->success($result);
}
/**
* 获取出差申请详情
*/
public function detail(Request $request): Response
{
$id = (int) $this->getRouteParam($request, 'id');
if (!$id) {
return $this->error('参数错误');
}
$detail = $this->service->getDetail($id);
return $this->success($detail);
}
/**
* 创建出差申请(草稿)
*/
public function create(Request $request): Response
{
$userId = $this->getUserId($request);
$data = $request->post();
$data = $data ?? [];
// 验证必填字段
if (empty($data['reason'])) {
return $this->error('请输入出差原因');
}
if (empty($data['itineraries']) || !is_array($data['itineraries'])) {
return $this->error('请至少添加一条行程');
}
// 验证行程数据
foreach ($data['itineraries'] as $index => $itinerary) {
if (empty($itinerary['start_time'])) {
return $this->error("行程" . ($index + 1) . ":请选择开始时间");
}
if (empty($itinerary['destination'])) {
return $this->error("行程" . ($index + 1) . ":请输入目的地");
}
}
try {
$businessTrip = $this->service->createDraft($userId, $data);
return $this->success($businessTrip, '创建成功');
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* 更新出差申请
*/
public function update(Request $request): Response
{
$userId = $this->getUserId($request);
$id = (int) $this->getRouteParam($request, 'id');
if (!$id) {
return $this->error('参数错误');
}
$data = $request->post();
$data = $data ?? [];
try {
$businessTrip = $this->service->update($userId, $id, $data);
return $this->success($businessTrip, '更新成功');
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* 提交出差申请
*/
public function submit(Request $request): Response
{
$userId = $this->getUserId($request);
$id = (int) $this->getRouteParam($request, 'id');
if (!$id) {
return $this->error('参数错误');
}
$data = $request->post();
$data = $data ?? [];
$approvers = $data['approvers'] ?? [];
$ccUsers = $data['cc_users'] ?? [];
if (empty($approvers)) {
return $this->error('请选择审批人');
}
try {
$businessTrip = $this->service->submit($userId, $id, $approvers, $ccUsers);
return $this->success($businessTrip, '提交成功');
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* 取消出差申请
*/
public function cancel(Request $request): Response
{
$userId = $this->getUserId($request);
$id = (int) $this->getRouteParam($request, 'id');
if (!$id) {
return $this->error('参数错误');
}
try {
$businessTrip = $this->service->cancel($userId, $id);
return $this->success($businessTrip, '取消成功');
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
}
@@ -1,7 +1,8 @@
<?php
namespace app\controller;
namespace App\Controller;
use App\Model\User;
use Webman\Http\Request;
use Webman\Http\Response;
@@ -13,14 +14,13 @@ class UserController extends BaseController
public function info(Request $request): Response
{
$userId = $this->getUserId($request);
$user = User::find($userId);
// TODO: 从数据库获取用户信息
return $this->success([
'id' => $userId,
'name' => '测试用户',
'email' => 'test@example.com',
'company_id' => 1,
]);
if (!$user) {
return $this->error('用户不存在');
}
return $this->success($user);
}
/**
@@ -59,4 +59,34 @@ class UserController extends BaseController
return $this->success();
}
/**
* 获取用户列表(用于选择审批人等)
*/
public function list(Request $request): Response
{
$userId = $this->getUserId($request);
$keyword = $request->get('keyword', '');
$page = (int) $request->get('page', 1);
$pageSize = (int) $request->get('page_size', 50);
$query = User::active();
if ($keyword) {
$query->search($keyword);
}
$total = $query->count();
$list = $query->orderBy('id')
->offset(($page - 1) * $pageSize)
->limit($pageSize)
->get();
return $this->success([
'list' => $list,
'total' => $total,
'page' => $page,
'page_size' => $pageSize,
]);
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace App\Middleware;
use App\Util\JwtUtil;
use Webman\Http\Request;
use Webman\Http\Response;
class AuthMiddleware
{
public function process(Request $request, callable $next): Response
{
// 从请求头获取 token
$token = $request->header('Authorization');
$token = str_replace('Bearer ', '', $token);
// 如果没有 token,返回 401
if (!$token) {
return json([
'code' => 401,
'message' => '未登录',
'data' => null,
]);
}
// 验证 token
$payload = JwtUtil::decode($token);
if (!$payload) {
return json([
'code' => 401,
'message' => '登录已过期',
'data' => null,
]);
}
// 将用户ID注入到请求中
$request->userId = (int) ($payload['user_id'] ?? 0);
$request->companyId = (int) ($payload['company_id'] ?? 0);
return $next($request);
}
}
@@ -1,8 +1,8 @@
<?php
namespace app\model;
namespace App\Model;
use support\Model;
use Illuminate\Database\Eloquent\Model;
class ApprovalFlow extends Model
{
@@ -1,8 +1,8 @@
<?php
namespace app\model;
namespace App\Model;
use support\Model;
use Illuminate\Database\Eloquent\Model;
class ApprovalNode extends Model
{
@@ -1,8 +1,8 @@
<?php
namespace app\model;
namespace App\Model;
use support\Model;
use Illuminate\Database\Eloquent\Model;
class ApprovalOrder extends Model
{
@@ -1,8 +1,8 @@
<?php
namespace app\model;
namespace App\Model;
use support\Model;
use Illuminate\Database\Eloquent\Model;
class ApprovalRecord extends Model
{
+101
View File
@@ -0,0 +1,101 @@
<?php
namespace App\Model;
use Illuminate\Database\Eloquent\Model;
class BusinessTrip extends Model
{
protected $table = 'business_trip';
protected $fillable = [
'user_id',
'reason',
'start_date',
'end_date',
'destination',
'estimated_cost',
'status',
'approval_order_id',
];
protected $casts = [
'user_id' => 'integer',
'estimated_cost' => 'decimal:2',
'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 user()
{
return $this->belongsTo(User::class, 'user_id');
}
/**
* 关联行程明细
*/
public function itineraries()
{
return $this->hasMany(BusinessTripItinerary::class, 'business_trip_id');
}
/**
* 关联审批单
*/
public function approvalOrder()
{
return $this->belongsTo(ApprovalOrder::class, 'approval_order_id');
}
/**
* 计算总天数
*/
public function getDurationDays(): int
{
if (!$this->start_date || !$this->end_date) {
return 0;
}
$start = \DateTime::createFromFormat('Y-m-d', $this->start_date);
$end = \DateTime::createFromFormat('Y-m-d', $this->end_date);
if (!$start || !$end) {
return 0;
}
return $end->diff($start)->days + 1;
}
/**
* 判断是否可以编辑
*/
public function canEdit(): bool
{
return $this->status === self::STATUS_DRAFT;
}
/**
* 判断是否可以提交
*/
public function canSubmit(): bool
{
return $this->status === self::STATUS_DRAFT;
}
/**
* 判断是否可以取消
*/
public function canCancel(): bool
{
return $this->status === self::STATUS_PENDING;
}
}
@@ -0,0 +1,59 @@
<?php
namespace App\Model;
use Illuminate\Database\Eloquent\Model;
class BusinessTripItinerary extends Model
{
protected $table = 'business_trip_itinerary';
protected $fillable = [
'business_trip_id',
'start_time',
'end_time',
'departure',
'destination',
'transport',
'trip_type',
'remark',
];
protected $casts = [
'business_trip_id' => 'integer',
'start_time' => 'datetime',
'end_time' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* 行程类型常量
*/
const TRIP_TYPE_ONEWAY = 'oneway'; // 单程
const TRIP_TYPE_ROUNDTRIP = 'roundtrip'; // 往返
/**
* 关联出差申请
*/
public function businessTrip()
{
return $this->belongsTo(BusinessTrip::class, 'business_trip_id');
}
/**
* 计算天数
*/
public function getDurationDays(): int
{
if (!$this->start_time || !$this->end_time) {
return 0;
}
$start = \DateTime::createFromFormat('Y-m-d H:i:s', $this->start_time);
$end = \DateTime::createFromFormat('Y-m-d H:i:s', $this->end_time);
if (!$start || !$end) {
return 0;
}
return $end->diff($start)->days + 1;
}
}
+62
View File
@@ -0,0 +1,62 @@
<?php
namespace App\Model;
use Illuminate\Database\Eloquent\Model;
class Company extends Model
{
protected $table = 'companies';
protected $fillable = [
'name',
'abbr',
'tenant_key',
'sort',
'status',
'feishu_enabled',
];
protected $casts = [
'sort' => 'integer',
'status' => 'integer',
'feishu_enabled' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* 获取启用的公司
*/
public function scopeActive($query)
{
return $query->where('status', 1);
}
/**
* 根据 tenant_key 查找
*/
public function scopeByTenantKey($query, ?string $tenantKey)
{
if ($tenantKey === null) {
return $query;
}
return $query->where('tenant_key', $tenantKey);
}
/**
* 关联飞书应用配置
*/
public function feishuApps()
{
return $this->hasMany(CompanyFeishuApp::class, 'company_id');
}
/**
* 关联用户
*/
public function users()
{
return $this->hasMany(User::class, 'company_id');
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace App\Model;
use Illuminate\Database\Eloquent\Model;
class CompanyFeishuApp extends Model
{
protected $table = 'company_feishu_apps';
protected $fillable = [
'company_id',
'system_id',
'name',
'tenant_key',
'app_id',
'app_secret',
'base_uri',
'callback_url',
'mobile_callback_url',
'status',
];
protected $casts = [
'company_id' => 'integer',
'system_id' => 'integer',
'status' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* 获取启用的应用
*/
public function scopeActive($query)
{
return $query->where('status', 1);
}
/**
* 关联公司
*/
public function company()
{
return $this->belongsTo(Company::class, 'company_id');
}
}
@@ -1,8 +1,8 @@
<?php
namespace app\model;
namespace App\Model;
use support\Model;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
@@ -1,13 +1,13 @@
<?php
namespace app\service;
namespace App\Service;
use app\model\ApprovalFlow;
use app\model\ApprovalNode;
use app\model\ApprovalOrder;
use app\model\ApprovalRecord;
use app\model\User;
use app\util\Db;
use App\Model\ApprovalFlow;
use App\Model\ApprovalNode;
use App\Model\ApprovalOrder;
use App\Model\ApprovalRecord;
use App\Model\User;
use App\Util\Db;
use Illuminate\Database\Connection;
class ApprovalService
+303
View File
@@ -0,0 +1,303 @@
<?php
namespace App\Service;
use App\Model\BusinessTrip;
use App\Model\BusinessTripItinerary;
use App\Model\ApprovalOrder;
use App\Model\ApprovalFlow;
use App\Model\ApprovalRecord;
use App\Model\User;
use App\Util\Db;
use Illuminate\Database\Connection;
class BusinessTripService
{
private Connection $db;
public function __construct()
{
Db::init();
$this->db = Db::connection();
}
/**
* 创建出差申请(草稿)
*/
public function createDraft(int $userId, array $data): BusinessTrip
{
return $this->db->transaction(function () use ($userId, $data) {
try {
// 计算最早和最晚时间
$itineraries = $data['itineraries'] ?? [];
if (empty($itineraries)) {
throw new \Exception('请至少添加一条行程');
}
$startDate = null;
$endDate = null;
$destinations = [];
foreach ($itineraries as $itinerary) {
if ($itinerary['start_time']) {
if (!$startDate || $itinerary['start_time'] < $startDate) {
$startDate = $itinerary['start_time'];
}
}
if ($itinerary['end_time']) {
if (!$endDate || $itinerary['end_time'] > $endDate) {
$endDate = $itinerary['end_time'];
}
}
if (!empty($itinerary['destination'])) {
$destinations[] = $itinerary['destination'];
}
}
// 创建出差申请
$businessTrip = BusinessTrip::create([
'user_id' => $userId,
'reason' => $data['reason'] ?? '',
'start_date' => $startDate ? date('Y-m-d', strtotime($startDate)) : null,
'end_date' => $endDate ? date('Y-m-d', strtotime($endDate)) : null,
'destination' => implode(', ', array_unique($destinations)),
'estimated_cost' => $data['estimated_cost'] ?? 0,
'status' => BusinessTrip::STATUS_DRAFT,
]);
// 创建行程明细
foreach ($itineraries as $itinerary) {
BusinessTripItinerary::create([
'business_trip_id' => $businessTrip->id,
'start_time' => $itinerary['start_time'],
'end_time' => $itinerary['end_time'] ?? null,
'departure' => $itinerary['departure'] ?? '',
'destination' => $itinerary['destination'] ?? '',
'transport' => $itinerary['transport'] ?? '',
'trip_type' => $itinerary['trip_type'] ?? 'oneway',
'remark' => $itinerary['remark'] ?? '',
]);
}
return $businessTrip->load('itineraries');
} catch (\Exception $e) {
error_log('BusinessTripService::createDraft error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
throw $e;
}
});
}
/**
* 提交出差申请
*/
public function submit(int $userId, int $businessTripId, array $approvers, array $ccUsers = []): BusinessTrip
{
return $this->db->transaction(function () use ($userId, $businessTripId, $approvers, $ccUsers) {
$businessTrip = BusinessTrip::with('itineraries')->findOrFail($businessTripId);
if ($businessTrip->user_id !== $userId) {
throw new \Exception('无权操作此申请');
}
if (!$businessTrip->canSubmit()) {
throw new \Exception('当前状态无法提交');
}
// 获取审批流程(简化版:直接使用第一个审批人作为单人审批)
$flow = ApprovalFlow::where('business_type', 'business_trip')
->where('status', 1)
->first();
if (!$flow) {
// 创建默认流程
$flow = ApprovalFlow::create([
'business_type' => 'business_trip',
'name' => '出差审批',
'description' => '出差申请审批流程',
'status' => 1,
]);
}
// 创建审批单
$approvalOrder = ApprovalOrder::create([
'business_type' => 'business_trip',
'business_id' => $businessTrip->id,
'title' => '出差申请 - ' . $businessTrip->user->name,
'applicant_id' => $userId,
'status' => ApprovalOrder::STATUS_PENDING,
'current_nodes' => [1], // 简化:使用节点1
'flow_id' => $flow->id,
'form_data' => [
'reason' => $businessTrip->reason,
'start_date' => $businessTrip->start_date,
'end_date' => $businessTrip->end_date,
'destination' => $businessTrip->destination,
'approvers' => $approvers,
'cc_users' => $ccUsers,
],
]);
// 更新出差申请状态
$businessTrip->status = BusinessTrip::STATUS_PENDING;
$businessTrip->approval_order_id = $approvalOrder->id;
$businessTrip->save();
// 记录提交操作
ApprovalRecord::create([
'order_id' => $approvalOrder->id,
'node_id' => 1,
'node_type' => 'single_approve',
'approver_id' => $userId,
'action' => 'submit',
'comment' => null,
]);
return $businessTrip->fresh(['itineraries', 'approvalOrder']);
});
}
/**
* 获取出差申请列表
*/
public function getList(int $userId, string $type = 'my', int $page = 1, int $pageSize = 20): array
{
$query = BusinessTrip::with(['user', 'itineraries']);
switch ($type) {
case 'my':
$query->where('user_id', $userId);
break;
case 'pending':
// 获取待审批的申请
$pendingOrderIds = ApprovalOrder::where('status', ApprovalOrder::STATUS_PENDING)
->where('business_type', 'business_trip')
->pluck('business_id')
->toArray();
$query->whereIn('id', $pendingOrderIds);
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,
];
}
/**
* 获取出差申请详情
*/
public function getDetail(int $id): BusinessTrip
{
return BusinessTrip::with(['user', 'itineraries'])
->findOrFail($id);
}
/**
* 更新出差申请
*/
public function update(int $userId, int $id, array $data): BusinessTrip
{
$businessTrip = BusinessTrip::findOrFail($id);
if ($businessTrip->user_id !== $userId) {
throw new \Exception('无权操作此申请');
}
if (!$businessTrip->canEdit()) {
throw new \Exception('当前状态无法编辑');
}
return $this->db->transaction(function () use ($businessTrip, $data) {
// 更新基本信息
if (isset($data['reason'])) {
$businessTrip->reason = $data['reason'];
}
// 更新行程
if (isset($data['itineraries'])) {
// 删除旧行程
BusinessTripItinerary::where('business_trip_id', $businessTrip->id)->delete();
// 创建新行程
$startDate = null;
$endDate = null;
$destinations = [];
foreach ($data['itineraries'] as $itinerary) {
BusinessTripItinerary::create([
'business_trip_id' => $businessTrip->id,
'start_time' => $itinerary['start_time'],
'end_time' => $itinerary['end_time'] ?? null,
'departure' => $itinerary['departure'] ?? '',
'destination' => $itinerary['destination'] ?? '',
'transport' => $itinerary['transport'] ?? '',
'trip_type' => $itinerary['trip_type'] ?? 'oneway',
'remark' => $itinerary['remark'] ?? '',
]);
if ($itinerary['start_time']) {
if (!$startDate || $itinerary['start_time'] < $startDate) {
$startDate = $itinerary['start_time'];
}
}
if ($itinerary['end_time']) {
if (!$endDate || $itinerary['end_time'] > $endDate) {
$endDate = $itinerary['end_time'];
}
}
if (!empty($itinerary['destination'])) {
$destinations[] = $itinerary['destination'];
}
}
// 更新日期和目的地
$businessTrip->start_date = $startDate ? date('Y-m-d', strtotime($startDate)) : null;
$businessTrip->end_date = $endDate ? date('Y-m-d', strtotime($endDate)) : null;
$businessTrip->destination = implode(', ', array_unique($destinations));
}
$businessTrip->save();
return $businessTrip->fresh('itineraries');
});
}
/**
* 取消出差申请
*/
public function cancel(int $userId, int $id): BusinessTrip
{
$businessTrip = BusinessTrip::findOrFail($id);
if ($businessTrip->user_id !== $userId) {
throw new \Exception('无权操作此申请');
}
if (!$businessTrip->canCancel()) {
throw new \Exception('当前状态无法取消');
}
$businessTrip->status = BusinessTrip::STATUS_CANCELLED;
$businessTrip->save();
// 同时取消审批单
if ($businessTrip->approval_order_id) {
$approvalOrder = ApprovalOrder::find($businessTrip->approval_order_id);
if ($approvalOrder) {
$approvalOrder->status = ApprovalOrder::STATUS_CANCELLED;
$approvalOrder->current_nodes = [];
$approvalOrder->save();
}
}
return $businessTrip->fresh();
}
}
@@ -1,11 +1,11 @@
<?php
namespace app\service;
namespace App\Service;
use app\model\Company;
use app\model\User;
use app\util\Db;
use support\Cache;
use App\Model\Company;
use App\Model\CompanyFeishuApp;
use App\Model\User;
use App\Util\Db;
class FeishuService
{
@@ -13,6 +13,9 @@ class FeishuService
private ?int $companyId = null;
private ?int $appId = null;
// 简单的内存缓存(生产环境应使用 Redis)
private static array $tokenCache = [];
/**
* 设置当前操作的公司
*/
@@ -46,16 +49,13 @@ class FeishuService
*/
private function getFeishuApp(int $appId): array
{
$sql = "SELECT * FROM company_feishu_apps WHERE id = ? AND status = 1";
$stmt = Db::connection()->getPdo()->prepare($sql);
$stmt->execute([$appId]);
$app = $stmt->fetch(\PDO::FETCH_ASSOC);
$app = CompanyFeishuApp::find($appId);
if (!$app) {
if (!$app || $app->status !== 1) {
throw new \Exception('飞书应用配置不存在');
}
return $app;
return $app->toArray();
}
/**
@@ -63,16 +63,17 @@ class FeishuService
*/
private function getDefaultFeishuApp(int $companyId): array
{
$sql = "SELECT * FROM company_feishu_apps WHERE company_id = ? AND system_id = 1 AND status = 1 ORDER BY sort ASC LIMIT 1";
$stmt = Db::connection()->getPdo()->prepare($sql);
$stmt->execute([$companyId]);
$app = $stmt->fetch(\PDO::FETCH_ASSOC);
$app = CompanyFeishuApp::where('company_id', $companyId)
->where('system_id', 1)
->where('status', 1)
->orderBy('sort')
->first();
if (!$app) {
throw new \Exception('未配置飞书应用');
}
return $app;
return $app->toArray();
}
/**
@@ -101,14 +102,12 @@ class FeishuService
}
$app = $this->getAppConfig();
// 从缓存获取
$cacheKey = "feishu:access_token:{$app['id']}";
$token = Cache::get($cacheKey);
if ($token) {
$this->accessToken = $token;
return $token;
// 从内存缓存获取
if (isset(self::$tokenCache[$cacheKey]) && self::$tokenCache[$cacheKey]['expire'] > time()) {
$this->accessToken = self::$tokenCache[$cacheKey]['token'];
return $this->accessToken;
}
// 请求新的 token
@@ -125,7 +124,10 @@ class FeishuService
$expire = $response['expire'] ?? 7200;
// 缓存 token(提前5分钟过期)
Cache::set($cacheKey, $token, $expire - 300);
self::$tokenCache[$cacheKey] = [
'token' => $token,
'expire' => time() + $expire - 300,
];
$this->accessToken = $token;
return $token;
@@ -290,7 +292,7 @@ class FeishuService
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// curl_close($ch); // 不再需要,PHP 8.5+ 自动关闭
if ($httpCode !== 200) {
throw new \Exception("HTTP 请求失败: {$httpCode}");
@@ -1,6 +1,6 @@
<?php
namespace app\util;
namespace App\Util;
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Events\Dispatcher;
@@ -24,11 +24,11 @@ class Db
// Add connection
self::$capsule->addConnection([
'driver' => 'mysql',
'host' => getenv('DB_HOST') ?: '127.0.0.1',
'host' => getenv('DB_HOST') ?: '1Panel-mysql-409s',
'port' => getenv('DB_PORT') ?: '3306',
'database' => getenv('DB_DATABASE') ?: 'xserver',
'username' => getenv('DB_USERNAME') ?: 'root',
'password' => getenv('DB_PASSWORD') ?: '',
'password' => getenv('DB_PASSWORD') ?: 'mysql_xEsdPa',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
@@ -1,6 +1,6 @@
<?php
namespace app\util;
namespace App\Util;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
@@ -1,6 +1,6 @@
<?php
namespace app\util;
namespace App\Util;
/**
* 多语言翻译类
-59
View File
@@ -1,59 +0,0 @@
<?php
namespace app\controller;
use app\util\JwtUtil;
use Webman\Http\Request;
use Webman\Http\Response;
class AuthController extends BaseController
{
/**
* 用户登录
*/
public function login(Request $request): Response
{
$username = $request->post('username');
$password = $request->post('password');
// TODO: 实现真实的登录逻辑
if (!$username || !$password) {
return $this->error('用户名或密码不能空');
}
// 模拟用户数据
$user = [
'user_id' => 1,
'username' => $username,
'name' => '测试用户',
'company_id' => 1,
];
$token = JwtUtil::encode($user);
return $this->success([
'token' => $token,
'user' => $user,
]);
}
/**
* 飞书免登
*/
public function feishuLogin(Request $request): Response
{
$authCode = $request->post('auth_code');
$tenantKey = $request->post('tenant_key');
// TODO: 实现飞书免登逻辑
// 1. 根据 tenant_key 获取公司配置
// 2. 使用 auth_code 换取用户信息
// 3. 创建或更新用户
// 4. 生成 JWT Token
return $this->success([
'token' => JwtUtil::encode(['user_id' => 1, 'company_id' => 1]),
'user' => ['id' => 1, 'name' => '飞书用户', 'company_id' => 1],
]);
}
}
-41
View File
@@ -1,41 +0,0 @@
<?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;
}
}
-96
View File
@@ -1,96 +0,0 @@
<?php
namespace app\model;
use support\Model;
class Company extends Model
{
protected $table = 'companies';
protected $fillable = [
'name',
'abbr',
'tenant_key',
'sort',
'status',
'feishu_enabled',
];
protected $casts = [
'sort' => 'integer',
'status' => 'integer',
'feishu_enabled' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* 关联用户
*/
public function users()
{
return $this->hasMany(User::class, 'company_id');
}
/**
* 获取启用的公司
*/
public function scopeActive($query)
{
return $query->where('status', 1);
}
/**
* 根据租户Key查找
*/
public function scopeByTenantKey($query, string $tenantKey)
{
return $query->where('tenant_key', $tenantKey);
}
/**
* 根据缩写查找
*/
public function scopeByAbbr($query, string $abbr)
{
return $query->where('abbr', $abbr);
}
/**
* 搜索公司
*/
public function scopeSearch($query, ?string $keyword)
{
if (!$keyword) {
return $query;
}
return $query->where(function ($q) use ($keyword) {
$q->where('name', 'like', "%{$keyword}%")
->orWhere('abbr', 'like', "%{$keyword}%");
});
}
/**
* 获取默认飞书应用配置
*/
public function getDefaultFeishuApp(): ?array
{
$sql = "SELECT * FROM company_feishu_apps WHERE company_id = ? AND system_id = 1 AND status = 1 ORDER BY sort ASC LIMIT 1";
$stmt = \app\util\Db::connection()->getPdo()->prepare($sql);
$stmt->execute([$this->id]);
return $stmt->fetch(\PDO::FETCH_ASSOC);
}
/**
* 获取所有飞书应用配置
*/
public function getFeishuApps(): array
{
$sql = "SELECT * FROM company_feishu_apps WHERE company_id = ? AND status = 1 ORDER BY sort";
$stmt = \app\util\Db::connection()->getPdo()->prepare($sql);
$stmt->execute([$this->id]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
}
+5
View File
@@ -0,0 +1,5 @@
<?php
return [
App\Bootstrap\Database::class,
];
+1 -1
View File
@@ -5,7 +5,7 @@ return [
'connections' => [
'mysql' => [
'driver' => 'mysql',
'host' => getenv('DB_HOST') ?: 'mysql',
'host' => getenv('DB_HOST') ?: '1Panel-mysql-409s',
'port' => getenv('DB_PORT') ?: 3306,
'database' => getenv('DB_DATABASE') ?: 'xserver',
'username' => getenv('DB_USERNAME') ?: 'root',
+25 -23
View File
@@ -4,35 +4,37 @@ 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::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::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::get('/user/list', [App\Controller\UserController::class, 'list']);
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('/approval/list', [App\Controller\ApprovalController::class, 'list']);
Route::get('/approval/detail/{id}', [App\Controller\ApprovalController::class, 'detail']);
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::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/create', [App\Controller\BusinessTripController::class, 'create']);
Route::post('/business-trip/update/{id}', [App\Controller\BusinessTripController::class, 'update']);
Route::post('/business-trip/submit/{id}', [App\Controller\BusinessTripController::class, 'submit']);
Route::post('/business-trip/cancel/{id}', [App\Controller\BusinessTripController::class, 'cancel']);
});
});
// 默认路由