Compare commits

...

8 Commits

Author SHA1 Message Date
f9adabf5d8 wip 2026-08-26 17:50:41 +08:00
d3866efcd8 wip 2026-08-19 19:56:47 +08:00
07a24c5297 wip 2026-08-19 14:14:29 +08:00
30ae8f813d Merge branch 'dev' of https://gitea.fzcaiji.com/Ggy_shinidie/NewTicket into dev 2026-08-18 14:55:41 +08:00
94efeb524d Merge branch 'main' into dev 2026-08-18 14:55:24 +08:00
497a47382d Merge branch 'main' into dev 2026-08-18 11:35:39 +08:00
7eda834a9f 删除部分不需要的文件 2026-08-18 11:22:46 +08:00
dc118e1368 wip 2026-08-14 13:50:43 +08:00
23 changed files with 254 additions and 140 deletions

11
.env.example Normal file
View File

@@ -0,0 +1,11 @@
APP_DEBUG = true
DB_TYPE = mysql
DB_HOST = 127.0.0.1
DB_NAME =
DB_USER =
DB_PASS =
DB_PORT = 3306
DB_CHARSET = utf8
DEFAULT_LANG = zh-cn

2
.gitignore vendored
View File

@@ -11,4 +11,4 @@ Thumbs.db
/.buildpath /.buildpath
/.project /.project
/runtime /runtime
/.kunsdd /.github

View File

@@ -1,32 +0,0 @@
ThinkPHP遵循Apache2开源协议发布,并提供免费使用。
版权所有Copyright © 2006-2025 by ThinkPHP (http://thinkphp.cn)
All rights reserved。
ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。
Apache Licence是著名的非盈利开源组织Apache采用的协议。
该协议和BSD类似,鼓励代码共享和尊重原作者的著作权,
允许代码修改,再作为开源或商业软件发布。需要满足
的条件:
1. 需要给代码的用户一份Apache Licence ;
2. 如果你修改了代码,需要在被修改的文件中说明;
3. 在延伸的代码中(修改和有源代码衍生的代码中)需要
带有原来代码中的协议,商标,专利声明和其他原来作者规
定需要包含的说明;
4. 如果再发布的产品中包含一个Notice文件,则在Notice文
件中需要带有本协议内容。你可以在Notice中增加自己的
许可,但不可以表现为对Apache Licence构成更改。
具体的协议参考:http://www.apache.org/licenses/LICENSE-2.0
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,77 +0,0 @@
![](https://www.thinkphp.cn/uploads/images/20230630/300c856765af4d8ae758c503185f8739.png)
ThinkPHP 8
===============
## 特性
* 基于PHP`8.0+`重构
* 升级`PSR`依赖
* 依赖`think-orm`3.0+版本
* 全新的`think-dumper`服务,支持远程调试
* 支持`6.0`/`6.1`无缝升级
> ThinkPHP8的运行环境要求PHP8.0+
现在开始,你可以使用官方提供的[ThinkChat](https://chat.topthink.com/),让你在学习ThinkPHP的旅途中享受私人AI助理服务!
![](https://www.topthink.com/uploads/assistant/20230630/4d1a3f0ad2958b49bb8189b7ef824cb0.png)
ThinkPHP生态服务由[顶想云](https://www.topthink.com)(TOPThink Cloud)提供,为生态提供专业的开发者服务和价值之选。
## 文档
[完全开发手册](https://doc.thinkphp.cn)
## 赞助
全新的[赞助计划](https://www.thinkphp.cn/sponsor)可以让你通过我们的网站、手册、欢迎页及GIT仓库获得巨大曝光,同时提升企业的品牌声誉,也更好保障ThinkPHP的可持续发展。
[![](https://www.thinkphp.cn/sponsor/special.svg)](https://www.thinkphp.cn/sponsor/special)
[![](https://www.thinkphp.cn/sponsor.svg)](https://www.thinkphp.cn/sponsor)
## 安装
~~~
composer create-project topthink/think tp
~~~
启动服务
~~~
cd tp
php think run
~~~
然后就可以在浏览器中访问
~~~
http://localhost:8000
~~~
如果需要更新框架使用
~~~
composer update topthink/framework
~~~
## 命名规范
`ThinkPHP`遵循PSR-2命名规范和PSR-4自动加载规范。
## 参与开发
直接提交PR或者Issue即可
## 版权信息
ThinkPHP遵循Apache2开源协议发布,并提供免费使用。
本项目包含的第三方源码和二进制文件之版权信息另行标注。
版权所有Copyright © 2006-2024 by ThinkPHP (http://thinkphp.cn) All rights reserved。
ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。
更多细节参阅 [LICENSE.txt](LICENSE.txt)

View File

@@ -8,6 +8,8 @@ use app\common\model\Tickets;
use app\common\service\TicketNotifier; use app\common\service\TicketNotifier;
use app\common\service\TicketService; use app\common\service\TicketService;
use think\facade\Session; use think\facade\Session;
use app\common\service\TicketScope;
class Ticket extends BaseController class Ticket extends BaseController
{ {
@@ -15,7 +17,7 @@ class Ticket extends BaseController
/** /**
* 查询工单 * 查询工单
*/ */
public function ticketsFind(TicketService $ticketService) public function fetchTickets(TicketService $ticketService)
{ {
// 在控制器明确接收参数 // 在控制器明确接收参数
$params = [ $params = [
@@ -32,7 +34,11 @@ class Ticket extends BaseController
$limit = min(max(($params['limit'] ?? 20), 1), 100); $limit = min(max(($params['limit'] ?? 20), 1), 100);
$order = strtolower($params['sortOrder'] ?? 'desc') === 'asc' ? 'asc' : 'desc'; $order = strtolower($params['sortOrder'] ?? 'desc') === 'asc' ? 'asc' : 'desc';
$result = $ticketService->queryTickets($params, $page, $limit, $order); try {
$result = $ticketService->queryTickets($params, $page, $limit, $order, TicketScope::admin());
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
return $this->success('查询成功', $result); return $this->success('查询成功', $result);
} }
@@ -47,13 +53,13 @@ class Ticket extends BaseController
try { try {
$result = $ticketService->createTicket($data); $result = $ticketService->createTicket($data);
return $this->success('创建成功', $result);
} catch (\Exception $e) { } catch (\Exception $e) {
if ($e->getCode() === 409) { if ($e->getCode() === 409) {
return $this->error($e->getMessage(), ['confirm' => true], 409); return $this->error($e->getMessage(), ['confirm' => true], 409);
} }
return $this->error($e->getMessage()); return $this->error($e->getMessage());
} }
return $this->success('创建成功', $result);
} }
@@ -70,10 +76,11 @@ class Ticket extends BaseController
try { try {
$result = $ticketService->updateTicket($id, $data, $allowFields, $adminId, true); $result = $ticketService->updateTicket($id, $data, $allowFields, $adminId, true);
return $this->success('更新成功', $result);
} catch (\Exception $e) { } catch (\Exception $e) {
return $this->error($e->getMessage()); return $this->error($e->getMessage());
} }
return $this->success('更新成功', $result);
} }

View File

@@ -5,7 +5,7 @@ use app\admin\middleware\Auth;
use app\middleware\CheckLogin; use app\middleware\CheckLogin;
Route::group(function () { Route::group(function () {
Route::get('ticketsfind', 'Ticket/ticketsFind'); Route::get('ticketsfind', 'Ticket/fetchTickets');
Route::post('addticket', 'Ticket/createTicket'); Route::post('addticket', 'Ticket/createTicket');
Route::put('ticket/:id', 'Ticket/updateTicket'); Route::put('ticket/:id', 'Ticket/updateTicket');
Route::delete('ticket/:id', 'Ticket/deleteTicket'); Route::delete('ticket/:id', 'Ticket/deleteTicket');

View File

@@ -12,15 +12,20 @@ class AccountService
{ {
$users = UserModel::with('company')->select(); $users = UserModel::with('company')->select();
return $users->map(function ($user) { return $users->map(function ($user) {
$companyName = $user->role === 'client' && (bool) $user->can_view_all_tickets
? '全部客户'
: ($user->company->company_name ?? '');
return [ return [
'id' => $user->id, 'id' => $user->id,
'company' => [ 'company' => [
'id' => $user->company_id, 'id' => $user->company_id,
'company_name' => $user->company->company_name 'company_name' => $companyName
], ],
'username' => $user->username, 'username' => $user->username,
'real_name' => $user->real_name, 'real_name' => $user->real_name,
'role' => $user->role, 'role' => $user->role,
'can_view_all_tickets' => (bool) $user->can_view_all_tickets,
'status' => $user->status, 'status' => $user->status,
]; ];
})->toArray(); })->toArray();
@@ -40,19 +45,34 @@ class AccountService
throw new \Exception('用户名已存在'); throw new \Exception('用户名已存在');
} }
$role = $data['role'] ?? 'user';
$companyId = (int) ($data['company_id'] ?? 0);
$canViewAllTickets = $role === 'client' && !empty($data['can_view_all_tickets']);
// 系统中只允许存在一个可查看全部工单的客户汇总账号。
if ($canViewAllTickets) {
$allAccessClient = UserModel::where('role', 'client')
->where('can_view_all_tickets', 1)
->find();
if ($allAccessClient) {
throw new \Exception('已存在可查看全部工单的客户账号');
}
}
$insertData = [ $insertData = [
'company_id' => $data['company_id'] ?? 0, 'company_id' => $companyId,
'username' => $data['username'], 'username' => $data['username'],
'real_name' => $data['real_name'] ?? $data['username'], 'real_name' => $data['real_name'] ?? $data['username'],
'password' => password_hash('123456', PASSWORD_DEFAULT), 'password' => password_hash('123456', PASSWORD_DEFAULT),
'role' => $data['role'] ?? 'user', 'role' => $role,
'can_view_all_tickets' => $canViewAllTickets ? 1 : 0,
'status' => 1, 'status' => 1,
]; ];
$user = UserModel::create($insertData); $user = UserModel::create($insertData);
/** @var \think\Model $user */ /** @var \think\Model $user */
return $user->visible(['id', 'username', 'real_name', 'role', 'status'])->toArray(); return $user->visible(['id', 'username', 'real_name', 'role', 'can_view_all_tickets', 'status'])->toArray();
} }
/* 更新用户状态 */ /* 更新用户状态 */

View File

@@ -0,0 +1,12 @@
<?php
declare (strict_types = 1);
namespace app\api\controller;
class Index
{
public function index()
{
return '您好!这是一个[api]示例应用';
}
}

View File

@@ -16,7 +16,6 @@ class Tickets extends Model
protected $createTime = 'create_time'; protected $createTime = 'create_time';
protected $updateTime = 'update_time'; protected $updateTime = 'update_time';
# 定义字段类型 # 定义字段类型
protected $type = [ protected $type = [
'id' => 'integer', 'id' => 'integer',
@@ -34,6 +33,13 @@ class Tickets extends Model
]; ];
# 定义状态常量
const STATUS_UNASSIGNED = 0;
const STATUS_PROCESSING = 1;
const STATUS_COMPLETED = 2;
const STATUS_CLOSED = 3;
/* /*
时间戳获取器 时间戳获取器
*/ */

View File

@@ -12,6 +12,10 @@ class User extends Model
{ {
protected $table = 'users'; protected $table = 'users';
protected $type = [
'can_view_all_tickets' => 'boolean',
];
public function company(){ public function company(){
return $this->belongsTo(Companys::class,'company_id','id'); return $this->belongsTo(Companys::class,'company_id','id');
} }

View File

@@ -18,7 +18,6 @@ class AuthService
->find(); ->find();
if (!$user || !password_verify($password, $user['password'])) { if (!$user || !password_verify($password, $user['password'])) {
throw new \Exception('用户名或密码错误'); throw new \Exception('用户名或密码错误');
} }
@@ -31,13 +30,16 @@ class AuthService
$userInfo = [ $userInfo = [
'id' => $user['id'], // 用户ID 'id' => $user['id'], // 用户ID
'company_id' => $user['company_id'], // 所属公司ID 'company_id' => $user['company_id'], // 所属公司ID
'company_name' => $user['company']['company_name'], // 所属公司名称 'company_name' => $user['role'] === 'client' && (bool) $user['can_view_all_tickets']
? '全部客户'
: ($user['company']['company_name'] ?? ''), // 所属公司名称
'username' => $user['username'], 'username' => $user['username'],
'role' => $user['role'], 'role' => $user['role'],
'can_view_all_tickets' => (bool) $user['can_view_all_tickets'],
]; ];
Session::set('user_info', $userInfo); Session::set('user_info', $userInfo);
return $userInfo; return $userInfo; // 为了方便前端展示用户信息
} }
} }

View File

@@ -0,0 +1,35 @@
<?php
namespace app\common\service;
/**
* 把当前用户能看哪些工单封装成一个明确的对象TicketScope,避免用零散变量或含义不清的数组传递数据范围
* final 表示不允许被其他类继承
*/
final class TicketScope
{
# readonly 仅在PHP8.1中存在,当前版本8.0.2
private function __construct(
public string $type,
public ?int $staffId = null,
public ?int $clientId = null
) {}
/** 管理员:不限制状态、不限制归属 */
public static function admin(): self
{
return new self('admin');
}
/** 员工:未分配全部可见,其余状态仅自己 */
public static function staff(int $staffId): self
{
return new self('staff', staffId: $staffId);
}
/** 客户:$clientId 只能看自己的,传null可以看当前公司所有的 */
public static function client(?int $clientId): self
{
return new self('client', clientId: $clientId);
}
}

View File

@@ -2,11 +2,15 @@
namespace app\common\service; namespace app\common\service;
use app\admin\controller\Ticket;
use app\common\model\Tickets; 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\Db;
use app\common\service\TicketScope;
class TicketService class TicketService
{ {
@@ -17,12 +21,14 @@ class TicketService
* @param int $limit 每页数量 * @param int $limit 每页数量
* @param string $order 排序方式 * @param string $order 排序方式
*/ */
public function queryTickets(array $params, int $page, int $limit, string $order) public function queryTickets(array $params, int $page, int $limit, string $order, TicketScope $scope)
{ {
try { try {
// 查询构造器 // 查询构造器
$query = Tickets::with(['ticketStaff', 'ticketCompany', 'ticketOperator']); $query = Tickets::with(['ticketStaff', 'ticketCompany', 'ticketOperator']);
$this->applyScope($query, $scope);
$equalFilters = ['operator_id', 'user_id', 'status']; $equalFilters = ['operator_id', 'user_id', 'status'];
foreach ($equalFilters as $field) { foreach ($equalFilters as $field) {
if (isset($params[$field]) && $params[$field] !== '') { if (isset($params[$field]) && $params[$field] !== '') {
@@ -56,6 +62,21 @@ class TicketService
} }
private function applyScope($query, TicketScope $scope): void
{
match ($scope->type) {
'client' => null,
'staff' => $query->where(fn($q) => $q
->where('status', Tickets::STATUS_UNASSIGNED)
->where('user_id', $scope->staffId)),
};
}
/** /**
* 创建工单业务逻辑 * 创建工单业务逻辑
* @param array $data 工单数据 * @param array $data 工单数据
@@ -120,6 +141,8 @@ class TicketService
throw new \Exception('工单不存在'); throw new \Exception('工单不存在');
} }
// 提取 $allowFields 允许修改的字段 // 提取 $allowFields 允许修改的字段
$updateData = []; $updateData = [];
foreach ($allowFields as $field) { foreach ($allowFields as $field) {

View File

@@ -13,9 +13,11 @@ class ClientTicket extends BaseController
*/ */
public function getClientTickets(ClientTicketService $ClientTicketService) public function getClientTickets(ClientTicketService $ClientTicketService)
{ {
$clientCompanyId = Session::get('user_info.company_id'); // 获取当前登录客户ID $clientCompanyId = (int) Session::get('user_info.company_id'); // 获取当前登录客户ID
$canViewAll = Session::get('user_info.role') === 'client'
&& (bool) Session::get('user_info.can_view_all_tickets');
try { try {
$tickets = $ClientTicketService->getClientTickets($clientCompanyId); $tickets = $ClientTicketService->getClientTickets($clientCompanyId, $canViewAll);
return $this->success('获取成功', $tickets); return $this->success('获取成功', $tickets);
} catch (\Exception $e) { } catch (\Exception $e) {
return $this->error($e->getMessage()); return $this->error($e->getMessage());

View File

@@ -45,6 +45,6 @@ class Login extends BaseController
public function logout() public function logout()
{ {
Session::clear(); Session::clear();
return $this->success('退出成功'); return $this->success('退出登录');
} }
} }

View File

@@ -15,9 +15,12 @@ class Ticket extends BaseController
{ {
$isUser = Session::get('user_info.role') === 'user'; // 获取当前登录ID $isUser = Session::get('user_info.role') === 'user'; // 获取当前登录ID
$data = $this->request->post(); $post = $this->request->post();
$data = [
'datanum' => $post['datanum']
]
$allowFields = ['datanum', 'progress', 'status', 'remark', 'lock_time', 'user_id'];
try { try {
$result = $ticketService->updateTicket($id, $data, $allowFields, $isUser, false); $result = $ticketService->updateTicket($id, $data, $allowFields, $isUser, false);

View File

@@ -0,0 +1,26 @@
<?php
namespace app\client\controller;
use app\BaseController;
use app\index\service\ClientTicketService;
use think\facade\Session;
class Index extends BaseController
{
/**
* 获取客户关联工单
*/
public function fetchTickets(ClientTicketService $ClientTicketService)
{
$clientCompanyId = (int) Session::get('user_info.company_id'); // 获取当前登录客户ID
$canViewAll = Session::get('user_info.role') === 'client'
&& (bool) Session::get('user_info.can_view_all_tickets');
try {
$tickets = $ClientTicketService->getClientTickets($clientCompanyId, $canViewAll);
return $this->success('获取成功', $tickets);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace app\staff\controller;
use app\BaseController;
use app\common\service\TicketService;
use app\common\service\TicketScope;
use think\facade\Session;
class Ticket extends BaseController
{
public function fetchTickets(TicketService $TicketService)
{
$params = [
"page" => $this->request->param('page', 1, 'intval'),
"limit" => $this->request->param('limit', 20, 'intval'),
"operator_id" => $this->request->param('operator_id', null),
"status" => $this->request->param('status', null),
"searchKeyword" => $this->request->param('search', null),
"sortOrder" => $this->request->param('sort_order', 'desc'),
];
$staffId = (int) Session::get('user_info.id'); // 若没有,则为0
if ($staffId <= 0) {
return $this->error('无法获取当前员工信息');
}
$page = max(($params['page'] ?? 1), 1);
$limit = min(max(($params['limit'] ?? 20), 1), 100);
$order = strtolower($params['sortOrder'] ?? 'desc') === 'asc' ? 'asc' : 'desc';
try {
$result = $TicketService->queryTickets($params, $page, $limit, $order, TicketScope::staff($staffId));
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
return $this->success('查询成功', $result);
}
/**
* 更新工单(员工调用)
*/
public function updateTicket(TicketService $ticketService, $id)
{
$isUser = Session::get('user_info.role') === 'user';
$data = $this->request->post();
$allowFields = ['datanum', 'progress', 'status', 'remark', 'lock_time', 'user_id'];
try {
$result = $ticketService->updateTicket($id, $data, $allowFields, $isUser, false);
return $this->success('更新成功', $result);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
}

View File

@@ -8,17 +8,20 @@ use app\common\model\Tickets;
class ClientTicketService class ClientTicketService
{ {
/* 获取客户关联工单 */ /* 获取客户关联工单 */
public function getClientTickets($clientCompanyId) public function getClientTickets(int $clientCompanyId, bool $canViewAll = false)
{ {
# 查询当前登录用户所对应的公司的工单 // 普通客户仅查询所属公司的工单;唯一的汇总客户账号可查看全部。
$tickets = Tickets::where('company_id', $clientCompanyId)->select(); $query = Tickets::whereIn('status', [0, 1, 2]);
if (!$canViewAll) {
$query->where('company_id', $clientCompanyId);
}
$tickets = $query->select();
$newList = []; $newList = [];
foreach ($tickets as $ticket) { foreach ($tickets as $ticket) {
if (in_array($ticket->status, [0, 1, 2])) { $ticket->visible(['id', 'website', 'datanum', 'progress', 'status']);
$ticket->visible(['id', 'website', 'datanum', 'progress', 'status']); $newList[] = $ticket->toArray();
$newList[] = $ticket->toArray();
}
} }
return $newList; return $newList;

View File

@@ -4,7 +4,6 @@
// +---------------------------------------------------------------------- // +----------------------------------------------------------------------
return [ return [
// session name
'name' => 'PHPSESSID', 'name' => 'PHPSESSID',
// SESSION_ID的提交变量,解决flash上传跨域 // SESSION_ID的提交变量,解决flash上传跨域
'var_session_id' => '', 'var_session_id' => '',

View File

@@ -0,0 +1,6 @@
-- Up: 为客户汇总账号增加显式的全量工单查看标记。
ALTER TABLE `users`
ADD COLUMN `can_view_all_tickets` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0 AFTER `role`;
-- Rollback(需要回滚时单独执行):
-- ALTER TABLE `users` DROP COLUMN `can_view_all_tickets`;