Merge branch 'master' of https://gitee.com/v_cms/zyt
This commit is contained in:
@@ -9,6 +9,8 @@ use app\common\service\doctor\RosterSegmentService;
|
||||
use app\common\model\tcm\Diagnosis;
|
||||
use app\common\model\tcm\DiagnosisGuahaoLog;
|
||||
use app\common\model\auth\Admin;
|
||||
use app\common\model\auth\AdminRole;
|
||||
use app\common\service\DataScope\DataScopeService;
|
||||
use app\api\logic\ChatNotifyLogic;
|
||||
use app\adminapi\logic\tcm\PrescriptionLogic;
|
||||
use app\adminapi\logic\tcm\DiagnosisLogic;
|
||||
@@ -42,6 +44,61 @@ class AppointmentLogic extends BaseLogic
|
||||
return in_array($name, self::CHANNEL_NAMES_REQUIRING_SOURCE_DETAIL, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|string, mixed> $cols Db::name('doctor_appointment')->getTableFields()
|
||||
*/
|
||||
private static function assertAppointmentChannelWritable(array $cols, string $chSrc): ?string
|
||||
{
|
||||
$hasChannelSource = in_array('channel_source', $cols, true);
|
||||
$hasChannels = in_array('channels', $cols, true);
|
||||
if (!$hasChannelSource && !$hasChannels) {
|
||||
return '挂号表缺少渠道字段(channel_source 或 channels),无法保存渠道来源';
|
||||
}
|
||||
if (!$hasChannelSource && $hasChannels && $chSrc !== '' && !is_numeric($chSrc)) {
|
||||
return '当前挂号表仅有数值型渠道字段 channels,无法写入该渠道;请在数据库增加 channel_source 列';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
* @param array<int|string, mixed> $cols
|
||||
*/
|
||||
private static function mergeAppointmentChannelIntoRow(array &$row, array $cols, string $chSrc, string $chDetail): void
|
||||
{
|
||||
$hasChannelSource = in_array('channel_source', $cols, true);
|
||||
$hasChannelDetail = in_array('channel_source_detail', $cols, true);
|
||||
$hasChannels = in_array('channels', $cols, true);
|
||||
if ($hasChannelSource) {
|
||||
$row['channel_source'] = $chSrc;
|
||||
}
|
||||
if ($hasChannelDetail) {
|
||||
$row['channel_source_detail'] = $chDetail;
|
||||
}
|
||||
if ($hasChannels && is_numeric($chSrc)) {
|
||||
$row['channels'] = (int) $chSrc;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
* @param array<int|string, mixed> $cols
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function filterAppointmentRowByExistingColumns(array $row, array $cols): array
|
||||
{
|
||||
$out = [];
|
||||
foreach ($row as $k => $v) {
|
||||
if (in_array((string) $k, $cols, true)) {
|
||||
$out[$k] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取可用时间段
|
||||
* @param array $params
|
||||
@@ -262,25 +319,49 @@ class AppointmentLogic extends BaseLogic
|
||||
}
|
||||
}
|
||||
|
||||
$data = [
|
||||
'patient_id' => $params['patient_id'],
|
||||
'assistant_id'=>$params['assistant_id'],
|
||||
'doctor_id' => $params['doctor_id'],
|
||||
'roster_id' => 0, // 不再关联排班表
|
||||
$cols = Db::name('doctor_appointment')->getTableFields();
|
||||
$cols = is_array($cols) ? $cols : [];
|
||||
$channelErr = self::assertAppointmentChannelWritable($cols, $chSrc);
|
||||
if ($channelErr !== null) {
|
||||
self::setError($channelErr);
|
||||
Db::rollback();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$insert = [
|
||||
'patient_id' => (int) $params['patient_id'],
|
||||
'doctor_id' => (int) $params['doctor_id'],
|
||||
'appointment_date' => $params['appointment_date'],
|
||||
'period' => $params['period'] ?? 'all',
|
||||
'appointment_time' => $appointmentTime,
|
||||
'appointment_type' => $params['appointment_type'] ?? 'video',
|
||||
'remark' => $params['remark'] ?? '',
|
||||
// 表字段为 channel_source(与字典 type_value=channels 的 value 一致);勿再写入不存在的 channels 列
|
||||
'channel_source' => $chSrc,
|
||||
'channel_source_detail' => $chDetail,
|
||||
'status' => 1,
|
||||
'create_time' => time(),
|
||||
'update_time' => time(),
|
||||
];
|
||||
if (in_array('roster_id', $cols, true)) {
|
||||
$insert['roster_id'] = 0;
|
||||
}
|
||||
if (in_array('assistant_id', $cols, true)) {
|
||||
$insert['assistant_id'] = (int) ($params['assistant_id'] ?? 0);
|
||||
}
|
||||
$periodVal = $params['period'] ?? 'all';
|
||||
if (in_array('period', $cols, true)) {
|
||||
$insert['period'] = $periodVal;
|
||||
} elseif (in_array('type', $cols, true)) {
|
||||
$insert['type'] = $periodVal;
|
||||
}
|
||||
self::mergeAppointmentChannelIntoRow($insert, $cols, $chSrc, $chDetail);
|
||||
|
||||
$appointment = Appointment::create($data);
|
||||
$insert = self::filterAppointmentRowByExistingColumns($insert, $cols);
|
||||
$newId = (int) Db::name('doctor_appointment')->insertGetId($insert);
|
||||
if ($newId <= 0) {
|
||||
self::setError('预约创建失败');
|
||||
Db::rollback();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
|
||||
@@ -288,21 +369,21 @@ class AppointmentLogic extends BaseLogic
|
||||
$hm = substr((string) $appointmentTime, 0, 5);
|
||||
$summary = sprintf(
|
||||
'挂号 #%d:医生「%s」,%s %s',
|
||||
(int) $appointment->id,
|
||||
$newId,
|
||||
$doctorName !== '' ? $doctorName : ('ID:' . (int) $params['doctor_id']),
|
||||
(string) $params['appointment_date'],
|
||||
$hm
|
||||
);
|
||||
self::appendDiagnosisGuahaoLog(
|
||||
(int) $params['patient_id'],
|
||||
(int) $appointment->id,
|
||||
$newId,
|
||||
$operatorAdminId,
|
||||
$operatorAdminInfo,
|
||||
'create',
|
||||
$summary
|
||||
);
|
||||
|
||||
return ['id' => $appointment->id];
|
||||
return ['id' => $newId];
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
self::setError($e->getMessage());
|
||||
@@ -787,6 +868,244 @@ class AppointmentLogic extends BaseLogic
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 与 AppointmentLists 一致的可见性(不含 progress_board / diag_scope_relax)
|
||||
*/
|
||||
private static function appointmentRowManageableByAdmin(
|
||||
Appointment $appointment,
|
||||
?Diagnosis $diag,
|
||||
int $adminId,
|
||||
array $adminInfo
|
||||
): bool {
|
||||
$roleIds = array_map('intval', AdminRole::where('admin_id', $adminId)->column('role_id'));
|
||||
|
||||
if (in_array(1, $roleIds, true) && (int) $appointment->doctor_id !== $adminId) {
|
||||
return false;
|
||||
}
|
||||
if (in_array(2, $roleIds, true)) {
|
||||
$asst = $diag ? (int) $diag->assistant_id : 0;
|
||||
if ($asst !== $adminId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!DataScopeService::isEnabled()) {
|
||||
return true;
|
||||
}
|
||||
$ids = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
|
||||
if ($ids === []) {
|
||||
return false;
|
||||
}
|
||||
if ($ids === null) {
|
||||
return true;
|
||||
}
|
||||
$docId = (int) $appointment->doctor_id;
|
||||
$asstId = $diag ? (int) $diag->assistant_id : 0;
|
||||
|
||||
return in_array($docId, $ids, true)
|
||||
|| ($asstId > 0 && in_array($asstId, $ids, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台编辑挂号(预约日期/时段/类型/状态/备注/医助)
|
||||
*
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
public static function adminEdit(array $params, int $adminId, array $adminInfo): bool
|
||||
{
|
||||
try {
|
||||
$id = (int) ($params['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
self::setError('参数错误');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$appointment = Appointment::findOrEmpty($id);
|
||||
if ($appointment->isEmpty()) {
|
||||
self::setError('预约记录不存在');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$diag = Diagnosis::where('id', (int) $appointment->patient_id)->whereNull('delete_time')->find();
|
||||
|
||||
if (!self::appointmentRowManageableByAdmin($appointment, $diag ?: null, $adminId, $adminInfo)) {
|
||||
self::setError('无权限编辑');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$period = trim((string) ($params['period'] ?? ''));
|
||||
if (!in_array($period, ['morning', 'afternoon', 'all'], true)) {
|
||||
self::setError('时段无效');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$appointmentDate = trim((string) ($params['appointment_date'] ?? ''));
|
||||
if ($appointmentDate === '') {
|
||||
self::setError('预约日期不能为空');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$rawTime = trim((string) ($params['appointment_time'] ?? ''));
|
||||
if ($rawTime === '') {
|
||||
self::setError('预约时间不能为空');
|
||||
|
||||
return false;
|
||||
}
|
||||
$appointmentTime = strlen($rawTime) === 5 ? $rawTime . ':00' : $rawTime;
|
||||
|
||||
$status = (int) ($params['status'] ?? 0);
|
||||
if ($status < 1 || $status > 4) {
|
||||
self::setError('状态无效');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$appointmentType = trim((string) ($params['appointment_type'] ?? ''));
|
||||
if (!in_array($appointmentType, ['video', 'text', 'phone'], true)) {
|
||||
self::setError('预约类型无效');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$remark = isset($params['remark']) ? trim((string) $params['remark']) : '';
|
||||
if (mb_strlen($remark) > 500) {
|
||||
self::setError('备注过长');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$chSrc = trim((string) ($params['channel_source'] ?? ''));
|
||||
if ($chSrc === '') {
|
||||
self::setError('请选择渠道来源');
|
||||
|
||||
return false;
|
||||
}
|
||||
$chDetail = trim((string) ($params['channel_source_detail'] ?? ''));
|
||||
$channelDictName = trim((string) (DictData::where('type_value', 'channels')
|
||||
->where('value', $chSrc)
|
||||
->value('name') ?? ''));
|
||||
if ($channelDictName !== '' && self::channelDictNameRequiresSourceDetail($channelDictName)) {
|
||||
if ($chDetail === '') {
|
||||
self::setError('请填写自媒体渠道补充信息');
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (mb_strlen($chDetail) > 128) {
|
||||
self::setError('渠道补充说明过长');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$postAssistant = array_key_exists('assistant_id', $params);
|
||||
$assistantId = null;
|
||||
if ($postAssistant) {
|
||||
$assistantRaw = $params['assistant_id'];
|
||||
if ($assistantRaw !== null && $assistantRaw !== '') {
|
||||
$aid = (int) $assistantRaw;
|
||||
$assistantId = $aid > 0 ? $aid : null;
|
||||
}
|
||||
}
|
||||
|
||||
$patientId = (int) $appointment->patient_id;
|
||||
$doctorId = (int) $appointment->doctor_id;
|
||||
|
||||
if (in_array($status, [1, 4], true)) {
|
||||
$patientDup = Appointment::where('patient_id', $patientId)
|
||||
->where('appointment_date', $appointmentDate)
|
||||
->whereIn('status', [1, 4])
|
||||
->where('id', '<>', $id)
|
||||
->find();
|
||||
if ($patientDup) {
|
||||
self::setError('该诊单在所选日期已有其他有效挂号(已预约/已过号)');
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($status === 1) {
|
||||
$slotDup = Appointment::where('doctor_id', $doctorId)
|
||||
->where('appointment_date', $appointmentDate)
|
||||
->where('appointment_time', $appointmentTime)
|
||||
->where('status', 1)
|
||||
->where('id', '<>', $id)
|
||||
->find();
|
||||
if ($slotDup) {
|
||||
self::setError('该医生在所选时段已被占用');
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$beforeDate = (string) ($appointment->appointment_date ?? '');
|
||||
$beforeTimeRaw = (string) ($appointment->appointment_time ?? '');
|
||||
$beforeHm = strlen($beforeTimeRaw) >= 5 ? substr($beforeTimeRaw, 0, 5) : $beforeTimeRaw;
|
||||
$snap = $appointment->toArray();
|
||||
$beforePeriod = (string) ($snap['period'] ?? $snap['type'] ?? '');
|
||||
$beforeStatus = (int) $appointment->status;
|
||||
|
||||
$tblFields = Db::name('doctor_appointment')->getTableFields();
|
||||
$cols = is_array($tblFields) ? $tblFields : [];
|
||||
$channelErr = self::assertAppointmentChannelWritable($cols, $chSrc);
|
||||
if ($channelErr !== null) {
|
||||
self::setError($channelErr);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Db::startTrans();
|
||||
|
||||
$saveData = [
|
||||
'appointment_date' => $appointmentDate,
|
||||
'appointment_time' => $appointmentTime,
|
||||
'appointment_type' => $appointmentType,
|
||||
'status' => $status,
|
||||
'remark' => $remark,
|
||||
'update_time' => time(),
|
||||
];
|
||||
if (in_array('assistant_id', $cols, true) && $postAssistant) {
|
||||
$saveData['assistant_id'] = $assistantId;
|
||||
}
|
||||
if (in_array('period', $cols, true)) {
|
||||
$saveData['period'] = $period;
|
||||
} elseif (in_array('type', $cols, true)) {
|
||||
$saveData['type'] = $period;
|
||||
}
|
||||
self::mergeAppointmentChannelIntoRow($saveData, $cols, $chSrc, $chDetail);
|
||||
|
||||
Db::name('doctor_appointment')->where('id', $id)->update($saveData);
|
||||
|
||||
Db::commit();
|
||||
|
||||
$hm = substr($appointmentTime, 0, 5);
|
||||
$summary = sprintf(
|
||||
'后台修改挂号 #%d:日期 %s %s→%s %s,时段 %s→%s,状态 %d→%d',
|
||||
$id,
|
||||
$beforeDate,
|
||||
$beforeHm,
|
||||
$appointmentDate,
|
||||
$hm,
|
||||
$beforePeriod !== '' ? $beforePeriod : '—',
|
||||
$period,
|
||||
$beforeStatus,
|
||||
$status
|
||||
);
|
||||
self::appendDiagnosisGuahaoLog($patientId, $id, $adminId, $adminInfo, 'admin_edit', $summary);
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
self::setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 诊单列表维度:记录谁在后台发起挂号 / 取消挂号(写入失败不影响主流程)
|
||||
*/
|
||||
|
||||
@@ -5,12 +5,15 @@ declare(strict_types=1);
|
||||
namespace app\adminapi\logic\stats;
|
||||
|
||||
use app\adminapi\logic\dept\DeptLogic;
|
||||
use app\adminapi\logic\tcm\PrescriptionOrderLogic;
|
||||
use app\common\model\auth\AdminDept;
|
||||
use app\common\model\tcm\Diagnosis;
|
||||
use app\common\model\tcm\PrescriptionOrder;
|
||||
use app\common\service\DataScope\DataScopeService;
|
||||
use app\common\service\ExpressTrackingService;
|
||||
use app\common\service\qywx\MediaChannelService;
|
||||
use think\db\Query;
|
||||
use think\facade\Config;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
@@ -183,21 +186,95 @@ class YejiStatsLogic
|
||||
}
|
||||
}
|
||||
$c['adminToPrimary'] = $filtered;
|
||||
$allowedPrimaries = array_values(array_unique(array_values($filtered)));
|
||||
if ($allowedPrimaries === []) {
|
||||
$primariesFiltered = array_values(array_unique(array_filter(array_values($filtered), static fn (int $v): bool => $v > 0)));
|
||||
if ($primariesFiltered === []) {
|
||||
$c['tableRowDeptIds'] = [];
|
||||
|
||||
return;
|
||||
}
|
||||
$tableRowDeptIdsSrc = array_values(array_map('intval', $c['tableRowDeptIds'] ?? []));
|
||||
$deptById = isset($c['deptById']) && \is_array($c['deptById']) ? $c['deptById'] : [];
|
||||
$visibleAdminIds = array_map('intval', array_keys($filtered));
|
||||
|
||||
/** 与折叠业绩到表格行同源:SELF/本部门 等场景下台账锚点为父「中心」、表格仅有子行为时不能用 id 精确交集 */
|
||||
$keepFlip = [];
|
||||
if ($tableRowDeptIdsSrc !== [] && $deptById !== [] && $visibleAdminIds !== []) {
|
||||
$mapped = self::batchMapPerformanceAdminsToTableDept($visibleAdminIds, $tableRowDeptIdsSrc, $deptById);
|
||||
foreach ($mapped as $__tid) {
|
||||
$t = (int) $__tid;
|
||||
if ($t > 0) {
|
||||
$keepFlip[$t] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($keepFlip === [] && $tableRowDeptIdsSrc !== [] && $deptById !== []) {
|
||||
foreach ($primariesFiltered as $p) {
|
||||
foreach ($tableRowDeptIdsSrc as $tid) {
|
||||
if ($tid <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (self::yejiDeptOnPathBetween($tid, $p, $deptById)) {
|
||||
$keepFlip[$tid] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($keepFlip === []) {
|
||||
$c['tableRowDeptIds'] = [];
|
||||
|
||||
return;
|
||||
}
|
||||
$allowFlip = array_flip($allowedPrimaries);
|
||||
$c['tableRowDeptIds'] = array_values(array_filter(
|
||||
$c['tableRowDeptIds'],
|
||||
static function (int $did) use ($allowFlip): bool {
|
||||
return $did > 0 && isset($allowFlip[$did]);
|
||||
$tableRowDeptIdsSrc,
|
||||
static function (int $did) use ($keepFlip): bool {
|
||||
return $did > 0 && isset($keepFlip[$did]);
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 任一部门是否为另一部门的上级(ancestor 祖先、descendant 沿 pid 上移可遇到 ancestor)。
|
||||
*/
|
||||
private static function yejiDeptIsAncestorOfDept(int $ancestorId, int $descendantId, array $deptById): bool
|
||||
{
|
||||
if ($ancestorId <= 0 || $descendantId <= 0) {
|
||||
return false;
|
||||
}
|
||||
$cur = $descendantId;
|
||||
for ($i = 0; $i < 64; $i++) {
|
||||
if ($cur === $ancestorId) {
|
||||
return true;
|
||||
}
|
||||
if (!isset($deptById[$cur])) {
|
||||
return false;
|
||||
}
|
||||
$cur = (int) ($deptById[$cur]['pid'] ?? 0);
|
||||
if ($cur <= 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格行 dept 与台账 primary 是否应对齐到同一 subtree(任一为另一祖先)。
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $deptById
|
||||
*/
|
||||
private static function yejiDeptOnPathBetween(int $tableRowDeptId, int $ledgerPrimaryDeptId, array $deptById): bool
|
||||
{
|
||||
if ($tableRowDeptId <= 0 || $ledgerPrimaryDeptId <= 0) {
|
||||
return false;
|
||||
}
|
||||
if ($tableRowDeptId === $ledgerPrimaryDeptId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return self::yejiDeptIsAncestorOfDept($tableRowDeptId, $ledgerPrimaryDeptId, $deptById)
|
||||
|| self::yejiDeptIsAncestorOfDept($ledgerPrimaryDeptId, $tableRowDeptId, $deptById);
|
||||
}
|
||||
|
||||
/**
|
||||
* 与业绩看板相同的日期、部门树、渠道、标签与数据范围上下文,供医生日统计等复用。
|
||||
*
|
||||
@@ -5043,15 +5120,22 @@ class YejiStatsLogic
|
||||
return null;
|
||||
}
|
||||
$r = is_array($row) ? $row : $row->toArray();
|
||||
$st = (int) ($r['status'] ?? 0);
|
||||
|
||||
return [
|
||||
'status' => (int) ($r['status'] ?? 0),
|
||||
'status' => $st,
|
||||
'reconcile_note' => (string) ($r['reconcile_note'] ?? ''),
|
||||
'confirmed_admin_id' => (int) ($r['confirmed_admin_id'] ?? 0),
|
||||
'confirmed_admin_name' => (string) ($r['confirmed_admin_name'] ?? ''),
|
||||
'confirmed_at' => (int) ($r['confirmed_at'] ?? 0),
|
||||
'confirmed_at_text' => !empty($r['confirmed_at']) ? date('Y-m-d H:i:s', (int) $r['confirmed_at']) : '',
|
||||
'totals_json' => (string) ($r['totals_json'] ?? ''),
|
||||
'revoked_at' => (int) ($r['revoked_at'] ?? 0),
|
||||
'revoked_admin_id' => (int) ($r['revoked_admin_id'] ?? 0),
|
||||
'revoked_admin_name' => (string) ($r['revoked_admin_name'] ?? ''),
|
||||
'revoked_at_text' => !empty($r['revoked_at']) ? date('Y-m-d H:i:s', (int) $r['revoked_at']) : '',
|
||||
// 仅 status=1 锁定,禁止重复确定;status=2 已撤回可再核对/确定
|
||||
'is_confirmed_locked' => $st === 1,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5071,11 +5155,54 @@ class YejiStatsLogic
|
||||
}
|
||||
|
||||
$prevMonthAnchor = strtotime('-1 month', $anchor);
|
||||
$orderCreateStartTs = (int) strtotime(date('Y-m-01 00:00:00', $prevMonthAnchor));
|
||||
$orderCreateEndTs = (int) strtotime(date('Y-m-t 23:59:59', $prevMonthAnchor));
|
||||
if ($prevMonthAnchor === false) {
|
||||
throw new \think\Exception('结算月日期无效');
|
||||
}
|
||||
|
||||
$stRaw = trim((string) ($params['start_time'] ?? ''));
|
||||
$etRaw = trim((string) ($params['end_time'] ?? ''));
|
||||
$explicitOrderTimeWindow = ($stRaw !== '' && $etRaw !== '');
|
||||
|
||||
if ($explicitOrderTimeWindow) {
|
||||
$t0 = strtotime($stRaw);
|
||||
$t1 = strtotime($etRaw);
|
||||
if ($t0 === false || $t1 === false) {
|
||||
throw new \think\Exception('订单创建时间 start_time / end_time 无效,请与处方订单列表一致传入(如 2026-04-02 00:00:00)');
|
||||
}
|
||||
$orderCreateStartTs = (int) $t0;
|
||||
$orderCreateEndTs = (int) $t1;
|
||||
if ($orderCreateStartTs > $orderCreateEndTs) {
|
||||
throw new \think\Exception('订单开始时间不能晚于结束时间');
|
||||
}
|
||||
$orderMonthLabel = date('Y-m-d H:i:s', $orderCreateStartTs) . ' ~ ' . date('Y-m-d H:i:s', $orderCreateEndTs);
|
||||
} else {
|
||||
$orderCreateStartTs = (int) strtotime(date('Y-m-01 00:00:00', $prevMonthAnchor));
|
||||
$orderCreateEndTs = (int) strtotime(date('Y-m-t 23:59:59', $prevMonthAnchor));
|
||||
$orderMonthLabel = date('Y-m', $prevMonthAnchor);
|
||||
}
|
||||
|
||||
$cutoffTs = (int) strtotime(date('Y-m-07 23:59:59', $anchor));
|
||||
|
||||
$orderMonthLabel = date('Y-m', $prevMonthAnchor);
|
||||
$fulfillmentStatus = (int) ($params['fulfillment_status'] ?? 3);
|
||||
if ($fulfillmentStatus < 0) {
|
||||
$fulfillmentStatus = 3;
|
||||
}
|
||||
|
||||
if ($explicitOrderTimeWindow) {
|
||||
if (array_key_exists('require_system_auto_prescription', $params)) {
|
||||
$v = $params['require_system_auto_prescription'];
|
||||
$requireSystemAutoEffective = $v === true || $v === 1 || $v === '1';
|
||||
} else {
|
||||
$requireSystemAutoEffective = false;
|
||||
}
|
||||
$applyListSqlVisibility = true;
|
||||
$skipPhpDataScopeAttributionFilter = true;
|
||||
} else {
|
||||
$requireSystemAutoEffective = (bool) Config::get('project.commission_settlement.require_system_auto_prescription', true);
|
||||
$applyListSqlVisibility = false;
|
||||
$skipPhpDataScopeAttributionFilter = false;
|
||||
}
|
||||
|
||||
$ctxParams = array_merge($params, [
|
||||
'start_date' => date('Y-m-d', $orderCreateStartTs),
|
||||
'end_date' => date('Y-m-d', $orderCreateEndTs),
|
||||
@@ -5094,12 +5221,19 @@ class YejiStatsLogic
|
||||
$rxTable = self::tableWithPrefix('tcm_prescription');
|
||||
$att = self::sqlPerformanceAttributionAdminExpr();
|
||||
|
||||
$assistAttributionRestrictSelfId = 0;
|
||||
if ($viewerAdminId > 0 && PrescriptionOrderLogic::statsRestrictedToOwnAssistantPerformanceDomain($viewerAdminInfo)) {
|
||||
$assistAttributionRestrictSelfId = $viewerAdminId;
|
||||
}
|
||||
|
||||
return [
|
||||
'monthRaw' => $monthRaw,
|
||||
'cutoffTs' => $cutoffTs,
|
||||
'orderCreateStartTs' => $orderCreateStartTs,
|
||||
'orderCreateEndTs' => $orderCreateEndTs,
|
||||
'orderMonthLabel' => $orderMonthLabel,
|
||||
'orderStartTimeText' => date('Y-m-d H:i:s', $orderCreateStartTs),
|
||||
'orderEndTimeText' => date('Y-m-d H:i:s', $orderCreateEndTs),
|
||||
'nextSettlementMonth' => self::commissionSettlementNextMonth($monthRaw),
|
||||
'deptKey' => $deptKey,
|
||||
'scopeChannelCode' => $scopeChannelCode,
|
||||
@@ -5111,6 +5245,15 @@ class YejiStatsLogic
|
||||
'diagTable' => $diagTable,
|
||||
'rxTable' => $rxTable,
|
||||
'att' => $att,
|
||||
'assistAttributionRestrictSelfId' => $assistAttributionRestrictSelfId,
|
||||
'fulfillmentStatus' => $fulfillmentStatus,
|
||||
'explicitOrderTimeWindow' => $explicitOrderTimeWindow,
|
||||
'requireSystemAutoEffective' => $requireSystemAutoEffective,
|
||||
'applyListSqlVisibility' => $applyListSqlVisibility,
|
||||
'skipPhpDataScopeAttributionFilter' => $skipPhpDataScopeAttributionFilter,
|
||||
'viewerAdminId' => $viewerAdminId,
|
||||
'viewerAdminInfo' => $viewerAdminInfo,
|
||||
'commissionRequestParams' => $params,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5178,6 +5321,45 @@ class YejiStatsLogic
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 诊单关联挂号摘要展示:优先 status=3(已完成),否则取该诊单维度下最近一条挂号(patient_id=诊单 id)。
|
||||
* doctor_appointment.patient_id 与诊单主键对齐,见 DiagnosisLists。
|
||||
*
|
||||
* @param int[] $diagnosisIds
|
||||
*
|
||||
* @return array<int, array{appointment_id: int, channels: int, appointment_date: string, appointment_status: int}> diagnosis_id => 摘要
|
||||
*/
|
||||
private static function commissionSettlementDiagnosisLatestCompletedAppointment(array $diagnosisIds): array
|
||||
{
|
||||
$ids = array_values(array_unique(array_filter(array_map('intval', $diagnosisIds), static fn (int $x): bool => $x > 0)));
|
||||
if ($ids === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = Db::name('doctor_appointment')
|
||||
->whereIn('patient_id', $ids)
|
||||
->orderRaw('`patient_id` asc, (case when `status` = 3 then 1 else 0 end) desc, `id` desc')
|
||||
->field(['id', 'patient_id', 'channels', 'appointment_date', 'status'])
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$dg = (int) ($r['patient_id'] ?? 0);
|
||||
if ($dg <= 0 || isset($out[$dg])) {
|
||||
continue;
|
||||
}
|
||||
$out[$dg] = [
|
||||
'appointment_id' => (int) ($r['id'] ?? 0),
|
||||
'channels' => (int) ($r['channels'] ?? 0),
|
||||
'appointment_date' => trim((string) ($r['appointment_date'] ?? '')),
|
||||
'appointment_status' => (int) ($r['status'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 挂号字典 channels:value(int) => name(string)。仅取本次涉及到的 value 集合,减少 dict 全量读取。
|
||||
*
|
||||
@@ -5230,16 +5412,91 @@ class YejiStatsLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 签收 / 尾款时间:对物流与支付关联做 GROUP BY 后 LEFT JOIN,避免 SELECT 中逐行相关子查询导致 overview 超时。
|
||||
* 「本期提成 / 顺延下期」单笔判定(与 overview / orderLines / 确定结转一致)。
|
||||
*
|
||||
* @return array{eligible: bool, defer_code: string, defer_text: string}
|
||||
*/
|
||||
private static function commissionSettlementBucketEligible(int $signTs, int $payTs, int $cutoffTs): array
|
||||
{
|
||||
$deadlineText = date('Y-m-d H:i:s', $cutoffTs);
|
||||
if ($signTs <= 0) {
|
||||
return [
|
||||
'eligible' => false,
|
||||
'defer_code' => 'missing_sign',
|
||||
'defer_text' => '库内无有效物流签收时间(轨迹未入库或运单号与物流主表不匹配)',
|
||||
];
|
||||
}
|
||||
if ($payTs <= 0) {
|
||||
return [
|
||||
'eligible' => false,
|
||||
'defer_code' => 'missing_pay',
|
||||
'defer_text' => '无已支付关联单或未能解析尾款支付时间',
|
||||
];
|
||||
}
|
||||
if ($signTs > $cutoffTs) {
|
||||
return [
|
||||
'eligible' => false,
|
||||
'defer_code' => 'sign_after_cutoff',
|
||||
'defer_text' => '物流签收晚于结算截止(' . $deadlineText . ')',
|
||||
];
|
||||
}
|
||||
if ($payTs > $cutoffTs) {
|
||||
return [
|
||||
'eligible' => false,
|
||||
'defer_code' => 'pay_after_cutoff',
|
||||
'defer_text' => '尾款支付(' . date('Y-m-d H:i:s', $payTs) . ')晚于结算截止(' . $deadlineText . '),仅此一项也会导致顺延',
|
||||
];
|
||||
}
|
||||
|
||||
return ['eligible' => true, 'defer_code' => '', 'defer_text' => ''];
|
||||
}
|
||||
|
||||
/**
|
||||
* 签收 / 尾款时间:对物流与支付关联做 GROUP BY 后 LEFT JOIN。
|
||||
* 签收时间不仅用 express_tracking.sign_time:历史数据或部分同步路径下 sign_time 仍为 0,
|
||||
* 但 express_trace 已有「签收」语义轨迹(与 logisticsTrace 展示同源),此处用轨迹时间戳回填,避免误算为「无签收」而全部顺延。
|
||||
*/
|
||||
private static function commissionSettlementAttachExpressPayAggregates(Query $query): void
|
||||
{
|
||||
$etTable = self::tableWithPrefix('express_tracking');
|
||||
$traceTable = self::tableWithPrefix('express_trace');
|
||||
$linkTbl = self::tableWithPrefix('tcm_prescription_order_pay_order');
|
||||
$payTbl = self::tableWithPrefix('order');
|
||||
|
||||
$etAggSql = "(SELECT et.order_id, MAX(et.sign_time) AS cs_sign_ts FROM {$etTable} et "
|
||||
. "WHERE et.order_type = 'prescription' AND et.delete_time IS NULL GROUP BY et.order_id) cs_et_sig";
|
||||
// 单条物流记录上的「有效签收时间戳」:sign_time > 0 优先;否则签收类轨迹;再否则主表已签收/已标记签收时用最新轨迹时间兜底
|
||||
$sigTraceCond = "`tr_sig`.`status_code` IN ('3','301','302','304') OR `tr_sig`.`status` LIKE '%签收%' "
|
||||
. "OR `tr_sig`.`trace_context` LIKE '%签收%' OR `tr_sig`.`trace_context` LIKE '%妥投%' "
|
||||
. "OR `tr_sig`.`trace_context` LIKE '%已送达%' OR `tr_sig`.`trace_context` LIKE '%本人签收%'";
|
||||
$rowSignExpr = 'GREATEST('
|
||||
. 'IF(IFNULL(`et`.`sign_time`, 0) > 0, `et`.`sign_time`, 0), '
|
||||
. 'IFNULL((SELECT MAX(`tr_sig`.`trace_time_stamp`) FROM `' . $traceTable . '` `tr_sig` '
|
||||
. 'WHERE `tr_sig`.`tracking_id` = `et`.`id` AND IFNULL(`tr_sig`.`trace_time_stamp`, 0) > 0 AND ('
|
||||
. $sigTraceCond
|
||||
. ')), 0), '
|
||||
. 'IF((`et`.`current_state` = \'3\' OR IFNULL(`et`.`is_signed`, 0) = 1), '
|
||||
. 'IFNULL((SELECT MAX(`tr_any`.`trace_time_stamp`) FROM `' . $traceTable . '` `tr_any` '
|
||||
. 'WHERE `tr_any`.`tracking_id` = `et`.`id` AND IFNULL(`tr_any`.`trace_time_stamp`, 0) > 0), 0), '
|
||||
. '0)'
|
||||
. ')';
|
||||
|
||||
$poTable = self::tableWithPrefix('tcm_prescription_order');
|
||||
// tracking_number:TRIM 避免空格导致匹配失败;collation 统一避免 HY000 1267
|
||||
$trackingJoinOn =
|
||||
'TRIM(`po`.`tracking_number`) COLLATE utf8mb4_unicode_ci = TRIM(`et`.`tracking_number`) COLLATE utf8mb4_unicode_ci';
|
||||
// 与 ExpressTrackingService::getDetailByTrackingNumber 一致:按单号可查即应能统计签收;勿再依赖 et.order_type(历史数据可能空串/未回写)。
|
||||
// 口径一:express_tracking.order_id 指向处方业务单(用业务单表限定,而非 et.order_type 字符串)。
|
||||
// 口径二:order_id 未绑定或漂移时,用处方单运单号与物流主表 TRIM 后等值关联。
|
||||
$etAggSql = '(SELECT `u`.`order_id`, MAX(`u`.`cs_row_sign`) AS `cs_sign_ts` FROM ('
|
||||
. 'SELECT `et`.`order_id` AS `order_id`, ' . $rowSignExpr . ' AS `cs_row_sign` FROM `' . $etTable . '` `et` '
|
||||
. 'INNER JOIN `' . $poTable . '` `po_by_id` ON `po_by_id`.`id` = `et`.`order_id` AND `po_by_id`.`delete_time` IS NULL '
|
||||
. 'WHERE `et`.`delete_time` IS NULL AND IFNULL(`et`.`order_id`, 0) > 0 '
|
||||
. 'UNION ALL '
|
||||
. 'SELECT `po`.`id` AS `order_id`, ' . $rowSignExpr . ' AS `cs_row_sign` FROM `' . $etTable . '` `et` '
|
||||
. 'INNER JOIN `' . $poTable . '` `po` ON ' . $trackingJoinOn
|
||||
. " AND LENGTH(TRIM(IFNULL(`po`.`tracking_number`, ''))) > 0 "
|
||||
. ' AND `po`.`delete_time` IS NULL '
|
||||
. 'WHERE `et`.`delete_time` IS NULL'
|
||||
. ') AS `u` GROUP BY `u`.`order_id`) `cs_et_sig`';
|
||||
|
||||
$payAggSql = "(SELECT l.prescription_order_id AS cs_pay_oid, "
|
||||
. 'MAX(CASE WHEN pay.order_type IN (5, 7) THEN UNIX_TIMESTAMP(pay.payment_time) END) AS cs_pay_tail_ts, '
|
||||
@@ -5273,16 +5530,27 @@ class YejiStatsLogic
|
||||
$rxTable = $pack['rxTable'];
|
||||
$att = $pack['att'];
|
||||
|
||||
$requireSa = !empty($pack['requireSystemAutoEffective']);
|
||||
$fulfillmentStatus = (int) ($pack['fulfillmentStatus'] ?? 3);
|
||||
|
||||
$rxJoinCond = 'rx.id = o.prescription_id AND rx.delete_time IS NULL';
|
||||
if ($requireSa) {
|
||||
$rxJoinCond .= ' AND IFNULL(rx.is_system_auto, 0) = 1';
|
||||
}
|
||||
|
||||
$query = Db::name('tcm_prescription_order')
|
||||
->alias('o')
|
||||
->join("{$diagTable} dg", 'dg.id = o.diagnosis_id AND dg.delete_time IS NULL', 'INNER')
|
||||
->join("{$rxTable} rx", 'rx.id = o.prescription_id AND rx.delete_time IS NULL AND IFNULL(rx.is_system_auto, 0) = 1', 'INNER');
|
||||
->join("{$diagTable} dg", 'dg.id = o.diagnosis_id AND dg.delete_time IS NULL', 'INNER');
|
||||
if ($requireSa) {
|
||||
$query->join("{$rxTable} rx", $rxJoinCond, 'INNER');
|
||||
} else {
|
||||
$query->join("{$rxTable} rx", $rxJoinCond, 'LEFT');
|
||||
}
|
||||
self::commissionSettlementAttachExpressPayAggregates($query);
|
||||
|
||||
$query->whereNull('o.delete_time')
|
||||
->where('o.fulfillment_status', 3)
|
||||
->where('o.diagnosis_id', '>', 0)
|
||||
->whereRaw("({$att}) > 0", []);
|
||||
->where('o.fulfillment_status', $fulfillmentStatus)
|
||||
->where('o.diagnosis_id', '>', 0);
|
||||
|
||||
$query->where(function ($qq) use ($pack) {
|
||||
$qq->whereBetween('o.create_time', [$pack['orderCreateStartTs'], $pack['orderCreateEndTs']]);
|
||||
@@ -5295,6 +5563,24 @@ class YejiStatsLogic
|
||||
|
||||
self::applyCommissionSettlementChannelFilter($query, $c);
|
||||
|
||||
$viewerAdminId = (int) ($pack['viewerAdminId'] ?? 0);
|
||||
$viewerAdminInfo = $pack['viewerAdminInfo'] ?? [];
|
||||
if (!empty($pack['applyListSqlVisibility']) && $viewerAdminId > 0 && \is_array($viewerAdminInfo)) {
|
||||
$reqParams = $pack['commissionRequestParams'] ?? [];
|
||||
PrescriptionOrderLogic::applyPrescriptionOrderListAccessForCommissionQuery(
|
||||
$query,
|
||||
'o',
|
||||
$viewerAdminId,
|
||||
$viewerAdminInfo,
|
||||
\is_array($reqParams) ? $reqParams : []
|
||||
);
|
||||
}
|
||||
|
||||
$selfAid = (int) ($pack['assistAttributionRestrictSelfId'] ?? 0);
|
||||
if ($selfAid > 0) {
|
||||
$query->whereRaw("({$att}) = ?", [$selfAid]);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
@@ -5303,9 +5589,9 @@ class YejiStatsLogic
|
||||
*
|
||||
* 规则摘要:
|
||||
* - 订单池:结算月上一自然月内创建;关联处方 is_system_auto=1(系统代开);履约已完成 fulfillment_status=3;未软删。
|
||||
* - 签收时间:express_tracking.order_type=prescription 下 MAX(sign_time)。
|
||||
* - 签收时间:express_tracking 与处方单关联(order_id 命中业务单 **或** 运单号 TRIM 后一致);sign_time / express_trace 轨迹回填,与 logisticsTrace(getDetailByTrackingNumber) 同源,不依赖 et.order_type 字符串(历史数据可能未写入)。
|
||||
* - 尾款支付时间:关联支付单 status=2,优先 order_type∈(5,7),否则回退为任意已支付关联单的 MAX(payment_time)。
|
||||
* - 计入本期:签收与尾款时间均已成立且不晚于结算月 7 日 23:59:59。
|
||||
* - 计入本期:签收与尾款时间均已成立,且**两者均**不晚于结算月 7 日 23:59:59(仅签收在截止前但尾款更晚→仍顺延)。
|
||||
* - 其余已完成单计入「顺延下期」(含签收或支付缺失、或晚于截止)。
|
||||
*
|
||||
* @param array{settlement_month?: string, dept_ids?: int[]|string, channel_code?: string, tag_id?: string} $params
|
||||
@@ -5326,6 +5612,9 @@ class YejiStatsLogic
|
||||
|
||||
$confirm = self::commissionSettlementGetConfirmRow($monthRaw, $pack['scopeChannelCode'], $pack['deptKey']);
|
||||
|
||||
$requireSysAutoPo = !empty($pack['requireSystemAutoEffective']);
|
||||
$explicitOrderTimeWindow = !empty($pack['explicitOrderTimeWindow']);
|
||||
|
||||
$emptyTotal = [
|
||||
'completed_order_count' => 0,
|
||||
'completed_order_amount' => 0.0,
|
||||
@@ -5337,22 +5626,30 @@ class YejiStatsLogic
|
||||
];
|
||||
|
||||
if ($tableRowDeptIds === [] || $adminToPrimary === []) {
|
||||
$ruleLegacy = '订单创建月为结算月上一个月的自然月整段;或未传 start/end 时为整月时间段。签收与尾款均在结算月 7 日前完成的计入本期,否则顺延。上期「确定业绩」产生的顺延订单会并入本期。';
|
||||
$ruleAligned = '传 start_time+end_time 时订单池与处方订单列表一致(创建时间与 fulfillment_status);默认含手动与系统处方,可见性与列表同源。结转与签收/尾款截止日期规则同上。';
|
||||
|
||||
return [
|
||||
'settlement_month' => $monthRaw,
|
||||
'order_month' => $orderMonthLabel,
|
||||
'cutoff_end' => date('Y-m-d H:i:s', $cutoffTs),
|
||||
'rule_note' => '订单创建月为结算月的上一自然月;统计系统代开处方对应的已完成业务订单;签收与尾款均在结算月 7 日 24 点前完成的金额计入本期提成,否则计入顺延下期。上期「确定业绩」写入的顺延订单会在本期并入统计。',
|
||||
'channel_code' => $c['channelCode'],
|
||||
'channel_name' => $c['channelInfo'] ? (string) $c['channelInfo']['channel_name'] : '',
|
||||
'scope_dept_ids_key' => $pack['deptKey'],
|
||||
'carry_in_order_count' => 0,
|
||||
'confirm' => $confirm,
|
||||
'rows' => [],
|
||||
'assistant_rows' => [],
|
||||
'doctor_rows' => [],
|
||||
'assistant_channel_rows' => [],
|
||||
'doctor_channel_rows' => [],
|
||||
'total' => $emptyTotal,
|
||||
'settlement_month' => $monthRaw,
|
||||
'order_month' => $orderMonthLabel,
|
||||
'order_start_time' => $pack['orderStartTimeText'],
|
||||
'order_end_time' => $pack['orderEndTimeText'],
|
||||
'fulfillment_status' => $pack['fulfillmentStatus'],
|
||||
'explicit_order_time_window' => $explicitOrderTimeWindow,
|
||||
'cutoff_end' => date('Y-m-d H:i:s', $cutoffTs),
|
||||
'rule_note' => $explicitOrderTimeWindow ? $ruleAligned : $ruleLegacy,
|
||||
'channel_code' => $c['channelCode'],
|
||||
'channel_name' => $c['channelInfo'] ? (string) $c['channelInfo']['channel_name'] : '',
|
||||
'scope_dept_ids_key' => $pack['deptKey'],
|
||||
'carry_in_order_count' => 0,
|
||||
'confirm' => $confirm,
|
||||
'rows' => [],
|
||||
'assistant_rows' => [],
|
||||
'doctor_rows' => [],
|
||||
'assistant_channel_rows' => [],
|
||||
'doctor_channel_rows' => [],
|
||||
'total' => $emptyTotal,
|
||||
'require_system_auto_prescription' => $requireSysAutoPo,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5366,6 +5663,9 @@ class YejiStatsLogic
|
||||
'o.diagnosis_id',
|
||||
'o.create_time',
|
||||
'o.amount',
|
||||
'o.tracking_number',
|
||||
'o.express_company',
|
||||
'o.recipient_phone',
|
||||
Db::raw("({$att}) AS assistant_id"),
|
||||
Db::raw('IFNULL(rx.creator_id, 0) AS doctor_id'),
|
||||
], $signPayFields))->select()->toArray();
|
||||
@@ -5412,17 +5712,49 @@ class YejiStatsLogic
|
||||
$doctorChDefCnt = [];
|
||||
$doctorChCarry = [];
|
||||
|
||||
$assistScopeFlip = null;
|
||||
if (!empty($c['dataScopeRestricted'])
|
||||
&& isset($c['dataScopeVisibleAdminIds'])
|
||||
&& $c['dataScopeVisibleAdminIds'] !== []) {
|
||||
$assistScopeFlip = array_flip(array_map('intval', $c['dataScopeVisibleAdminIds']));
|
||||
}
|
||||
$selfOnlyAttributionAid = (int) ($pack['assistAttributionRestrictSelfId'] ?? 0);
|
||||
if ($selfOnlyAttributionAid > 0) {
|
||||
$assistScopeFlip = [$selfOnlyAttributionAid => true];
|
||||
}
|
||||
|
||||
$skipPhpDs = !empty($pack['skipPhpDataScopeAttributionFilter']);
|
||||
|
||||
foreach ($rowsRaw as $r) {
|
||||
$aid = (int) ($r['assistant_id'] ?? 0);
|
||||
if (!$skipPhpDs && $assistScopeFlip !== null && ($aid <= 0 || !isset($assistScopeFlip[$aid]))) {
|
||||
continue;
|
||||
}
|
||||
// 展示部门有勾选时:只统计可归入当前部门上下文(adminToPrimary)的医助,与部门汇总口径一致;否则「按医助下发」会混入部门外的业绩医助。
|
||||
if (!empty($c['explicitDeptFilter']) && ($aid <= 0 || !isset($adminToPrimary[$aid]))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$oid = (int) ($r['id'] ?? 0);
|
||||
$isCarry = $oid > 0 && isset($carryFlip[$oid]);
|
||||
if ($isCarry) {
|
||||
$carryInCnt++;
|
||||
}
|
||||
|
||||
$aid = (int) ($r['assistant_id'] ?? 0);
|
||||
$doctorId = (int) ($r['doctor_id'] ?? 0);
|
||||
$signTs = (int) ($r['sign_ts'] ?? 0);
|
||||
$payTs = (int) ($r['pay_ts'] ?? 0);
|
||||
if ($signTs <= 0) {
|
||||
$tn0 = trim((string) ($r['tracking_number'] ?? ''));
|
||||
if ($tn0 !== '') {
|
||||
$signTs = ExpressTrackingService::resolveSignUnixTimeForCommission(
|
||||
$tn0,
|
||||
(string) ($r['express_company'] ?? 'auto'),
|
||||
(string) ($r['recipient_phone'] ?? ''),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
$amt = round((float) ($r['amount'] ?? 0), 2);
|
||||
$dgId = (int) ($r['diagnosis_id'] ?? 0);
|
||||
$apCh = (int) ($diagToApptChannel[$dgId] ?? 0);
|
||||
@@ -5434,7 +5766,7 @@ class YejiStatsLogic
|
||||
$totAgg[$aid] = ($totAgg[$aid] ?? 0.0) + $amt;
|
||||
$cntByAsst[$aid] = ($cntByAsst[$aid] ?? 0) + 1;
|
||||
|
||||
$eligible = $signTs > 0 && $payTs > 0 && $signTs <= $cutoffTs && $payTs <= $cutoffTs;
|
||||
$eligible = self::commissionSettlementBucketEligible($signTs, $payTs, $cutoffTs)['eligible'];
|
||||
if ($eligible) {
|
||||
$curAgg[$aid] = ($curAgg[$aid] ?? 0.0) + $amt;
|
||||
$curCntByAsst[$aid] = ($curCntByAsst[$aid] ?? 0) + 1;
|
||||
@@ -5710,22 +6042,42 @@ class YejiStatsLogic
|
||||
return ($b['completed_order_amount'] ?? 0) <=> ($a['completed_order_amount'] ?? 0);
|
||||
});
|
||||
|
||||
if ($explicitOrderTimeWindow) {
|
||||
$ruleNoteFull = '订单池与「处方订单列表」对齐:create_time 介于 start_time~end_time(与列表 between_time 同源),'
|
||||
. ('fulfillment_status=' . (string) (int) ($pack['fulfillmentStatus'] ?? 3) . '。')
|
||||
. ($requireSysAutoPo
|
||||
? '当前已开启仅统计关联处方 is_system_auto=1(系统代开)的订单;与未限制开方类型的列表条数可能不一致。'
|
||||
: '未限制仅系统代开处方(含手动),与列表在同一时段、同一状态下条数应一致(另受本页展示部门/渠道筛选影响)。')
|
||||
. ' 签收与尾款时间不晚于结算月 7 日 24 点的计入「本期提成」,其余计入「顺延下期」。'
|
||||
. '上期同渠道同部门范围内「确定业绩」产生的顺延订单并入本期。'
|
||||
. '「按医助」「按医生」拆分按 (人 × 挂号渠道):挂号渠道取最新已完成挂号 channels;未关联展示为「未匹配挂号渠道」。';
|
||||
} else {
|
||||
$ruleNoteFull = ($requireSysAutoPo
|
||||
? '订单创建月为结算月的上一自然月;仅统计系统代开处方(is_system_auto=1)对应的履约已完成(默认 fulfillment_status=3)业务订单。 '
|
||||
: '订单创建月为结算月的上一自然月;当前配置同时包含手动与系统代开关联处方(订单池更接近列表;财务请确认口径)。 ')
|
||||
. '签收时间取物流签收最大值;尾款取关联支付单已支付时间(优先尾款/全部)。签收与尾款均不晚于结算月 7 日 24 点计入「本期提成」,其余计入「顺延下期」。结转规则同上。';
|
||||
}
|
||||
|
||||
return [
|
||||
'settlement_month' => $monthRaw,
|
||||
'order_month' => $orderMonthLabel,
|
||||
'cutoff_end' => date('Y-m-d H:i:s', $cutoffTs),
|
||||
'rule_note' => '订单创建月为结算月的上一自然月;仅统计系统代开处方(is_system_auto=1)对应的履约已完成业务订单。签收时间取物流表签收时间最大值;尾款时间取关联支付单已支付记录(优先类型为尾款/全部)。签收与尾款均不晚于结算月 7 日 24 点的计入「本期提成」,其余已完成订单计入「顺延下期」。上期同渠道同部门范围内「确定业绩」产生的顺延订单,会计入本期订单池。「按医助下发」「按医生下发」按 (人 × 挂号渠道) 拆分;挂号渠道取该订单对应诊单的最新已完成挂号 doctor_appointment.channels(dict_data type_value=channels),如「复诊2」「自媒体1」等保持各自独立一行,未关联挂号统一展示为「未匹配挂号渠道」。',
|
||||
'channel_code' => $c['channelCode'],
|
||||
'channel_name' => $c['channelInfo'] ? (string) $c['channelInfo']['channel_name'] : '',
|
||||
'scope_dept_ids_key' => $pack['deptKey'],
|
||||
'carry_in_order_count' => $carryInCnt,
|
||||
'confirm' => $confirm,
|
||||
'rows' => $rows,
|
||||
'assistant_rows' => $assistantRows,
|
||||
'doctor_rows' => $doctorRows,
|
||||
'assistant_channel_rows' => $assistantChannelRows,
|
||||
'doctor_channel_rows' => $doctorChannelRows,
|
||||
'total' => [
|
||||
'settlement_month' => $monthRaw,
|
||||
'order_month' => $orderMonthLabel,
|
||||
'order_start_time' => $pack['orderStartTimeText'],
|
||||
'order_end_time' => $pack['orderEndTimeText'],
|
||||
'fulfillment_status' => $pack['fulfillmentStatus'],
|
||||
'explicit_order_time_window' => $explicitOrderTimeWindow,
|
||||
'cutoff_end' => date('Y-m-d H:i:s', $cutoffTs),
|
||||
'rule_note' => $ruleNoteFull,
|
||||
'channel_code' => $c['channelCode'],
|
||||
'channel_name' => $c['channelInfo'] ? (string) $c['channelInfo']['channel_name'] : '',
|
||||
'scope_dept_ids_key' => $pack['deptKey'],
|
||||
'carry_in_order_count' => $carryInCnt,
|
||||
'confirm' => $confirm,
|
||||
'rows' => $rows,
|
||||
'assistant_rows' => $assistantRows,
|
||||
'doctor_rows' => $doctorRows,
|
||||
'assistant_channel_rows' => $assistantChannelRows,
|
||||
'doctor_channel_rows' => $doctorChannelRows,
|
||||
'total' => [
|
||||
'completed_order_count' => $sumCompletedCnt,
|
||||
'completed_order_amount' => round($sumCompletedAmt, 2),
|
||||
'current_period_count' => $sumCurCnt,
|
||||
@@ -5734,13 +6086,14 @@ class YejiStatsLogic
|
||||
'deferred_period_amount' => round($sumDefAmt, 2),
|
||||
'carry_in_order_count' => $carryInCnt,
|
||||
],
|
||||
'require_system_auto_prescription' => $requireSysAutoPo,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 提成核对:订单明细(分页)。
|
||||
*
|
||||
* @param array{settlement_month?: string, dept_ids?: int[]|string, channel_code?: string, page?: int|string, page_size?: int|string, bucket?: string} $params bucket: 空|current|deferred
|
||||
* @param array{settlement_month?: string, dept_ids?: int[]|string, channel_code?: string, page?: int|string, page_size?: int|string, bucket?: string, assistant_id?: int|string, doctor_id?: int|string, appt_channel_value?: int|string|null} $params bucket: 空|current|deferred;appt_channel_value 与其它筛选同时传递时一并生效(含 0=未匹配挂号渠道)
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
@@ -5756,11 +6109,31 @@ class YejiStatsLogic
|
||||
$bucket = trim((string) ($params['bucket'] ?? ''));
|
||||
$page = max(1, (int) ($params['page'] ?? 1));
|
||||
$pageSize = min(100, max(10, (int) ($params['page_size'] ?? 20)));
|
||||
$filterAssistantId = max(0, (int) ($params['assistant_id'] ?? 0));
|
||||
$filterDoctorId = max(0, (int) ($params['doctor_id'] ?? 0));
|
||||
$filterApptChannelActive = array_key_exists('appt_channel_value', $params)
|
||||
&& $params['appt_channel_value'] !== null
|
||||
&& $params['appt_channel_value'] !== '';
|
||||
$filterApptChannelVal = $filterApptChannelActive ? (int) $params['appt_channel_value'] : null;
|
||||
|
||||
if ($tableRowDeptIds === [] || $adminToPrimary === []) {
|
||||
return ['list' => [], 'count' => 0, 'page' => $page, 'page_size' => $pageSize];
|
||||
}
|
||||
|
||||
$cPack = $pack['c'];
|
||||
$linesAssistScopeFlip = null;
|
||||
if (!empty($cPack['dataScopeRestricted'])
|
||||
&& isset($cPack['dataScopeVisibleAdminIds'])
|
||||
&& $cPack['dataScopeVisibleAdminIds'] !== []) {
|
||||
$linesAssistScopeFlip = array_flip(array_map('intval', $cPack['dataScopeVisibleAdminIds']));
|
||||
}
|
||||
$linesSelfAid = (int) ($pack['assistAttributionRestrictSelfId'] ?? 0);
|
||||
if ($linesSelfAid > 0) {
|
||||
$linesAssistScopeFlip = [$linesSelfAid => true];
|
||||
}
|
||||
|
||||
$linesSkipPhpDs = !empty($pack['skipPhpDataScopeAttributionFilter']);
|
||||
|
||||
$query = self::commissionSettlementBuildFilteredOrderQuery($pack);
|
||||
$att = $pack['att'];
|
||||
$signPayFields = self::commissionSettlementSignPayFieldRawList();
|
||||
@@ -5768,6 +6141,9 @@ class YejiStatsLogic
|
||||
$baseRows = $query->field(array_merge([
|
||||
'o.id',
|
||||
'o.order_no',
|
||||
'o.tracking_number',
|
||||
'o.express_company',
|
||||
'o.recipient_phone',
|
||||
'o.diagnosis_id',
|
||||
'o.create_time',
|
||||
'o.amount',
|
||||
@@ -5775,11 +6151,59 @@ class YejiStatsLogic
|
||||
Db::raw('IFNULL(rx.creator_id, 0) AS doctor_id'),
|
||||
], $signPayFields))->order('o.id', 'desc')->select()->toArray();
|
||||
|
||||
$diagIdSet = [];
|
||||
foreach ($baseRows as $r) {
|
||||
$dg = (int) ($r['diagnosis_id'] ?? 0);
|
||||
if ($dg > 0) {
|
||||
$diagIdSet[$dg] = true;
|
||||
}
|
||||
}
|
||||
$diagKeys = array_keys($diagIdSet);
|
||||
$diagToApptChannel = self::commissionSettlementDiagnosisLatestApptChannel($diagKeys);
|
||||
$diagToApptDetail = self::commissionSettlementDiagnosisLatestCompletedAppointment($diagKeys);
|
||||
$chValsForDict = [];
|
||||
foreach ($diagToApptChannel as $v) {
|
||||
$chValsForDict[] = (int) $v;
|
||||
}
|
||||
foreach ($diagToApptDetail as $drow) {
|
||||
$chValsForDict[] = (int) ($drow['channels'] ?? 0);
|
||||
}
|
||||
$apptChannelsDictMap = self::commissionSettlementApptChannelsDictMap($chValsForDict);
|
||||
|
||||
$mapped = [];
|
||||
foreach ($baseRows as $r) {
|
||||
$dgId = (int) ($r['diagnosis_id'] ?? 0);
|
||||
$apCh = (int) ($diagToApptChannel[$dgId] ?? 0);
|
||||
if ($filterApptChannelActive && $apCh !== (int) $filterApptChannelVal) {
|
||||
continue;
|
||||
}
|
||||
$preAid = (int) ($r['assistant_id'] ?? 0);
|
||||
if (!$linesSkipPhpDs && $linesAssistScopeFlip !== null && ($preAid <= 0 || !isset($linesAssistScopeFlip[$preAid]))) {
|
||||
continue;
|
||||
}
|
||||
if (!empty($cPack['explicitDeptFilter']) && ($preAid <= 0 || !isset($adminToPrimary[$preAid]))) {
|
||||
continue;
|
||||
}
|
||||
$preDoctorId = (int) ($r['doctor_id'] ?? 0);
|
||||
if ($filterAssistantId > 0 && $preAid !== $filterAssistantId) {
|
||||
continue;
|
||||
}
|
||||
if ($filterDoctorId > 0 && $preDoctorId !== $filterDoctorId) {
|
||||
continue;
|
||||
}
|
||||
$signTs = (int) ($r['sign_ts'] ?? 0);
|
||||
$payTs = (int) ($r['pay_ts'] ?? 0);
|
||||
$eligible = $signTs > 0 && $payTs > 0 && $signTs <= $cutoffTs && $payTs <= $cutoffTs;
|
||||
$tnForSig = trim((string) ($r['tracking_number'] ?? ''));
|
||||
if ($signTs <= 0 && $tnForSig !== '') {
|
||||
$signTs = ExpressTrackingService::resolveSignUnixTimeForCommission(
|
||||
$tnForSig,
|
||||
(string) ($r['express_company'] ?? 'auto'),
|
||||
(string) ($r['recipient_phone'] ?? ''),
|
||||
true
|
||||
);
|
||||
}
|
||||
$bucketMeta = self::commissionSettlementBucketEligible($signTs, $payTs, $cutoffTs);
|
||||
$eligible = $bucketMeta['eligible'];
|
||||
$b = $eligible ? 'current' : 'deferred';
|
||||
if ($bucket === 'current' && $b !== 'current') {
|
||||
continue;
|
||||
@@ -5788,8 +6212,16 @@ class YejiStatsLogic
|
||||
continue;
|
||||
}
|
||||
$oid = (int) ($r['id'] ?? 0);
|
||||
$aid = (int) ($r['assistant_id'] ?? 0);
|
||||
$doctorId = (int) ($r['doctor_id'] ?? 0);
|
||||
$aid = $preAid;
|
||||
$doctorId = $preDoctorId;
|
||||
$apDet = $diagToApptDetail[$dgId] ?? null;
|
||||
$chForLabel = \is_array($apDet) ? (int) ($apDet['channels'] ?? 0) : 0;
|
||||
if ($chForLabel <= 0) {
|
||||
$chForLabel = (int) ($diagToApptChannel[$dgId] ?? 0);
|
||||
}
|
||||
$apptDateRaw = \is_array($apDet) ? trim((string) ($apDet['appointment_date'] ?? '')) : '';
|
||||
$apptStatus = \is_array($apDet) ? (int) ($apDet['appointment_status'] ?? 0) : 0;
|
||||
$trackingNo = trim((string) ($r['tracking_number'] ?? ''));
|
||||
$foldRows = [['assistant_id' => $aid, 'cnt' => 1]];
|
||||
$byDept = self::foldPerformanceRowsToDeptByTable($foldRows, $adminToPrimary, $tableRowDeptIds, $deptById, 'cnt');
|
||||
$primaryDeptId = 0;
|
||||
@@ -5801,6 +6233,8 @@ class YejiStatsLogic
|
||||
$mapped[] = [
|
||||
'id' => $oid,
|
||||
'order_no' => (string) ($r['order_no'] ?? ''),
|
||||
'tracking_number' => $trackingNo,
|
||||
'express_company' => trim((string) ($r['express_company'] ?? '')),
|
||||
'diagnosis_id' => (int) ($r['diagnosis_id'] ?? 0),
|
||||
'amount' => round((float) ($r['amount'] ?? 0), 2),
|
||||
'create_time' => (int) ($r['create_time'] ?? 0),
|
||||
@@ -5814,9 +6248,17 @@ class YejiStatsLogic
|
||||
'assistant_name' => '',
|
||||
'doctor_name' => '',
|
||||
'bucket' => $b,
|
||||
'bucket_defer_code' => $eligible ? '' : (string) ($bucketMeta['defer_code'] ?? ''),
|
||||
'bucket_defer_text' => $eligible ? '' : (string) ($bucketMeta['defer_text'] ?? ''),
|
||||
'is_carry_in' => $oid > 0 && isset($carryFlip[$oid]) ? 1 : 0,
|
||||
'primary_dept_id' => $primaryDeptId,
|
||||
'primary_dept_name' => $primaryDeptId > 0 ? self::formatYejiDeptRowDisplayName($primaryDeptId, $deptById) : '',
|
||||
'appointment_id' => \is_array($apDet) ? (int) ($apDet['appointment_id'] ?? 0) : 0,
|
||||
'appointment_date' => $apptDateRaw,
|
||||
'appointment_date_text' => $apptDateRaw !== '' ? $apptDateRaw : '',
|
||||
'appointment_status' => $apptStatus,
|
||||
'appt_channel_value' => $chForLabel,
|
||||
'appt_channel_name' => self::commissionSettlementApptChannelLabel($chForLabel, $apptChannelsDictMap),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5894,7 +6336,7 @@ class YejiStatsLogic
|
||||
if ($exists) {
|
||||
$ex = is_array($exists) ? $exists : $exists->toArray();
|
||||
if ((int) ($ex['status'] ?? 0) === 1) {
|
||||
throw new \think\Exception('已确定业绩的记录不可修改核对备注,如需更正请联系管理员处理数据');
|
||||
throw new \think\Exception('当前为「已确定业绩」锁定状态,不可修改备注;请先「撤回确定」后再编辑');
|
||||
}
|
||||
Db::name('commission_settlement_confirm')->where('id', (int) $ex['id'])->update([
|
||||
'reconcile_note' => $note,
|
||||
@@ -5960,6 +6402,9 @@ class YejiStatsLogic
|
||||
$rowsRaw = $query->field(array_merge([
|
||||
'o.id',
|
||||
'o.create_time',
|
||||
'o.tracking_number',
|
||||
'o.express_company',
|
||||
'o.recipient_phone',
|
||||
Db::raw("({$att}) AS assistant_id"),
|
||||
], $signPayFields))->select()->toArray();
|
||||
|
||||
@@ -5967,8 +6412,18 @@ class YejiStatsLogic
|
||||
foreach ($rowsRaw as $r) {
|
||||
$signTs = (int) ($r['sign_ts'] ?? 0);
|
||||
$payTs = (int) ($r['pay_ts'] ?? 0);
|
||||
$eligible = $signTs > 0 && $payTs > 0 && $signTs <= $cutoffTs && $payTs <= $cutoffTs;
|
||||
if (!$eligible) {
|
||||
if ($signTs <= 0) {
|
||||
$tn0 = trim((string) ($r['tracking_number'] ?? ''));
|
||||
if ($tn0 !== '') {
|
||||
$signTs = ExpressTrackingService::resolveSignUnixTimeForCommission(
|
||||
$tn0,
|
||||
(string) ($r['express_company'] ?? 'auto'),
|
||||
(string) ($r['recipient_phone'] ?? ''),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!self::commissionSettlementBucketEligible($signTs, $payTs, $cutoffTs)['eligible']) {
|
||||
$deferredIds[] = (int) ($r['id'] ?? 0);
|
||||
}
|
||||
}
|
||||
@@ -6022,6 +6477,9 @@ class YejiStatsLogic
|
||||
'confirmed_admin_id' => $viewerAdminId,
|
||||
'confirmed_admin_name' => mb_substr($adminName, 0, 64),
|
||||
'confirmed_at' => $t,
|
||||
'revoked_at' => 0,
|
||||
'revoked_admin_id' => 0,
|
||||
'revoked_admin_name' => '',
|
||||
'update_time' => $t,
|
||||
]);
|
||||
} else {
|
||||
@@ -6035,6 +6493,9 @@ class YejiStatsLogic
|
||||
'confirmed_admin_id' => $viewerAdminId,
|
||||
'confirmed_admin_name' => mb_substr($adminName, 0, 64),
|
||||
'confirmed_at' => $t,
|
||||
'revoked_at' => 0,
|
||||
'revoked_admin_id' => 0,
|
||||
'revoked_admin_name' => '',
|
||||
'create_time' => $t,
|
||||
'update_time' => $t,
|
||||
]);
|
||||
@@ -6052,4 +6513,65 @@ class YejiStatsLogic
|
||||
'deferred_carry_count' => count($deferredIds),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤回「确定本期业绩」:status 1→2,并删除本 scope 下因该次确定写入的顺延结转,避免下期重复汇入。
|
||||
* 仅 status=1 时可撤回;撤回后可再次保存备注并重新确定(仍受 uk_scope 唯一行约束)。
|
||||
*
|
||||
* @param array{settlement_month?: string, dept_ids?: int[]|string, channel_code?: string} $params
|
||||
*
|
||||
* @return array{ok: bool}
|
||||
*/
|
||||
public static function commissionSettlementConfirmRevoke(array $params, int $viewerAdminId = 0, array $viewerAdminInfo = []): array
|
||||
{
|
||||
if ($viewerAdminId <= 0) {
|
||||
throw new \think\Exception('未登录');
|
||||
}
|
||||
$pack = self::commissionSettlementResolveBasePack($params, $viewerAdminId, $viewerAdminInfo);
|
||||
$tableRowDeptIds = $pack['tableRowDeptIds'];
|
||||
$adminToPrimary = $pack['adminToPrimary'];
|
||||
|
||||
if ($tableRowDeptIds === [] || $adminToPrimary === []) {
|
||||
throw new \think\Exception('当前账号无可操作的展示部门');
|
||||
}
|
||||
|
||||
$exists = Db::name('commission_settlement_confirm')
|
||||
->where('settlement_month', $pack['monthRaw'])
|
||||
->where('scope_channel_code', $pack['scopeChannelCode'])
|
||||
->where('scope_dept_ids_key', $pack['deptKey'])
|
||||
->find();
|
||||
if (!$exists) {
|
||||
throw new \think\Exception('当前筛选下无核对记录,无需撤回');
|
||||
}
|
||||
$ex = is_array($exists) ? $exists : $exists->toArray();
|
||||
if ((int) ($ex['status'] ?? 0) !== 1) {
|
||||
throw new \think\Exception('仅「已确定业绩」状态可撤回;当前为草稿、或已撤回状态');
|
||||
}
|
||||
|
||||
$adminName = (string) (Db::name('admin')->where('id', $viewerAdminId)->whereNull('delete_time')->value('name') ?: '');
|
||||
$t = time();
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
Db::name('commission_settlement_carry')
|
||||
->where('source_settlement_month', $pack['monthRaw'])
|
||||
->where('scope_channel_code', $pack['scopeChannelCode'])
|
||||
->where('scope_dept_ids_key', $pack['deptKey'])
|
||||
->delete();
|
||||
|
||||
Db::name('commission_settlement_confirm')->where('id', (int) $ex['id'])->update([
|
||||
'status' => 2,
|
||||
'revoked_at' => $t,
|
||||
'revoked_admin_id' => $viewerAdminId,
|
||||
'revoked_admin_name' => mb_substr($adminName, 0, 64),
|
||||
'update_time' => $t,
|
||||
]);
|
||||
Db::commit();
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return ['ok' => true];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use app\common\model\auth\Admin;
|
||||
use app\common\service\DataScope\DataScopeService;
|
||||
use app\common\service\ExpressTrackService;
|
||||
use app\common\service\gancao\GancaoScmRecipelService;
|
||||
use think\db\Query;
|
||||
use think\facade\Config;
|
||||
use think\facade\Db;
|
||||
use think\facade\Log;
|
||||
@@ -268,6 +269,166 @@ class PrescriptionOrderLogic
|
||||
return self::roleIntersect($adminInfo, $allow);
|
||||
}
|
||||
|
||||
/**
|
||||
* 与业务订单列表金额统计一致:命中「统计医助」角色且非农单支付审核档时,业务侧按「仅限本人关联诊单的业绩域」收口。
|
||||
* 提成结算等对业绩归因 SQL 收窄时需与之对齐。
|
||||
*/
|
||||
public static function statsRestrictedToOwnAssistantPerformanceDomain(array $adminInfo): bool
|
||||
{
|
||||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||||
return false;
|
||||
}
|
||||
if (self::canViewOrderListStatsAllScope($adminInfo)) {
|
||||
return false;
|
||||
}
|
||||
$assistantRid = (int) Config::get('project.prescription_order_stats_assistant_role_id', 2);
|
||||
|
||||
return $assistantRid > 0 && self::roleIntersect($adminInfo, [$assistantRid]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提成结算等与列表对齐:按开方医生 / 诊单医助 ∪ 订单创建人 筛选(与 PrescriptionOrderLists::applyDoctorAssistantFilters 在非业绩抽屉场景一致)。
|
||||
*
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
public static function applyCommissionOrderDoctorAssistantFilters(Query $query, string $alias, array $params): void
|
||||
{
|
||||
if (isset($params['doctor_id']) && (int) $params['doctor_id'] > 0) {
|
||||
$doctorId = (int) $params['doctor_id'];
|
||||
$rxTbl = (new Prescription())->getTable();
|
||||
$query->whereExists(
|
||||
"SELECT 1 FROM `{$rxTbl}` rx WHERE rx.`id` = `{$alias}`.`prescription_id` "
|
||||
. "AND rx.`delete_time` IS NULL AND rx.`creator_id` = {$doctorId}"
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($params['assistant_id']) && (int) $params['assistant_id'] > 0) {
|
||||
$assistantId = (int) $params['assistant_id'];
|
||||
$diagTbl = (new Diagnosis())->getTable();
|
||||
$query->where(function ($q) use ($alias, $diagTbl, $assistantId): void {
|
||||
$q->where("{$alias}.creator_id", $assistantId);
|
||||
$q->whereOrRaw(
|
||||
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$alias}`.`diagnosis_id`"
|
||||
. " AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$assistantId})"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 与处方订单列表 HasDataScopeFilter + applyCreatorOrOwnPrescriptionVisibility 等价;主业务订单别名 $alias。
|
||||
*
|
||||
* @param array<string, mixed> $params 可含 assistant_id(显式收窄时校验数据域)
|
||||
*/
|
||||
public static function applyPrescriptionOrderListAccessForCommissionQuery(
|
||||
Query $query,
|
||||
string $alias,
|
||||
int $viewerAdminId,
|
||||
array $viewerAdminInfo,
|
||||
array $params = []
|
||||
): void {
|
||||
if ($viewerAdminId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::applyCommissionOrderDoctorAssistantFilters($query, $alias, $params);
|
||||
|
||||
if (self::canSeeAllPrescriptionOrders($viewerAdminInfo)) {
|
||||
self::applyCommissionOrderDataScope($query, $alias, $viewerAdminId, $viewerAdminInfo);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$rxTbl = (new Prescription())->getTable();
|
||||
$diagTbl = (new Diagnosis())->getTable();
|
||||
|
||||
$explicitAssistant = (int) ($params['assistant_id'] ?? 0);
|
||||
$allowExplicitAssistantVisibility = false;
|
||||
if ($explicitAssistant > 0) {
|
||||
if (!DataScopeService::isEnabled()) {
|
||||
$allowExplicitAssistantVisibility = true;
|
||||
} else {
|
||||
$visibleIds = DataScopeService::getVisibleAdminIds($viewerAdminId, $viewerAdminInfo);
|
||||
if ($visibleIds === null || in_array($explicitAssistant, $visibleIds, true)) {
|
||||
$allowExplicitAssistantVisibility = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$query->where(function ($q) use (
|
||||
$alias,
|
||||
$rxTbl,
|
||||
$diagTbl,
|
||||
$viewerAdminId,
|
||||
$explicitAssistant,
|
||||
$allowExplicitAssistantVisibility,
|
||||
$viewerAdminInfo
|
||||
): void {
|
||||
if (self::canViewOrdersForOwnPrescription($viewerAdminInfo)) {
|
||||
$q->where(function ($qq) use ($alias, $rxTbl, $diagTbl, $viewerAdminId): void {
|
||||
$qq->where("{$alias}.creator_id", $viewerAdminId);
|
||||
$qq->whereOrRaw(
|
||||
"EXISTS (SELECT 1 FROM `{$rxTbl}` rx WHERE rx.`id` = `{$alias}`.`prescription_id`"
|
||||
. " AND rx.`delete_time` IS NULL AND rx.`creator_id` = {$viewerAdminId})"
|
||||
);
|
||||
$qq->whereOrRaw(
|
||||
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$alias}`.`diagnosis_id`"
|
||||
. " AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$viewerAdminId})"
|
||||
);
|
||||
});
|
||||
} else {
|
||||
$q->where(function ($qq) use ($alias, $diagTbl, $viewerAdminId): void {
|
||||
$qq->where("{$alias}.creator_id", $viewerAdminId);
|
||||
$qq->whereOrRaw(
|
||||
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$alias}`.`diagnosis_id`"
|
||||
. " AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$viewerAdminId})"
|
||||
);
|
||||
});
|
||||
}
|
||||
if ($allowExplicitAssistantVisibility) {
|
||||
$q->whereOr("{$alias}.creator_id", $explicitAssistant);
|
||||
$q->whereOrRaw(
|
||||
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$alias}`.`diagnosis_id`"
|
||||
. " AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$explicitAssistant})"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
self::applyCommissionOrderDataScope($query, $alias, $viewerAdminId, $viewerAdminInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表数据域:创建人 ∈ 可见 或 诊单医助 ∈ 可见。
|
||||
*/
|
||||
private static function applyCommissionOrderDataScope(
|
||||
Query $query,
|
||||
string $alias,
|
||||
int $viewerAdminId,
|
||||
array $viewerAdminInfo
|
||||
): void {
|
||||
if (!DataScopeService::isEnabled()) {
|
||||
return;
|
||||
}
|
||||
$ids = DataScopeService::getVisibleAdminIds($viewerAdminId, $viewerAdminInfo);
|
||||
if ($ids === null) {
|
||||
return;
|
||||
}
|
||||
if ($ids === []) {
|
||||
$query->whereRaw('0 = 1');
|
||||
|
||||
return;
|
||||
}
|
||||
$diagTbl = (new Diagnosis())->getTable();
|
||||
$inList = implode(',', array_map('intval', $ids));
|
||||
$query->where(function ($q) use ($alias, $diagTbl, $inList): void {
|
||||
$q->whereIn("{$alias}.creator_id", explode(',', $inList));
|
||||
$q->whereOrRaw(
|
||||
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$alias}`.`diagnosis_id` "
|
||||
. "AND dg.`delete_time` IS NULL AND dg.`assistant_id` IN ({$inList}))"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $row
|
||||
*/
|
||||
@@ -2263,7 +2424,7 @@ class PrescriptionOrderLogic
|
||||
return $out;
|
||||
}
|
||||
|
||||
private static function fulfillmentStatusLabel(int $fs): string
|
||||
public static function fulfillmentStatusLabel(int $fs): string
|
||||
{
|
||||
$m = [
|
||||
1 => '待双审通过',
|
||||
@@ -2283,6 +2444,209 @@ class PrescriptionOrderLogic
|
||||
return $m[$fs] ?? ('状态' . (string) $fs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出用:诊单医助在 admin_dept 中的部门路径(多部门「;」、路径内「 / 」)
|
||||
*/
|
||||
public static function formatAssistantDeptPathForExport(int $assistantAdminId): string
|
||||
{
|
||||
if ($assistantAdminId <= 0) {
|
||||
return '';
|
||||
}
|
||||
$deptIds = Db::name('admin_dept')->where('admin_id', $assistantAdminId)->column('dept_id');
|
||||
$pathSegments = [];
|
||||
foreach ($deptIds as $did) {
|
||||
$p = self::buildDeptPath((int) $did);
|
||||
if ($p !== '') {
|
||||
$pathSegments[] = $p;
|
||||
}
|
||||
}
|
||||
$pathSegments = array_values(array_unique($pathSegments));
|
||||
|
||||
return $pathSegments === [] ? '' : implode(';', $pathSegments);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出用:药品形态(丸剂 / 饮片+代煎|自煎等)
|
||||
*
|
||||
* @param array<string, mixed> $rx
|
||||
*/
|
||||
public static function formatMedicationFormForExport(array $rx): string
|
||||
{
|
||||
$type = trim((string) ($rx['prescription_type'] ?? ''));
|
||||
if ($type === '') {
|
||||
return '';
|
||||
}
|
||||
if ($type === '饮片') {
|
||||
$nd = (int) ($rx['need_decoction'] ?? 0);
|
||||
|
||||
return $type . ($nd === 1 ? '(代煎)' : '(自煎)');
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务订单列表导出:写入与 PrescriptionOrderLists::setExcelFields 对应的字段(export=2 时由列表类调用)
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $lists
|
||||
*/
|
||||
public static function appendPrescriptionOrderListExportRows(array &$lists, array $adminInfo): void
|
||||
{
|
||||
if ($lists === []) {
|
||||
return;
|
||||
}
|
||||
$diagIds = [];
|
||||
$rxIds = [];
|
||||
$poIds = [];
|
||||
foreach ($lists as $row) {
|
||||
$poIds[] = (int) ($row['id'] ?? 0);
|
||||
$d = (int) ($row['diagnosis_id'] ?? 0);
|
||||
if ($d > 0) {
|
||||
$diagIds[$d] = true;
|
||||
}
|
||||
$r = (int) ($row['prescription_id'] ?? 0);
|
||||
if ($r > 0) {
|
||||
$rxIds[$r] = true;
|
||||
}
|
||||
}
|
||||
$poIds = array_values(array_filter(array_unique($poIds), static fn (int $id): bool => $id > 0));
|
||||
$diagIdList = array_keys($diagIds);
|
||||
$rxIdList = array_keys($rxIds);
|
||||
|
||||
$diagById = [];
|
||||
if ($diagIdList !== []) {
|
||||
$diagRows = Diagnosis::whereIn('id', $diagIdList)->whereNull('delete_time')
|
||||
->field(['id', 'patient_name', 'gender', 'age', 'phone', 'assistant_id'])
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($diagRows as $dr) {
|
||||
$diagById[(int) $dr['id']] = $dr;
|
||||
}
|
||||
}
|
||||
|
||||
$rxById = [];
|
||||
if ($rxIdList !== []) {
|
||||
$rxRows = Prescription::whereIn('id', $rxIdList)->whereNull('delete_time')
|
||||
->field(['id', 'prescription_type', 'need_decoction', 'dose_unit'])
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($rxRows as $xr) {
|
||||
$rxById[(int) $xr['id']] = $xr;
|
||||
}
|
||||
}
|
||||
|
||||
$logRows = PrescriptionOrderLog::whereIn('prescription_order_id', $poIds)
|
||||
->whereIn('action', ['audit_rx_approve', 'audit_rx_reject', 'revoke_rx_audit'])
|
||||
->field(['prescription_order_id', 'id', 'action', 'admin_name', 'create_time'])
|
||||
->order('id', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
$auditEffectiveByPo = [];
|
||||
foreach ($logRows as $lg) {
|
||||
$pid = (int) ($lg['prescription_order_id'] ?? 0);
|
||||
if ($pid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$act = (string) ($lg['action'] ?? '');
|
||||
if ($act === 'revoke_rx_audit') {
|
||||
$auditEffectiveByPo[$pid] = null;
|
||||
continue;
|
||||
}
|
||||
if ($act === 'audit_rx_approve' || $act === 'audit_rx_reject') {
|
||||
$auditEffectiveByPo[$pid] = $lg;
|
||||
}
|
||||
}
|
||||
|
||||
$assistantDeptCache = [];
|
||||
|
||||
foreach ($lists as &$item) {
|
||||
$poId = (int) ($item['id'] ?? 0);
|
||||
$fs = (int) ($item['fulfillment_status'] ?? 0);
|
||||
$item['export_fulfillment_status_text'] = self::fulfillmentStatusLabel($fs);
|
||||
$ct = (int) ($item['create_time'] ?? 0);
|
||||
$item['export_order_time'] = $ct > 0 ? date('Y-m-d H:i:s', $ct) : '';
|
||||
|
||||
$diagId = (int) ($item['diagnosis_id'] ?? 0);
|
||||
$dg = $diagById[$diagId] ?? null;
|
||||
$item['export_patient_name'] = \is_array($dg) ? (string) ($dg['patient_name'] ?? '') : '';
|
||||
if (\is_array($dg)) {
|
||||
$g = (int) ($dg['gender'] ?? 0);
|
||||
$item['export_patient_gender'] = $g === 1 ? '男' : '女';
|
||||
$item['export_patient_age'] = (string) ($dg['age'] ?? '');
|
||||
$item['export_patient_phone'] = (string) ($dg['phone'] ?? '');
|
||||
$astId = (int) ($dg['assistant_id'] ?? 0);
|
||||
if ($astId > 0) {
|
||||
if (!isset($assistantDeptCache[$astId])) {
|
||||
$assistantDeptCache[$astId] = self::formatAssistantDeptPathForExport($astId);
|
||||
}
|
||||
$item['export_assistant_dept'] = $assistantDeptCache[$astId];
|
||||
} else {
|
||||
$item['export_assistant_dept'] = '';
|
||||
}
|
||||
} else {
|
||||
$item['export_patient_gender'] = '';
|
||||
$item['export_patient_age'] = '';
|
||||
$item['export_patient_phone'] = '';
|
||||
$item['export_assistant_dept'] = '';
|
||||
}
|
||||
|
||||
$rxId = (int) ($item['prescription_id'] ?? 0);
|
||||
$rx = $rxById[$rxId] ?? [];
|
||||
$item['export_medication_form'] = self::formatMedicationFormForExport(\is_array($rx) ? $rx : []);
|
||||
|
||||
$item['export_channel'] = (string) ($item['service_channel'] ?? '');
|
||||
$md = $item['medication_days'] ?? null;
|
||||
$item['export_medication_days'] = $md !== null && $md !== '' ? (string) $md : '';
|
||||
|
||||
$amt = round((float) ($item['amount'] ?? 0), 2);
|
||||
$paid = round((float) ($item['linked_pay_paid_total'] ?? 0), 2);
|
||||
$item['export_amount'] = number_format($amt, 2, '.', '');
|
||||
$item['export_paid_amount'] = number_format($paid, 2, '.', '');
|
||||
$item['export_agency_collect'] = number_format(round($amt - $paid, 2), 2, '.', '');
|
||||
|
||||
$item['export_tracking_number'] = (string) ($item['tracking_number'] ?? '');
|
||||
$isGc = trim((string) ($item['gancao_reciperl_order_no'] ?? '')) !== '';
|
||||
$item['export_supply_mode'] = $isGc ? '甘草' : '自营';
|
||||
// 「处方成本」列:与列表/详情一致,取订单 internal_cost(含「测试价格」预报价写入;甘草/自营均导出)
|
||||
if (self::canViewInternalCost($adminInfo)) {
|
||||
$rawCost = $item['internal_cost'] ?? null;
|
||||
$item['export_gancao_prescription_cost'] = $rawCost !== null && $rawCost !== ''
|
||||
? number_format(round((float) $rawCost, 2), 2, '.', '')
|
||||
: '';
|
||||
} else {
|
||||
$item['export_gancao_prescription_cost'] = '';
|
||||
}
|
||||
|
||||
$isFu = (int) ($item['is_follow_up'] ?? 0) === 1;
|
||||
$item['export_first_visit_assistant'] = $isFu ? (string) ($item['prev_staff'] ?? '') : '';
|
||||
|
||||
$hit = (int) ($item['po_assign_snapshot_hit'] ?? 0);
|
||||
$rel = (int) ($item['po_assign_is_release'] ?? 0);
|
||||
$er = (int) ($item['po_assign_to_er_center'] ?? 0);
|
||||
if ($hit !== 1) {
|
||||
$item['export_center_label'] = '—';
|
||||
} elseif ($rel === 1) {
|
||||
$item['export_center_label'] = '已释放';
|
||||
} elseif ($er === 1) {
|
||||
$item['export_center_label'] = '二中心';
|
||||
} else {
|
||||
$item['export_center_label'] = '一中心';
|
||||
}
|
||||
|
||||
$paSt = (int) ($item['prescription_audit_status'] ?? 0);
|
||||
$aud = $auditEffectiveByPo[$poId] ?? null;
|
||||
if (($paSt === 1 || $paSt === 2) && \is_array($aud)) {
|
||||
$at = (int) ($aud['create_time'] ?? 0);
|
||||
$item['export_rx_audit_time'] = $at > 0 ? date('Y-m-d H:i:s', $at) : '';
|
||||
$item['export_rx_auditor'] = (string) ($aud['admin_name'] ?? '');
|
||||
} else {
|
||||
$item['export_rx_audit_time'] = '';
|
||||
$item['export_rx_auditor'] = '';
|
||||
}
|
||||
}
|
||||
unset($item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成订单时把关联诊单的 assistant_id 清空,让患者回到「待分配」状态
|
||||
* 仅当该患者没有其它进行中的处方订单且无指派日志时才清空,避免误覆盖
|
||||
|
||||
Reference in New Issue
Block a user