Merge branch 'master' of ssh://47.101.48.251:/data/git/wx.sstbc.com

master
lion 10 months ago
commit e61ad953ae

1
.gitignore vendored

@ -13,3 +13,4 @@ npm-debug.log
yarn-error.log
/.idea
/.vscode
/.snapshots

@ -0,0 +1,30 @@
<?php
namespace App\Console\Commands;
use App\Services\TestData\SupplyDemandTestDataGenerator;
use Illuminate\Console\Command;
class GenerateSupplyDemandDemo extends Command
{
protected $signature = 'demo:generate-supply-demand {--count=50} {--users=20}';
protected $description = '生成供需模块演示数据(含附件、会话、消息、收藏等)';
public function handle(): int
{
$count = (int)$this->option('count');
$users = (int)$this->option('users');
$this->info("开始生成:供需 {$count} 条,最少用户 {$users} 个...");
$generator = new SupplyDemandTestDataGenerator();
$generator->generate($count, $users, function (string $msg) {
$this->line($msg);
});
$this->info('生成完成');
return self::SUCCESS;
}
}

@ -0,0 +1,302 @@
<?php
namespace App\Console\Commands;
use App\Models\Course;
use App\Models\Calendar;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class LinkCoursesToCalendar extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'link:courses-to-calendar';
/**
* The console command description.
*
* @var string
*/
protected $description = '将指定的课程列表关联到calendars日历表';
/**
* 课程列表
*
* @var array
*/
protected $courseList = [
'第三期:张平院士— 6G通信与AI融合',
'高研班|第四期高级科创人才研修班-第七模块',
'高研班|第五期高级科创人才研修班-第五模块',
'校友返校日AI+产业融合创新论坛',
'高研班|第六期高级科创人才研修班-第三模块',
'第二课堂|走进珂玛科技',
'人才培训|省科技厅高级技术经理人专班-开学模块',
'初创班|首期技术经理人领航班-第一模块',
'专题培训|苏州市科技企业资本运作公开课',
'初创班|首期高校科技成果转化班-第一模块',
'高研班|第五期高级科创人才研修班-毕业模块',
'高研班|第四期高级科创人才研修班-第八模块',
'第二课堂|走进世华科技',
'初创班|首期高校科技成果转化班-第二模块',
'高研班|第六期高级科创人才研修班-第四模块',
'人才培训|省科技厅高级技术经理人专班-实践模块',
'初创班|首期技术经理人领航班-第二模块',
'攀峰班|首期苏州科技企业资本运作研修班-开学模块',
'初创班|首期技术经理人领航班-结业模块',
'高研班|第七期高级科创人才研修班-开学模块',
'产业加速营|具身智能极客营-开学模块',
'产业加速营|苏州市人工智能潜在独角兽训练营-开学模块',
'初创班|首期高校科技成果转化班-结业模块',
'第二课堂|走进姑苏区',
'高研班|第六期高级科创人才研修班-第五模块',
'产业加速营|苏州市人工智能潜在独角兽训练营-第二模块',
'夏令营2025年度小科学家夏令营',
'攀峰班|首期苏州科技企业资本运作研修班-第二模块',
'产业加速营|苏州市人工智能潜在独角兽训练营-第三模块',
'人才培训|江苏青年科技人才"U35青创学院"培训',
'高研班|第七期高级科创人才研修班-第二模块',
'第二课堂|走进科沃斯',
'高研班|第四期高级科创人才研修班-开学模块',
'高研班|第三期高级科创人才研修班-结业模块',
'第二课堂|走进永鼎',
'第二课堂|走进旭创',
'高研班|第五期高级科创人才研修班-开学模块',
'高研班|第四期高级科创人才研修班-第二模块',
'第二课堂|走进亨通',
'第二课堂|走进企查查',
'科技大讲堂|第一期: 凯文凯利、丁文江院士领衔',
'高研班|第四期高级科创人才研修班-第三模块',
'第二课堂|走进华为苏研所',
'人才培训2024年姑苏领军人才培育营',
'专题培训|苏州市标杆孵化器培训',
'高研班|第五期高级科创人才研修班-第一模块',
'第二课堂|走进亚盛医药',
'第二课堂|新加坡海外游学',
'第二课堂|走进苏州市市场监督管理局',
'第二课堂|走进信达生物',
'人才培训|江苏省高层次人才专题培训',
'夏令营2024年度小科学家夏令营',
'高研班|第五期高级科创人才研修班-第二模块',
'高研班|第四期高级科创人才研修班-第四模块',
'专题培训|太仓市国资审计高质量发展专题培训',
'第二课堂|走进天准科技',
'高研班|第五期高级科创人才研修班-第三模块',
'科技金融沙龙|上海交通大学新能源沙龙',
'科技大讲堂|第二期:伊雷娜·克罗宁解析空间计算',
'人才培训|苏州市科技企业孵化器沙龙',
'高研班|第四期高级科创人才研修班-第五模块',
'人才培训|苏州乡镇党委书记专题研修班',
'科技金融沙龙|资本市场新机遇研讨沙龙',
'产业加速营|人工智能产业加速营',
'高研班|第六期高级科创人才研修班-开学模块',
'第二课堂|走进苏州市低空经济发展展示馆',
'高研班|第四期高级科创人才研修班-第六模块',
'人才培训2024姑苏领军人才创业营',
'产业加速营|集成电路产业专班',
'专题培训关税与出海应对2025美国新政',
'高研班|第五期高级科创人才研修班-第四模块',
'高研班|第六期高级科创人才研修班-第二模块',
];
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->info("开始将课程关联到calendars日历表...");
$this->info("总共需要处理 " . count($this->courseList) . " 个课程");
$linkedCount = 0;
$notFoundCourses = [];
$alreadyLinkedCourses = [];
DB::beginTransaction();
try {
foreach ($this->courseList as $courseName) {
$this->info("正在处理课程: {$courseName}");
// 查找匹配的课程
$course = $this->findCourse($courseName);
if (!$course) {
$this->warn("✗ 未找到匹配的课程: {$courseName}");
$notFoundCourses[] = $courseName;
continue;
}
$this->info("✓ 找到匹配课程: {$course->name} (ID: {$course->id})");
// 检查是否已经存在日历记录
$existingCalendar = Calendar::where('course_id', $course->id)
->where('type', 1) // 类型1为课程
->first();
if ($existingCalendar) {
$this->warn("⚠ 课程已存在日历记录: {$course->name}");
$alreadyLinkedCourses[] = $course->name;
continue;
}
// 创建日历记录
$calendarData = $this->createCalendarData($course);
$calendar = Calendar::create($calendarData);
$this->info("✓ 成功创建日历记录 (ID: {$calendar->id}) 关联课程: {$course->name}");
$linkedCount++;
}
DB::commit();
$this->info("\n" . str_repeat('=', 60));
$this->info("处理完成!");
$this->info("成功关联课程数量: {$linkedCount}");
$this->info("已存在日历记录: " . count($alreadyLinkedCourses));
$this->info("未找到匹配课程: " . count($notFoundCourses));
// 显示未找到的课程
if (!empty($notFoundCourses)) {
$this->warn("\n未找到匹配的课程列表:");
foreach ($notFoundCourses as $course) {
$this->warn(" - {$course}");
}
}
// 显示已存在日历记录的课程
if (!empty($alreadyLinkedCourses)) {
$this->warn("\n已存在日历记录的课程列表:");
foreach ($alreadyLinkedCourses as $course) {
$this->warn(" - {$course}");
}
}
} catch (\Exception $e) {
DB::rollback();
$this->error("处理过程中发生错误: " . $e->getMessage());
$this->error("已回滚所有更改");
return;
}
$this->info("\n所有操作已完成");
}
/**
* 查找匹配的课程
*/
private function findCourse($courseName)
{
// 1. 精确匹配
$course = Course::where('name', $courseName)
->whereNull('deleted_at')
->first();
if ($course) {
return $course;
}
// 2. 模糊匹配
$course = Course::where('name', 'like', "%{$courseName}%")
->whereNull('deleted_at')
->first();
if ($course) {
$this->info("通过模糊匹配找到课程: '{$course->name}'");
return $course;
}
// 3. 相似度匹配
$courses = Course::whereNull('deleted_at')
->whereNotNull('name')
->where('name', '!=', '')
->get();
$bestMatch = null;
$highestSimilarity = 0;
foreach ($courses as $course) {
$similarity = $this->calculateSimilarity($courseName, $course->name);
if ($similarity > $highestSimilarity) {
$highestSimilarity = $similarity;
$bestMatch = $course;
}
}
if ($bestMatch && $highestSimilarity > 0.3) { // 设置最低相似度阈值
$this->info("通过相似度匹配找到课程 (相似度: " . round($highestSimilarity * 100, 2) . "%): '{$bestMatch->name}'");
return $bestMatch;
}
return null;
}
/**
* 创建日历数据
*/
private function createCalendarData($course)
{
return [
'type' => 1, // 类型1为课程
'course_id' => $course->id,
'date' => $course->start_date ?? now()->format('Y-m-d'),
'title' => $course->name,
'content' => $course->content ?? '',
'start_time' => $course->start_date ? $course->start_date . ' 09:00:00' : null,
'end_time' => $course->end_date ? $course->end_date . ' 17:00:00' : null,
'url' => $course->url ?? '',
'created_at' => now(),
'updated_at' => now(),
];
}
/**
* 计算字符串相似度
*/
private function calculateSimilarity($str1, $str2)
{
// 移除空格并转换为小写
$str1 = strtolower(preg_replace('/\s+/', '', $str1));
$str2 = strtolower(preg_replace('/\s+/', '', $str2));
if ($str1 === $str2) {
return 1.0;
}
if (empty($str1) || empty($str2)) {
return 0.0;
}
// 使用Levenshtein距离计算相似度
$maxLen = max(strlen($str1), strlen($str2));
if ($maxLen == 0) {
return 1.0;
}
$distance = levenshtein($str1, $str2);
$similarity = 1 - ($distance / $maxLen);
// 如果其中一个字符串包含另一个,提高相似度
if (strpos($str1, $str2) !== false || strpos($str2, $str1) !== false) {
$containsSimilarity = min(strlen($str1), strlen($str2)) / $maxLen;
$similarity = max($similarity, $containsSimilarity);
}
return max(0, $similarity);
}
}

@ -0,0 +1,58 @@
<?php
namespace App\Console\Commands;
use App\Services\TestData\CourseContentEvaluationTestDataGenerator;
use Illuminate\Console\Command;
class SeedCourseContentEvaluations extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'seed:course-evaluations
{--evaluations=20 : 生成评价问卷数量}
{--users=50 : 最少用户数量}';
/**
* The console command description.
*
* @var string
*/
protected $description = '生成课程内容评价模块的测试数据,包括问卷、问题字段和用户提交数据';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$evaluationCount = (int) $this->option('evaluations');
$minUsers = (int) $this->option('users');
$this->info("开始生成课程内容评价测试数据...");
$this->info("计划生成 {$evaluationCount} 个评价问卷,确保至少 {$minUsers} 个用户");
$generator = new CourseContentEvaluationTestDataGenerator();
$startTime = microtime(true);
try {
$generator->generate($evaluationCount, $minUsers, function (string $message) {
$this->line(" → {$message}");
});
$duration = round(microtime(true) - $startTime, 2);
$this->info("✅ 数据生成完成!耗时 {$duration} 秒");
return Command::SUCCESS;
} catch (\Exception $e) {
$this->error("❌ 数据生成失败:" . $e->getMessage());
return Command::FAILURE;
}
}
}

@ -0,0 +1,331 @@
<?php
namespace App\Console\Commands;
use App\Models\Book;
use App\Models\Config;
use App\Models\Upload;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class UpdateBookIsbnData extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'book:update-isbn-data {--book_id=} ';
/**
* The console command description.
*
* @var string
*/
protected $description = '从ISBN接口获取书籍数据更新出版社、作者、出版年份、简介等信息并下载封面图片';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$apiKey = Config::getValueByKey('book_key');
$book_id = $this->option('book_id');
// 获取所有有ISBN的书籍
$books = Book::whereNotNull('isbn')
->where(function ($query) use ($book_id) {
if ($book_id) {
$query->where('id', $book_id);
}
})->where('isbn', '!=', '')
->where(function ($query) {
// 如果没有封面或者缺少基本信息字段,都需要处理
$query->whereNull('cover_id')
->orWhereNull('publisher')
->orWhereNull('author')
->orWhereNull('publish_year')
->orWhereNull('description')
->orWhere('publisher', '')
->orWhere('author', '')
->orWhere('publish_year', '')
->orWhere('description', '');
})->get();
if ($books->isEmpty()) {
$this->info('没有找到需要更新数据的书籍');
return 0;
}
$this->info("找到 {$books->count()} 本书需要处理");
$this->info("API限制每秒最多10次请求预计耗时约 " . ceil($books->count() / 10) . " 秒");
$bar = $this->output->createProgressBar($books->count());
$bar->start();
$successCount = 0;
$failCount = 0;
$requestCount = 0;
$startTime = microtime(true);
$lastResetTime = $startTime;
foreach ($books as $book) {
try {
// 每秒重置请求计数器
$currentTime = microtime(true);
if ($currentTime - $lastResetTime >= 1.0) {
$requestCount = 0;
$lastResetTime = $currentTime;
}
// API频率限制控制每秒最多10次请求
if ($requestCount >= 10) {
$waitTime = 1.0 - ($currentTime - $lastResetTime);
if ($waitTime > 0) {
usleep(intval($waitTime * 1000000));
$requestCount = 0;
$lastResetTime = microtime(true);
}
}
$result = $this->processBook($book, $apiKey);
$requestCount++;
if ($result) {
$successCount++;
$this->line("\n✓ 成功处理书籍: {$book->title} (ISBN: {$book->isbn})");
} else {
$failCount++;
$this->line("\n✗ 处理失败: {$book->title} (ISBN: {$book->isbn})");
}
} catch (\Exception $e) {
$failCount++;
$this->line("\n✗ 处理异常: {$book->title} - {$e->getMessage()}");
// 如果是API相关错误增加等待时间
if (strpos($e->getMessage(), 'API') !== false || strpos($e->getMessage(), 'HTTP') !== false) {
$this->line("检测到API错误等待2秒后继续...");
sleep(2);
$requestCount = 0;
$lastResetTime = microtime(true);
}
}
$bar->advance();
}
$bar->finish();
$totalTime = microtime(true) - $startTime;
$avgTimePerBook = $totalTime / $books->count();
$actualRequestsPerSecond = $books->count() / $totalTime;
$this->line('');
$this->info("处理完成!");
$this->info("成功: {$successCount}, 失败: {$failCount}");
$this->info("总耗时: " . round($totalTime, 2) . " 秒");
$this->info("平均每本书耗时: " . round($avgTimePerBook, 2) . " 秒");
$this->info("实际请求频率: " . round($actualRequestsPerSecond, 2) . " 次/秒");
return 0;
}
/**
* 处理单本书籍
*
* @param Book $book
* @param string $apiKey
* @return bool
*/
private function processBook(Book $book, string $apiKey): bool
{
$attempt = 0;
$maxRetries = 3;
while ($attempt < $maxRetries) {
try {
$attempt++;
// 调用ISBN接口
$response = Http::timeout(30)->get('https://api.tanshuapi.com/api/isbn/v2/index', [
'key' => $apiKey,
'isbn' => $book->isbn
]);
if (!$response->successful()) {
throw new \Exception("API请求失败: HTTP {$response->status()}");
}
$data = $response->json();
if (!$data || $data['code'] !== 1) {
// 如果是API密钥错误或其他不可重试的错误直接返回失败
if (isset($data['code']) && in_array($data['code'], [10001, 10002, 10003])) {
$this->error("API返回不可重试错误: " . ($data['msg'] ?? '未知错误'));
return false;
}
throw new \Exception("API返回错误: " . ($data['msg'] ?? '未知错误'));
}
$bookData = $data['data'];
// 更新书籍的基本信息字段
if (!empty($bookData['publisher'])) {
$book->publisher = $bookData['publisher'];
}
if (!empty($bookData['author'])) {
$book->author = $bookData['author'];
}
if (!empty($bookData['pubdate'])) {
$book->publish_year = $bookData['pubdate'];
}
if (!empty($bookData['summary'])) {
$book->description = $bookData['summary'];
}
// 更新书籍的other_data字段保存完整的API响应数据
$book->other_data = $bookData;
// 如果有图片URL下载图片
if (!empty($bookData['img'])) {
$coverId = $this->downloadAndSaveImage($bookData['img'], $book);
if ($coverId) {
$book->cover_id = $coverId;
}
}
$book->save();
return true;
} catch (\Exception $e) {
if ($attempt >= $maxRetries) {
$this->error("重试 {$maxRetries} 次后仍然失败: {$e->getMessage()}");
return false;
}
// 指数退避第1次重试等待1秒第2次等待2秒第3次等待4秒
$waitTime = pow(2, $attempt - 1);
$this->line("第 {$attempt} 次尝试失败,{$waitTime} 秒后重试: {$e->getMessage()}");
sleep($waitTime);
}
}
return false;
}
/**
* 下载图片并保存到本地
*
* @param string $imageUrl
* @param Book $book
* @return int|null
*/
private function downloadAndSaveImage(string $imageUrl, Book $book): ?int
{
try {
// 下载图片
$response = Http::timeout(30)->get($imageUrl);
if (!$response->successful()) {
$this->error("图片下载失败: {$imageUrl}");
return null;
}
$imageContent = $response->body();
// 获取文件扩展名
$extension = $this->getImageExtension($imageUrl, $response->header('Content-Type'));
// 生成文件名
$filename = 'book_cover_' . $book->id . '_' . time() . '.' . $extension;
// 定义存储目录
$folder = 'uploads/book_covers';
// 确保目录存在
$fullPath = public_path($folder);
if (!file_exists($fullPath)) {
mkdir($fullPath, 0755, true);
}
// 保存文件
$filePath = $folder . '/' . $filename;
file_put_contents(public_path($filePath), $imageContent);
// 获取文件大小
$fileSize = strlen($imageContent);
// 创建uploads记录
$upload = Upload::create([
'belongs_type' => 'App\\Models\\Book',
'belongs_id' => $book->id,
'original_name' => basename($imageUrl),
'folder' => $folder,
'name' => $filename,
'extension' => $extension,
'size' => $fileSize,
'creator_type' => 'console',
'creator_id' => null,
]);
return $upload->id;
} catch (\Exception $e) {
$this->error("保存图片时出错: {$e->getMessage()}");
return null;
}
}
/**
* 获取图片扩展名
*
* @param string $url
* @param string|null $contentType
* @return string
*/
private function getImageExtension(string $url, ?string $contentType = null): string
{
// 首先尝试从URL获取扩展名
$pathInfo = pathinfo(parse_url($url, PHP_URL_PATH));
if (!empty($pathInfo['extension'])) {
return strtolower($pathInfo['extension']);
}
// 从Content-Type获取扩展名
if ($contentType) {
$mimeToExt = [
'image/jpeg' => 'jpg',
'image/jpg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
'image/bmp' => 'bmp',
];
$contentType = strtolower(trim(explode(';', $contentType)[0]));
if (isset($mimeToExt[$contentType])) {
return $mimeToExt[$contentType];
}
}
// 默认返回jpg
return 'jpg';
}
}

@ -60,7 +60,7 @@ class UpdateCompany extends Command
$users = User::where('id', $user_id)->get();
} else {
// 批量更新
$users = User::whereDoesntHave('company')->get();
$users = User::whereDoesntHave('company')->whereNotNull('company_name')->get();
}
$YuanheRepository = new YuanheRepository();
foreach ($users as $user) {

@ -0,0 +1,313 @@
<?php
namespace App\Console\Commands;
use App\Models\Course;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Facades\Excel;
use PhpOffice\PhpSpreadsheet\IOFactory;
class UpdateCourseUrls extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'update:course-urls {file=课程台账.xlsx}';
/**
* The console command description.
*
* @var string
*/
protected $description = '从Excel文件读取课程信息匹配phome_ecms_news表的titleurl并更新courses表的url字段';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$fileName = $this->argument('file');
$filePath = base_path($fileName);
if (!file_exists($filePath)) {
$this->error("文件不存在: {$filePath}");
return;
}
$this->info("开始处理文件: {$fileName}");
try {
// 读取Excel文件
$spreadsheet = IOFactory::load($filePath);
$sheetCount = $spreadsheet->getSheetCount();
$this->info("Excel文件包含 {$sheetCount} 个工作表");
$totalUpdated = 0;
// 处理每个工作表
for ($sheetIndex = 0; $sheetIndex < $sheetCount; $sheetIndex++) {
$worksheet = $spreadsheet->getSheet($sheetIndex);
$sheetName = $worksheet->getTitle();
$this->info("正在处理工作表: {$sheetName}");
$updated = $this->processWorksheet($worksheet, $sheetName);
$totalUpdated += $updated;
}
$this->info("处理完成,总共更新了 {$totalUpdated} 条记录");
} catch (\Exception $e) {
$this->error("处理Excel文件时发生错误: " . $e->getMessage());
return;
}
}
/**
* 处理单个工作表
*/
private function processWorksheet($worksheet, $sheetName)
{
$highestRow = $worksheet->getHighestRow();
$highestColumn = $worksheet->getHighestColumn();
$this->info("工作表 {$sheetName} 有 {$highestRow} 行,最高列为 {$highestColumn}");
// 读取第一行作为表头
$headers = [];
$highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn);
for ($col = 1; $col <= $highestColumnIndex; $col++) {
$cellValue = $worksheet->getCellByColumnAndRow($col, 1)->getCalculatedValue();
$headers[$col] = trim($cellValue);
}
$this->info("表头: " . implode(', ', $headers));
// 找到"课程"和"跳转链接"列的位置
$courseColumn = null;
$linkColumn = null;
foreach ($headers as $colIndex => $header) {
if (strpos($header, '课程') !== false) {
$courseColumn = $colIndex;
}
if (strpos($header, '跳转链接') !== false) {
$linkColumn = $colIndex;
}
}
if (!$courseColumn || !$linkColumn) {
$this->warn("工作表 {$sheetName} 中未找到'课程'或'跳转链接'列");
return 0;
}
$this->info("找到课程列: {$courseColumn},跳转链接列: {$linkColumn}");
$updated = 0;
$failedCourses = [];
// 处理数据行
for ($row = 2; $row <= $highestRow; $row++) {
$courseName = trim($worksheet->getCellByColumnAndRow($courseColumn, $row)->getCalculatedValue());
$jumpLink = trim($worksheet->getCellByColumnAndRow($linkColumn, $row)->getCalculatedValue());
if (empty($courseName) || empty($jumpLink)) {
continue;
}
$this->info("处理行 {$row}: 课程='{$courseName}', 跳转链接='{$jumpLink}'");
// 从phome_ecms_news表获取titleurl
$titleUrl = $this->getTitleUrlFromNews($jumpLink);
if ($titleUrl) {
// 更新courses表
$updateCount = $this->updateCourseUrl($courseName, $titleUrl);
$updated += $updateCount;
if ($updateCount > 0) {
$this->info("✓ 成功更新课程 '{$courseName}' 的URL为: {$titleUrl}");
} else {
$this->warn("✗ 未找到匹配的课程: '{$courseName}'");
$failedCourses[] = $courseName;
}
} else {
$this->warn("✗ 未找到匹配的新闻标题: '{$jumpLink}'");
}
}
// 显示匹配失败的课程
if (!empty($failedCourses)) {
$this->warn("工作表 {$sheetName} 中匹配失败的课程:");
foreach ($failedCourses as $failedCourse) {
$this->warn(" - {$failedCourse}");
}
}
return $updated;
}
/**
* 从phome_ecms_news表获取titleurl
*/
private function getTitleUrlFromNews($title)
{
try {
// 直接匹配
$news = DB::table('phome_ecms_news')
->where('title', $title)
->first();
if ($news && !empty($news->titleurl)) {
return $news->titleurl;
}
// 模糊匹配
$news = DB::table('phome_ecms_news')
->where('title', 'like', "%{$title}%")
->first();
if ($news && !empty($news->titleurl)) {
$this->info("通过模糊匹配找到: '{$news->title}' -> '{$news->titleurl}'");
return $news->titleurl;
}
// 使用相似度匹配
$allNews = DB::table('phome_ecms_news')
->whereNotNull('title')
->whereNotNull('titleurl')
->where('title', '!=', '')
->where('titleurl', '!=', '')
->get();
$bestMatch = null;
$highestSimilarity = 0;
foreach ($allNews as $news) {
$similarity = $this->calculateSimilarity($title, $news->title);
if ($similarity > $highestSimilarity) {
$highestSimilarity = $similarity;
$bestMatch = $news;
}
}
if ($bestMatch && $highestSimilarity > 0) {
$this->info("通过相似度匹配找到 (相似度: " . round($highestSimilarity * 100, 2) . "%): '{$bestMatch->title}' -> '{$bestMatch->titleurl}'");
return $bestMatch->titleurl;
}
} catch (\Exception $e) {
$this->error("查询phome_ecms_news表时发生错误: " . $e->getMessage());
}
return null;
}
/**
* 更新courses表的url字段
*/
private function updateCourseUrl($courseName, $titleUrl)
{
try {
// 直接匹配
$updateCount = Course::where('name', $courseName)
->whereNull('deleted_at')
->update(['url' => $titleUrl]);
if ($updateCount > 0) {
return $updateCount;
}
// 模糊匹配
$updateCount = Course::where('name', 'like', "%{$courseName}%")
->whereNull('deleted_at')
->update(['url' => $titleUrl]);
if ($updateCount > 0) {
$this->info("通过模糊匹配更新了课程");
return $updateCount;
}
// 使用相似度匹配
$courses = Course::whereNull('deleted_at')
->whereNotNull('name')
->where('name', '!=', '')
->get();
$bestMatch = null;
$highestSimilarity = 0;
foreach ($courses as $course) {
$similarity = $this->calculateSimilarity($courseName, $course->name);
if ($similarity > $highestSimilarity) {
$highestSimilarity = $similarity;
$bestMatch = $course;
}
}
if ($bestMatch && $highestSimilarity > 0) {
$bestMatch->url = $titleUrl;
$bestMatch->save();
$this->info("通过相似度匹配更新了课程 (相似度: " . round($highestSimilarity * 100, 2) . "%): '{$bestMatch->name}'");
return 1;
}
} catch (\Exception $e) {
$this->error("更新courses表时发生错误: " . $e->getMessage());
}
return 0;
}
/**
* 计算字符串相似度
*/
private function calculateSimilarity($str1, $str2)
{
// 移除空格并转换为小写
$str1 = strtolower(preg_replace('/\s+/', '', $str1));
$str2 = strtolower(preg_replace('/\s+/', '', $str2));
if ($str1 === $str2) {
return 1.0;
}
if (empty($str1) || empty($str2)) {
return 0.0;
}
// 使用Levenshtein距离计算相似度
$maxLen = max(strlen($str1), strlen($str2));
if ($maxLen == 0) {
return 1.0;
}
$distance = levenshtein($str1, $str2);
$similarity = 1 - ($distance / $maxLen);
// 如果其中一个字符串包含另一个,提高相似度
if (strpos($str1, $str2) !== false || strpos($str2, $str1) !== false) {
$containsSimilarity = min(strlen($str1), strlen($str2)) / $maxLen;
$similarity = max($similarity, $containsSimilarity);
}
return max(0, $similarity);
}
}

@ -2,6 +2,8 @@
namespace App\Console\Commands;
use App\Models\Course;
use App\Models\CourseSign;
use App\Models\User;
use App\Repositories\MeetRepository;
use Illuminate\Console\Command;
@ -40,13 +42,40 @@ class UpdateUserNo extends Command
*/
public function handle()
{
$users = User::whereNull('no')->get();
foreach ($users as $user) {
$no = User::updateNo($user->id);
$this->info($no . '更新成功');
// 已经开始的课程日期(所有历史数据处理)
// $dateList = Course::whereNotNull('start_date')
// ->where('start_date', '<=', date('Y-m-d'))
// ->orderBy('start_date')
// ->groupBy('start_date')
// ->pluck('start_date')
// ->toArray();
// 当日数据处理(日常定时任务)
$dateList = [date('Y-m-d')];
foreach ($dateList as $date) {
$courses = Course::with(['courseSigns' => function ($query) {
$query->where('status', 1);
}])->where('start_date', $date)
->orderBy('start_date')
->get();
$i = 1;
// 编号前缀
$prefix = date('Ymd', strtotime($date));
foreach ($courses as $course) {
foreach ($course->courseSigns as $sign) {
$user = User::find($sign->user_id);
if ($user->no) {
continue;
}
$no = $prefix . str_pad($i, 3, '0', STR_PAD_LEFT);
// 更新用户编号
$user->no = $no;
$user->save();
$this->info($no);
$i++;
}
}
}
return $this->info('更新完成');
}
}

@ -26,6 +26,8 @@ class Kernel extends ConsoleKernel
$schedule->command('send_email')->everyMinute();
// 推送课程人员信息
$schedule->command('push_courses')->dailyAt('23:00');
// 更新学员编号
$schedule->command('update_user_no')->dailyAt('00:05');
}
/**

@ -9,6 +9,7 @@ use App\Models\Book;
use App\Models\CustomForm;
use App\Models\CustomFormField;
use App\Models\SupplyDemand;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Maatwebsite\Excel\Facades\Excel;
@ -206,6 +207,8 @@ class BookController extends BaseController
* @OA\Parameter(name="category", in="query", @OA\Schema(type="string"), required=false, description="分类"),
* @OA\Parameter(name="description", in="query", @OA\Schema(type="string"), required=false, description="图书简介"),
* @OA\Parameter(name="cover_id", in="query", @OA\Schema(type="integer"), required=false, description="图书封面ID"),
* @OA\Parameter(name="total", in="query", @OA\Schema(type="integer"), required=false, description="数量"),
* @OA\Parameter(name="bookshelf", in="query", @OA\Schema(type="integer"), required=false, description="书架"),
* @OA\Parameter(name="status", in="query", @OA\Schema(type="integer"), required=false, description="状态0可借阅1已借出2维护中"),
* @OA\Response(
* response="200",
@ -215,7 +218,33 @@ class BookController extends BaseController
*/
public function save()
{
return parent::save();
$all = \request()->all();
DB::beginTransaction();
try {
if (isset($all['id'])) {
$model = $this->model->find($all['id']);
if (empty($model)) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '数据不存在']);
}
} else {
$model = $this->model;
$all['admin_id'] = $this->getUserId();
$all['department_id'] = $this->getUser()->department_id;
}
$original = $model->getOriginal();
$model->fill($all);
$model->save();
DB::commit();
// 写封面和其他信息
// 调用命令行更新
Artisan::call("book:update-isbn-data --book_id={$model->id}");
// 记录日志
$this->saveLogs($original, $model);
return $this->success($model);
} catch (\Exception $exception) {
DB::rollBack();
return $this->fail([$exception->getCode(), $exception->getMessage()]);
}
}
/**

@ -54,7 +54,7 @@ class CalendarsController extends BaseController
if ($validator->fails()) {
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
$list = Calendar::with('course', 'courseContent')->where('date', 'like', $all['month'] . '%')->orderBy('date')->get();
$list = Calendar::with('course', 'courseContent')->where('start_time', 'like', $all['month'] . '%')->orderBy('date')->get();
return $this->success($list);
}

@ -191,6 +191,7 @@ class CourseController extends BaseController
* @OA\Parameter(name="total", in="query", @OA\Schema(type="integer"), description="开课人数"),
* @OA\Parameter(name="class", in="query", @OA\Schema(type="string"), description="所在班级"),
* @OA\Parameter(name="price", in="query", @OA\Schema(type="string"), description="价格"),
* @OA\Parameter(name="url", in="query", @OA\Schema(type="string"), description="过期后链接"),
* @OA\Parameter(name="supply_wechat_pay", in="query", @OA\Schema(type="string"), description="是否支持微信支付0否1是"),
* @OA\Parameter(name="is_arrange", in="query", @OA\Schema(type="integer"), description="是否排课-0否1是"),
* @OA\Parameter(name="is_fee", in="query", @OA\Schema(type="integer"), description="是否缴费-0否1是"),

@ -41,9 +41,9 @@ class OtherController extends CommonController
public function home()
{
// 校友总数
$schoolmate['schoolmate_total'] = User::where('is_schoolmate', 1)->count();
$schoolmate['schoolmate_total'] = User::where('is_schoolmate', 1)->distinct('mobile')->count();
// 2025年校友数
$schoolmate['schoolmate_year'] = User::where('is_schoolmate', 1)->where('created_at', 'like', '%' . date('Y') . '%')->count();
$schoolmate['schoolmate_year'] = User::where('is_schoolmate', 1)->where('created_at', 'like', '%' . date('Y') . '%')->distinct('mobile')->count();
// 上市企业总市值
$company['company_market'] = Company::where('company_market', 1)->sum('market_value');
// 校友企业总融资额
@ -60,11 +60,6 @@ class OtherController extends CommonController
'total' => User::whereIn('company_industry', $level2Names)->count()
];
}
// 追加其他领域
$industryTotal[] = [
'industry' => '其他',
'total' => User::count() - collect($industryTotal)->sum('total')
];
// 课程统计
$courseTypes = CourseType::where('is_chart', 1)->get();
foreach ($courseTypes as $courseType) {

@ -6,6 +6,7 @@ use App\Exports\BaseExport;
use App\Helpers\ResponseCode;
use App\Models\AppointmentType;
use App\Models\CustomForm;
use App\Models\Message;
use App\Models\SupplyDemand;
use App\Models\SupplyDemandType;
use Illuminate\Support\Facades\Validator;
@ -193,6 +194,9 @@ class SupplyDemandController extends BaseController
* tags={"供需信息管理"},
* summary="交互统计",
* description="",
* @OA\Parameter(name="start_date", in="query", @OA\Schema(type="string"), required=true, description="开始日期"),
* @OA\Parameter(name="end_date", in="query", @OA\Schema(type="string"), required=true, description="结束日期"),
* @OA\Parameter(name="type", in="query", @OA\Schema(type="string"), required=true, description="token"),
* @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="token"),
* @OA\Response(
* response="200",
@ -202,7 +206,121 @@ class SupplyDemandController extends BaseController
*/
public function chart()
{
$now = date('Y-m-d');
$startDate = request('start_date', $now);
$endDate = request('end_date', $now);
$type = request('type');
// 计算上期时间段(与当前时间段长度相同)
$daysDiff = (strtotime($endDate) - strtotime($startDate)) / (60 * 60 * 24) + 1;
$prevEndDate = date('Y-m-d', strtotime($startDate) - 1);
$prevStartDate = date('Y-m-d', strtotime($prevEndDate) - $daysDiff + 1);
// 当期供需发布数
$supplyDemand = SupplyDemand::where(function ($query) use ($type) {
if ($type) {
$query->where('type', $type);
}
})->whereBetween('created_at', [$startDate, $endDate])
->get();
$supplyDemandCount = $supplyDemand->count();
// 上期供需发布数
$prevSupplyDemandCount = SupplyDemand::where(function ($query) use ($type) {
if ($type) {
$query->where('type', $type);
}
})->whereBetween('created_at', [$prevStartDate, $prevEndDate])
->count();
// 当期私信数量
$messageCount = Message::whereIn('supply_demand_id', $supplyDemand->pluck('id'))->count();
// 上期私信数量
$prevSupplyDemand = SupplyDemand::where(function ($query) use ($type) {
if ($type) {
$query->where('type', $type);
}
})->whereBetween('created_at', [$prevStartDate, $prevEndDate])
->get();
$prevMessageCount = Message::whereIn('supply_demand_id', $prevSupplyDemand->pluck('id'))->count();
// 当期交互次数同一个dialogue_id一来一回算一次交互
$interactionCount = Message::whereBetween('created_at', [$startDate, $endDate])
->whereNotNull('dialogue_id')
->groupBy('dialogue_id')
->selectRaw('dialogue_id, COUNT(*) as message_count')
->having('message_count', '>=', 2)
->count();
// 上期交互次数
$prevInteractionCount = Message::whereBetween('created_at', [$prevStartDate, $prevEndDate])
->whereNotNull('dialogue_id')
->groupBy('dialogue_id')
->selectRaw('dialogue_id, COUNT(*) as message_count')
->having('message_count', '>=', 2)
->count();
// 计算增减比率
$supplyDemandGrowthRate = $this->calculateGrowthRate($supplyDemandCount, $prevSupplyDemandCount);
$messageGrowthRate = $this->calculateGrowthRate($messageCount, $prevMessageCount);
$interactionGrowthRate = $this->calculateGrowthRate($interactionCount, $prevInteractionCount);
return $this->success([
'supply_demand_count' => $supplyDemandCount,
'prev_supply_demand_count' => $prevSupplyDemandCount,
'supply_demand_growth_rate' => $supplyDemandGrowthRate,
'message_count' => $messageCount,
'prev_message_count' => $prevMessageCount,
'message_growth_rate' => $messageGrowthRate,
'interaction_count' => $interactionCount,
'prev_interaction_count' => $prevInteractionCount,
'interaction_growth_rate' => $interactionGrowthRate,
]);
}
/**
* 计算增长率
* @param int $current 当前数值
* @param int $previous 上期数值
* @return array 包含增长率和增长状态
*/
private function calculateGrowthRate($current, $previous)
{
if ($previous == 0) {
if ($current > 0) {
return [
'rate' => 100,
'status' => 'increase',
'display' => '+100%'
];
} else {
return [
'rate' => 0,
'status' => 'stable',
'display' => '0%'
];
}
}
$rate = round(($current - $previous) / $previous * 100, 2);
if ($rate > 0) {
$status = 'increase';
$display = '+' . $rate . '%';
} elseif ($rate < 0) {
$status = 'decrease';
$display = $rate . '%';
} else {
$status = 'stable';
$display = '0%';
}
return [
'rate' => $rate,
'status' => $status,
'display' => $display
];
}
}

@ -2,12 +2,14 @@
namespace App\Http\Controllers\Admin;
use App\Exports\BaseExport;
use App\Exports\CommonExport;
use App\Helpers\ResponseCode;
use App\Helpers\StarterResponseCode;
use App\Models\Course;
use App\Models\CourseAppointmentTotal;
use App\Models\CourseSign;
use App\Models\CustomForm;
use App\Models\CustomFormField;
use App\Models\User;
use Illuminate\Support\Carbon;
@ -40,7 +42,8 @@ class UserController extends BaseController
* @OA\Parameter(name="page_size", in="query", @OA\Schema(type="string"), required=false, description="每页显示的条数"),
* @OA\Parameter(name="page", in="query", @OA\Schema(type="string"), required=false, description="页码"),
* @OA\Parameter(name="sort_name", in="query", @OA\Schema(type="string"), required=false, description="排序字段名字"),
* @OA\Parameter(name="sort_type", in="query", @OA\Schema(type="string"), required=false, description="排序类型"),
* @OA\Parameter(name="has_course", in="query", @OA\Schema(type="string"), required=false, description="是否有课程0否1是"),
* @OA\Parameter(name="keyword", in="query", @OA\Schema(type="string"), required=true, description="关键词"),
* @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="token"),
* @OA\Response(
* response="200",
@ -50,7 +53,92 @@ class UserController extends BaseController
*/
public function index()
{
return parent::index();
$all = request()->all();
$list = $this->model->with(underlineToHump($all['show_relation'] ?? []))
->with(['courseSigns' => function ($query) use ($all) {
$query->where('status', 1)->with('course.teacher', 'course.typeDetail');
}])->where(function ($query) use ($all) {
if (isset($all['keyword'])) {
$query->whereHas('courses', function ($q) use ($all) {
$q->where('name', 'like', '%' . $all['keyword'] . '%');
})->orWhere('name', 'like', '%' . $all['keyword'] . '%');
}
if (isset($all['has_course']) && $all['has_course'] == 1) {
$query->whereHas('courseSigns', function ($q) {
$q->where('status', 1);
});
}
if (isset($all['filter']) && !empty($all['filter'])) {
foreach ($all['filter'] as $condition) {
$key = $condition['key'] ?? null;
$op = $condition['op'] ?? null;
$value = $condition['value'] ?? null;
if (!isset($key) || !isset($op) || !isset($value)) {
continue;
}
// 等于
if ($op == 'eq') {
$query->where($key, $value);
}
// 不等于
if ($op == 'neq') {
$query->where($key, '!=', $value);
}
// 大于
if ($op == 'gt') {
$query->where($key, '>', $value);
}
// 大于等于
if ($op == 'egt') {
$query->where($key, '>=', $value);
}
// 小于
if ($op == 'lt') {
$query->where($key, '<', $value);
}
// 小于等于
if ($op == 'elt') {
$query->where($key, '<=', $value);
}
// 模糊搜索
if ($op == 'like') {
$query->where($key, 'like', '%' . $value . '%');
}
// 否定模糊搜索
if ($op == 'notlike') {
$query->where($key, 'not like', '%' . $value . '%');
}
// null搜索
if ($op == 'null') {
$query->whereNull($key);
}
// notnull搜索
if ($op == 'notnull') {
$query->whereNotNull($key);
}
// 范围搜索
if ($op == 'range') {
list($from, $to) = explode(',', $value);
if (empty($from) || empty($to)) {
continue;
}
$query->whereBetween($key, [$from, $to]);
}
}
}
})->orderBy($all['sort_name'] ?? 'id', $all['sort_type'] ?? 'desc');
if (isset($all['is_export']) && !empty($all['is_export'])) {
$list = $list->get()->toArray();
$export_fields = $all['export_fields'] ?? [];
// 导出文件名字
$tableName = $this->model->getTable();
$filename = (new CustomForm())->getTableComment($tableName);
return Excel::download(new BaseExport($export_fields, $list, $tableName), $filename . date('YmdHis') . '.xlsx');
} else {
// 输出
$list = $list->paginate($all['page_size'] ?? 20);
}
return $this->success($list);
}
/**

@ -484,10 +484,7 @@ class CourseController extends CommonController
$content_check_range = Config::getValueByKey('content_check_range');
$courseContent = CourseContent::find($all['course_content_id']);
$distance = getDistance($courseContent->longitude, $courseContent->latitude, $all['longitude'], $all['latitude']);
if ($distance > $content_check_range) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '超出打卡范围']);
}
return $this->success('成功');
return $this->success(compact('distance', 'content_check_range'));
}
/**
@ -531,6 +528,13 @@ class CourseController extends CommonController
if (date('Y-m-d') != $courseContent->date) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '不在签到时间']);
}
// 不能重复签到
$courseContentCheck = CourseContentCheck::where('course_content_id', $all['course_content_id'])
->where('user_id', $this->getUserId())
->first();
if ($courseContentCheck) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '不能重复签到']);
}
CourseContentCheck::create([
'course_content_id' => $all['course_content_id'],
'user_id' => $this->getUserId(),
@ -544,9 +548,7 @@ class CourseController extends CommonController
* @OA\Get(
* path="/api/mobile/course/content-check-list",
* tags={"小程序-课程"},
* summary="获取签到记录和距离",
* @OA\Parameter(name="longitude", in="query", @OA\Schema(type="string"), required=false, description="longitude"),
* @OA\Parameter(name="latitude", in="query", @OA\Schema(type="string"), required=false, description="latitude"),
* summary="获取签到记录",
* @OA\Parameter(name="course_content_id", in="query", @OA\Schema(type="string"), required=false, description="课表id"),
* @OA\Response(
* response=200,
@ -558,27 +560,20 @@ class CourseController extends CommonController
{
$all = \request()->all();
$messages = [
'longitude.required' => '经度必填',
'latitude.required' => '纬度必填',
'course_content_id.required' => '课程id必填',
];
$validator = Validator::make($all, [
'longitude' => 'required',
'latitude' => 'required',
'course_content_id' => 'required'
], $messages);
if ($validator->fails()) {
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
// 获取打卡范围,千米
$content_check_range = Config::getValueByKey('content_check_range');
$courseContent = CourseContent::find($all['course_content_id']);
$distance = getDistance($courseContent->longitude, $courseContent->latitude, $all['longitude'], $all['latitude']);
$list = CourseContentCheck::where('course_content_id', $all['course_content_id'])
->where('user_id', $this->getUserId())
->orderBy('created_at', 'desc')
->get();
return $this->success(compact('list', 'content_check_range', 'distance'));
return $this->success(compact('list'));
}
/**
@ -839,6 +834,7 @@ class CourseController extends CommonController
* path="/api/mobile/course/calendars",
* tags={"小程序-课程"},
* summary="日历",
* @OA\Parameter(name="type", in="query", @OA\Schema(type="string"), required=false, description="类型"),
* @OA\Parameter(name="month", in="query", @OA\Schema(type="string"), required=false, description="月份例如2025-01"),
* @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="token"),
* @OA\Response(
@ -859,20 +855,29 @@ class CourseController extends CommonController
if ($validator->fails()) {
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
$startDate = $all['month'] . '-01';
$endDate = date('Y-m-t', strtotime($startDate));
$range = getDates($startDate, $endDate);
$list = [];
foreach ($range as $date) {
// 查询Calendar模型里start_time和end_time在日期内的数据,其中date是年月日start_time和end_time是时分秒
$list[] = [
'date' => $date,
'details' => Calendar::with('course', 'courseContent')
->whereDate('start_time', '<=', $date)
->whereDate('end_time', '>=', $date)
->get()
];
}
// $startDate = $all['month'] . '-01';
// $endDate = date('Y-m-t', strtotime($startDate));
$list = Calendar::with('course', 'courseContent')
->where(function ($query) use ($all) {
if (isset($all['type'])) {
$query->where('type', $all['type']);
}
})->where('start_time', 'like', '%' . $all['month'] . '%')
->orderBy('start_time', 'asc')
->get();
// $range = getDates($startDate, $endDate);
// $list = [];
// foreach ($range as $date) {
// // 查询Calendar模型里start_time和end_time在日期内的数据,其中date是年月日start_time和end_time是时分秒
// $list[] = [
// 'date' => $date,
// 'details' => Calendar::with('course', 'courseContent')
// ->whereDate('start_time', '<=', $date)
// ->whereDate('end_time', '>=', $date)
// ->get()
// ];
// }
return $this->success($list);
}

@ -90,7 +90,7 @@ class OtherController extends CommonController
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
$YuanheRepository = new YuanheRepository();
$result = $YuanheRepository->companyInfo(['enterpriseName' => $all['company_name']]);
$result = $YuanheRepository->companyInfo(['keyword' => $all['company_name']]);
if (!$result) {
return $this->fail([ResponseCode::ERROR_PARAMETER, '获取失败']);
}

@ -15,7 +15,6 @@ use App\Models\Message;
use App\Models\SupplyDemand;
use App\Models\SupplyDemandKeep;
use App\Notifications\BirthdayNotify;
use App\Notifications\SupplyDemandNotify;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Notification;
@ -48,17 +47,18 @@ class SupplyDemandController extends CommonController
public function index()
{
$all = request()->all();
$status = request('status', 1);
$supplyDemands = SupplyDemand::with([
'user' => function ($query) {
$query->select('id', 'nickname', 'name', 'headimgurl', 'username');
}
])->where(function ($query) use ($all) {
])->where(function ($query) use ($all, $status) {
if ($status != -1) {
$query->where('status', $status);
}
if (isset($all['type'])) {
$query->where('type', $all['type']);
}
if (isset($all['status'])) {
$query->where('status', $all['status']);
}
if (isset($all['keyword'])) {
$query->where('content', 'like', '%' . $all['keyword'] . '%');
}
@ -66,12 +66,13 @@ class SupplyDemandController extends CommonController
$query->where('user_id', $this->getUserId());
}
if (isset($all['expire_type'])) {
$now = date('Y-m-d');
if ($all['expire_type'] == 1) {
$query->where(function ($q) {
$q->whereNull('expire_time')->orWhere('expire_time', '>', Carbon::now());
$query->where(function ($q) use ($now) {
$q->whereNull('expire_time')->orWhere('expire_time', '')->orWhere('expire_time', '>', $now);
});
} else {
$query->where('expire_time', '<', Carbon::now());
$query->where('expire_time', '<', $now);
}
}
})->orderBy($all['sort_name'] ?? 'id', $all['sort_type'] ?? 'desc')
@ -105,7 +106,7 @@ class SupplyDemandController extends CommonController
}
$detail = SupplyDemand::with([
'user' => function ($query) {
$query->select('id', 'nickname', 'name', 'headimgurl');
$query->select('id', 'nickname', 'name', 'headimgurl', 'username');
}
])->find($all['id']);
// 增加view_count
@ -237,10 +238,6 @@ class SupplyDemandController extends CommonController
if ($validator->fails()) {
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
if (isset($all['supply_demand_id']) && !empty($all['supply_demand_id'])) {
// 增加view_count
SupplyDemand::where('id', $all['supply_demand_id'])->increment('view_count');
}
// 判断是否有会话,没有则创建
$dialogue = Dialogue::where(function ($query) use ($all) {
$query->where('user_id', $this->getUserId())->where('to_user_id', $all['to_user_id']);
@ -248,12 +245,26 @@ class SupplyDemandController extends CommonController
$query->where('user_id', $all['to_user_id'])->where('to_user_id', $this->getUserId());
})->first();
if (empty($dialogue)) {
// 不能给自己发信息
if ($all['to_user_id'] == $this->getUserId()) {
return $this->error('不能给自己发信息');
}
// 创建一条会话
$dialogue = Dialogue::create([
'user_id' => $this->getUserId(),
'to_user_id' => $all['to_user_id'],
'supply_demand_id' => $all['supply_demand_id'] ?? 0
]);
// 增加联系次数
SupplyDemand::where('id', $all['supply_demand_id'])->increment('contact_count');
}
if (isset($all['supply_demand_id'])) {
$now = date('Y-m-d');
// 过期数据不能私信
$supplyDemand = SupplyDemand::find($all['supply_demand_id']);
if ($supplyDemand->expire_time > $now) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '该信息已过期,不能私信']);
}
}
// 每天限制私信次数限制
$message_limit = Config::getValueByKey('message_limit');

@ -74,8 +74,6 @@ class UserController extends CommonController
$score = Config::getValueByKey('share_score');
ScoreLog::add($pid, $score, '分享获得');
}
// 更新编号
User::updateNo($user->id);
}
$token = $user->createToken("mobile-token")->plainTextToken;
return $this->success(compact('token'));
@ -176,13 +174,13 @@ class UserController extends CommonController
if (isset($all['name']) && !empty($all['name'])) {
$all['letter'] = strtoupper(Pinyin::abbr(mb_substr($all['name'], 0, 1))[0]);
}
$model->fill($all);
$model->save();
// 如果有公司信息,就更新一下公司
if (isset($all['company_name']) && !empty($all['company_name'])) {
if (isset($all['company_name']) && !empty($all['company_name']) && $model->company_name != $all['company_name']) {
// 调用命令行更新
Artisan::call("update_company --user_id={$model->id}");
}
$model->fill($all);
$model->save();
// 判断下,如果用户新加入车牌号,并且有未开始或者进行中的预约,则直接预约车牌号
$appointmentModel = Appointment::where('user_id', $this->getUserId())
->where('status', 1)
@ -231,7 +229,6 @@ class UserController extends CommonController
}])->find($this->getUserId());
$doorRepository = new DoorRepository();
$nowDate = date('Y-m-d H:i:s');
$door_appointments = Appointment::where('user_id', $this->getUserId())
->where('status', 1)
->orderBy('id', 'desc')
@ -247,6 +244,7 @@ class UserController extends CommonController
$query->where('start_date', '<=', $nowDate)->where('end_date', '>=', $nowDate);
})->first();
if ($course_signs) {
// todo::上线解开注释
//$course_signs->qrcode = $doorRepository->getEmpQrCodeByCourse($course_signs, $out);
$course_signs->qrcode = '';
}

@ -5,9 +5,20 @@ namespace App\Models;
class Book extends SoftDeletesModel
{
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'other_data' => 'json'
];
/**
* Get the cover image for the book.
*/
public function cover()
{
return $this->hasOne(Upload::class, 'id', 'cover_id');
}
}

@ -14,9 +14,7 @@ class Config extends SoftDeletesModel
*/
public static function getValueByKey($key)
{
$config = Cache::remember('config_array', 300, function () {
return self::pluck('value', 'key')->toArray();
});
$config = self::pluck('value', 'key')->toArray();
if (isset($config[$key])) {
return $config[$key];
}

@ -94,6 +94,11 @@ class Course extends SoftDeletesModel
return $this->hasOne(Upload::class, 'id', 'qun_image_id');
}
public function courseContents()
{
return $this->hasMany(CourseContent::class, 'course_id', 'id');
}
/**
* 更新课程报名状态
*/

@ -77,9 +77,9 @@ class CourseContent extends SoftDeletesModel
'secret' => \config('app.applet_secret')
];
$app = Factory::miniProgram($config);
$tmp = $app->app_code->get('/packages/surveyFill/index?course_content_id' . $courseContentId, [
'env_version' => "release" // 正式版
// 'env_version' => "trial" // 体验版
$tmp = $app->app_code->get('/packages/sign/index?course_content_id' . $courseContentId, [
// 'env_version' => "release" // 正式版
'env_version' => "trial" // 体验版
]);
$dir = dirname($path);
$fileSys->ensureDirectoryExists($dir, 0755, true);

@ -9,6 +9,7 @@ use Illuminate\Support\Facades\Cache;
class SupplyDemand extends SoftDeletesModel
{
protected $appends = ['files'];
protected $casts = ['file_ids' => 'json'];
public function getFilesAttribute($value)

@ -182,49 +182,4 @@ class User extends Authenticatable implements Auditable
return $user->appointment_total - $useTotal >= 0 ? $user->appointment_total - $useTotal : 0;
}
/**
* 更新用户编号
*/
public static function updateNo($userId)
{
// todo::编号可能回重复,还需要详细排查
$user = self::find($userId);
if (!empty($user->no)) {
return false;
}
// 获取最早一条审核通过的报名数据
$courseSigns = CourseSign::with('course')
->where('user_id', $userId)
->where('status', 1)
->orderBy('created_at', 'asc')
->first();
if (empty($courseSigns)) {
return false;
}
if (empty($courseSigns->course->start_date)) {
return false;
}
// 编号前缀
$prefix = date('Ymd', strtotime($courseSigns->course->start_date));
// 获取同一天开始的所有课程
$course = Course::where('start_date', $courseSigns->course->start_date)->orderBy('created_at', 'asc')->get();
// 获取同一天开始所有课程的报名信息
$courseSignsList = CourseSign::whereIn('id', function ($query) use ($course) {
$query->from('course_signs')
->where('status', 1)
->whereIn('course_id', $course->pluck('id'))
->selectRaw('MIN(id)')
->groupBy('user_id');
})->orderBy('created_at', 'asc')->get();
// 获取当前用户id在$courseSigns中第几位
$index = $courseSignsList->search(function ($item) use ($user) {
return $item->user_id == $user->id;
});
$no = $prefix . str_pad($index + 1, 3, '0', STR_PAD_LEFT);
$user->no = $no;
$user->save();
return $user->no;
}
}

@ -0,0 +1,405 @@
<?php
namespace App\Services\TestData;
use App\Models\Course;
use App\Models\CourseContent;
use App\Models\CourseContentEvaluation;
use App\Models\CourseContentEvaluationAsk;
use App\Models\CourseContentEvaluationForm;
use App\Models\User;
use Faker\Factory as FakerFactory;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
/**
* 课程内容评价模块测试数据生成器
*/
class CourseContentEvaluationTestDataGenerator
{
/**
* 生成课程内容评价相关的全量测试数据
*
* @param int $evaluationCount 生成评价问卷数量
* @param int $minUsers 最少用户数量,不足会自动补充
* @param callable|null $logger 可选日志输出函数 function(string $message): void
*/
public function generate(int $evaluationCount = 20, int $minUsers = 50, ?callable $logger = null): void
{
$log = $logger ?? static function (string $message): void {
// no-op
};
$faker = FakerFactory::create('zh_CN');
DB::transaction(function () use ($evaluationCount, $minUsers, $faker, $log) {
// 1) 确保用户数量
$currentUserCount = User::count();
if ($currentUserCount < $minUsers) {
$need = $minUsers - $currentUserCount;
$log("创建用户: {$need} 个");
User::factory($need)->create();
}
$allUsers = User::all();
if ($allUsers->count() < 5) {
throw new \RuntimeException('生成评价数据至少需要5个用户');
}
// 2) 确保课程和课程内容存在
$courses = Course::with('courseContents')->get();
if ($courses->isEmpty()) {
$log("创建示例课程数据");
$this->createSampleCoursesAndContents($faker);
$courses = Course::with('courseContents')->get();
}
$allCourseContents = CourseContent::all();
if ($allCourseContents->isEmpty()) {
throw new \RuntimeException('无课程内容数据,无法生成评价');
}
// 3) 生成评价问卷主数据
for ($i = 0; $i < $evaluationCount; $i++) {
$courseContent = $allCourseContents->random();
$course = $courses->where('id', $courseContent->course_id)->first();
if (!$course) {
// 如果找不到课程,跳过这个课程内容
continue;
}
$evaluation = $this->createEvaluation($courseContent, $course, $faker);
// 4) 为每个问卷生成问题字段
$askCount = $faker->numberBetween(5, 15);
$asks = $this->createEvaluationAsks($evaluation, $courseContent, $course, $askCount, $faker);
// 5) 生成用户提交的表单数据
$formCount = $faker->numberBetween(10, min(40, $allUsers->count()));
$submittedUsers = $allUsers->random($formCount);
foreach ($submittedUsers as $user) {
$this->createEvaluationForm($evaluation, $user, $asks, $faker);
}
$log(sprintf('评价问卷#%d "%s" 已生成:%d个问题字段%d份用户提交',
$evaluation->id,
$evaluation->title,
$askCount,
$formCount
));
}
});
}
/**
* 创建评价问卷主数据
*/
private function createEvaluation(CourseContent $courseContent, Course $course, $faker): CourseContentEvaluation
{
$evaluation = new CourseContentEvaluation();
$evaluation->course_id = $course->id;
$evaluation->course_content_id = $courseContent->id;
$evaluation->title = $this->generateEvaluationTitle($course, $courseContent, $faker);
$evaluation->desc = $this->generateEvaluationDesc($faker);
$evaluation->type_id = $faker->numberBetween(1, 5); // 问卷类型ID
// 时间设置:开始时间在课程内容时间前后,截止时间在开始时间之后
$startTime = $faker->dateTimeBetween('-30 days', '+7 days');
$endTime = $faker->dateTimeBetween($startTime, $startTime->format('Y-m-d H:i:s') . ' +30 days');
$evaluation->start_time = $startTime->format('Y-m-d H:i:s');
$evaluation->end_time = $endTime->format('Y-m-d H:i:s');
$evaluation->status = $faker->randomElement([0, 1]); // 0未发布, 1已发布
$evaluation->save();
return $evaluation;
}
/**
* 创建评价问题字段
*/
private function createEvaluationAsks(CourseContentEvaluation $evaluation, CourseContent $courseContent, Course $course, int $count, $faker): array
{
$asks = [];
$fieldTemplates = $this->getEvaluationFieldTemplates();
for ($i = 0; $i < $count; $i++) {
$template = $faker->randomElement($fieldTemplates);
$ask = new CourseContentEvaluationAsk();
$ask->admin_id = $faker->numberBetween(1, 10);
$ask->department_id = $faker->numberBetween(1, 5);
$ask->course_id = $course->id;
$ask->course_content_id = $courseContent->id;
$ask->course_content_evaluation_id = $evaluation->id;
$ask->name = $template['name'];
$ask->field = $template['field'] . '_' . ($i + 1);
$ask->edit_input = $template['edit_input'];
$ask->rule = $template['rule'];
$ask->sort = $i + 1;
$ask->help = $template['help'];
$ask->select_item = $template['select_item'];
$ask->need_fill = $template['need_fill'];
$ask->belong_user = $template['belong_user'];
$ask->allow_input = $template['allow_input'];
$ask->save();
$asks[] = $ask;
}
return $asks;
}
/**
* 创建用户提交的评价表单
*/
private function createEvaluationForm(CourseContentEvaluation $evaluation, User $user, array $asks, $faker): void
{
$formData = [];
foreach ($asks as $ask) {
$formData[$ask->field] = $this->generateFieldValue($ask, $faker);
}
$form = new CourseContentEvaluationForm();
$form->course_content_evaluation_id = $evaluation->id;
$form->user_id = $user->id;
$form->time_total = $faker->numberBetween(180, 1800); // 3分钟到30分钟
$form->data = $formData;
$form->created_at = $faker->dateTimeBetween($evaluation->start_time, $evaluation->end_time ?: 'now');
$form->save();
}
/**
* 生成评价标题
*/
private function generateEvaluationTitle(Course $course, CourseContent $courseContent, $faker): string
{
$templates = [
'《%s》课程满意度调查',
'%s 教学效果评价',
'%s 学习体验反馈',
'关于 %s 的教学质量评估',
'%s 课程内容评价问卷',
'%s 授课情况调研',
'%s 学员反馈调查'
];
$courseName = $course->title ?? '课程';
return sprintf($faker->randomElement($templates), $courseName);
}
/**
* 生成评价描述
*/
private function generateEvaluationDesc($faker): string
{
$descriptions = [
'为了提升教学质量,改进课程内容,请您根据实际学习体验,客观填写本次评价问卷。您的宝贵意见将帮助我们持续优化课程设计。',
'此次评价旨在了解您对本课程的学习感受和建议。问卷采用匿名形式,请放心填写真实想法,感谢您的配合!',
'请根据您的实际学习情况,对本次课程的各个方面进行客观评价。您的反馈对我们改进教学方法具有重要意义。',
'为持续提升课程品质,特设立本次学员满意度调查。请您花费几分钟时间,帮助我们了解课程的优点与不足。',
'感谢您参与本次课程学习!为了给后续学员提供更好的学习体验,恳请您如实填写这份评价问卷。'
];
return $faker->randomElement($descriptions);
}
/**
* 获取评价字段模板
*/
private function getEvaluationFieldTemplates(): array
{
return [
[
'name' => '课程内容满意度',
'field' => 'content_satisfaction',
'edit_input' => 'radio',
'rule' => 'required',
'help' => '请选择您对课程内容的满意程度',
'select_item' => ['非常满意', '满意', '一般', '不满意', '非常不满意'],
'need_fill' => true,
'belong_user' => false,
'allow_input' => false
],
[
'name' => '授课方式评价',
'field' => 'teaching_method',
'edit_input' => 'radio',
'rule' => 'required',
'help' => '请评价老师的授课方式',
'select_item' => ['很好', '好', '一般', '较差', '很差'],
'need_fill' => true,
'belong_user' => false,
'allow_input' => false
],
[
'name' => '课程难度评价',
'field' => 'difficulty_level',
'edit_input' => 'radio',
'rule' => 'required',
'help' => '您认为课程难度如何',
'select_item' => ['太简单', '偏简单', '适中', '偏难', '太难'],
'need_fill' => true,
'belong_user' => false,
'allow_input' => false
],
[
'name' => '学习收获评价',
'field' => 'learning_gain',
'edit_input' => 'checkbox',
'rule' => '',
'help' => '您在本次学习中获得了哪些收获(可多选)',
'select_item' => ['理论知识', '实践技能', '思维方法', '行业认知', '人际交往', '其他'],
'need_fill' => false,
'belong_user' => false,
'allow_input' => true
],
[
'name' => '课程推荐度',
'field' => 'recommendation',
'edit_input' => 'radio',
'rule' => 'required',
'help' => '您是否愿意向他人推荐此课程',
'select_item' => ['非常愿意', '愿意', '无所谓', '不愿意', '绝对不会'],
'need_fill' => true,
'belong_user' => false,
'allow_input' => false
],
[
'name' => '整体评分',
'field' => 'overall_rating',
'edit_input' => 'select',
'rule' => 'required',
'help' => '请为本次课程打分10分制',
'select_item' => ['10分', '9分', '8分', '7分', '6分', '5分', '4分', '3分', '2分', '1分'],
'need_fill' => true,
'belong_user' => false,
'allow_input' => false
],
[
'name' => '意见建议',
'field' => 'suggestions',
'edit_input' => 'textarea',
'rule' => '',
'help' => '请提出您的宝贵意见和建议',
'select_item' => null,
'need_fill' => false,
'belong_user' => false,
'allow_input' => true
],
[
'name' => '您的姓名',
'field' => 'student_name',
'edit_input' => 'text',
'rule' => '',
'help' => '请填写您的真实姓名(可选)',
'select_item' => null,
'need_fill' => false,
'belong_user' => true,
'allow_input' => false
],
[
'name' => '联系方式',
'field' => 'contact_info',
'edit_input' => 'text',
'rule' => '',
'help' => '如需回访,请留下联系方式',
'select_item' => null,
'need_fill' => false,
'belong_user' => true,
'allow_input' => false
],
[
'name' => '课堂互动评价',
'field' => 'interaction_rating',
'edit_input' => 'radio',
'rule' => '',
'help' => '您对课堂互动环节的评价',
'select_item' => ['很活跃', '较活跃', '一般', '较沉闷', '很沉闷'],
'need_fill' => false,
'belong_user' => false,
'allow_input' => false
]
];
}
/**
* 根据字段类型生成对应的值
*/
private function generateFieldValue(CourseContentEvaluationAsk $ask, $faker)
{
switch ($ask->edit_input) {
case 'radio':
case 'select':
return $faker->randomElement($ask->select_item ?? []);
case 'checkbox':
$options = $ask->select_item ?? [];
$selected = $faker->randomElements($options, $faker->numberBetween(1, min(3, count($options))));
return implode(',', $selected);
case 'textarea':
$suggestions = [
'希望增加更多实践环节',
'课程进度可以适当放慢',
'案例分析很有帮助,建议增加',
'老师讲解很清晰,受益良多',
'课程资料很丰富,感谢分享',
'希望提供更多课后练习',
'建议增加小组讨论时间',
'整体非常满意,期待后续课程'
];
return $faker->optional(0.7)->randomElement($suggestions) ?: '';
case 'text':
if ($ask->field === 'student_name') {
return $faker->optional(0.4)->name ?: '';
} elseif ($ask->field === 'contact_info') {
return $faker->optional(0.3)->phoneNumber ?: '';
}
return $faker->optional(0.5)->words(3, true) ?: '';
default:
return '';
}
}
/**
* 创建示例课程和课程内容数据(如果不存在)
*/
private function createSampleCoursesAndContents($faker): void
{
$courseData = [
['title' => 'Python 程序设计基础', 'contents' => ['Python 语言概述', '数据类型与变量', '控制结构', '函数与模块', '面向对象编程']],
['title' => '数据库原理与应用', 'contents' => ['数据库基础概念', 'SQL 语言基础', '数据库设计', '事务处理', '性能优化']],
['title' => '项目管理实务', 'contents' => ['项目管理概述', '项目计划制定', '风险管理', '团队管理', '项目收尾']],
['title' => '市场营销学', 'contents' => ['市场营销概论', '消费者行为分析', '产品策略', '价格策略', '推广策略']],
['title' => 'Web 前端开发', 'contents' => ['HTML 基础', 'CSS 样式设计', 'JavaScript 编程', 'Vue.js 框架', '项目实战']]
];
foreach ($courseData as $data) {
$course = Course::create([
'title' => $data['title'],
'description' => $data['title'] . '课程',
'status' => 1,
'created_at' => now(),
'updated_at' => now()
]);
foreach ($data['contents'] as $index => $contentTitle) {
CourseContent::create([
'course_id' => $course->id,
'title' => $contentTitle,
'description' => $contentTitle . '相关内容',
'sort' => $index + 1,
'status' => 1,
'created_at' => now(),
'updated_at' => now()
]);
}
}
}
}

@ -0,0 +1,266 @@
<?php
namespace App\Services\TestData;
use App\Models\Dialogue;
use App\Models\Message;
use App\Models\SupplyDemand;
use App\Models\SupplyDemandKeep;
use App\Models\Upload;
use App\Models\User;
use Faker\Factory as FakerFactory;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
/**
* 供需模块测试数据生成器
*/
class SupplyDemandTestDataGenerator
{
/**
* 生成供需相关的全量测试数据
*
* @param int $supplyDemandCount 生成供需数量
* @param int $minUsers 最少用户数量,不足会自动补充
* @param callable|null $logger 可选日志输出函数 function(string $message): void
*/
public function generate(int $supplyDemandCount = 50, int $minUsers = 20, ?callable $logger = null): void
{
$log = $logger ?? static function (string $message): void {
// no-op
};
$faker = FakerFactory::create('zh_CN');
DB::transaction(function () use ($supplyDemandCount, $minUsers, $faker, $log) {
// 1) 确保用户数量
$currentUserCount = User::count();
if ($currentUserCount < $minUsers) {
$need = $minUsers - $currentUserCount;
$log("创建用户: {$need}");
// 使用 factory 填充用户
\App\Models\User::factory($need)->create();
}
$allUsers = User::query()->get(['id', 'name', 'nickname', 'username']);
if ($allUsers->count() < 2) {
throw new \RuntimeException('生成对话与消息至少需要2个用户');
}
// 2) 生成供需主数据
for ($i = 0; $i < $supplyDemandCount; $i++) {
$publisher = $allUsers->random();
$supplyDemand = new SupplyDemand();
$supplyDemand->user_id = $publisher->id;
$supplyDemand->title = $this->generateTechBusinessTitle($faker);
$supplyDemand->type = $faker->randomElement([1, 2]);
$supplyDemand->content = $this->generateContentFromTitle($supplyDemand->title, $supplyDemand->type, $faker);
$supplyDemand->tag = implode(',', $this->generateTagsFromTitle($supplyDemand->title, $faker));
$supplyDemand->wechat = 'wx_' . $faker->bothify('??####');
$supplyDemand->mobile = $faker->phoneNumber();
$supplyDemand->email = $faker->safeEmail();
$supplyDemand->status = $faker->randomElement([0, 1, 2, 3, 4]);
$supplyDemand->view_count = 0; // 稍后回填
$supplyDemand->contact_count = 0; // 稍后回填
$supplyDemand->expire_time = $faker->optional(0.6)->dateTimeBetween('now', '+90 days')?->format('Y-m-d H:i:s');
// public_way 实际业务需要三态(1/2/3),但列类型为 boolean这里按 0/1 赋值
$supplyDemand->public_way = $faker->boolean ? 1 : 0;
$supplyDemand->contact_name = $faker->name();
$supplyDemand->save();
// 3) 附件0~3 个,并同步回填 file_ids
$uploadIds = [];
$attachmentsCount = $faker->numberBetween(0, 3);
for ($k = 0; $k < $attachmentsCount; $k++) {
$originalName = $faker->lexify('file_????') . '.' . $faker->randomElement(['pdf', 'png', 'jpg', 'docx']);
$extension = pathinfo($originalName, PATHINFO_EXTENSION);
$upload = new Upload();
$upload->belongs_type = SupplyDemand::class;
$upload->belongs_id = $supplyDemand->id;
$upload->original_name = $originalName;
$upload->folder = 'test/' . date('Ymd');
$upload->name = $faker->uuid . '.' . $extension;
$upload->extension = $extension;
$upload->size = $faker->numberBetween(5 * 1024, 2 * 1024 * 1024);
$upload->creator_type = 'seeder';
$upload->creator_id = $publisher->id;
$upload->save();
$uploadIds[] = $upload->id;
}
if (!empty($uploadIds)) {
$supplyDemand->file_ids = $uploadIds;
$supplyDemand->save();
}
// 4) 会话与消息0~2 个会话,每个会话 1~5 条消息,严格交替
$dialogueCount = $faker->numberBetween(0, 2);
$totalMessages = 0;
$totalContacts = 0; // 统计对发布者的有效联系次数
for ($d = 0; $d < $dialogueCount; $d++) {
// 选择一个不同于发布者的用户作为对话方
$other = $allUsers->where('id', '!=', $publisher->id)->random();
$dialogue = new Dialogue();
$dialogue->user_id = $faker->randomElement([$publisher->id, $other->id]); // 对话发起方随机
$dialogue->to_user_id = ($dialogue->user_id === $publisher->id) ? $other->id : $publisher->id;
$dialogue->supply_demand_id = $supplyDemand->id;
$dialogue->last_content = null;
$dialogue->last_datetime = null;
$dialogue->save();
$messageCount = $faker->numberBetween(1, 5);
$sender = $faker->randomElement([$publisher->id, $other->id]);
$receiver = ($sender === $publisher->id) ? $other->id : $publisher->id;
$lastContent = null;
$lastDatetime = null;
for ($m = 0; $m < $messageCount; $m++) {
$content = $faker->realText($faker->numberBetween(20, 80));
$createdAt = Carbon::now()->subDays($faker->numberBetween(0, 10))->addMinutes($faker->numberBetween(0, 1440));
$msg = new Message();
$msg->dialogue_id = $dialogue->id;
$msg->user_id = $sender;
$msg->to_user_id = $receiver;
$msg->supply_demand_id = $supplyDemand->id;
$msg->content = $content;
$msg->is_read = $faker->boolean ? 1 : 0;
$msg->created_at = $createdAt;
$msg->updated_at = $createdAt;
$msg->save();
// 统计“对发布者的联系”
if ($receiver === $publisher->id) {
$totalContacts++;
}
$lastContent = $content;
$lastDatetime = $createdAt->format('Y-m-d H:i:s');
// 严格交替:交换 sender/receiver
[$sender, $receiver] = [$receiver, $sender];
}
$dialogue->last_content = $lastContent;
$dialogue->last_datetime = $lastDatetime;
$dialogue->save();
$totalMessages += $messageCount;
}
// 5) 收藏0~5 个
$keepCount = $faker->numberBetween(0, 5);
if ($keepCount > 0) {
$keeperPool = $allUsers->where('id', '!=', $publisher->id)->pluck('id')->all();
$keepers = Arr::random($keeperPool, min($keepCount, count($keeperPool)));
foreach ((array)$keepers as $keeperId) {
SupplyDemandKeep::firstOrCreate([
'user_id' => $keeperId,
'supply_demand_id' => $supplyDemand->id,
]);
}
}
// 6) 回填统计:浏览量 >= 消息数,联系次数 = 对发布者的消息次数
$supplyDemand->contact_count = $totalContacts;
$supplyDemand->view_count = $totalMessages + $faker->numberBetween(0, 20);
$supplyDemand->save();
$log(sprintf('供需#%d 已生成:附件%s个会话%s个消息%s条收藏%s个',
$supplyDemand->id,
count($uploadIds),
$dialogueCount,
$totalMessages,
$keepCount
));
}
});
}
/**
* 生成科技/商业取向的标题
*/
private function generateTechBusinessTitle($faker): string
{
$regions = ['华东', '华南', '华北', '西南', '中原', '长三角', '珠三角'];
$industries = ['制造', '零售', '医药', '教育', '金融', '能源', '物流', '政企'];
$materials = ['光刻胶', '硅片', 'CMP 抛光液', '陶瓷基板'];
$domains = ['电商推荐', '客服质检', '金融风控', '工业检测', '内容审核'];
$countries = ['新加坡', '印度尼西亚', '阿联酋', '沙特', '巴西', '墨西哥'];
$templates = [
'SaaS 渠道分销伙伴招募(' . $faker->randomElement($regions) . '',
'云计算成本优化服务对接(' . $faker->randomElement($industries) . '行业)',
'AI 数据标注外包合作(' . $faker->randomElement($domains) . '',
'跨境电商供应链合作(' . $faker->randomElement($countries) . '仓)',
'半导体材料采购需求(' . $faker->randomElement($materials) . '',
'新能源充电桩 OEM/ODM 代工合作',
'企业私有化部署 DevOps 顾问服务',
'工业物联网传感器批量采购',
'大模型微调服务(' . $faker->randomElement($domains) . '',
'移动端 SDK 联合推广与结算',
'本地化运营团队招募(' . $faker->randomElement($regions) . '',
'出海广告投放合作(' . $faker->randomElement($countries) . '',
'数据中台建设项目外包',
'企业安全渗透测试服务对接',
];
return $faker->randomElement($templates);
}
/**
* 基于标题生成结构化内容,围绕标题展开
* @param int $type 1=供应 2=需求
*/
private function generateContentFromTitle(string $title, int $type, $faker): string
{
$timeframeWeeks = $faker->numberBetween(2, 12);
$budget = $faker->randomElement(['5万-10万', '10万-30万', '30万-80万', '80万以上', '按效果结算']);
$scale = $faker->randomElement(['小规模试点', '区域级铺开', '全国推广', '跨境协同']);
$partner = $faker->randomElement(['渠道商', 'ISV', '系统集成商', '服务外包商', '硬件厂商', '联合营销伙伴']);
$kpi = $faker->randomElement(['留存率', '转化率', '交付周期', '单点成本', '渠道覆盖']);
$roleLine = $type === 1
? '供给能力:我们可提供成熟方案/产品/产能,支持灵活对接与深度合作。'
: '需求说明:我们需要优质方案/资源/产能,期待高效、稳定的交付能力。';
$sections = [
"【项目标题】{$title}",
'项目背景:' . $faker->realText($faker->numberBetween(40, 80)),
$roleLine,
'合作模式:' . $faker->randomElement(['佣金', '代理', '分销', '联合投放', '项目外包', '里程碑结算']),
"目标指标:重点关注{$kpi},预计{$timeframeWeeks}周达到{$scale}阶段性目标。",
"预算与周期:预算 {$budget},计划周期 {$timeframeWeeks} 周。",
"适配伙伴:优先 {$partner},具备行业交付经验者加分。",
'补充信息:' . $faker->realText($faker->numberBetween(40, 80)),
];
return implode("\n", $sections);
}
/**
* 根据标题提取/生成标签
* @return array<string>
*/
private function generateTagsFromTitle(string $title, $faker): array
{
$dictionary = ['AI', 'SaaS', '云计算', '大模型', '供应链', '半导体', '新能源', '跨境电商', '营销', '渠道', 'OEM', 'ODM', '物联网', '安全', '出海', '数据中台', 'DevOps'];
$matched = [];
foreach ($dictionary as $keyword) {
if (mb_stripos($title, $keyword) !== false) {
$matched[] = $keyword;
}
}
$extra = $faker->randomElements($dictionary, $faker->numberBetween(0, 2));
$tags = array_values(array_unique(array_merge($matched, $extra)));
if (empty($tags)) {
$tags = $faker->randomElements($dictionary, $faker->numberBetween(1, 3));
}
return $tags;
}
}

@ -19,6 +19,7 @@
"overtrue/pinyin": "^5.0",
"overtrue/wechat": "~5.0",
"owen-it/laravel-auditing": "^13.6",
"phpoffice/phpspreadsheet": "^1.29",
"prettus/l5-repository": "^2.9",
"rap2hpoutre/fast-excel": "^5.2",
"simplesoftwareio/simple-qrcode": "^4.2",

@ -16,14 +16,17 @@ return new class extends Migration {
$table->comment('图书表');
$table->increments('id');
$table->string('title')->comment('书名');
// 书架
$table->string('bookshelf')->comment('书架');
$table->string('author')->nullable()->comment('作者');
$table->string('isbn')->nullable()->comment('ISBN');
$table->string('publisher')->nullable()->comment('出版社');
$table->year('publish_year')->nullable()->comment('出版年份');
$table->string('publish_year')->nullable()->comment('出版年份');
$table->string('category')->nullable()->comment('分类');
$table->mediumText('description')->nullable()->comment('图书简介');
$table->integer('cover_id')->nullable()->comment('图书封面');
$table->tinyInteger('status')->default(0)->comment('状态0可借阅1已借出2维护中');
$table->json('other_data')->nullable()->comment('其他数据');
$table->timestamps();
$table->softDeletes();
});

@ -25,8 +25,6 @@ return new class extends Migration
$table->json('file_ids')->nullable()->comment('文件id数组');
// 详细地址
$table->string('address_detail')->nullable()->comment('详细地址');
$table->string('sex')->nullable()->comment('性别');
$table->string('mobile')->nullable()->comment('联系方式');
});
}

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('courses', function (Blueprint $table) {
// 过期后链接
$table->string('url')->nullable()->comment('过期后链接');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('courses', function (Blueprint $table) {
//
});
}
};

@ -0,0 +1,19 @@
<?php
namespace Database\Seeders;
use App\Services\TestData\SupplyDemandTestDataGenerator;
use Illuminate\Database\Seeder;
class SupplyDemandDemoSeeder extends Seeder
{
public function run(): void
{
$generator = new SupplyDemandTestDataGenerator();
$generator->generate(50, 20, function (string $msg) {
$this->command?->info($msg);
});
}
}

@ -0,0 +1,96 @@
### 供需发布模块数据结构与功能分析
#### 一、数据结构
- **表:`supply_demands`(供需主表)**
- 字段:`id`, `user_id`, `title`, `type`(1=供应,2=需求), `content`, `tag`, `wechat`, `mobile`, `email`, `status`(0待审核/1通过/2拒绝/3退回修改/4永久隐藏), `view_count`, `contact_count`, `expire_time`, `public_way`(布尔实际业务含义为1/2/3), `file_ids`(JSON), `contact_name`, `timestamps`, `deleted_at`
- 关系:
- `user`: 发布者(`hasOne User(id=user_id)`
- `keeps`: 收藏(`hasMany SupplyDemandKeep(supply_demand_id=id)`
- 虚拟属性:`files`(根据 `file_ids` 关联 `Upload` 列表)
- **表:`supply_demand_keeps`(收藏表)**
- 字段:`id`, `user_id`, `supply_demand_id`, `timestamps`, `deleted_at`
- 关系:
- `user`: 收藏者(`hasOne User(id=user_id)`
- `supplyDemand`: 被收藏的供需(`hasOne SupplyDemand(id=supply_demand_id)`
- **表:`dialogues`(会话表)**
- 字段:`id`, `user_id`, `to_user_id`, `supply_demand_id`, `last_content`, `last_datetime`, `timestamps`, `deleted_at`
- 关系:
- `user`: 会话发起方(`hasOne User(id=user_id)`
- `toUser`: 会话接收方(`hasOne User(id=to_user_id)`
- `supplyDemand`: 关联供需(`hasOne SupplyDemand(id=supply_demand_id)`
- **表:`messages`(消息表)**
- 字段:`id`, `dialogue_id`, `user_id`, `to_user_id`, `supply_demand_id`, `content`, `is_read`, `timestamps`, `deleted_at`
- 关系:
- `user`/`toUser`: 发送方/接收方(`hasOne User`
- `dialogue`: 所属会话(`hasOne Dialogue(id=dialogue_id)`
- `supplyDemand`: 关联供需(`hasOne SupplyDemand(id=supply_demand_id)`
- **表:`uploads`(附件表)**
- 字段:`id`, `belongs_type`, `belongs_id`, `original_name`, `folder`, `name`, `extension`, `size`, `creator_type`, `creator_id`, `timestamps`, `deleted_at`
- 用途:`supply_demands.file_ids` 存放附件 `id` 列表,模型通过 `files` 访问器取回 `Upload` 集合
说明:`public_way` 迁移中为 boolean但注释为 1/2/3 三种模式1直接公开/2私信后公开/3不公开。当前以 0/1 存储,若需完整三态应在后续迁移中改为 tinyInteger。
#### 二、功能说明(`SupplyDemandController`
- **列表 `index`**:按类型、状态、关键词、是否只看自己、有效期(有效/失效)筛选,分页排序;关联返回 `user` 基本信息。
- **详情 `detail`**:按 `id` 查询,返回 `user`,并自增 `view_count`;附带当前用户对该供需的已发私信次数。
- **保存 `save`**:新增或更新(新增时绑定当前用户并短信通知管理员),使用事务保存,支持 `file_ids`、`expire_time`、`contact_name` 等字段。
- **删除 `destroy`**:按 `id` 软删除。
- **发私信 `sendMessage`**
- 若带 `supply_demand_id` 则自增浏览量;
- 无会话则创建会话;
- 限流:每天发送条数不超过配置 `message_limit`
- 反骚扰:自己连续发送后需等待对方回复;
- 保存消息并更新会话最后内容与时间。
- **消息列表 `messageList`**:按 `to_user_id` 定位当前和对方的会话,分页返回消息(含双方用户信息)。
- **会话列表 `dialogues`**:返回与当前用户相关的会话(发起或接收),含双方用户与关联供需。
- **收藏相关**
- `keepIndex`:我的收藏列表;
- `keepSupplyDemand`:收藏,去重创建;
- `unKeepSupplyDemand`:取消收藏。
#### 三、核心业务要点
- 审核机制:多状态闭环(待审/通过/拒绝/退回/隐藏)。
- 私信策略:限流 + 反骚扰(需对方回复后再发)。
- 有效期:支持有效/失效筛选。
- 公开模式当前存储为布尔0/1业务含义为三态后续建议迁移调整。
- 数据统计:浏览数 `view_count`、联系数 `contact_count`(可按消息交互推导)。
- 附件:`file_ids` JSON + `files` 访问器联表读取。
#### 四、测试数据生成目标
`supply_demands` 为起点,为每条主记录自动生成:
- 合理的 `uploads` 附件0~3 个),`file_ids` 同步写入;
- 真实的会话 `dialogues`0~2 个),双方用户随机;
- 合法的消息序列 `messages`1~5 条),严格交替往来确保不违反“需对方回复”约束;
- 合理的收藏 `supply_demand_keeps`0~5 条,去重);
- 统计字段:`view_count` ≥ 消息条数,`contact_count` 依据消息往来推导。
#### 五、两种生成方式对比
- **方式A数据库填充Seeder**
- 优点:
- 一次性执行、可集成到 CI 或初始化流程;
- 可与 `DatabaseSeeder` 串联;
- 便于多环境批量重置数据。
- 缺点:
- 运行参数(数量、用户规模)固定或需改代码;
- 无交互,临时性需求需改代码或 env。
- **方式BArtisan 命令Console Command**
- 优点:
- 支持运行参数(如 `--count`、`--users`),灵活生成规模;
- 可多次按需执行,便于演示或局部补数;
- 缺点:
- 需要单独维护命令逻辑;
- 不会被自动纳入 `db:seed` 的全局流程。
推荐:开发/演示期使用命令B灵活试验集成测试或初始化环境使用 SeederA

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.
Loading…
Cancel
Save