first commit

This commit is contained in:
Your Name
2026-09-08 11:40:15 +08:00
commit a5353f7eb5
9568 changed files with 1646214 additions and 0 deletions
@@ -0,0 +1,36 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists;
use app\common\lists\BaseDataLists;
/**
* 管理员模块数据列表基类
* Class BaseAdminDataLists
* @package app\adminapi\lists
*/
abstract class BaseAdminDataLists extends BaseDataLists
{
protected array $adminInfo;
protected int $adminId;
protected function initAdminIdentity(): void
{
$this->adminInfo = $this->request->adminInfo;
$this->adminId = $this->request->adminId;
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\adminapi\lists;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\model\Fan;
use app\common\model\FanVisitRecord;
use app\common\lists\ListsSearchInterface;
class FanLists extends BaseAdminDataLists implements ListsSearchInterface
{
public function setSearch(): array
{
return [
'%like%' => ['name', 'phone'],
'=' => ['gender', 'status'],
];
}
public function lists(): array
{
$lists = Fan::where($this->searchWhere)
->field(['id', 'name', 'phone', 'id_card', 'age', 'gender', 'remark', 'creator_id', 'creator_name', 'status', 'create_time', 'update_time'])
->append(['gender_desc', 'status_desc'])
->order(['id' => 'desc'])
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
$fanIds = array_column($lists, 'id');
if (!empty($fanIds)) {
$visitCounts = FanVisitRecord::where('fan_id', 'in', $fanIds)
->where('delete_time', null)
->group('fan_id')
->column('count(*) as cnt', 'fan_id');
foreach ($lists as &$item) {
$item['visit_count'] = $visitCounts[$item['id']] ?? 0;
}
}
return $lists;
}
public function count(): int
{
return Fan::where($this->searchWhere)->count();
}
}
@@ -0,0 +1,98 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\article;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\lists\ListsSearchInterface;
use app\common\lists\ListsSortInterface;
use app\common\model\article\ArticleCate;
/**
* 资讯分类列表
* Class ArticleCateLists
* @package app\adminapi\lists\article
*/
class ArticleCateLists extends BaseAdminDataLists implements ListsSearchInterface, ListsSortInterface
{
/**
* @notes 设置搜索条件
* @return array
* @author heshihu
* @date 2022/2/8 18:39
*/
public function setSearch(): array
{
return [];
}
/**
* @notes 设置支持排序字段
* @return array
* @author heshihu
* @date 2022/2/9 15:11
*/
public function setSortFields(): array
{
return ['create_time' => 'create_time', 'id' => 'id'];
}
/**
* @notes 设置默认排序
* @return array
* @author heshihu
* @date 2022/2/9 15:08
*/
public function setDefaultOrder(): array
{
return ['sort' => 'desc','id' => 'desc'];
}
/**
* @notes 获取管理列表
* @return array
* @author heshihu
* @date 2022/2/21 17:11
*/
public function lists(): array
{
$ArticleCateLists = ArticleCate::where($this->searchWhere)
->append(['is_show_desc'])
->limit($this->limitOffset, $this->limitLength)
->order($this->sortOrder)
->append(['article_count'])
->select()
->toArray();
return $ArticleCateLists;
}
/**
* @notes 获取数量
* @return int
* @author heshihu
* @date 2022/2/9 15:12
*/
public function count(): int
{
return ArticleCate::where($this->searchWhere)->count();
}
public function extend()
{
return [];
}
}
@@ -0,0 +1,99 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\article;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\lists\ListsSearchInterface;
use app\common\lists\ListsSortInterface;
use app\common\model\article\Article;
/**
* 资讯列表
* Class ArticleLists
* @package app\adminapi\lists\article
*/
class ArticleLists extends BaseAdminDataLists implements ListsSearchInterface, ListsSortInterface
{
/**
* @notes 设置搜索条件
* @return array
* @author heshihu
* @date 2022/2/8 18:39
*/
public function setSearch(): array
{
return [
'%like%' => ['title'],
'=' => ['cid', 'is_show']
];
}
/**
* @notes 设置支持排序字段
* @return array
* @author heshihu
* @date 2022/2/9 15:11
*/
public function setSortFields(): array
{
return ['create_time' => 'create_time', 'id' => 'id'];
}
/**
* @notes 设置默认排序
* @return array
* @author heshihu
* @date 2022/2/9 15:08
*/
public function setDefaultOrder(): array
{
return ['sort' => 'desc', 'id' => 'desc'];
}
/**
* @notes 获取管理列表
* @return array
* @author heshihu
* @date 2022/2/21 17:11
*/
public function lists(): array
{
$ArticleLists = Article::where($this->searchWhere)
->append(['cate_name', 'click'])
->limit($this->limitOffset, $this->limitLength)
->order($this->sortOrder)
->select()
->toArray();
return $ArticleLists;
}
/**
* @notes 获取数量
* @return int
* @author heshihu
* @date 2022/2/9 15:12
*/
public function count(): int
{
return Article::where($this->searchWhere)->count();
}
public function extend()
{
return [];
}
}
@@ -0,0 +1,270 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\auth;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\lists\ListsExcelInterface;
use app\common\lists\ListsExtendInterface;
use app\common\lists\ListsSearchInterface;
use app\common\lists\ListsSortInterface;
use app\common\lists\Traits\HasDataScopeFilter;
use app\common\model\auth\Admin;
use app\common\model\auth\AdminRole;
use app\common\model\auth\SystemRole;
use app\common\model\dept\Dept;
use app\common\model\dept\Jobs;
use app\common\model\doctor\Appointment;
use app\common\model\tcm\Diagnosis;
use think\facade\Db;
/**
* 管理员列表
* Class AdminLists
* @package app\adminapi\lists\auth
*/
class AdminLists extends BaseAdminDataLists implements ListsExtendInterface, ListsSearchInterface, ListsSortInterface,ListsExcelInterface
{
use HasDataScopeFilter;
/**
* @notes 设置导出字段
* @return string[]
* @author 段誉
* @date 2021/12/29 10:08
*/
public function setExcelFields(): array
{
return [
'account' => '账号',
'name' => '名称',
'role_name' => '角色',
'dept_name' => '部门',
'create_time' => '创建时间',
'login_time' => '最近登录时间',
'login_ip' => '最近登录IP',
'disable_desc' => '状态',
];
}
/**
* @notes 设置导出文件名
* @return string
* @author 段誉
* @date 2021/12/29 10:08
*/
public function setFileName(): string
{
return '管理员列表';
}
/**
* @notes 设置搜索条件
* @return \string[][]
* @author 段誉
* @date 2021/12/29 10:07
*/
public function setSearch(): array
{
return [
'%like%' => ['name', 'account'],
];
}
/**
* @notes 设置支持排序字段
* @return string[]
* @author 段誉
* @date 2021/12/29 10:07
* @remark 格式: ['前端传过来的字段名' => '数据库中的字段名'];
*/
public function setSortFields(): array
{
return ['create_time' => 'create_time', 'id' => 'id'];
}
/**
* @notes 设置默认排序
* @return string[]
* @author 段誉
* @date 2021/12/29 10:06
*/
public function setDefaultOrder(): array
{
return ['id' => 'desc'];
}
/**
* @notes 查询条件
* @return array
* @author 段誉
* @date 2022/11/29 11:33
*/
public function queryWhere()
{
$where = [];
$progressBoard = (int) ($this->params['progress_board'] ?? 0) === 1;
if ($progressBoard) {
// 面诊进度:固定只拉「医生」角色且未禁用,忽略客户端篡改的 role_id
$adminIds = array_map('intval', AdminRole::where('role_id', 1)->column('admin_id'));
// 数据范围:医生与业务/医助通常不在同一部门,按 admin id 直接取交集会全空。
// 这里改为「按当前账号可见的挂号反查涉及的医生」,与 AppointmentLists 的可见性一致:
// 可见挂号 = a.doctor_id ∈ visible OR diagnosis.assistant_id ∈ visible
$visibleIds = $this->getDataScopeVisibleAdminIds();
if ($visibleIds !== null) {
if ($visibleIds === [] || $adminIds === []) {
$adminIds = [];
} else {
$aTbl = (new Appointment())->getTable();
$dTbl = (new Diagnosis())->getTable();
$inList = implode(',', array_map('intval', $visibleIds));
$rows = Db::query(
"SELECT DISTINCT a.doctor_id FROM `{$aTbl}` a "
. "LEFT JOIN `{$dTbl}` u ON a.patient_id = u.id "
. "WHERE (a.doctor_id IN ({$inList}) "
. " OR (u.id IS NOT NULL AND u.delete_time IS NULL AND u.assistant_id IN ({$inList})))"
);
$apptDoctorIds = array_filter(array_map(static function ($r): int {
return (int) ($r['doctor_id'] ?? 0);
}, $rows ?: []));
$adminIds = array_values(array_intersect($adminIds, $apptDoctorIds));
}
}
if (!empty($adminIds)) {
$where[] = ['id', 'in', $adminIds];
} else {
// 数据范围下没有可见医生:强制空集
$where[] = ['id', '=', 0];
}
$where[] = ['disable', '=', 0];
} else {
if (isset($this->params['role_id']) && $this->params['role_id'] != '') {
$adminIds = AdminRole::where('role_id', $this->params['role_id'])->column('admin_id');
if (!empty($adminIds)) {
$where[] = ['id', 'in', $adminIds];
}
}
// 排除禁止登录(disable=1),医生选择器等场景
if ((int) ($this->params['exclude_disabled'] ?? 0) === 1) {
$where[] = ['disable', '=', 0];
}
}
// 数据范围:仅当调用方主动传 apply_data_scope=1 时启用
// (管理员表被多场景共用:医生/医助选择器、看板、组织架构等;不主动启用避免误伤)
if ((int) ($this->params['apply_data_scope'] ?? 0) === 1) {
$visibleIds = $this->getDataScopeVisibleAdminIds();
if ($visibleIds !== null) {
if ($visibleIds === []) {
$where[] = ['id', '=', 0]; // 强制空
} else {
$where[] = ['id', 'in', $visibleIds];
}
}
}
return $where;
}
/**
* @notes 获取管理列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2021/12/29 10:05
*/
public function lists(): array
{
$field = [
'id', 'name', 'account', 'create_time', 'disable', 'root',
'login_time', 'login_ip', 'multipoint_login', 'avatar',
'gender', 'age', 'phone', 'title', 'department',
'specialty', 'education', 'experience', 'honors'
];
$adminLists = Admin::field($field)
->where($this->searchWhere)
->where($this->queryWhere())
->limit($this->limitOffset, $this->limitLength)
->order($this->sortOrder)
->append(['role_id', 'dept_id', 'jobs_id', 'disable_desc'])
->select()
->toArray();
// 角色数组('角色id'=>'角色名称')
$roleLists = SystemRole::column('name', 'id');
// 部门列表
$deptLists = Dept::column('name', 'id');
// 岗位列表
$jobsLists = Jobs::column('name', 'id');
//管理员列表增加角色名称
foreach ($adminLists as $k => $v) {
$roleName = '';
if ($v['root'] == 1) {
$roleName = '系统管理员';
} else {
foreach ($v['role_id'] as $roleId) {
$roleName .= $roleLists[$roleId] ?? '';
$roleName .= '/';
}
}
$deptName = '';
foreach ($v['dept_id'] as $deptId) {
$deptName .= $deptLists[$deptId] ?? '';
$deptName .= '/';
}
$jobsName = '';
foreach ($v['jobs_id'] as $jobsId) {
$jobsName .= $jobsLists[$jobsId] ?? '';
$jobsName .= '/';
}
$adminLists[$k]['role_name'] = trim($roleName, '/');
$adminLists[$k]['dept_name'] = trim($deptName, '/');
$adminLists[$k]['jobs_name'] = trim($jobsName, '/');
}
return $adminLists;
}
/**
* @notes 获取数量
* @return int
* @author 令狐冲
* @date 2021/7/13 00:52
*/
public function count(): int
{
return Admin::where($this->searchWhere)
->where($this->queryWhere())
->count();
}
public function extend()
{
return [];
}
}
@@ -0,0 +1,58 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\auth;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\model\auth\SystemMenu;
/**
* 菜单列表
* Class MenuLists
* @package app\adminapi\lists\auth
*/
class MenuLists extends BaseAdminDataLists
{
/**
* @notes 获取菜单列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/6/29 16:41
*/
public function lists(): array
{
$lists = SystemMenu::order(['sort' => 'desc', 'id' => 'asc'])
->select()
->toArray();
return linear_to_tree($lists, 'children');
}
/**
* @notes 获取菜单数量
* @return int
* @author 段誉
* @date 2022/6/29 16:41
*/
public function count(): int
{
return SystemMenu::count();
}
}
@@ -0,0 +1,93 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\auth;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\model\auth\AdminRole;
use app\common\model\auth\SystemRole;
/**
* 角色列表
* Class RoleLists
* @package app\adminapi\lists\auth
*/
class RoleLists extends BaseAdminDataLists
{
/**
* @notes 导出字段
* @return string[]
* @author Tab
* @date 2021/9/22 18:52
*/
public function setExcelFields(): array
{
return [
'name' => '角色名称',
'desc' => '备注',
'create_time' => '创建时间'
];
}
/**
* @notes 导出表名
* @return string
* @author Tab
* @date 2021/9/22 18:52
*/
public function setFileName(): string
{
return '角色表';
}
/**
* @notes 角色列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author cjhao
* @date 2021/8/25 18:00
*/
public function lists(): array
{
$lists = SystemRole::with(['role_menu_index'])
->field('id,name,desc,sort,data_scope,create_time')
->limit($this->limitOffset, $this->limitLength)
->order(['sort' => 'desc', 'id' => 'desc'])
->select()
->toArray();
foreach ($lists as $key => $role) {
//使用角色的人数
$lists[$key]['num'] = AdminRole::where('role_id', $role['id'])->count();
$menuId = array_column($role['role_menu_index'], 'menu_id');
$lists[$key]['menu_id'] = $menuId;
unset($lists[$key]['role_menu_index']);
}
return $lists;
}
/**
* @notes 总记录数
* @return int
* @author Tab
* @date 2021/7/13 11:26
*/
public function count(): int
{
return SystemRole::count();
}
}
@@ -0,0 +1,80 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\channel;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\lists\ListsSearchInterface;
use app\common\model\channel\OfficialAccountReply;
/**
* 微信公众号回复列表
* Class OfficialAccountLists
* @package app\adminapi\lists
*/
class OfficialAccountReplyLists extends BaseAdminDataLists implements ListsSearchInterface
{
/**
* @notes 设置搜索
* @return \string[][]
* @author 段誉
* @date 2022/3/30 15:02
*/
public function setSearch(): array
{
return [
'=' => ['reply_type']
];
}
/**
* @notes 回复列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/3/30 15:02
*/
public function lists(): array
{
$field = 'id,name,keyword,matching_type,content,content_type,status,sort';
$field .= ',matching_type as matching_type_desc,content_type as content_type_desc,status as status_desc';
$lists = OfficialAccountReply::field($field)
->where($this->searchWhere)
->order(['sort' => 'desc', 'id' => 'desc'])
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
return $lists;
}
/**
* @notes 回复记录数
* @return int
* @author 段誉
* @date 2022/3/30 15:02
*/
public function count(): int
{
$count = OfficialAccountReply::where($this->searchWhere)->count();
return $count;
}
}
@@ -0,0 +1,61 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\crontab;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\model\Crontab;
/**
* 定时任务列表
* Class CrontabLists
* @package app\adminapi\lists\crontab
*/
class CrontabLists extends BaseAdminDataLists
{
/**
* @notes 定时任务列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/3/29 14:30
*/
public function lists(): array
{
$field = 'id,name,type,type as type_desc,command,params,expression,
status,status as status_desc,error,last_time,time,max_time';
$lists = Crontab::field($field)
->limit($this->limitOffset, $this->limitLength)
->order('id', 'desc')
->select()
->toArray();
return $lists;
}
/**
* @notes 定时任务数量
* @return int
* @author 段誉
* @date 2022/3/29 14:38
*/
public function count(): int
{
return Crontab::count();
}
}
@@ -0,0 +1,64 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\decorate;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\enum\MenuEnum;
use app\common\model\decorate\Menu;
/**
* 菜单列表
* Class MenuLists
* @package app\adminapi\lists\decorate
*/
class MenuLists extends BaseAdminDataLists
{
/**
* @notes 菜单列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author ljj
* @date 2022/2/14 11:29 上午
*/
public function lists(): array
{
$lists = (new Menu())->field('id,name,image,link_type,link_address,sort,status')
->order(['sort'=>'asc','id'=>'desc'])
->append(['link_address_desc','status_desc'])
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
foreach ($lists as &$list) {
$list['link_address_desc'] = MenuEnum::getLinkDesc($list['link_type']).':'.$list['link_address_desc'];
}
return $lists;
}
/**
* @notes 菜单总数
* @return int
* @author ljj
* @date 2022/2/14 11:29 上午
*/
public function count(): int
{
return (new Menu())->count();
}
}
@@ -0,0 +1,52 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\decorate;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\model\decorate\Navigation;
/**
* 底部导航列表
* Class NavigationLists
* @package app\adminapi\lists\decorate
*/
class NavigationLists extends BaseAdminDataLists
{
/**
* @notes 底部导航列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author ljj
* @date 2022/2/14 10:12 上午
*/
public function lists(): array
{
return (new Navigation())->select()->toArray();
}
/**
* @notes 底部导航总数
* @return int
* @author ljj
* @date 2022/2/14 10:13 上午
*/
public function count(): int
{
return (new Navigation())->count();
}
}
@@ -0,0 +1,105 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\dept;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\lists\ListsExcelInterface;
use app\common\lists\ListsSearchInterface;
use app\common\model\dept\Jobs;
/**
* 岗位列表
* Class JobsLists
* @package app\adminapi\lists\dept
*/
class JobsLists extends BaseAdminDataLists implements ListsSearchInterface,ListsExcelInterface
{
/**
* @notes 设置搜索条件
* @return \string[][]
* @author 段誉
* @date 2022/5/26 9:46
*/
public function setSearch(): array
{
return [
'%like%' => ['name'],
'=' => ['code', 'status']
];
}
/**
* @notes 获取管理列表
* @return array
* @author heshihu
* @date 2022/2/21 17:11
*/
public function lists(): array
{
$lists = Jobs::where($this->searchWhere)
->append(['status_desc'])
->limit($this->limitOffset, $this->limitLength)
->order(['sort' => 'desc', 'id' => 'desc'])
->select()
->toArray();
return $lists;
}
/**
* @notes 获取数量
* @return int
* @author 段誉
* @date 2022/5/26 9:48
*/
public function count(): int
{
return Jobs::where($this->searchWhere)->count();
}
/**
* @notes 导出文件名
* @return string
* @author 段誉
* @date 2022/11/24 16:17
*/
public function setFileName(): string
{
return '岗位列表';
}
/**
* @notes 导出字段
* @return string[]
* @author 段誉
* @date 2022/11/24 16:17
*/
public function setExcelFields(): array
{
return [
'code' => '岗位编码',
'name' => '岗位名称',
'remark' => '备注',
'status_desc' => '状态',
'create_time' => '添加时间',
];
}
}
@@ -0,0 +1,491 @@
<?php
namespace app\adminapi\lists\doctor;
use app\adminapi\lists\BaseAdminDataLists;
use app\adminapi\logic\dept\DeptLogic;
use app\common\model\auth\AdminDept;
use app\common\model\DiagnosisViewRecord;
use app\common\model\doctor\Appointment;
use app\common\model\tcm\Prescription;
use app\common\model\auth\AdminRole;
use app\common\model\dict\DictData;
use app\common\lists\ListsExtendInterface;
use app\common\lists\ListsSearchInterface;
use app\common\lists\Traits\HasDataScopeFilter;
use think\facade\Db;
/**
* 医生预约列表
* Class AppointmentLists
* @package app\adminapi\lists\doctor
*/
class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterface, ListsExtendInterface
{
use HasDataScopeFilter;
/**
* 诊单编辑/详情「挂号记录」Tab:按诊单 ID 拉全量挂号,不做医生/医助角色收窄与数据范围过滤。
* 须同时传 patient_id(挂号表存的是诊单 id)与本开关,避免列表页被滥用拓宽可见范围。
*/
private function appointmentListsScopeRelaxedForDiagnosis(): bool
{
return (int) ($this->params['diag_scope_relax'] ?? 0) === 1
&& (int) ($this->params['patient_id'] ?? 0) > 0;
}
/**
* 数据隔离:医生或医助(u.assistant_id = diag.assistant_id)命中可见集合;progress_board 场景放开(看板跨医生查看)
*/
private function applyDataScopeForAppointment($query, bool $progressBoard): void
{
if ($progressBoard) {
return;
}
if ($this->appointmentListsScopeRelaxedForDiagnosis()) {
return;
}
if (!$this->dataScopeShouldApply()) {
return;
}
$ids = $this->getDataScopeVisibleAdminIds();
if ($ids === null) {
return;
}
if ($ids === []) {
$query->whereRaw('0 = 1');
return;
}
$inList = implode(',', $ids);
$query->whereRaw("(a.doctor_id IN ({$inList}) OR u.assistant_id IN ({$inList}))");
}
/**
* 按医助筛选:诊单指派医助或挂号记录上的医助任一命中即可
*
* @param mixed $query
*/
private function applyAssistantIdFilter($query): void
{
$aid = (int) ($this->params['assistant_id'] ?? 0);
if ($aid <= 0) {
return;
}
$query->where(function ($q) use ($aid): void {
$q->where('u.assistant_id', $aid)->whereOr('a.assistant_id', $aid);
});
}
/**
* 按部门筛选:接诊医生、诊单医助或挂号医助所属部门命中子树即可(选父级含子级)
*
* @param mixed $query
*/
private function applyAssistantDeptIdFilter($query): void
{
if (!isset($this->params['assistant_dept_id']) || $this->params['assistant_dept_id'] === '' || (int) $this->params['assistant_dept_id'] <= 0) {
return;
}
$rootDeptId = (int) $this->params['assistant_dept_id'];
$deptIds = DeptLogic::getSelfAndDescendantIds($rootDeptId);
$deptIds = array_values(array_filter(array_map('intval', $deptIds), static function (int $id): bool {
return $id > 0;
}));
if ($deptIds === []) {
$query->whereRaw('0 = 1');
return;
}
$inList = implode(',', $deptIds);
$adTbl = (new AdminDept())->getTable();
$query->whereRaw(
"(EXISTS (SELECT 1 FROM `{$adTbl}` ad WHERE ad.`admin_id` = a.`doctor_id` AND ad.`dept_id` IN ({$inList}))"
. " OR EXISTS (SELECT 1 FROM `{$adTbl}` ad WHERE ad.`admin_id` = u.`assistant_id` AND ad.`dept_id` IN ({$inList}))"
. " OR EXISTS (SELECT 1 FROM `{$adTbl}` ad WHERE ad.`admin_id` = a.`assistant_id` AND ad.`dept_id` IN ({$inList})))"
);
}
/**
* 渠道筛选:与 AppointmentLogic 一致,兼容仅有 channel_source、仅有 channels、或两者皆有的表结构
*
* @param mixed $query
*/
private function applyChannelSourceFilter($query, string $chFilter): void
{
if ($chFilter === '') {
return;
}
$tblFields = Db::name('doctor_appointment')->getTableFields();
$cols = \is_array($tblFields) ? $tblFields : [];
$hasChannelSource = \in_array('channel_source', $cols, true);
$hasChannels = \in_array('channels', $cols, true);
if (!$hasChannelSource && !$hasChannels) {
return;
}
$query->where(function ($q) use ($chFilter, $hasChannelSource, $hasChannels): void {
if ($hasChannelSource && $hasChannels) {
$q->where('a.channel_source', '=', $chFilter)
->whereOr('a.channels', '=', $chFilter);
if (is_numeric($chFilter)) {
$q->whereOr('a.channels', '=', (int) $chFilter);
}
return;
}
if ($hasChannelSource) {
$q->where('a.channel_source', '=', $chFilter);
return;
}
$q->where('a.channels', '=', $chFilter);
if (is_numeric($chFilter)) {
$q->whereOr('a.channels', '=', (int) $chFilter);
}
});
}
/**
* @notes 设置搜索条件
* @return array
*/
public function setSearch(): array
{
return [];
}
/**
* @notes 获取列表
* @return array
*/
public function lists(): array
{
// 获取当前管理员的角色ID
$roleIds = AdminRole::where('admin_id', $this->adminId)->column('role_id');
// 处理患者姓名搜索
if (!empty($this->params['patient_name'])) {
$this->searchWhere[] = ['u.patient_name', 'like', '%' . $this->params['patient_name'] . '%'];
}
// 处理医生姓名搜索
if (!empty($this->params['doctor_name'])) {
$this->searchWhere[] = ['ad.name', 'like', '%' . $this->params['doctor_name'] . '%'];
}
// 处理状态搜索
if (isset($this->params['status']) && $this->params['status'] !== '') {
$this->searchWhere[] = ['a.status', '=', $this->params['status']];
}
// 渠道字典 value(命中 channel_source 或 legacy channels
$chFilter = isset($this->params['channel_source']) ? trim((string) $this->params['channel_source']) : '';
// 处理日期范围搜索
if (!empty($this->params['start_date']) && !empty($this->params['end_date'])) {
$this->searchWhere[] = ['a.appointment_date', 'between', [$this->params['start_date'], $this->params['end_date']]];
} elseif (!empty($this->params['start_date'])) {
$this->searchWhere[] = ['a.appointment_date', '>=', $this->params['start_date']];
} elseif (!empty($this->params['end_date'])) {
$this->searchWhere[] = ['a.appointment_date', '<=', $this->params['end_date']];
}
// 诊单维度患者(挂号表 patient_id 存诊单 id
if (!empty($this->params['patient_id'])) {
$this->searchWhere[] = ['a.patient_id', '=', (int) $this->params['patient_id']];
}
// 按接诊医生筛选(管理端医生进度看板等)
if (!empty($this->params['doctor_id'])) {
$this->searchWhere[] = ['a.doctor_id', '=', (int) $this->params['doctor_id']];
}
// 排除已取消挂号 status=2(医生进度看板等;未显式筛选「已取消」时生效)
if ((int) ($this->params['exclude_cancelled'] ?? 0) === 1) {
$sf = $this->params['status'] ?? '';
if ($sf === '' || (int) $sf !== 2) {
$this->searchWhere[] = ['a.status', '<>', 2];
}
}
// 构建查询(searchWhere 为空时避免部分环境下 where([]) 异常)
$query = Appointment::alias('a')
->with('diagnosis')
->leftJoin('tcm_diagnosis u', 'a.patient_id = u.id')
->leftJoin('admin ad', 'a.doctor_id = ad.id')
->leftJoin('admin asst', 'u.assistant_id = asst.id')
->field('a.*, u.patient_name as patient_name, u.phone as patient_phone, u.gender as gender, u.age as age, u.weight as weight, u.height as height, u.assistant_id as assistant_id, ad.name as doctor_name, asst.name as assistant_name, u.id as diagnosis_id, a.assistant_id as appointment_assistant_id');
if ($this->searchWhere !== []) {
$query->where($this->searchWhere);
}
$this->applyAssistantIdFilter($query);
$this->applyAssistantDeptIdFilter($query);
$this->applyChannelSourceFilter($query, $chFilter);
// 是否确认诊单:1=已确认 0=未确认
if (isset($this->params['diagnosis_confirmed']) && $this->params['diagnosis_confirmed'] !== '') {
$confirmed = (int)$this->params['diagnosis_confirmed'];
$tbl = (new DiagnosisViewRecord())->getTable();
$subSql = "SELECT 1 FROM {$tbl} dvr WHERE dvr.diagnosis_id = u.id AND dvr.is_confirmed = 1 AND dvr.delete_time IS NULL";
if ($confirmed === 1) {
$query->whereExists($subSql);
} else {
$query->whereNotExists($subSql);
}
}
// 面诊进度看板:可切换查看各医生挂号,不按当前账号角色收窄
$progressBoard = (int) ($this->params['progress_board'] ?? 0) === 1;
$diagScopeRelax = $this->appointmentListsScopeRelaxedForDiagnosis();
if (!$progressBoard && !$diagScopeRelax) {
// 如果是医生角色(role_id=1),只显示挂自己号的预约
if (in_array(1, $roleIds)) {
$query->where('a.doctor_id', $this->adminId);
}
// 如果是医助角色(role_id=2),只显示自己添加的患者的预约
if (in_array(2, $roleIds)) {
$query->where('u.assistant_id', $this->adminId);
}
}
$this->applyDataScopeForAppointment($query, $progressBoard);
// 诊单软删除后不再展示对应挂号(leftJoin 时无诊单或诊单未删)
$query->whereRaw('(u.id IS NULL OR u.delete_time IS NULL)');
$lists = $query
->order('a.status', 'asc')
->order('a.appointment_date', 'asc')
->order('a.appointment_time', 'asc')
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
// 添加状态、类型描述及确认诊单状态
$diagnosisIds = array_filter(array_unique(array_column($lists, 'diagnosis_id')));
$confirmedMap = [];
if (!empty($diagnosisIds)) {
$confirmedIds = \think\facade\Db::name('diagnosis_view_records')
->whereIn('diagnosis_id', $diagnosisIds)
->where('is_confirmed', 1)
->whereNull('delete_time')
->column('diagnosis_id');
$confirmedMap = array_flip($confirmedIds ?: []);
}
// 开方状态:诊单是否有处方(按 diagnosis_id 查)
// prescription_today_only=1:仅统计「今日创建」的处方(医生进度看板用,与历史处方区分)
$rxTodayOnly = (int) ($this->params['prescription_today_only'] ?? 0) === 1;
$prescribedDiagnosisIds = [];
if (!empty($diagnosisIds)) {
$rxQ = Prescription::whereIn('diagnosis_id', $diagnosisIds)
->whereNull('delete_time')
->where('void_status', 0);
if ($rxTodayOnly) {
$dayStart = strtotime(date('Y-m-d 00:00:00'));
$dayEnd = strtotime(date('Y-m-d 23:59:59'));
$rxQ->whereBetween('create_time', [$dayStart, $dayEnd]);
}
$prescribedDiagnosisIds = $rxQ->column('diagnosis_id');
$prescribedDiagnosisIds = array_flip($prescribedDiagnosisIds ?: []);
}
// 当前页预约关联的处方(按 appointment_id,取最新一条):用于「开方/查看」与审核状态
$appointmentIds = array_filter(array_map('intval', array_column($lists, 'id')));
$rxByAppointmentId = [];
if (!empty($appointmentIds)) {
$rxRows = Prescription::whereIn('appointment_id', $appointmentIds)
->whereNull('delete_time')
->where('void_status', 0)
->order('id', 'desc')
->field(['id', 'appointment_id', 'audit_status', 'void_status', 'is_system_auto'])
->select()
->toArray();
foreach ($rxRows as $rx) {
$aid = (int) ($rx['appointment_id'] ?? 0);
if ($aid > 0 && !isset($rxByAppointmentId[$aid])) {
$rxByAppointmentId[$aid] = $rx;
}
}
}
$channelNameByValue = DictData::where('type_value', 'channels')->column('name', 'value');
foreach ($lists as &$item) {
$statusMap = [
1 => '已预约',
2 => '已取消',
3 => '已完成',
4 => '已过号',
];
$item['status_desc'] = $statusMap[$item['status']] ?? '未知';
$typeMap = [
'video' => '视频问诊',
'text' => '图文问诊',
'phone' => '电话问诊',
];
$item['appointment_type_desc'] = $typeMap[$item['appointment_type']] ?? '未知';
$periodRaw = (string) ($item['period'] ?? ($item['type'] ?? ''));
$periodMap = [
'morning' => '上午',
'afternoon' => '下午',
'all' => '全天',
];
$item['period_desc'] = $periodMap[$periodRaw] ?? ($periodRaw !== '' ? $periodRaw : '—');
$srcKey = trim((string) ($item['channel_source'] ?? ''));
if ($srcKey === '' && isset($item['channels']) && $item['channels'] !== '' && $item['channels'] !== null) {
$srcKey = trim((string) $item['channels']);
}
if ($srcKey !== '') {
$item['channel_source_desc'] = (string) ($channelNameByValue[$srcKey] ?? $channelNameByValue[(string) (int) $srcKey] ?? $srcKey);
} else {
$item['channel_source_desc'] = '—';
}
$item['diagnosis_confirmed'] = isset($confirmedMap[$item['diagnosis_id'] ?? 0]) ? 1 : 0;
$item['has_prescription'] = isset($prescribedDiagnosisIds[$item['diagnosis_id'] ?? 0]) ? 1 : 0;
$apptId = (int) ($item['id'] ?? 0);
$apptRx = $rxByAppointmentId[$apptId] ?? null;
$item['prescription_audit_status'] = $apptRx !== null ? (int) ($apptRx['audit_status'] ?? -1) : -1;
$item['prescription_void_status'] = $apptRx !== null ? (int) ($apptRx['void_status'] ?? 0) : 0;
$item['prescription_is_system_auto'] = $apptRx !== null ? (int) ($apptRx['is_system_auto'] ?? 0) : 0;
// 格式化时间戳为日期时间
if (isset($item['create_time']) && is_numeric($item['create_time'])) {
$item['create_time'] = date('Y-m-d H:i:s', $item['create_time']);
}
if (isset($item['update_time']) && is_numeric($item['update_time'])) {
$item['update_time'] = date('Y-m-d H:i:s', $item['update_time']);
}
}
return $lists;
}
/**
* 列表 count / Tab 角标统计共用的过滤条件(不依赖 lists() 对 searchWhere 的副作用)
* @param mixed $query
* @param bool $applyStatusFilter 为 false 时按状态分组统计各 Tab 数量
*/
private function applyAppointmentCountFilters($query, bool $applyStatusFilter): void
{
$roleIds = AdminRole::where('admin_id', $this->adminId)->column('role_id');
if (!empty($this->params['patient_name'])) {
$query->where('u.patient_name', 'like', '%' . $this->params['patient_name'] . '%');
}
if (!empty($this->params['doctor_name'])) {
$query->where('ad.name', 'like', '%' . $this->params['doctor_name'] . '%');
}
if ($applyStatusFilter && isset($this->params['status']) && $this->params['status'] !== '') {
$query->where('a.status', '=', $this->params['status']);
}
$chFilter = isset($this->params['channel_source']) ? trim((string) $this->params['channel_source']) : '';
if (!empty($this->params['start_date']) && !empty($this->params['end_date'])) {
$query->whereBetween('a.appointment_date', [$this->params['start_date'], $this->params['end_date']]);
} elseif (!empty($this->params['start_date'])) {
$query->where('a.appointment_date', '>=', $this->params['start_date']);
} elseif (!empty($this->params['end_date'])) {
$query->where('a.appointment_date', '<=', $this->params['end_date']);
}
if (!empty($this->params['patient_id'])) {
$query->where('a.patient_id', '=', (int) $this->params['patient_id']);
}
if (!empty($this->params['doctor_id'])) {
$query->where('a.doctor_id', '=', (int) $this->params['doctor_id']);
}
$this->applyAssistantIdFilter($query);
$this->applyAssistantDeptIdFilter($query);
$this->applyChannelSourceFilter($query, $chFilter);
if ((int) ($this->params['exclude_cancelled'] ?? 0) === 1) {
$sf = $this->params['status'] ?? '';
if ($sf === '' || (int) $sf !== 2) {
$query->where('a.status', '<>', 2);
}
}
if (isset($this->params['diagnosis_confirmed']) && $this->params['diagnosis_confirmed'] !== '') {
$confirmed = (int)$this->params['diagnosis_confirmed'];
$tbl = (new DiagnosisViewRecord())->getTable();
$subSql = "SELECT 1 FROM {$tbl} dvr WHERE dvr.diagnosis_id = u.id AND dvr.is_confirmed = 1 AND dvr.delete_time IS NULL";
if ($confirmed === 1) {
$query->whereExists($subSql);
} else {
$query->whereNotExists($subSql);
}
}
$progressBoard = (int) ($this->params['progress_board'] ?? 0) === 1;
$diagScopeRelax = $this->appointmentListsScopeRelaxedForDiagnosis();
if (!$progressBoard && !$diagScopeRelax) {
if (in_array(1, $roleIds)) {
$query->where('a.doctor_id', $this->adminId);
}
if (in_array(2, $roleIds)) {
$query->where('u.assistant_id', $this->adminId);
}
}
$this->applyDataScopeForAppointment($query, $progressBoard);
$query->whereRaw('(u.id IS NULL OR u.delete_time IS NULL)');
}
/**
* @notes 获取数量
* @return int
*/
public function count(): int
{
$query = Appointment::alias('a')
->leftJoin('tcm_diagnosis u', 'a.patient_id = u.id')
->leftJoin('admin ad', 'a.doctor_id = ad.id');
$this->applyAppointmentCountFilters($query, true);
return (int)$query->count();
}
/**
* @notes 扩展字段:按需返回各状态数量(一次 GROUP BY,替代前端 4 次列表请求)
* @return array
*/
public function extend(): array
{
if (empty($this->params['include_status_counts'])) {
return [];
}
$query = Appointment::alias('a')
->leftJoin('tcm_diagnosis u', 'a.patient_id = u.id')
->leftJoin('admin ad', 'a.doctor_id = ad.id');
$this->applyAppointmentCountFilters($query, false);
$rows = $query->field('a.status, COUNT(*) AS cnt')
->group('a.status')
->select()
->toArray();
$out = [1 => 0, 2 => 0, 3 => 0, 4 => 0];
foreach ($rows as $r) {
$s = (int)($r['status'] ?? 0);
if (array_key_exists($s, $out)) {
$out[$s] = (int)($r['cnt'] ?? 0);
}
}
return ['status_count' => $out];
}
}
@@ -0,0 +1,76 @@
<?php
namespace app\adminapi\lists\doctor;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\model\doctor\Medicine;
use app\common\lists\ListsSearchInterface;
/**
* 药品库列表
*/
class MedicineLists extends BaseAdminDataLists implements ListsSearchInterface
{
/**
* 设置搜索条件
*/
public function setSearch(): array
{
return [
'%like%' => ['supplier'],
'=' => ['status'],
];
}
/**
* name 参数:中文等按名称模糊;纯英文字母按拼音首字母字段 + 名称模糊(OR)
*/
private function appendMedicineNameSearch($query): void
{
$keyword = trim((string) ($this->params['name'] ?? ''));
if ($keyword === '') {
return;
}
if (preg_match('/^[a-zA-Z]+$/', $keyword)) {
$kw = strtolower($keyword);
$query->where(function ($q) use ($kw, $keyword) {
$q->where('name_pinyin_abbr', 'like', '%' . $kw . '%')
->whereOr('name', 'like', '%' . $keyword . '%');
});
} else {
$query->where('name', 'like', '%' . $keyword . '%');
}
}
/**
* 列表字段
*/
public function lists(): array
{
$query = Medicine::where($this->searchWhere);
$this->appendMedicineNameSearch($query);
return $query
->field([
'id', 'name', 'name_pinyin_abbr', 'supplier', 'unit',
'settlement_price', 'retail_price', 'stock',
'image', 'status', 'remark',
'create_time', 'update_time',
])
->limit($this->limitOffset, $this->limitLength)
->order(['id' => 'desc'])
->select()
->toArray();
}
/**
* 列表数量
*/
public function count(): int
{
$query = Medicine::where($this->searchWhere);
$this->appendMedicineNameSearch($query);
return $query->count();
}
}
@@ -0,0 +1,70 @@
<?php
namespace app\adminapi\lists\doctor;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\model\doctor\Roster;
use app\common\lists\ListsSearchInterface;
/**
* 医生排班列表
* Class RosterLists
* @package app\adminapi\lists\doctor
*/
class RosterLists extends BaseAdminDataLists implements ListsSearchInterface
{
/**
* @notes 设置搜索条件
* @return array
*/
public function setSearch(): array
{
return [
'=' => ['doctor_id', 'period', 'status'],
];
}
/**
* @notes 获取列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function lists(): array
{
$where = $this->searchWhere;
// 处理日期范围搜索
if (!empty($this->params['start_date']) && !empty($this->params['end_date'])) {
$where[] = ['date', 'between', [$this->params['start_date'], $this->params['end_date']]];
}
$lists = Roster::where($where)
->field([
'id', 'doctor_id', 'date', 'period', 'start_time', 'end_time', 'shift_type', 'slot_minutes',
'status', 'quota', 'max_patients', 'booked_count', 'remark', 'create_time', 'update_time',
])
->order(['date' => 'asc', 'start_time' => 'asc', 'id' => 'asc'])
->select()
->toArray();
return $lists;
}
/**
* @notes 获取数量
* @return int
*/
public function count(): int
{
$where = $this->searchWhere;
// 处理日期范围搜索
if (!empty($this->params['start_date']) && !empty($this->params['end_date'])) {
$where[] = ['date', 'between', [$this->params['start_date'], $this->params['end_date']]];
}
return Roster::where($where)->count();
}
}
@@ -0,0 +1,516 @@
<?php
namespace app\adminapi\lists\doctor;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\model\doctor\Appointment;
use app\common\model\auth\Admin;
/**
* 医生统计列表
* Class StatisticsLists
* @package app\adminapi\lists\doctor
*/
class StatisticsLists extends BaseAdminDataLists
{
protected $type = 'doctor';
/**
* @param string $type 统计类型:doctor(医生统计) 或 dept(部门统计)
*/
public function __construct($type = 'doctor')
{
parent::__construct();
$this->type = $type;
}
/**
* @notes 搜索条件
* @return array
*/
public function setSearch(): array
{
return [];
}
/**
* @notes 获取列表
* @return array
*/
public function lists(): array
{
if ($this->type === 'dept') {
return $this->getDeptStatistics();
}
$doctorId = $this->params['doctor_id'] ?? '';
$timeType = $this->params['time_type'] ?? 'today';
$startDate = $this->params['start_date'] ?? '';
$endDate = $this->params['end_date'] ?? '';
// 计算时间范围
list($startDate, $endDate, $timeRangeText) = $this->getTimeRange($timeType, $startDate, $endDate);
// 获取医生列表(role_id=1 表示医生角色)
$doctorAdminIds = \app\common\model\auth\AdminRole::where('role_id', 1)->column('admin_id');
if (empty($doctorAdminIds)) {
return [];
}
// 查询1:获取预约统计数据(快速查询,只查appointment表)
$statisticsData = \think\facade\Db::name('doctor_appointment')
->field([
'doctor_id',
'COUNT(*) as total_count',
'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as registered_count',
'SUM(CASE WHEN status = 3 THEN 1 ELSE 0 END) as completed_count',
'SUM(CASE WHEN status = 4 THEN 1 ELSE 0 END) as missed_count',
'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as cancelled_count'
])
->whereIn('doctor_id', $doctorAdminIds)
->where('appointment_date', '>=', $startDate)
->where('appointment_date', '<=', $endDate);
if ($doctorId) {
$statisticsData->where('doctor_id', $doctorId);
}
$statisticsData = $statisticsData->group('doctor_id')->select()->toArray();
// 将统计数据按 doctor_id 索引
$statsMap = [];
foreach ($statisticsData as $stat) {
$statsMap[$stat['doctor_id']] = $stat;
}
// 诊单数:统计期内该医生挂号对应的 distinct 诊单(patient_id 存的是诊单 id
$diagnosisCountQuery = \think\facade\Db::name('doctor_appointment')
->field('doctor_id, COUNT(DISTINCT patient_id) as diagnosis_count')
->whereIn('doctor_id', $doctorAdminIds)
->where('appointment_date', '>=', $startDate)
->where('appointment_date', '<=', $endDate);
if ($doctorId) {
$diagnosisCountQuery->where('doctor_id', $doctorId);
}
$diagnosisCountRows = $diagnosisCountQuery->group('doctor_id')->select()->toArray();
$diagnosisCountMap = [];
foreach ($diagnosisCountRows as $row) {
$diagnosisCountMap[(int) $row['doctor_id']] = (int) $row['diagnosis_count'];
}
// 成交单:上述诊单中存在有效处方(未删除、未作废)的数量
$dealCountQuery = \think\facade\Db::name('doctor_appointment')->alias('apt')
->join('tcm_prescription rx', 'rx.diagnosis_id = apt.patient_id')
->field('apt.doctor_id, COUNT(DISTINCT apt.patient_id) as deal_count')
->whereIn('apt.doctor_id', $doctorAdminIds)
->where('apt.appointment_date', '>=', $startDate)
->where('apt.appointment_date', '<=', $endDate)
->whereNull('rx.delete_time')
->whereRaw('IFNULL(rx.void_status, 0) <> 1');
if ($doctorId) {
$dealCountQuery->where('apt.doctor_id', $doctorId);
}
$dealCountRows = $dealCountQuery->group('apt.doctor_id')->select()->toArray();
$dealCountMap = [];
foreach ($dealCountRows as $row) {
$dealCountMap[(int) $row['doctor_id']] = (int) $row['deal_count'];
}
// 获取医生信息
$doctorQuery = Admin::field('id,name')->whereIn('id', $doctorAdminIds);
if ($doctorId) {
$doctorQuery->where('id', $doctorId);
}
$doctors = $doctorQuery->select()->toArray();
// 查询2:获取部门统计信息(通过assistant_id直接关联,按部门和状态分组统计数量)
$doctorIdsWithAppointments = array_keys($statsMap);
$deptStatsMap = [];
$deptStatusMap = [];
$channelStatsMap = [];
$channelStatusMap = [];
if (!empty($doctorIdsWithAppointments)) {
// 通过 assistant_id → admin_dept → dept 获取部门统计(按状态分组)
$deptStats = \think\facade\Db::name('doctor_appointment')
->alias('apt')
->leftJoin('admin_dept ad', 'apt.assistant_id = ad.admin_id')
->leftJoin('dept dept', 'ad.dept_id = dept.id')
->field([
'apt.doctor_id',
'dept.id as dept_id',
'dept.name as dept_name',
'apt.status',
'COUNT(*) as count'
])
->whereIn('apt.doctor_id', $doctorIdsWithAppointments)
->where('apt.appointment_date', '>=', $startDate)
->where('apt.appointment_date', '<=', $endDate)
->whereNotNull('dept.id')
->group('apt.doctor_id, dept.id, apt.status')
->select()
->toArray();
// 组装部门统计数据:医生ID => "部门1(数量1) 部门2(数量2)"
// 同时组装部门状态数据:医生ID => 部门ID => 状态 => 数量
foreach ($deptStats as $stat) {
$doctorId = $stat['doctor_id'];
$deptId = $stat['dept_id'];
$deptName = $stat['dept_name'];
$status = $stat['status'];
$count = $stat['count'];
// 组装部门统计数据
if (!isset($deptStatsMap[$doctorId])) {
$deptStatsMap[$doctorId] = [];
}
if (!isset($deptStatsMap[$doctorId][$deptId])) {
$deptStatsMap[$doctorId][$deptId] = [
'dept_name' => $deptName,
'total_count' => 0
];
}
$deptStatsMap[$doctorId][$deptId]['total_count'] += $count;
// 组装部门状态数据
if (!isset($deptStatusMap[$doctorId])) {
$deptStatusMap[$doctorId] = [];
}
if (!isset($deptStatusMap[$doctorId][$deptId])) {
$deptStatusMap[$doctorId][$deptId] = [
'dept_name' => $deptName,
'statuses' => []
];
}
$deptStatusMap[$doctorId][$deptId]['statuses'][$status] = $count;
}
// 格式化部门统计数据
foreach ($deptStatsMap as $doctorId => &$depts) {
$formattedDepts = [];
foreach ($depts as $dept) {
$formattedDepts[] = $dept['dept_name'] . '(' . $dept['total_count'] . ')';
}
$deptStatsMap[$doctorId] = $formattedDepts;
}
// 获取渠道字典(dict_data.value => name),与挂号创建时 channel_source 存字典 value 一致
$channelDict = \think\facade\Db::name('dict_data')
->where('type_value', 'channels')
->column('name', 'value');
// 渠道:业务保存的是 channel_source(字典 value 字符串,见 AppointmentLogic::create);旧库可能仅有 channels(tinyint)
$channelBuckets = [];
$channelStatusBuckets = [];
try {
$channelStats = \think\facade\Db::name('doctor_appointment')
->field('doctor_id, channel_source, status, COUNT(*) as channel_count')
->whereIn('doctor_id', $doctorIdsWithAppointments)
->where('appointment_date', '>=', $startDate)
->where('appointment_date', '<=', $endDate)
->where('channel_source', '<>', '')
->group('doctor_id, channel_source, status')
->select()
->toArray();
foreach ($channelStats as $stat) {
$did = (int) $stat['doctor_id'];
$src = trim((string) ($stat['channel_source'] ?? ''));
$status = $stat['status'];
$cnt = (int) ($stat['channel_count'] ?? 0);
if ($src === '' || $cnt < 1) {
continue;
}
// 组装渠道统计数据
if (!isset($channelBuckets[$did])) {
$channelBuckets[$did] = [];
}
if (!isset($channelBuckets[$did][$src])) {
$channelBuckets[$did][$src] = 0;
}
$channelBuckets[$did][$src] += $cnt;
// 组装渠道状态数据
if (!isset($channelStatusBuckets[$did])) {
$channelStatusBuckets[$did] = [];
}
if (!isset($channelStatusBuckets[$did][$src])) {
$channelStatusBuckets[$did][$src] = [];
}
$channelStatusBuckets[$did][$src][$status] = $cnt;
}
} catch (\Throwable) {
// 无 channel_source 字段等
}
$mergeLegacyChannels = static function (array &$buckets, array &$statusBuckets, array $rows): void {
foreach ($rows as $stat) {
$did = (int) $stat['doctor_id'];
$key = (string) (int) ($stat['channels'] ?? 0);
$status = $stat['status'];
$cnt = (int) ($stat['channel_count'] ?? 0);
if ($key === '0' || $cnt < 1) {
continue;
}
// 组装渠道统计数据
if (!isset($buckets[$did])) {
$buckets[$did] = [];
}
if (!isset($buckets[$did][$key])) {
$buckets[$did][$key] = 0;
}
$buckets[$did][$key] += $cnt;
// 组装渠道状态数据
if (!isset($statusBuckets[$did])) {
$statusBuckets[$did] = [];
}
if (!isset($statusBuckets[$did][$key])) {
$statusBuckets[$did][$key] = [];
}
$statusBuckets[$did][$key][$status] = $cnt;
}
};
try {
$legacyStats = \think\facade\Db::name('doctor_appointment')
->field('doctor_id, channels, status, COUNT(*) as channel_count')
->whereIn('doctor_id', $doctorIdsWithAppointments)
->where('appointment_date', '>=', $startDate)
->where('appointment_date', '<=', $endDate)
->where('channels', '>', 0)
->where(function ($q) {
$q->whereNull('channel_source')->whereOr('channel_source', '=', '');
})
->group('doctor_id, channels, status')
->select()
->toArray();
$mergeLegacyChannels($channelBuckets, $channelStatusBuckets, $legacyStats);
} catch (\Throwable) {
try {
$legacyStats = \think\facade\Db::name('doctor_appointment')
->field('doctor_id, channels, status, COUNT(*) as channel_count')
->whereIn('doctor_id', $doctorIdsWithAppointments)
->where('appointment_date', '>=', $startDate)
->where('appointment_date', '<=', $endDate)
->where('channels', '>', 0)
->group('doctor_id, channels, status')
->select()
->toArray();
$mergeLegacyChannels($channelBuckets, $channelStatusBuckets, $legacyStats);
} catch (\Throwable) {
// 无 channels 字段
}
}
foreach ($channelBuckets as $did => $byKey) {
$parts = [];
foreach ($byKey as $key => $cnt) {
$label = $channelDict[$key] ?? $channelDict[(string) $key] ?? $key;
$parts[] = $label . '(' . $cnt . ')';
}
$channelStatsMap[$did] = $parts;
}
// 组装渠道状态明细数据
foreach ($channelStatusBuckets as $did => $byKey) {
foreach ($byKey as $key => $statuses) {
$label = $channelDict[$key] ?? $channelDict[(string) $key] ?? $key;
if (!isset($channelStatusMap[$did])) {
$channelStatusMap[$did] = [];
}
$channelStatusMap[$did][] = [
'channel_name' => $label,
'statuses' => $statuses
];
}
}
}
// 组装结果
$result = [];
foreach ($doctors as $doctor) {
$stat = $statsMap[$doctor['id']] ?? [
'total_count' => 0,
'registered_count' => 0,
'completed_count' => 0,
'missed_count' => 0,
'cancelled_count' => 0
];
$did = (int) $doctor['id'];
$diagnosisCount = $diagnosisCountMap[$did] ?? 0;
$dealCount = $dealCountMap[$did] ?? 0;
$dealRate = $diagnosisCount > 0
? round(($dealCount / $diagnosisCount) * 100, 2)
: 0;
// 计算完成率
$completionRate = $stat['total_count'] > 0
? round(($stat['completed_count'] / $stat['total_count']) * 100, 2)
: 0;
// 格式化部门状态明细
$deptStatusDetails = [];
if (isset($deptStatusMap[$doctor['id']])) {
foreach ($deptStatusMap[$doctor['id']] as $deptId => $deptInfo) {
$deptStatusDetails[] = [
'dept_id' => $deptId,
'dept_name' => $deptInfo['dept_name'],
'statuses' => $deptInfo['statuses']
];
}
}
// 格式化渠道状态明细
$channelStatusDetails = [];
if (isset($channelStatusMap[$doctor['id']])) {
$channelStatusDetails = $channelStatusMap[$doctor['id']];
}
$result[] = [
'doctor_id' => $doctor['id'],
'doctor_name' => $doctor['name'],
'total_count' => (int)$stat['total_count'] ?? 0,
'registered_count' => (int)$stat['registered_count'] ?? 0,
'completed_count' => (int)$stat['completed_count'] ?? 0,
'missed_count' => (int)$stat['missed_count'] ?? 0,
'cancelled_count' => (int)$stat['cancelled_count'] ?? 0,
'diagnosis_count' => $diagnosisCount ?? 0,
'deal_count' => $dealCount ?? 0,
'deal_rate' => $dealRate ?? 0,
'completion_rate' => $completionRate ?? 0,
'dept_status_details' => $deptStatusDetails,
'channel_status_details' => $channelStatusDetails,
'time_range' => $timeRangeText
];
}
return $result;
}
/**
* @notes 获取部门统计数据
* @return array
*/
private function getDeptStatistics()
{
$deptId = $this->params['dept_id'] ?? '';
$timeType = $this->params['time_type'] ?? 'today';
$startDate = $this->params['start_date'] ?? '';
$endDate = $this->params['end_date'] ?? '';
// 计算时间范围
list($startDate, $endDate, $timeRangeText) = $this->getTimeRange($timeType, $startDate, $endDate);
// 查询部门统计数据
$deptStatsQuery = \think\facade\Db::name('doctor_appointment')
->alias('apt')
->leftJoin('zyt_admin_dept ad', 'apt.assistant_id = ad.admin_id')
->leftJoin('zyt_dept dept', 'ad.dept_id = dept.id')
->field([
'dept.id as dept_id',
'dept.name as dept_name',
'COUNT(*) as total_count',
'SUM(CASE WHEN apt.status = 1 THEN 1 ELSE 0 END) as registered_count',
'SUM(CASE WHEN apt.status = 3 THEN 1 ELSE 0 END) as completed_count',
'SUM(CASE WHEN apt.status = 2 THEN 1 ELSE 0 END) as canceled_count',
'SUM(CASE WHEN apt.status = 4 THEN 1 ELSE 0 END) as expired_count'
])
->where('apt.appointment_date', '>=', $startDate)
->where('apt.appointment_date', '<=', $endDate)
->whereNotNull('dept.id');
if ($deptId) {
$deptStatsQuery->where('dept.id', $deptId);
}
$deptStats = $deptStatsQuery->group('dept.id')->select()->toArray();
// 组装结果
$result = [];
foreach ($deptStats as $stat) {
// 计算完成率
$completionRate = $stat['total_count'] > 0
? round(($stat['completed_count'] / $stat['total_count']) * 100, 2)
: 0;
$result[] = [
'dept_id' => $stat['dept_id'],
'dept_name' => $stat['dept_name'],
'total_count' => (int)$stat['total_count'] ?? 0,
'registered_count' => (int)$stat['registered_count'] ?? 0,
'completed_count' => (int)$stat['completed_count'] ?? 0,
'canceled_count' => (int)$stat['canceled_count'] ?? 0,
'expired_count' => (int)$stat['expired_count'] ?? 0,
'completion_rate' => $completionRate,
'time_range' => $timeRangeText
];
}
return $result;
}
/**
* @notes 获取时间范围
* @param string $timeType
* @param string $customStartDate
* @param string $customEndDate
* @return array
*/
private function getTimeRange($timeType, $customStartDate, $customEndDate)
{
$today = date('Y-m-d');
switch ($timeType) {
case 'today':
$startDate = $today;
$endDate = $today;
$timeRangeText = '今天 (' . $today . ')';
break;
case 'week':
$startDate = date('Y-m-d', strtotime('-6 days'));
$endDate = $today;
$timeRangeText = '最近7天 (' . $startDate . ' 至 ' . $endDate . ')';
break;
case 'month':
$startDate = date('Y-m-d', strtotime('-29 days'));
$endDate = $today;
$timeRangeText = '最近30天 (' . $startDate . ' 至 ' . $endDate . ')';
break;
case 'custom':
if ($customStartDate && $customEndDate) {
$startDate = $customStartDate;
$endDate = $customEndDate;
$timeRangeText = '自定义 (' . $startDate . ' 至 ' . $endDate . ')';
} else {
$startDate = $today;
$endDate = $today;
$timeRangeText = '今天 (' . $today . ')';
}
break;
default:
$startDate = $today;
$endDate = $today;
$timeRangeText = '今天 (' . $today . ')';
}
return [$startDate, $endDate, $timeRangeText];
}
/**
* @notes 获取数量
* @return int
*/
public function count(): int
{
return count($this->lists());
}
}
@@ -0,0 +1,74 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\file;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\lists\ListsSearchInterface;
use app\common\model\file\FileCate;
/**
* 文件分类列表
* Class FileCateLists
* @package app\adminapi\lists\file
*/
class FileCateLists extends BaseAdminDataLists implements ListsSearchInterface
{
/**
* @notes 文件分类搜素条件
* @return \string[][]
* @author 段誉
* @date 2021/12/29 14:24
*/
public function setSearch(): array
{
return [
'=' => ['type']
];
}
/**
* @notes 获取文件分类列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2021/12/29 14:24
*/
public function lists(): array
{
$lists = (new FileCate())->field(['id,pid,type,name'])
->where($this->searchWhere)
->order('id desc')
->select()->toArray();
return linear_to_tree($lists, 'children');
}
/**
* @notes 获取文件分类数量
* @return int
* @author 段誉
* @date 2021/12/29 14:24
*/
public function count(): int
{
return (new FileCate())->where($this->searchWhere)->count();
}
}
@@ -0,0 +1,111 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\file;
use app\adminapi\lists\BaseAdminDataLists;
use app\adminapi\logic\FileLogic;
use app\common\enum\FileEnum;
use app\common\lists\ListsSearchInterface;
use app\common\model\file\File;
use app\common\model\file\FileCate;
use app\common\service\FileService;
/**
* 文件列表
* Class FileLists
* @package app\adminapi\lists\file
*/
class FileLists extends BaseAdminDataLists implements ListsSearchInterface
{
/**
* @notes 文件搜索条件
* @return \string[][]
* @author 段誉
* @date 2021/12/29 14:27
*/
public function setSearch(): array
{
// 不按 source 搜索:列表固定为「当前管理员后台上传」,避免请求参数覆盖权限条件
return [
'=' => ['type'],
'%like%' => ['name']
];
}
/**
* @notes 额外查询处理
* @return array
* @author 段誉
* @date 2024/2/7 10:26
*/
public function queryWhere(): array
{
$where = [];
if (!empty($this->params['cid'])) {
$cateChild = FileLogic::getCateIds($this->params['cid']);
array_push($cateChild, $this->params['cid']);
$where[] = ['cid', 'in', $cateChild];
}
return $where;
}
/**
* @notes 获取文件列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2021/12/29 14:27
*/
public function lists(): array
{
$lists = (new File())->field(['id,cid,type,name,uri,create_time'])
->order('id', 'desc')
->where($this->searchWhere)
->where($this->queryWhere())
->where('source', FileEnum::SOURCE_ADMIN)
->where('source_id', $this->adminId)
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
foreach ($lists as &$item) {
$item['url'] = FileService::getFileUrl($item['uri']);
}
return $lists;
}
/**
* @notes 获取文件数量
* @return int
* @author 段誉
* @date 2021/12/29 14:29
*/
public function count(): int
{
return (new File())->where($this->searchWhere)
->where($this->queryWhere())
->where('source', FileEnum::SOURCE_ADMIN)
->where('source_id', $this->adminId)
->count();
}
}
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace app\adminapi\lists\finance;
use app\adminapi\logic\dept\DeptLogic;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\lists\ListsExtendInterface;
use app\common\lists\ListsSearchInterface;
use app\common\model\finance\AccountCost;
use app\common\service\qywx\MediaChannelService;
class AccountCostLists extends BaseAdminDataLists implements ListsSearchInterface, ListsExtendInterface
{
public function setSearch(): array
{
$searchFields = ['remark', 'creator_name', 'updater_name'];
if (AccountCost::supportsDeptBinding()) {
$searchFields[] = 'dept_name';
}
return [
'%like%' => $searchFields,
];
}
private function baseQuery()
{
$query = AccountCost::where($this->searchWhere);
if (!empty($this->params['start_date']) && !empty($this->params['end_date'])) {
$query->whereBetween('cost_date', [$this->params['start_date'], $this->params['end_date']]);
} elseif (!empty($this->params['start_date'])) {
$query->where('cost_date', '>=', $this->params['start_date']);
} elseif (!empty($this->params['end_date'])) {
$query->where('cost_date', '<=', $this->params['end_date']);
}
if (!empty($this->params['media_channel_code'])) {
$query->where('media_channel_code', trim((string) $this->params['media_channel_code']));
}
if (AccountCost::supportsDeptBinding() && !empty($this->params['dept_id'])) {
$query->where('dept_id', (int) $this->params['dept_id']);
}
return $query;
}
public function lists(): array
{
return $this->baseQuery()
->order(['cost_date' => 'desc', 'id' => 'desc'])
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
}
public function count(): int
{
return (int) $this->baseQuery()->count();
}
public function extend(): array
{
return [
'total_amount' => round((float) $this->baseQuery()->sum('amount'), 2),
'days_count' => (int) $this->baseQuery()->distinct(true)->count('cost_date'),
'media_channel_options' => MediaChannelService::getOptions(),
'media_channel_groups' => MediaChannelService::getOptionGroups(),
'dept_options' => DeptLogic::getAllData(),
'supports_dept_binding' => AccountCost::supportsDeptBinding(),
'default_media_channel_code' => MediaChannelService::getDefaultCode(),
];
}
}
@@ -0,0 +1,119 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\finance;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\enum\user\AccountLogEnum;
use app\common\lists\ListsSearchInterface;
use app\common\model\user\UserAccountLog;
use app\common\service\FileService;
/**
* 账记流水列表
* Class AccountLogLists
* @package app\adminapi\lists\finance
*/
class AccountLogLists extends BaseAdminDataLists implements ListsSearchInterface
{
/**
* @notes 搜索条件
* @return array
* @author 段誉
* @date 2023/2/24 15:26
*/
public function setSearch(): array
{
return [
'=' => ['al.change_type'],
];
}
/**
* @notes 搜索条件
* @author 段誉
* @date 2023/2/24 15:26
*/
public function queryWhere()
{
$where = [];
// 用户余额
if (isset($this->params['type']) && $this->params['type'] == 'um') {
$where[] = ['change_type', 'in', AccountLogEnum::getUserMoneyChangeType()];
}
if (!empty($this->params['user_info'])) {
$where[] = ['u.sn|u.nickname|u.mobile|u.account', 'like', '%' . $this->params['user_info'] . '%'];
}
if (!empty($this->params['start_time'])) {
$where[] = ['al.create_time', '>=', strtotime($this->params['start_time'])];
}
if (!empty($this->params['end_time'])) {
$where[] = ['al.create_time', '<=', strtotime($this->params['end_time'])];
}
return $where;
}
/**
* @notes 获取列表
* @return array
* @author 段誉
* @date 2023/2/24 15:31
*/
public function lists(): array
{
$field = 'u.nickname,u.account,u.sn,u.avatar,u.mobile,al.action,al.change_amount,al.left_amount,al.change_type,al.source_sn,al.create_time';
$lists = UserAccountLog::alias('al')
->join('user u', 'u.id = al.user_id')
->field($field)
->where($this->searchWhere)
->where($this->queryWhere())
->order('al.id', 'desc')
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
foreach ($lists as &$item) {
$item['avatar'] = FileService::getFileUrl($item['avatar']);
$item['change_type_desc'] = AccountLogEnum::getChangeTypeDesc($item['change_type']);
$symbol = $item['action'] == AccountLogEnum::INC ? '+' : '-';
$item['change_amount'] = $symbol . $item['change_amount'];
}
return $lists;
}
/**
* @notes 获取数量
* @return int
* @author 段誉
* @date 2023/2/24 15:36
*/
public function count(): int
{
return UserAccountLog::alias('al')
->join('user u', 'u.id = al.user_id')
->where($this->queryWhere())
->where($this->searchWhere)
->count();
}
}
@@ -0,0 +1,79 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\finance;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\model\refund\RefundLog;
/**
* 退款日志列表
* Class RefundLogLists
* @package app\adminapi\lists\product
*/
class RefundLogLists extends BaseAdminDataLists
{
/**
* @notes 查询条件
* @return array
* @author 段誉
* @date 2023/3/1 9:55
*/
public function queryWhere()
{
$where[] = ['record_id', '=', $this->params['record_id'] ?? 0];
return $where;
}
/**
* @notes 获取列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2023/3/1 9:56
*/
public function lists(): array
{
$lists = (new RefundLog())
->order(['id' => 'desc'])
->where($this->queryWhere())
->limit($this->limitOffset, $this->limitLength)
->hidden(['refund_msg'])
->append(['handler', 'refund_status_text'])
->select()
->toArray();
return $lists;
}
/**
* @notes 获取数量
* @return int
* @author 段誉
* @date 2023/3/1 9:56
*/
public function count(): int
{
return (new RefundLog())
->where($this->queryWhere())
->count();
}
}
@@ -0,0 +1,143 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\finance;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\enum\RefundEnum;
use app\common\lists\ListsExtendInterface;
use app\common\lists\ListsSearchInterface;
use app\common\model\refund\RefundRecord;
use app\common\service\FileService;
/**
* 退款记录列表
* Class RefundRecordLists
* @package app\adminapi\lists\product
*/
class RefundRecordLists extends BaseAdminDataLists implements ListsSearchInterface, ListsExtendInterface
{
/**
* @notes 查询条件
* @return \string[][]
* @author 段誉
* @date 2023/3/1 9:51
*/
public function setSearch(): array
{
return [
'=' => ['r.sn', 'r.order_sn', 'r.refund_type'],
];
}
/**
* @notes 查询条件
* @param bool $flag
* @return array
* @author 段誉
* @date 2023/3/1 9:51
*/
public function queryWhere($flag = true)
{
$where = [];
if (!empty($this->params['user_info'])) {
$where[] = ['u.sn|u.nickname|u.mobile|u.account', 'like', '%' . $this->params['user_info'] . '%'];
}
if (!empty($this->params['start_time'])) {
$where[] = ['r.create_time', '>=', strtotime($this->params['start_time'])];
}
if (!empty($this->params['end_time'])) {
$where[] = ['r.create_time', '<=', strtotime($this->params['end_time'])];
}
if ($flag == true) {
if (isset($this->params['refund_status']) && $this->params['refund_status'] != '') {
$where[] = ['r.refund_status', '=', $this->params['refund_status']];
}
}
return $where;
}
/**
* @notes 获取列表
* @return array
* @author 段誉
* @date 2023/3/1 9:51
*/
public function lists(): array
{
$lists = (new RefundRecord())->alias('r')
->field('r.*,u.nickname,u.avatar')
->join('user u', 'u.id = r.user_id')
->order(['r.id' => 'desc'])
->where($this->searchWhere)
->where($this->queryWhere())
->limit($this->limitOffset, $this->limitLength)
->append(['refund_type_text', 'refund_status_text', 'refund_way_text'])
->select()
->toArray();
foreach ($lists as &$item) {
$item['avatar'] = FileService::getFileUrl($item['avatar']);
}
return $lists;
}
/**
* @notes 获取数量
* @return int
* @author 段誉
* @date 2023/3/1 9:51
*/
public function count(): int
{
return (new RefundRecord())->alias('r')
->join('user u', 'u.id = r.user_id')
->where($this->searchWhere)
->where($this->queryWhere())
->count();
}
/**
* @notes 额外参数
* @return mixed|null
* @author 段誉
* @date 2023/3/1 9:51
*/
public function extend()
{
$count = (new RefundRecord())->alias('r')
->join('user u', 'u.id = r.user_id')
->field([
'count(r.id) as total',
'count(if(r.refund_status='.RefundEnum::REFUND_ING.', true, null)) as ing',
'count(if(r.refund_status='.RefundEnum::REFUND_SUCCESS.', true, null)) as success',
'count(if(r.refund_status='.RefundEnum::REFUND_ERROR.', true, null)) as error',
])
->where($this->searchWhere)
->where($this->queryWhere(false))
->select()->toArray();
return array_shift($count);
}
}
@@ -0,0 +1,372 @@
<?php
declare(strict_types=1);
namespace app\adminapi\lists\firstvisit;
use app\adminapi\lists\BaseAdminDataLists;
use app\adminapi\logic\firstvisit\MyPatientLogic;
use app\common\lists\ListsExtendInterface;
use app\common\lists\ListsSearchInterface;
use app\common\model\DiagnosisViewRecord;
use app\common\model\auth\Admin;
use app\common\model\doctor\Appointment;
use app\common\model\tcm\Diagnosis;
use think\db\Query;
use think\facade\Db;
class MyPatientLists extends BaseAdminDataLists implements ListsSearchInterface, ListsExtendInterface
{
private const EFFECTIVE_APPOINTMENT_STATUSES = [1, 3, 4];
public function setSearch(): array
{
return [];
}
public function lists(): array
{
$query = $this->buildQuery(true, true);
$appointmentTable = (new Appointment())->getTable();
$today = date('Y-m-d');
$upcomingSql = "SELECT MIN(CONCAT(sort_apt.appointment_date, ' ', IFNULL(NULLIF(TRIM(sort_apt.appointment_time), ''), '00:00:00')))"
. " FROM {$appointmentTable} sort_apt"
. ' WHERE sort_apt.patient_id = d.id'
. ' AND sort_apt.status IN (1,4)'
. " AND sort_apt.appointment_date >= '{$today}'";
$rows = $query
->field([
'd.id', 'd.patient_id', 'd.patient_name', 'd.phone', 'd.id_card', 'd.gender', 'd.age',
'd.diagnosis_date', 'd.diagnosis_type', 'd.syndrome_type', 'd.assistant_id',
'd.assign_read_at', 'd.create_time',
])
->orderRaw("CASE WHEN ({$upcomingSql}) IS NULL THEN 1 ELSE 0 END ASC")
->orderRaw("IFNULL(({$upcomingSql}), '9999-12-31 23:59:59') ASC")
->order('d.id', 'desc')
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
return $this->appendRelations($rows);
}
public function count(): int
{
return (int) $this->buildQuery(true, true)->count('d.id');
}
public function extend(): array
{
$today = date('Y-m-d');
$tomorrow = date('Y-m-d', strtotime('+1 day'));
$dayAfter = date('Y-m-d', strtotime('+2 days'));
return [
'summary' => [
'today' => $this->countByAppointmentDate($today),
'tomorrow' => $this->countByAppointmentDate($tomorrow),
'day_after' => $this->countByAppointmentDate($dayAfter),
],
'scope' => MyPatientLogic::scopeMeta($this->adminId, $this->adminInfo),
'dates' => [
'today' => $today,
'tomorrow' => $tomorrow,
'day_after' => $dayAfter,
],
];
}
private function buildQuery(bool $applyStatusFilter, bool $applyDateFilter): Query
{
$diagnosisTable = (new Diagnosis())->getTable();
$query = Db::table($diagnosisTable)
->alias('d')
->whereNull('d.delete_time')
->where('d.status', 1);
MyPatientLogic::applyScope($query, $this->adminId, $this->adminInfo);
$this->applyKeyword($query);
$statusFilter = $applyStatusFilter ? trim((string) ($this->params['status_filter'] ?? '')) : '';
$appointmentTable = (new Appointment())->getTable();
if ($statusFilter === 'unbooked') {
$query->whereNotExists(
"SELECT 1 FROM {$appointmentTable} unbooked_apt"
. ' WHERE unbooked_apt.patient_id = d.id'
. ' AND unbooked_apt.status IN (' . implode(',', self::EFFECTIVE_APPOINTMENT_STATUSES) . ')'
);
}
$appointmentStatuses = self::EFFECTIVE_APPOINTMENT_STATUSES;
if (in_array($statusFilter, ['pending_interview', 'booked'], true)) {
$appointmentStatuses = [1];
} elseif ($statusFilter === 'completed') {
$appointmentStatuses = [3];
} elseif ($statusFilter === 'missed') {
$appointmentStatuses = [4];
}
$needsAppointmentFilter = in_array(
$statusFilter,
['pending_interview', 'booked', 'completed', 'missed'],
true
);
[$startDate, $endDate] = $applyDateFilter ? $this->dateRange() : ['', ''];
if ($startDate !== '' || $endDate !== '') {
$needsAppointmentFilter = true;
}
if ($needsAppointmentFilter) {
$conditions = [
'filter_apt.patient_id = d.id',
'filter_apt.status IN (' . implode(',', $appointmentStatuses) . ')',
];
if ($startDate !== '') {
$conditions[] = "filter_apt.appointment_date >= '{$startDate}'";
}
if ($endDate !== '') {
$conditions[] = "filter_apt.appointment_date <= '{$endDate}'";
}
$query->whereExists(
"SELECT 1 FROM {$appointmentTable} filter_apt WHERE " . implode(' AND ', $conditions)
);
}
return $query;
}
private function applyKeyword(Query $query): void
{
$keyword = trim((string) ($this->params['keyword'] ?? ''));
if ($keyword === '') {
return;
}
$needle = addslashes($keyword);
$adminTable = (new Admin())->getTable();
$appointmentTable = (new Appointment())->getTable();
$query->whereRaw(
"(d.patient_name LIKE '%{$needle}%'"
. " OR d.phone LIKE '%{$needle}%'"
. " OR EXISTS (SELECT 1 FROM {$adminTable} assistant_admin"
. ' WHERE assistant_admin.id = CAST(d.assistant_id AS UNSIGNED)'
. ' AND assistant_admin.delete_time IS NULL'
. " AND assistant_admin.name LIKE '%{$needle}%')"
. " OR EXISTS (SELECT 1 FROM {$appointmentTable} keyword_apt"
. " INNER JOIN {$adminTable} doctor_admin ON doctor_admin.id = keyword_apt.doctor_id"
. ' AND doctor_admin.delete_time IS NULL'
. ' WHERE keyword_apt.patient_id = d.id'
. ' AND keyword_apt.status IN (1,3,4)'
. " AND doctor_admin.name LIKE '%{$needle}%'))"
);
}
/** @return array{0:string,1:string} */
private function dateRange(): array
{
$startDate = $this->normalizeDate($this->params['start_date'] ?? '');
$endDate = $this->normalizeDate($this->params['end_date'] ?? '');
if ($startDate === '' && $endDate !== '') {
$startDate = $endDate;
}
if ($endDate === '' && $startDate !== '') {
$endDate = $startDate;
}
if ($startDate !== '' && $endDate !== '' && $startDate > $endDate) {
[$startDate, $endDate] = [$endDate, $startDate];
}
return [$startDate, $endDate];
}
private function normalizeDate($value): string
{
$value = trim((string) $value);
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) ? $value : '';
}
private function countByAppointmentDate(string $date): int
{
$query = $this->buildQuery(false, false);
$appointmentTable = (new Appointment())->getTable();
$query->whereExists(
"SELECT 1 FROM {$appointmentTable} summary_apt"
. ' WHERE summary_apt.patient_id = d.id'
. " AND summary_apt.appointment_date = '{$date}'"
. ' AND summary_apt.status IN (1,3,4)'
);
return (int) $query->count('d.id');
}
/**
* @param array<int, array<string, mixed>> $rows
* @return array<int, array<string, mixed>>
*/
private function appendRelations(array $rows): array
{
if ($rows === []) {
return [];
}
$diagnosisIds = array_values(array_unique(array_map('intval', array_column($rows, 'id'))));
$assistantIds = array_values(array_unique(array_filter(array_map('intval', array_column($rows, 'assistant_id')))));
$appointmentTable = (new Appointment())->getTable();
$appointments = Db::table($appointmentTable)
->whereIn('patient_id', $diagnosisIds)
->whereIn('status', self::EFFECTIVE_APPOINTMENT_STATUSES)
->field(['id', 'patient_id', 'doctor_id', 'appointment_date', 'appointment_time', 'status'])
->order('appointment_date', 'asc')
->order('appointment_time', 'asc')
->order('id', 'asc')
->select()
->toArray();
$doctorIds = array_values(array_unique(array_filter(array_map('intval', array_column($appointments, 'doctor_id')))));
$adminIds = array_values(array_unique(array_merge($assistantIds, $doctorIds)));
$adminNames = $adminIds === [] ? [] : Admin::whereIn('id', $adminIds)->whereNull('delete_time')->column('name', 'id');
$appointmentMap = [];
foreach ($appointments as $appointment) {
$diagnosisId = (int) ($appointment['patient_id'] ?? 0);
if ($diagnosisId > 0) {
$appointmentMap[$diagnosisId][] = $appointment;
}
}
$viewTable = (new DiagnosisViewRecord())->getTable();
$confirmedIds = Db::table($viewTable)
->whereIn('diagnosis_id', $diagnosisIds)
->where('is_confirmed', 1)
->whereNull('delete_time')
->column('diagnosis_id');
$confirmedSet = array_fill_keys(array_map('intval', $confirmedIds), true);
[$rangeStart, $rangeEnd] = $this->dateRange();
$today = date('Y-m-d');
$statusFilter = trim((string) ($this->params['status_filter'] ?? ''));
$preferredStatuses = [
'pending_interview' => [1],
'booked' => [1],
'completed' => [3],
'missed' => [4],
][$statusFilter] ?? [];
foreach ($rows as &$row) {
$diagnosisId = (int) $row['id'];
$rowAppointments = $appointmentMap[$diagnosisId] ?? [];
$primary = $this->pickPrimaryAppointment(
$rowAppointments,
$rangeStart,
$rangeEnd,
$today,
$preferredStatuses
);
$completedCount = count(array_filter($rowAppointments, static function (array $appointment): bool {
return (int) ($appointment['status'] ?? 0) === 3;
}));
$assistantId = (int) ($row['assistant_id'] ?? 0);
$row['diagnosis_id'] = $diagnosisId;
$row['source_patient_id'] = (int) ($row['patient_id'] ?? 0);
$row['phone_masked'] = $this->maskPhone((string) ($row['phone'] ?? ''));
unset($row['phone']);
$row['has_id_card'] = trim((string) ($row['id_card'] ?? '')) !== '' ? 1 : 0;
unset($row['id_card']);
$row['gender_desc'] = (int) ($row['gender'] ?? 0) === 1 ? '男' : '女';
$row['diagnosis_date_text'] = $this->formatDiagnosisDate($row['diagnosis_date'] ?? '');
$row['assistant_name'] = (string) ($adminNames[$assistantId] ?? '未分配');
$row['confirmed'] = isset($confirmedSet[$diagnosisId]) ? 1 : 0;
$row['confirmation_text'] = $row['confirmed'] ? '已确认' : '待确认';
$row['visit_count'] = $completedCount;
$row['revisit_count'] = max(0, $completedCount - 1);
$row['appointment_id'] = $primary ? (int) $primary['id'] : 0;
$row['appointment_status'] = $primary ? (int) $primary['status'] : 0;
$row['appointment_status_text'] = $this->appointmentStatusText((int) ($primary['status'] ?? 0));
$row['appointment_doctor_id'] = $primary ? (int) $primary['doctor_id'] : 0;
$row['appointment_doctor_name'] = $primary
? (string) ($adminNames[(int) $primary['doctor_id']] ?? '未知医生')
: '未预约';
$row['appointment_time_text'] = $primary ? $this->appointmentTimeText($primary) : '';
$row['has_appointment'] = $primary !== null ? 1 : 0;
}
unset($row);
return $rows;
}
/**
* @param array<int, array<string, mixed>> $appointments
* @return array<string, mixed>|null
*/
private function pickPrimaryAppointment(
array $appointments,
string $rangeStart,
string $rangeEnd,
string $today,
array $preferredStatuses = []
): ?array
{
if ($appointments === []) {
return null;
}
$candidates = $appointments;
if ($preferredStatuses !== []) {
$candidates = array_values(array_filter($candidates, static function (array $appointment) use ($preferredStatuses): bool {
return in_array((int) ($appointment['status'] ?? 0), $preferredStatuses, true);
}));
}
if ($rangeStart !== '' || $rangeEnd !== '') {
$candidates = array_values(array_filter($candidates, static function (array $appointment) use ($rangeStart, $rangeEnd): bool {
$date = (string) ($appointment['appointment_date'] ?? '');
return ($rangeStart === '' || $date >= $rangeStart) && ($rangeEnd === '' || $date <= $rangeEnd);
}));
}
if ($candidates === []) {
$candidates = $appointments;
}
foreach ($candidates as $appointment) {
$status = (int) ($appointment['status'] ?? 0);
$date = (string) ($appointment['appointment_date'] ?? '');
if (in_array($status, [1, 4], true) && $date >= $today) {
return $appointment;
}
}
return $candidates[count($candidates) - 1] ?? null;
}
private function maskPhone(string $phone): string
{
return preg_replace('/^(\d{3})\d{4}(\d{4})$/', '$1****$2', $phone) ?: $phone;
}
private function formatDiagnosisDate($value): string
{
if (is_numeric($value)) {
return (int) $value > 0 ? date('Y-m-d', (int) $value) : '';
}
$text = trim((string) $value);
return $text === '' ? '' : substr($text, 0, 10);
}
private function appointmentTimeText(array $appointment): string
{
$time = trim((string) ($appointment['appointment_time'] ?? ''));
if (strlen($time) > 5) {
$time = substr($time, 0, 5);
}
return trim((string) ($appointment['appointment_date'] ?? '') . ' ' . $time);
}
private function appointmentStatusText(int $status): string
{
return [1 => '待面诊', 3 => '已完成', 4 => '已过号'][$status] ?? '未预约';
}
}
@@ -0,0 +1,362 @@
<?php
declare(strict_types=1);
namespace app\adminapi\lists\firstvisit;
use app\adminapi\lists\BaseAdminDataLists;
use app\adminapi\logic\firstvisit\MyPatientLogic;
use app\adminapi\logic\stats\YejiStatsLogic;
use app\adminapi\logic\tcm\PrescriptionOrderLogic;
use app\common\lists\ListsExtendInterface;
use app\common\lists\ListsSearchInterface;
use app\common\model\Order;
use app\common\model\auth\Admin;
use app\common\model\tcm\Prescription;
use app\common\model\tcm\PrescriptionOrder;
use app\common\model\tcm\PrescriptionOrderPayOrder;
use think\db\Query;
use think\facade\Db;
/**
* “我的患者”内嵌订单列表。
*
* 订单可见性始终锚定 diagnosis 别名 d,并复用 MyPatientLogic;订单创建人仅用于展示,
* 不能作为患者归属或数据范围条件。
*/
class MyPatientOrderLists extends BaseAdminDataLists implements ListsSearchInterface, ListsExtendInterface
{
public function setSearch(): array
{
return [];
}
public function lists(): array
{
$rows = $this->buildQuery()
->field([
'po.id', 'po.order_no', 'po.prescription_id', 'po.diagnosis_id', 'po.creator_id',
'po.recipient_name', 'po.recipient_phone', 'po.fee_type', 'po.amount',
'po.prescription_audit_status', 'po.payment_slip_audit_status',
'po.fulfillment_status', 'po.express_company', 'po.tracking_number', 'po.ship_mode',
'po.gancao_reciperl_order_no', 'po.ej_pharmacy_order_no',
'po.gancao_submit_time', 'po.ej_pharmacy_submit_time',
'po.ej_pharmacy_status', 'po.ej_pharmacy_review_status', 'po.refund_amount',
'po.create_time',
'd.patient_name', 'd.phone AS patient_phone', 'd.assistant_id',
])
->order('po.id', 'desc')
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
return $this->appendRelations($rows);
}
public function count(): int
{
return (int) $this->buildQuery()->count('po.id');
}
public function extend(): array
{
$query = $this->buildQuery();
$pendingQuery = clone $query;
$effectiveAmountQuery = clone $query;
YejiStatsLogic::applyPrescriptionOrderEffectiveAmountQuery($effectiveAmountQuery, 'po');
// 拒收指标保留关键词、审核和日期条件,但不受当前履约状态按钮影响,
// 避免点击“拒收订单”后分母被收窄为拒收状态而固定显示 100%。
$rejectionScopeQuery = $this->buildQuery(true);
$rejectionScopeOrderCount = (int) (clone $rejectionScopeQuery)->count('po.id');
$rejectedCount = (int) (clone $rejectionScopeQuery)
->where('po.fulfillment_status', 9)
->count('po.id');
$orderCount = (int) (clone $query)->count('po.id');
return [
'summary' => [
'orders' => $orderCount,
'amount' => round((float) $effectiveAmountQuery->sum('po.amount'), 2),
'pending' => (int) $pendingQuery
->where(function ($q) {
$q->where('po.prescription_audit_status', 0)
->whereOr('po.payment_slip_audit_status', 0);
})
->count('po.id'),
'completed' => (int) (clone $query)->whereIn('po.fulfillment_status', [3, 6])->count('po.id'),
'rejected' => $rejectedCount,
'rejection_rate' => $rejectionScopeOrderCount > 0
? round($rejectedCount / $rejectionScopeOrderCount * 100, 2)
: 0.0,
],
'scope' => MyPatientLogic::scopeMeta($this->adminId, $this->adminInfo),
];
}
private function buildQuery(bool $ignoreFulfillmentStatus = false): Query
{
$query = PrescriptionOrder::alias('po')
->join('tcm_diagnosis d', 'po.diagnosis_id = d.id')
->whereNull('po.delete_time')
->whereNull('d.delete_time')
->where('d.status', 1);
MyPatientLogic::applyScope($query, $this->adminId, $this->adminInfo);
$this->applyKeyword($query);
$this->applyStatusFilters($query, $ignoreFulfillmentStatus);
$this->applyDateFilter($query);
return $query;
}
private function applyKeyword(Query $query): void
{
$keyword = trim((string) ($this->params['keyword'] ?? ''));
if ($keyword === '') {
return;
}
$query->where(function ($q) use ($keyword) {
$like = '%' . $keyword . '%';
$q->whereLike('po.order_no', $like)
->whereOr('d.patient_name', 'like', $like)
->whereOr('d.phone', 'like', $like)
->whereOr('po.recipient_name', 'like', $like)
->whereOr('po.recipient_phone', 'like', $like);
if (preg_match('/^\d+$/', $keyword)) {
$id = (int) $keyword;
if ($id > 0) {
$q->whereOr('po.id', $id)
->whereOr('po.prescription_id', $id)
->whereOr('po.diagnosis_id', $id);
}
}
});
}
private function applyStatusFilters(Query $query, bool $ignoreFulfillmentStatus = false): void
{
foreach (['prescription_audit_status', 'payment_slip_audit_status', 'fulfillment_status'] as $field) {
if ($ignoreFulfillmentStatus && $field === 'fulfillment_status') {
continue;
}
$raw = $this->params[$field] ?? '';
if ($raw === '' || $raw === null) {
continue;
}
$query->where('po.' . $field, (int) $raw);
}
}
private function applyDateFilter(Query $query): void
{
[$startDate, $endDate] = $this->dateRange();
if ($startDate !== '') {
$query->where('po.create_time', '>=', strtotime($startDate . ' 00:00:00'));
}
if ($endDate !== '') {
$query->where('po.create_time', '<=', strtotime($endDate . ' 23:59:59'));
}
}
/** @return array{0:string,1:string} */
private function dateRange(): array
{
$startDate = $this->normalizeDate($this->params['start_date'] ?? '');
$endDate = $this->normalizeDate($this->params['end_date'] ?? '');
if ($startDate === '' && $endDate !== '') {
$startDate = $endDate;
}
if ($endDate === '' && $startDate !== '') {
$endDate = $startDate;
}
if ($startDate !== '' && $endDate !== '' && $startDate > $endDate) {
[$startDate, $endDate] = [$endDate, $startDate];
}
return [$startDate, $endDate];
}
private function normalizeDate($value): string
{
$value = trim((string) $value);
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) ? $value : '';
}
/**
* @param array<int, array<string, mixed>> $rows
* @return array<int, array<string, mixed>>
*/
private function appendRelations(array $rows): array
{
if ($rows === []) {
return [];
}
$orderIds = array_values(array_unique(array_filter(array_map('intval', array_column($rows, 'id')))));
$prescriptionIds = array_values(array_unique(array_filter(array_map('intval', array_column($rows, 'prescription_id')))));
$creatorIds = array_values(array_unique(array_filter(array_map('intval', array_column($rows, 'creator_id')))));
$assistantIds = array_values(array_unique(array_filter(array_map('intval', array_column($rows, 'assistant_id')))));
$prescriptionMap = [];
$doctorIds = [];
if ($prescriptionIds !== []) {
$prescriptions = Prescription::whereIn('id', $prescriptionIds)
->whereNull('delete_time')
->field(['id', 'creator_id', 'doctor_name'])
->select()
->toArray();
foreach ($prescriptions as $prescription) {
$prescriptionId = (int) ($prescription['id'] ?? 0);
$doctorId = (int) ($prescription['creator_id'] ?? 0);
if ($prescriptionId > 0) {
$prescriptionMap[$prescriptionId] = $prescription;
}
if ($doctorId > 0) {
$doctorIds[] = $doctorId;
}
}
}
$adminIds = array_values(array_unique(array_merge($creatorIds, $assistantIds, $doctorIds)));
$adminNames = $adminIds === []
? []
: Admin::whereIn('id', $adminIds)->whereNull('delete_time')->column('name', 'id');
$linkCounts = [];
$paidTotals = [];
if ($orderIds !== []) {
$linkRows = PrescriptionOrderPayOrder::whereIn('prescription_order_id', $orderIds)
->field(['prescription_order_id', 'pay_order_id'])
->select()
->toArray();
$payOrderIds = array_values(array_unique(array_filter(array_map('intval', array_column($linkRows, 'pay_order_id')))));
$payOrders = $payOrderIds === []
? []
: Order::whereIn('id', $payOrderIds)
->whereNull('delete_time')
->field(['id', 'amount', 'status'])
->select()
->toArray();
$payOrderMap = [];
foreach ($payOrders as $payOrder) {
$payOrderMap[(int) ($payOrder['id'] ?? 0)] = $payOrder;
}
foreach ($linkRows as $linkRow) {
$orderId = (int) ($linkRow['prescription_order_id'] ?? 0);
if ($orderId > 0) {
$linkCounts[$orderId] = ($linkCounts[$orderId] ?? 0) + 1;
}
$payOrder = $payOrderMap[(int) ($linkRow['pay_order_id'] ?? 0)] ?? [];
if ($orderId > 0 && in_array((int) ($payOrder['status'] ?? 0), [2, 5], true)) {
$paidTotals[$orderId] = round(
(float) ($paidTotals[$orderId] ?? 0) + (float) ($payOrder['amount'] ?? 0),
2
);
}
}
}
$assistantByDiagnosis = [];
foreach ($rows as $row) {
$diagnosisId = (int) ($row['diagnosis_id'] ?? 0);
if ($diagnosisId > 0) {
$assistantByDiagnosis[$diagnosisId] = (int) ($row['assistant_id'] ?? 0);
}
}
$claimByOrder = [];
if ($orderIds !== []) {
$claimRows = Db::name('pharmacy_submission_claim')
->whereIn('prescription_order_id', $orderIds)
->field(['prescription_order_id', 'target', 'status', 'lease_expires_at'])
->order('source_revision', 'desc')
->select()
->toArray();
foreach ($claimRows as $claimRow) {
$orderId = (int) ($claimRow['prescription_order_id'] ?? 0);
if ($orderId > 0 && !isset($claimByOrder[$orderId])) {
$claimByOrder[$orderId] = $claimRow;
}
}
}
foreach ($rows as &$row) {
$prescription = $prescriptionMap[(int) ($row['prescription_id'] ?? 0)] ?? [];
$doctorId = (int) ($prescription['creator_id'] ?? 0);
$doctorName = trim((string) ($prescription['doctor_name'] ?? ''));
$creatorId = (int) ($row['creator_id'] ?? 0);
$assistantId = (int) ($row['assistant_id'] ?? 0);
$row['patient_phone_masked'] = $this->maskPhone((string) ($row['patient_phone'] ?? ''));
$row['recipient_phone_masked'] = $this->maskPhone((string) ($row['recipient_phone'] ?? ''));
unset($row['patient_phone'], $row['recipient_phone']);
$row['creator_name'] = (string) ($adminNames[$creatorId] ?? '—');
$row['assistant_name'] = (string) ($adminNames[$assistantId] ?? '未分配');
$row['doctor_name'] = $doctorName !== '' ? $doctorName : (string) ($adminNames[$doctorId] ?? '—');
$row['linked_pay_order_count'] = (int) ($linkCounts[(int) $row['id']] ?? 0);
$row['linked_pay_paid_total'] = (float) ($paidTotals[(int) $row['id']] ?? 0);
$claim = $claimByOrder[(int) $row['id']] ?? [];
$row['pharmacy_claim_target'] = (string) ($claim['target'] ?? '');
$row['pharmacy_claim_status'] = (string) ($claim['status'] ?? '');
$row['pharmacy_claim_lease_expires_at'] = (int) ($claim['lease_expires_at'] ?? 0);
$row['can_upload_pharmacy'] = PrescriptionOrderLogic::canUploadToPharmacy(
$row,
$this->adminId,
$this->adminInfo,
$assistantByDiagnosis
);
$row['create_time_text'] = $this->formatTimestamp($row['create_time'] ?? 0);
$row['fee_type_text'] = $this->feeTypeText((int) ($row['fee_type'] ?? 0));
$row['prescription_audit_text'] = $this->auditStatusText((int) ($row['prescription_audit_status'] ?? 0));
$row['payment_slip_audit_text'] = $this->auditStatusText((int) ($row['payment_slip_audit_status'] ?? 0));
$row['fulfillment_text'] = $this->fulfillmentStatusText((int) ($row['fulfillment_status'] ?? 0));
$fulfillmentStatus = (int) ($row['fulfillment_status'] ?? 0);
$refundAmount = round((float) ($row['refund_amount'] ?? 0), 2);
$amountIncluded = !in_array(
$fulfillmentStatus,
YejiStatsLogic::PRESCRIPTION_ORDER_FULFILLMENT_EXCLUDED_FROM_PERFORMANCE,
true
) && $refundAmount <= 0;
$row['amount_included'] = $amountIncluded;
$row['effective_amount'] = $amountIncluded ? round((float) ($row['amount'] ?? 0), 2) : 0.0;
$row['amount_exclusion_text'] = $amountIncluded
? ''
: ($refundAmount > 0 || $fulfillmentStatus === 10 ? '退款不计入' : $row['fulfillment_text'] . '不计入');
}
unset($row);
return $rows;
}
private function maskPhone(string $phone): string
{
return preg_replace('/^(\d{3})\d{4}(\d{4})$/', '$1****$2', $phone) ?: $phone;
}
private function formatTimestamp($value): string
{
return is_numeric($value) && (int) $value > 0 ? date('Y-m-d H:i', (int) $value) : '';
}
private function auditStatusText(int $status): string
{
return [0 => '待审核', 1 => '已通过', 2 => '已驳回'][$status] ?? '未知';
}
private function feeTypeText(int $type): string
{
return [1 => '挂号', 2 => '问诊', 3 => '药品', 4 => '首付', 5 => '尾款', 6 => '其他', 7 => '全部'][$type] ?? '其他';
}
private function fulfillmentStatusText(int $status): string
{
return [
1 => '待双审通过', 2 => '待发货', 3 => '已完成', 4 => '已取消',
5 => '已发货', 6 => '已签收', 7 => '进行中', 8 => '暂不制药',
9 => '拒收', 10 => '退款', 11 => '保留药方', 12 => '制药缓发',
][$status] ?? '未知';
}
}
@@ -0,0 +1,702 @@
<?php
declare(strict_types=1);
namespace app\adminapi\lists\firstvisit;
use app\adminapi\lists\BaseAdminDataLists;
use app\adminapi\logic\firstvisit\MyPatientLogic;
use app\common\lists\ListsExtendInterface;
use app\common\lists\ListsSearchInterface;
use app\common\model\DiagnosisViewRecord;
use app\common\model\auth\Admin;
use app\common\model\auth\AdminRole;
use app\common\model\doctor\Appointment;
use app\common\model\doctor\Roster;
use app\common\model\tcm\Prescription;
use app\common\service\doctor\RosterSegmentService;
use think\db\Query;
use think\facade\Db;
/**
* “我的患者”内嵌面诊进度。
*
* 一条挂号一行;只返回脱敏患者信息,并严格复用 MyPatientLogic 的患者级范围。
*/
class MyPatientProgressLists extends BaseAdminDataLists implements ListsSearchInterface, ListsExtendInterface
{
private const EFFECTIVE_STATUSES = [1, 3, 4];
private const AVG_MINUTES_PER_VISIT = 15;
public function setSearch(): array
{
return [];
}
public function lists(): array
{
$rows = $this->buildQuery(true)
->field([
'a.id', 'a.patient_id AS diagnosis_id', 'a.doctor_id', 'a.appointment_date',
'a.appointment_time', 'a.appointment_type', 'a.status', 'a.create_time',
'd.patient_id AS source_patient_id', 'd.patient_name', 'd.phone', 'd.gender', 'd.age',
'd.assistant_id', 'doctor_admin.name AS doctor_name', 'assistant_admin.name AS assistant_name',
])
->order('a.appointment_date', 'asc')
->order('a.appointment_time', 'asc')
->order('a.id', 'asc')
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
return $this->appendProgress($rows);
}
public function count(): int
{
return (int) $this->buildQuery(true)->count('a.id');
}
public function extend(): array
{
$query = $this->buildQuery(false);
[$startDate, $endDate] = $this->dateRange();
$summary = [
'total' => (int) (clone $query)->count('a.id'),
'booked' => (int) (clone $query)->where('a.status', 1)->count('a.id'),
'completed' => (int) (clone $query)->where('a.status', 3)->count('a.id'),
'missed' => (int) (clone $query)->where('a.status', 4)->count('a.id'),
];
$scheduleMode = $this->usesOwnershipSchedule() ? 'ownership' : 'roster';
$weekSchedule = $scheduleMode === 'ownership' ? $this->ownershipWeekSchedule() : $this->weekSchedule();
$todaySchedule = $weekSchedule[0] ?? $this->emptyScheduleDay(date('Y-m-d'));
$todayOverview = $scheduleMode === 'ownership'
? [
'total_visits' => (int) ($todaySchedule['total_appointments'] ?? 0),
'booked' => (int) ($todaySchedule['waiting_appointments'] ?? 0),
'completed' => (int) ($todaySchedule['completed_appointments'] ?? 0),
'missed' => (int) ($todaySchedule['missed_appointments'] ?? 0),
'empty_slots' => 0,
'passed_slots' => 0,
'remaining_slots' => 0,
'doctor_count' => (int) ($todaySchedule['doctor_count'] ?? 0),
]
: [
'total_visits' => (int) ($todaySchedule['total_slots'] ?? 0),
'booked' => (int) ($todaySchedule['booked_slots'] ?? 0),
'completed' => 0,
'missed' => 0,
// 空号口径:剩余可预约号源(未过时刻且未被有效挂号占用),过号单独给出
'empty_slots' => (int) ($todaySchedule['remaining_slots'] ?? 0),
'passed_slots' => (int) ($todaySchedule['passed_slots'] ?? 0),
'remaining_slots' => (int) ($todaySchedule['remaining_slots'] ?? 0),
'doctor_count' => (int) ($todaySchedule['doctor_count'] ?? 0),
];
return [
'summary' => $summary,
'schedule_mode' => $scheduleMode,
'today_overview' => $todayOverview,
'week_schedule' => $weekSchedule,
'scope' => MyPatientLogic::scopeMeta($this->adminId, $this->adminInfo),
'dates' => ['start' => $startDate, 'end' => $endDate],
];
}
private function buildQuery(bool $applyStatus): Query
{
$query = Appointment::alias('a')
->join('tcm_diagnosis d', 'a.patient_id = d.id')
->leftJoin('admin doctor_admin', 'a.doctor_id = doctor_admin.id')
->leftJoin('admin assistant_admin', 'CAST(d.assistant_id AS UNSIGNED) = assistant_admin.id')
->whereNull('d.delete_time')
->where('d.status', 1);
MyPatientLogic::applyScope($query, $this->adminId, $this->adminInfo);
$this->applyKeyword($query);
$this->applyDateFilter($query);
if ($applyStatus) {
$status = $this->params['status'] ?? '';
if ($status !== '' && $status !== null && in_array((int) $status, self::EFFECTIVE_STATUSES, true)) {
$query->where('a.status', (int) $status);
} else {
$query->whereIn('a.status', self::EFFECTIVE_STATUSES);
}
} else {
$query->whereIn('a.status', self::EFFECTIVE_STATUSES);
}
return $query;
}
private function applyKeyword(Query $query): void
{
$keyword = trim((string) ($this->params['keyword'] ?? ''));
if ($keyword === '') {
return;
}
$query->where(function ($q) use ($keyword) {
$like = '%' . $keyword . '%';
$q->whereLike('d.patient_name', $like)
->whereOr('d.phone', 'like', $like)
->whereOr('doctor_admin.name', 'like', $like)
->whereOr('assistant_admin.name', 'like', $like);
if (preg_match('/^\d+$/', $keyword)) {
$id = (int) $keyword;
if ($id > 0) {
$q->whereOr('a.id', $id)->whereOr('d.id', $id);
}
}
});
}
private function applyDateFilter(Query $query): void
{
[$startDate, $endDate] = $this->dateRange();
$query->whereBetween('a.appointment_date', [$startDate, $endDate]);
}
/** @return array{0:string,1:string} */
private function dateRange(): array
{
$today = date('Y-m-d');
$startDate = $this->normalizeDate($this->params['start_date'] ?? '') ?: $today;
$endDate = $this->normalizeDate($this->params['end_date'] ?? '') ?: $startDate;
if ($startDate > $endDate) {
[$startDate, $endDate] = [$endDate, $startDate];
}
$startTs = strtotime($startDate);
$endTs = strtotime($endDate);
if ($startTs !== false && $endTs !== false && $endTs - $startTs > 31 * 86400) {
$endDate = date('Y-m-d', $startTs + 31 * 86400);
}
return [$startDate, $endDate];
}
private function normalizeDate($value): string
{
$value = trim((string) $value);
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) ? $value : '';
}
/**
* @param array<int, array<string, mixed>> $rows
* @return array<int, array<string, mixed>>
*/
private function appendProgress(array $rows): array
{
if ($rows === []) {
return [];
}
$diagnosisIds = array_values(array_unique(array_filter(array_map('intval', array_column($rows, 'diagnosis_id')))));
$appointmentIds = array_values(array_unique(array_filter(array_map('intval', array_column($rows, 'id')))));
$queuePositionMap = $this->queuePositionMap($rows);
$confirmedSet = [];
if ($diagnosisIds !== []) {
$viewTable = (new DiagnosisViewRecord())->getTable();
$confirmedIds = Db::table($viewTable)
->whereIn('diagnosis_id', $diagnosisIds)
->where('is_confirmed', 1)
->whereNull('delete_time')
->column('diagnosis_id');
$confirmedSet = array_fill_keys(array_map('intval', $confirmedIds), true);
}
$prescriptionMap = [];
if ($appointmentIds !== []) {
$prescriptions = Prescription::whereIn('appointment_id', $appointmentIds)
->whereNull('delete_time')
->where('void_status', 0)
->field(['id', 'appointment_id', 'audit_status', 'is_system_auto'])
->order('id', 'desc')
->select()
->toArray();
foreach ($prescriptions as $prescription) {
$appointmentId = (int) ($prescription['appointment_id'] ?? 0);
if ($appointmentId > 0 && !isset($prescriptionMap[$appointmentId])) {
$prescriptionMap[$appointmentId] = $prescription;
}
}
}
foreach ($rows as &$row) {
$appointmentId = (int) ($row['id'] ?? 0);
$diagnosisId = (int) ($row['diagnosis_id'] ?? 0);
$status = (int) ($row['status'] ?? 0);
$prescription = $prescriptionMap[$appointmentId] ?? [];
$confirmed = isset($confirmedSet[$diagnosisId]);
$prescribed = $prescription !== [];
$aheadCount = $status === 1 ? (int) ($queuePositionMap[$appointmentId] ?? 0) : 0;
$row['phone_masked'] = $this->maskPhone((string) ($row['phone'] ?? ''));
unset($row['phone']);
$row['gender_desc'] = (int) ($row['gender'] ?? 0) === 1 ? '男' : '女';
$row['assistant_name'] = trim((string) ($row['assistant_name'] ?? '')) ?: '未分配';
$row['doctor_name'] = trim((string) ($row['doctor_name'] ?? '')) ?: '未知医生';
$row['appointment_time_text'] = $this->appointmentTimeText($row);
$row['status_text'] = $this->appointmentStatusText($status);
$row['appointment_type_text'] = $this->appointmentTypeText((string) ($row['appointment_type'] ?? ''));
$row['registered'] = 1;
$row['diagnosis_confirmed'] = $confirmed ? 1 : 0;
$row['visit_completed'] = $status === 3 ? 1 : 0;
$row['has_prescription'] = $prescribed ? 1 : 0;
$row['prescription_id'] = (int) ($prescription['id'] ?? 0);
$row['prescription_audit_status'] = $prescribed ? (int) ($prescription['audit_status'] ?? 0) : -1;
$row['progress_text'] = $this->progressText($confirmed, $status === 3, $prescribed, $status);
$row['queue_no'] = $status === 1 ? $aheadCount + 1 : 0;
$row['ahead_count'] = $aheadCount;
$row['estimated_wait_minutes'] = $aheadCount * self::AVG_MINUTES_PER_VISIT;
$row['queue_status'] = $this->queueStatus($status, $confirmed, $aheadCount);
$row['queue_status_text'] = $this->queueStatusText((string) $row['queue_status']);
$row['is_self_patient'] = (
(int) ($row['assistant_id'] ?? 0) === $this->adminId
|| (int) ($row['doctor_id'] ?? 0) === $this->adminId
) ? 1 : 0;
}
unset($row);
return $rows;
}
/**
* 候诊位次按 progress.vue 的真实规则计算:同医生、同日、待就诊,按预约时刻和挂号 ID 升序。
* 队列计算读取完整医生队列,只向当前范围列表返回人数,不暴露范围外患者身份。
*
* @param array<int,array<string,mixed>> $rows
* @return array<int,int>
*/
private function queuePositionMap(array $rows): array
{
$doctorIds = array_values(array_unique(array_filter(array_map('intval', array_column($rows, 'doctor_id')))));
$dates = array_values(array_unique(array_filter(array_map('strval', array_column($rows, 'appointment_date')))));
if ($doctorIds === [] || $dates === []) {
return [];
}
$queueRows = Appointment::whereIn('doctor_id', $doctorIds)
->whereIn('appointment_date', $dates)
->where('status', 1)
->field(['id', 'doctor_id', 'appointment_date', 'appointment_time'])
->order('doctor_id', 'asc')
->order('appointment_date', 'asc')
->order('appointment_time', 'asc')
->order('id', 'asc')
->select()
->toArray();
$groupCounts = [];
$positions = [];
foreach ($queueRows as $queueRow) {
$group = (int) ($queueRow['doctor_id'] ?? 0) . '|' . (string) ($queueRow['appointment_date'] ?? '');
$positions[(int) ($queueRow['id'] ?? 0)] = (int) ($groupCounts[$group] ?? 0);
$groupCounts[$group] = (int) ($groupCounts[$group] ?? 0) + 1;
}
return $positions;
}
/**
* 未来七天号源:完全复用 paiban/availableSlots 的生成口径,按医生+日期+时刻去重。
*
* @return array<int,array<string,mixed>>
*/
private function weekSchedule(): array
{
$startDate = date('Y-m-d');
$endDate = date('Y-m-d', strtotime($startDate . ' +6 days'));
$days = [];
for ($offset = 0; $offset < 7; $offset++) {
$date = date('Y-m-d', strtotime($startDate . " +{$offset} days"));
$days[$date] = $this->emptyScheduleDay($date);
}
$doctorIds = $this->visibleDoctorIds($startDate, $endDate);
if ($doctorIds === []) {
return array_values($days);
}
$rosters = Roster::whereIn('doctor_id', $doctorIds)
->whereBetween('date', [$startDate, $endDate])
->where('status', 1)
->whereNull('delete_time')
->field(['doctor_id', 'date', 'period', 'start_time', 'end_time', 'slot_minutes', 'quota'])
->select()
->toArray();
$doctorNames = Admin::whereIn('id', $doctorIds)->column('name', 'id');
$doctorSlotSets = [];
$doctorWindowSets = [];
foreach ($rosters as $roster) {
$date = (string) ($roster['date'] ?? '');
$doctorId = (int) ($roster['doctor_id'] ?? 0);
$window = RosterSegmentService::resolveWindow($roster);
if (!isset($days[$date]) || $doctorId <= 0 || $window === null) {
continue;
}
[$startTime, $endTime] = $window;
$times = RosterSegmentService::generateSlotTimes(
$startTime,
$endTime,
RosterSegmentService::normalizeSlotMinutes($roster['slot_minutes'] ?? 15)
);
$times = RosterSegmentService::applyQuotaCap($times, (int) ($roster['quota'] ?? 0));
foreach ($times as $time) {
$doctorSlotSets[$date][$doctorId][$time] = true;
}
$doctorWindowSets[$date][$doctorId][$startTime . '-' . $endTime] = true;
}
$appointments = Appointment::whereIn('doctor_id', $doctorIds)
->whereBetween('appointment_date', [$startDate, $endDate])
->where('status', 1)
->field(['doctor_id', 'appointment_date', 'appointment_time'])
->select()
->toArray();
$doctorBookedSets = [];
foreach ($appointments as $appointment) {
$date = (string) ($appointment['appointment_date'] ?? '');
$doctorId = (int) ($appointment['doctor_id'] ?? 0);
$time = substr((string) ($appointment['appointment_time'] ?? ''), 0, 5);
if (isset($doctorSlotSets[$date][$doctorId][$time])) {
$doctorBookedSets[$date][$doctorId][$time] = true;
}
}
$today = date('Y-m-d');
$nowHm = date('H:i');
foreach ($days as $date => &$day) {
$doctorDetails = [];
$total = 0;
$booked = 0;
$passed = 0;
$remaining = 0;
foreach ($doctorSlotSets[$date] ?? [] as $doctorId => $slotSet) {
$doctorTotal = count($slotSet);
$bookedSet = $doctorBookedSets[$date][$doctorId] ?? [];
$doctorBooked = count($bookedSet);
$doctorPassed = 0;
$doctorRemaining = 0;
foreach ($slotSet as $time => $_) {
if (isset($bookedSet[$time])) {
continue;
}
// 与挂号选号一致:今日已过(含当前分钟)的未约号源计为过号,其余为剩余可约
$isPassed = $date < $today || ($date === $today && strcmp((string) $time, $nowHm) <= 0);
if ($isPassed) {
$doctorPassed++;
} else {
$doctorRemaining++;
}
}
$total += $doctorTotal;
$booked += $doctorBooked;
$passed += $doctorPassed;
$remaining += $doctorRemaining;
$scheduleWindows = array_values(array_keys($doctorWindowSets[$date][$doctorId] ?? []));
sort($scheduleWindows, SORT_STRING);
$doctorDetails[] = [
'doctor_id' => (int) $doctorId,
'doctor_name' => trim((string) ($doctorNames[$doctorId] ?? '')) ?: '未知医生',
'schedule_windows' => $scheduleWindows,
'total_slots' => $doctorTotal,
'booked_slots' => $doctorBooked,
'passed_slots' => $doctorPassed,
'remaining_slots' => $doctorRemaining,
// 空号对外展示剩余可约;过号/未挂号细分见 passed/remaining
'empty_slots' => $doctorRemaining,
];
}
usort($doctorDetails, static function (array $left, array $right): int {
return $right['remaining_slots'] <=> $left['remaining_slots']
?: $right['booked_slots'] <=> $left['booked_slots']
?: $right['total_slots'] <=> $left['total_slots']
?: strcmp((string) $left['doctor_name'], (string) $right['doctor_name']);
});
$day['total_slots'] = $total;
$day['booked_slots'] = $booked;
$day['passed_slots'] = $passed;
$day['remaining_slots'] = $remaining;
$day['empty_slots'] = $remaining;
$day['doctor_count'] = count($doctorDetails);
$day['doctors'] = $doctorDetails;
}
unset($day);
return array_values($days);
}
/**
* 医助“本人归属”只统计其患者的真实挂号,不再把历史接诊医生的整周号源算到本人名下。
*
* @return array<int,array<string,mixed>>
*/
private function ownershipWeekSchedule(): array
{
$startDate = date('Y-m-d');
$endDate = date('Y-m-d', strtotime($startDate . ' +6 days'));
$days = [];
for ($offset = 0; $offset < 7; $offset++) {
$date = date('Y-m-d', strtotime($startDate . " +{$offset} days"));
$days[$date] = $this->emptyScheduleDay($date);
}
$query = Appointment::alias('ownership_a')
->join('tcm_diagnosis d', 'ownership_a.patient_id = d.id')
->leftJoin('admin ownership_doctor', 'ownership_a.doctor_id = ownership_doctor.id')
->whereNull('d.delete_time')
->where('d.status', 1)
->whereBetween('ownership_a.appointment_date', [$startDate, $endDate])
->whereIn('ownership_a.status', self::EFFECTIVE_STATUSES);
MyPatientLogic::applyScope($query, $this->adminId, $this->adminInfo);
$appointments = $query
->field([
'ownership_a.id', 'ownership_a.doctor_id', 'ownership_a.appointment_date',
'ownership_a.appointment_time', 'ownership_a.status',
'ownership_doctor.name AS doctor_name',
])
->order('ownership_a.appointment_date', 'asc')
->order('ownership_a.appointment_time', 'asc')
->order('ownership_a.id', 'asc')
->select()
->toArray();
$doctorDetails = [];
foreach ($appointments as $appointment) {
$date = (string) ($appointment['appointment_date'] ?? '');
$doctorId = (int) ($appointment['doctor_id'] ?? 0);
if (!isset($days[$date]) || $doctorId <= 0) {
continue;
}
if (!isset($doctorDetails[$date][$doctorId])) {
$doctorDetails[$date][$doctorId] = [
'doctor_id' => $doctorId,
'doctor_name' => trim((string) ($appointment['doctor_name'] ?? '')) ?: '未知医生',
'appointment_time_set' => [],
'total_appointments' => 0,
'waiting_appointments' => 0,
'completed_appointments' => 0,
'missed_appointments' => 0,
];
}
$time = substr((string) ($appointment['appointment_time'] ?? ''), 0, 5);
if ($time !== '') {
$doctorDetails[$date][$doctorId]['appointment_time_set'][$time] = true;
}
$status = (int) ($appointment['status'] ?? 0);
$doctorDetails[$date][$doctorId]['total_appointments']++;
if ($status === 1) {
$doctorDetails[$date][$doctorId]['waiting_appointments']++;
} elseif ($status === 3) {
$doctorDetails[$date][$doctorId]['completed_appointments']++;
} elseif ($status === 4) {
$doctorDetails[$date][$doctorId]['missed_appointments']++;
}
}
foreach ($days as $date => &$day) {
$rows = [];
foreach ($doctorDetails[$date] ?? [] as $doctor) {
$times = array_values(array_keys($doctor['appointment_time_set'] ?? []));
sort($times, SORT_STRING);
unset($doctor['appointment_time_set']);
$doctor['appointment_times'] = $times;
$rows[] = $doctor;
}
usort($rows, static function (array $left, array $right): int {
return $right['waiting_appointments'] <=> $left['waiting_appointments']
?: $right['total_appointments'] <=> $left['total_appointments']
?: strcmp((string) $left['doctor_name'], (string) $right['doctor_name']);
});
$day['total_appointments'] = array_sum(array_column($rows, 'total_appointments'));
$day['waiting_appointments'] = array_sum(array_column($rows, 'waiting_appointments'));
$day['completed_appointments'] = array_sum(array_column($rows, 'completed_appointments'));
$day['missed_appointments'] = array_sum(array_column($rows, 'missed_appointments'));
$day['doctor_count'] = count($rows);
$day['doctors'] = $rows;
}
unset($day);
return array_values($days);
}
private function usesOwnershipSchedule(): bool
{
if ((int) ($this->adminInfo['root'] ?? 0) === 1) {
return false;
}
$roleIds = $this->currentRoleIds();
return in_array(2, $roleIds, true) && array_intersect($roleIds, [3, 7, 8]) === [];
}
/** @return int[] */
private function visibleDoctorIds(string $startDate, string $endDate): array
{
if ((int) ($this->adminInfo['root'] ?? 0) === 1) {
$doctorIds = array_values(array_unique(array_map('intval', Roster::whereBetween('date', [$startDate, $endDate])
->where('status', 1)
->whereNull('delete_time')
->column('doctor_id'))));
return $this->activeDoctorIds($doctorIds);
}
$roleIds = $this->currentRoleIds();
$isTeamRole = array_intersect($roleIds, [3, 7, 8]) !== [];
$isDoctor = in_array(1, $roleIds, true);
$isAssistant = in_array(2, $roleIds, true);
// 纯医生账号的概览只统计本人排班,避免同一患者曾由其他医生接诊时放大到其他医生。
if (!$isTeamRole && $isDoctor && !$isAssistant) {
return $this->activeDoctorIds([$this->adminId]);
}
$query = Appointment::alias('scope_a')
->join('tcm_diagnosis d', 'scope_a.patient_id = d.id')
->whereNull('d.delete_time')
->where('d.status', 1)
->whereIn('scope_a.status', self::EFFECTIVE_STATUSES)
->where('scope_a.doctor_id', '>', 0);
MyPatientLogic::applyScope($query, $this->adminId, $this->adminInfo);
$doctorIds = array_values(array_unique(array_filter(array_map('intval', $query->distinct(true)->column('scope_a.doctor_id')))));
if (!$isTeamRole && $isDoctor) {
$doctorIds[] = $this->adminId;
}
return $this->activeDoctorIds(array_values(array_unique($doctorIds)));
}
/** @return int[] */
private function currentRoleIds(): array
{
return array_values(array_unique(array_filter(array_map('intval', AdminRole::where('admin_id', $this->adminId)->column('role_id')))));
}
/** @param int[] $doctorIds @return int[] */
private function activeDoctorIds(array $doctorIds): array
{
$doctorIds = array_values(array_unique(array_filter(array_map('intval', $doctorIds))));
if ($doctorIds === []) {
return [];
}
$roleDoctorIds = array_values(array_unique(array_map('intval', AdminRole::whereIn('admin_id', $doctorIds)
->where('role_id', 1)
->column('admin_id'))));
if ($roleDoctorIds === []) {
return [];
}
$activeSet = array_fill_keys(array_map('intval', Admin::whereIn('id', $roleDoctorIds)
->where('disable', 0)
->column('id')), true);
return array_values(array_filter($doctorIds, static function (int $doctorId) use ($activeSet): bool {
return isset($activeSet[$doctorId]);
}));
}
/** @return array<string,mixed> */
private function emptyScheduleDay(string $date): array
{
$weekdayLabels = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
$timestamp = strtotime($date) ?: time();
return [
'date' => $date,
'date_text' => date('m-d', $timestamp),
'weekday' => $weekdayLabels[(int) date('w', $timestamp)],
'total_slots' => 0,
'booked_slots' => 0,
'passed_slots' => 0,
'remaining_slots' => 0,
'empty_slots' => 0,
'doctor_count' => 0,
'doctors' => [],
'total_appointments' => 0,
'waiting_appointments' => 0,
'completed_appointments' => 0,
'missed_appointments' => 0,
];
}
private function queueStatus(int $status, bool $confirmed, int $aheadCount): string
{
if ($status === 3) {
return 'completed';
}
if ($status === 4) {
return 'missed';
}
if ($confirmed) {
return 'consulting';
}
return $aheadCount === 0 ? 'next' : 'waiting';
}
private function queueStatusText(string $status): string
{
return [
'completed' => '已完成',
'missed' => '已过号',
'consulting' => '就诊中',
'next' => '待确认',
'waiting' => '等待中',
][$status] ?? '等待中';
}
private function maskPhone(string $phone): string
{
return preg_replace('/^(\d{3})\d{4}(\d{4})$/', '$1****$2', $phone) ?: $phone;
}
private function appointmentTimeText(array $row): string
{
$time = trim((string) ($row['appointment_time'] ?? ''));
if (strlen($time) > 5) {
$time = substr($time, 0, 5);
}
return trim((string) ($row['appointment_date'] ?? '') . ' ' . $time);
}
private function appointmentStatusText(int $status): string
{
return [1 => '已挂号', 3 => '已完成', 4 => '已过号'][$status] ?? '未知';
}
private function appointmentTypeText(string $type): string
{
return ['video' => '视频问诊', 'text' => '图文问诊', 'phone' => '电话问诊'][$type] ?? '面诊';
}
private function progressText(bool $confirmed, bool $completed, bool $prescribed, int $status): string
{
if ($status === 4) {
return '已过号';
}
if (!$confirmed) {
return '待确认诊单';
}
if (!$completed) {
return '待完诊';
}
return $prescribed ? '已开方' : '待开方';
}
}
@@ -0,0 +1,71 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\notice;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\lists\ListsSearchInterface;
use app\common\model\notice\NoticeSetting;
/**
* 通知设置
* Class NoticeSettingLists
* @package app\adminapi\lists\notice
*/
class NoticeSettingLists extends BaseAdminDataLists implements ListsSearchInterface
{
/**
* @notes 搜索条件
* @return \string[][]
* @author ljj
* @date 2022/2/17 2:21 下午
*/
public function setSearch(): array
{
return [
'=' => ['recipient', 'type']
];
}
/**
* @notes 通知设置列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author ljj
* @date 2022/2/16 3:18 下午
*/
public function lists(): array
{
$lists = (new NoticeSetting())->field('id,scene_name,sms_notice,type')
->append(['sms_status_desc','type_desc'])
->where($this->searchWhere)
->select()
->toArray();
return $lists;
}
/**
* @notes 通知设置数量
* @return int
* @author ljj
* @date 2022/2/16 3:18 下午
*/
public function count(): int
{
return (new NoticeSetting())->where($this->searchWhere)->count();
}
}
@@ -0,0 +1,262 @@
<?php
declare(strict_types=1);
namespace app\adminapi\lists\order;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\lists\ListsSearchInterface;
use app\common\lists\Traits\HasDataScopeFilter;
use app\common\model\Order;
use app\common\model\auth\AdminRole;
/**
* 订单列表
* Class OrderLists
* @package app\adminapi\lists\order
*/
class OrderLists extends BaseAdminDataLists implements ListsSearchInterface
{
use HasDataScopeFilter;
/**
* 将列表「创建时间」参数规范为可解析的日期时间(datetimerange 已是 Y-m-d H:i:s,不能再去拼接 00:00:00,否则会变成非法字符串)
*/
private function normalizeListDateTimeString(string $raw, bool $isEnd): string
{
$raw = trim($raw);
if ($raw === '') {
return '';
}
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $raw) === 1) {
return $isEnd ? ($raw . ' 23:59:59') : ($raw . ' 00:00:00');
}
if (strlen($raw) === 19 && preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $raw) === 1) {
return $raw;
}
$ts = strtotime($raw);
if ($ts !== false) {
return date('Y-m-d H:i:s', $ts);
}
return '';
}
/**
* 患者关键词:在诊单表按姓名、手机号或诊单ID匹配(子查询里显式 whereOr,避免 | 在闭包/子查下兼容问题)
*
* @return \Closure|array{} 无匹配关键词时返回空数组表示不加条件
*/
private function patientKeywordSubWhere()
{
$kw = trim((string) ($this->params['patient_keyword'] ?? ''));
if ($kw === '') {
return [];
}
return function ($query) use ($kw) {
$query->table('zyt_tcm_diagnosis')
->whereNull('delete_time')
->where(function ($q) use ($kw) {
$q->where('patient_name', 'like', '%' . $kw . '%')
->whereOr('phone', 'like', '%' . $kw . '%');
if (preg_match('/^\d+$/', $kw) && (int) $kw > 0) {
$q->whereOr('id', '=', (int) $kw);
}
})
->field('id');
};
}
/**
* 创建时间:支持 int 时间戳 与 字符串 datetime 两种存法(用 OR 包裹,至少命中一类)
*/
private function appendCreateTimeWhere(array &$where): void
{
$s = !empty($this->params['create_time_start'])
? $this->normalizeListDateTimeString((string) $this->params['create_time_start'], false)
: '';
$e = !empty($this->params['create_time_end'])
? $this->normalizeListDateTimeString((string) $this->params['create_time_end'], true)
: '';
if ($s === '' && $e === '') {
return;
}
$i0 = $s !== '' ? (int) strtotime($s) : 0;
$i1 = $e !== '' ? (int) strtotime($e) : 0;
if ($i0 > 0 && $i1 > 0 && $i1 < $i0) {
$t = $i0;
$i0 = $i1;
$i1 = $t;
$ts = $s;
$s = $e;
$e = $ts;
}
$intOk = $i0 > 0 || $i1 > 0;
$strOk = $s !== '' || $e !== '';
if (!$intOk && !$strOk) {
return;
}
$where[] = function ($query) use ($s, $e, $i0, $i1, $intOk, $strOk) {
$query->where(function ($q) use ($s, $e, $i0, $i1, $intOk, $strOk) {
if ($intOk) {
$q->where(function ($qi) use ($i0, $i1) {
if ($i0 > 0) {
$qi->where('create_time', '>=', $i0);
}
if ($i1 > 0) {
$qi->where('create_time', '<=', $i1);
}
});
}
if ($strOk) {
if ($intOk) {
$q->whereOr(function ($qs) use ($s, $e) {
if ($s !== '') {
$qs->where('create_time', '>=', $s);
}
if ($e !== '') {
$qs->where('create_time', '<=', $e);
}
});
} else {
$q->where(function ($qs) use ($s, $e) {
if ($s !== '') {
$qs->where('create_time', '>=', $s);
}
if ($e !== '') {
$qs->where('create_time', '<=', $e);
}
});
}
}
});
};
}
/**
* @notes 设置搜索条件
* @return array
*/
public function setSearch(): array
{
return [
'=' => ['order_type', 'status'],
// ListsSearchTrait 只识别 %like% / like% 等,「like」不会命中任何 case,订单号因此搜不到
'%like%' => ['order_no'],
];
}
/**
* 按医助筛选:与「创建人」一致(指派后创建人为该医助)
*/
private function appendAssistantFilter(array &$where): void
{
$assistantId = (int) ($this->params['assistant_id'] ?? 0);
if ($assistantId <= 0) {
return;
}
$where[] = ['creator_id', '=', $assistantId];
}
private function filterByPermission(array &$where): void
{
// 检查是否是超管(root字段为1)
if (!empty($this->adminInfo['root']) && $this->adminInfo['root'] == 1) {
// 超管不过滤,可以看所有订单
return;
}
// 可查看全部订单:project.order_list_view_all_roles;未配置时回退 order_edit_all_roles
$supervisorRoles = config('project.order_list_view_all_roles', null);
if (!\is_array($supervisorRoles) || $supervisorRoles === []) {
$supervisorRoles = config('project.order_edit_all_roles', [0, 3, 4, 9, 6]);
}
// 检查当前用户是否是主管
$roleIds = \app\common\model\auth\AdminRole::where('admin_id', $this->adminId)->column('role_id');
// 如果不是主管,只能看自己创建的订单
$isSupervisor = count(array_intersect($roleIds, $supervisorRoles)) > 0;
if (!$isSupervisor) {
$where[] = ['creator_id', '=', $this->adminId];
}
}
/**
* @notes 获取列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function lists(): array
{
$where = $this->searchWhere;
// 权限控制:普通员工只能看自己创建的订单
$this->filterByPermission($where);
// 处理患者关联状态:pending=待关联, associated=已关联
$patientAssociation = $this->params['patient_association'] ?? '';
if ($patientAssociation === 'pending') {
$where[] = ['patient_id', 'null', ''];
} elseif ($patientAssociation === 'associated') {
$where[] = ['patient_id', 'not null', ''];
}
// 处理患者关键词搜索(从诊单表搜索)
$pkw = $this->patientKeywordSubWhere();
if ($pkw !== []) {
$where[] = ['patient_id', 'in', $pkw];
}
$this->appendAssistantFilter($where);
$this->appendCreateTimeWhere($where);
$query = Order::where($where);
$this->applyDataScopeByOwner($query, 'creator_id');
return $query
->with(['patient', 'creator'])
->order(['create_time' => 'desc'])
->limit($this->limitOffset, $this->pageSize)
->select()
->toArray();
}
/**
* @notes 获取数量
* @return int
*/
public function count(): int
{
$where = $this->searchWhere;
// 权限控制:普通员工只能看自己创建的订单
$this->filterByPermission($where);
// 处理患者关联状态
$patientAssociation = $this->params['patient_association'] ?? '';
if ($patientAssociation === 'pending') {
$where[] = ['patient_id', 'null', ''];
} elseif ($patientAssociation === 'associated') {
$where[] = ['patient_id', 'not null', ''];
}
// 处理患者关键词搜索(从诊单表搜索)
$pkw = $this->patientKeywordSubWhere();
if ($pkw !== []) {
$where[] = ['patient_id', 'in', $pkw];
}
$this->appendAssistantFilter($where);
$this->appendCreateTimeWhere($where);
$query = Order::where($where);
$this->applyDataScopeByOwner($query, 'creator_id');
return $query->count();
}
}
@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace app\adminapi\lists\pharmacy;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\lists\ListsSearchInterface;
use think\facade\Db;
class MedicineMappingLists extends BaseAdminDataLists implements ListsSearchInterface
{
public function setSearch(): array
{
return [];
}
public function lists(): array
{
$rows = $this->query()
->field($this->fields())
->limit($this->limitOffset, $this->limitLength)
->order('l.id', 'desc')
->select()
->toArray();
foreach ($rows as &$row) {
foreach ([
'local_medicine_id', 'local_status', 'mapping_id', 'mapping_status',
'operator_id', 'mapping_update_time', 'catalog_version', 'remote_status', 'remote_deleted',
] as $field) {
if ($row[$field] !== null) {
$row[$field] = (int) $row[$field];
}
}
}
unset($row);
return $rows;
}
public function count(): int
{
return (int) $this->query()->count('l.id');
}
private function query()
{
$query = Db::name('doctor_medicine')->alias('l')
->leftJoin(
'ej_medicine_mapping m',
'm.local_medicine_id = l.id AND m.status = 1 AND m.delete_time IS NULL'
)
->leftJoin('ej_medicine_catalog c', 'c.medicine_code = m.medicine_code')
->whereNull('l.delete_time');
$localName = trim((string) ($this->params['local_name'] ?? ''));
if ($localName !== '') {
$query->where('l.name', 'like', '%' . $localName . '%');
}
$remoteKeyword = trim((string) ($this->params['remote_keyword'] ?? ''));
if ($remoteKeyword !== '') {
$query->where(function ($nested) use ($remoteKeyword): void {
$nested->where('c.name', 'like', '%' . $remoteKeyword . '%')
->whereOr('c.medicine_code', 'like', '%' . $remoteKeyword . '%');
});
}
$mappingStatus = trim((string) ($this->params['mapping_status'] ?? ''));
if ($mappingStatus === 'mapped') {
$query->whereNotNull('m.id')->where('c.status', 1)->where('c.remote_deleted', 0);
} elseif ($mappingStatus === 'unmapped') {
$query->whereNull('m.id');
} elseif ($mappingStatus === 'invalid') {
$query->whereNotNull('m.id')
->where(function ($nested): void {
$nested->whereNull('c.id')
->whereOr('c.status', '<>', 1)
->whereOr('c.remote_deleted', 1);
});
}
return $query;
}
private function fields(): string
{
return implode(',', [
'l.id AS local_medicine_id',
'l.name AS local_name',
'l.unit AS local_unit',
'l.status AS local_status',
'm.id AS mapping_id',
'm.medicine_code',
'm.operator_id',
'm.operator_name',
'm.update_time AS mapping_update_time',
'c.name AS remote_name',
'c.brand AS remote_brand',
'c.unit AS remote_unit',
'c.settlement_price',
'c.retail_price',
'c.catalog_version',
'c.status AS remote_status',
'c.remote_deleted',
"CASE WHEN m.id IS NULL THEN 0 "
. "WHEN c.id IS NULL OR c.status <> 1 OR c.remote_deleted = 1 THEN 2 ELSE 1 END AS mapping_status",
]);
}
}
@@ -0,0 +1,339 @@
<?php
declare(strict_types=1);
namespace app\adminapi\lists\qywx;
use app\adminapi\lists\BaseAdminDataLists;
use app\adminapi\logic\qywx\CustomerLogic;
use app\common\lists\ListsSearchInterface;
use app\common\model\auth\Admin;
use app\common\model\QywxExternalContact;
use think\facade\Db;
/**
* 企业微信客户列表
*/
class CustomerLists extends BaseAdminDataLists implements ListsSearchInterface
{
/**
* @notes 搜索条件
*/
public function setSearch(): array
{
// 表无 follow_user 列,跟进人在 follow_users(JSON);跟进人筛选在 baseQuery() 中处理
return [
'%like%' => ['name'],
];
}
/**
* @return int[] 入参 tag_ids 规范化后的非空字符串数组(实际为 string[])
*/
private function normalizeTagIds(): array
{
$raw = $this->params['tag_ids'] ?? null;
if ($raw === null || $raw === '') {
return [];
}
if (is_string($raw)) {
$raw = explode(',', $raw);
}
if (!is_array($raw)) {
return [];
}
$ids = [];
foreach ($raw as $v) {
$s = trim((string) $v);
if ($s !== '') {
$ids[] = $s;
}
}
return array_values(array_unique($ids));
}
/**
* 去重筛选:
* - first(默认):按客户首次添加时间筛选(客户去重,系统原口径)
* - any:按添加事件流水筛选(含老客被其他员工重加,对齐企微「当天有添加动作」)
*/
private function dedupeMode(): string
{
$mode = strtolower(trim((string) ($this->params['dedupe_mode'] ?? 'first')));
return $mode === 'any' ? 'any' : 'first';
}
/**
* @return string[] 跟进人关键词对应的企微 userid(含已软删 admin
*/
private function resolveFollowStaffWxUserids(): array
{
$kw = trim((string) ($this->params['follow_user'] ?? ''));
if ($kw === '') {
return [];
}
$escaped = addcslashes($kw, '%_\\');
$rows = Db::name('admin')
->whereLike('name', '%' . $escaped . '%')
->where('work_wechat_userid', '<>', '')
->whereNotNull('work_wechat_userid')
->column('work_wechat_userid');
$wxUserids = [];
foreach ($rows as $uid) {
$uid = trim((string) $uid);
if ($uid !== '') {
$wxUserids[$uid] = true;
}
}
// 关键词本身也可能是企微 userid(通常无中文);中文名不能当 userid 去筛事件流水
if ($kw !== '' && !preg_match('/\p{Han}/u', $kw)) {
$wxUserids[$kw] = true;
}
return array_keys($wxUserids);
}
/**
* 跟进人筛选:列表展示优先用 admin.name(见 lists() 补 admin_name),
* 但库内 follow_users JSON 通常只有企微 userid,直接 LIKE 中文名会搜不到。
* 策略:admin.name 模糊匹配 → work_wechat_userid / admin.id,再按 userid / follow_admin_ids 过滤;
* 同时保留对 follow_users 原文的 LIKE(兼容直接搜 userid、备注等)。
*/
private function applyFollowUserFilter($query)
{
$kw = trim((string) ($this->params['follow_user'] ?? ''));
if ($kw === '') {
return;
}
$escaped = addcslashes($kw, '%_\\');
// 含已软删账号,避免离职后跟进人姓名映射丢失导致搜不到历史客户
$adminRows = Db::name('admin')
->whereLike('name', '%' . $escaped . '%')
->where('work_wechat_userid', '<>', '')
->whereNotNull('work_wechat_userid')
->field(['id', 'work_wechat_userid'])
->select()
->toArray();
$wxUserids = [];
$adminIds = [];
foreach ($adminRows as $row) {
$uid = trim((string) ($row['work_wechat_userid'] ?? ''));
if ($uid !== '') {
$wxUserids[$uid] = true;
}
$aid = (int) ($row['id'] ?? 0);
if ($aid > 0) {
$adminIds[$aid] = true;
}
}
$wxUserids = array_keys($wxUserids);
$adminIds = array_keys($adminIds);
$query->where(function ($q) use ($escaped, $wxUserids, $adminIds) {
$q->whereLike('follow_users', '%' . $escaped . '%');
foreach ($wxUserids as $uid) {
$uidEsc = addcslashes($uid, '%_\\');
$q->whereOr('follow_users', 'like', '%"userid":"' . $uidEsc . '"%');
}
foreach ($adminIds as $aid) {
// JSON 数组精确包含,避免 id=12 误命中 123
$q->whereOrRaw(
'JSON_CONTAINS(IFNULL(follow_admin_ids, \'[]\'), ?)',
[json_encode($aid)]
);
}
});
}
/**
* @return array{0:int,1:int} [startTs, endTs],未设置则为 0
*/
private function parseAddTimeBounds(): array
{
$addStart = trim((string) ($this->params['add_time_start'] ?? ''));
$addEnd = trim((string) ($this->params['add_time_end'] ?? ''));
$startTs = 0;
$endTs = 0;
if ($addStart !== '') {
$t = strtotime($addStart . ' 00:00:00');
if ($t !== false) {
$startTs = (int) $t;
}
}
if ($addEnd !== '') {
$t = strtotime($addEnd . ' 23:59:59');
if ($t !== false) {
$endTs = (int) $t;
}
}
return [$startTs, $endTs];
}
/**
* 添加时间筛选。
* firstexternal_first_add_time(客户去重)
* any:事件流水 add_external_contact(含重加;可叠加跟进人 userid)
*
* @return bool true=已按事件+跟进人收窄,调用方勿再 applyFollowUserFilter
*/
private function applyAddTimeFilter($query): bool
{
[$startTs, $endTs] = $this->parseAddTimeBounds();
if ($startTs <= 0 && $endTs <= 0) {
return false;
}
if ($this->dedupeMode() === 'any') {
$eventQuery = Db::name('qywx_external_contact_event')
->where('change_type', 'add_external_contact')
->where('external_userid', '<>', '');
if ($startTs > 0) {
$eventQuery->where('event_time', '>=', $startTs);
}
if ($endTs > 0) {
$eventQuery->where('event_time', '<=', $endTs);
}
$followKw = trim((string) ($this->params['follow_user'] ?? ''));
$staffScoped = false;
if ($followKw !== '') {
$staffIds = $this->resolveFollowStaffWxUserids();
// 去掉「关键词本身」这一项后若仍有 admin 映射,或关键词像 userid,则按事件员工收窄
$staffIds = array_values(array_filter($staffIds, static fn (string $id): bool => $id !== ''));
if ($staffIds !== []) {
$eventQuery->whereIn('user_id', $staffIds);
$staffScoped = true;
}
}
$extIds = $eventQuery->group('external_userid')->column('external_userid');
if ($extIds === []) {
$query->whereRaw('1=0');
} else {
$query->whereIn('external_userid', $extIds);
}
return $staffScoped;
}
$effExpr = 'COALESCE(NULLIF(external_first_add_time, 0), create_time)';
if ($startTs > 0) {
$query->whereRaw($effExpr . ' > 0 AND ' . $effExpr . ' >= ?', [$startTs]);
}
if ($endTs > 0) {
$query->whereRaw($effExpr . ' > 0 AND ' . $effExpr . ' <= ?', [$endTs]);
}
return false;
}
private function baseQuery()
{
$query = QywxExternalContact::where($this->searchWhere);
// 添加时间可能已按「跟进人+事件流水」收窄;此时不必再 LIKE follow_users
$followAlreadyScoped = $this->applyAddTimeFilter($query);
if (!$followAlreadyScoped) {
$this->applyFollowUserFilter($query);
}
// 标签筛选:JOIN 关系表按 tag_id 过滤;多个标签为 OR(命中任一即返回)。
// 走 zyt_qywx_external_contact_tag.idx_tag 索引,比 LIKE follow_users 快得多
$tagIds = $this->normalizeTagIds();
if ($tagIds !== []) {
$matchedExtIds = Db::name('qywx_external_contact_tag')
->whereIn('tag_id', $tagIds)
->group('external_userid')
->column('external_userid');
if ($matchedExtIds === []) {
// 没人命中:直接给一个不可能成立的条件,避免下面命中所有客户
$query->whereRaw('1=0');
} else {
$query->whereIn('external_userid', $matchedExtIds);
}
}
return $query;
}
/**
* @notes 获取列表
*/
public function lists(): array
{
// 按首次添加时间倒序:库字段 external_first_add_time;未回填(0)时回退 create_time(与列表「添加时间」展示一致)
$lists = $this->baseQuery()
->orderRaw(
'(COALESCE(NULLIF(external_first_add_time, 0), create_time) = 0) ASC, '
. 'COALESCE(NULLIF(external_first_add_time, 0), create_time) DESC, id DESC'
)
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
$wxUserids = [];
foreach ($lists as $item) {
$raw = json_decode($item['follow_users'] ?? '[]', true);
if (!is_array($raw)) {
continue;
}
foreach ($raw as $fu) {
if (!is_array($fu)) {
continue;
}
$wx = trim((string) ($fu['userid'] ?? ''));
if ($wx !== '') {
$wxUserids[$wx] = true;
}
}
}
$wxUserids = array_keys($wxUserids);
$adminNameByWx = [];
if ($wxUserids !== []) {
$adminNameByWx = Admin::whereIn('work_wechat_userid', $wxUserids)->column('name', 'work_wechat_userid');
}
foreach ($lists as &$item) {
$followUsers = json_decode($item['follow_users'] ?? '[]', true);
$followUsers = is_array($followUsers) ? $followUsers : [];
foreach ($followUsers as &$fu) {
if (!is_array($fu)) {
continue;
}
$wx = trim((string) ($fu['userid'] ?? ''));
if ($wx !== '' && isset($adminNameByWx[$wx]) && $adminNameByWx[$wx] !== '') {
$fu['admin_name'] = $adminNameByWx[$wx];
}
}
unset($fu);
$item['follow_users'] = $followUsers;
$followAdminIds = json_decode($item['follow_admin_ids'] ?? '[]', true);
$item['follow_admin_ids'] = is_array($followAdminIds) ? $followAdminIds : [];
// 解析标签 JSON 数组(值由 CustomerLogic::extractFollowUserTags 写入;按 tag_id 去重)
$tags = json_decode((string) ($item['tags'] ?? '[]'), true);
$item['tags'] = is_array($tags) ? $tags : [];
$fromDb = (int) ($item['external_first_add_time'] ?? 0);
$fromJson = CustomerLogic::minFollowCreatetime($followUsers);
$item['external_first_add_time'] = $fromDb > 0 ? $fromDb : $fromJson;
}
unset($item);
return $lists;
}
/**
* @notes 获取数量
*/
public function count(): int
{
return $this->baseQuery()->count();
}
}
@@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
namespace app\adminapi\lists\qywx;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\lists\ListsSearchInterface;
use app\common\model\auth\Admin;
use app\common\model\QywxExternalContact;
use app\common\model\QywxMsgArchive;
use app\common\model\QywxMsgArchiveMedia;
/**
* 单会话消息历史
*
* 两种查询模式,二选一:
* 1) session_id 传 QywxMsgSession.id(推荐)
* 2) staff_userid + external_userid(单聊)/ roomid(群聊)
*
* 消息默认按 send_time 升序,便于前端从下往上追加。
* 支持 before_time / after_time 游标翻页。
*/
class MsgArchiveLists extends BaseAdminDataLists implements ListsSearchInterface
{
public function setSearch(): array
{
return [
'=' => ['roomid', 'msgtype'],
];
}
private function resolveScope(): array
{
$sessionId = (int) ($this->params['session_id'] ?? 0);
if ($sessionId > 0) {
$session = \app\common\model\QywxMsgSession::find($sessionId);
if ($session) {
return [
'staff_userid' => (string) $session['staff_userid'],
'external_userid' => (string) $session['external_userid'],
'roomid' => (string) $session['roomid'],
];
}
}
return [
'staff_userid' => trim((string) ($this->params['staff_userid'] ?? '')),
'external_userid' => trim((string) ($this->params['external_userid'] ?? '')),
'roomid' => trim((string) ($this->params['roomid'] ?? '')),
];
}
private function baseQuery()
{
$scope = $this->resolveScope();
$query = QywxMsgArchive::where($this->searchWhere);
if ($scope['roomid'] !== '') {
$query->where('roomid', $scope['roomid']);
} else {
// 单聊:两端顺序不定,from_user/tolist 都可能为员工或客户
$staff = $scope['staff_userid'];
$external = $scope['external_userid'];
if ($staff === '' && $external === '') {
$query->where('id', 0);
} else {
$query->where('roomid', '');
if ($staff !== '' && $external !== '') {
$query->where(function ($q) use ($staff, $external) {
$q->where(function ($sub) use ($staff, $external) {
$sub->where('from_user', $staff)->whereLike('to_list', '%"' . $external . '"%');
})->whereOr(function ($sub) use ($staff, $external) {
$sub->where('from_user', $external)->whereLike('to_list', '%"' . $staff . '"%');
});
});
} elseif ($staff !== '') {
$query->where(function ($q) use ($staff) {
$q->where('from_user', $staff)->whereOr('to_list', 'like', '%"' . $staff . '"%');
});
} else {
$query->where(function ($q) use ($external) {
$q->where('from_user', $external)->whereOr('to_list', 'like', '%"' . $external . '"%');
});
}
}
}
if (!empty($this->params['before_time'])) {
$query->where('send_time', '<', (int) $this->params['before_time']);
}
if (!empty($this->params['after_time'])) {
$query->where('send_time', '>', (int) $this->params['after_time']);
}
return $query;
}
public function lists(): array
{
$rows = $this->baseQuery()
->order('send_time', 'desc')
->order('id', 'desc')
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
if (empty($rows)) {
return [];
}
$rows = array_reverse($rows); // 前端按时间升序渲染
$users = [];
foreach ($rows as $r) {
$users[$r['from_user']] = true;
$toList = $r['to_list'];
if (is_array($toList)) {
foreach ($toList as $to) {
$users[$to] = true;
}
}
}
$users = array_keys(array_filter($users, fn ($_, $k) => $k !== '', ARRAY_FILTER_USE_BOTH));
$adminMap = [];
$extMap = [];
if ($users) {
$adminMap = Admin::whereIn('work_wechat_userid', $users)
->column('id,name,avatar,work_wechat_userid', 'work_wechat_userid');
$extMap = QywxExternalContact::whereIn('external_userid', $users)
->column('external_userid,name,avatar,type,unionid', 'external_userid');
}
$msgIds = array_column($rows, 'msgid');
$mediaMap = [];
if ($msgIds) {
$mediaList = QywxMsgArchiveMedia::whereIn('msgid', $msgIds)->select()->toArray();
foreach ($mediaList as $m) {
$mediaMap[$m['msgid']][] = $m;
}
}
foreach ($rows as &$r) {
$fromId = (string) $r['from_user'];
$r['from_is_staff'] = isset($adminMap[$fromId]);
$r['from_profile'] = $adminMap[$fromId] ?? ($extMap[$fromId] ?? null);
$r['media'] = $mediaMap[$r['msgid']] ?? [];
}
unset($r);
return $rows;
}
public function count(): int
{
return $this->baseQuery()->count();
}
}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace app\adminapi\lists\qywx;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\lists\ListsSearchInterface;
use app\common\model\QywxMsgSendTask;
class MsgSendTaskLists extends BaseAdminDataLists implements ListsSearchInterface
{
public function setSearch(): array
{
return [
'=' => ['sender_userid', 'status', 'admin_id'],
];
}
public function lists(): array
{
return QywxMsgSendTask::where($this->searchWhere)
->order('id', 'desc')
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
}
public function count(): int
{
return QywxMsgSendTask::where($this->searchWhere)->count();
}
}
@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace app\adminapi\lists\qywx;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\lists\ListsSearchInterface;
use app\common\model\auth\Admin;
use app\common\model\QywxExternalContact;
use app\common\model\QywxMsgSession;
/**
* 企微消息会话列表
*
* 查询参数:
* staff_userid - 过滤某员工的会话(支持传 admin_id 反查)
* external_userid- 过滤某客户
* keyword - 按客户名 / 摘要 模糊搜索
* only_unread - 仅未读
*/
class MsgSessionLists extends BaseAdminDataLists implements ListsSearchInterface
{
public function setSearch(): array
{
return [
'=' => ['staff_userid', 'external_userid', 'roomid', 'session_type'],
];
}
private function baseQuery()
{
$query = QywxMsgSession::where($this->searchWhere);
$adminId = (int) ($this->params['admin_id'] ?? 0);
if ($adminId > 0) {
$wxId = Admin::where('id', $adminId)->value('work_wechat_userid');
if ($wxId) {
$query->where('staff_userid', $wxId);
} else {
// 找不到对应企微 userid 时不返回任何会话
$query->where('id', 0);
}
}
$keyword = trim((string) ($this->params['keyword'] ?? ''));
if ($keyword !== '') {
$kw = addcslashes($keyword, '%_\\');
$extIds = QywxExternalContact::whereLike('name', '%' . $kw . '%')->column('external_userid');
$query->where(function ($q) use ($kw, $extIds) {
$q->whereLike('last_msg_summary', '%' . $kw . '%');
if ($extIds) {
$q->whereOr('external_userid', 'in', $extIds);
}
});
}
if (!empty($this->params['only_unread'])) {
$query->where('unread_staff', '>', 0);
}
return $query->order('last_msg_time', 'desc');
}
public function lists(): array
{
$rows = $this->baseQuery()
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
if (empty($rows)) {
return [];
}
$extIds = array_values(array_unique(array_filter(array_map(
fn ($r) => (string) ($r['external_userid'] ?? ''),
$rows
))));
$extMap = [];
if ($extIds) {
$extMap = QywxExternalContact::whereIn('external_userid', $extIds)
->column('name,avatar,type,gender,corp_name,unionid', 'external_userid');
}
$staffIds = array_values(array_unique(array_filter(array_map(
fn ($r) => (string) ($r['staff_userid'] ?? ''),
$rows
))));
$staffMap = [];
if ($staffIds) {
$staffMap = Admin::whereIn('work_wechat_userid', $staffIds)
->column('id,name,avatar', 'work_wechat_userid');
}
foreach ($rows as &$r) {
$r['customer'] = $extMap[$r['external_userid']] ?? null;
$r['staff'] = $staffMap[$r['staff_userid']] ?? null;
}
unset($r);
return $rows;
}
public function count(): int
{
return $this->baseQuery()->count();
}
}
@@ -0,0 +1,146 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\recharge;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\enum\PayEnum;
use app\common\lists\ListsExcelInterface;
use app\common\lists\ListsSearchInterface;
use app\common\model\recharge\RechargeOrder;
use app\common\service\FileService;
/**
* 充值记录列表
* Class RecharLists
* @package app\adminapi\lists
*/
class RechargeLists extends BaseAdminDataLists implements ListsSearchInterface, ListsExcelInterface
{
/**
* @notes 导出字段
* @return string[]
* @author 段誉
* @date 2023/2/24 16:07
*/
public function setExcelFields(): array
{
return [
'sn' => '充值单号',
'nickname' => '用户昵称',
'order_amount' => '充值金额',
'pay_way_text' => '支付方式',
'pay_status_text' => '支付状态',
'pay_time' => '支付时间',
'create_time' => '下单时间',
];
}
/**
* @notes 导出表名
* @return string
* @author 段誉
* @date 2023/2/24 16:07
*/
public function setFileName(): string
{
return '充值记录';
}
/**
* @notes 搜索条件
* @return \string[][]
* @author 段誉
* @date 2023/2/24 16:08
*/
public function setSearch(): array
{
return [
'=' => ['ro.sn', 'ro.pay_way', 'ro.pay_status'],
];
}
/**
* @notes 搜索条件
* @author 段誉
* @date 2023/2/24 16:08
*/
public function queryWhere()
{
$where = [];
// 用户编号
if (!empty($this->params['user_info'])) {
$where[] = ['u.sn|u.nickname|u.mobile|u.account', 'like', '%' . $this->params['user_info'] . '%'];
}
// 下单时间
if (!empty($this->params['start_time']) && !empty($this->params['end_time'])) {
$time = [strtotime($this->params['start_time']), strtotime($this->params['end_time'])];
$where[] = ['ro.create_time', 'between', $time];
}
return $where;
}
/**
* @notes 获取列表
* @return array
* @author 段誉
* @date 2023/2/24 16:13
*/
public function lists(): array
{
$field = 'ro.id,ro.sn,ro.order_amount,ro.pay_way,ro.pay_time,ro.pay_status,ro.create_time,ro.refund_status';
$field .= ',u.avatar,u.nickname,u.account';
$lists = RechargeOrder::alias('ro')
->join('user u', 'u.id = ro.user_id')
->field($field)
->where($this->queryWhere())
->where($this->searchWhere)
->order('ro.id', 'desc')
->limit($this->limitOffset, $this->limitLength)
->append(['pay_status_text', 'pay_way_text'])
->select()
->toArray();
foreach ($lists as &$item) {
$item['avatar'] = FileService::getFileUrl($item['avatar']);
$item['pay_time'] = empty($item['pay_time']) ? '' : date('Y-m-d H:i:s', $item['pay_time']);
}
return $lists;
}
/**
* @notes 获取数量
* @return int
* @author 段誉
* @date 2023/2/24 16:13
*/
public function count(): int
{
return RechargeOrder::alias('ro')
->join('user u', 'u.id = ro.user_id')
->where($this->queryWhere())
->where($this->searchWhere)
->count();
}
}
@@ -0,0 +1,76 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\setting\dict;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\lists\ListsSearchInterface;
use app\common\model\dict\DictData;
/**
* 字典数据列表
* Class DictDataLists
* @package app\adminapi\lists\dict
*/
class DictDataLists extends BaseAdminDataLists implements ListsSearchInterface
{
/**
* @notes 设置搜索条件
* @return \string[][]
* @author 段誉
* @date 2022/6/20 16:29
*/
public function setSearch(): array
{
return [
'%like%' => ['name', 'type_value'],
'=' => ['status', 'type_id']
];
}
/**
* @notes 获取列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/6/20 16:35
*/
public function lists(): array
{
return DictData::where($this->searchWhere)
->append(['status_desc'])
->limit($this->limitOffset, $this->limitLength)
->order(['sort' => 'desc', 'id' => 'desc'])
->select()
->toArray();
}
/**
* @notes 获取数量
* @return int
* @author 段誉
* @date 2022/6/20 16:35
*/
public function count(): int
{
return DictData::where($this->searchWhere)->count();
}
}
@@ -0,0 +1,76 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\setting\dict;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\lists\ListsSearchInterface;
use app\common\model\dict\DictType;
/**
* 字典类型列表
* Class DictTypeLists
* @package app\adminapi\lists\dictionary
*/
class DictTypeLists extends BaseAdminDataLists implements ListsSearchInterface
{
/**
* @notes 设置搜索条件
* @return \string[][]
* @author 段誉
* @date 2022/6/20 15:53
*/
public function setSearch(): array
{
return [
'%like%' => ['name', 'type'],
'=' => ['status']
];
}
/**
* @notes 获取列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/6/20 15:54
*/
public function lists(): array
{
return DictType::where($this->searchWhere)
->limit($this->limitOffset, $this->limitLength)
->append(['status_desc'])
->order(['id' => 'desc'])
->select()
->toArray();
}
/**
* @notes 获取数量
* @return int
* @author 段誉
* @date 2022/6/20 15:54
*/
public function count(): int
{
return DictType::where($this->searchWhere)->count();
}
}
@@ -0,0 +1,62 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\setting\pay;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\model\pay\PayConfig;
/**
* 支付配置列表
* Class PayConfigLists
* @package app\adminapi\lists\setting\pay
*/
class PayConfigLists extends BaseAdminDataLists
{
/**
* @notes 获取列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2023/2/23 16:15
*/
public function lists(): array
{
$lists = PayConfig::field('id,name,pay_way,icon,sort')
->append(['pay_way_name'])
->order('sort','desc')
->select()
->toArray();
return $lists;
}
/**
* @notes 获取数量
* @return int
* @author 段誉
* @date 2023/2/23 16:15
*/
public function count(): int
{
return PayConfig::count();
}
}
@@ -0,0 +1,108 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\setting\system;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\lists\ListsExcelInterface;
use app\common\lists\ListsSearchInterface;
use app\common\model\OperationLog;
/**
* 日志列表
* Class LogLists
* @package app\adminapi\lists\setting\system
*/
class LogLists extends BaseAdminDataLists implements ListsSearchInterface, ListsExcelInterface
{
/**
* @notes 设置搜索条件
* @return \string[][]
* @author ljj
* @date 2021/8/3 4:21 下午
*/
public function setSearch(): array
{
return [
'%like%' => ['admin_name','url','ip','type'],
'between_time' => 'create_time',
];
}
/**
* @notes 查看系统日志列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author ljj
* @date 2021/8/3 4:21 下午
*/
public function lists(): array
{
$lists = OperationLog::field('id,action,admin_name,admin_id,url,type,params,ip,create_time')
->where($this->searchWhere)
->limit($this->limitOffset, $this->limitLength)
->order('id','desc')
->select()
->toArray();
return $lists;
}
/**
* @notes 查看系统日志总数
* @return int
* @author ljj
* @date 2021/8/3 4:23 下午
*/
public function count(): int
{
return OperationLog::where($this->searchWhere)->count();
}
/**
* @notes 设置导出字段
* @return string[]
* @author ljj
* @date 2021/8/3 4:48 下午
*/
public function setExcelFields(): array
{
return [
// '数据库字段名(支持别名) => 'Excel表字段名'
'id' => '记录ID',
'action' => '操作',
'admin_name' => '管理员',
'admin_id' => '管理员ID',
'url' => '访问链接',
'type' => '访问方式',
'params' => '访问参数',
'ip' => '来源IP',
'create_time' => '日志时间',
];
}
/**
* @notes 设置默认表名
* @return string
* @author ljj
* @date 2021/8/3 4:48 下午
*/
public function setFileName(): string
{
return '系统日志';
}
}
@@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
namespace app\adminapi\lists\stats;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\lists\ListsSearchInterface;
use think\facade\Db;
/**
* 待分配诊单自动指派日志列表
*
* 数据来源:tcm_diagnosis_auto_assign_log(定时命令 tcm:auto-assign-pending 写入)
* 筛选:run_date / create_time 区间 / action / keyword / rollback 状态
*/
class AutoAssignLogLists extends BaseAdminDataLists implements ListsSearchInterface
{
private const TIER_LABELS = [
'gt70' => '>70%',
'60_70' => '60%~70%',
'50_60' => '50%~60%',
];
/**
* @notes 设置搜索条件
*/
public function setSearch(): array
{
return [
'=' => ['run_date', 'action', 'assistant_id', 'batch_no', 'stat_month'],
];
}
/**
* @notes 获取列表
*/
public function lists(): array
{
$rows = $this->buildQuery()
->order(['id' => 'desc'])
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
foreach ($rows as &$row) {
$ct = (int) ($row['create_time'] ?? 0);
$row['create_time_text'] = $ct > 0 ? date('Y-m-d H:i:s', $ct) : '';
$row['action_text'] = (int) ($row['action'] ?? 0) === 1 ? '已分配' : '未分配';
$row['tier_text'] = self::TIER_LABELS[(string) ($row['tier'] ?? '')] ?? '';
$row['visit2_rate'] = $row['visit2_rate'] !== null ? (float) $row['visit2_rate'] : null;
$rbTs = (int) ($row['rollback_time'] ?? 0);
$row['rollback_time_text'] = $rbTs > 0 ? date('Y-m-d H:i:s', $rbTs) : '';
$row['is_rollback'] = $rbTs > 0 ? 1 : 0;
$row['can_rollback'] = ((int) ($row['action'] ?? 0) === 1 && $rbTs === 0) ? 1 : 0;
if ($rbTs > 0) {
$row['rollback_status_text'] = '已回退'
. (($row['rollback_admin_name'] ?? '') !== '' ? (' · ' . $row['rollback_admin_name']) : '');
} else {
$row['rollback_status_text'] = ((int) ($row['action'] ?? 0) === 1) ? '可回退' : '—';
}
}
unset($row);
return $rows;
}
/**
* @notes 获取数量
*/
public function count(): int
{
return $this->buildQuery()->count();
}
/**
* @return \think\db\Query
*/
private function buildQuery()
{
$query = Db::name('tcm_diagnosis_auto_assign_log')->where($this->searchWhere);
$keyword = trim((string) ($this->params['keyword'] ?? ''));
if ($keyword !== '') {
$query->where(function ($q) use ($keyword) {
$q->whereLike('patient_name', '%' . $keyword . '%')
->whereOr('patient_phone', 'like', '%' . $keyword . '%')
->whereOr('assistant_name', 'like', '%' . $keyword . '%');
if (preg_match('/^\d+$/', $keyword) === 1 && (int) $keyword > 0) {
$q->whereOr('diagnosis_id', '=', (int) $keyword);
}
});
}
$startDate = trim((string) ($this->params['start_date'] ?? ''));
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate) === 1) {
$query->where('run_date', '>=', $startDate);
}
$endDate = trim((string) ($this->params['end_date'] ?? ''));
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate) === 1) {
$query->where('run_date', '<=', $endDate);
}
// 记录时间(create_time)区间:支持 Y-m-d 或 Y-m-d H:i:s
$startTs = $this->parseTimeParam((string) ($this->params['start_time'] ?? ''), false);
if ($startTs > 0) {
$query->where('create_time', '>=', $startTs);
}
$endTs = $this->parseTimeParam((string) ($this->params['end_time'] ?? ''), true);
if ($endTs > 0) {
$query->where('create_time', '<=', $endTs);
}
$rollback = trim((string) ($this->params['is_rollback'] ?? ''));
if ($rollback === '1') {
$query->where('rollback_time', '>', 0);
} elseif ($rollback === '0') {
$query->where('rollback_time', '=', 0);
}
return $query;
}
/**
* @param bool $endOfDay 仅日期时是否取当天结束
*/
private function parseTimeParam(string $raw, bool $endOfDay): int
{
$raw = trim($raw);
if ($raw === '') {
return 0;
}
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $raw) === 1) {
return (int) strtotime($raw . ($endOfDay ? ' 23:59:59' : ' 00:00:00'));
}
if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(:\d{2})?$/', $raw) === 1) {
if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/', $raw) === 1) {
$raw .= $endOfDay ? ':59' : ':00';
}
return (int) strtotime($raw);
}
return 0;
}
}
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace app\adminapi\lists\stats;
use app\adminapi\lists\BaseAdminDataLists;
use app\adminapi\logic\stats\PersonalStatsScopeTrait;
use app\common\lists\ListsExtendInterface;
use app\common\lists\ListsSearchInterface;
use app\common\lists\Traits\HasDataScopeFilter;
use app\common\model\stats\PersonalAccountCost;
class PersonalAccountCostLists extends BaseAdminDataLists implements ListsSearchInterface, ListsExtendInterface
{
use HasDataScopeFilter;
use PersonalStatsScopeTrait;
public function setSearch(): array
{
return [
'%like%' => ['media_source', 'creator_name', 'remark'],
];
}
private function baseQuery()
{
$query = PersonalAccountCost::where($this->searchWhere);
$visibleIds = $this->getDataScopeVisibleAdminIds();
$deptId = (int) ($this->params['dept_id'] ?? 0);
$finalIds = self::intersectVisibleByDept($visibleIds, $deptId);
if ($finalIds === []) {
$query->whereRaw('0 = 1');
} elseif ($finalIds !== null) {
$query->whereIn('creator_id', $finalIds);
}
if (!empty($this->params['start_date']) && !empty($this->params['end_date'])) {
$query->whereBetween('cost_date', [$this->params['start_date'], $this->params['end_date']]);
} elseif (!empty($this->params['start_date'])) {
$query->where('cost_date', '>=', $this->params['start_date']);
} elseif (!empty($this->params['end_date'])) {
$query->where('cost_date', '<=', $this->params['end_date']);
}
if (!empty($this->params['media_source'])) {
$query->where('media_source', trim((string) $this->params['media_source']));
}
return $query;
}
public function lists(): array
{
$rows = $this->baseQuery()
->order(['cost_date' => 'desc', 'id' => 'desc'])
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
return self::attachDeptInfoToRows($rows);
}
public function count(): int
{
return (int) $this->baseQuery()->count();
}
public function extend(): array
{
return [
'total_amount' => round((float) $this->baseQuery()->sum('amount'), 2),
'days_count' => (int) $this->baseQuery()->distinct(true)->count('cost_date'),
];
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace app\adminapi\lists\stats;
use app\adminapi\lists\BaseAdminDataLists;
use app\adminapi\logic\stats\PersonalStatsScopeTrait;
use app\common\lists\ListsSearchInterface;
use app\common\lists\Traits\HasDataScopeFilter;
use app\common\model\stats\PersonalYeji;
class PersonalYejiLists extends BaseAdminDataLists implements ListsSearchInterface
{
use HasDataScopeFilter;
use PersonalStatsScopeTrait;
public function setSearch(): array
{
return [
'%like%' => ['media_source', 'creator_name', 'remark'],
];
}
private function baseQuery()
{
$query = PersonalYeji::where($this->searchWhere);
$visibleIds = $this->getDataScopeVisibleAdminIds();
$deptId = (int) ($this->params['dept_id'] ?? 0);
$finalIds = self::intersectVisibleByDept($visibleIds, $deptId);
if ($finalIds === []) {
$query->whereRaw('0 = 1');
} elseif ($finalIds !== null) {
$query->whereIn('creator_id', $finalIds);
}
if (!empty($this->params['start_date']) && !empty($this->params['end_date'])) {
$query->whereBetween('yeji_date', [$this->params['start_date'], $this->params['end_date']]);
} elseif (!empty($this->params['start_date'])) {
$query->where('yeji_date', '>=', $this->params['start_date']);
} elseif (!empty($this->params['end_date'])) {
$query->where('yeji_date', '<=', $this->params['end_date']);
}
if (!empty($this->params['media_source'])) {
$query->where('media_source', trim((string) $this->params['media_source']));
}
if (!empty($this->params['creator_id'])) {
$query->where('creator_id', (int) $this->params['creator_id']);
}
return $query;
}
public function lists(): array
{
$rows = $this->baseQuery()
->order(['yeji_date' => 'desc', 'id' => 'desc'])
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
return self::attachDeptInfoToRows($rows);
}
public function count(): int
{
return (int) $this->baseQuery()->count();
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,73 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace app\adminapi\lists\tcm;
use app\adminapi\lists\BaseAdminDataLists;
use app\adminapi\logic\tcm\DiagnosisTodoLogic;
use app\common\lists\ListsSearchInterface;
use app\common\model\tcm\DiagnosisTodo;
/**
* 诊单待办事项列表
*
* 默认按 remind_time desc 排序;按 diagnosis_id 过滤。
*
* @package app\adminapi\lists\tcm
*/
class DiagnosisTodoLists extends BaseAdminDataLists implements ListsSearchInterface
{
public function setSearch(): array
{
return [
'=' => ['diagnosis_id', 'status', 'creator_id'],
];
}
public function lists(): array
{
$lists = DiagnosisTodo::where($this->searchWhere)
->field([
'id', 'diagnosis_id', 'patient_id', 'content', 'remind_time',
'status', 'creator_id', 'creator_name', 'notified_at', 'error',
'cancelled_at', 'cancelled_by', 'create_time', 'update_time',
])
->append([
'status_text',
'remind_time_text',
'notified_at_text',
'cancelled_at_text',
])
->order('remind_time', 'desc')
->order('id', 'desc')
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
// 业务态:是否能被「我」取消(前端按钮显隐用)
$adminId = (int) $this->adminId;
foreach ($lists as &$item) {
$item['can_cancel'] = DiagnosisTodoLogic::canCancel($item, $adminId);
}
return $lists;
}
public function count(): int
{
return DiagnosisTodo::where($this->searchWhere)->count();
}
}
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace app\adminapi\lists\tcm;
use app\adminapi\lists\BaseAdminDataLists;
use app\adminapi\logic\tcm\PrescriptionLibraryLogic;
use app\common\model\tcm\PrescriptionLibrary;
use app\common\lists\ListsSearchInterface;
/**
* 处方库列表
*/
class PrescriptionLibraryLists extends BaseAdminDataLists implements ListsSearchInterface
{
/**
* @notes 设置搜索条件
*/
public function setSearch(): array
{
return [
'%like%' => ['prescription_name'],
'=' => ['is_public', 'creator_id', 'formula_type']
];
}
/**
* @notes 列表数据范围:超管/管理员角色看全部;普通账号仅看自己创建 + 公开处方
*/
private function applyDataScope($query)
{
if (PrescriptionLibraryLogic::canManageAllPrescriptions($this->adminId, $this->adminInfo)) {
return $query;
}
// 开方页导入专用参数(勿与搜索项 creator_id 混用,否则无法 OR 出他人公开模板)
$prescribingCreatorId = (int) ($this->params['prescribing_creator_id'] ?? 0);
if ($prescribingCreatorId > 0
&& PrescriptionLibraryLogic::canListLibraryForCreator($this->adminId, $this->adminInfo, $prescribingCreatorId)) {
return $query->where(function ($q) use ($prescribingCreatorId) {
$q->where('creator_id', $prescribingCreatorId)
->whereOr('is_public', 1);
});
}
return $query->where(function ($q) {
$q->where('creator_id', $this->adminId)
->whereOr('is_public', 1);
});
}
/**
* @notes 获取列表
*/
public function lists(): array
{
$field = [
'id', 'prescription_name', 'formula_type', 'herbs', 'is_public', 'disable_edit',
'creator_id', 'creator_name', 'create_time', 'update_time'
];
$query = PrescriptionLibrary::where($this->searchWhere);
$this->applyDataScope($query);
$lists = $query
->field($field)
->limit($this->limitOffset, $this->limitLength)
->order('id', 'desc')
->select()
->toArray();
// 解析药材JSON
foreach ($lists as &$item) {
if (!empty($item['herbs'])) {
$item['herbs'] = json_decode($item['herbs'], true);
} else {
$item['herbs'] = [];
}
}
return $lists;
}
/**
* @notes 获取数量
*/
public function count(): int
{
$query = PrescriptionLibrary::where($this->searchWhere);
$this->applyDataScope($query);
return $query->count();
}
}
@@ -0,0 +1,343 @@
<?php
declare(strict_types=1);
namespace app\adminapi\lists\tcm;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\lists\ListsSearchInterface;
use app\common\lists\Traits\HasDataScopeFilter;
use app\adminapi\logic\tcm\PrescriptionLogic;
use app\common\model\tcm\Prescription;
use app\common\model\tcm\PrescriptionOrder;
/**
* 处方列表
*/
class PrescriptionLists extends BaseAdminDataLists implements ListsSearchInterface
{
use HasDataScopeFilter;
/**
* 识别「有业务订单且处方药材为空白/重复」的处方 ID(用于全局置顶排序)
*
* @param array<int,int|string> $candidateIds
* @return array<int,int>
*/
private function collectRiskPrescriptionIds(array $candidateIds): array
{
$candidateIds = array_values(array_unique(array_filter(array_map('intval', $candidateIds), static function (int $id): bool {
return $id > 0;
})));
if ($candidateIds === []) {
return [];
}
// 仅在「存在有效业务订单」的处方里做风险判定
$orderRxIds = PrescriptionOrder::whereIn('prescription_id', $candidateIds)
->whereNull('delete_time')
->where('fulfillment_status', '<>', 4)
->column('prescription_id');
$orderRxIds = array_values(array_unique(array_filter(array_map('intval', $orderRxIds), static function (int $id): bool {
return $id > 0;
})));
if ($orderRxIds === []) {
return [];
}
$rows = Prescription::whereIn('id', $orderRxIds)
->whereNull('delete_time')
->field(['id', 'herbs'])
->select()
->toArray();
$riskIds = [];
foreach ($rows as $row) {
$rid = (int) ($row['id'] ?? 0);
if ($rid <= 0) {
continue;
}
if ($this->isRiskHerbs($row['herbs'] ?? null)) {
$riskIds[] = $rid;
}
}
return array_values(array_unique($riskIds));
}
/**
* 药材风险判定:空白 or 重复(按名称,忽略空格与大小写)
*/
private function isRiskHerbs($herbsRaw): bool
{
$herbs = [];
if (is_array($herbsRaw)) {
$herbs = $herbsRaw;
} elseif (is_string($herbsRaw) && $herbsRaw !== '') {
$decoded = json_decode($herbsRaw, true);
if (is_array($decoded)) {
$herbs = $decoded;
}
}
$names = [];
foreach ($herbs as $h) {
if (!is_array($h)) {
continue;
}
$name = trim((string) ($h['name'] ?? ''));
if ($name !== '') {
$names[] = $name;
}
}
// 空白药材
if ($names === []) {
return true;
}
// 重复药材
$seen = [];
foreach ($names as $name) {
$key = strtolower(preg_replace('/\s+/', '', $name) ?? '');
if ($key === '') {
continue;
}
if (isset($seen[$key])) {
return true;
}
$seen[$key] = true;
}
return false;
}
/**
* @notes 搜索条件
*/
public function setSearch(): array
{
return [
'%like%' => ['patient_name', 'sn'],
'between_time' => 'create_time',
];
}
/** 创建人(医师账号)多选,参数 creator_ids:数组或逗号分隔 ID */
private function applyCreatorIdsFilter($query): void
{
$raw = $this->params['creator_ids'] ?? null;
if ($raw === null || $raw === '') {
return;
}
$ids = \is_array($raw) ? $raw : explode(',', (string) $raw);
$ids = array_values(array_filter(array_map('intval', $ids), static function (int $id): bool {
return $id > 0;
}));
if ($ids === []) {
return;
}
$query->whereIn('creator_id', $ids);
}
/**
* audit_filterpassed=已通过,not_passed=未通过(待审+驳回),pendingrejected
*/
private function applyAuditFilter($query): void
{
$af = (string) ($this->params['audit_filter'] ?? '');
if ($af === '' || $af === 'all') {
return;
}
switch ($af) {
case 'passed':
$query->where('audit_status', '=', 1);
break;
case 'not_passed':
$query->whereIn('audit_status', [0, 2]);
break;
case 'pending':
$query->where('audit_status', '=', 0);
break;
case 'rejected':
$query->where('audit_status', '=', 2);
break;
default:
break;
}
}
/**
* 来源:system=系统代开(is_system_auto=1),manual=手工(0 或空)
*/
private function applySourceFilter($query): void
{
$sf = (string) ($this->params['source_filter'] ?? '');
if ($sf === '' || $sf === 'all') {
return;
}
if ($sf === 'system') {
$query->where('is_system_auto', 1);
return;
}
if ($sf === 'manual') {
$query->where('is_system_auto', 0);
}
}
private function applyVisibilityScope($query): void
{
$query->where(function ($query) {
// 超级管理员可查看全部
if (!empty($this->adminInfo['root']) && (int) $this->adminInfo['root'] === 1) {
return;
}
// 检查是否属于可查看全部处方的角色
$manageAllRoles = config('project.prescription_library_manage_all_roles', [0, 3]);
$roleIds = array_values(array_unique(array_map('intval', $this->adminInfo['role_id'] ?? [])));
$canSeeAll = false;
foreach ($roleIds as $rid) {
if (in_array($rid, $manageAllRoles, true)) {
$canSeeAll = true;
break;
}
}
// 如果属于可查看全部的角色,不添加任何限制
if ($canSeeAll) {
return;
}
// 其他用户只能查看:共享的、自己创建的、自己是医助的、或指定给自己角色的
$adminId = $this->adminId;
$query->where(function ($q) use ($adminId, $roleIds) {
$q->whereOr('is_shared', '=', 1);
$q->whereOr('creator_id', '=', $adminId);
$q->whereOr('assistant_id', '=', $adminId);
foreach ($roleIds as $rid) {
if ($rid > 0) {
$q->whereOrRaw('FIND_IN_SET(?, `visible_role_ids`)', [(string) $rid]);
}
}
});
});
}
/**
* @notes 获取列表
*/
public function lists(): array
{
$query = Prescription::where($this->searchWhere);
$this->applyAuditFilter($query);
$this->applySourceFilter($query);
$this->applyVisibilityScope($query);
$this->applyCreatorIdsFilter($query);
$this->applyDataScopeByOwnerColumns($query, ['creator_id', 'assistant_id']);
// 全局置顶:有业务订单且处方药材为空白/重复
$candidateIds = (clone $query)->whereNull('delete_time')->column('id');
$riskIds = $this->collectRiskPrescriptionIds(is_array($candidateIds) ? $candidateIds : []);
$listQuery = $query->whereNull('delete_time');
if ($riskIds !== []) {
$riskIdStr = implode(',', array_map('intval', $riskIds));
$listQuery->orderRaw("CASE WHEN id IN ({$riskIdStr}) THEN 0 ELSE 1 END ASC");
}
$lists = $listQuery
->order('id', 'desc')
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
$rxIds = array_column($lists, 'id');
$rxIds = array_map('intval', $rxIds);
$rejectedRx = [];
$bizRejectRemark = [];
$hasBizOrderRx = [];
if ($rxIds !== []) {
$bizRejectRows = PrescriptionOrder::whereIn('prescription_id', $rxIds)
->where('prescription_audit_status', 2)
->whereNull('delete_time')
->where('fulfillment_status', '<>', 4)
->field(['prescription_id', 'prescription_audit_remark', 'id'])
->order('id', 'desc')
->select()
->toArray();
foreach ($bizRejectRows as $br) {
$pid = (int) ($br['prescription_id'] ?? 0);
if ($pid <= 0) {
continue;
}
$rejectedRx[$pid] = true;
if (!isset($bizRejectRemark[$pid])) {
$bizRejectRemark[$pid] = (string) ($br['prescription_audit_remark'] ?? '');
}
}
$orderRxIds = array_unique(array_map(
'intval',
PrescriptionOrder::whereIn('prescription_id', $rxIds)
->whereNull('delete_time')
->where('fulfillment_status', '<>', 4)
->column('prescription_id')
));
$hasBizOrderRx = array_fill_keys($orderRxIds, true);
}
// 获取医助姓名
$assistantIds = array_unique(array_filter(array_map('intval', array_column($lists, 'assistant_id'))));
$assistantNames = [];
if (!empty($assistantIds)) {
$assistantNames = \app\common\model\auth\Admin::whereIn('id', $assistantIds)
->column('name', 'id');
}
// 处理字段格式
foreach ($lists as &$item) {
// 设置默认值
$item['usage_time'] = $item['usage_time'] ?? '饭前';
$item['usage_way'] = $item['usage_way'] ?? '温水送服';
$item['usage_notes'] = $item['usage_notes'] ?? '';
$item['usage_days'] = $item['usage_days'] ?? 7;
$item['is_shared'] = $item['is_shared'] ?? 0;
$item['is_system_auto'] = (int) ($item['is_system_auto'] ?? 0);
$item['audit_status'] = (int) ($item['audit_status'] ?? 1);
$item['void_status'] = (int) ($item['void_status'] ?? 0);
$item['visible_role_ids'] = PrescriptionLogic::visibleRoleIdsToArray((string) ($item['visible_role_ids'] ?? ''));
$rid = (int) ($item['id'] ?? 0);
$item['business_prescription_audit_rejected'] = !empty($rejectedRx[$rid]) ? 1 : 0;
$item['business_prescription_audit_remark'] = (string) ($bizRejectRemark[$rid] ?? '');
$item['has_prescription_order'] = !empty($hasBizOrderRx[$rid]) ? 1 : 0;
// 添加医助姓名
$assistantId = (int) ($item['assistant_id'] ?? 0);
$item['assistant_name'] = $assistantId > 0 ? ($assistantNames[$assistantId] ?? '') : '';
// 将 dietary_taboo 从逗号分隔的字符串转换为数组(前端需要数组格式)
if (!empty($item['dietary_taboo']) && is_string($item['dietary_taboo'])) {
$item['dietary_taboo'] = array_filter(explode(',', $item['dietary_taboo']));
} else {
$item['dietary_taboo'] = [];
}
}
return $lists;
}
/**
* @notes 获取数量
*/
public function count(): int
{
$query = Prescription::where($this->searchWhere);
$this->applyAuditFilter($query);
$this->applySourceFilter($query);
$this->applyVisibilityScope($query);
$this->applyCreatorIdsFilter($query);
$this->applyDataScopeByOwnerColumns($query, ['creator_id', 'assistant_id']);
return $query->whereNull('delete_time')->count();
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,74 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\tools;
use app\adminapi\lists\BaseAdminDataLists;
use think\facade\Db;
/**
* 数据表列表
* Class GeneratorLists
* @package app\adminapi\lists\tools
*/
class DataTableLists extends BaseAdminDataLists
{
/**
* @notes 查询结果
* @return mixed
* @author 段誉
* @date 2022/6/13 18:54
*/
public function queryResult()
{
$sql = 'SHOW TABLE STATUS WHERE 1=1 ';
if (!empty($this->params['name'])) {
$sql .= "AND name LIKE '%" . $this->params['name'] . "%'";
}
if (!empty($this->params['comment'])) {
$sql .= "AND comment LIKE '%" . $this->params['comment'] . "%'";
}
return Db::query($sql);
}
/**
* @notes 处理列表
* @return array
* @author 段誉
* @date 2022/6/13 18:54
*/
public function lists(): array
{
$lists = array_map("array_change_key_case", $this->queryResult());
$offset = max(0, ($this->pageNo - 1) * $this->pageSize);
$lists = array_slice($lists, $offset, $this->pageSize, true);
return array_values($lists);
}
/**
* @notes 获取数量
* @return int
* @author 段誉
* @date 2022/6/13 18:54
*/
public function count(): int
{
return count($this->queryResult());
}
}
@@ -0,0 +1,75 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\tools;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\lists\ListsSearchInterface;
use app\common\model\tools\GenerateTable;
/**
* 代码生成所选数据表列表
* Class GenerateTableLists
* @package app\adminapi\lists\tools
*/
class GenerateTableLists extends BaseAdminDataLists implements ListsSearchInterface
{
/**
* @notes 设置搜索条件
* @return \string[][]
* @author 段誉
* @date 2022/6/14 10:55
*/
public function setSearch(): array
{
return [
'%like%' => ['table_name', 'table_comment']
];
}
/**
* @notes 查询列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/6/14 10:55
*/
public function lists(): array
{
return GenerateTable::where($this->searchWhere)
->order(['id' => 'desc'])
->append(['template_type_desc'])
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
}
/**
* @notes 获取数量
* @return int
* @author 段誉
* @date 2022/6/14 10:55
*/
public function count(): int
{
return GenerateTable::count();
}
}
@@ -0,0 +1,111 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\lists\user;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\enum\user\UserTerminalEnum;
use app\common\lists\ListsExcelInterface;
use app\common\model\user\User;
/**
* 用户列表
* Class UserLists
* @package app\adminapi\lists\user
*/
class UserLists extends BaseAdminDataLists implements ListsExcelInterface
{
/**
* @notes 搜索条件
* @return array
* @author 段誉
* @date 2022/9/22 15:50
*/
public function setSearch(): array
{
$allowSearch = ['keyword', 'channel', 'create_time_start', 'create_time_end'];
return array_intersect(array_keys($this->params), $allowSearch);
}
/**
* @notes 获取用户列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/9/22 15:50
*/
public function lists(): array
{
$field = "id,sn,nickname,sex,avatar,account,mobile,channel,create_time";
$lists = User::withSearch($this->setSearch(), $this->params)
->limit($this->limitOffset, $this->limitLength)
->field($field)
->order('id desc')
->select()->toArray();
foreach ($lists as &$item) {
$item['channel'] = UserTerminalEnum::getTermInalDesc($item['channel']);
}
return $lists;
}
/**
* @notes 获取数量
* @return int
* @author 段誉
* @date 2022/9/22 15:51
*/
public function count(): int
{
return User::withSearch($this->setSearch(), $this->params)->count();
}
/**
* @notes 导出文件名
* @return string
* @author 段誉
* @date 2022/11/24 16:17
*/
public function setFileName(): string
{
return '用户列表';
}
/**
* @notes 导出字段
* @return string[]
* @author 段誉
* @date 2022/11/24 16:17
*/
public function setExcelFields(): array
{
return [
'sn' => '用户编号',
'nickname' => '用户昵称',
'account' => '账号',
'mobile' => '手机号码',
'channel' => '注册来源',
'create_time' => '注册时间',
];
}
}