master
cody 3 months ago
parent 04416d3899
commit 4981d6dbe6

@ -5,6 +5,7 @@ namespace App\Console\Commands;
use App\Models\Appointment;
use App\Models\AppointmentAccompany;
use App\Models\AppointmentConfig;
use App\Models\BirthdayMessage;
use App\Models\Config;
use App\Models\Course;
use App\Models\CourseSign;
@ -239,10 +240,29 @@ class SendNotification extends Command
$this->smsNotice($vo, $content);
break;
case "App\Notifications\BirthdayNotify":
// 生日通知todo::文案待定
// 生日通知
$user = User::find($data['user_id']);
$url = $this->urlLink();
$content = "{$smsSign}亲爱的同学:祝您生日快乐!登陆小程序有惊喜:{$url}";
if (!$user) {
break;
}
// 从数据表随机获取一条启用的生日祝福文案
$messageTemplate = BirthdayMessage::getRandomMessage();
if ($messageTemplate) {
// 获取学员姓名,如果为空则使用"校友"作为备用
$username = $user->username ?: '校友';
// 替换{username}占位符
$content = str_replace('{username}', $username, $messageTemplate);
// 添加短信签名
$content = $smsSign . $content;
} else {
// 如果没有可用的文案,使用默认文案
$url = $this->urlLink();
$username = $user->username ?: '校友';
$content = "{$smsSign}亲爱的{$username}校友苏州科技商学院祝您生日快乐打开STBC小程序查看您的生日惊喜";
}
$this->smsNotice($vo, $content);
break;
}

@ -178,6 +178,8 @@ class UserController extends BaseController
* @OA\Parameter(name="end_company_date", in="query", @OA\Schema(type="string"), required=true, description="结束成立日期"),
* @OA\Parameter(name="start_birthday", in="query", @OA\Schema(type="string"), required=true, description="开始出生日期"),
* @OA\Parameter(name="end_birthday", in="query", @OA\Schema(type="string"), required=true, description="结束出生日期"),
* @OA\Parameter(name="sign_start_date", in="query", @OA\Schema(type="string"), required=false, description="报名开始时间"),
* @OA\Parameter(name="sign_end_date", in="query", @OA\Schema(type="string"), required=false, description="报名结束时间"),
* @OA\Parameter(name="company_need_fund", in="query", @OA\Schema(type="string"), required=true, description="是否需要融资"),
* @OA\Parameter(name="is_fee", in="query", @OA\Schema(type="string"), required=true, description="是否缴费0否1是"),
* @OA\Parameter(name="has_openid", in="query", @OA\Schema(type="string"), required=true, description="是否绑定小程序0否1是"),
@ -211,10 +213,10 @@ class UserController extends BaseController
$query->with('course.typeDetail')->orderBy('fee_status', 'desc');
}
])->withCount([
'appointments' => function ($query) {
$query->whereIn('status', [0, 1]);
}
]);
'appointments' => function ($query) {
$query->whereIn('status', [0, 1]);
}
]);
// 是否被投企业
if (isset($all['is_yh_invested'])) {
$list = $list->whereHas('company', function ($query) use ($all) {
@ -253,6 +255,14 @@ class UserController extends BaseController
if (isset($all['status'])) {
$query->where('status', $all['status']);
}
// 报名时间筛选
if (isset($all['sign_start_date']) && isset($all['sign_end_date'])) {
$query->whereBetween('created_at', [$all['sign_start_date'], $all['sign_end_date']]);
} elseif (isset($all['sign_start_date'])) {
$query->where('created_at', '>=', $all['sign_start_date']);
} elseif (isset($all['sign_end_date'])) {
$query->where('created_at', '<=', $all['sign_end_date']);
}
$query->whereHas('course', function ($q) use ($all) {
if (isset($all['year'])) {
$q->where('year', $all['year']);
@ -542,7 +552,8 @@ class UserController extends BaseController
} else {
if (in_array($k, ['company_type', 'type'])) {
$list[$key][$k] = str_replace('、', ',', $value[$v]);
$list[$key][$k] = str_replace('', ',', $list[$key][$k]);;
$list[$key][$k] = str_replace('', ',', $list[$key][$k]);
;
} else {
$list[$key][$k] = $value[$v];
}
@ -693,11 +704,331 @@ class UserController extends BaseController
if (isset($all['is_schoolmate'])) {
$data['is_schoolmate'] = $all['is_schoolmate'];
}
// if (isset($all['is_black'])) {
// $data['is_black'] = $all['is_black'];
// }
$this->model->whereIn('id', $idsArray)->update($data);
return $this->success('批量更新成功');
}
/**
* @OA\Post(
* path="/api/admin/users/batch-update",
* tags={"用户信息"},
* summary="批量更新用户信息",
* description="",
* @OA\Parameter(name="ids", in="query", @OA\Schema(type="string"), required=true, description="英文逗号分隔的用户id"),
* @OA\Parameter(name="data", in="query", @OA\Schema(type="object"), required=true, description="需要更新的字段对象,键为字段名,值为字段值,例如:{\"is_vip\":\"1\",\"is_schoolmate\":\"1\",\"talent_tags\":\"标签1,标签2\"}"),
* @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="token"),
* @OA\Response(
* response="200",
* description="暂无"
* )
* )
*/
public function batchUpdate()
{
$all = \request()->all();
$messages = [
'ids.required' => '用户ID必填',
'data.required' => '更新数据必填',
];
$validator = Validator::make($all, [
'ids' => 'required',
'data' => 'required|array',
], $messages);
if ($validator->fails()) {
return $this->fail([StarterResponseCode::START_ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
// 获取可更新的字段列表fillable字段
$fillableFields = $this->model->getFillable();
// 构建更新数据只保留fillable中的字段
$data = [];
foreach ($all['data'] as $field => $value) {
// 只允许更新fillable中的字段
if (in_array($field, $fillableFields)) {
$data[$field] = $value;
}
}
if (empty($data)) {
return $this->fail([StarterResponseCode::START_ERROR_PARAMETER, '没有可更新的有效字段']);
}
// 解析用户ID
$idsArray = explode(',', $all['ids']);
$idsArray = array_filter(array_map('trim', $idsArray));
if (empty($idsArray)) {
return $this->fail([StarterResponseCode::START_ERROR_PARAMETER, '用户ID不能为空']);
}
DB::beginTransaction();
try {
$this->model->whereIn('id', $idsArray)->update($data);
DB::commit();
return $this->success('批量更新成功,共更新 ' . count($idsArray) . ' 条记录');
} catch (\Exception $exception) {
DB::rollBack();
return $this->fail([$exception->getCode(), $exception->getMessage()]);
}
}
/**
* @OA\Post(
* path="/api/admin/users/excel-show-special",
* tags={"用户信息"},
* summary="特殊导入规则预览(通过姓名、公司、职位匹配,仅更新不创建)",
* description="通过姓名、公司名称、职位三个字段匹配现有用户,未匹配到的用户会在返回结果中提示",
* @OA\Parameter(name="file", 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",
* description="返回匹配结果,包含未匹配用户列表"
* )
* )
*/
public function excelShowSpecial()
{
$file = \request()->file('file');
//判断文件是否有效
if (!(\request()->hasFile('file') && $file->isValid())) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '文件不存在或无效']);
}
//获取文件大小
$img_size = floor($file->getSize() / 1024);
if ($img_size >= 50 * 1024) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '文件必须小于50M']);
}
//过滤文件后缀
$ext = $file->getClientOriginalExtension();
if (!in_array($ext, ['xls', 'xlsx', 'csv'])) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '仅支持xls/xlsx/csv格式']);
}
$tempFile = $file->getRealPath();
$dataArray = (new FastExcel)->import($tempFile)->toArray();
// 数据过滤,只能导入数据表有的字段
$tableName = $this->model->getTable();
$rowTableFieldByComment = (new CustomFormField)->getRowTableFieldsByComment($tableName);
$list = [];
$unmatchedUsers = []; // 记录未匹配到的用户
foreach ($dataArray as $key => $value) {
// 获取姓名、公司名称、职位
$name = $value['姓名'] ?? $value['name'] ?? '';
$companyName = $value['公司名称'] ?? $value['company_name'] ?? '';
$companyPosition = $value['职位'] ?? $value['company_position'] ?? '';
// 通过姓名、公司名称、职位匹配用户
$matchedUser = null;
if (!empty($name) && !empty($companyName) && !empty($companyPosition)) {
$matchedUser = $this->model->where('username', $name)
->where('company_name', $companyName)
->where('company_position', $companyPosition)
->first();
}
// 记录未匹配到的用户
if (!$matchedUser) {
$unmatchedUsers[] = [
'row' => $key + 1, // Excel行号从1开始
'name' => $name,
'company_name' => $companyName,
'company_position' => $companyPosition,
];
}
// 构建返回数据(使用手机号作为唯一标识)
$list[$key] = [
'name' => $name,
'company_name' => $companyName,
'company_position' => $companyPosition,
'matched' => $matchedUser ? true : false,
'mobile' => $matchedUser ? $matchedUser->mobile : null, // 使用手机号作为唯一标识
'existing_data' => $matchedUser ? [
'id' => $matchedUser->id,
'username' => $matchedUser->username,
'name' => $matchedUser->name,
'mobile' => $matchedUser->mobile,
'company_name' => $matchedUser->company_name,
'company_position' => $matchedUser->company_position,
] : null,
];
// 处理其他字段
foreach ($rowTableFieldByComment as $k => $v) {
if (isset($value[$v])) {
// 日期格式
if ($value[$v] instanceof \DateTimeImmutable) {
$list[$key][$k] = Carbon::parse($value[$v])->toDateString();
} else {
if (in_array($k, ['company_type', 'type'])) {
$list[$key][$k] = str_replace('、', ',', $value[$v]);
$list[$key][$k] = str_replace('', ',', $list[$key][$k]);
} else {
$list[$key][$k] = $value[$v];
}
}
}
}
}
// 构建返回结果
$result = [
'list' => $list,
'matched_count' => count($list) - count($unmatchedUsers),
'unmatched_count' => count($unmatchedUsers),
'unmatched_users' => $unmatchedUsers,
];
// 如果有未匹配到的用户,添加提示信息
if (count($unmatchedUsers) > 0) {
$unmatchedNames = array_map(function ($user) {
return "第{$user['row']}行:{$user['name']}{$user['company_name']} - {$user['company_position']}";
}, $unmatchedUsers);
$result['message'] = '以下用户未匹配到,请检查数据:' . implode('', $unmatchedNames);
}
return $this->success($result);
}
/**
* @OA\Post(
* path="/api/admin/users/import-special",
* tags={"用户信息"},
* summary="特殊导入规则导入(通过姓名、公司、职位匹配,仅更新不创建)",
* description="仅更新已匹配到的用户,如果存在未匹配到的用户,将返回错误并回滚事务",
* @OA\Parameter(name="data", 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",
* description="返回更新结果"
* )
* )
*/
public function importSpecial()
{
$all = \request()->all();
$messages = [
'data.required' => '数据必填',
];
$validator = Validator::make($all, [
'data' => 'required',
], $messages);
if ($validator->fails()) {
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
$filteredRecords = $all['data'];
$suc = 0;
$updateCount = 0;
$unmatchedUsers = []; // 记录未匹配到的用户
DB::beginTransaction();
try {
foreach ($filteredRecords as $index => $record) {
// 获取匹配的手机号(如果预览时已匹配到)
$mobile = $record['mobile'] ?? null;
// 如果没有mobile尝试通过姓名、公司、职位再次匹配
if (!$mobile) {
$name = $record['name'] ?? '';
$companyName = $record['company_name'] ?? '';
$companyPosition = $record['company_position'] ?? '';
if (!empty($name) && !empty($companyName) && !empty($companyPosition)) {
$matchedUser = $this->model->where('username', $name)
->where('company_name', $companyName)
->where('company_position', $companyPosition)
->first();
if ($matchedUser) {
$mobile = $matchedUser->mobile;
}
}
}
// 如果仍未匹配到手机号,记录并跳过
if (!$mobile) {
$unmatchedUsers[] = [
'row' => $index + 1,
'name' => $record['name'] ?? '',
'company_name' => $record['company_name'] ?? '',
'company_position' => $record['company_position'] ?? '',
];
continue;
}
// 去除匹配相关的字段,避免更新到数据库
unset($record['matched'], $record['existing_data']);
// 去除空值
$record = array_filter($record, function ($value) {
return $value != '';
});
// 通过手机号查找并更新用户
$user = $this->model->where('mobile', $mobile)->first();
if ($user) {
// 设置username如果name字段存在
if (isset($record['name'])) {
$record['username'] = $record['name'];
}
// 所有数据通过追加的形式更新参考importStudy的逻辑
foreach ($record as $k => &$v) {
if (!in_array($k, User::$coverFields)) {
// 追加更新
$tempArray = explode(',', ($user->$k ?? '') . ',' . $v);
$tempArray = array_unique(array_filter($tempArray));
$v = implode(',', $tempArray);
}
}
$user->fill($record);
$user->save();
$updateCount++;
$suc++;
// 写入报名表(如果有课程信息)
if (isset($record['course_id']) && !empty($record['course_id'])) {
$whereSign = ['course_id' => $record['course_id'], 'user_id' => $user->id];
$dataSign = [
'course_id' => $record['course_id'],
'user_id' => $user->id,
'is_import' => 1,
'status' => $record['status'] ?? 1,
'fee_status' => $record['fee_status'] ?? 0
];
$courseSign = CourseSign::updateOrCreate($whereSign, $dataSign);
// 加导入次数,加预约次数
if ($courseSign->status == 1) {
CourseAppointmentTotal::add($courseSign->user_id, $courseSign->id);
}
}
}
}
// 如果有未匹配到的用户,返回错误
if (count($unmatchedUsers) > 0) {
DB::rollBack();
$unmatchedNames = array_map(function ($user) {
return "第{$user['row']}行:{$user['name']}{$user['company_name']} - {$user['company_position']}";
}, $unmatchedUsers);
return $this->fail([
ResponseCode::ERROR_BUSINESS,
'以下用户未匹配到,无法更新:' . implode('', $unmatchedNames)
]);
}
DB::commit();
return $this->success([
'total' => count($filteredRecords),
'suc' => $suc,
'update_count' => $updateCount
]);
} catch (\Exception $exception) {
DB::rollBack();
return $this->fail([$exception->getCode(), $exception->getMessage()]);
}
}
}

@ -0,0 +1,22 @@
<?php
namespace App\Models;
class BirthdayMessage extends SoftDeletesModel
{
/**
* 随机获取一条启用的生日祝福文案
*
* @return string|null
*/
public static function getRandomMessage()
{
$message = self::where('status', 1)
->inRandomOrder()
->first();
return $message ? $message->content : null;
}
}

@ -74,7 +74,8 @@ class User extends Authenticatable implements Auditable
'deleted_at',
'no',
'from',
'open_course_types'
'open_course_types',
'talent_tags'
];
protected $appends = ['is_vip_text', 'is_schoolmate_text', 'appointment_total', 'name'];

@ -0,0 +1,94 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('birthday_messages', function (Blueprint $table) {
$table->id();
// 短信内容
$table->text('content')->nullable()->comment('短信内容,包含{username}占位符');
// 状态0禁用1启用
$table->tinyInteger('status')->default(1)->comment('状态0禁用1启用');
// 排序
$table->integer('sort')->default(0)->comment('排序');
$table->timestamps();
$table->softDeletes();
});
// 插入初始7条文案数据
$messages = [
[
'content' => '亲爱的{username}校友苏州科技商学院祝您生日快乐愿您的科创征途繁花似锦打开STBC小程序查看您的生日惊喜',
'status' => 1,
'sort' => 1,
'created_at' => now(),
'updated_at' => now(),
],
[
'content' => '亲爱的{username}校友苏州科技商学院祝您生日快乐愿您的科创蓝图皆能落地生花打开STBC小程序查看您的生日惊喜',
'status' => 1,
'sort' => 2,
'created_at' => now(),
'updated_at' => now(),
],
[
'content' => '亲爱的{username}校友苏州科技商学院祝您生日快乐愿您所行皆坦途打开STBC小程序查看您的生日惊喜',
'status' => 1,
'sort' => 3,
'created_at' => now(),
'updated_at' => now(),
],
[
'content' => '亲爱的{username}校友苏州科技商学院祝您生日快乐愿您科创破局事业长红打开STBC小程序查看您的生日惊喜',
'status' => 1,
'sort' => 4,
'created_at' => now(),
'updated_at' => now(),
],
[
'content' => '亲爱的{username}校友苏州科技商学院祝您生日快乐愿您前路商道开阔所遇皆为坦途岁岁年年皆得圆满打开STBC小程序查看您的生日惊喜',
'status' => 1,
'sort' => 5,
'created_at' => now(),
'updated_at' => now(),
],
[
'content' => '亲爱的{username}校友苏州科技商学院祝您生日快乐愿您执掌事业乾坤亦拥生活繁花似锦打开STBC小程序查看您的生日惊喜',
'status' => 1,
'sort' => 6,
'created_at' => now(),
'updated_at' => now(),
],
[
'content' => '亲爱的{username}校友苏州科技商学院祝您生日快乐愿您基业长青生活常愉岁岁皆有新光景打开STBC小程序查看您的生日惊喜',
'status' => 1,
'sort' => 7,
'created_at' => now(),
'updated_at' => now(),
],
];
DB::table('birthday_messages')->insert($messages);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('birthday_messages');
}
};

@ -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('users', function (Blueprint $table) {
$table->string('talent_tags')->nullable()->comment('人才标签,多个值用英文逗号分隔');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('talent_tags');
});
}
};

@ -0,0 +1,65 @@
<?php
namespace Database\Seeders;
use App\Models\BirthdayMessage;
use Illuminate\Database\Seeder;
class BirthdayMessageSeeder extends Seeder
{
/**
* Run the database seeds.
* php artisan db:seed --class=BirthdayMessageSeeder
* @return void
*/
public function run()
{
$messages = [
[
'content' => '亲爱的{username}校友苏州科技商学院祝您生日快乐愿您的科创征途繁花似锦打开STBC小程序查看您的生日惊喜',
'status' => 1,
'sort' => 1,
],
[
'content' => '亲爱的{username}校友苏州科技商学院祝您生日快乐愿您的科创蓝图皆能落地生花打开STBC小程序查看您的生日惊喜',
'status' => 1,
'sort' => 2,
],
[
'content' => '亲爱的{username}校友苏州科技商学院祝您生日快乐愿您所行皆坦途打开STBC小程序查看您的生日惊喜',
'status' => 1,
'sort' => 3,
],
[
'content' => '亲爱的{username}校友苏州科技商学院祝您生日快乐愿您科创破局事业长红打开STBC小程序查看您的生日惊喜',
'status' => 1,
'sort' => 4,
],
[
'content' => '亲爱的{username}校友苏州科技商学院祝您生日快乐愿您前路商道开阔所遇皆为坦途岁岁年年皆得圆满打开STBC小程序查看您的生日惊喜',
'status' => 1,
'sort' => 5,
],
[
'content' => '亲爱的{username}校友苏州科技商学院祝您生日快乐愿您执掌事业乾坤亦拥生活繁花似锦打开STBC小程序查看您的生日惊喜',
'status' => 1,
'sort' => 6,
],
[
'content' => '亲爱的{username}校友苏州科技商学院祝您生日快乐愿您基业长青生活常愉岁岁皆有新光景打开STBC小程序查看您的生日惊喜',
'status' => 1,
'sort' => 7,
],
];
foreach ($messages as $message) {
BirthdayMessage::updateOrCreate(
['sort' => $message['sort']],
$message
);
}
$this->command->info('已生成 ' . count($messages) . ' 条生日祝福短信文案数据');
}
}

@ -84,9 +84,12 @@ Route::group(["namespace" => "Admin", "prefix" => "admin"], function () {
Route::post('users/save', [\App\Http\Controllers\Admin\UserController::class, "save"]);
Route::get('users/destroy', [\App\Http\Controllers\Admin\UserController::class, "destroy"]);
Route::post('users/excel-show', [\App\Http\Controllers\Admin\UserController::class, "excelShow"]);
Route::post('users/excel-show-special', [\App\Http\Controllers\Admin\UserController::class, "excelShowSpecial"]);
Route::post('users/import', [\App\Http\Controllers\Admin\UserController::class, "import"]);
Route::post('users/import-study', [\App\Http\Controllers\Admin\UserController::class, "importStudy"]);
Route::post('users/import-special', [\App\Http\Controllers\Admin\UserController::class, "importSpecial"]);
Route::post('users/batch-update-schoolmate', [\App\Http\Controllers\Admin\UserController::class, "batchUpdateSchoolmate"]);
Route::post('users/batch-update', [\App\Http\Controllers\Admin\UserController::class, "batchUpdate"]);
// 老师管理
Route::get('teachers/index', [\App\Http\Controllers\Admin\TeacherController::class, "index"]);

Loading…
Cancel
Save