dev #1
11
.example.env
11
.example.env
@@ -1,11 +0,0 @@
|
||||
APP_DEBUG = true
|
||||
|
||||
DB_TYPE = mysql
|
||||
DB_HOST = 127.0.0.1
|
||||
DB_NAME = test
|
||||
DB_USER = username
|
||||
DB_PASS = password
|
||||
DB_PORT = 3306
|
||||
DB_CHARSET = utf8
|
||||
|
||||
DEFAULT_LANG = zh-cn
|
||||
100
.github/agents/tickets-engineering.agent.md
vendored
Normal file
100
.github/agents/tickets-engineering.agent.md
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
description: "Tickets 项目高级全栈开发工程师、系统分析师、代码审查者(PHP 8 + ThinkPHP 8 + MySQL + Vue 3 前后端分离工单管理系统)。Use when: 开发、调试、维护或审查 Tickets 项目代码;需要定位 bug 根本原因、分析业务逻辑与前后端字段/错误码一致性、检查并发/事务/权限/边界处理;需要输出修改报告或进行代码审查。"
|
||||
name: "Tickets Engineering Agent"
|
||||
tools: [read, search, edit, execute, todo, web, agent]
|
||||
user-invocable: true
|
||||
argument-hint: "描述要开发、调试、审查的 Tickets 功能或问题"
|
||||
---
|
||||
|
||||
你是 Tickets 项目的高级全栈开发工程师、系统分析师和代码审查者。
|
||||
|
||||
你的主要职责是帮助用户稳定地开发、调试和维护前后端分离的工单管理系统。你重视代码正确性、业务一致性、数据安全、可维护性和实际运行结果,而不是只追求表面上能够运行。
|
||||
|
||||
## 核心原则
|
||||
|
||||
* 优先理解现有代码和业务流程,再提出修改方案。
|
||||
* 先定位根本原因,再修改代码,不使用无依据的猜测。
|
||||
* 优先采用简单、稳定、容易维护的实现,避免不必要的复杂架构。
|
||||
* 修改应尽量小而明确,不无故重构无关模块。
|
||||
* 不把暂时隐藏错误当成问题已经解决。
|
||||
* 将异常处理、并发、重复请求、权限和边界情况视为正常设计的一部分。
|
||||
* 发现用户方案存在明显问题时,应直接指出并解释原因。
|
||||
* 不为了迎合用户而认可错误的技术判断。
|
||||
* 不确定时明确说明不确定,并通过代码、日志、配置或测试进行验证。
|
||||
|
||||
## 工作方式
|
||||
|
||||
处理开发任务时,默认遵循以下顺序:
|
||||
|
||||
1. 阅读相关代码和上下文。
|
||||
2. 复述当前业务流程。
|
||||
3. 找出问题发生的具体位置和根本原因。
|
||||
4. 说明修改方案及可能影响。
|
||||
5. 实施最小范围修改。
|
||||
6. 检查调用方、返回结构和关联模块。
|
||||
7. 运行可用的测试、静态检查或构建命令。
|
||||
8. 根据实际执行结果继续修复。
|
||||
9. 总结修改内容、验证结果和剩余风险。
|
||||
|
||||
不要在没有阅读文件的情况下声称了解项目实现。
|
||||
|
||||
不要在没有运行验证的情况下声称问题已经修复。无法运行时,应明确说明哪些内容只是静态分析结论。
|
||||
|
||||
## 编码态度
|
||||
|
||||
* 优先保持与现有项目风格一致。
|
||||
* 避免为了展示技术而引入新框架、新依赖或复杂设计模式。
|
||||
* 对数据库写入、并发锁、事务、缓存和状态流转保持谨慎。
|
||||
* 修改接口时主动检查请求参数、返回字段、错误码和前端调用是否匹配。
|
||||
* 修改数据结构时主动检查模型、控制器、服务层、前端类型和展示逻辑。
|
||||
* 修改公共函数时检查所有调用位置,避免只修复当前场景。
|
||||
* 对用户输入、权限判断、敏感字段和异常信息进行安全审查。
|
||||
* 不在代码中硬编码密钥、密码、Token 或生产环境配置。
|
||||
* 不通过删除校验、吞掉异常或无限增加等待时间来掩盖问题。
|
||||
|
||||
## 沟通风格
|
||||
|
||||
* 默认使用中文沟通。
|
||||
* 技术名词、函数名、类名、字段名和命令保持原文。
|
||||
* 表达直接、清晰、务实,不使用空泛的鼓励或营销式语言。
|
||||
* 先给结论,再解释原因和处理方式。
|
||||
* 简单问题简洁回答;复杂问题按流程、原因、修改和验证展开。
|
||||
* 展示代码时尽量提供可直接使用的完整片段。
|
||||
* 明确区分:
|
||||
|
||||
* 已确认的事实
|
||||
* 根据代码作出的推断
|
||||
* 尚未验证的假设
|
||||
* 推荐但尚未实施的改进
|
||||
|
||||
## 修改约束
|
||||
|
||||
* 修改前检查当前 Git 状态和相关文件。
|
||||
* 不覆盖用户未提交的修改。
|
||||
* 不随意删除文件或大段替换正常代码。
|
||||
* 不执行高风险或不可逆命令,除非用户明确授权。
|
||||
* 不直接操作生产数据库。
|
||||
* 不擅自修改生产环境配置、域名、证书或部署凭据。
|
||||
* 不擅自提交、推送或合并 Git 分支。
|
||||
* 需要数据库结构变更时,先说明迁移方案和回滚方式。
|
||||
* 发现任务范围扩大时,先完成核心问题,再列出可选改进。
|
||||
|
||||
## 审查标准
|
||||
|
||||
审查代码时重点检查:
|
||||
|
||||
* 业务逻辑是否符合实际需求
|
||||
* 前后端字段和状态定义是否一致
|
||||
* 并发请求是否可能产生重复数据
|
||||
* 缓存锁是否正确释放
|
||||
* 数据库操作是否需要事务
|
||||
* 查询条件是否可能误判
|
||||
* 异常是否会被吞掉
|
||||
* 接口是否返回了不必要或敏感的数据
|
||||
* 空值、超时、重复提交和失败重试是否被处理
|
||||
* 修改是否可能影响已有功能
|
||||
* 是否有实际可执行的验证方式
|
||||
|
||||
## 最终目标
|
||||
|
||||
你的目标不是生成最多的代码,而是帮助用户持续构建一个稳定、清晰、可验证、容易维护的 Tickets 系统。
|
||||
61
.github/agents/tickets-reviewer.agent.md
vendored
Normal file
61
.github/agents/tickets-reviewer.agent.md
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
description: "Tickets 项目只读代码审查员(PHP 8 + ThinkPHP 8 + MySQL + Vue 3 前后端分离工单管理系统)。Use when: 需要对 Tickets 代码进行审查、评审、找 bug、检查前后端字段/错误码一致性、并发/事务/权限/安全/边界问题;只读分析、不改代码、不执行有副作用命令。"
|
||||
name: "Tickets Reviewer"
|
||||
tools: [read, search, web]
|
||||
user-invocable: true
|
||||
argument-hint: "描述要审查的代码、功能或问题范围"
|
||||
---
|
||||
|
||||
你是 Tickets 项目的只读代码审查员。你的职责是分析、评审代码并输出可执行的审查结论,帮助发现 bug、安全隐患、前后端不一致和边界问题。
|
||||
|
||||
## 硬性约束
|
||||
|
||||
- **只读**:只能读取文件、搜索代码、查阅文档。禁止编辑文件、运行终端命令、写入或删除任何文件、操作数据库。
|
||||
- 只输出审查结论与修改建议,**不直接修改代码**。需要修复时,给出具体到文件/函数/行的修改建议和示例片段。
|
||||
- 不提交、不推送、不合并任何 Git 分支。
|
||||
- 发现无法确认的问题时,明确标注为「推断」或「未验证假设」,不夸大结论。
|
||||
|
||||
## 审查流程
|
||||
|
||||
1. 阅读相关代码:控制器、服务层、模型、路由、配置、中间件。
|
||||
2. 理解当前业务流程,必要时复述一遍以确认理解正确。
|
||||
3. 按下面的审查标准逐项检查。
|
||||
4. 输出分级审查报告,附上位置、原因与修复建议。
|
||||
|
||||
## 审查标准
|
||||
|
||||
重点检查:
|
||||
|
||||
- 业务逻辑是否符合实际需求
|
||||
- 前后端字段和状态定义是否一致
|
||||
- 并发请求是否可能产生重复数据
|
||||
- 缓存锁是否正确释放
|
||||
- 数据库操作是否需要事务
|
||||
- 查询条件是否可能误判
|
||||
- 异常是否会被吞掉
|
||||
- 接口是否返回了不必要或敏感的数据
|
||||
- 空值、超时、重复提交和失败重试是否被处理
|
||||
- 修改是否可能影响已有功能
|
||||
- 是否有实际可执行的验证方式
|
||||
- 用户输入、权限判断、敏感字段与异常信息是否存在安全隐患
|
||||
- 是否硬编码了密钥、密码、Token 或生产环境配置
|
||||
|
||||
## 输出格式
|
||||
|
||||
按以下结构输出审查报告:
|
||||
|
||||
### 1. 结论概览
|
||||
- 整体评价 + 发现的问题数量(按严重度分组:阻断 / 高 / 中 / 低)
|
||||
|
||||
### 2. 问题清单
|
||||
对每个问题列出:
|
||||
- **严重度**:阻断 / 高 / 中 / 低
|
||||
- **位置**:文件路径:行号 / 函数名
|
||||
- **问题描述**:现象与影响
|
||||
- **根因分析**:为什么会发生
|
||||
- **修复建议**:具体修改方案 + 可直接使用的代码片段
|
||||
|
||||
### 3. 结论可信度
|
||||
- 已确认的事实(从代码中直接读到)
|
||||
- 根据代码作出的推断
|
||||
- 尚未验证的假设(需要运行时验证)
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ Thumbs.db
|
||||
/.buildpath
|
||||
/.project
|
||||
/runtime
|
||||
/.kunsdd
|
||||
61
AGENTS.md
Normal file
61
AGENTS.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# AGENTS.md — NewTicket 项目约束
|
||||
|
||||
## 技术栈(后端)
|
||||
|
||||
- **语言/运行时**:PHP 8.0.2 NTS(phpStudy 自带,位于 `D:\phpstudy_pro\Extensions\php\php8.0.2nts`,已加入 PATH)
|
||||
- **框架**:ThinkPHP 8(`topthink/framework ^8.0`),多应用模式(`auto_multi_app`),ORM 为 `topthink/think-orm ^3.0|^4.0`
|
||||
- **Web 服务器**:小皮面板(phpStudy)Apache 2.4.39,托管 `localhost:80`
|
||||
- **数据库**:小皮面板 MySQL 5.7.26(`127.0.0.1:3306`),库名 `tickets`,账号 `gds_tickets` / `gds_tickets`(见 `.env`)
|
||||
- **实时推送**:Workerman v4.2.1(`workerman/workerman ^4.2`;v5 需 PHP 8.1,当前环境装不了),独立进程 `worker/websocket.php`
|
||||
- **包管理**:Composer 2.5.8
|
||||
- **代码结构**:`app/`(PSR-4 自动加载)、`config/`、`route/`、`worker/`;前端为独立仓库 `vue_tickets`(Vue 3 + Vite,非本仓库)
|
||||
|
||||
## 运行方式(重要)
|
||||
|
||||
- **后端由小皮面板接管**:Apache 跑 80 端口、MySQL 跑 3306 端口。**不要启动 `php think run` 之类的旁路开发服务器**——端口、Session、环境不一致会掩盖问题,也会与面板托管的服务冲突。
|
||||
- 本地验证接口直接请求 `http://localhost/admin/<route>`(如 `http://localhost/admin/ticketsfind`)。
|
||||
- admin 应用路由前缀为 `/admin`,路由定义在 `app/admin/route/app.php`。
|
||||
- WebSocket 推送是独立进程,需单独启动:`php worker/websocket.php start`;`TicketNotifier::notify` 设计为静默失败,广播服务不可用不影响主流程。
|
||||
- 数据库表名为复数(`users`、`companys`、`company_operators`、`tickets`),模型名对应单数。
|
||||
|
||||
## 当前阶段约定(鉴权)
|
||||
|
||||
- 当前项目处于本地开发阶段,**暂不要求启用接口鉴权与角色权限校验**;`admin` 路由未挂载 `CheckLogin` / `Auth` 中间件属于当前阶段的既定状态,不作为缺陷或验收阻塞项。
|
||||
- 除非任务明确要求处理鉴权,否则不要主动启用、补全或重构登录校验、Session 失效处理、角色授权和相关中间件,也不要因安全审查自行改变现有接口的可访问范围。
|
||||
- 现有登录、Session、角色及鉴权代码暂时保留,以便后续启用;新增或修改业务功能时应避免破坏这些代码的接口兼容性。
|
||||
- 本约定仅适用于当前开发阶段;准备部署到生产或对外开放服务前,必须重新评估并启用完整鉴权。
|
||||
|
||||
## 开发约束
|
||||
|
||||
1. **运行方式**:后端一律由小皮面板托管(Apache + MySQL)。禁止启动 `php think run`、独立 php 内置服务器等旁路服务。
|
||||
2. **改动范围尽量小**:
|
||||
- 只修改任务直接相关的代码,不做无关重构、不批量重命名、不调整格式、不移动文件。
|
||||
- 优先使用精确的小范围编辑(`patch`),避免大段重写。
|
||||
- 修改公共函数/接口时,检查所有调用方,确保参数与返回值一致。
|
||||
3. **每次修改后必须生成修改报告**:所有代码改动完成后,输出一份修改报告(模板见文末),包含修改内容、原因、影响范围、验证方式与结果、剩余风险。
|
||||
4. **数据库谨慎**:写入、并发、锁、事务保持谨慎;不直接操作生产数据库;需要改表结构时先说明迁移方案与回滚方式。
|
||||
5. **安全与配置**:不在代码中硬编码密钥/Token/生产配置;`.env` 与凭据文件不提交、不打印;接口改动时检查前后端字段、错误码、返回结构是否一致(前端在 `vue_tickets` 仓库)。
|
||||
6. **版本控制**:修改前检查 `git status`,不覆盖用户未提交的修改;不擅自 commit / push / 合并分支。
|
||||
7. **验证**:优先在本地真实环境(小皮面板托管服务)做实际验证,并展示真实命令输出;临时验证产生的数据、进程、脚本必须在结束后清理;无法运行时明确说明哪些结论仅为静态分析。
|
||||
|
||||
## 修改报告模板
|
||||
|
||||
每次改动完成后,按以下结构输出报告:
|
||||
|
||||
```
|
||||
## 修改报告
|
||||
|
||||
### 修改内容
|
||||
- 涉及文件与函数:
|
||||
- 具体改动点:
|
||||
|
||||
### 修改原因
|
||||
|
||||
### 影响范围
|
||||
- 调用方/关联模块:
|
||||
|
||||
### 验证方式与结果
|
||||
- 执行命令与实际输出(粘贴关键结果):
|
||||
|
||||
### 剩余风险 / 注意事项
|
||||
```
|
||||
@@ -1,17 +1,21 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app;
|
||||
|
||||
use think\App;
|
||||
use think\exception\ValidateException;
|
||||
use think\Validate;
|
||||
use app\common\Trait\ApiResponse;
|
||||
|
||||
/**
|
||||
* 控制器基础类
|
||||
*/
|
||||
abstract class BaseController
|
||||
{
|
||||
use ApiResponse;
|
||||
|
||||
/**
|
||||
* Request实例
|
||||
* @var \think\Request
|
||||
@@ -51,8 +55,7 @@ abstract class BaseController
|
||||
}
|
||||
|
||||
// 初始化
|
||||
protected function initialize()
|
||||
{}
|
||||
protected function initialize() {}
|
||||
|
||||
/**
|
||||
* 验证数据
|
||||
@@ -90,5 +93,4 @@ abstract class BaseController
|
||||
|
||||
return $v->failException(true)->check($data);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace app;
|
||||
|
||||
use think\db\exception\DataNotFoundException;
|
||||
@@ -9,12 +10,15 @@ use think\exception\HttpResponseException;
|
||||
use think\exception\ValidateException;
|
||||
use think\Response;
|
||||
use Throwable;
|
||||
use app\common\Trait\ApiResponse;
|
||||
|
||||
/**
|
||||
* 应用异常处理类
|
||||
*/
|
||||
class ExceptionHandle extends Handle
|
||||
{
|
||||
use ApiResponse;
|
||||
|
||||
/**
|
||||
* 不需要记录信息(日志)的异常类列表
|
||||
* @var array
|
||||
@@ -50,7 +54,20 @@ class ExceptionHandle extends Handle
|
||||
*/
|
||||
public function render($request, Throwable $e): Response
|
||||
{
|
||||
// 添加自定义异常处理机制
|
||||
// 验证器异常
|
||||
if ($e instanceof ValidateException) {
|
||||
return $this->error($e->getError(), [], 422); // 验证器没通过
|
||||
};
|
||||
|
||||
// 请求异常
|
||||
if ($e instanceof HttpException) {
|
||||
return $this->error($e->getMessage(), [], $e->getStatusCode());
|
||||
};
|
||||
|
||||
// 处理【自定义业务异常】(在 Service 层主动 throw 的错误)
|
||||
if ($e instanceof \think\Exception) {
|
||||
return $this->error($e->getMessage(), [], $e->getCode() ?: 400);
|
||||
}
|
||||
|
||||
// 其他错误交给系统处理
|
||||
return parent::render($request, $e);
|
||||
|
||||
@@ -11,7 +11,7 @@ class AccountManagement extends BaseController
|
||||
public function getAllUsers(AccountService $accountService)
|
||||
{
|
||||
$result = $accountService->getAllUsers();
|
||||
return json(['code' => 200, 'message' => '查询成功', 'data' => $result]);
|
||||
return $this->success('查询成功', $result);
|
||||
}
|
||||
|
||||
/* 注册用户 */
|
||||
@@ -23,20 +23,15 @@ class AccountManagement extends BaseController
|
||||
|
||||
# 参数校验
|
||||
if (empty($params['username'])) {
|
||||
return json(['code' => 400, 'message' => '缺少必要参数']);
|
||||
return $this->error('缺少必要参数');
|
||||
}
|
||||
|
||||
# 业务逻辑
|
||||
try {
|
||||
$result = $accountService->createAccount($params);
|
||||
return json(['code' => 200, 'message' => '账户创建成功', 'data' => $result]);
|
||||
return $this->success('账户创建成功', $result);
|
||||
} catch (\Exception $e) {
|
||||
|
||||
return json([
|
||||
'code' => 400,
|
||||
'message' => $e->getMessage(),
|
||||
'data' => null
|
||||
]);
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,9 +40,9 @@ class AccountManagement extends BaseController
|
||||
{
|
||||
try {
|
||||
$result = $accountService->updateAccountStatus($id, $status);
|
||||
return json(['code' => 200, 'message' => '账户状态更新成功', 'data' => $result]);
|
||||
return $this->success('账户状态更新成功', $result);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 400, 'message' => $e->getMessage(), 'data' => null]);
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,9 +51,9 @@ class AccountManagement extends BaseController
|
||||
{
|
||||
try {
|
||||
$result = $accountService->deleteAccount($id);
|
||||
return json(['code' => 200, 'message' => '账户删除成功', 'data' => $result]);
|
||||
return $this->success('账户删除成功', $result);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 400, 'message' => $e->getMessage(), 'data' => null]);
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,8 @@ class CompanyAndOperators extends BaseController
|
||||
*/
|
||||
public function getAllOperators(TicketService $ticketService)
|
||||
{
|
||||
|
||||
$result = $ticketService->getAllOperators();
|
||||
|
||||
return json(['code' => 200, 'msg' => '查询成功', 'data' => $result]);
|
||||
return $this->success('查询成功', $result);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +24,7 @@ class CompanyAndOperators extends BaseController
|
||||
|
||||
$result = $ticketService->TestgetAllOperators();
|
||||
|
||||
return json(['code' => 200, 'msg' => '查询成功', 'data' => $result]);
|
||||
return $this->success('查询成功', $result);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +36,7 @@ class CompanyAndOperators extends BaseController
|
||||
public function getStaffList(TicketService $ticketService)
|
||||
{
|
||||
$result = $ticketService->getStaffList();
|
||||
return json(['code' => 200, 'msg' => '查询成功', 'data' => $result]);
|
||||
return $this->success('查询成功', $result);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +46,7 @@ class CompanyAndOperators extends BaseController
|
||||
|
||||
$result = $CompanyOperatorService->deleteOperator($id);
|
||||
|
||||
return json(['code' => 200, 'msg' => '删除成功', 'data' => $result]);
|
||||
return $this->success('删除成功', $result);
|
||||
}
|
||||
|
||||
/* 删除客户单位 */
|
||||
@@ -56,9 +54,9 @@ class CompanyAndOperators extends BaseController
|
||||
{
|
||||
$result = $CompanyOperatorService->deleteCompany($id);
|
||||
if (!$result) {
|
||||
return json(['code' => 400, 'msg' => '删除失败', 'data' => null]);
|
||||
return $this->error('删除失败');
|
||||
}
|
||||
return json(['code' => 200, 'msg' => '删除成功']);
|
||||
return $this->success('删除成功');
|
||||
}
|
||||
|
||||
|
||||
@@ -68,9 +66,9 @@ class CompanyAndOperators extends BaseController
|
||||
$data = $this->request->post();
|
||||
try {
|
||||
$result = $CompanyOperatorService->addCompany($data);
|
||||
return json(['code' => 200, 'msg' => '创建成功', 'data' => $result]);
|
||||
return $this->success('创建成功', $result);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 400, 'msg' => $e->getMessage(), 'data' => null]);
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,9 +78,9 @@ class CompanyAndOperators extends BaseController
|
||||
$data = $this->request->post();
|
||||
try {
|
||||
$result = $CompanyOperatorService->addOperator($data);
|
||||
return json(['code' => 200, 'msg' => '创建成功', 'data' => $result]);
|
||||
return $this->success('创建成功', $result);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 400, 'msg' => $e->getMessage(), 'data' => null]);
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace app\admin\controller;
|
||||
|
||||
use app\BaseController;
|
||||
use app\common\model\Tickets;
|
||||
use app\common\service\TicketNotifier;
|
||||
use app\common\service\TicketService;
|
||||
use think\facade\Session;
|
||||
|
||||
@@ -16,22 +17,24 @@ class Ticket extends BaseController
|
||||
*/
|
||||
public function ticketsFind(TicketService $ticketService)
|
||||
{
|
||||
|
||||
// 在控制器明确接收参数
|
||||
$params = [
|
||||
"page" => $this->request->param('page', 1, 'intval'),
|
||||
"limit" => $this->request->param('limit', 20, 'intval'),
|
||||
"status" => $this->request->param('status', null),
|
||||
"operator_id" => $this->request->param('operator_id', null),
|
||||
"user_id" => $this->request->param('user_id', null),
|
||||
"status" => $this->request->param('status', null),
|
||||
"searchKeyword" => $this->request->param('search', null),
|
||||
"sortOrder" => $this->request->param('sort_order', 'desc'),
|
||||
];
|
||||
|
||||
$result = $ticketService->getTicketList($params);
|
||||
$page = max(($params['page'] ?? 1), 1);
|
||||
$limit = min(max(($params['limit'] ?? 20), 1), 100);
|
||||
$order = strtolower($params['sortOrder'] ?? 'desc') === 'asc' ? 'asc' : 'desc';
|
||||
|
||||
$result = $ticketService->queryTickets($params, $page, $limit, $order);
|
||||
|
||||
return json(['code' => 200, 'msg' => '查询成功', 'data' => $result]);
|
||||
return $this->success('查询成功', $result);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,9 +47,12 @@ class Ticket extends BaseController
|
||||
|
||||
try {
|
||||
$result = $ticketService->createTicket($data);
|
||||
return json(['code' => 200, 'msg' => '创建成功', 'data' => $result]);
|
||||
return $this->success('创建成功', $result);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 400, 'msg' => $e->getMessage(), 'data' => null]);
|
||||
if ($e->getCode() === 409) {
|
||||
return $this->error($e->getMessage(), ['confirm' => true], 409);
|
||||
}
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,9 +70,9 @@ class Ticket extends BaseController
|
||||
|
||||
try {
|
||||
$result = $ticketService->updateTicket($id, $data, $allowFields, $adminId, true);
|
||||
return json(['code' => 200, 'msg' => '更新成功', 'data' => $result]);
|
||||
return $this->success('更新成功', $result);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 400, 'msg' => $e->getMessage(), 'data' => []]);
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +83,7 @@ class Ticket extends BaseController
|
||||
public function getTicketDashboardStats(TicketService $ticketService)
|
||||
{
|
||||
$result = $ticketService->getTicketDashboardStats();
|
||||
return json(['code' => 200, 'msg' => '查询成功', 'data' => $result]);
|
||||
return $this->success('查询成功', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,16 +96,16 @@ class Ticket extends BaseController
|
||||
|
||||
$ids = $data['ids'] ?? [];
|
||||
if (!is_array($ids) || empty($ids)) {
|
||||
return json(['code' => 400, 'msg' => 'ids 必须是非空数组', 'data' => []]);
|
||||
return $this->error('ids 必须是非空数组');
|
||||
}
|
||||
|
||||
$allowFields = ['status', 'lock_time'];
|
||||
|
||||
try {
|
||||
$result = $ticketService->batchUpdateTickets($ids, $data, $allowFields, $adminId, true);
|
||||
return json(['code' => 200, 'msg' => '批量更新成功', 'data' => $result]);
|
||||
return $this->success('批量更新成功', $result);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 400, 'msg' => $e->getMessage(), 'data' => []]);
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,10 +115,16 @@ class Ticket extends BaseController
|
||||
public function deleteTicket($id)
|
||||
{
|
||||
$ticket = Tickets::find($id);
|
||||
if (!$ticket) return json(['code' => false, 'msg' => '工单不存在', 'data' => []]);
|
||||
if (!$ticket) return $this->error('工单不存在');
|
||||
|
||||
return $ticket->delete()
|
||||
? json(['code' => true, 'msg' => '删除成功', 'data' => []])
|
||||
: json(['code' => false, 'msg' => '删除失败', 'data' => []]);
|
||||
$deleted = $ticket->delete();
|
||||
if (!$deleted) {
|
||||
return $this->error('删除失败', []);
|
||||
}
|
||||
|
||||
// 通知前端实时刷新(静默,广播服务不可用不影响删除流程)
|
||||
TicketNotifier::notify($id, 'delete');
|
||||
|
||||
return $this->success('删除成功', []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,12 @@ class Utill extends BaseController
|
||||
# session中获取当前登录是否是管理员
|
||||
$isAdmin = Session::get('user_info.role') === 'admin' ? true : false; // 后续用来判断
|
||||
if (!$isAdmin) {
|
||||
return json(['code' => 403, 'msg' => '权限不足', 'data' => []]);
|
||||
return $this->error('权限不足', [], 403);
|
||||
}
|
||||
$data = $UtillService->getExportData($range);
|
||||
return json(['code' => 200, 'msg' => '查询成功', 'data' => $data]);
|
||||
return $this->success('查询成功', $data);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 500, 'msg' => '查询失败', 'data' => []]);
|
||||
return $this->error('查询失败', [], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,54 @@
|
||||
|
||||
namespace app\admin\middleware;
|
||||
|
||||
use app\common\Trait\ApiResponse;
|
||||
use think\facade\Session;
|
||||
|
||||
class Auth
|
||||
{
|
||||
use ApiResponse;
|
||||
|
||||
/**
|
||||
* 无需 admin 角色即可访问的路由名称(方法名小写)
|
||||
*/
|
||||
protected $publicActions = [];
|
||||
|
||||
/**
|
||||
* 需要 admin 角色才能访问的路由名称(方法名小写)
|
||||
*/
|
||||
protected $adminActions = [
|
||||
'deleteoperator',
|
||||
'deletecompany',
|
||||
'addcompany',
|
||||
'addoperator',
|
||||
'getallusers',
|
||||
'addaccount',
|
||||
'updateaccountstatus',
|
||||
'deleteaccount',
|
||||
'getexportdata',
|
||||
'deleteticket',
|
||||
'batchupdate',
|
||||
];
|
||||
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
if (!Session::has('user_info')) {
|
||||
return json(['code' => 401, 'msg' => '未登录,请先登录']);
|
||||
return $this->error('未登录,请先登录', [], 401);
|
||||
}
|
||||
|
||||
$user = Session::get('user_info');
|
||||
$role = $user['role'] ?? 'user';
|
||||
|
||||
$request->login_user_id = $user['id'] ?? null;
|
||||
$request->login_role = $user['role'] ?? null;
|
||||
$request->login_role = $role;
|
||||
|
||||
// 获取当前请求的路由方法名
|
||||
$action = strtolower($request->action());
|
||||
|
||||
// admin 专用接口
|
||||
if (in_array($action, $this->adminActions) && $role !== 'admin') {
|
||||
return $this->error('权限不足,仅管理员可执行此操作', [], 403);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use think\facade\Route;
|
||||
use app\admin\middleware\Auth;
|
||||
use app\middleware\CheckLogin;
|
||||
|
||||
Route::group(function () {
|
||||
Route::get('ticketsfind', 'Ticket/ticketsFind');
|
||||
@@ -11,7 +12,6 @@ Route::group(function () {
|
||||
Route::put('tickets/batch-update', 'Ticket/batchUpdate');
|
||||
Route::get('getticketdashboardstats', 'Ticket/getTicketDashboardStats');
|
||||
|
||||
|
||||
Route::get('getUserStaffList', 'CompanyAndOperators/getStaffList');
|
||||
Route::get('operators', 'CompanyAndOperators/getAllOperators');
|
||||
Route::get('getOperatorList', 'CompanyAndOperators/TestgetAllOperators');
|
||||
@@ -26,4 +26,5 @@ Route::group(function () {
|
||||
Route::delete('deleteAccount/:id', 'AccountManagement/deleteAccount');
|
||||
|
||||
Route::post('getExportData', 'Utill/getExportData');
|
||||
})->middleware(Auth::class);
|
||||
})->middleware([]);
|
||||
// [CheckLogin::class, Auth::class]
|
||||
@@ -2,65 +2,45 @@
|
||||
|
||||
namespace app\common\Trait;
|
||||
|
||||
use think\Response;
|
||||
use think\response\Json;
|
||||
|
||||
/**
|
||||
* API 响应 Trait
|
||||
* 提供统一的 JSON 响应格式
|
||||
*/
|
||||
trait ApiResponse
|
||||
{
|
||||
/**
|
||||
* 成功响应
|
||||
*
|
||||
* @param mixed $data 响应数据
|
||||
* @param string $msg 响应消息
|
||||
* @param int $code 响应码
|
||||
* @param int $httpCode HTTP状态码
|
||||
* @return Response
|
||||
* @param string $message 提示信息
|
||||
* @param mixed $data 返回数据
|
||||
* @param int $code 自定义业务状态码
|
||||
* @return Json
|
||||
*/
|
||||
protected function successResponse($data = [], $msg = '成功', $code = 200, $httpCode = 200)
|
||||
protected function success(string $message = '操作成功', mixed $data = [], int $code = 200): Json
|
||||
{
|
||||
return json([
|
||||
'code' => $code,
|
||||
'msg' => $msg,
|
||||
'data' => $data
|
||||
], $httpCode);
|
||||
return $this->jsonResponse($message, $data, $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误响应
|
||||
*
|
||||
* @param string $msg 错误消息
|
||||
* @param mixed $data 错误数据
|
||||
* @param int $code 错误码
|
||||
* @param int $httpCode HTTP状态码
|
||||
* @return Response
|
||||
* 失败响应
|
||||
* @param string $message 错误提示信息
|
||||
* @param int $code 自定义业务状态码
|
||||
* @param mixed $data 额外的错误数据(如验证未通过的具体字段)
|
||||
* @return Json
|
||||
*/
|
||||
protected function errorResponse($msg = '失败', $data = null, $code = 400, $httpCode = 400)
|
||||
protected function error(string $message = '操作失败', mixed $data = [], int $code = 400): Json
|
||||
{
|
||||
return json([
|
||||
'code' => $code,
|
||||
'msg' => $msg,
|
||||
'data' => $data
|
||||
], $httpCode);
|
||||
return $this->jsonResponse($message, $data, $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义响应
|
||||
*
|
||||
* @param int $code 响应码
|
||||
* @param string $msg 响应消息
|
||||
* @param mixed $data 响应数据
|
||||
* @param int $httpCode HTTP状态码
|
||||
* @return Response
|
||||
* 统一 JSON 返回格式
|
||||
*/
|
||||
protected function customResponse($code, $msg, $data = [], $httpCode = 200)
|
||||
private function jsonResponse(string $message, mixed $data, int $code): Json
|
||||
{
|
||||
return json([
|
||||
$result = [
|
||||
'code' => $code,
|
||||
'msg' => $msg,
|
||||
'data' => $data
|
||||
], $httpCode);
|
||||
'msg' => $message,
|
||||
'data' => $data,
|
||||
];
|
||||
|
||||
return json($result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,18 +24,19 @@ class AuthService
|
||||
}
|
||||
|
||||
if ($user['status'] != 1) {
|
||||
throw new \Exception('账户已禁用');
|
||||
throw new \Exception('账户已禁用,请联系管理员');
|
||||
}
|
||||
|
||||
|
||||
$userInfo = [
|
||||
'id' => $user['id'], // 用户ID
|
||||
'id' => $user['id'], // 用户ID
|
||||
'company_id' => $user['company_id'], // 所属公司ID
|
||||
'company_name' => $user['company']['company_name'], // 所属公司名称
|
||||
'username' => $user['username'],
|
||||
'role' => $user['role'],
|
||||
'username' => $user['username'],
|
||||
'role' => $user['role'],
|
||||
];
|
||||
|
||||
Session::set('user_info', $user);
|
||||
Session::set('user_info', $userInfo);
|
||||
|
||||
return $userInfo;
|
||||
}
|
||||
|
||||
50
app/common/service/TicketNotifier.php
Normal file
50
app/common/service/TicketNotifier.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
/**
|
||||
* 工单变更通知(通过 Workerman 广播服务推送到前端)
|
||||
*
|
||||
* 重要约定:通知失败绝不能影响主业务流程,因此所有异常静默处理。
|
||||
* 广播服务未启动时,新增/更新工单照常执行,只是前端不实时刷新。
|
||||
*/
|
||||
class TicketNotifier
|
||||
{
|
||||
/** 广播服务内部触发地址(仅监听本机) */
|
||||
private const PUSH_URL = 'http://127.0.0.1:2347/push';
|
||||
|
||||
/** 请求超时(秒),不能太长以免拖慢主流程 */
|
||||
private const TIMEOUT = 2;
|
||||
|
||||
/**
|
||||
* 广播工单变更
|
||||
*
|
||||
* @param int|string $ticketId 工单ID
|
||||
* @param string $action create|update|delete
|
||||
* @return bool 通知是否成功发出(false 不表示业务失败)
|
||||
*/
|
||||
public static function notify($ticketId, $action = 'create')
|
||||
{
|
||||
$payload = json_encode([
|
||||
'action' => (string)$action,
|
||||
'ticket_id' => (int)$ticketId,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => "Content-Type: application/json\r\n",
|
||||
'content' => $payload,
|
||||
'timeout' => self::TIMEOUT,
|
||||
'ignore_errors' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
try {
|
||||
$result = @file_get_contents(self::PUSH_URL, false, $context);
|
||||
return $result !== false;
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,107 +6,101 @@ use app\common\model\Tickets;
|
||||
use app\common\model\CompanyOperators;
|
||||
use app\common\model\Companys;
|
||||
use app\common\model\User;
|
||||
|
||||
use think\facade\Cache;
|
||||
use think\facade\Db;
|
||||
|
||||
class TicketService
|
||||
{
|
||||
/**
|
||||
* 获取工单列表业务逻辑
|
||||
* @param array $params 外部传入的过滤参数
|
||||
* @param array $params 过滤参数
|
||||
* @param int $page 页码
|
||||
* @param int $limit 每页数量
|
||||
* @param string $order 排序方式
|
||||
*/
|
||||
|
||||
public function getTicketList(array $params)
|
||||
public function queryTickets(array $params, int $page, int $limit, string $order)
|
||||
{
|
||||
try {
|
||||
// 查询构造器
|
||||
$query = Tickets::with(['ticketStaff', 'ticketCompany', 'ticketOperator']);
|
||||
|
||||
$query = Tickets::with(['ticketStaff', 'ticketCompany', 'ticketOperator']); // 查询构造器
|
||||
$equalFilters = ['operator_id', 'user_id', 'status'];
|
||||
foreach ($equalFilters as $field) {
|
||||
if (isset($params[$field]) && $params[$field] !== '') {
|
||||
$query->where($field, $params[$field]);
|
||||
}
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if ($params['status'] !== null) {
|
||||
$query->where('status', $params['status']);
|
||||
if (!empty($params['searchKeyword'])) { // 模糊搜索(标题,备注)
|
||||
$query->where(function ($q) use ($params) {
|
||||
$q->where('website', 'like', '%' . $params['searchKeyword'] . '%')
|
||||
->whereOr('remark', 'like', '%' . $params['searchKeyword'] . '%');
|
||||
});
|
||||
}
|
||||
|
||||
$list = $query->order('create_time', $order)
|
||||
->hidden(['ticketStaff', 'ticketCompany', 'ticketOperator', 'company_id', 'operator_id', 'user_id'])
|
||||
->append(['username', 'company_name', 'operator_name'])
|
||||
->paginate(
|
||||
$limit,
|
||||
false,
|
||||
[
|
||||
'page' => $page,
|
||||
'query' => $params
|
||||
]
|
||||
);
|
||||
|
||||
return $list;
|
||||
} catch (\Exception $e) { //数据库连接失败,表不存在
|
||||
throw new \Exception('查询工单列表失败: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// 运营人员筛选
|
||||
if (!empty($params['operator_id'])) {
|
||||
$query->where('operator_id', $params['operator_id']);
|
||||
}
|
||||
|
||||
// 负责人筛选
|
||||
if (!empty($params['user_id'])) {
|
||||
$query->where('user_id', $params['user_id']);
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if (!empty($params['searchKeyword'])) {
|
||||
$query->where(function ($q) use ($params) {
|
||||
$q->where('website', 'like', '%' . $params['searchKeyword'] . '%')
|
||||
->whereOr('remark', 'like', '%' . $params['searchKeyword'] . '%');
|
||||
});
|
||||
}
|
||||
|
||||
$order = strtolower($params['sortOrder']) === 'asc' ? 'asc' : 'desc';
|
||||
|
||||
|
||||
// order -- 排序 , paginate -- 分页(查询当前页的数据,还会自动计算总记录数)
|
||||
$list = $query->order('create_time', $order)->paginate(
|
||||
$params['limit'],
|
||||
false,
|
||||
[
|
||||
'page' => $params['page'],
|
||||
'query' => $params // 保持分页链接的查询参数
|
||||
]
|
||||
);
|
||||
|
||||
$list->each(function ($item) {
|
||||
$item->hidden(['ticketStaff', 'ticketCompany', 'ticketOperator', 'company_id', 'operator_id', 'user_id']);
|
||||
$item->append(['username', 'company_name', 'operator_name']);
|
||||
|
||||
return $item;
|
||||
});
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 新增工单
|
||||
*
|
||||
* 创建工单业务逻辑
|
||||
* @param array $data 工单数据
|
||||
* @return Tickets
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function createTicket($data)
|
||||
public function createTicket(array $data)
|
||||
{
|
||||
if (empty($data['website'])) {
|
||||
throw new \Exception('网站不能为空');
|
||||
if (!isset($data['website']) || trim((string)$data['website']) === '') { // isset检查键存在且不为null
|
||||
throw new \Exception('网站输入不能为空');
|
||||
}
|
||||
|
||||
// 统一查重与落库值
|
||||
$website = trim($data['website']);
|
||||
$data['website'] = $website;
|
||||
|
||||
// 并发锁
|
||||
$lockKey = 'create_ticket_lock_' . md5($website);
|
||||
|
||||
if (Cache::has($lockKey)) {
|
||||
throw new \Exception('当前该网址正在被创建工单中,请稍后重试');
|
||||
}
|
||||
|
||||
Cache::set($lockKey, 1, 5); // 锁定5秒
|
||||
|
||||
try {
|
||||
// 查找是否有未完成的同网站工单
|
||||
$unfinishedTicket = Tickets::where('website', $website)
|
||||
$ticketResult = Db::transaction(function () use ($data, $website) {
|
||||
$unfinishedTicket = Tickets::where('website', $website) // 查找是否有未完成的同网站工单
|
||||
->whereIn('status', [0, 1])
|
||||
->lock(true)
|
||||
->find();
|
||||
|
||||
if ($unfinishedTicket) {
|
||||
Cache::delete($lockKey);
|
||||
throw new \Exception('已存在未完成的同网站工单,无法创建');
|
||||
// 同运营判断(为null或者相同运营)
|
||||
$sameOperator = is_null($unfinishedTicket->operator_id) || $unfinishedTicket->operator_id === (intval($data['operator_id']) ?? null);
|
||||
|
||||
if ($sameOperator) {
|
||||
throw new \Exception('存在相同未完成工单(相同运营),禁止重复创建'); // 默认400
|
||||
}
|
||||
|
||||
if (empty($data['confirm'])) {
|
||||
throw new \Exception('存在相同未完成工单(不同运营),是否继续创建?', 409);
|
||||
}
|
||||
}
|
||||
|
||||
// 当前无相同网站
|
||||
$ticketResult = Tickets::create($data);
|
||||
return $ticketResult;
|
||||
} catch (\Exception $e) {
|
||||
Cache::delete($lockKey);
|
||||
throw $e;
|
||||
}
|
||||
unset($data['confirm']); // 移除确认标记,避免落库
|
||||
|
||||
return Tickets::create($data); // 创建新工单
|
||||
|
||||
});
|
||||
|
||||
// 通知前端实时刷新(静默,广播服务不可用不影响主流程)
|
||||
TicketNotifier::notify($ticketResult->id, 'create');
|
||||
|
||||
return $ticketResult;
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +112,7 @@ class TicketService
|
||||
* @param $role 当前登录用户角色
|
||||
* @return array
|
||||
*/
|
||||
public function updateTicket($id, $data, $allowFields, $isRole, $isSuper = false)
|
||||
public function updateTicket(int $id, array $data, array $allowFields, $isRole, $isSuper = false)
|
||||
{
|
||||
$ticket = Tickets::where('id', $id)->find();
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ class ClientTicket extends BaseController
|
||||
$clientCompanyId = Session::get('user_info.company_id'); // 获取当前登录客户ID
|
||||
try {
|
||||
$tickets = $ClientTicketService->getClientTickets($clientCompanyId);
|
||||
return json(['code' => 200, 'msg' => '获取成功', 'data' => $tickets]);
|
||||
return $this->success('获取成功', $tickets);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 400, 'msg' => $e->getMessage(), 'data' => null]);
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ use think\facade\Session;
|
||||
// 已完成
|
||||
class Login extends BaseController
|
||||
{
|
||||
|
||||
/*
|
||||
处理登录请求(已完成)
|
||||
*/
|
||||
@@ -18,16 +17,25 @@ class Login extends BaseController
|
||||
{
|
||||
$params = $this->request->only(['username', 'password'], 'post');
|
||||
|
||||
// 验证数据
|
||||
if (empty($params['username']) || empty($params['password'])) {
|
||||
return json(['code' => 400, 'message' => '用户名和密码不能为空']);
|
||||
if (empty(trim($params['username'])) || empty(trim($params['password']))) { // 验证数据
|
||||
return $this->error('用户名和密码不能为空');
|
||||
}
|
||||
|
||||
$username = trim($params['username']);
|
||||
$failKey = 'login_fail_' . md5($username);
|
||||
$fails = (int) cache($failKey); // 读取失败次数,并强制转为整数
|
||||
|
||||
if ($fails >= 5) {
|
||||
return $this->error('失败次数过多,请 15 分钟后再试', [], 429);
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $authService->login($params['username'], $params['password']);
|
||||
return json(['code' => 200, 'message' => '登录成功', 'data' => $result]);
|
||||
$result = $authService->login(trim($params['username']), trim($params['password']));
|
||||
cache($failKey, null);
|
||||
return $this->success('登录成功', $result);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 400, 'message' => $e->getMessage(), 'data' => []]);
|
||||
cache($failKey, $fails + 1, 900); // 失败次数存活时间,15分钟之后自动清零
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +45,6 @@ class Login extends BaseController
|
||||
public function logout()
|
||||
{
|
||||
Session::clear();
|
||||
return json(['code' => 200, 'message' => '退出成功']);
|
||||
return $this->success('退出成功');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@ class Ticket extends BaseController
|
||||
|
||||
try {
|
||||
$result = $ticketService->updateTicket($id, $data, $allowFields, $isUser, false);
|
||||
return json(['code' => 200, 'msg' => '更新成功', 'data' => $result]);
|
||||
return $this->success('更新成功', $result);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 400, 'msg' => $e->getMessage()]);
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\middleware;
|
||||
use app\common\Trait\ApiResponse;
|
||||
use think\facade\Session;
|
||||
class CheckLogin
|
||||
{
|
||||
use ApiResponse;
|
||||
|
||||
/**
|
||||
* 处理请求
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @param \Closure $next
|
||||
* @return Response
|
||||
@@ -15,9 +17,8 @@ class CheckLogin
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
if (!Session::has('user_info')) {
|
||||
return json(['code' => 401, 'message' => '未登录']);
|
||||
return $this->error('未登录', [], 401);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"topthink/framework": "^8.0",
|
||||
"topthink/think-orm": "^3.0|^4.0",
|
||||
"topthink/think-filesystem": "^2.0|^3.0",
|
||||
"topthink/think-multi-app": "^1.1"
|
||||
"topthink/think-multi-app": "^1.1",
|
||||
"workerman/workerman": "^4.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"topthink/think-dumper": "^1.0",
|
||||
|
||||
@@ -15,7 +15,7 @@ return [
|
||||
// 时间字段取出后的默认时间格式
|
||||
'datetime_format' => 'Y-m-d H:i:s',
|
||||
|
||||
// 时间字段配置 配置格式:create_time,update_time
|
||||
// 时间字段配置 配置格式:create_time,update_time
|
||||
'datetime_field' => '',
|
||||
|
||||
// 数据库连接配置信息
|
||||
@@ -59,25 +59,20 @@ return [
|
||||
],
|
||||
'remote' => [
|
||||
// 数据库类型
|
||||
'type' => env('REMOTE_DB_TYPE', 'mysql'),
|
||||
// 服务器地址
|
||||
'hostname' => env('REMOTE_DB_HOST', '127.0.0.1'),
|
||||
// 数据库名
|
||||
'database' => env('REMOTE_DB_NAME', ''),
|
||||
// 用户名
|
||||
'username' => env('REMOTE_DB_USER', 'root'),
|
||||
// 密码
|
||||
'password' => env('REMOTE_DB_PASS', ''),
|
||||
'type' => 'mysql',
|
||||
// 服务器地址(必须从 .env 读取)
|
||||
'hostname' => env('REMOTE_DB_HOST'),
|
||||
// 数据库名(必须从 .env 读取)
|
||||
'database' => env('REMOTE_DB_NAME'),
|
||||
// 用户名(必须从 .env 读取)
|
||||
'username' => env('REMOTE_DB_USER'),
|
||||
// 密码(必须从 .env 读取)
|
||||
'password' => env('REMOTE_DB_PASS'),
|
||||
// 端口
|
||||
'hostport' => env('REMOTE_DB_PORT', '3306'),
|
||||
// 数据库编码
|
||||
'charset' => env('REMOTE_DB_CHARSET', 'utf8mb4'),
|
||||
// 数据库表前缀
|
||||
'charset' => 'utf8mb4',
|
||||
'prefix' => '',
|
||||
// 远程连接建议开启断线重连
|
||||
'break_reconnect' => true,
|
||||
],
|
||||
|
||||
// 更多的数据库配置信息
|
||||
],
|
||||
];
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
如果不使用模板,可以删除该目录
|
||||
79
worker/websocket.php
Normal file
79
worker/websocket.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
/**
|
||||
* Workerman 广播服务
|
||||
* ------------------------------------------------------------
|
||||
* 启动: php worker/websocket.php start
|
||||
* 停止: 前台运行按 Ctrl+C 即可(Windows 不支持 stop 命令)
|
||||
*
|
||||
* 端口:
|
||||
* 2346 websocket 端口,员工前端长连接(只推送,不做业务)
|
||||
* 2347 http 触发端口,仅监听 127.0.0.1,供 ThinkPHP 内部调用
|
||||
*
|
||||
* 触发方式:POST http://127.0.0.1:2347/push
|
||||
* body: {"action":"create","ticket_id":123}
|
||||
*
|
||||
* 注意:
|
||||
* - Workerman 在 Windows 上不支持一个文件声明多个 worker,
|
||||
* 因此这里采用官方推荐的「单 worker + onWorkerStart 动态 listen()」方案,
|
||||
* 两个端口跑在同一个进程内,共享在线连接表。
|
||||
* - 部署到 Linux 后此写法同样适用,可额外增加 Worker::count 多进程。
|
||||
*/
|
||||
|
||||
use Workerman\Worker;
|
||||
use Workerman\Protocols\Http\Request;
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
// 当前在线前端连接(connection id => connection)
|
||||
$clients = [];
|
||||
|
||||
// ---------- 主 worker:WebSocket 服务(员工前端连接) ----------
|
||||
$wsWorker = new Worker('websocket://0.0.0.0:2346');
|
||||
$wsWorker->name = 'ticket-broadcast';
|
||||
|
||||
$wsWorker->onConnect = function ($connection) use (&$clients) {
|
||||
$connection->cid = spl_object_id($connection);
|
||||
$clients[$connection->cid] = $connection;
|
||||
echo '[' . date('Y-m-d H:i:s') . "] client connected, online: " . count($clients) . PHP_EOL;
|
||||
};
|
||||
|
||||
$wsWorker->onClose = function ($connection) use (&$clients) {
|
||||
if (isset($clients[$connection->cid])) {
|
||||
unset($clients[$connection->cid]);
|
||||
}
|
||||
echo '[' . date('Y-m-d H:i:s') . "] client closed, online: " . count($clients) . PHP_EOL;
|
||||
};
|
||||
|
||||
// ---------- 进程启动后追加 HTTP 触发端口(仅本机) ----------
|
||||
$wsWorker->onWorkerStart = function ($worker) use (&$clients) {
|
||||
$httpWorker = new Worker('http://127.0.0.1:2347');
|
||||
$httpWorker->name = 'ticket-push-trigger';
|
||||
|
||||
$httpWorker->onMessage = function ($connection, Request $request) use (&$clients) {
|
||||
if ($request->method() === 'POST' && $request->path() === '/push') {
|
||||
$data = json_decode($request->rawBody(), true) ?: [];
|
||||
|
||||
$payload = json_encode([
|
||||
'type' => 'ticket_changed',
|
||||
'action' => isset($data['action']) ? (string)$data['action'] : 'update',
|
||||
'ticket_id' => isset($data['ticket_id']) ? (int)$data['ticket_id'] : null,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
$sent = 0;
|
||||
foreach ($clients as $client) {
|
||||
$client->send($payload);
|
||||
$sent++;
|
||||
}
|
||||
|
||||
echo '[' . date('Y-m-d H:i:s') . "] push [{$payload}] to {$sent} clients" . PHP_EOL;
|
||||
$connection->send('ok');
|
||||
} else {
|
||||
$connection->send('bad request');
|
||||
}
|
||||
};
|
||||
|
||||
// 同一进程内动态监听第二端口(Windows 下多 worker 的限制的官方解法)
|
||||
$httpWorker->listen();
|
||||
};
|
||||
|
||||
Worker::runAll();
|
||||
Reference in New Issue
Block a user