first commit

This commit is contained in:
Your Name
2026-09-08 11:40:15 +08:00
commit a5353f7eb5
9568 changed files with 1646214 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\common\command;
use app\common\enum\CrontabEnum;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use Cron\CronExpression;
use think\facade\Console;
use app\common\model\Crontab as CrontabModel;
/**
* 定时任务
* Class Crontab
* @package app\command
*/
class Crontab extends Command
{
protected function configure()
{
$this->setName('crontab')
->setDescription('定时任务');
}
protected function execute(Input $input, Output $output)
{
$lists = CrontabModel::where('status', CrontabEnum::START)->select()->toArray();
if (empty($lists)) {
return false;
}
$time = time();
foreach ($lists as $item) {
if (empty($item['last_time'])) {
$lastTime = (new CronExpression($item['expression']))
->getNextRunDate()
->getTimestamp();
CrontabModel::where('id', $item['id'])->update([
'last_time' => $lastTime,
]);
continue;
}
$nextTime = (new CronExpression($item['expression']))
->getNextRunDate($item['last_time'])
->getTimestamp();
if ($nextTime > $time) {
// 未到时间,不执行
continue;
}
// 开始执行
self::start($item);
}
}
public static function start($item)
{
// 开始执行
$startTime = microtime(true);
try {
$params = explode(' ', $item['params']);
if (is_array($params) && !empty($item['params'])) {
Console::call($item['command'], $params);
} else {
Console::call($item['command']);
}
// 清除错误信息
CrontabModel::where('id', $item['id'])->update(['error' => '']);
} catch (\Exception $e) {
// 记录错误信息
CrontabModel::where('id', $item['id'])->update([
'error' => $e->getMessage(),
'status' => CrontabEnum::ERROR
]);
} finally {
$endTime = microtime(true);
// 本次执行时间
$useTime = round(($endTime - $startTime), 2);
// 最大执行时间
$maxTime = max($useTime, $item['max_time']);
// 更新最后执行时间
CrontabModel::where('id', $item['id'])->update([
'last_time' => time(),
'time' => $useTime,
'max_time' => $maxTime
]);
}
}
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace app\common\command;
use think\console\Input;
use think\console\Output;
/**
* 定时:补全订单 creator_id(逻辑在 SyncWechatWorkBills::fillOrderCreatorsFromPayee
*/
class FillOrderCreatorFromPayee extends SyncWechatWorkBills
{
protected function configure()
{
$this->setName('fill_order_creator_from_payee')
->setDescription('补全订单创建人:creator_id 为空时按 payee_userid 匹配 zyt_admin.work_wechat_userid');
}
protected function execute(Input $input, Output $output)
{
$fixed = $this->fillOrderCreatorsFromPayee();
$output->writeln("补全创建人完成,共更新 {$fixed} 条订单");
}
}
+202
View File
@@ -0,0 +1,202 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\common\command;
use app\common\enum\PayEnum;
use app\common\enum\RefundEnum;
use app\common\model\recharge\RechargeOrder;
use app\common\model\refund\RefundLog;
use app\common\model\refund\RefundRecord;
use app\common\service\pay\WeChatPayService;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Log;
class QueryRefund extends Command
{
protected function configure()
{
$this->setName('query_refund')
->setDescription('订单退款状态处理');
}
protected function execute(Input $input, Output $output)
{
try {
// 查找退款中的退款记录(微信,支付宝支付)
$refundRecords = (new RefundLog())->alias('l')
->join('refund_record r', 'r.id = l.record_id')
->field([
'l.id' => 'log_id', 'l.sn' => 'log_sn',
'r.id' => 'record_id', 'r.order_id', 'r.sn' => 'record_sn', 'r.order_type'
])
->where(['l.refund_status' => RefundEnum::REFUND_ING])
->select()->toArray();
if (empty($refundRecords)) {
return false;
}
// 分别处理各个类型订单
$rechargeRecords = array_filter($refundRecords, function ($item) {
return $item['order_type'] == RefundEnum::ORDER_TYPE_RECHARGE;
});
if (!empty($rechargeRecords)) {
$this->handleRechargeOrder($rechargeRecords);
}
return true;
} catch (\Exception $e) {
Log::write('订单退款状态查询失败,失败原因:' . $e->getMessage());
return false;
}
}
/**
* @notes 处理充值订单
* @param $refundRecords
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @author 段誉
* @date 2023/3/1 15:55
*/
public function handleRechargeOrder($refundRecords)
{
$orderIds = array_unique(array_column($refundRecords, 'order_id'));
$Orders = RechargeOrder::whereIn('id', $orderIds)->column('*', 'id');
foreach ($refundRecords as $record) {
if (!isset($Orders[$record['order_id']])) {
continue;
}
$order = $Orders[$record['order_id']];
if (!in_array($order['pay_way'], [PayEnum::WECHAT_PAY, PayEnum::ALI_PAY])) {
continue;
}
$this->checkReFundStatus([
'record_id' => $record['record_id'],
'log_id' => $record['log_id'],
'log_sn' => $record['log_sn'],
'pay_way' => $order['pay_way'],
'order_terminal' => $order['order_terminal'],
]);
}
}
/**
* @notes 校验退款状态
* @param $refundData
* @return bool
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @author 段誉
* @date 2023/3/1 15:54
*/
public function checkReFundStatus($refundData)
{
$result = null;
switch ($refundData['pay_way']) {
case PayEnum::WECHAT_PAY:
$result = self::checkWechatRefund($refundData['order_terminal'], $refundData['log_sn']);
break;
}
if (is_null($result)) {
return false;
}
if (true === $result) {
$this->updateRefundSuccess($refundData['log_id'], $refundData['record_id']);
} else {
$this->updateRefundMsg($refundData['log_id'], $result);
}
return true;
}
/**
* @notes 查询微信支付退款状态
* @param $orderTerminal
* @param $refundLogSn
* @return bool|string|null
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @author 段誉
* @date 2023/3/1 15:47
*/
public function checkWechatRefund($orderTerminal, $refundLogSn)
{
// 根据商户退款单号查询退款
$result = (new WeChatPayService($orderTerminal))->queryRefund($refundLogSn);
if (!empty($result['status']) && $result['status'] == 'SUCCESS') {
return true;
}
if (!empty($result['code']) || !empty($result['message'])) {
return '微信:' . $result['code'] . '-' . $result['message'];
}
return null;
}
/**
* @notes 更新记录为成功
* @param $logId
* @param $recordId
* @author 段誉
* @date 2023/3/1 15:38
*/
public function updateRefundSuccess($logId, $recordId)
{
// 更新日志
RefundLog::update([
'id' => $logId,
'refund_status' => RefundEnum::REFUND_SUCCESS,
]);
// 更新记录
RefundRecord::update([
'id' => $recordId,
'refund_status' => RefundEnum::REFUND_SUCCESS,
]);
}
/**
* @notes 更新退款信息
* @param $logId
* @param $msg
* @author 段誉
* @date 2023/3/1 15:47
*/
public function updateRefundMsg($logId, $msg)
{
// 更新日志
RefundLog::update([
'id' => $logId,
'refund_msg' => $msg,
]);
}
}
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace app\common\command;
use app\adminapi\logic\tcm\DiagnosisLogic;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
use think\facade\Log;
/**
* 从腾讯云 IM 拉取诊单单聊漫游消息并写入本地归档表(建议 cron 每几小时执行)
*/
class SyncImChatArchive extends Command
{
protected function configure()
{
$this->setName('sync_im_chat_archive')
->setDescription('同步诊单腾讯云 IM 聊天记录到数据库归档')
->addOption('since-days', null, Option::VALUE_OPTIONAL, '仅处理最近 N 天内更新过的诊单;0 表示不限制', '7')
->addOption('limit', 'l', Option::VALUE_OPTIONAL, '本轮最多处理的诊单数量(1-500)', '50')
->addOption('diagnosis-id', null, Option::VALUE_OPTIONAL, '只同步指定诊单 ID,设置后忽略 since-days', '0');
}
protected function execute(Input $input, Output $output)
{
$sinceDays = (int)$input->getOption('since-days');
$sinceDays = max(0, $sinceDays);
$limit = (int)$input->getOption('limit');
$onlyId = (int)$input->getOption('diagnosis-id');
$only = $onlyId > 0 ? $onlyId : null;
if ($only !== null) {
$output->writeln("同步诊单 ID={$only} ...");
} else {
$output->writeln('同步 IM 归档:since-days=' . ($sinceDays > 0 ? $sinceDays : '不限制') . ", limit={$limit}");
}
$stats = DiagnosisLogic::syncImChatArchiveBatch($sinceDays, $limit, $only);
$output->writeln("处理诊单数: {$stats['diagnoses']},新插入行数(INSERT IGNORE 成功数): {$stats['inserted']}");
if (!empty($stats['errors'])) {
foreach ($stats['errors'] as $e) {
$output->writeln("<error>{$e}</error>");
Log::error('sync_im_chat_archive: ' . $e);
}
}
}
}
@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace app\common\command;
use app\common\model\doctor\Medicine;
use app\common\service\doctor\MedicineNameAbbrService;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
/**
* 批量回填药材库 name_pinyin_abbr(拼音首字母,供检索)
*
* 用法:
* php think sync_medicine_pinyin_abbr # 仅更新 abbr 为空的记录
* php think sync_medicine_pinyin_abbr --all # 全部按当前名称重算
* php think sync_medicine_pinyin_abbr --dry # 只打印将更新多少条,不写库
*/
class SyncMedicinePinyinAbbr extends Command
{
protected function configure()
{
$this->setName('sync_medicine_pinyin_abbr')
->setDescription('回填/刷新药材表 name_pinyin_abbr(需已执行迁移并 composer 安装 overtrue/pinyin')
->addOption('all', null, Option::VALUE_NONE, '重算全部记录(默认只处理 abbr 为空的)')
->addOption('dry', null, Option::VALUE_NONE, '仅统计/预览,不写入数据库');
}
protected function execute(Input $input, Output $output)
{
if (!class_exists(\Overtrue\Pinyin\Pinyin::class)) {
$output->writeln('<error>未检测到 overtrue/pinyin,请在 server 目录执行: composer update</error>');
return 1;
}
$all = (bool) $input->getOption('all');
$dry = (bool) $input->getOption('dry');
$makeQuery = function () use ($all) {
$q = Medicine::order('id', 'asc');
if (!$all) {
$q->where('name_pinyin_abbr', '');
}
return $q;
};
$total = $makeQuery()->count();
if ($total === 0) {
$output->writeln('没有需要处理的记录。');
return 0;
}
$output->writeln(($all ? '模式: 全部重算' : '模式: 仅空 abbr') . ",待处理 {$total}" . ($dry ? 'dry-run' : ''));
$updated = 0;
$skipped = 0;
$makeQuery()->chunk(200, function ($rows) use (&$updated, &$skipped, $dry) {
foreach ($rows as $row) {
$data = $row instanceof \think\Model ? $row->toArray() : (array) $row;
$id = (int) ($data['id'] ?? 0);
$name = trim((string) ($data['name'] ?? ''));
if ($id <= 0 || $name === '') {
++$skipped;
continue;
}
$abbr = MedicineNameAbbrService::build($name);
if ($abbr === '') {
++$skipped;
continue;
}
if ($dry) {
++$updated;
continue;
}
Medicine::where('id', $id)->update([
'name_pinyin_abbr' => $abbr,
'update_time' => time(),
]);
++$updated;
}
});
if ($dry) {
$output->writeln("[预览] 将写入/覆盖拼音首字母: {$updated} 条,跳过(空名或无法生成): {$skipped}");
} else {
$output->writeln("完成: 已更新 {$updated} 条,跳过 {$skipped} 条。");
}
return 0;
}
}
@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace app\common\command;
use app\adminapi\logic\order\OrderLogic;
use app\common\model\auth\Admin;
use app\common\model\Order;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
use think\facade\Log;
/**
* 同步企业微信对外收款到订单
* 按天拉取,避免接口时间范围限制
*/
class SyncWechatWorkBills extends Command
{
protected function configure()
{
$this->setName('sync_wechat_work_bills')
->setDescription('同步企业微信对外收款到订单(按天拉取)')
->addOption('date', 'd', Option::VALUE_OPTIONAL, '指定日期 Y-m-d,默认今天', '')
->addOption('days', null, Option::VALUE_OPTIONAL, '拉取最近N天,每天单独请求', '1')
->addOption(
'fill-order-creators-only',
null,
Option::VALUE_NONE,
'仅补全订单创建人:creator_id 为空时用 payee_userid 匹配 admin.work_wechat_userid(可单独做定时任务)'
);
}
protected function execute(Input $input, Output $output)
{
if ($input->getOption('fill-order-creators-only')) {
$fixed = $this->fillOrderCreatorsFromPayee();
$output->writeln("补全创建人完成,共更新 {$fixed} 条订单");
return;
}
$dateStr = $input->getOption('date');
$days = (int)$input->getOption('days');
$days = $days > 0 ? min($days, 31) : 1;
$totalCreated = 0;
$totalUpdated = 0;
$totalSkipped = 0;
$errors = [];
if ($dateStr) {
$dates = [$dateStr];
} else {
$dates = [];
for ($i = 0; $i < $days; $i++) {
$dates[] = date('Y-m-d', strtotime("-{$i} days"));
}
}
foreach ($dates as $d) {
$beginTime = strtotime($d . ' 00:00:00');
$endTime = strtotime($d . ' 23:59:59');
$output->writeln("同步 {$d} ...");
$result = OrderLogic::syncFromWechatWorkBills($beginTime, $endTime);
$totalCreated += $result['created'];
$totalUpdated += $result['updated'];
$totalSkipped += $result['skipped'];
if (!empty($result['errors'])) {
$errors = array_merge($errors, $result['errors']);
}
$output->writeln(" -> 新增 {$result['created']} 更新 {$result['updated']} 跳过 {$result['skipped']}");
}
$output->writeln("完成: 共新增 {$totalCreated} 更新 {$totalUpdated} 跳过 {$totalSkipped}");
if (!empty($errors)) {
foreach ($errors as $e) {
$output->writeln("<error>{$e}</error>");
Log::error('sync_wechat_work_bills: ' . $e);
}
}
}
/**
* creator_id 为空的订单,用 payee_userid 匹配管理员 work_wechat_userid 写入 creator_id
*/
protected function fillOrderCreatorsFromPayee(): int
{
$fixed = 0;
Order::whereRaw('(creator_id IS NULL OR creator_id = 0)')
->whereNotNull('payee_userid')
->where('payee_userid', '<>', '')
->chunk(200, function ($orders) use (&$fixed) {
foreach ($orders as $order) {
$payee = trim((string) $order->getAttr('payee_userid'));
if ($payee === '') {
continue;
}
$admin = Admin::where('work_wechat_userid', $payee)->whereNull('delete_time')->find();
if (!$admin) {
continue;
}
$order->creator_id = (int) $admin->getAttr('id');
$order->save();
$fixed++;
}
});
return $fixed;
}
}
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace app\common\command;
use app\common\model\doctor\Appointment;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
use think\facade\Db;
use think\facade\Log;
/**
* 挂号单状态自动更新
* - 已预约(status=1) 且预约时间已过超过 35 分钟 → status=4(已过号)
* - 已过号(status=4) 且预约时间已过超过 8 小时 → status=2(已取消)
* - 不处理:status=3(已完成)、未到预约时间、距预约未满 35 分钟的单子
*/
class UpdateAppointmentStatus extends Command
{
protected function configure()
{
$this->setName('update_appointment_status')
->setDescription('挂号单:已预约过号35分钟→已过号;已过号超8小时→已取消')
->addOption('dry', null, Option::VALUE_NONE, '仅预览不执行');
}
protected function execute(Input $input, Output $output)
{
$dryRun = $input->getOption('dry');
$now = time();
$table = (new Appointment())->getTable();
$dtExpr = "CONCAT(appointment_date, ' ', IFNULL(appointment_time, '00:00:00'))";
// 1. 已预约 → 已过号:预约时间早于「当前 − 35 分钟」
$missedWhere = "status = 1 AND {$dtExpr} < DATE_SUB(NOW(), INTERVAL 35 MINUTE)";
$missedSql = "UPDATE {$table} SET status = 4, update_time = ? WHERE {$missedWhere}";
// 2. 已过号 → 已取消:预约时间早于或等于「当前 − 8 小时」
$cancelWhere = "status = 4 AND {$dtExpr} <= DATE_SUB(NOW(), INTERVAL 8 HOUR)";
$cancelSql = "UPDATE {$table} SET status = 2, update_time = ? WHERE {$cancelWhere}";
if ($dryRun) {
$missedPreview = Db::query("SELECT id FROM {$table} WHERE {$missedWhere}");
$cancelPreview = Db::query("SELECT id FROM {$table} WHERE {$cancelWhere}");
$output->writeln('[预览] 将改为已过号(1→4): ' . count($missedPreview) . ' 条');
$output->writeln('[预览] 将改为已取消(4→2,过号超8小时): ' . count($cancelPreview) . ' 条');
$output->writeln('[说明] 真实执行时先执行 1→4,再执行 4→2;同一次内刚由 1 变 4 且已超 8 小时的会再被改为 2。');
return;
}
$missedCount = Db::execute($missedSql, [$now]);
$cancelCount = Db::execute($cancelSql, [$now]);
$output->writeln("已过号(1→4): {$missedCount}");
$output->writeln("已取消(4→2,过号超8小时): {$cancelCount}");
Log::info("update_appointment_status: 已过号 {$missedCount}, 已取消 {$cancelCount}");
}
}