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,21 @@
<?php
declare(strict_types=1);
namespace app\adminapi\controller\stats;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\logic\stats\AssistantPerformanceLogic;
/**
* 医助个人业绩
*
* - GET stats.assistantPerformance/overview 个人业绩概览
*/
class AssistantPerformanceController extends BaseAdminController
{
public function overview()
{
$params = $this->request->get();
return $this->data(AssistantPerformanceLogic::overview($params, $this->adminId, $this->adminInfo));
}
}
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace app\adminapi\controller\stats;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\lists\stats\AutoAssignLogLists;
use app\adminapi\logic\stats\AutoAssignLogLogic;
/**
* 待分配诊单自动指派日志
*
* - GET stats.autoAssignLog/lists 日志列表
* - POST stats.autoAssignLog/rollback 批量回退已自动分配的医助
*/
class AutoAssignLogController extends BaseAdminController
{
public function lists()
{
return $this->dataLists(new AutoAssignLogLists());
}
public function rollback()
{
$ids = $this->request->post('ids', []);
if (!is_array($ids)) {
$ids = [$ids];
}
$result = AutoAssignLogLogic::rollback($ids, $this->adminId, $this->adminInfo);
if ($result === false) {
return $this->fail(AutoAssignLogLogic::getError());
}
$msg = sprintf('回退成功 %d 条', (int) ($result['success'] ?? 0));
$failed = (int) ($result['failed'] ?? 0);
if ($failed > 0) {
$msg .= sprintf(',跳过/失败 %d 条', $failed);
}
// show=0:由前端按 success/failed 明细提示,避免与部分失败警告重复
return $this->success($msg, $result, 1, 0);
}
}
@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace app\adminapi\controller\stats;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\logic\stats\YejiStatsLogic;
/**
* 提成结算业绩(独立于业绩看板 yejiStats 接口)
*
* - GET stats.commissionSettlement/overview
* - GET stats.commissionSettlement/orderLines
* - GET stats.commissionSettlement/confirmStatus
* - POST stats.commissionSettlement/saveReconcile
* - POST stats.commissionSettlement/confirmFinalize
* - POST stats.commissionSettlement/confirmRevoke
* - GET stats.commissionSettlement/deptOptions
* - GET stats.commissionSettlement/channelOptions
*/
class CommissionSettlementController extends BaseAdminController
{
public function overview()
{
@set_time_limit(120);
$params = $this->request->get();
return $this->data(YejiStatsLogic::commissionSettlementOverview($params, $this->adminId, $this->adminInfo));
}
public function orderLines()
{
@set_time_limit(120);
return $this->data(YejiStatsLogic::commissionSettlementOrderLines($this->request->get(), $this->adminId, $this->adminInfo));
}
public function confirmStatus()
{
return $this->data(YejiStatsLogic::commissionSettlementConfirmStatus($this->request->get(), $this->adminId, $this->adminInfo));
}
public function saveReconcile()
{
return $this->data(YejiStatsLogic::commissionSettlementSaveReconcile($this->request->post(), $this->adminId, $this->adminInfo));
}
public function confirmFinalize()
{
@set_time_limit(120);
return $this->data(YejiStatsLogic::commissionSettlementConfirmFinalize($this->request->post(), $this->adminId, $this->adminInfo));
}
public function confirmRevoke()
{
return $this->data(YejiStatsLogic::commissionSettlementConfirmRevoke($this->request->post(), $this->adminId, $this->adminInfo));
}
public function deptOptions()
{
return $this->data(YejiStatsLogic::deptOptions($this->adminId, $this->adminInfo));
}
public function channelOptions()
{
return $this->data(YejiStatsLogic::channelOptions(false));
}
}
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace app\adminapi\controller\stats;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\logic\stats\ConversionLogic;
class ConversionController extends BaseAdminController
{
public function overview()
{
$result = ConversionLogic::overview($this->request->get(), $this->adminId, $this->adminInfo);
return $this->data($result);
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace app\adminapi\controller\stats;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\logic\stats\DoctorDailyStatsLogic;
/**
* 医生日统计(系统/手动开方、成交、挂号状态、总挂号、挂号率)
*
* - GET stats.doctorDailyStats/overview
* ?start_date=&end_date=&channel_code=&dept_ids=
* dept_ids 可传逗号串或数组;选中父级会自动展开全部子级(与业绩看板部门树同口径)。
* 筛选口径:部门 → admin_dept 命中的医助集合 → 该医助经手的挂号(COALESCE(a.assistant_id,u.assistant_id)/
* 订单(o.creator_id/ 处方(tcm_diagnosis.assistant_id),再按医生(rx.creator_id / a.doctor_id)汇总;
* 显式选部门时隐藏全 0 医生行。未传日期时由后端默认当日。
*/
class DoctorDailyStatsController extends BaseAdminController
{
public function overview()
{
$params = $this->request->get();
return $this->data(DoctorDailyStatsLogic::overview($params, $this->adminId, $this->adminInfo));
}
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace app\adminapi\controller\stats;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\logic\stats\PerformanceDashboardLogic;
/**
* 角色数据驾驶舱。
*
* 所有数据在服务端按当前管理员的数据范围聚合,前端不参与权限裁剪。
*/
class PerformanceDashboardController extends BaseAdminController
{
public function overview()
{
@set_time_limit(120);
$response = $this->data(PerformanceDashboardLogic::overview(
$this->adminId,
$this->adminInfo,
$this->request->get()
));
// 驾驶舱包含分钟级实时数据,禁止浏览器和中间代理缓存旧统计结果。
return $response->header([
'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0',
'Pragma' => 'no-cache',
'Expires' => '0',
]);
}
}
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace app\adminapi\controller\stats;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\lists\stats\PersonalAccountCostLists;
use app\adminapi\logic\stats\PersonalAccountCostLogic;
use app\adminapi\validate\stats\PersonalAccountCostValidate;
class PersonalAccountCostController extends BaseAdminController
{
public function lists()
{
return $this->dataLists(new PersonalAccountCostLists());
}
public function add()
{
$params = (new PersonalAccountCostValidate())->post()->goCheck('add');
$result = PersonalAccountCostLogic::add($params, $this->adminId, (string) ($this->adminInfo['name'] ?? ''));
if ($result === false) {
return $this->fail(PersonalAccountCostLogic::getError());
}
return $this->success('添加成功', [], 1, 1);
}
public function edit()
{
$params = (new PersonalAccountCostValidate())->post()->goCheck('edit');
$result = PersonalAccountCostLogic::edit($params, $this->adminId, (string) ($this->adminInfo['name'] ?? ''), $this->adminInfo);
if ($result === false) {
return $this->fail(PersonalAccountCostLogic::getError());
}
return $this->success('编辑成功', [], 1, 1);
}
public function detail()
{
$params = (new PersonalAccountCostValidate())->goCheck('detail');
$detail = PersonalAccountCostLogic::detail((int) $params['id'], $this->adminId, $this->adminInfo);
if ($detail === []) {
return $this->fail('记录不存在或无权查看');
}
return $this->data($detail);
}
public function delete()
{
$params = (new PersonalAccountCostValidate())->post()->goCheck('delete');
$result = PersonalAccountCostLogic::delete((int) $params['id'], $this->adminId, $this->adminInfo);
if ($result === false) {
return $this->fail(PersonalAccountCostLogic::getError());
}
return $this->success('删除成功', [], 1, 1);
}
}
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace app\adminapi\controller\stats;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\lists\stats\PersonalYejiLists;
use app\adminapi\logic\stats\PersonalYejiLogic;
use app\adminapi\validate\stats\PersonalYejiValidate;
class PersonalYejiController extends BaseAdminController
{
public function lists()
{
return $this->dataLists(new PersonalYejiLists());
}
public function add()
{
$params = (new PersonalYejiValidate())->post()->goCheck('add');
$result = PersonalYejiLogic::add($params, $this->adminId, (string) ($this->adminInfo['name'] ?? ''));
if ($result === false) {
return $this->fail(PersonalYejiLogic::getError());
}
return $this->success('添加成功', [], 1, 1);
}
public function edit()
{
$params = (new PersonalYejiValidate())->post()->goCheck('edit');
$result = PersonalYejiLogic::edit($params, $this->adminId, (string) ($this->adminInfo['name'] ?? ''), $this->adminInfo);
if ($result === false) {
return $this->fail(PersonalYejiLogic::getError());
}
return $this->success('编辑成功', [], 1, 1);
}
public function detail()
{
$params = (new PersonalYejiValidate())->goCheck('detail');
$detail = PersonalYejiLogic::detail((int) $params['id'], $this->adminId, $this->adminInfo);
if ($detail === []) {
return $this->fail('记录不存在或无权查看');
}
return $this->data($detail);
}
public function delete()
{
$params = (new PersonalYejiValidate())->post()->goCheck('delete');
$result = PersonalYejiLogic::delete((int) $params['id'], $this->adminId, $this->adminInfo);
if ($result === false) {
return $this->fail(PersonalYejiLogic::getError());
}
return $this->success('删除成功', [], 1, 1);
}
}
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace app\adminapi\controller\stats;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\logic\stats\RevisitRateLogic;
/**
* 复诊接诊率统计(按月)
*
* - GET stats.revisitRate/overview 当月 N 诊接诊率(部门 → 医助分组 + 合计)
* - GET stats.revisitRate/deptOptions 部门下拉(前端组树)
* - GET stats.revisitRate/assignLines 「被指派数」明细(按诊单聚合)
* - GET stats.revisitRate/visitOrderLines 「N 诊单数」订单明细
*/
class RevisitRateController extends BaseAdminController
{
public function overview()
{
// 涉及指派日志 + 全量订单序列扫描,放宽执行时间兜底
@set_time_limit(120);
$params = $this->request->get();
return $this->data(RevisitRateLogic::overview($params));
}
public function deptOptions()
{
return $this->data(RevisitRateLogic::deptOptions());
}
/** 「被指派数」点击下钻:诊单维度明细 */
public function assignLines()
{
@set_time_limit(120);
$params = $this->request->get();
return $this->data(RevisitRateLogic::assignLines($params));
}
/** 「N 诊单数」点击下钻:具体订单明细 */
public function visitOrderLines()
{
@set_time_limit(120);
$params = $this->request->get();
return $this->data(RevisitRateLogic::visitOrderLines($params));
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace app\adminapi\controller\stats;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\logic\stats\SelfInputLogic;
class SelfInputController extends BaseAdminController
{
public function overview()
{
$result = SelfInputLogic::overview($this->request->get(), $this->adminId, $this->adminInfo);
return $this->data($result);
}
/**
* 自媒体来源下拉:从已录入业绩/账户消耗去重提取(随录入动态变化)
*/
public function mediaSourceOptions()
{
$list = SelfInputLogic::mediaSourceOptions($this->adminId, $this->adminInfo);
return $this->data($list);
}
}
@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace app\adminapi\controller\stats;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\logic\stats\YejiStatsLogic;
/**
* 业绩看板(部门 × 时间区间)控制器
*
* - GET stats.yejiStats/overview 单区间
* - GET stats.yejiStats/multi 多区间一次返回(默认 月/周/今日/昨日 四张表)
* - GET stats.yejiStats/deptOptions 部门下拉
* - GET stats.yejiStats/leaderboard 医助排行榜(按展示部门分表)
* - GET stats.yejiStats/leadLines 进线数据明细(add_external_contact 逐条)
* - GET stats.yejiStats/appointmentLines 医助排行榜「预约诊单」挂号逐条明细
* - GET stats.yejiStats/revisitBreakdown 二中心复诊下钻:医助 × 业务订单笔数
* - GET stats.yejiStats/assignLines 被指派数明细(医助×诊单去重,与看板同口径)
*/
class YejiStatsController extends BaseAdminController
{
public function overview()
{
// 业绩看板涉及多张大表 + 标签穿透计算,单 30s 上限不够;放宽到 120s 兜底
@set_time_limit(120);
$params = $this->request->get();
return $this->data(YejiStatsLogic::overview($params, $this->adminId, $this->adminInfo));
}
/**
* 一次返回 4 个时间区间的数据,对应前端 4 张表。
* 默认区间(不传 ranges 时):当月、当周(周一→今天)、今日、昨日。
* ranges 也可由前端自定义传入:[{label:..., start_date:..., end_date:...}]
*/
public function multi()
{
// 4 个区间叠加,30s 上限对部分线上数据量来说过紧,放宽到 120s
@set_time_limit(120);
$params = $this->request->get();
$rangesRaw = $params['ranges'] ?? null;
$today = date('Y-m-d');
$monthStart = date('Y-m-01');
// 周一为起点(PHP date('N') 1=周一)
$weekStart = date('Y-m-d', strtotime('monday this week'));
$yesterday = date('Y-m-d', strtotime('-1 day'));
$ranges = [];
if (is_array($rangesRaw) && $rangesRaw !== []) {
foreach ($rangesRaw as $r) {
if (!is_array($r)) {
continue;
}
$ranges[] = [
'label' => (string) ($r['label'] ?? ''),
'start_date' => (string) ($r['start_date'] ?? $today),
'end_date' => (string) ($r['end_date'] ?? ($r['start_date'] ?? $today)),
];
}
}
if ($ranges === []) {
$ranges = [
['label' => '本月(' . date('n') . '月)', 'start_date' => $monthStart, 'end_date' => $today],
['label' => '本周(' . substr($weekStart, 5) . '~' . substr($today, 5) . '', 'start_date' => $weekStart, 'end_date' => $today],
['label' => '今日(' . substr($today, 5) . '', 'start_date' => $today, 'end_date' => $today],
['label' => '昨日(' . substr($yesterday, 5) . '', 'start_date' => $yesterday, 'end_date' => $yesterday],
];
}
$base = [];
if (isset($params['dept_ids'])) {
$base['dept_ids'] = $params['dept_ids'];
}
if (isset($params['tag_id'])) {
$base['tag_id'] = $params['tag_id'];
}
if (isset($params['channel_code'])) {
$base['channel_code'] = $params['channel_code'];
}
return $this->data([
'ranges' => $ranges,
'tables' => YejiStatsLogic::overviewBatch($ranges, $base, $this->adminId, $this->adminInfo),
]);
}
public function deptOptions()
{
return $this->data(YejiStatsLogic::deptOptions($this->adminId, $this->adminInfo));
}
/** 渠道(标签)下拉,按 source_group_name 分组 */
public function channelOptions()
{
return $this->data(YejiStatsLogic::channelOptions());
}
/** 医助排行榜:与 overview 同筛选;日期由前端传入(多表模式下前端传「今日」单区间) */
public function leaderboard()
{
@set_time_limit(120);
$params = $this->request->get();
return $this->data(YejiStatsLogic::assistantLeaderboards($params, $this->adminId, $this->adminInfo));
}
/** 「未归属中心」行:按诊单医助拆解业绩(与表格补差、合计业绩诊单医助归属一致) */
public function unassignedBreakdown()
{
@set_time_limit(120);
$params = $this->request->get();
return $this->data(YejiStatsLogic::unassignedCenterBreakdown($params, $this->adminId, $this->adminInfo));
}
/** 进线数据明细:与看板「进线数据」同口径 */
public function leadLines()
{
@set_time_limit(120);
$params = $this->request->get();
return $this->data(YejiStatsLogic::leadLineList($params, $this->adminId, $this->adminInfo));
}
/** 医助排行榜「预约诊单」:挂号记录逐条列表(与计数同口径) */
public function appointmentLines()
{
@set_time_limit(120);
$params = $this->request->get();
return $this->data(YejiStatsLogic::leaderboardAppointmentLines($params, $this->adminId, $this->adminInfo));
}
/** 二中心复诊:按部门行 + 复诊分项拆解医助与业务订单笔数 */
public function revisitBreakdown()
{
@set_time_limit(120);
$params = $this->request->get();
return $this->data(YejiStatsLogic::revisitDeptAssistantBreakdown($params, $this->adminId, $this->adminInfo));
}
/** 被指派数明细:与看板「被指派数」同口径(区间内非继承指派,医助×诊单去重) */
public function assignLines()
{
@set_time_limit(120);
$params = $this->request->get();
return $this->data(YejiStatsLogic::assignLines($params, $this->adminId, $this->adminInfo));
}
}