Compare commits
22 Commits
ae001252b0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9aa0006990 | |||
| 396556a30f | |||
| 831113e22f | |||
| b8cd7e7658 | |||
| d4c08a8cdf | |||
| 98b60c333a | |||
| e831ce810b | |||
| 9ffc9687d0 | |||
| 8380b8797b | |||
| b4e5e98353 | |||
| 83c0865272 | |||
| 9789a64ebb | |||
| 9f8d6d629a | |||
| 247e113014 | |||
| 77930b3232 | |||
| cd1175b304 | |||
| 131cbccb35 | |||
| c30385b073 | |||
| 02a7a4e518 | |||
| 0d82bd27b3 | |||
| bcae1cd06e | |||
| d332721f5c |
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
|
/.buildpath
|
||||||
/.project
|
/.project
|
||||||
/runtime
|
/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,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace app;
|
namespace app;
|
||||||
@@ -6,12 +7,15 @@ namespace app;
|
|||||||
use think\App;
|
use think\App;
|
||||||
use think\exception\ValidateException;
|
use think\exception\ValidateException;
|
||||||
use think\Validate;
|
use think\Validate;
|
||||||
|
use app\common\Trait\ApiResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 控制器基础类
|
* 控制器基础类
|
||||||
*/
|
*/
|
||||||
abstract class BaseController
|
abstract class BaseController
|
||||||
{
|
{
|
||||||
|
use ApiResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request实例
|
* Request实例
|
||||||
* @var \think\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);
|
return $v->failException(true)->check($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace app;
|
namespace app;
|
||||||
|
|
||||||
use think\db\exception\DataNotFoundException;
|
use think\db\exception\DataNotFoundException;
|
||||||
@@ -9,12 +10,15 @@ use think\exception\HttpResponseException;
|
|||||||
use think\exception\ValidateException;
|
use think\exception\ValidateException;
|
||||||
use think\Response;
|
use think\Response;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
use app\common\Trait\ApiResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用异常处理类
|
* 应用异常处理类
|
||||||
*/
|
*/
|
||||||
class ExceptionHandle extends Handle
|
class ExceptionHandle extends Handle
|
||||||
{
|
{
|
||||||
|
use ApiResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 不需要记录信息(日志)的异常类列表
|
* 不需要记录信息(日志)的异常类列表
|
||||||
* @var array
|
* @var array
|
||||||
@@ -50,7 +54,20 @@ class ExceptionHandle extends Handle
|
|||||||
*/
|
*/
|
||||||
public function render($request, Throwable $e): Response
|
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);
|
return parent::render($request, $e);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class AccountManagement extends BaseController
|
|||||||
public function getAllUsers(AccountService $accountService)
|
public function getAllUsers(AccountService $accountService)
|
||||||
{
|
{
|
||||||
$result = $accountService->getAllUsers();
|
$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'])) {
|
if (empty($params['username'])) {
|
||||||
return json(['code' => 400, 'message' => '缺少必要参数']);
|
return $this->error('缺少必要参数');
|
||||||
}
|
}
|
||||||
|
|
||||||
# 业务逻辑
|
# 业务逻辑
|
||||||
try {
|
try {
|
||||||
$result = $accountService->createAccount($params);
|
$result = $accountService->createAccount($params);
|
||||||
return json(['code' => 200, 'message' => '账户创建成功', 'data' => $result]);
|
return $this->success('账户创建成功', $result);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
return $this->error($e->getMessage());
|
||||||
return json([
|
|
||||||
'code' => 400,
|
|
||||||
'message' => $e->getMessage(),
|
|
||||||
'data' => null
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,9 +40,9 @@ class AccountManagement extends BaseController
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$result = $accountService->updateAccountStatus($id, $status);
|
$result = $accountService->updateAccountStatus($id, $status);
|
||||||
return json(['code' => 200, 'message' => '账户状态更新成功', 'data' => $result]);
|
return $this->success('账户状态更新成功', $result);
|
||||||
} catch (\Exception $e) {
|
} 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 {
|
try {
|
||||||
$result = $accountService->deleteAccount($id);
|
$result = $accountService->deleteAccount($id);
|
||||||
return json(['code' => 200, 'message' => '账户删除成功', 'data' => $result]);
|
return $this->success('账户删除成功', $result);
|
||||||
} catch (\Exception $e) {
|
} 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)
|
public function getAllOperators(TicketService $ticketService)
|
||||||
{
|
{
|
||||||
|
|
||||||
$result = $ticketService->getAllOperators();
|
$result = $ticketService->getAllOperators();
|
||||||
|
return $this->success('查询成功', $result);
|
||||||
return json(['code' => 200, 'msg' => '查询成功', 'data' => $result]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -26,7 +24,7 @@ class CompanyAndOperators extends BaseController
|
|||||||
|
|
||||||
$result = $ticketService->TestgetAllOperators();
|
$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)
|
public function getStaffList(TicketService $ticketService)
|
||||||
{
|
{
|
||||||
$result = $ticketService->getStaffList();
|
$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);
|
$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);
|
$result = $CompanyOperatorService->deleteCompany($id);
|
||||||
if (!$result) {
|
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();
|
$data = $this->request->post();
|
||||||
try {
|
try {
|
||||||
$result = $CompanyOperatorService->addCompany($data);
|
$result = $CompanyOperatorService->addCompany($data);
|
||||||
return json(['code' => 200, 'msg' => '创建成功', 'data' => $result]);
|
return $this->success('创建成功', $result);
|
||||||
} catch (\Exception $e) {
|
} 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();
|
$data = $this->request->post();
|
||||||
try {
|
try {
|
||||||
$result = $CompanyOperatorService->addOperator($data);
|
$result = $CompanyOperatorService->addOperator($data);
|
||||||
return json(['code' => 200, 'msg' => '创建成功', 'data' => $result]);
|
return $this->success('创建成功', $result);
|
||||||
} catch (\Exception $e) {
|
} 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\BaseController;
|
||||||
use app\common\model\Tickets;
|
use app\common\model\Tickets;
|
||||||
|
use app\common\service\TicketNotifier;
|
||||||
use app\common\service\TicketService;
|
use app\common\service\TicketService;
|
||||||
use think\facade\Session;
|
use think\facade\Session;
|
||||||
|
|
||||||
@@ -16,22 +17,24 @@ class Ticket extends BaseController
|
|||||||
*/
|
*/
|
||||||
public function ticketsFind(TicketService $ticketService)
|
public function ticketsFind(TicketService $ticketService)
|
||||||
{
|
{
|
||||||
|
|
||||||
// 在控制器明确接收参数
|
// 在控制器明确接收参数
|
||||||
$params = [
|
$params = [
|
||||||
"page" => $this->request->param('page', 1, 'intval'),
|
"page" => $this->request->param('page', 1, 'intval'),
|
||||||
"limit" => $this->request->param('limit', 20, 'intval'),
|
"limit" => $this->request->param('limit', 20, 'intval'),
|
||||||
"status" => $this->request->param('status', null),
|
|
||||||
"operator_id" => $this->request->param('operator_id', null),
|
"operator_id" => $this->request->param('operator_id', null),
|
||||||
"user_id" => $this->request->param('user_id', null),
|
"user_id" => $this->request->param('user_id', null),
|
||||||
|
"status" => $this->request->param('status', null),
|
||||||
"searchKeyword" => $this->request->param('search', null),
|
"searchKeyword" => $this->request->param('search', null),
|
||||||
"sortOrder" => $this->request->param('sort_order', 'desc'),
|
"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 {
|
try {
|
||||||
$result = $ticketService->createTicket($data);
|
$result = $ticketService->createTicket($data);
|
||||||
return json(['code' => 200, 'msg' => '创建成功', 'data' => $result]);
|
return $this->success('创建成功', $result);
|
||||||
} catch (\Exception $e) {
|
} 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 {
|
try {
|
||||||
$result = $ticketService->updateTicket($id, $data, $allowFields, $adminId, true);
|
$result = $ticketService->updateTicket($id, $data, $allowFields, $adminId, true);
|
||||||
return json(['code' => 200, 'msg' => '更新成功', 'data' => $result]);
|
return $this->success('更新成功', $result);
|
||||||
} catch (\Exception $e) {
|
} 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)
|
public function getTicketDashboardStats(TicketService $ticketService)
|
||||||
{
|
{
|
||||||
$result = $ticketService->getTicketDashboardStats();
|
$result = $ticketService->getTicketDashboardStats();
|
||||||
return json(['code' => 200, 'msg' => '查询成功', 'data' => $result]);
|
return $this->success('查询成功', $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,21 +91,21 @@ class Ticket extends BaseController
|
|||||||
*/
|
*/
|
||||||
public function batchUpdate(TicketService $ticketService)
|
public function batchUpdate(TicketService $ticketService)
|
||||||
{
|
{
|
||||||
$adminId = Session::get('user_info.id'); // 获取当前登录管理员ID
|
$adminId = Session::get('user_info.id'); // 获取管理员身份
|
||||||
$data = $this->request->post();
|
$data = $this->request->post();
|
||||||
|
|
||||||
$ids = $data['ids'] ?? [];
|
$ids = $data['ids'] ?? [];
|
||||||
if (!is_array($ids) || empty($ids)) {
|
if (!is_array($ids) || empty($ids)) {
|
||||||
return json(['code' => 400, 'msg' => 'ids 必须是非空数组', 'data' => []]);
|
return $this->error('ids 必须是非空数组');
|
||||||
}
|
}
|
||||||
|
|
||||||
$allowFields = ['status', 'lock_time'];
|
$allowFields = ['status', 'lock_time'];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$result = $ticketService->batchUpdateTickets($ids, $data, $allowFields, $adminId, true);
|
$result = $ticketService->batchUpdateTickets($ids, $data, $allowFields, $adminId, true);
|
||||||
return json(['code' => 200, 'msg' => '批量更新成功', 'data' => $result]);
|
return $this->success('批量更新成功', $result);
|
||||||
} catch (\Exception $e) {
|
} 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)
|
public function deleteTicket($id)
|
||||||
{
|
{
|
||||||
$ticket = Tickets::find($id);
|
$ticket = Tickets::find($id);
|
||||||
if (!$ticket) return json(['code' => false, 'msg' => '工单不存在', 'data' => []]);
|
if (!$ticket) return $this->error('工单不存在');
|
||||||
|
|
||||||
return $ticket->delete()
|
$deleted = $ticket->delete();
|
||||||
? json(['code' => true, 'msg' => '删除成功', 'data' => []])
|
if (!$deleted) {
|
||||||
: json(['code' => false, 'msg' => '删除失败', 'data' => []]);
|
return $this->error('删除失败', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知前端实时刷新(静默,广播服务不可用不影响删除流程)
|
||||||
|
TicketNotifier::notify($id, 'delete');
|
||||||
|
|
||||||
|
return $this->success('删除成功', []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ class Utill extends BaseController
|
|||||||
# session中获取当前登录是否是管理员
|
# session中获取当前登录是否是管理员
|
||||||
$isAdmin = Session::get('user_info.role') === 'admin' ? true : false; // 后续用来判断
|
$isAdmin = Session::get('user_info.role') === 'admin' ? true : false; // 后续用来判断
|
||||||
if (!$isAdmin) {
|
if (!$isAdmin) {
|
||||||
return json(['code' => 403, 'msg' => '权限不足', 'data' => []]);
|
return $this->error('权限不足', [], 403);
|
||||||
}
|
}
|
||||||
$data = $UtillService->getExportData($range);
|
$data = $UtillService->getExportData($range);
|
||||||
return json(['code' => 200, 'msg' => '查询成功', 'data' => $data]);
|
return $this->success('查询成功', $data);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return json(['code' => 500, 'msg' => '查询失败', 'data' => []]);
|
return $this->error('查询失败', [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,54 @@
|
|||||||
|
|
||||||
namespace app\admin\middleware;
|
namespace app\admin\middleware;
|
||||||
|
|
||||||
|
use app\common\Trait\ApiResponse;
|
||||||
use think\facade\Session;
|
use think\facade\Session;
|
||||||
|
|
||||||
class Auth
|
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)
|
public function handle($request, \Closure $next)
|
||||||
{
|
{
|
||||||
if (!Session::has('user_info')) {
|
if (!Session::has('user_info')) {
|
||||||
return json(['code' => 401, 'msg' => '未登录,请先登录']);
|
return $this->error('未登录,请先登录', [], 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = Session::get('user_info');
|
$user = Session::get('user_info');
|
||||||
|
$role = $user['role'] ?? 'user';
|
||||||
|
|
||||||
$request->login_user_id = $user['id'] ?? null;
|
$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);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use think\facade\Route;
|
use think\facade\Route;
|
||||||
use app\admin\middleware\Auth;
|
use app\admin\middleware\Auth;
|
||||||
|
use app\middleware\CheckLogin;
|
||||||
|
|
||||||
Route::group(function () {
|
Route::group(function () {
|
||||||
Route::get('ticketsfind', 'Ticket/ticketsFind');
|
Route::get('ticketsfind', 'Ticket/ticketsFind');
|
||||||
@@ -11,7 +12,6 @@ Route::group(function () {
|
|||||||
Route::put('tickets/batch-update', 'Ticket/batchUpdate');
|
Route::put('tickets/batch-update', 'Ticket/batchUpdate');
|
||||||
Route::get('getticketdashboardstats', 'Ticket/getTicketDashboardStats');
|
Route::get('getticketdashboardstats', 'Ticket/getTicketDashboardStats');
|
||||||
|
|
||||||
|
|
||||||
Route::get('getUserStaffList', 'CompanyAndOperators/getStaffList');
|
Route::get('getUserStaffList', 'CompanyAndOperators/getStaffList');
|
||||||
Route::get('operators', 'CompanyAndOperators/getAllOperators');
|
Route::get('operators', 'CompanyAndOperators/getAllOperators');
|
||||||
Route::get('getOperatorList', 'CompanyAndOperators/TestgetAllOperators');
|
Route::get('getOperatorList', 'CompanyAndOperators/TestgetAllOperators');
|
||||||
@@ -26,4 +26,5 @@ Route::group(function () {
|
|||||||
Route::delete('deleteAccount/:id', 'AccountManagement/deleteAccount');
|
Route::delete('deleteAccount/:id', 'AccountManagement/deleteAccount');
|
||||||
|
|
||||||
Route::post('getExportData', 'Utill/getExportData');
|
Route::post('getExportData', 'Utill/getExportData');
|
||||||
})->middleware(Auth::class);
|
})->middleware([]);
|
||||||
|
// [CheckLogin::class, Auth::class]
|
||||||
@@ -2,65 +2,45 @@
|
|||||||
|
|
||||||
namespace app\common\Trait;
|
namespace app\common\Trait;
|
||||||
|
|
||||||
use think\Response;
|
use think\response\Json;
|
||||||
|
|
||||||
/**
|
|
||||||
* API 响应 Trait
|
|
||||||
* 提供统一的 JSON 响应格式
|
|
||||||
*/
|
|
||||||
trait ApiResponse
|
trait ApiResponse
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* 成功响应
|
* 成功响应
|
||||||
*
|
* @param string $message 提示信息
|
||||||
* @param mixed $data 响应数据
|
* @param mixed $data 返回数据
|
||||||
* @param string $msg 响应消息
|
* @param int $code 自定义业务状态码
|
||||||
* @param int $code 响应码
|
* @return Json
|
||||||
* @param int $httpCode HTTP状态码
|
|
||||||
* @return Response
|
|
||||||
*/
|
*/
|
||||||
protected function successResponse($data = [], $msg = '成功', $code = 200, $httpCode = 200)
|
protected function success(string $message = '操作成功', mixed $data = [], int $code = 200): Json
|
||||||
{
|
{
|
||||||
return json([
|
return $this->jsonResponse($message, $data, $code);
|
||||||
'code' => $code,
|
|
||||||
'msg' => $msg,
|
|
||||||
'data' => $data
|
|
||||||
], $httpCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 错误响应
|
* 失败响应
|
||||||
*
|
* @param string $message 错误提示信息
|
||||||
* @param string $msg 错误消息
|
* @param int $code 自定义业务状态码
|
||||||
* @param mixed $data 错误数据
|
* @param mixed $data 额外的错误数据(如验证未通过的具体字段)
|
||||||
* @param int $code 错误码
|
* @return Json
|
||||||
* @param int $httpCode HTTP状态码
|
|
||||||
* @return Response
|
|
||||||
*/
|
*/
|
||||||
protected function errorResponse($msg = '失败', $data = null, $code = 400, $httpCode = 400)
|
protected function error(string $message = '操作失败', mixed $data = [], int $code = 400): Json
|
||||||
{
|
{
|
||||||
return json([
|
return $this->jsonResponse($message, $data, $code);
|
||||||
'code' => $code,
|
|
||||||
'msg' => $msg,
|
|
||||||
'data' => $data
|
|
||||||
], $httpCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自定义响应
|
* 统一 JSON 返回格式
|
||||||
*
|
|
||||||
* @param int $code 响应码
|
|
||||||
* @param string $msg 响应消息
|
|
||||||
* @param mixed $data 响应数据
|
|
||||||
* @param int $httpCode HTTP状态码
|
|
||||||
* @return Response
|
|
||||||
*/
|
*/
|
||||||
protected function customResponse($code, $msg, $data = [], $httpCode = 200)
|
private function jsonResponse(string $message, mixed $data, int $code): Json
|
||||||
{
|
{
|
||||||
return json([
|
$result = [
|
||||||
'code' => $code,
|
'code' => $code,
|
||||||
'msg' => $msg,
|
'msg' => $message,
|
||||||
'data' => $data
|
'data' => $data,
|
||||||
], $httpCode);
|
];
|
||||||
|
|
||||||
|
return json($result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,19 +24,20 @@ class AuthService
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($user['status'] != 1) {
|
if ($user['status'] != 1) {
|
||||||
throw new \Exception('账户已禁用');
|
throw new \Exception('账户已禁用,请联系管理员');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$userInfo = [
|
$userInfo = [
|
||||||
|
'id' => $user['id'], // 用户ID
|
||||||
|
'company_id' => $user['company_id'], // 所属公司ID
|
||||||
'company_name' => $user['company']['company_name'], // 所属公司名称
|
'company_name' => $user['company']['company_name'], // 所属公司名称
|
||||||
'username' => $user['username'],
|
'username' => $user['username'],
|
||||||
'role' => $user['role'],
|
'role' => $user['role'],
|
||||||
];
|
];
|
||||||
|
|
||||||
Session::set('user_info', $user);
|
Session::set('user_info', $userInfo);
|
||||||
|
|
||||||
return $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,109 +6,103 @@ use app\common\model\Tickets;
|
|||||||
use app\common\model\CompanyOperators;
|
use app\common\model\CompanyOperators;
|
||||||
use app\common\model\Companys;
|
use app\common\model\Companys;
|
||||||
use app\common\model\User;
|
use app\common\model\User;
|
||||||
|
use think\facade\Db;
|
||||||
use think\facade\Cache;
|
|
||||||
|
|
||||||
class TicketService
|
class TicketService
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* 获取工单列表业务逻辑
|
* 获取工单列表业务逻辑
|
||||||
* @param array $params 外部传入的过滤参数
|
* @param array $params 过滤参数
|
||||||
|
* @param int $page 页码
|
||||||
|
* @param int $limit 每页数量
|
||||||
|
* @param string $order 排序方式
|
||||||
*/
|
*/
|
||||||
|
public function queryTickets(array $params, int $page, int $limit, string $order)
|
||||||
public function getTicketList(array $params)
|
|
||||||
{
|
{
|
||||||
|
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] !== '') {
|
||||||
if ($params['status'] !== null) {
|
$query->where($field, $params[$field]);
|
||||||
$query->where('status', $params['status']);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 运营人员筛选
|
if (!empty($params['searchKeyword'])) { // 模糊搜索(标题,备注)
|
||||||
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) {
|
$query->where(function ($q) use ($params) {
|
||||||
$q->where('website', 'like', '%' . $params['searchKeyword'] . '%')
|
$q->where('website', 'like', '%' . $params['searchKeyword'] . '%')
|
||||||
->whereOr('remark', 'like', '%' . $params['searchKeyword'] . '%');
|
->whereOr('remark', 'like', '%' . $params['searchKeyword'] . '%');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$order = strtolower($params['sortOrder']) === 'asc' ? 'asc' : 'desc';
|
$list = $query->order('create_time', $order)
|
||||||
|
->hidden(['ticketStaff', 'ticketCompany', 'ticketOperator', 'company_id', 'operator_id', 'user_id'])
|
||||||
|
->append(['username', 'company_name', 'operator_name'])
|
||||||
// order -- 排序 , paginate -- 分页(查询当前页的数据,还会自动计算总记录数)
|
->paginate(
|
||||||
$list = $query->order('create_time', $order)->paginate(
|
$limit,
|
||||||
$params['limit'],
|
|
||||||
false,
|
false,
|
||||||
[
|
[
|
||||||
'page' => $params['page'],
|
'page' => $page,
|
||||||
'query' => $params // 保持分页链接的查询参数
|
'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;
|
return $list;
|
||||||
|
} catch (\Exception $e) { //数据库连接失败,表不存在
|
||||||
|
throw new \Exception('查询工单列表失败: ' . $e->getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新增工单
|
* 创建工单业务逻辑
|
||||||
*
|
* @param array $data 工单数据
|
||||||
|
* @return Tickets
|
||||||
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
public function createTicket($data)
|
public function createTicket(array $data)
|
||||||
{
|
{
|
||||||
if (empty($data['website'])) {
|
if (!isset($data['website']) || trim((string)$data['website']) === '') { // isset检查键存在且不为null
|
||||||
throw new \Exception('网站不能为空');
|
throw new \Exception('网站输入不能为空');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 统一查重与落库值
|
||||||
$website = trim($data['website']);
|
$website = trim($data['website']);
|
||||||
|
$data['website'] = $website;
|
||||||
|
|
||||||
// 并发锁
|
$ticketResult = Db::transaction(function () use ($data, $website) {
|
||||||
$lockKey = 'create_ticket_lock_' . md5($website);
|
$unfinishedTicket = Tickets::where('website', $website) // 查找是否有未完成的同网站工单
|
||||||
|
|
||||||
if (Cache::has($lockKey)) {
|
|
||||||
throw new \Exception('当前该网址正在被创建工单中,请稍后重试');
|
|
||||||
}
|
|
||||||
|
|
||||||
Cache::set($lockKey, 1, 5); // 锁定5秒
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 查找是否有未完成的同网站工单
|
|
||||||
$unfinishedTicket = Tickets::where('website', $website)
|
|
||||||
->whereIn('status', [0, 1])
|
->whereIn('status', [0, 1])
|
||||||
|
->lock(true)
|
||||||
->find();
|
->find();
|
||||||
|
|
||||||
if ($unfinishedTicket) {
|
if ($unfinishedTicket) {
|
||||||
Cache::delete($lockKey);
|
// 同运营判断(为null或者相同运营)
|
||||||
throw new \Exception('已存在未完成的同网站工单,无法创建');
|
$sameOperator = is_null($unfinishedTicket->operator_id) || $unfinishedTicket->operator_id === (intval($data['operator_id']) ?? null);
|
||||||
|
|
||||||
|
if ($sameOperator) {
|
||||||
|
throw new \Exception('存在相同未完成工单(相同运营),禁止重复创建'); // 默认400
|
||||||
}
|
}
|
||||||
|
|
||||||
// 当前无相同网站
|
if (empty($data['confirm'])) {
|
||||||
$ticketResult = Tickets::create($data);
|
throw new \Exception('存在相同未完成工单(不同运营),是否继续创建?', 409);
|
||||||
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 当前登录用户角色
|
* @param $role 当前登录用户角色
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function updateTicket($id, $data, $allowFields, $uid, $isSuper = false)
|
public function updateTicket(int $id, array $data, array $allowFields, $isRole, $isSuper = false)
|
||||||
{
|
{
|
||||||
$ticket = Tickets::where('id', $id)->find();
|
$ticket = Tickets::where('id', $id)->find();
|
||||||
|
|
||||||
@@ -126,14 +120,6 @@ class TicketService
|
|||||||
throw new \Exception('工单不存在');
|
throw new \Exception('工单不存在');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 当不是管理员,进入判断
|
|
||||||
if (!$isSuper) {
|
|
||||||
if (!is_null($ticket->user_id) && $ticket->user_id != $uid) {
|
|
||||||
throw new \Exception('没有权限修改该工单');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 提取 $allowFields 允许修改的字段
|
// 提取 $allowFields 允许修改的字段
|
||||||
$updateData = [];
|
$updateData = [];
|
||||||
foreach ($allowFields as $field) {
|
foreach ($allowFields as $field) {
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ class ClientTicket extends BaseController
|
|||||||
$clientCompanyId = Session::get('user_info.company_id'); // 获取当前登录客户ID
|
$clientCompanyId = Session::get('user_info.company_id'); // 获取当前登录客户ID
|
||||||
try {
|
try {
|
||||||
$tickets = $ClientTicketService->getClientTickets($clientCompanyId);
|
$tickets = $ClientTicketService->getClientTickets($clientCompanyId);
|
||||||
return json(['code' => 200, 'msg' => '获取成功', 'data' => $tickets]);
|
return $this->success('获取成功', $tickets);
|
||||||
} catch (\Exception $e) {
|
} 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
|
class Login extends BaseController
|
||||||
{
|
{
|
||||||
|
|
||||||
/*
|
/*
|
||||||
处理登录请求(已完成)
|
处理登录请求(已完成)
|
||||||
*/
|
*/
|
||||||
@@ -18,18 +17,26 @@ class Login extends BaseController
|
|||||||
{
|
{
|
||||||
$params = $this->request->only(['username', 'password'], 'post');
|
$params = $this->request->only(['username', 'password'], 'post');
|
||||||
|
|
||||||
// 验证数据
|
if (empty(trim($params['username'])) || empty(trim($params['password']))) { // 验证数据
|
||||||
if (empty($params['username']) || empty($params['password'])) {
|
return $this->error('用户名和密码不能为空');
|
||||||
return json(['code' => 400, 'message' => '用户名和密码不能为空']);
|
}
|
||||||
|
|
||||||
|
$username = trim($params['username']);
|
||||||
|
$failKey = 'login_fail_' . md5($username);
|
||||||
|
$fails = (int) cache($failKey); // 读取失败次数,并强制转为整数
|
||||||
|
|
||||||
|
if ($fails >= 5) {
|
||||||
|
return $this->error('失败次数过多,请 15 分钟后再试', [], 429);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$result = $authService->login($params['username'], $params['password']);
|
$result = $authService->login(trim($params['username']), trim($params['password']));
|
||||||
|
cache($failKey, null);
|
||||||
|
return $this->success('登录成功', $result);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return json(['code' => 400, 'message' => $e->getMessage(),'data' => []]);
|
cache($failKey, $fails + 1, 900); // 失败次数存活时间,15分钟之后自动清零
|
||||||
|
return $this->error($e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return json(['code' => 200, 'message' => '登录成功', 'data' => $result]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -38,6 +45,6 @@ class Login extends BaseController
|
|||||||
public function logout()
|
public function logout()
|
||||||
{
|
{
|
||||||
Session::clear();
|
Session::clear();
|
||||||
return json(['code' => 200, 'message' => '退出成功']);
|
return $this->success('退出成功');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,16 +14,16 @@ class Ticket extends BaseController
|
|||||||
public function updateTicket(TicketService $ticketService, $id)
|
public function updateTicket(TicketService $ticketService, $id)
|
||||||
{
|
{
|
||||||
|
|
||||||
$adminId = Session::get('id'); // 获取当前登录ID
|
$isUser = Session::get('user_info.role') === 'user'; // 获取当前登录ID
|
||||||
$data = $this->request->post();
|
$data = $this->request->post();
|
||||||
|
|
||||||
$allowFields = ['datanum', 'progress', 'status', 'remark', 'lock_time'];
|
$allowFields = ['datanum', 'progress', 'status', 'remark', 'lock_time', 'user_id'];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$result = $ticketService->updateTicket($id, $data, $allowFields, $adminId, false);
|
$result = $ticketService->updateTicket($id, $data, $allowFields, $isUser, false);
|
||||||
return json(['code' => 200, 'msg' => '更新成功', 'data' => $result]);
|
return $this->success('更新成功', $result);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return json(['code' => 400, 'msg' => $e->getMessage()]);
|
return $this->error($e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
declare (strict_types = 1);
|
declare (strict_types = 1);
|
||||||
|
|
||||||
namespace app\middleware;
|
namespace app\middleware;
|
||||||
|
use app\common\Trait\ApiResponse;
|
||||||
use think\facade\Session;
|
use think\facade\Session;
|
||||||
class CheckLogin
|
class CheckLogin
|
||||||
{
|
{
|
||||||
|
use ApiResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理请求
|
* 处理请求
|
||||||
*
|
|
||||||
* @param \think\Request $request
|
* @param \think\Request $request
|
||||||
* @param \Closure $next
|
* @param \Closure $next
|
||||||
* @return Response
|
* @return Response
|
||||||
@@ -15,9 +17,8 @@ class CheckLogin
|
|||||||
public function handle($request, \Closure $next)
|
public function handle($request, \Closure $next)
|
||||||
{
|
{
|
||||||
if (!Session::has('user_info')) {
|
if (!Session::has('user_info')) {
|
||||||
return json(['code' => 401, 'message' => '未登录']);
|
return $this->error('未登录', [], 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,8 @@
|
|||||||
"topthink/framework": "^8.0",
|
"topthink/framework": "^8.0",
|
||||||
"topthink/think-orm": "^3.0|^4.0",
|
"topthink/think-orm": "^3.0|^4.0",
|
||||||
"topthink/think-filesystem": "^2.0|^3.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": {
|
"require-dev": {
|
||||||
"topthink/think-dumper": "^1.0",
|
"topthink/think-dumper": "^1.0",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ return [
|
|||||||
// 时间字段取出后的默认时间格式
|
// 时间字段取出后的默认时间格式
|
||||||
'datetime_format' => 'Y-m-d H:i:s',
|
'datetime_format' => 'Y-m-d H:i:s',
|
||||||
|
|
||||||
// 时间字段配置 配置格式:create_time,update_time
|
// 时间字段配置 配置格式:create_time,update_time
|
||||||
'datetime_field' => '',
|
'datetime_field' => '',
|
||||||
|
|
||||||
// 数据库连接配置信息
|
// 数据库连接配置信息
|
||||||
@@ -58,17 +58,21 @@ return [
|
|||||||
'fields_cache' => false,
|
'fields_cache' => false,
|
||||||
],
|
],
|
||||||
'remote' => [
|
'remote' => [
|
||||||
|
// 数据库类型
|
||||||
'type' => 'mysql',
|
'type' => 'mysql',
|
||||||
'hostname' => '112.5.15.136',
|
// 服务器地址(必须从 .env 读取)
|
||||||
'database' => '112_5_15_136_255',
|
'hostname' => env('REMOTE_DB_HOST'),
|
||||||
'username' => '112_5_15_136_255',
|
// 数据库名(必须从 .env 读取)
|
||||||
'password' => 'xz1YX1jswSWzkE4K',
|
'database' => env('REMOTE_DB_NAME'),
|
||||||
'hostport' => '3306',
|
// 用户名(必须从 .env 读取)
|
||||||
|
'username' => env('REMOTE_DB_USER'),
|
||||||
|
// 密码(必须从 .env 读取)
|
||||||
|
'password' => env('REMOTE_DB_PASS'),
|
||||||
|
// 端口
|
||||||
|
'hostport' => env('REMOTE_DB_PORT', '3306'),
|
||||||
'charset' => 'utf8mb4',
|
'charset' => 'utf8mb4',
|
||||||
'prefix' => '', // 如果有表前缀请填写
|
'prefix' => '',
|
||||||
'break_reconnect' => true, // 远程连接建议开启断线重连
|
'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