更新
This commit is contained in:
@@ -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,31 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model\doctor;
|
||||
|
||||
use app\common\model\BaseModel;
|
||||
|
||||
/**
|
||||
* 药品库模型
|
||||
*/
|
||||
class Medicine extends BaseModel
|
||||
{
|
||||
protected $name = 'doctor_medicine';
|
||||
|
||||
// 设置字段信息
|
||||
protected $schema = [
|
||||
'id' => 'int',
|
||||
'name' => 'string',
|
||||
'name_pinyin_abbr' => 'string',
|
||||
'supplier' => 'string',
|
||||
'unit' => 'string',
|
||||
'settlement_price' => 'float',
|
||||
'retail_price' => 'float',
|
||||
'stock' => 'int',
|
||||
'image' => 'string',
|
||||
'status' => 'int',
|
||||
'remark' => 'string',
|
||||
'create_time' => 'int',
|
||||
'update_time' => 'int',
|
||||
'delete_time' => 'int',
|
||||
];
|
||||
}
|
||||
@@ -47,7 +47,22 @@ class Roster extends BaseModel
|
||||
*/
|
||||
public function getPeriodDescAttr($value, $data)
|
||||
{
|
||||
return $data['period'] == 'morning' ? '上午' : '下午';
|
||||
$st = $data['start_time'] ?? '';
|
||||
$et = $data['end_time'] ?? '';
|
||||
if ($st !== '' && $et !== '') {
|
||||
return $st . '-' . $et;
|
||||
}
|
||||
if (($data['period'] ?? '') === 'morning') {
|
||||
return '上午';
|
||||
}
|
||||
if (($data['period'] ?? '') === 'afternoon') {
|
||||
return '下午';
|
||||
}
|
||||
if (($data['period'] ?? '') === 'night') {
|
||||
return '夜班';
|
||||
}
|
||||
|
||||
return '时段';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model\tcm;
|
||||
|
||||
use app\common\model\BaseModel;
|
||||
|
||||
/**
|
||||
* 诊单 IM 聊天记录归档
|
||||
*/
|
||||
class ImChatMessage extends BaseModel
|
||||
{
|
||||
protected $name = 'tcm_im_chat_message';
|
||||
|
||||
protected $autoWriteTimestamp = false;
|
||||
|
||||
protected $dateFormat = false;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service\doctor;
|
||||
|
||||
use Overtrue\Pinyin\Pinyin;
|
||||
|
||||
/**
|
||||
* 药材名称 → 拼音首字母连写(小写),供列表检索;未安装 overtrue/pinyin 时返回空字符串。
|
||||
*/
|
||||
class MedicineNameAbbrService
|
||||
{
|
||||
public static function build(string $name): string
|
||||
{
|
||||
$name = trim($name);
|
||||
if ($name === '') {
|
||||
return '';
|
||||
}
|
||||
if (!class_exists(Pinyin::class)) {
|
||||
return '';
|
||||
}
|
||||
$abbr = (string) Pinyin::abbr($name)->join('');
|
||||
|
||||
return strtolower($abbr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service\doctor;
|
||||
|
||||
/**
|
||||
* 排班时段:起止时间 + 号源间隔
|
||||
*/
|
||||
class RosterSegmentService
|
||||
{
|
||||
/**
|
||||
* 从排班记录解析 [开始 HH:mm, 结束 HH:mm],无效返回 null
|
||||
*/
|
||||
public static function resolveWindow(array $roster): ?array
|
||||
{
|
||||
$start = isset($roster['start_time']) ? trim((string) $roster['start_time']) : '';
|
||||
$end = isset($roster['end_time']) ? trim((string) $roster['end_time']) : '';
|
||||
|
||||
if ($start !== '' && $end !== ''
|
||||
&& preg_match('/^\d{2}:\d{2}$/', $start)
|
||||
&& preg_match('/^\d{2}:\d{2}$/', $end)
|
||||
&& $start < $end) {
|
||||
return [$start, $end];
|
||||
}
|
||||
|
||||
$p = $roster['period'] ?? '';
|
||||
|
||||
if ($p == 1 || $p === 'morning' || $p === '上午') {
|
||||
return ['09:00', '12:00'];
|
||||
}
|
||||
if ($p == 2 || $p === 'afternoon' || $p === '下午') {
|
||||
return ['14:00', '18:00'];
|
||||
}
|
||||
if ($p === 'night' || $p === '夜班' || $p === 'evening') {
|
||||
return ['18:00', '22:00'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function normalizeSlotMinutes($raw): int
|
||||
{
|
||||
$n = (int) $raw;
|
||||
if ($n < 5) {
|
||||
return 15;
|
||||
}
|
||||
if ($n > 120) {
|
||||
return 120;
|
||||
}
|
||||
|
||||
return $n;
|
||||
}
|
||||
|
||||
/**
|
||||
* 左闭右开 [start, end) 按 slotMinutes 切分,得到每个号的开始时刻 HH:mm
|
||||
*/
|
||||
public static function generateSlotTimes(string $start, string $end, int $slotMinutes): array
|
||||
{
|
||||
$slotMinutes = self::normalizeSlotMinutes($slotMinutes);
|
||||
$base = '1970-01-01 ';
|
||||
$cur = strtotime($base . $start . ':00');
|
||||
$endTs = strtotime($base . $end . ':00');
|
||||
if ($cur === false || $endTs === false || $endTs <= $cur) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$times = [];
|
||||
$step = $slotMinutes * 60;
|
||||
while ($cur < $endTs) {
|
||||
$times[] = date('H:i', $cur);
|
||||
$cur += $step;
|
||||
}
|
||||
|
||||
return $times;
|
||||
}
|
||||
|
||||
/**
|
||||
* quota>0 时限制最大可约槽位数;quota=0 表示不限制(由时段长度决定)
|
||||
*/
|
||||
public static function applyQuotaCap(array $times, int $quota): array
|
||||
{
|
||||
if ($quota > 0 && count($times) > $quota) {
|
||||
return array_slice($times, 0, $quota);
|
||||
}
|
||||
|
||||
return $times;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user