新增
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\controller\qywx;
|
||||
|
||||
use app\adminapi\controller\BaseAdminController;
|
||||
use app\adminapi\lists\qywx\CustomerLists;
|
||||
use app\adminapi\logic\qywx\CustomerLogic;
|
||||
use app\adminapi\validate\qywx\CustomerValidate;
|
||||
|
||||
/**
|
||||
* 企业微信客户管理控制器
|
||||
*/
|
||||
class CustomerController extends BaseAdminController
|
||||
{
|
||||
/**
|
||||
* @notes 客户列表
|
||||
*/
|
||||
public function lists()
|
||||
{
|
||||
return $this->dataLists(new CustomerLists());
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 同步企业微信客户
|
||||
*/
|
||||
public function sync()
|
||||
{
|
||||
$result = CustomerLogic::syncCustomers();
|
||||
if ($result === false) {
|
||||
return $this->fail(CustomerLogic::getError());
|
||||
}
|
||||
return $this->success('同步成功', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取统计信息
|
||||
*/
|
||||
public function stats()
|
||||
{
|
||||
$stats = CustomerLogic::getStats();
|
||||
return $this->data($stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取同步设置
|
||||
*/
|
||||
public function getSyncSettings()
|
||||
{
|
||||
$settings = CustomerLogic::getSyncSettings();
|
||||
return $this->data($settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 保存同步设置
|
||||
*/
|
||||
public function saveSyncSettings()
|
||||
{
|
||||
$params = (new CustomerValidate())->post()->goCheck('syncSettings');
|
||||
$result = CustomerLogic::saveSyncSettings($params);
|
||||
if ($result === false) {
|
||||
return $this->fail(CustomerLogic::getError());
|
||||
}
|
||||
return $this->success('保存成功');
|
||||
}
|
||||
}
|
||||
@@ -301,6 +301,9 @@ class DiagnosisController extends BaseAdminController
|
||||
*/
|
||||
public function getImChatMessages()
|
||||
{
|
||||
// 增加执行时间限制到 120 秒
|
||||
set_time_limit(120);
|
||||
|
||||
$diagnosisId = (int)$this->request->get('diagnosis_id', 0);
|
||||
if ($diagnosisId <= 0) {
|
||||
return $this->fail('诊单ID不能为空');
|
||||
|
||||
@@ -21,7 +21,7 @@ class PrescriptionOrderController extends BaseAdminController
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定诊单下已支付支付单(创建/编辑业务订单时多选关联)
|
||||
* 指定诊单下可关联的支付单(待支付/已支付,创建/编辑业务订单时多选关联)
|
||||
*/
|
||||
public function paidPayOrders()
|
||||
{
|
||||
@@ -236,6 +236,20 @@ class PrescriptionOrderController extends BaseAdminController
|
||||
return $this->success('新增支付单成功', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为「已发货」订单关联已有支付单,并重置支付单审核状态为待审核
|
||||
*/
|
||||
public function linkPayOrder()
|
||||
{
|
||||
$params = (new PrescriptionOrderValidate())->post()->goCheck('linkPayOrder');
|
||||
$result = PrescriptionOrderLogic::linkPayOrder($params, $this->adminId, $this->adminInfo);
|
||||
if ($result === false) {
|
||||
return $this->fail(PrescriptionOrderLogic::getError());
|
||||
}
|
||||
|
||||
return $this->success('关联支付单成功', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将「已发货」且支付审核已通过的订单标记为「已完成」
|
||||
*/
|
||||
|
||||
@@ -38,7 +38,7 @@ class StatisticsLists extends BaseAdminDataLists
|
||||
|
||||
// 获取医生列表(role_id=1 表示医生角色)
|
||||
$doctorAdminIds = \app\common\model\auth\AdminRole::where('role_id', 1)->column('admin_id');
|
||||
|
||||
|
||||
if (empty($doctorAdminIds)) {
|
||||
return [];
|
||||
}
|
||||
@@ -56,11 +56,11 @@ class StatisticsLists extends BaseAdminDataLists
|
||||
->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 索引
|
||||
@@ -109,32 +109,75 @@ class StatisticsLists extends BaseAdminDataLists
|
||||
}
|
||||
$doctors = $doctorQuery->select()->toArray();
|
||||
|
||||
// 查询2:获取部门统计信息(通过assistant_id直接关联,按部门分组统计数量)
|
||||
// 查询2:获取部门统计信息(通过assistant_id直接关联,按部门和状态分组统计数量)
|
||||
$doctorIdsWithAppointments = array_keys($statsMap);
|
||||
$deptStatsMap = [];
|
||||
$deptStatusMap = [];
|
||||
$channelStatsMap = [];
|
||||
|
||||
$channelStatusMap = [];
|
||||
|
||||
if (!empty($doctorIdsWithAppointments)) {
|
||||
// 通过 assistant_id → admin_dept → dept 获取部门统计
|
||||
// 通过 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.name as dept_name, COUNT(*) as dept_count')
|
||||
->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')
|
||||
->group('apt.doctor_id, dept.id, apt.status')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
|
||||
// 组装部门统计数据:医生ID => "部门1(数量1) 部门2(数量2)"
|
||||
// 同时组装部门状态数据:医生ID => 部门ID => 状态 => 数量
|
||||
foreach ($deptStats as $stat) {
|
||||
if (!isset($deptStatsMap[$stat['doctor_id']])) {
|
||||
$deptStatsMap[$stat['doctor_id']] = [];
|
||||
$doctorId = $stat['doctor_id'];
|
||||
$deptId = $stat['dept_id'];
|
||||
$deptName = $stat['dept_name'];
|
||||
$status = $stat['status'];
|
||||
$count = $stat['count'];
|
||||
|
||||
// 组装部门统计数据
|
||||
if (!isset($deptStatsMap[$doctorId])) {
|
||||
$deptStatsMap[$doctorId] = [];
|
||||
}
|
||||
$deptStatsMap[$stat['doctor_id']][] = $stat['dept_name'] . '(' . $stat['dept_count'] . ')';
|
||||
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 一致
|
||||
@@ -144,24 +187,28 @@ class StatisticsLists extends BaseAdminDataLists
|
||||
|
||||
// 渠道:业务保存的是 channel_source(字典 value 字符串,见 AppointmentLogic::create);旧库可能仅有 channels(tinyint)
|
||||
$channelBuckets = [];
|
||||
$channelStatusBuckets = [];
|
||||
|
||||
try {
|
||||
$channelStats = \think\facade\Db::name('doctor_appointment')
|
||||
->field('doctor_id, channel_source, COUNT(*) as channel_count')
|
||||
->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')
|
||||
->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] = [];
|
||||
}
|
||||
@@ -169,19 +216,31 @@ class StatisticsLists extends BaseAdminDataLists
|
||||
$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 $rows): void {
|
||||
$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] = [];
|
||||
}
|
||||
@@ -189,12 +248,21 @@ class StatisticsLists extends BaseAdminDataLists
|
||||
$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, COUNT(*) as channel_count')
|
||||
->field('doctor_id, channels, status, COUNT(*) as channel_count')
|
||||
->whereIn('doctor_id', $doctorIdsWithAppointments)
|
||||
->where('appointment_date', '>=', $startDate)
|
||||
->where('appointment_date', '<=', $endDate)
|
||||
@@ -202,22 +270,22 @@ class StatisticsLists extends BaseAdminDataLists
|
||||
->where(function ($q) {
|
||||
$q->whereNull('channel_source')->whereOr('channel_source', '=', '');
|
||||
})
|
||||
->group('doctor_id, channels')
|
||||
->group('doctor_id, channels, status')
|
||||
->select()
|
||||
->toArray();
|
||||
$mergeLegacyChannels($channelBuckets, $legacyStats);
|
||||
$mergeLegacyChannels($channelBuckets, $channelStatusBuckets, $legacyStats);
|
||||
} catch (\Throwable) {
|
||||
try {
|
||||
$legacyStats = \think\facade\Db::name('doctor_appointment')
|
||||
->field('doctor_id, channels, COUNT(*) as channel_count')
|
||||
->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')
|
||||
->group('doctor_id, channels, status')
|
||||
->select()
|
||||
->toArray();
|
||||
$mergeLegacyChannels($channelBuckets, $legacyStats);
|
||||
$mergeLegacyChannels($channelBuckets, $channelStatusBuckets, $legacyStats);
|
||||
} catch (\Throwable) {
|
||||
// 无 channels 字段
|
||||
}
|
||||
@@ -231,6 +299,20 @@ class StatisticsLists extends BaseAdminDataLists
|
||||
}
|
||||
$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
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组装结果
|
||||
@@ -252,24 +334,42 @@ class StatisticsLists extends BaseAdminDataLists
|
||||
: 0;
|
||||
|
||||
// 计算完成率
|
||||
$completionRate = $stat['total_count'] > 0
|
||||
? round(($stat['completed_count'] / $stat['total_count']) * 100, 2)
|
||||
$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'],
|
||||
'registered_count' => (int)$stat['registered_count'],
|
||||
'completed_count' => (int)$stat['completed_count'],
|
||||
'missed_count' => (int)$stat['missed_count'],
|
||||
'cancelled_count' => (int)$stat['cancelled_count'],
|
||||
'diagnosis_count' => $diagnosisCount,
|
||||
'deal_count' => $dealCount,
|
||||
'deal_rate' => $dealRate,
|
||||
'completion_rate' => $completionRate,
|
||||
'dept_stats' => isset($deptStatsMap[$doctor['id']]) ? implode(' ', $deptStatsMap[$doctor['id']]) : '未分配',
|
||||
'channel_stats' => isset($channelStatsMap[$doctor['id']]) ? implode(' ', $channelStatsMap[$doctor['id']]) : '未设置',
|
||||
'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
|
||||
];
|
||||
}
|
||||
@@ -287,26 +387,26 @@ class StatisticsLists extends BaseAdminDataLists
|
||||
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;
|
||||
@@ -318,7 +418,7 @@ class StatisticsLists extends BaseAdminDataLists
|
||||
$timeRangeText = '今天 (' . $today . ')';
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
$startDate = $today;
|
||||
$endDate = $today;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\lists\qywx;
|
||||
|
||||
use app\adminapi\lists\BaseAdminDataLists;
|
||||
use app\common\lists\ListsSearchInterface;
|
||||
use app\common\model\QywxExternalContact;
|
||||
|
||||
/**
|
||||
* 企业微信客户列表
|
||||
*/
|
||||
class CustomerLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
{
|
||||
/**
|
||||
* @notes 搜索条件
|
||||
*/
|
||||
public function setSearch(): array
|
||||
{
|
||||
return [
|
||||
'%like%' => ['name', 'follow_user'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取列表
|
||||
*/
|
||||
public function lists(): array
|
||||
{
|
||||
$lists = QywxExternalContact::where($this->searchWhere)
|
||||
->whereNull('delete_time')
|
||||
->order('id', 'desc')
|
||||
->limit($this->limitOffset, $this->limitLength)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
foreach ($lists as &$item) {
|
||||
// 解析跟进人JSON
|
||||
$followUsers = json_decode($item['follow_users'] ?? '[]', true);
|
||||
$item['follow_users'] = is_array($followUsers) ? $followUsers : [];
|
||||
}
|
||||
|
||||
return $lists;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取数量
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return QywxExternalContact::where($this->searchWhere)
|
||||
->whereNull('delete_time')
|
||||
->count();
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,8 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
$query->whereExists("SELECT 1 FROM {$adTbl} ad WHERE ad.admin_id = {$diagTbl}.assistant_id AND ad.dept_id = {$deptId}");
|
||||
}
|
||||
|
||||
// 按挂号日期+时间升序。若传了 appointment_date(当天/明天等筛选),只按「该日」的挂号排序与展示,避免仍按历史最早一天把昨天号顶到最前
|
||||
// 按挂号状态优先级排序:已过号(4) > 已预约(1) > 已完成(3),然后按挂号日期+时间升序
|
||||
// 若传了 appointment_date(当天/明天等筛选),只按「该日」的挂号排序与展示
|
||||
$diagTbl = (new Diagnosis())->getTable();
|
||||
$aptTbl = (new Appointment())->getTable();
|
||||
$minAptDateCond = '';
|
||||
@@ -120,13 +121,18 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
$sortAptDate = addslashes((string) $this->params['appointment_date']);
|
||||
$minAptDateCond = " AND apt.appointment_date = '{$sortAptDate}'";
|
||||
}
|
||||
|
||||
// 获取最早的挂号状态(用于排序优先级)
|
||||
$minAptStatusExpr = '(SELECT apt.status FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status IN (1,3,4)' . $minAptDateCond . ' ORDER BY CASE apt.status WHEN 4 THEN 1 WHEN 1 THEN 2 WHEN 3 THEN 3 ELSE 4 END, apt.appointment_date ASC, apt.appointment_time ASC LIMIT 1)';
|
||||
|
||||
// 获取最早的挂号时间(用于同状态内排序)
|
||||
$minAptExpr = '(SELECT MIN(CONCAT(apt.appointment_date, \' \', IFNULL(NULLIF(TRIM(apt.appointment_time), \'\'), \'00:00:00\'))) FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status IN (1,3,4)' . $minAptDateCond . ')';
|
||||
|
||||
$lists = $query
|
||||
->with(['DiagnosisViewRecord'])
|
||||
->field(['id', 'patient_id', 'patient_name', 'id_card', 'phone', 'gender', 'age', 'diagnosis_date', 'diagnosis_type', 'syndrome_type', 'assistant_id', 'status', 'create_time', 'update_time'])
|
||||
->append(['gender_desc', 'status_desc', 'diagnosis_date_text'])
|
||||
->orderRaw('IFNULL(' . $minAptExpr . ", '9999-12-31 23:59:59') ASC, {$diagTbl}.id DESC")
|
||||
->orderRaw('CASE IFNULL(' . $minAptStatusExpr . ', 999) WHEN 4 THEN 1 WHEN 1 THEN 2 WHEN 3 THEN 3 ELSE 4 END ASC, IFNULL(' . $minAptExpr . ", '9999-12-31 23:59:59') ASC, {$diagTbl}.id DESC")
|
||||
->limit($this->limitOffset, $this->limitLength)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
@@ -73,11 +73,29 @@ class PrescriptionLists extends BaseAdminDataLists implements ListsSearchInterfa
|
||||
private function applyVisibilityScope($query): void
|
||||
{
|
||||
$query->where(function ($query) {
|
||||
// 超级管理员可查看全部
|
||||
if (!empty($this->adminInfo['root']) && (int) $this->adminInfo['root'] === 1) {
|
||||
return;
|
||||
}
|
||||
$adminId = $this->adminId;
|
||||
|
||||
// 检查是否属于可查看全部处方的角色
|
||||
$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);
|
||||
@@ -143,6 +161,14 @@ class PrescriptionLists extends BaseAdminDataLists implements ListsSearchInterfa
|
||||
$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) {
|
||||
// 设置默认值
|
||||
@@ -159,6 +185,10 @@ class PrescriptionLists extends BaseAdminDataLists implements ListsSearchInterfa
|
||||
$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']));
|
||||
|
||||
@@ -925,7 +925,7 @@ class OrderLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 某诊单下已支付支付单,供关联业务订单多选(主管/诊单医助看该诊单全部;否则仅本人创建)
|
||||
* 某诊单下可关联的支付单(待支付/已支付),供关联业务订单多选(主管/诊单医助看该诊单全部;否则仅本人创建)
|
||||
* 已占用(关联到未撤回/未取消的业务订单)的支付单会排除;编辑某业务订单时传 $exceptPrescriptionOrderId 以保留当前单已选中的项
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
@@ -941,7 +941,7 @@ class OrderLogic
|
||||
}
|
||||
|
||||
$q = Order::where('patient_id', $diagnosisId)
|
||||
->where('status', 2)
|
||||
->whereIn('status', [2]) // 1=待支付, 2=已支付, 5=待审核
|
||||
->whereNull('delete_time');
|
||||
|
||||
if (!self::isOrderListSupervisor($adminId, $adminInfo)) {
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\logic\qywx;
|
||||
|
||||
use app\common\logic\BaseLogic;
|
||||
use app\common\model\QywxExternalContact;
|
||||
use app\common\model\QywxSyncSettings;
|
||||
use app\common\service\wechat\WechatWorkService;
|
||||
use think\facade\Db;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 企业微信客户逻辑层
|
||||
*/
|
||||
class CustomerLogic extends BaseLogic
|
||||
{
|
||||
/**
|
||||
* @notes 同步企业微信客户
|
||||
*/
|
||||
public static function syncCustomers()
|
||||
{
|
||||
try {
|
||||
$service = new WechatWorkService();
|
||||
|
||||
// 1. 获取部门成员列表(从根部门开始,递归获取所有成员)
|
||||
$departmentId = config('project.qywx_sync_department_id', 1);
|
||||
Log::info('开始同步企业微信客户 - 部门ID: ' . $departmentId);
|
||||
|
||||
$userList = $service->getDepartmentUserList($departmentId, true);
|
||||
Log::info('获取到企业成员数量: ' . count($userList));
|
||||
|
||||
if (empty($userList)) {
|
||||
Log::error('未获取到企业成员列表');
|
||||
self::$error = '未获取到企业成员列表';
|
||||
return false;
|
||||
}
|
||||
|
||||
$syncCount = 0;
|
||||
$updateCount = 0;
|
||||
$newCount = 0;
|
||||
$processedCustomers = []; // 记录已处理的客户,避免重复
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 2. 遍历每个成员,获取其客户列表
|
||||
foreach ($userList as $user) {
|
||||
$userId = $user['userid'] ?? '';
|
||||
if (empty($userId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Log::info('获取成员客户列表 - userId: ' . $userId . ', name: ' . ($user['name'] ?? ''));
|
||||
|
||||
// 获取该成员的客户列表
|
||||
$customerList = $service->getExternalContactList($userId);
|
||||
|
||||
// 如果返回false,说明API调用失败(权限问题)
|
||||
if ($customerList === false) {
|
||||
Log::error('获取客户列表失败 - 可能是API权限未开启(错误码48002)');
|
||||
throw new \Exception('企业微信API权限不足,请在企业微信管理后台开启"客户联系"权限');
|
||||
}
|
||||
|
||||
Log::info('成员 ' . $userId . ' 的客户数量: ' . count($customerList));
|
||||
|
||||
if (empty($customerList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. 遍历客户列表,获取详情并保存
|
||||
foreach ($customerList as $externalUserId) {
|
||||
// 避免重复处理同一个客户
|
||||
if (isset($processedCustomers[$externalUserId])) {
|
||||
continue;
|
||||
}
|
||||
$processedCustomers[$externalUserId] = true;
|
||||
|
||||
// 获取客户详情
|
||||
$detail = $service->getExternalContactDetail($externalUserId);
|
||||
if (empty($detail)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$externalContact = $detail['external_contact'] ?? [];
|
||||
$followUsers = $detail['follow_user'] ?? [];
|
||||
|
||||
// 保存或更新客户信息
|
||||
$data = [
|
||||
'external_userid' => $externalContact['external_userid'] ?? '',
|
||||
'name' => $externalContact['name'] ?? '',
|
||||
'avatar' => $externalContact['avatar'] ?? '',
|
||||
'type' => $externalContact['type'] ?? 1,
|
||||
'gender' => $externalContact['gender'] ?? 0,
|
||||
'unionid' => $externalContact['unionid'] ?? '',
|
||||
'position' => $externalContact['position'] ?? '',
|
||||
'corp_name' => $externalContact['corp_name'] ?? '',
|
||||
'corp_full_name' => $externalContact['corp_full_name'] ?? '',
|
||||
'external_profile' => json_encode($externalContact['external_profile'] ?? [], JSON_UNESCAPED_UNICODE),
|
||||
'follow_users' => json_encode($followUsers, JSON_UNESCAPED_UNICODE),
|
||||
'update_time' => time(),
|
||||
];
|
||||
|
||||
$existing = QywxExternalContact::where('external_userid', $data['external_userid'])->find();
|
||||
if ($existing) {
|
||||
$existing->save($data);
|
||||
$updateCount++;
|
||||
} else {
|
||||
$data['create_time'] = time();
|
||||
QywxExternalContact::create($data);
|
||||
$newCount++;
|
||||
}
|
||||
|
||||
$syncCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 更新同步设置
|
||||
self::updateSyncStatus('success', $syncCount);
|
||||
|
||||
Db::commit();
|
||||
|
||||
Log::info('同步完成 - 总数: ' . $syncCount . ', 新增: ' . $newCount . ', 更新: ' . $updateCount);
|
||||
|
||||
return [
|
||||
'sync_count' => $syncCount,
|
||||
'new_count' => $newCount,
|
||||
'update_count' => $updateCount,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
Log::error('同步企业微信客户失败: ' . $e->getMessage());
|
||||
self::$error = '同步失败: ' . $e->getMessage();
|
||||
self::updateSyncStatus('failed', 0, $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('同步企业微信客户异常: ' . $e->getMessage());
|
||||
self::$error = '同步异常: ' . $e->getMessage();
|
||||
self::updateSyncStatus('failed', 0, $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取统计信息
|
||||
*/
|
||||
public static function getStats()
|
||||
{
|
||||
$total = QywxExternalContact::whereNull('delete_time')->count();
|
||||
|
||||
$todayStart = strtotime(date('Y-m-d 00:00:00'));
|
||||
$today = QywxExternalContact::where('create_time', '>=', $todayStart)
|
||||
->whereNull('delete_time')
|
||||
->count();
|
||||
|
||||
$settings = self::getSyncSettings();
|
||||
$lastSyncTime = $settings['last_sync_time'] ?? 0;
|
||||
$lastSync = $lastSyncTime > 0 ? date('Y-m-d H:i:s', $lastSyncTime) : '';
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'today' => $today,
|
||||
'lastSync' => $lastSync,
|
||||
'syncStatus' => $settings['sync_status'] ?? 'idle',
|
||||
'syncStatusText' => self::getSyncStatusText($settings['sync_status'] ?? 'idle'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取同步设置
|
||||
*/
|
||||
public static function getSyncSettings()
|
||||
{
|
||||
$setting = QywxSyncSettings::where('setting_key', 'customer_sync')->find();
|
||||
if (!$setting) {
|
||||
return [
|
||||
'auto_sync' => false,
|
||||
'interval' => 3600,
|
||||
'last_sync_time' => 0,
|
||||
'sync_status' => 'idle',
|
||||
];
|
||||
}
|
||||
|
||||
$value = json_decode($setting->setting_value, true);
|
||||
return $value ?: [
|
||||
'auto_sync' => false,
|
||||
'interval' => 3600,
|
||||
'last_sync_time' => 0,
|
||||
'sync_status' => 'idle',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 保存同步设置
|
||||
*/
|
||||
public static function saveSyncSettings(array $params)
|
||||
{
|
||||
try {
|
||||
$currentSettings = self::getSyncSettings();
|
||||
$newSettings = array_merge($currentSettings, [
|
||||
'auto_sync' => (bool)($params['auto_sync'] ?? false),
|
||||
'interval' => (int)($params['interval'] ?? 3600),
|
||||
]);
|
||||
|
||||
$setting = QywxSyncSettings::where('setting_key', 'customer_sync')->find();
|
||||
if ($setting) {
|
||||
$setting->setting_value = json_encode($newSettings, JSON_UNESCAPED_UNICODE);
|
||||
$setting->update_time = time();
|
||||
$setting->save();
|
||||
} else {
|
||||
QywxSyncSettings::create([
|
||||
'setting_key' => 'customer_sync',
|
||||
'setting_value' => json_encode($newSettings, JSON_UNESCAPED_UNICODE),
|
||||
'create_time' => time(),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
self::$error = '保存失败: ' . $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 更新同步状态
|
||||
*/
|
||||
private static function updateSyncStatus(string $status, int $syncCount = 0, string $error = '')
|
||||
{
|
||||
try {
|
||||
$settings = self::getSyncSettings();
|
||||
$settings['sync_status'] = $status;
|
||||
$settings['last_sync_time'] = time();
|
||||
$settings['last_sync_count'] = $syncCount;
|
||||
if ($error) {
|
||||
$settings['last_error'] = $error;
|
||||
}
|
||||
|
||||
$setting = QywxSyncSettings::where('setting_key', 'customer_sync')->find();
|
||||
if ($setting) {
|
||||
$setting->setting_value = json_encode($settings, JSON_UNESCAPED_UNICODE);
|
||||
$setting->update_time = time();
|
||||
$setting->save();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('更新同步状态失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取同步状态文本
|
||||
*/
|
||||
private static function getSyncStatusText(string $status): string
|
||||
{
|
||||
$map = [
|
||||
'idle' => '未同步',
|
||||
'syncing' => '同步中',
|
||||
'success' => '同步成功',
|
||||
'failed' => '同步失败',
|
||||
];
|
||||
return $map[$status] ?? '未知';
|
||||
}
|
||||
}
|
||||
@@ -357,6 +357,7 @@ class PrescriptionOrderLogic
|
||||
2 => '已支付',
|
||||
3 => '已取消',
|
||||
4 => '已退款',
|
||||
5 => '待审核',
|
||||
];
|
||||
foreach ($rows as &$r) {
|
||||
$ot = (int) ($r['order_type'] ?? 0);
|
||||
@@ -689,6 +690,15 @@ class PrescriptionOrderLogic
|
||||
|
||||
if (array_key_exists('pay_order_ids', $params)) {
|
||||
$payOrderIds = self::normalizePayOrderIds($params['pay_order_ids']);
|
||||
|
||||
// 过滤掉已删除的支付单ID(拒绝审核时可能软删除了待审核支付单)
|
||||
if (!empty($payOrderIds)) {
|
||||
$existingIds = Order::whereIn('id', $payOrderIds)
|
||||
->whereNull('delete_time')
|
||||
->column('id');
|
||||
$payOrderIds = array_values(array_intersect($payOrderIds, $existingIds));
|
||||
}
|
||||
|
||||
$linkErr = OrderLogic::assertPayOrdersLinkable($payOrderIds, $diagId, $adminId, $adminInfo);
|
||||
if ($linkErr !== null) {
|
||||
self::$error = $linkErr;
|
||||
@@ -745,6 +755,14 @@ class PrescriptionOrderLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
// 编辑后重置支付审核状态为待审核(需要重新审核)
|
||||
// 但如果当前状态是"已发货"(5),则不重置审核状态
|
||||
if ((int) $order->fulfillment_status !== 5) {
|
||||
$order->payment_slip_audit_status = 0;
|
||||
$order->payment_slip_audit_remark = '';
|
||||
}
|
||||
self::syncFulfillmentStatus($order);
|
||||
|
||||
try {
|
||||
$order->save();
|
||||
} catch (\Throwable $e) {
|
||||
@@ -1005,13 +1023,61 @@ class PrescriptionOrderLogic
|
||||
if ($action === 'approve') {
|
||||
$order->payment_slip_audit_status = 1;
|
||||
$order->payment_slip_audit_remark = mb_substr(trim($remark), 0, 500);
|
||||
|
||||
// 将关联的待审核支付单(status=5)更新为已支付(status=2)
|
||||
$payOrderIds = self::linkedPayOrderIdList($id);
|
||||
if (!empty($payOrderIds)) {
|
||||
Order::whereIn('id', $payOrderIds)
|
||||
->where('status', 5) // 只更新待审核状态的
|
||||
->update([
|
||||
'status' => 2, // 已支付
|
||||
'payment_time' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
|
||||
// 如果有完单申请,自动完成订单
|
||||
if ((int) $order->completion_request === 1 && (int) $order->fulfillment_status === 5) {
|
||||
$order->fulfillment_status = 3; // 已完成
|
||||
$order->completion_request = 0; // 清除完单申请标记
|
||||
self::writeLog((int) $order->id, $adminId, $adminInfo, 'auto_complete',
|
||||
'支付审核通过,根据完单申请自动完成订单');
|
||||
}
|
||||
} else {
|
||||
$order->payment_slip_audit_status = 2;
|
||||
$order->payment_slip_audit_remark = mb_substr(trim($remark), 0, 500);
|
||||
|
||||
// 拒绝审核时,软删除待审核状态的关联支付单(status=5)
|
||||
$payOrderIds = self::linkedPayOrderIdList($id);
|
||||
if (!empty($payOrderIds)) {
|
||||
Order::whereIn('id', $payOrderIds)
|
||||
->where('status', 5) // 只删除待审核状态的
|
||||
->update([
|
||||
'delete_time' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
|
||||
// 拒绝审核时,清除完单申请
|
||||
if ((int) $order->completion_request === 1) {
|
||||
$order->completion_request = 0;
|
||||
$order->completion_request_time = 0;
|
||||
$order->completion_request_by = 0;
|
||||
$order->completion_request_by_name = '';
|
||||
}
|
||||
}
|
||||
self::syncFulfillmentStatus($order);
|
||||
if ($action === 'reject') {
|
||||
self::clearPayOrderLinks($order);
|
||||
|
||||
// 状态同步和关联清除逻辑:
|
||||
// - 如果是"已发货"状态(5),无论同意还是拒绝,都保持状态不变
|
||||
// - 如果不是"已发货"状态,同步状态;拒绝时清除所有支付单关联关系
|
||||
$currentStatus = (int) $order->fulfillment_status;
|
||||
if ($currentStatus === 5) {
|
||||
// 已发货状态:保持已发货状态不变
|
||||
// 不调用 syncFulfillmentStatus
|
||||
// 拒绝时也不清除关联(只删除了待审核支付单)
|
||||
} else {
|
||||
self::syncFulfillmentStatus($order);
|
||||
if ($action === 'reject') {
|
||||
self::clearPayOrderLinks($order);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1137,6 +1203,18 @@ class PrescriptionOrderLogic
|
||||
$order->payment_slip_audit_status = 0;
|
||||
$order->payment_slip_audit_remark = '';
|
||||
self::syncFulfillmentStatus($order);
|
||||
|
||||
// 将关联的已支付支付单(status=2,且payment_method='manual')恢复为待审核(status=5)
|
||||
$payOrderIds = self::linkedPayOrderIdList($id);
|
||||
if (!empty($payOrderIds)) {
|
||||
Order::whereIn('id', $payOrderIds)
|
||||
->where('status', 2) // 只恢复已支付状态的
|
||||
->where('payment_method', 'manual') // 只恢复手动创建的
|
||||
->update([
|
||||
'status' => 5, // 待审核
|
||||
'payment_time' => null
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$order->save();
|
||||
@@ -1207,22 +1285,23 @@ class PrescriptionOrderLogic
|
||||
$orderType = (int) ($params['order_type'] ?? 3);
|
||||
$amount = round((float) ($params['pay_amount'] ?? 0), 2);
|
||||
$remark = mb_substr(trim((string) ($params['pay_remark'] ?? '')), 0, 200);
|
||||
$completionRequest = (int) ($params['completion_request'] ?? 0);
|
||||
|
||||
if ($amount <= 0) {
|
||||
self::$error = '支付单金额须大于 0';
|
||||
return false;
|
||||
}
|
||||
|
||||
// 创建 zyt_order 并标记为已支付(后台直接确认)
|
||||
// 创建 zyt_order 并标记为待审核(需要审核后才算已支付)
|
||||
$payOrder = new Order();
|
||||
$payOrder->order_no = OrderLogic::generateOrderNo();
|
||||
$payOrder->patient_id = (int) $order->diagnosis_id;
|
||||
$payOrder->creator_id = $adminId;
|
||||
$payOrder->order_type = $orderType;
|
||||
$payOrder->amount = $amount;
|
||||
$payOrder->status = 2; // 已支付
|
||||
$payOrder->status = 5; // 待审核
|
||||
$payOrder->payment_method = 'manual';
|
||||
$payOrder->payment_time = date('Y-m-d H:i:s');
|
||||
$payOrder->payment_time = null; // 审核通过后再设置支付时间
|
||||
$payOrder->remark = $remark;
|
||||
|
||||
try {
|
||||
@@ -1242,6 +1321,14 @@ class PrescriptionOrderLogic
|
||||
$order->payment_slip_audit_status = 0;
|
||||
$order->payment_slip_audit_remark = '';
|
||||
|
||||
// 处理完单申请
|
||||
if ($completionRequest === 1) {
|
||||
$order->completion_request = 1;
|
||||
$order->completion_request_time = time();
|
||||
$order->completion_request_by = $adminId;
|
||||
$order->completion_request_by_name = (string) ($adminInfo['name'] ?? '');
|
||||
}
|
||||
|
||||
try {
|
||||
$order->save();
|
||||
} catch (\Throwable $e) {
|
||||
@@ -1249,8 +1336,114 @@ class PrescriptionOrderLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
self::writeLog($id, $adminId, $adminInfo, 'add_pay_order',
|
||||
'新增支付单 #' . $payOrder->id . '(¥' . $amount . '),类型:' . $orderType . ',等待支付审核');
|
||||
$logMsg = '新增支付单 #' . $payOrder->id . '(¥' . $amount . '),类型:' . $orderType . ',等待支付审核';
|
||||
if ($completionRequest === 1) {
|
||||
$logMsg .= ',并申请完成订单';
|
||||
}
|
||||
self::writeLog($id, $adminId, $adminInfo, 'add_pay_order', $logMsg);
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为「已发货」(fulfillment_status=5) 的业务订单关联已有支付单,
|
||||
* 关联后将支付审核状态重置为待审核以启动再次审核流程。
|
||||
*
|
||||
* @param array<string,mixed> $params id, pay_order_id
|
||||
* @return array<string,mixed>|false
|
||||
*/
|
||||
public static function linkPayOrder(array $params, int $adminId, array $adminInfo)
|
||||
{
|
||||
self::$error = '';
|
||||
$id = (int) $params['id'];
|
||||
$completionRequest = (int) ($params['completion_request'] ?? 0);
|
||||
|
||||
// 支持单个或多个支付单ID
|
||||
$payOrderIds = [];
|
||||
if (isset($params['pay_order_ids']) && is_array($params['pay_order_ids'])) {
|
||||
// 多个支付单(数组)
|
||||
$payOrderIds = array_values(array_unique(array_filter(array_map('intval', $params['pay_order_ids']))));
|
||||
} elseif (isset($params['pay_order_id'])) {
|
||||
// 单个支付单(兼容旧接口)
|
||||
$payOrderIds = [(int) $params['pay_order_id']];
|
||||
}
|
||||
|
||||
if (empty($payOrderIds)) {
|
||||
self::$error = '请选择要关联的支付单';
|
||||
return false;
|
||||
}
|
||||
|
||||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||||
if (!$order) {
|
||||
self::$error = '订单不存在';
|
||||
return false;
|
||||
}
|
||||
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
|
||||
self::$error = '无权限操作';
|
||||
return false;
|
||||
}
|
||||
if ((int) $order->fulfillment_status !== 5) {
|
||||
self::$error = '仅「已发货」状态的订单可关联支付单';
|
||||
return false;
|
||||
}
|
||||
|
||||
$diagId = (int) $order->diagnosis_id;
|
||||
|
||||
// 验证所有支付单是否可关联
|
||||
$linkErr = OrderLogic::assertPayOrdersLinkable($payOrderIds, $diagId, $adminId, $adminInfo);
|
||||
if ($linkErr !== null) {
|
||||
self::$error = $linkErr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证所有支付单是否已被其他业务订单占用
|
||||
$exErr = self::assertPayOrdersExclusive($payOrderIds, $id);
|
||||
if ($exErr !== null) {
|
||||
self::$error = $exErr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 将支付单追加到业务订单关联列表
|
||||
$existingIds = self::linkedPayOrderIdList($id);
|
||||
$newIds = array_unique(array_merge($existingIds, $payOrderIds));
|
||||
self::replacePayOrderLinks($id, $newIds);
|
||||
$order->linked_pay_order_id = $newIds[0];
|
||||
|
||||
// 重置支付单审核为待审核,以启动新一轮审核(处方审核保持通过状态不变)
|
||||
$order->payment_slip_audit_status = 0;
|
||||
$order->payment_slip_audit_remark = '';
|
||||
|
||||
// 处理完单申请
|
||||
if ($completionRequest === 1) {
|
||||
$order->completion_request = 1;
|
||||
$order->completion_request_time = time();
|
||||
$order->completion_request_by = $adminId;
|
||||
$order->completion_request_by_name = (string) ($adminInfo['name'] ?? '');
|
||||
}
|
||||
|
||||
try {
|
||||
$order->save();
|
||||
} catch (\Throwable $e) {
|
||||
self::$error = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 计算总金额
|
||||
$totalAmount = 0;
|
||||
$payOrders = Order::whereIn('id', $payOrderIds)->select();
|
||||
foreach ($payOrders as $po) {
|
||||
$totalAmount += round((float) $po->amount, 2);
|
||||
}
|
||||
|
||||
$logMsg = '关联支付单 ' . count($payOrderIds) . ' 笔(共¥' . $totalAmount . '),等待支付审核';
|
||||
if ($completionRequest === 1) {
|
||||
$logMsg .= ',并申请完成订单';
|
||||
}
|
||||
self::writeLog($id, $adminId, $adminInfo, 'link_pay_order', $logMsg);
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\validate\qywx;
|
||||
|
||||
use app\common\validate\BaseValidate;
|
||||
|
||||
/**
|
||||
* 企业微信客户验证器
|
||||
*/
|
||||
class CustomerValidate extends BaseValidate
|
||||
{
|
||||
protected $rule = [
|
||||
'auto_sync' => 'require|boolean',
|
||||
'interval' => 'require|integer|between:3600,86400',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'auto_sync.require' => '请选择是否自动同步',
|
||||
'auto_sync.boolean' => '自动同步参数格式错误',
|
||||
'interval.require' => '请选择同步间隔',
|
||||
'interval.integer' => '同步间隔必须为整数',
|
||||
'interval.between' => '同步间隔必须在1小时到24小时之间',
|
||||
];
|
||||
|
||||
/**
|
||||
* @notes 同步设置场景
|
||||
*/
|
||||
public function sceneSyncSettings()
|
||||
{
|
||||
return $this->only(['auto_sync', 'interval']);
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,7 @@ class PrescriptionOrderValidate extends BaseValidate
|
||||
'logs' => ['id'],
|
||||
'paidPayOrders' => ['diagnosis_id'],
|
||||
'addPayOrder' => ['id', 'order_type', 'pay_amount', 'pay_remark'],
|
||||
'linkPayOrder' => ['id', 'pay_order_id'],
|
||||
'complete' => ['id'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\common\service\wechat\WechatWorkService;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
|
||||
/**
|
||||
* 检查企业微信API权限
|
||||
*/
|
||||
class QywxCheckPermissions extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('qywx:check-permissions')
|
||||
->setDescription('检查企业微信API权限配置');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$output->writeln('开始检查企业微信API权限...');
|
||||
$output->writeln('');
|
||||
|
||||
try {
|
||||
$service = new WechatWorkService();
|
||||
|
||||
// 检查应用权限
|
||||
$output->writeln('1. 检查应用配置...');
|
||||
$permissionCheck = $service->checkAppPermissions();
|
||||
|
||||
if ($permissionCheck['success']) {
|
||||
$output->writeln(' ✓ 应用配置正常');
|
||||
$output->writeln(' 应用ID: ' . ($permissionCheck['data']['agentid'] ?? 'N/A'));
|
||||
$output->writeln(' 应用名称: ' . ($permissionCheck['data']['name'] ?? 'N/A'));
|
||||
} else {
|
||||
$output->writeln(' ✗ 应用配置异常: ' . $permissionCheck['message']);
|
||||
}
|
||||
$output->writeln('');
|
||||
|
||||
// 测试获取部门成员
|
||||
$output->writeln('2. 测试获取部门成员...');
|
||||
$userList = $service->getDepartmentUserList(1, false);
|
||||
if (!empty($userList)) {
|
||||
$output->writeln(' ✓ 成功获取 ' . count($userList) . ' 个成员');
|
||||
$testUser = $userList[0] ?? null;
|
||||
if ($testUser) {
|
||||
$output->writeln(' 测试用户: ' . ($testUser['name'] ?? '') . ' (' . ($testUser['userid'] ?? '') . ')');
|
||||
}
|
||||
} else {
|
||||
$output->writeln(' ✗ 未能获取成员列表');
|
||||
}
|
||||
$output->writeln('');
|
||||
|
||||
// 测试获取客户列表(这里会暴露权限问题)
|
||||
if (!empty($userList)) {
|
||||
$output->writeln('3. 测试获取客户列表权限...');
|
||||
$testUser = $userList[0];
|
||||
$customerList = $service->getExternalContactList($testUser['userid']);
|
||||
|
||||
if ($customerList === false) {
|
||||
$output->writeln(' ✗ 客户联系API权限未开启(错误码48002)');
|
||||
$output->writeln('');
|
||||
$output->writeln('解决方案:');
|
||||
$output->writeln('1. 登录企业微信管理后台');
|
||||
$output->writeln('2. 进入"应用管理"');
|
||||
$output->writeln('3. 找到您的应用');
|
||||
$output->writeln('4. 开启以下权限:');
|
||||
$output->writeln(' - 客户联系 - 获取客户列表');
|
||||
$output->writeln(' - 客户联系 - 获取客户详情');
|
||||
$output->writeln('5. 将服务器IP添加到可信IP白名单');
|
||||
} elseif (is_array($customerList)) {
|
||||
$output->writeln(' ✓ 客户联系API权限正常');
|
||||
$output->writeln(' 该成员的客户数量: ' . count($customerList));
|
||||
} else {
|
||||
$output->writeln(' ? 未知响应类型');
|
||||
}
|
||||
}
|
||||
$output->writeln('');
|
||||
$output->writeln('检查完成!');
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$output->writeln('');
|
||||
$output->writeln('✗ 检查过程出错: ' . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\adminapi\logic\qywx\CustomerLogic;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 企业微信客户同步定时任务
|
||||
*
|
||||
* 使用方法:
|
||||
* php think qywx:sync-customer
|
||||
*
|
||||
* 配置crontab(每小时执行一次):
|
||||
* 0 * * * * cd /path/to/project && php think qywx:sync-customer >> /dev/null 2>&1
|
||||
*/
|
||||
class QywxSyncCustomer extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('qywx:sync-customer')
|
||||
->addOption('force', 'f', \think\console\input\Option::VALUE_NONE, '强制同步,忽略自动同步设置')
|
||||
->setDescription('同步企业微信客户数据');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$output->writeln('开始同步企业微信客户...');
|
||||
|
||||
try {
|
||||
$force = $input->getOption('force');
|
||||
|
||||
if (!$force) {
|
||||
// 检查是否开启自动同步
|
||||
$settings = CustomerLogic::getSyncSettings();
|
||||
if (!($settings['auto_sync'] ?? false)) {
|
||||
$output->writeln('自动同步未开启,跳过(使用 --force 强制同步)');
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 检查距离上次同步是否超过间隔时间
|
||||
$lastSyncTime = $settings['last_sync_time'] ?? 0;
|
||||
$interval = $settings['interval'] ?? 3600;
|
||||
$now = time();
|
||||
|
||||
if ($now - $lastSyncTime < $interval) {
|
||||
$output->writeln('距离上次同步时间不足,跳过(使用 --force 强制同步)');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 执行同步
|
||||
$result = CustomerLogic::syncCustomers();
|
||||
if ($result === false) {
|
||||
$error = CustomerLogic::getError();
|
||||
$output->writeln('同步失败: ' . $error);
|
||||
Log::error('企业微信客户同步失败: ' . $error);
|
||||
return 1;
|
||||
}
|
||||
|
||||
$output->writeln('同步成功!');
|
||||
$output->writeln('同步数量: ' . ($result['sync_count'] ?? 0));
|
||||
$output->writeln('新增数量: ' . ($result['new_count'] ?? 0));
|
||||
$output->writeln('更新数量: ' . ($result['update_count'] ?? 0));
|
||||
|
||||
return 0;
|
||||
} catch (\Throwable $e) {
|
||||
$output->writeln('同步异常: ' . $e->getMessage());
|
||||
Log::error('企业微信客户同步异常: ' . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
/**
|
||||
* 企业微信外部联系人模型
|
||||
*/
|
||||
class QywxExternalContact extends BaseModel
|
||||
{
|
||||
use SoftDelete;
|
||||
|
||||
protected $name = 'qywx_external_contact';
|
||||
protected $deleteTime = 'delete_time';
|
||||
protected $defaultSoftDelete = 0;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
/**
|
||||
* 企业微信同步设置模型
|
||||
*/
|
||||
class QywxSyncSettings extends BaseModel
|
||||
{
|
||||
protected $name = 'qywx_sync_settings';
|
||||
}
|
||||
@@ -286,7 +286,8 @@ class TencentImService
|
||||
if ($lastMsgTime !== null && $lastMsgTime > 0) {
|
||||
$data['LastMsgTime'] = $lastMsgTime;
|
||||
}
|
||||
$result = $this->httpPost($url, json_encode($data), 30);
|
||||
// 增加超时时间到 60 秒
|
||||
$result = $this->httpPost($url, json_encode($data), 60);
|
||||
if (!$result) {
|
||||
$empty['error'] = 'IM接口无响应';
|
||||
return $empty;
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service\wechat;
|
||||
|
||||
use think\facade\Cache;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 企业微信服务类
|
||||
* @see https://developer.work.weixin.qq.com/document/
|
||||
*/
|
||||
class WechatWorkService
|
||||
{
|
||||
private $corpId;
|
||||
private $secret;
|
||||
private $accessToken;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->corpId = config('pay.wechat_work.corp_id');
|
||||
$this->secret = config('pay.wechat_work.external_pay_secret');
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取access_token
|
||||
* @see https://developer.work.weixin.qq.com/document/path/91039
|
||||
*/
|
||||
private function getAccessToken(): string
|
||||
{
|
||||
if ($this->accessToken) {
|
||||
return $this->accessToken;
|
||||
}
|
||||
|
||||
// 从缓存获取
|
||||
$cacheKey = 'qywx_access_token';
|
||||
$token = Cache::get($cacheKey);
|
||||
if ($token) {
|
||||
$this->accessToken = $token;
|
||||
return $token;
|
||||
}
|
||||
|
||||
// 请求新token
|
||||
$url = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken';
|
||||
$params = [
|
||||
'corpid' => $this->corpId,
|
||||
'corpsecret' => $this->secret,
|
||||
];
|
||||
|
||||
$response = $this->httpGet($url, $params);
|
||||
if (isset($response['access_token'])) {
|
||||
$token = $response['access_token'];
|
||||
$expiresIn = $response['expires_in'] ?? 7200;
|
||||
|
||||
// 缓存token,提前5分钟过期
|
||||
Cache::set($cacheKey, $token, $expiresIn - 300);
|
||||
|
||||
$this->accessToken = $token;
|
||||
return $token;
|
||||
}
|
||||
|
||||
throw new \Exception('获取access_token失败: ' . ($response['errmsg'] ?? '未知错误'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取部门成员列表
|
||||
* @see https://developer.work.weixin.qq.com/document/path/90200
|
||||
*/
|
||||
public function getDepartmentUserList(int $departmentId = 1, bool $fetchChild = true): array
|
||||
{
|
||||
$accessToken = $this->getAccessToken();
|
||||
$url = 'https://qyapi.weixin.qq.com/cgi-bin/user/list';
|
||||
$params = [
|
||||
'access_token' => $accessToken,
|
||||
'department_id' => $departmentId,
|
||||
'fetch_child' => $fetchChild ? 1 : 0,
|
||||
];
|
||||
|
||||
$response = $this->httpGet($url, $params);
|
||||
|
||||
Log::info('企业微信-获取部门成员列表响应: ' . json_encode($response, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
if (isset($response['userlist'])) {
|
||||
return $response['userlist'];
|
||||
}
|
||||
|
||||
Log::error('获取部门成员列表失败: ' . json_encode($response));
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取成员的客户列表
|
||||
* @see https://developer.work.weixin.qq.com/document/path/92113
|
||||
* @param string $userId 成员ID
|
||||
* @return array|false 返回客户列表数组,权限错误返回false
|
||||
*/
|
||||
public function getExternalContactList(string $userId)
|
||||
{
|
||||
$accessToken = $this->getAccessToken();
|
||||
$url = 'https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list';
|
||||
$params = [
|
||||
'access_token' => $accessToken,
|
||||
'userid' => $userId,
|
||||
];
|
||||
|
||||
$response = $this->httpGet($url, $params);
|
||||
|
||||
Log::info('企业微信-获取成员客户列表响应 userId=' . $userId . ': ' . json_encode($response, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
// 检查是否是权限错误
|
||||
if (isset($response['errcode']) && $response['errcode'] == 48002) {
|
||||
Log::error('API权限不足(48002): 请在企业微信管理后台开启"客户联系"权限');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($response['external_userid'])) {
|
||||
return $response['external_userid'];
|
||||
}
|
||||
|
||||
// 如果没有客户,返回空数组(不记录错误)
|
||||
if (isset($response['errcode']) && $response['errcode'] == 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
Log::error('获取客户列表失败: ' . json_encode($response));
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取客户详情
|
||||
* @see https://developer.work.weixin.qq.com/document/path/92114
|
||||
*/
|
||||
public function getExternalContactDetail(string $externalUserId): array
|
||||
{
|
||||
$accessToken = $this->getAccessToken();
|
||||
$url = 'https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get';
|
||||
$params = [
|
||||
'access_token' => $accessToken,
|
||||
'external_userid' => $externalUserId,
|
||||
];
|
||||
|
||||
$response = $this->httpGet($url, $params);
|
||||
|
||||
if (isset($response['external_contact'])) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
Log::error('获取客户详情失败: ' . json_encode($response));
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 检查应用权限(用于诊断)
|
||||
* @return array 返回应用的权限信息
|
||||
*/
|
||||
public function checkAppPermissions(): array
|
||||
{
|
||||
try {
|
||||
$accessToken = $this->getAccessToken();
|
||||
|
||||
// 尝试获取应用详情
|
||||
$url = 'https://qyapi.weixin.qq.com/cgi-bin/agent/get';
|
||||
$params = [
|
||||
'access_token' => $accessToken,
|
||||
'agentid' => config('pay.wechat_work.agent_id', 1000002),
|
||||
];
|
||||
|
||||
$response = $this->httpGet($url, $params);
|
||||
|
||||
return [
|
||||
'success' => isset($response['agentid']),
|
||||
'data' => $response,
|
||||
'message' => isset($response['errmsg']) ? $response['errmsg'] : 'OK',
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'data' => [],
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes HTTP GET请求
|
||||
*/
|
||||
private function httpGet(string $url, array $params = []): array
|
||||
{
|
||||
if (!empty($params)) {
|
||||
$url .= '?' . http_build_query($params);
|
||||
}
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
Log::error('HTTP请求失败: ' . $url . ', HTTP Code: ' . $httpCode);
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = json_decode($response, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
Log::error('JSON解析失败: ' . $response);
|
||||
return [];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user