master
lion 7 months ago
commit d15dc80867

@ -16,14 +16,14 @@ class UpdateCompany extends Command
*
* @var string
*/
protected $signature = 'update_company {--user_id=}';
protected $signature = 'update_company {--user_id=} {--address=0}';
/**
* The console command description.
*
* @var string
*/
protected $description = '批量公司信息';
protected $description = '批量更新公司信息';
/**
* Create a new command instance.
@ -43,10 +43,13 @@ class UpdateCompany extends Command
public function handle()
{
$user_id = $this->option('user_id');
$updateLocal = (int)$this->option('address');
// 更新公司信息
$this->compnay($user_id);
// 更新经纬度信息
$this->local($user_id);
// 更新经纬度信息(可选)
if ($updateLocal) {
$this->local($user_id);
}
return $this->info('全部更新完成');
}
@ -57,22 +60,43 @@ class UpdateCompany extends Command
{
if ($user_id) {
// 强制单个更新
$users = User::where('id', $user_id)->get();
$users = User::whereHas('courseSigns', function ($query) {
$query->where('status', 1);
})->where('id', $user_id)->get();
} else {
// 批量更新
$users = User::whereDoesntHave('company')->whereNotNull('company_name')->get();
// 批量更新(只更新有报名审核通过的用户)
$users = User::whereHas('courseSigns', function ($query) {
$query->where('status', 1);
})->whereNotNull('company_name')->get();
}
$total = $users->count();
if ($total == 0) {
return $this->info('没有需要更新的用户');
}
$this->info("开始更新公司信息,共 {$total} 个用户");
$bar = $this->output->createProgressBar($total);
$bar->start();
$YuanheRepository = new YuanheRepository();
foreach ($users as $user) {
// 获取公司详细信息
$result = $YuanheRepository->companyInfo(['enterpriseName' => $user->company_name]);
if (!$result) {
$this->info($user->company_name . '公司不存在');
$bar->setMessage($user->company_name . ' 公司不存在', 'status');
$bar->advance();
continue;
}
// 如果$result['enterpriseName']存在数字,跳过
if (preg_match('/\d/', $result['enterpriseName'])) {
$this->info($user->company_name . '公司名称包含数字,跳过');
$bar->setMessage($user->company_name . ' 公司名称包含数字,跳过', 'status');
$bar->advance();
continue;
}
if ($result['status'] == '未注册') {
$bar->setMessage($user->company_name . ' 公司未注册,跳过', 'status');
$bar->advance();
continue;
}
$where = ['company_name' => $result['enterpriseName']];
@ -102,13 +126,23 @@ class UpdateCompany extends Command
'stock_number' => $result['stockNumber'],
'stock_type' => $result['stockType'],
'company_tag' => implode(',', $result['tagList']),
// 更新日期
'update_date' => $result['updatedDate'] ?? null,
// 管理平台
'project_users' => $result['projectUsers'] ?? null,
// 股东信息
'partners' => $result['partners'] ?? null,
];
$company = Company::updateOrCreate($where, $data);
// 更新用户关联
$user->company_id = $company->id;
$user->save();
$this->info($result['enterpriseName'] . '-更新成功');
$bar->setMessage($result['enterpriseName'] . ' 更新成功', 'status');
$bar->advance();
}
$bar->finish();
$this->newLine();
return $this->info('公司信息-全部更新完成');
}
@ -121,7 +155,7 @@ class UpdateCompany extends Command
// 强制单个更新
$user = User::find($user_id);
if (empty($user->company_id)) {
return false;
return $this->error('用户没有关联公司');
}
$companys = Company::where('id', $user->company_id)->get();
} else {
@ -131,19 +165,33 @@ class UpdateCompany extends Command
->where('company_address', '!=', '')
->get();
}
$total = $companys->count();
if ($total == 0) {
return $this->info('没有需要更新经纬度的公司');
}
$this->info("开始更新经纬度信息,共 {$total} 个公司");
$bar = $this->output->createProgressBar($total);
$bar->start();
// 每3个数据分一个chunk 。接口限制了一秒只能3次请求
$companys = $companys->chunk(3);
foreach ($companys as $company) {
foreach ($company as $item) {
$companysChunk = $companys->chunk(3);
foreach ($companysChunk as $companyChunk) {
foreach ($companyChunk as $item) {
$local = Company::addressTolocation($item->company_address);
$item->company_longitude = $local['lng'];
$item->company_latitude = $local['lat'];
$item->save();
$this->info($item->company_name . "-{$local['lng']}-{$local['lat']}-经纬度信息更新成功");
$bar->setMessage($item->company_name . " 经纬度({$local['lng']}, {$local['lat']})更新成功", 'status');
$bar->advance();
}
sleep(1);
}
return true;
$bar->finish();
$this->newLine();
return $this->info('经纬度信息-全部更新完成');
}
}

@ -0,0 +1,218 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Exports\BaseExport;
use App\Helpers\ResponseCode;
use App\Models\AppointmentType;
use App\Models\Article;
use App\Models\Book;
use App\Models\Calendar;
use App\Models\Company;
use App\Models\CourseContentEvaluationAsk;
use App\Models\CourseContentEvaluationForm;
use App\Models\CustomForm;
use App\Models\CustomFormField;
use App\Models\EmailTemplate;
use App\Models\SupplyDemand;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Maatwebsite\Excel\Facades\Excel;
use Rap2hpoutre\FastExcel\FastExcel;
class ArticleController extends BaseController
{
/**
* 构造函数
*/
public function __construct()
{
parent::__construct(new Article());
}
/**
* @OA\Get(
* path="/api/admin/article/index",
* tags={"文章"},
* summary="列表",
* description="",
* @OA\Parameter(name="is_export", in="query", @OA\Schema(type="string"), required=false, description="是否导出0否1是"),
* @OA\Parameter(name="export_fields", in="query", @OA\Schema(type="string"), required=false, description="需要导出的字段数组"),
* @OA\Parameter(name="filter", in="query", @OA\Schema(type="string"), required=false, description="查询条件。数组"),
* @OA\Parameter(name="show_relation", in="query", @OA\Schema(type="string"), required=false, description="需要输出的关联关系数组包括teachercourseSettingscoursePeriods"),
* @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="token", in="query", @OA\Schema(type="string"), required=true, description="token"),
* @OA\Response(
* response="200",
* description="暂无"
* )
* )
*/
public function index()
{
$all = request()->all();
$list = $this->model->where(function ($query) use ($all) {
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 . '%');
}
// 范围搜索
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);
}
/**
* @OA\Get(
* path="/api/admin/article/show",
* tags={"文章"},
* summary="详情",
* description="",
* @OA\Parameter(name="id", in="query", @OA\Schema(type="string"), required=true, description="id"),
* @OA\Parameter(name="show_relation", in="query", @OA\Schema(type="string"), required=false, description="需要输出的关联关系数组,填写输出指定数据"),
* @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="token"),
* @OA\Response(
* response="200",
* description="暂无"
* )
* )
*/
public function show()
{
$all = \request()->all();
$messages = [
'id.required' => 'Id必填',
];
$validator = Validator::make($all, [
'id' => 'required'
], $messages);
if ($validator->fails()) {
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
$detail = $this->model->find($all['id']);
return $this->success($detail);
}
/**
* @OA\Post(
* path="/api/admin/article/save",
* tags={"文章"},
* summary="保存",
* description="",
* @OA\Parameter(name="id", in="query", @OA\Schema(type="int"), required=true, description="Id(存在更新,不存在新增)"),
* @OA\Parameter(name="title", in="query", @OA\Schema(type="string", nullable=true), description="标题"),
* @OA\Parameter(name="content", in="query", @OA\Schema(type="string", nullable=true), description="内容"),
* @OA\Parameter(name="type", in="query", @OA\Schema(type="string", nullable=true), description="类型1校友动态2业界动态"),
* @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="认证token"),
* @OA\Response(
* response="200",
* description="操作成功"
* )
* )
*/
public function 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();
return $this->success($model);
} catch (\Exception $exception) {
DB::rollBack();
return $this->fail([$exception->getCode(), $exception->getMessage()]);
}
}
/**
* @OA\Get(
* path="/api/admin/article/destroy",
* tags={"文章"},
* summary="删除",
* description="",
* @OA\Parameter(name="id", in="query", @OA\Schema(type="string"), required=true, description="id"),
* @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="token"),
* @OA\Response(
* response="200",
* description="暂无"
* )
* )
*/
public function destroy()
{
return parent::destroy();
}
}

@ -115,6 +115,7 @@ class CalendarsController extends BaseController
* @OA\Parameter(name="end_time", in="query", @OA\Schema(type="string", format="date-time"), required=false, description="结束时间YYYY-MM-DD HH:MM:SS"),
* @OA\Parameter(name="is_publish", in="query", @OA\Schema(type="string"), required=true, description="是否向用户发布0否1是"),
* @OA\Parameter(name="address", in="query", @OA\Schema(type="string"), required=true, description="地址"),
* @OA\Parameter(name="days", 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",

@ -30,6 +30,34 @@ class CompanyController extends BaseController
parent::__construct(new Company());
}
/**
* @OA\Get(
* path="/api/admin/company/config",
* tags={"公司管理"},
* summary="参数",
* description="",
* @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="token"),
* @OA\Response(
* response="200",
* description="暂无"
* )
* )
*/
public function config()
{
// 企业标签
$companiesTags = Company::where('company_tag', '!=', '')->pluck('company_tag');
$companiesTags = $companiesTags->flatten()->implode(',');
$companiesTags =array_unique(explode(',', $companiesTags));
// 去除$companiesTags中包含小数点的元素
$companiesTags = array_filter($companiesTags, function ($item) {
return !strpos($item, '.');
});
$companiesTags = array_values($companiesTags);
return $this->success(compact('companiesTags'));
}
/**
* @OA\Get(

@ -58,91 +58,107 @@ class CourseController extends BaseController
{
$all = request()->all();
$list = $this->model->with(underlineToHump($all['show_relation'] ?? []))
->withCount(['courseSigns' => function ($query) {
$query->whereNotIn('status', [4, 5]);
}])->withCount(['courseSigns as sign_pass_total' => function ($query) {
$query->where('status', 1)->whereHas('user');
}])->withCount(['courseSigns as sign_wait_total' => function ($query) {
$query->where('status', 0)->whereHas('user');
}])->withCount(['courseSigns as sign_fault_total' => function ($query) {
$query->where('status', 2)->whereHas('user');
}])->withCount(['courseSigns as sign_prepare_total' => function ($query) {
$query->where('status', 3)->whereHas('user');
}])->withCount(['courseSigns as sign_cancel_total' => function ($query) {
$query->where('status', 4)->whereHas('user');
}])->withCount(['courseSigns as sign_give_up_total' => function ($query) {
$query->where('status', 5)->whereHas('user');
}])->withCount(['courseSigns as sign_black_total' => function ($query) {
$query->where('status', 6)->whereHas('user');
}])->where(function ($query) use ($all) {
if (isset($all['has_course_forms']) && !empty($all['has_course_forms'])) {
$query->whereHas('courseForms');
->withCount([
'courseSigns' => function ($query) {
$query->whereNotIn('status', [4, 5]);
}
if (isset($all['start_date'])) {
$query->where('start_date', '>=', $all['start_date']);
}
if (isset($all['end_date'])) {
$query->where('end_date', '<=', $all['end_date']);
}
if (isset($all['course_type_id'])) {
$course_type_id = explode(',', $all['course_type_id']);
$query->whereIn('type', $course_type_id);
}
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 . '%');
}
// 范围搜索
if ($op == 'range') {
list($from, $to) = explode(',', $value);
if (empty($from) || empty($to)) {
])->withCount([
'courseSigns as sign_pass_total' => function ($query) {
$query->where('status', 1)->whereHas('user');
}
])->withCount([
'courseSigns as sign_wait_total' => function ($query) {
$query->where('status', 0)->whereHas('user');
}
])->withCount([
'courseSigns as sign_fault_total' => function ($query) {
$query->where('status', 2)->whereHas('user');
}
])->withCount([
'courseSigns as sign_prepare_total' => function ($query) {
$query->where('status', 3)->whereHas('user');
}
])->withCount([
'courseSigns as sign_cancel_total' => function ($query) {
$query->where('status', 4)->whereHas('user');
}
])->withCount([
'courseSigns as sign_give_up_total' => function ($query) {
$query->where('status', 5)->whereHas('user');
}
])->withCount([
'courseSigns as sign_black_total' => function ($query) {
$query->where('status', 6)->whereHas('user');
}
])->where(function ($query) use ($all) {
if (isset($all['has_course_forms']) && !empty($all['has_course_forms'])) {
$query->whereHas('courseForms');
}
if (isset($all['start_date'])) {
$query->where('start_date', '>=', $all['start_date']);
}
if (isset($all['end_date'])) {
$query->where('end_date', '<=', $all['end_date']);
}
if (isset($all['course_type_id'])) {
$course_type_id = explode(',', $all['course_type_id']);
$query->whereIn('type', $course_type_id);
}
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;
}
$query->whereBetween($key, [$from, $to]);
}
if ($op == 'in') {
$array = explode(',', $value);
$query->whereIn($key, $array);
// 等于
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 . '%');
}
// 范围搜索
if ($op == 'range') {
list($from, $to) = explode(',', $value);
if (empty($from) || empty($to)) {
continue;
}
$query->whereBetween($key, [$from, $to]);
}
if ($op == 'in') {
$array = explode(',', $value);
$query->whereIn($key, $array);
}
}
}
}
});
});
$list = $list->orderBy($all['sort_name'] ?? 'sign_status', $all['sort_type'] ?? 'asc');
if (isset($all['is_export']) && !empty($all['is_export'])) {
$list = $list->limit(5000)->get()->toArray();
@ -219,6 +235,8 @@ class CourseController extends BaseController
* @OA\Parameter(name="latitude", in="query", @OA\Schema(type="string"), description="纬度"),
* @OA\Parameter(name="address_detail", in="query", @OA\Schema(type="string"), description="详细地址"),
* @OA\Parameter(name="url_title", in="query", @OA\Schema(type="string"), description="链接地址"),
* @OA\Parameter(name="is_ganbu", in="query", @OA\Schema(type="integer"), description="是否干部课程-0否1是"),
* @OA\Parameter(name="is_chart", in="query", @OA\Schema(type="integer"), description="是否参与统计-0否1是默认1"),
* @OA\Response(
* response=200,
* description="操作成功"
@ -240,7 +258,6 @@ class CourseController extends BaseController
$all['admin_id'] = $this->getUserId();
$all['department_id'] = $this->getUser()->department_id;
}
$original = $model->getOriginal();
$model->fill($all);
$model->save();
DB::commit();
@ -250,8 +267,6 @@ class CourseController extends BaseController
if ($model->status == 1 && $model->start_date) {
CourseAppointmentTotal::addByCourse($model->id);
}
// 记录日志
$this->saveLogs($original, $model);
return $this->success($model);
} catch (\Exception $exception) {
DB::rollBack();

@ -144,12 +144,12 @@ class CourseSignController extends BaseController
if (isset($all['course_type_id'])) {
$course_type_id = explode(',', $all['course_type_id']);
$courses = Course::where(function ($query) use ($all) {
if (isset($all['start_date'])) {
$query->where('start_date', '>=', $all['start_date']);
}
if (isset($all['end_date'])) {
$query->where('start_date', '<=', $all['end_date']);
}
// if (isset($all['start_date'])) {
// $query->where('start_date', '>=', $all['start_date']);
// }
// if (isset($all['end_date'])) {
// $query->where('start_date', '<=', $all['end_date']);
// }
})->whereIn('type', $course_type_id)->get();
$query->whereNotIn('status', [4, 5])
->where(function ($query) use ($courses) {

@ -7,6 +7,7 @@ use App\Jobs\CancelAppointMeet;
use App\Models\Admin;
use App\Models\Appointment;
use App\Models\AppointmentConfig;
use App\Models\Article;
use App\Models\Calendar;
use App\Models\CarparkLog;
use App\Models\Company;
@ -15,6 +16,8 @@ use App\Models\CourseType;
use App\Models\CustomFormField;
use App\Models\Department;
use App\Models\ParameterDetail;
use App\Models\SupplyDemand;
use App\Models\TimeEvent;
use App\Models\User;
use App\Repositories\DoorRepository;
use App\Repositories\EntranceRepository;
@ -101,6 +104,103 @@ class OtherController extends CommonController
return $this->success(compact('courseTypes', 'schoolmate', 'company', 'industryTotal', 'suzhou', 'country'));
}
/**
* @OA\Get(
* path="/api/admin/other/home-v2",
* tags={"其他"},
* summary="驾驶舱V2",
* description="",
* @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="token"),
* @OA\Response(
* response="200",
* description="暂无"
* )
* )
*/
public function homeV2()
{
// 校友总数
$list['schoolmate_total'] = User::where('is_schoolmate', 1)->count();
// 今年新增校友数
$list['schoolmate_year'] = User::where('is_schoolmate', 1)->where('created_at', 'like', '%' . date('Y') . '%')->count();
// 投后企业
$list['company_invested_total'] = Company::where('is_yh_invested', 1)->count();
// 元和员工参与
// 元和员工参与企业
$companyNameKeyword = ['元禾控股', '元禾原点', '元禾厚望', '元禾重元', '元禾璞华', '元禾谷风', '元禾绿柳', '元禾辰坤', '元禾沙湖', '禾裕集团', '苏州科服', '信诚管理咨询', '集成电路公司', '常州团队', '国企元禾'];
// 获取公司名字包含$companyNameKeyword任意数据的公司需要模糊匹配
$list['company_join_total'] = Company::where(function ($query) use ($companyNameKeyword) {
foreach ($companyNameKeyword as $item) {
$query->orWhere('company_name', 'like', '%' . $item . '%');
}
})->count();
// 全市干部参与企业
$list['company_ganbu_total'] = Company::whereHas('users', function ($query) {
$query->where('from', '跟班学员');
})->count();
// 三个全覆盖
// 苏州头部企业
$list['cover_head_total'] = Company::where('company_tag', 'like', '%' . '高新技术企业' . '%')->count();
// 高层次人才
// 获取人才培训课程
$renCaiCourseIds = Course::whereHas('typeDetail', function ($query) {
$query->where('name', '人才培训');
})->pluck('id');
$list['cover_rencai_total'] = CourseSign::whereIn('course_id', $renCaiCourseIds)->where('status', 1)->count();
// 重点上市公司
$list['cover_stock_total'] = Company::where('company_market', 1)->count();
// 本月课程
$monthCourses = Calendar::with('course.teacher')->where('type', 1)
->where('date', 'like', '%' . date('Y-m') . '%')
->get();
// 课程统计
$courseTypes = CourseType::where('is_chart', 1)->get();
$start_date = CourseType::START_DATE;
$end_date = date('Y-m-d');
foreach ($courseTypes as $courseType) {
// 课程
$courses = Course::where('type', $courseType->id)->get();
// 已开设期数
$courseType->course_periods_total = Course::where('type', $courseType->id)->count();
// 培养人数去重
$courseType->course_signs_total = CourseSign::courseSignsTotalByUnique($start_date, $end_date, 1, $courses->pluck('id'), null, $userIds);
}
// 苏州区域数据
$suzhouArea = Company::where('company_city', '苏州市')->groupBy('company_area')
->whereNotNull('company_area')
->get(['company_area']);
$suzhou = [];
foreach ($suzhouArea as $item) {
$suzhou[] = [
'company_area' => $item->company_area,
'company_total' => User::whereHas('company', function ($query) use ($item) {
$query->where('company_area', $item->company_area);
})->where('is_schoolmate', 1)->count()
];
}
// 全国数据
$countryArea = Company::groupBy('company_city')->whereNotNull('company_city')
->get(['company_city']);
$country = [];
foreach ($countryArea as $item) {
$country[] = [
'company_city' => $item->company_city,
'company_total' => User::whereHas('company', function ($query) use ($item) {
$query->where('company_city', $item->company_city);
})->where('is_schoolmate', 1)->count()
];
}
// 时间轴
$time_axis = TimeEvent::orderBy('sort', 'asc')->get();
// 动态信息
$article['xiaoyou'] = Article::where('type', 1)->limit(7)->orderBy('sort', 'desc')->get();
$article['yejie'] = Article::where('type', 2)->limit(7)->orderBy('sort', 'desc')->get();
$article['supply_demands'] = SupplyDemand::limit(7)->orderBy('created_at', 'desc')->get();
return $this->success(compact('list', 'courseTypes', 'suzhou', 'country', 'monthCourses', 'time_axis', 'article'));
}
/**
* @OA\Get(
* path="/api/admin/other/courses-home",
@ -130,18 +230,18 @@ class OtherController extends CommonController
$course_type_id = CourseType::pluck('id')->toArray();
}
// 课程
$courses = Course::where('start_date', '>=', $start_date)
->where('start_date', '<=', $end_date)
->whereIn('type', $course_type_id)
$courses = Course::whereIn('type', $course_type_id)
// ->where('start_date', '<=', $end_date)
// ->where('start_date', '>=', $start_date)
->get();
// 被投企业数
$list['course_signs_invested'] = CourseSign::yhInvested($start_date, $end_date);
// 报名人数
$list['course_signs_total'] = CourseSign::courseSignsTotal($start_date, $end_date);
// 审核通过人数
$list['course_signs_pass'] = CourseSign::courseSignsTotal($start_date, $end_date, 1);
$list['course_signs_pass'] = CourseSign::courseSignsTotal($start_date, $end_date, 1, $courses->pluck('id'));
// 审核通过人数去重
$list['course_signs_pass_unique'] = CourseSign::courseSignsTotalByUnique($start_date, $end_date, 1);
$list['course_signs_pass_unique'] = CourseSign::courseSignsTotalByUnique($start_date, $end_date, 1, $courses->pluck('id'), null);
// 开课场次
$calendar = Calendar::whereIn('course_id', $courses->pluck('id'))->whereBetween('date', [$start_date, $end_date])->get();
$list['course_total'] = $calendar->count();
@ -156,34 +256,27 @@ class OtherController extends CommonController
$courseTypes = CourseType::whereIn('id', $course_type_id)->get();
foreach ($courseTypes as $courseType) {
// 获取课程
$courses2 = Course::where('start_date', '>=', $start_date)
->where('start_date', '<=', $end_date)
->where('type', $courseType->id)
$courses2 = Course::where('type', $courseType->id)
// ->where('start_date', '<=', $end_date)
// ->where('start_date', '>=', $start_date)
->get();
foreach ($courses2 as $course) {
$courseTypesSum[] = [
'course_type' => $courseType->name,
// 培养人数
'course_type_signs_pass' => CourseSign::courseSignsTotal($start_date, $end_date, 1, $courses2->pluck('id')->toArray()),
'course_type_signs_pass' => CourseSign::courseSignsTotal($start_date, $end_date, 1, $courses2->pluck('id')),
// 去重培养人数
'course_type_signs_pass_unique' => CourseSign::courseSignsTotalByUnique($start_date, $end_date, 1, $courses2->pluck('id')->toArray()),
'course_type_signs_pass_unique' => CourseSign::courseSignsTotalByUnique($start_date, $end_date, 1, $courses2->pluck('id'), null),
'course_name' => $course->name,
'course_signs_pass' => CourseSign::courseSignsTotal($start_date, $end_date, 1, [$course->id]),
];
}
}
// 区域明细统计
$areas = ParameterDetail::where('parameter_id', 5)->get();
foreach ($areas as $area) {
$courseSignByArea = CourseSign::where('status', 1)
->whereHas('user', function ($query) use ($area) {
$query->where('company_area', $area->value);
})->whereDate('created_at', '>=', $start_date)
->whereDate('created_at', '<=', $end_date)
->get();
$area->course_signs_pass = $courseSignByArea->count();
$area->course_signs_pass_unique = User::whereIn('id', $courseSignByArea->pluck('user_id'))->distinct('mobile')->count();
$area->course_signs_pass = CourseSign::courseSignsTotal($start_date, $end_date, 1, $courses->pluck('id'), $area->value);
$area->course_signs_pass_unique = CourseSign::courseSignsTotalByUnique($start_date, $end_date, 1, $courses->pluck('id'), $area->value);
}
return $this->success(compact('list', 'courseTypesSum', 'areas'));
}
@ -305,7 +398,7 @@ class OtherController extends CommonController
{
$model = new DoorRepository();
$result = $model->getAllDoorInfo();
dd(json_encode($result,JSON_UNESCAPED_UNICODE));
dd(json_encode($result, JSON_UNESCAPED_UNICODE));
}

@ -0,0 +1,279 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Exports\BaseExport;
use App\Helpers\ResponseCode;
use App\Models\AppointmentType;
use App\Models\Article;
use App\Models\Book;
use App\Models\Calendar;
use App\Models\Company;
use App\Models\CourseContentEvaluationAsk;
use App\Models\CourseContentEvaluationForm;
use App\Models\CustomForm;
use App\Models\CustomFormField;
use App\Models\EmailTemplate;
use App\Models\StatisticsConfig;
use App\Models\SupplyDemand;
use App\Models\TimeEvent;
use App\Models\User;
use App\Models\CourseSign;
use App\Models\Course;
use App\Models\CourseType;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Maatwebsite\Excel\Facades\Excel;
use Rap2hpoutre\FastExcel\FastExcel;
class StatisticsConfigController extends BaseController
{
/**
* 构造函数
*/
public function __construct()
{
parent::__construct(new StatisticsConfig());
}
/**
* @OA\Get(
* path="/api/admin/statistics-config/index",
* tags={"动态统计"},
* summary="列表",
* description="",
* @OA\Parameter(name="is_export", in="query", @OA\Schema(type="string"), required=false, description="是否导出0否1是"),
* @OA\Parameter(name="export_fields", in="query", @OA\Schema(type="string"), required=false, description="需要导出的字段数组"),
* @OA\Parameter(name="filter", in="query", @OA\Schema(type="string"), required=false, description="查询条件。数组"),
* @OA\Parameter(name="show_relation", in="query", @OA\Schema(type="string"), required=false, description="需要输出的关联关系数组包括teachercourseSettingscoursePeriods"),
* @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="token", in="query", @OA\Schema(type="string"), required=true, description="token"),
* @OA\Response(
* response="200",
* description="暂无"
* )
* )
*/
public function index()
{
$all = request()->all();
$list = $this->model->where(function ($query) use ($all) {
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 . '%');
}
// 范围搜索
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);
}
/**
* @OA\Get(
* path="/api/admin/statistics-config/show",
* tags={"动态统计"},
* summary="详情",
* description="",
* @OA\Parameter(name="id", in="query", @OA\Schema(type="string"), required=true, description="id"),
* @OA\Parameter(name="show_relation", in="query", @OA\Schema(type="string"), required=false, description="需要输出的关联关系数组,填写输出指定数据"),
* @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="token"),
* @OA\Response(
* response="200",
* description="暂无"
* )
* )
*/
public function show()
{
$all = \request()->all();
$messages = [
'id.required' => 'Id必填',
];
$validator = Validator::make($all, [
'id' => 'required'
], $messages);
if ($validator->fails()) {
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
$detail = $this->model->find($all['id']);
return $this->success($detail);
}
/**
* @OA\Post(
* path="/api/admin/statistics-config/save",
* tags={"动态统计"},
* summary="保存统计数据配置",
* description="根据传入的id决定是更新现有配置还是新增新的配置。",
* @OA\Parameter(name="id", in="query", @OA\Schema(type="integer"), required=false, description="配置ID存在则更新不存在则新增"),
* @OA\Parameter(name="name", in="query", @OA\Schema(type="string"), required=true, description="名字"),
* @OA\Parameter(name="key", in="query", @OA\Schema(type="string"), required=false, description="标识key"),
* @OA\Parameter(name="decimal_places", in="query", @OA\Schema(type="integer"), required=false, description="小数点位数默认0"),
* @OA\Parameter(name="description", in="query", @OA\Schema(type="string"), required=false, description="描述"),
* @OA\Parameter(name="config_json", in="query", @OA\Schema(type="string"), required=false, description="配置json包含数据来源、条件设置、统计方式等配置详见配置文档"),
* @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="认证token"),
* @OA\Response(
* response="200",
* description="操作成功"
* )
* )
*/
public function 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();
return $this->success($model);
} catch (\Exception $exception) {
DB::rollBack();
return $this->fail([$exception->getCode(), $exception->getMessage()]);
}
}
/**
* @OA\Get(
* path="/api/admin/statistics-config/destroy",
* tags={"动态统计"},
* summary="删除",
* description="",
* @OA\Parameter(name="id", in="query", @OA\Schema(type="string"), required=true, description="id"),
* @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="token"),
* @OA\Response(
* response="200",
* description="暂无"
* )
* )
*/
public function destroy()
{
return parent::destroy();
}
/**
* @OA\Get(
* path="/api/admin/statistics-config/calculate",
* tags={"动态统计"},
* summary="根据配置key执行统计",
* description="根据配置的key获取对应的统计配置然后根据config_json的配置执行统计查询返回数据列表和统计结果",
* @OA\Parameter(name="key", in="query", @OA\Schema(type="string"), required=true, description="配置的key标识"),
* @OA\Parameter(name="page", in="query", @OA\Schema(type="integer"), required=false, description="页码默认1"),
* @OA\Parameter(name="page_size", in="query", @OA\Schema(type="integer"), required=false, description="每页显示的条数默认10"),
* @OA\Parameter(name="show_type", in="query", @OA\Schema(type="string"), required=false, description="显示类型statistics-统计数据list-列表数据默认statistics"),
* @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="认证token"),
* @OA\Response(
* response="200",
* description="返回统计数据"
* )
* )
*/
public function calculate()
{
$all = \request()->all();
$messages = [
'key.required' => 'key必填',
];
$validator = Validator::make($all, [
'key' => 'required'
], $messages);
if ($validator->fails()) {
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
// 根据key查找配置
$config = $this->model->where('key', $all['key'])->first();
if (empty($config)) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '配置不存在']);
}
try {
// 调用模型中的统计方法
$params = [
'page' => isset($all['page']) ? (int) $all['page'] : 1,
'page_size' => isset($all['page_size']) ? (int) $all['page_size'] : 10,
'show_type' => isset($all['show_type']) ? $all['show_type'] : 'statistics',
];
$result = $config->calculateStatistics($params);
return $this->success($result);
} catch (\Exception $exception) {
return $this->fail([$exception->getCode(), $exception->getMessage()]);
}
}
}

@ -0,0 +1,219 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Exports\BaseExport;
use App\Helpers\ResponseCode;
use App\Models\AppointmentType;
use App\Models\Article;
use App\Models\Book;
use App\Models\Calendar;
use App\Models\Company;
use App\Models\CourseContentEvaluationAsk;
use App\Models\CourseContentEvaluationForm;
use App\Models\CustomForm;
use App\Models\CustomFormField;
use App\Models\EmailTemplate;
use App\Models\SupplyDemand;
use App\Models\TimeEvent;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Maatwebsite\Excel\Facades\Excel;
use Rap2hpoutre\FastExcel\FastExcel;
class TimeEventController extends BaseController
{
/**
* 构造函数
*/
public function __construct()
{
parent::__construct(new TimeEvent());
}
/**
* @OA\Get(
* path="/api/admin/time-event/index",
* tags={"时间轴"},
* summary="列表",
* description="",
* @OA\Parameter(name="is_export", in="query", @OA\Schema(type="string"), required=false, description="是否导出0否1是"),
* @OA\Parameter(name="export_fields", in="query", @OA\Schema(type="string"), required=false, description="需要导出的字段数组"),
* @OA\Parameter(name="filter", in="query", @OA\Schema(type="string"), required=false, description="查询条件。数组"),
* @OA\Parameter(name="show_relation", in="query", @OA\Schema(type="string"), required=false, description="需要输出的关联关系数组包括teachercourseSettingscoursePeriods"),
* @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="token", in="query", @OA\Schema(type="string"), required=true, description="token"),
* @OA\Response(
* response="200",
* description="暂无"
* )
* )
*/
public function index()
{
$all = request()->all();
$list = $this->model->where(function ($query) use ($all) {
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 . '%');
}
// 范围搜索
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);
}
/**
* @OA\Get(
* path="/api/admin/time-event/show",
* tags={"时间轴"},
* summary="详情",
* description="",
* @OA\Parameter(name="id", in="query", @OA\Schema(type="string"), required=true, description="id"),
* @OA\Parameter(name="show_relation", in="query", @OA\Schema(type="string"), required=false, description="需要输出的关联关系数组,填写输出指定数据"),
* @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="token"),
* @OA\Response(
* response="200",
* description="暂无"
* )
* )
*/
public function show()
{
$all = \request()->all();
$messages = [
'id.required' => 'Id必填',
];
$validator = Validator::make($all, [
'id' => 'required'
], $messages);
if ($validator->fails()) {
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
$detail = $this->model->find($all['id']);
return $this->success($detail);
}
/**
* @OA\Post(
* path="/api/admin/time-event/save",
* tags={"时间轴"},
* summary="保存",
* description="",
* @OA\Parameter(name="id", in="query", @OA\Schema(type="int"), required=true, description="Id(存在更新,不存在新增)"),
* @OA\Parameter(name="title", in="query", @OA\Schema(type="string", nullable=true), description="标题"),
* @OA\Parameter(name="content", in="query", @OA\Schema(type="string", nullable=true), description="内容"),
* @OA\Parameter(name="type", in="query", @OA\Schema(type="string", nullable=true), description="类型1校友动态2业界动态"),
* @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="认证token"),
* @OA\Response(
* response="200",
* description="操作成功"
* )
* )
*/
public function 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();
return $this->success($model);
} catch (\Exception $exception) {
DB::rollBack();
return $this->fail([$exception->getCode(), $exception->getMessage()]);
}
}
/**
* @OA\Get(
* path="/api/admin/time-event/destroy",
* tags={"时间轴"},
* summary="删除",
* description="",
* @OA\Parameter(name="id", in="query", @OA\Schema(type="string"), required=true, description="id"),
* @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="token"),
* @OA\Response(
* response="200",
* description="暂无"
* )
* )
*/
public function destroy()
{
return parent::destroy();
}
}

@ -0,0 +1,7 @@
<?php
namespace App\Models;
class Article extends SoftDeletesModel
{
}

@ -4,6 +4,9 @@ namespace App\Models;
class Company extends SoftDeletesModel
{
protected $casts = ['project_users' => 'json', 'partners' => 'json'];
public function users()
{
return $this->hasMany(User::class, 'company_id', 'id');

@ -4,6 +4,7 @@
namespace App\Models;
use Illuminate\Support\Facades\Log;
use OwenIt\Auditing\Models\Audit;
class CourseSign extends SoftDeletesModel
@ -77,30 +78,53 @@ class CourseSign extends SoftDeletesModel
/**
* 指定时间内的报名信息(未去重)
*/
public static function courseSignsTotal($start_date, $end_date, $status = null, $course_ids = null)
public static function courseSignsTotal($start_date, $end_date, $status = null, $course_ids = null, $area = null, $retList = false)
{
$total = CourseSign::whereDate('created_at', '>=', $start_date)
->whereDate('created_at', '<=', $end_date)
->where(function ($query) use ($status, $course_ids) {
->whereHas('user', function ($query) use ($area) {
if (isset($area)) {
if ($area == '苏州市外') {
$allArea = ParameterDetail::where('parameter_id', 5)->get();
$query->whereNotIn('company_area', $allArea->pluck('value'));
} else {
$query->where('company_area', $area);
}
}
})->where(function ($query) use ($status, $course_ids) {
if (isset($status)) {
$query->where('status', $status);
}
if (isset($course_ids)) {
$query->whereIn('course_id', $course_ids);
}
})->whereNotIn('status', [4, 5])
->count();
return $total;
})->whereNotIn('status', [4, 5]);
if ($retList) {
// 返回列表
return $total->get();
} else {
// 返回统计数据
return $total->count();
}
}
/**
* 指定时间内的报名信息(去重)
*/
public static function courseSignsTotalByUnique($start_date, $end_date, $status = null, $course_ids = null)
public static function courseSignsTotalByUnique($start_date, $end_date, $status = null, $course_ids = null, $area = null, $retList = false)
{
$courseSignByType = CourseSign::whereDate('created_at', '>=', $start_date)
->whereDate('created_at', '<=', $end_date)
->where(function ($query) use ($status, $course_ids) {
->whereHas('user', function ($query) use ($area) {
if ($area) {
if ($area == '苏州市外') {
$allArea = ParameterDetail::where('parameter_id', 5)->get();
$query->whereNotIn('company_area', $allArea->pluck('value'));
} else {
$query->where('company_area', $area);
}
}
})->where(function ($query) use ($status, $course_ids) {
if (isset($status)) {
$query->where('status', $status);
}
@ -109,7 +133,14 @@ class CourseSign extends SoftDeletesModel
}
})->whereNotIn('status', [4, 5])
->get();
return User::whereIn('id', $courseSignByType->pluck('user_id'))->distinct('mobile')->count();
$user = User::whereIn('id', $courseSignByType->pluck('user_id'))->distinct('mobile');
if ($retList) {
// 列表
return $user->get();
} else {
// 统计数据
return $user->count();
}
}
/**
@ -130,7 +161,7 @@ class CourseSign extends SoftDeletesModel
->get();
return Company::whereHas('users', function ($query) use ($courseSignByType) {
$query->whereIn('id', $courseSignByType->pluck('user_id'));
})->count();
})->where('is_yh_invested', 1)->count();
}
}

@ -6,6 +6,8 @@ namespace App\Models;
class CourseType extends SoftDeletesModel
{
const START_DATE = '2020-01-01 00:00:00';
public function courses()
{
return $this->hasMany(Course::class, 'type', 'id');

@ -4,7 +4,7 @@ namespace App\Models;
use Illuminate\Support\Facades\Cache;
class Sms
class Sms extends SoftDeletesModel
{
/**
* 检查IP是否被锁定

@ -0,0 +1,587 @@
<?php
namespace App\Models;
use Illuminate\Support\Facades\DB;
class StatisticsConfig extends SoftDeletesModel
{
protected $casts = [
'config_json' => 'json',
];
/**
* 根据配置执行统计计算
*
* @param array $params 参数数组包含page, page_size, show_type
* @return array 返回统计结果数组
* @throws \Exception
*/
public function calculateStatistics($params = [])
{
$page = isset($params['page']) ? (int) $params['page'] : 1;
$pageSize = isset($params['page_size']) ? (int) $params['page_size'] : 10;
$showType = isset($params['show_type']) ? $params['show_type'] : 'statistics';
if (!in_array($showType, ['statistics', 'list'])) {
$showType = 'statistics';
}
$configJson = $this->config_json;
if (empty($configJson)) {
throw new \Exception('配置数据为空');
}
// 如果 config_json 是字符串,尝试解析
if (is_string($configJson)) {
$configJson = json_decode($configJson, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception('配置 JSON 格式错误:' . json_last_error_msg());
}
}
// 检查数据结构
if (!isset($configJson['data_source'])) {
throw new \Exception('配置数据缺少 data_source 字段,当前配置键:' . implode(', ', array_keys($configJson)));
}
// 获取主模型
$mainModelName = $configJson['data_source']['main_model'] ?? '';
if (empty($mainModelName)) {
$dataSourceKeys = isset($configJson['data_source']) ? implode(', ', array_keys($configJson['data_source'])) : '无';
throw new \Exception('主模型名称未配置data_source 中的键:' . $dataSourceKeys);
}
$mainModel = $this->getModel($mainModelName);
if (empty($mainModel)) {
throw new \Exception('主模型不存在:' . $mainModelName . '可用的模型user, company, course_sign, course, course_type');
}
$query = $mainModel::query();
// 获取主模型表名
$tableName = $mainModel::make()->getTable();
// 加载关联模型
$relations = $configJson['data_source']['relations'] ?? [];
if (!empty($relations)) {
foreach ($relations as $relation) {
$query->with($relation);
}
}
// 应用条件
$conditions = $configJson['conditions'] ?? [];
if (!empty($conditions['items'])) {
$logic = $conditions['logic'] ?? 'and';
if ($logic === 'or') {
$query->where(function ($q) use ($conditions, $tableName) {
foreach ($conditions['items'] as $index => $item) {
if ($index === 0) {
$this->applyCondition($q, $item, 'and', 0, $tableName);
} else {
$q->orWhere(function ($subQ) use ($item, $tableName) {
$this->applyCondition($subQ, $item, 'and', 0, $tableName);
});
}
}
});
} else {
foreach ($conditions['items'] as $item) {
$this->applyCondition($query, $item, 'and', 0, $tableName);
}
}
}
// 执行统计
$statistics = $configJson['statistics'] ?? [];
$statisticsType = $statistics['type'] ?? 'count';
$groupBy = $statistics['group_by'] ?? null;
// 确保空字符串也被视为不分组
$groupBy = !empty($groupBy) ? $groupBy : null;
// 保存原始查询用于获取列表
$listQuery = clone $query;
if ($groupBy) {
// 分组统计
$groupParts = explode('.', $groupBy);
if (count($groupParts) > 1) {
// 关联模型字段分组,需要 join
$relationName = $groupParts[0];
$fieldName = $groupParts[1];
$relationModel = $this->getRelationModel($mainModel, $relationName);
if ($relationModel) {
$relationTable = $relationModel::make()->getTable();
$relationKey = $this->getRelationKey($mainModel, $relationName);
$query->join($relationTable, $tableName . '.' . $relationKey, '=', $relationTable . '.id');
$selectFields = [$relationTable . '.' . $fieldName . ' as group_value'];
} else {
$selectFields = [$groupBy . ' as group_value'];
}
} else {
// 主模型字段分组
$selectFields = [$tableName . '.' . $groupBy . ' as group_value'];
}
// 根据统计类型构建 SQL
$statisticsField = $statistics['field'] ?? null;
if ($statisticsType === 'sum' && $statisticsField) {
// 处理关联模型字段
$fieldParts = explode('.', $statisticsField);
if (count($fieldParts) > 1) {
$fieldRelationName = $fieldParts[0];
$fieldFieldName = $fieldParts[1];
$fieldRelationModel = $this->getRelationModel($mainModel, $fieldRelationName);
if ($fieldRelationModel) {
$fieldRelationTable = $fieldRelationModel::make()->getTable();
$fieldRelationKey = $this->getRelationKey($mainModel, $fieldRelationName);
// 如果还没有 join需要 join
$query->leftJoin($fieldRelationTable, $tableName . '.' . $fieldRelationKey, '=', $fieldRelationTable . '.id');
$selectFields[] = DB::raw('SUM(' . $fieldRelationTable . '.' . $fieldFieldName . ') as total');
} else {
$selectFields[] = DB::raw('SUM(' . $tableName . '.' . $statisticsField . ') as total');
}
} else {
$selectFields[] = DB::raw('SUM(' . $tableName . '.' . $statisticsField . ') as total');
}
} elseif ($statisticsType === 'max' && $statisticsField) {
// 处理关联模型字段
$fieldParts = explode('.', $statisticsField);
if (count($fieldParts) > 1) {
$fieldRelationName = $fieldParts[0];
$fieldFieldName = $fieldParts[1];
$fieldRelationModel = $this->getRelationModel($mainModel, $fieldRelationName);
if ($fieldRelationModel) {
$fieldRelationTable = $fieldRelationModel::make()->getTable();
$fieldRelationKey = $this->getRelationKey($mainModel, $fieldRelationName);
// 如果还没有 join需要 join
$query->leftJoin($fieldRelationTable, $tableName . '.' . $fieldRelationKey, '=', $fieldRelationTable . '.id');
$selectFields[] = DB::raw('MAX(' . $fieldRelationTable . '.' . $fieldFieldName . ') as total');
} else {
$selectFields[] = DB::raw('MAX(' . $tableName . '.' . $statisticsField . ') as total');
}
} else {
$selectFields[] = DB::raw('MAX(' . $tableName . '.' . $statisticsField . ') as total');
}
} elseif ($statisticsType === 'min' && $statisticsField) {
// 处理关联模型字段
$fieldParts = explode('.', $statisticsField);
if (count($fieldParts) > 1) {
$fieldRelationName = $fieldParts[0];
$fieldFieldName = $fieldParts[1];
$fieldRelationModel = $this->getRelationModel($mainModel, $fieldRelationName);
if ($fieldRelationModel) {
$fieldRelationTable = $fieldRelationModel::make()->getTable();
$fieldRelationKey = $this->getRelationKey($mainModel, $fieldRelationName);
// 如果还没有 join需要 join
$query->leftJoin($fieldRelationTable, $tableName . '.' . $fieldRelationKey, '=', $fieldRelationTable . '.id');
$selectFields[] = DB::raw('MIN(' . $fieldRelationTable . '.' . $fieldFieldName . ') as total');
} else {
$selectFields[] = DB::raw('MIN(' . $tableName . '.' . $statisticsField . ') as total');
}
} else {
$selectFields[] = DB::raw('MIN(' . $tableName . '.' . $statisticsField . ') as total');
}
} elseif ($statisticsType === 'count_distinct' && isset($statistics['distinct_field'])) {
// 去重数量统计
$distinctField = $statistics['distinct_field'];
// 处理关联模型字段
$fieldParts = explode('.', $distinctField);
if (count($fieldParts) > 1) {
$fieldRelationName = $fieldParts[0];
$fieldFieldName = $fieldParts[1];
$fieldRelationModel = $this->getRelationModel($mainModel, $fieldRelationName);
if ($fieldRelationModel) {
$fieldRelationTable = $fieldRelationModel::make()->getTable();
$fieldRelationKey = $this->getRelationKey($mainModel, $fieldRelationName);
// 如果还没有 join需要 join
$query->leftJoin($fieldRelationTable, $tableName . '.' . $fieldRelationKey, '=', $fieldRelationTable . '.id');
$selectFields[] = DB::raw('COUNT(DISTINCT ' . $fieldRelationTable . '.' . $fieldFieldName . ') as total');
} else {
$selectFields[] = DB::raw('COUNT(DISTINCT ' . $tableName . '.' . $distinctField . ') as total');
}
} else {
$selectFields[] = DB::raw('COUNT(DISTINCT ' . $tableName . '.' . $distinctField . ') as total');
}
} else {
// count 统计总数量
$selectFields[] = DB::raw('COUNT(*) as total');
}
$query->select($selectFields);
if (count($groupParts) > 1) {
$query->groupBy($relationTable . '.' . $fieldName);
} else {
$query->groupBy($tableName . '.' . $groupBy);
}
} else {
// 不分组统计,获取所有数据列表
// 列表查询保持原样,获取所有符合条件的记录
}
// 排序(分组统计时对分组结果排序,不分组时对列表排序)
if (!empty($statistics['order_by'])) {
$orderField = $statistics['order_by']['field'] ?? ($groupBy ? 'total' : 'id');
$orderDirection = $statistics['order_by']['direction'] ?? 'desc';
$query->orderBy($orderField, $orderDirection);
if (!$groupBy) {
// 不分组时,列表查询也需要排序
$listQuery->orderBy($orderField, $orderDirection);
}
} else {
// 没有指定排序时,不分组情况使用默认排序
if (!$groupBy) {
$listQuery->orderBy('id', 'desc');
}
}
// 获取统计查询的 SQL 语句(在执行查询前)
$statisticsSql = $query->toSql();
$statisticsBindings = $query->getBindings();
$statisticsSqlFull = $this->getFullSql($statisticsSql, $statisticsBindings);
// 获取分组统计结果列表(分页)
// 先获取所有结果用于计算统计结果
$allResults = $query->get();
// 对结果进行分页处理
$totalCount = $allResults->count();
$pagedResults = $allResults->slice(($page - 1) * $pageSize, $pageSize);
// 格式化分组统计结果
$data = [];
foreach ($pagedResults as $item) {
$row = [
'group_value' => $item->group_value ?? null,
'total' => round($item->total, $this->decimal_places)
];
$data[] = $row;
}
// 计算统计结果
$statisticsResult = null;
$pagination = null;
if ($groupBy) {
// 分组统计:根据统计类型计算最终结果
$statisticsField = $statistics['field'] ?? null;
if ($statisticsType === 'sum' && $statisticsField) {
// 分组求和时,统计结果是所有分组的总和
$allTotalValue = 0;
foreach ($allResults as $item) {
$allTotalValue += $item->total;
}
$statisticsResult = round($allTotalValue, $this->decimal_places);
} elseif ($statisticsType === 'max' && $statisticsField) {
// 分组最大值时,统计结果是所有分组中的最大值
$maxValue = null;
foreach ($allResults as $item) {
if ($maxValue === null || $item->total > $maxValue) {
$maxValue = $item->total;
}
}
$statisticsResult = $maxValue !== null ? round($maxValue, $this->decimal_places) : 0;
} elseif ($statisticsType === 'min' && $statisticsField) {
// 分组最小值时,统计结果是所有分组中的最小值
$minValue = null;
foreach ($allResults as $item) {
if ($minValue === null || $item->total < $minValue) {
$minValue = $item->total;
}
}
$statisticsResult = $minValue !== null ? round($minValue, $this->decimal_places) : 0;
} else {
// 分组计数时,统计结果是所有分组的计数总和
$allTotalValue = 0;
foreach ($allResults as $item) {
$allTotalValue += $item->total;
}
$statisticsResult = $allTotalValue;
}
// 分页信息
$pagination = [
'current_page' => $page,
'page_size' => $pageSize,
'total' => $totalCount,
'total_pages' => ceil($totalCount / $pageSize)
];
} else {
// 不分组统计:先获取 SQL再执行查询
// 克隆查询用于获取 SQL避免执行后无法获取
$calcQuery = clone $listQuery;
$listQueryForData = clone $listQuery;
// 根据统计类型计算统计值
$statisticsField = $statistics['field'] ?? null;
if ($statisticsType === 'sum' && $statisticsField) {
// 处理关联模型字段
$fieldParts = explode('.', $statisticsField);
if (count($fieldParts) > 1) {
$fieldRelationName = $fieldParts[0];
$fieldFieldName = $fieldParts[1];
$fieldRelationModel = $this->getRelationModel($mainModel, $fieldRelationName);
if ($fieldRelationModel) {
$fieldRelationTable = $fieldRelationModel::make()->getTable();
$fieldRelationKey = $this->getRelationKey($mainModel, $fieldRelationName);
$calcQuery->leftJoin($fieldRelationTable, $tableName . '.' . $fieldRelationKey, '=', $fieldRelationTable . '.id');
$statisticsResult = round($calcQuery->sum($fieldRelationTable . '.' . $fieldFieldName), $this->decimal_places);
} else {
$statisticsResult = round($listQuery->sum($statisticsField), $this->decimal_places);
}
} else {
$statisticsResult = round($listQuery->sum($statisticsField), $this->decimal_places);
}
} elseif ($statisticsType === 'max' && $statisticsField) {
// 处理关联模型字段
$fieldParts = explode('.', $statisticsField);
if (count($fieldParts) > 1) {
$fieldRelationName = $fieldParts[0];
$fieldFieldName = $fieldParts[1];
$fieldRelationModel = $this->getRelationModel($mainModel, $fieldRelationName);
if ($fieldRelationModel) {
$fieldRelationTable = $fieldRelationModel::make()->getTable();
$fieldRelationKey = $this->getRelationKey($mainModel, $fieldRelationName);
$calcQuery->leftJoin($fieldRelationTable, $tableName . '.' . $fieldRelationKey, '=', $fieldRelationTable . '.id');
$statisticsResult = round($calcQuery->max($fieldRelationTable . '.' . $fieldFieldName), $this->decimal_places);
} else {
$statisticsResult = round($listQuery->max($statisticsField), $this->decimal_places);
}
} else {
$statisticsResult = round($listQuery->max($statisticsField), $this->decimal_places);
}
} elseif ($statisticsType === 'min' && $statisticsField) {
// 处理关联模型字段
$fieldParts = explode('.', $statisticsField);
if (count($fieldParts) > 1) {
$fieldRelationName = $fieldParts[0];
$fieldFieldName = $fieldParts[1];
$fieldRelationModel = $this->getRelationModel($mainModel, $fieldRelationName);
if ($fieldRelationModel) {
$fieldRelationTable = $fieldRelationModel::make()->getTable();
$fieldRelationKey = $this->getRelationKey($mainModel, $fieldRelationName);
$calcQuery->leftJoin($fieldRelationTable, $tableName . '.' . $fieldRelationKey, '=', $fieldRelationTable . '.id');
$statisticsResult = round($calcQuery->min($fieldRelationTable . '.' . $fieldFieldName), $this->decimal_places);
} else {
$statisticsResult = round($listQuery->min($statisticsField), $this->decimal_places);
}
} else {
$statisticsResult = round($listQuery->min($statisticsField), $this->decimal_places);
}
} elseif ($statisticsType === 'count_distinct' && isset($statistics['distinct_field'])) {
// 去重数量统计
$distinctField = $statistics['distinct_field'];
// 处理关联模型字段
$fieldParts = explode('.', $distinctField);
if (count($fieldParts) > 1) {
$fieldRelationName = $fieldParts[0];
$fieldFieldName = $fieldParts[1];
$fieldRelationModel = $this->getRelationModel($mainModel, $fieldRelationName);
if ($fieldRelationModel) {
$fieldRelationTable = $fieldRelationModel::make()->getTable();
$fieldRelationKey = $this->getRelationKey($mainModel, $fieldRelationName);
$calcQuery->leftJoin($fieldRelationTable, $tableName . '.' . $fieldRelationKey, '=', $fieldRelationTable . '.id');
$statisticsResult = $calcQuery->selectRaw('COUNT(DISTINCT ' . $fieldRelationTable . '.' . $fieldFieldName . ') as total')->value('total') ?? 0;
} else {
$statisticsResult = $listQuery->selectRaw('COUNT(DISTINCT ' . $distinctField . ') as total')->value('total') ?? 0;
}
} else {
$statisticsResult = $listQuery->selectRaw('COUNT(DISTINCT ' . $tableName . '.' . $distinctField . ') as total')->value('total') ?? 0;
}
} else {
// count 统计总数量
$statisticsResult = $listQuery->count();
}
// 获取统计查询的 SQL在计算完统计值之后确保包含所有修改
$statisticsSqlForCalc = $calcQuery->toSql();
$statisticsBindingsForCalc = $calcQuery->getBindings();
$statisticsSqlFull = $this->getFullSql($statisticsSqlForCalc, $statisticsBindingsForCalc);
// 获取列表总数(用于分页)
$listTotalCount = $listQueryForData->count();
// 不分组时,获取分页数据作为列表
$listResults = $listQueryForData->skip(($page - 1) * $pageSize)->take($pageSize)->get();
$data = [];
foreach ($listResults as $item) {
$data[] = $item->toArray();
}
// 分页信息
$pagination = [
'current_page' => $page,
'page_size' => $pageSize,
'total' => $listTotalCount,
'total_pages' => ceil($listTotalCount / $pageSize)
];
}
// 根据显示类型决定返回的数据
if ($showType === 'statistics') {
// 只返回统计数据
return [
'total' => $statisticsResult,
'sql' => $statisticsSqlFull
];
} else {
// 只返回列表数据
return [
'list' => $data,
'pagination' => $pagination,
'sql' => $statisticsSqlFull
];
}
}
/**
* 获取模型实例
*/
private function getModel($modelName)
{
$models = [
'user' => User::class,
'company' => Company::class,
'course_sign' => CourseSign::class,
'course' => Course::class,
'course_type' => CourseType::class,
];
return $models[$modelName] ?? null;
}
/**
* 应用查询条件
*/
private function applyCondition($query, $condition, $logic, $index, $tableName = null)
{
$key = $condition['key'] ?? '';
$operator = $condition['operator'] ?? 'eq';
$value = $condition['value'] ?? '';
if (empty($key)) {
return;
}
// 处理关联模型的字段
$keyParts = explode('.', $key);
if (count($keyParts) > 1) {
// 关联模型字段,使用 whereHas
$relationName = $keyParts[0];
$fieldName = $keyParts[1];
$query->whereHas($relationName, function ($q) use ($fieldName, $operator, $value) {
$this->applyOperator($q, $fieldName, $operator, $value);
});
return;
}
// 主模型字段,添加表名前缀避免字段歧义
$qualifiedKey = $tableName ? $tableName . '.' . $key : $key;
$this->applyOperator($query, $qualifiedKey, $operator, $value);
}
/**
* 获取关联模型
*/
private function getRelationModel($mainModel, $relationName)
{
$model = new $mainModel();
if (method_exists($model, $relationName)) {
$relation = $model->$relationName();
return get_class($relation->getRelated());
}
return null;
}
/**
* 获取关联键名
*/
private function getRelationKey($mainModel, $relationName)
{
$model = new $mainModel();
if (method_exists($model, $relationName)) {
$relation = $model->$relationName();
return $relation->getForeignKeyName();
}
return 'id';
}
/**
* 获取完整的 SQL 语句(包含绑定参数)
*/
private function getFullSql($sql, $bindings)
{
if (empty($bindings)) {
return $sql;
}
$fullSql = $sql;
foreach ($bindings as $binding) {
$value = is_numeric($binding) ? $binding : "'" . addslashes($binding) . "'";
$fullSql = preg_replace('/\?/', $value, $fullSql, 1);
}
return $fullSql;
}
/**
* 应用操作符
*/
private function applyOperator($query, $key, $operator, $value)
{
switch ($operator) {
case 'eq':
$query->where($key, $value);
break;
case 'neq':
$query->where($key, '!=', $value);
break;
case 'gt':
$query->where($key, '>', $value);
break;
case 'egt':
$query->where($key, '>=', $value);
break;
case 'lt':
$query->where($key, '<', $value);
break;
case 'elt':
$query->where($key, '<=', $value);
break;
case 'like':
$query->where($key, 'like', '%' . $value . '%');
break;
case 'notlike':
$query->where($key, 'not like', '%' . $value . '%');
break;
case 'in':
$array = explode(',', $value);
$query->whereIn($key, $array);
break;
case 'notin':
$array = explode(',', $value);
$query->whereNotIn($key, $array);
break;
case 'between':
list($from, $to) = explode(',', $value);
if (!empty($from) && !empty($to)) {
$query->whereBetween($key, [$from, $to]);
}
break;
case 'notbetween':
list($from, $to) = explode(',', $value);
if (!empty($from) && !empty($to)) {
$query->whereNotBetween($key, [$from, $to]);
}
break;
case 'isnull':
$query->whereNull($key);
break;
case 'isnotnull':
$query->whereNotNull($key);
break;
}
}
}

@ -0,0 +1,7 @@
<?php
namespace App\Models;
class TimeEvent extends SoftDeletesModel
{
}

@ -1,52 +0,0 @@
#!/bin/bash
# 定义1个变量变量是git裸仓库名字例如/www/git/demo.git
<<<<<<< HEAD
GIT_REPO_NAME="/www/git/wx.sstbc.com.git"
=======
GIT_REPO_NAME="/data/git/wx.sstbc.com.git"
>>>>>>> 8508a4aca7d83d334aa2ec18b4291c6d5750a769
#检测系统里是否存在用户名是git的用户如果不存在就创建一个并且设置密码为git@2023
if ! id -u git > /dev/null 2>&1; then
useradd git
echo "已创建用户 git"
echo git:git@2023 | chpasswd
echo "密码已设置为 Git@2018"
else
echo "git用户已存在"
fi
# 检测系统里是否存在git用户组如果不存在就创建一个并把git用户加入git用户组
if grep "^git:" /etc/group >/dev/null 2>&1; then
echo "git用户组已存在"
else
echo "创建git用户组..."
groupadd git
usermod -aG git git
fi
# 根据定义的裸仓库路径,在指定路径下创建裸仓库
echo "创建裸仓库..."
mkdir -p $GIT_REPO_NAME && cd $GIT_REPO_NAME && git init --bare
# 给上一步创建的裸仓库给予git用户组里git用户读写执行权限
echo "给裸仓库设置权限..."
chown git:git * -R
cd -
chown git:git "$GIT_REPO_NAME"
# 切换到当前脚本所在的目录初始化git仓库并且关联之前创建的本地裸仓库地址。
cd "$(dirname "$0")"
if [ -d ".git" ]; then
echo "该目录是一个Git仓库"
else
echo "该目录不是一个Git仓库"
echo "初始化git仓库..."
git init
fi
echo "关联仓库"
git remote add origin $GIT_REPO_NAME
# 输出这个裸仓库的本地链接地址和远程连接地址
echo "远程仓库关联git remote add production ssh://git@ip:$GIT_REPO_NAME"

@ -0,0 +1,306 @@
# 课程统计数据说明文档
## 一、文档说明
本文档用于说明课程统计系统中各项数据的含义和统计方式,帮助您更好地理解和使用统计数据。
---
## 二、如何查询统计数据
### 2.1 查询方式
通过系统后台的"课程统计"功能,您可以查询以下信息:
- **时间范围**:选择需要统计的开始日期和结束日期
- **课程类型**:可以选择全部课程类型,也可以选择特定的课程类型进行统计
### 2.2 统计时间说明
- 统计数据基于**报名记录的创建时间**,而不是课程开课时间
- 例如选择2024年1月1日到2024年12月31日会统计在这个时间段内创建的所有报名记录
---
## 三、核心统计数据说明
### 3.1 被投企业数
**含义**:在统计时间段内,有多少家元禾已投资的企业有员工报名参加了课程。
**统计方式**
- 统计所有在指定时间段内报名的用户
- 查看这些用户所在的公司
- 统计其中被元禾投资的公司数量
- **重要**:如果一家公司有多个员工报名,这家公司只统计一次
**使用场景**:了解元禾投资企业的参与情况
---
### 3.2 报名人数
**含义**:在统计时间段内,总共有多少条报名记录。
**统计方式**
- 统计所有在指定时间段内创建的报名记录
- 包括所有状态的报名(待审核、审核通过、审核不通过等)
- 不包括已取消和主动放弃的报名
- **重要**:如果同一个人报名了多个课程,会按报名次数分别计算
**使用场景**:了解课程的整体报名热度
**举例**
- 张三报名了3个课程 → 统计为3人
- 李四报名了1个课程 → 统计为1人
- 总计4人
---
### 3.3 审核通过人数
**含义**:在统计时间段内,有多少条报名记录通过了审核。
**统计方式**
- 只统计状态为"审核通过"的报名记录
- 不包括已取消和主动放弃的报名
- **重要**:如果同一个人报名了多个课程且都通过了,会按通过次数分别计算
**使用场景**:了解实际可以参加课程的人数
**举例**
- 张三报名了3个课程都通过了 → 统计为3人
- 李四报名了1个课程通过了 → 统计为1人
- 总计4人
---
### 3.4 审核通过人数(去重)
**含义**:在统计时间段内,有多少个不同的用户通过了审核。
**统计方式**
- 只统计状态为"审核通过"的报名记录
- 根据用户的手机号进行去重
- 如果同一个手机号报名了多个课程,只统计一次
- **重要**:这是真实的培养人数,不会重复计算同一个人
**使用场景**:了解实际培养了多少个不同的学员
**举例**
- 张三手机号13800138000报名了3个课程都通过了 → 统计为1人
- 李四手机号13900139000报名了1个课程通过了 → 统计为1人
- 总计2人去重后
---
### 3.5 开课场次
**含义**:在统计时间段内,总共开了多少场课程。
**统计方式**
- 统计指定课程类型下,在指定时间范围内的所有开课记录
- 统计数据源来自于课程日历上的数据
- **重要**:统计的是开课场次,不是课程数量
**使用场景**:了解课程开展的频率和规模
---
### 3.6 开课天数
**含义**:在统计时间段内,所有课程总共开了多少天。
**统计方式**
- 对每场课程计算开课天数(从开始日期到结束日期)
- 包含开始日期和结束日期
- 将所有场次的天数加起来
- 统计数据源来自于课程日历上的数据
**使用场景**:了解课程的总时长
**举例**
- 某场课程从2024年1月1日到1月3日 → 3天包含1日、2日、3日
- 某场课程从2024年2月10日到2月12日 → 3天
- 总计6天
---
## 四、课程分类明细统计说明
### 4.1 统计内容
系统会按照每个课程类型,详细统计以下信息:
#### 课程类型名称
显示课程体系的名称,例如:"高研班"、"初创班"等。
#### 培养人数(未去重,按课程类型)
**含义**:该课程类型下所有课程的审核通过报名人数总和。
**说明**
- 如果同一个学员在该课程类型下报名了多个课程,会按报名次数分别计算
- 例如:张三在"高研班"类型下报名了2个课程都通过了 → 统计为2人
#### 培养人数(去重,按课程类型)
**含义**:该课程类型下有多少个不同的学员通过了审核。
**说明**
- 根据学员的手机号去重
- 同一个手机号在该课程类型下只统计一次
- 例如张三手机号13800138000在"高研班"类型下报名了2个课程都通过了 → 统计为1人
#### 课程名称
显示具体课程的名称,例如:"2024年第一期高研班"。
#### 培养人数(按单个课程)
**含义**:该单个课程的审核通过报名人数。
**说明**
- 只统计该课程的审核通过人数
- 不包括其他课程的数据
---
## 五、区域明细统计说明
### 5.1 统计内容
系统会按照每个区域,详细统计以下信息:
#### 区域名称
显示区域名称,例如:"工业园区"、"高新区"、"相城区"等。
**特殊区域**"苏州市外"表示不在苏州所有区域内的用户。
#### 审核通过人数(未去重,按区域)
**含义**:该区域有多少条审核通过的报名记录。
**说明**
- 根据学员所在公司的区域进行统计
- 如果同一个学员在该区域报名了多个课程,会按报名次数分别计算
- 例如张三公司位于工业园区报名了2个课程都通过了 → 统计为2人
#### 审核通过人数(去重,按区域)
**含义**:该区域有多少个不同的学员通过了审核。
**说明**
- 根据学员的手机号去重
- 同一个手机号在该区域只统计一次
- 例如张三手机号13800138000公司位于工业园区报名了2个课程都通过了 → 统计为1人
---
## 七、数据关系说明
### 7.1 人数关系
通常情况下,数据之间存在以下关系:
**报名人数** ≥ **审核通过人数** ≥ **审核通过人数(去重)**
**解释**
- 报名人数最多,因为包括所有状态的报名
- 审核通过人数次之,因为只包括审核通过的
- 去重人数最少,因为同一个学员只统计一次
**举例**
- 报名人数500人包括待审核、审核通过、审核不通过等
- 审核通过人数450人只包括审核通过的
- 审核通过人数去重380人同一个学员只统计一次
### 7.2 区域汇总与全局去重的关系
**区域汇总** 可能大于 **全局去重人数**
**原因**
- 区域汇总时,每个区域是分别统计的
- 而全局去重是按所有区域统一去重的
---
## 八、报名状态说明
### 8.1 报名状态类型
报名记录有以下几种状态:
| 状态名称 | 说明 | 是否参与统计 |
|---------|------|------------|
| 待审核 | 报名已提交,等待审核 | 参与"报名人数"统计,不参与"审核通过人数"统计 |
| 审核通过 | 报名已通过审核,可以参加课程 | 参与所有统计 |
| 审核不通过 | 报名未通过审核 | 参与"报名人数"统计,不参与"审核通过人数"统计 |
| 备选 | 报名作为备选 | 参与"报名人数"统计,不参与"审核通过人数"统计 |
| 已取消 | 报名已取消 | 不参与任何统计 |
| 主动放弃 | 学员主动放弃报名 | 不参与任何统计 |
| 黑名单 | 学员在黑名单中 | 参与"报名人数"统计,不参与"审核通过人数"统计 |
### 8.2 统计规则
- **报名人数**:统计除"已取消"和"主动放弃"外的所有状态
- **审核通过人数**:只统计"审核通过"状态的记录
- **所有统计**:都不包括"已取消"和"主动放弃"的记录
---
## 九、常见问题解答
### Q1: 为什么"报名人数"比"审核通过人数"多?
**A**: 因为"报名人数"包括所有状态的报名记录(待审核、审核通过、审核不通过、备选等),而"审核通过人数"只包括审核通过的记录。所以报名人数会更多。
### Q2: 为什么"审核通过人数"比"审核通过人数(去重)"多?
**A**: 因为"审核通过人数"是报名记录数,如果同一个学员报名了多个课程,会按报名次数分别计算。而"审核通过人数(去重)"是按学员手机号去重的,同一个学员只统计一次。
### Q4: "被投企业数"是如何统计的?
**A**: 先统计所有报名的学员,然后查看这些学员所在的公司,统计其中被元禾投资的公司数量。如果一家公司有多个员工报名,这家公司只统计一次。
### Q5: "开课天数"是如何计算的?
**A**: 对每场课程计算从开始日期到结束日期的天数包含开始日期和结束日期然后将所有场次的天数加起来。例如某场课程从1月1日到1月3日算3天。
### Q6: 统计时间范围是什么意思?
**A**: 统计时间范围是指报名记录的创建时间不是课程的开课时间。例如选择2024年1月1日到12月31日会统计在这个时间段内创建的所有报名记录即使这些课程可能在2025年才开课。
### Q7: 为什么选择不同的课程类型,统计数据会不同?
**A**: 因为统计数据是基于选择的课程类型进行筛选的。如果选择全部课程类型,会统计所有课程的数据;如果选择特定的课程类型,只统计该类型下的课程数据。
---
## 十、使用建议
### 10.1 查看整体情况
- 使用"报名人数"了解课程的整体报名热度
- 使用"审核通过人数(去重)"了解实际培养了多少个不同的学员
- 使用"开课场次"和"开课天数"了解课程开展的频率和时长
### 10.2 查看分类情况
- 使用"课程分类明细统计"了解不同课程类型的培养情况
- 使用"区域明细统计"了解不同区域的参与情况
### 10.3 数据对比
- 对比"报名人数"和"审核通过人数",了解审核通过率
- 对比"审核通过人数"和"审核通过人数(去重)",了解学员的参与深度
- 对比不同课程类型的数据,了解各类课程的受欢迎程度
---
## 十一、注意事项
1. **时间范围选择**:统计数据基于报名记录的创建时间,不是课程开课时间
2. **去重逻辑**:去重统计使用学员的手机号作为依据
3. **区域统计**:区域统计基于学员所在公司的区域
4. **数据关系**:不同统计数据之间可能存在包含关系,请注意理解
5. **汇总数据**:区域汇总可能包含跨区域的重复学员,请注意区分

@ -0,0 +1,37 @@
<?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('companies', function (Blueprint $table) {
// 更新日期
$table->dateTime('update_date')->nullable()->comment('更新日期');
// 管理平台
$table->json('project_users')->nullable()->comment('管理平台');
// 股东信息
$table->json('partners')->nullable()->comment('股东信息');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('companies', function (Blueprint $table) {
//
});
}
};

@ -0,0 +1,37 @@
<?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::create('articles', function (Blueprint $table) {
$table->id();
$table->string('title')->comment('标题');
$table->mediumText('content')->nullable()->comment('内容');
// 类型
$table->tinyInteger('type')->default(0)->comment('类型1校友动态2业界动态');
// 排序
$table->integer('sort')->default(0)->comment('排序');
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('articles');
}
};

@ -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('calendars', function (Blueprint $table) {
$table->decimal('days', 10, 1)->nullable()->comment('天数')->after('end_time');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('calendars', function (Blueprint $table) {
$table->dropColumn('days');
});
}
};

@ -0,0 +1,33 @@
<?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->tinyInteger('is_ganbu')->nullable()->default(0)->comment('是否干部课程-0否1是');
$table->tinyInteger('is_chart')->nullable()->default(1)->comment('是否参与统计-0否1是');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('courses', function (Blueprint $table) {
$table->dropColumn(['is_ganbu', 'is_chart']);
});
}
};

@ -0,0 +1,37 @@
<?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::create('time_events', function (Blueprint $table) {
$table->id();
$table->string('title')->comment('标题');
$table->date('date')->nullable()->comment('日期');
$table->integer('sort')->default(0)->comment('排序');
$table->mediumText('content')->nullable()->comment('内容');
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('time_events');
}
};

@ -0,0 +1,36 @@
<?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::create('statistics_configs', function (Blueprint $table) {
$table->id();
$table->string('name')->comment('名字');
$table->string('key')->nullable()->comment('标识key');
$table->tinyInteger('decimal_places')->default(0)->comment('小数点位数');
$table->text('description')->nullable()->comment('描述');
$table->json('config_json')->nullable()->comment('配置json');
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('statistics_configs');
}
};

@ -0,0 +1,616 @@
<?php
namespace Database\Seeders;
use App\Models\StatisticsConfig;
use Illuminate\Database\Seeder;
class StatisticsConfigSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
$configs = [
// 1. 校友总数 - 对应 homeV2 中的 schoolmate_total
[
'name' => '校友总数',
'key' => 'schoolmate_total',
'decimal_places' => 0,
'description' => '统计所有校友的总数',
'config_json' => [
'data_source' => [
'main_model' => 'user',
'relations' => []
],
'conditions' => [
'logic' => 'and',
'items' => [
[
'key' => 'is_schoolmate',
'operator' => 'eq',
'value' => '1'
]
]
],
'statistics' => [
'type' => 'count',
'order_by' => [
'field' => 'id',
'direction' => 'desc'
]
]
]
],
// 2. 2025年校友数 - 对应 homeV2 中的 schoolmate_year
[
'name' => '2025年校友数',
'key' => 'schoolmate_year',
'decimal_places' => 0,
'description' => '统计2025年创建的校友数量',
'config_json' => [
'data_source' => [
'main_model' => 'user',
'relations' => []
],
'conditions' => [
'logic' => 'and',
'items' => [
[
'key' => 'is_schoolmate',
'operator' => 'eq',
'value' => '1'
],
[
'key' => 'created_at',
'operator' => 'like',
'value' => date('Y')
]
]
],
'statistics' => [
'type' => 'count',
'order_by' => [
'field' => 'created_at',
'direction' => 'desc'
]
]
]
],
// 3. 已开设期数 - 对应 homeV2 中的 course_periods_total按课程类型
[
'name' => '各课程类型已开设期数',
'key' => 'course_periods_total_by_type',
'decimal_places' => 0,
'description' => '统计各课程类型已开设的期数',
'config_json' => [
'data_source' => [
'main_model' => 'course',
'relations' => []
],
'conditions' => [
'logic' => 'and',
'items' => []
],
'statistics' => [
'type' => 'count',
'group_by' => 'type',
'order_by' => [
'field' => 'total',
'direction' => 'desc'
]
]
]
],
// 4. 培养人数去重 - 对应 homeV2 中的 course_signs_total按课程类型
// 注意:去重逻辑需要在应用层处理,这里先统计总数
[
'name' => '各课程类型培养人数',
'key' => 'course_signs_total_by_type',
'decimal_places' => 0,
'description' => '统计各课程类型的审核通过报名人数2020-01-01至今',
'config_json' => [
'data_source' => [
'main_model' => 'course_sign',
'relations' => ['course']
],
'conditions' => [
'logic' => 'and',
'items' => [
[
'key' => 'status',
'operator' => 'eq',
'value' => '1'
],
[
'key' => 'created_at',
'operator' => 'between',
'value' => '2020-01-01,' . date('Y-m-d')
],
[
'key' => 'status',
'operator' => 'neq',
'value' => '4'
],
[
'key' => 'status',
'operator' => 'neq',
'value' => '5'
]
]
],
'statistics' => [
'type' => 'count',
'group_by' => 'course.type',
'order_by' => [
'field' => 'total',
'direction' => 'desc'
]
]
]
],
// 5. 苏州区域数据 - 对应 homeV2 中的 suzhou
[
'name' => '苏州各区域校友人数',
'key' => 'suzhou_schoolmate_by_area',
'decimal_places' => 0,
'description' => '统计苏州各区域的校友人数',
'config_json' => [
'data_source' => [
'main_model' => 'user',
'relations' => ['company']
],
'conditions' => [
'logic' => 'and',
'items' => [
[
'key' => 'is_schoolmate',
'operator' => 'eq',
'value' => '1'
],
[
'key' => 'company.company_city',
'operator' => 'eq',
'value' => '苏州市'
],
[
'key' => 'company.company_area',
'operator' => 'isnotnull'
]
]
],
'statistics' => [
'type' => 'count',
'group_by' => 'company.company_area',
'order_by' => [
'field' => 'total',
'direction' => 'desc'
]
]
]
],
// 6. 全国数据 - 对应 homeV2 中的 country
[
'name' => '全国各城市校友人数',
'key' => 'country_schoolmate_by_city',
'decimal_places' => 0,
'description' => '统计全国各城市的校友人数',
'config_json' => [
'data_source' => [
'main_model' => 'user',
'relations' => ['company']
],
'conditions' => [
'logic' => 'and',
'items' => [
[
'key' => 'is_schoolmate',
'operator' => 'eq',
'value' => '1'
],
[
'key' => 'company.company_city',
'operator' => 'isnotnull'
]
]
],
'statistics' => [
'type' => 'count',
'group_by' => 'company.company_city',
'order_by' => [
'field' => 'total',
'direction' => 'desc'
]
]
]
],
// 7. 本月课程 - 对应 homeV2 中的 monthCourses
[
'name' => '本月课程列表',
'key' => 'month_courses',
'decimal_places' => 0,
'description' => '获取本月开课的课程列表',
'config_json' => [
'data_source' => [
'main_model' => 'course',
'relations' => ['teacher']
],
'conditions' => [
'logic' => 'and',
'items' => [
[
'key' => 'start_date',
'operator' => 'like',
'value' => date('Y-m')
]
]
],
'statistics' => [
'type' => 'count',
'order_by' => [
'field' => 'start_date',
'direction' => 'asc'
]
]
]
],
// 8. 投后企业 - 对应 homeV2 中的 yh_invested_total
[
'name' => '投后企业总数',
'key' => 'yh_invested_total',
'decimal_places' => 0,
'description' => '统计元禾已投企业的总数',
'config_json' => [
'data_source' => [
'main_model' => 'company',
'relations' => []
],
'conditions' => [
'logic' => 'and',
'items' => [
[
'key' => 'is_yh_invested',
'operator' => 'eq',
'value' => '1'
]
]
],
'statistics' => [
'type' => 'count',
'order_by' => [
'field' => 'id',
'direction' => 'desc'
]
]
]
],
// 9. 元和员工参与企业 - 对应 homeV2 中的 yh_join_company_total
[
'name' => '元和员工参与企业总数',
'key' => 'yh_join_company_total',
'decimal_places' => 0,
'description' => '统计公司名称包含元禾相关关键词的企业总数',
'config_json' => [
'data_source' => [
'main_model' => 'company',
'relations' => []
],
'conditions' => [
'logic' => 'or',
'items' => [
[
'key' => 'company_name',
'operator' => 'like',
'value' => '元禾控股'
],
[
'key' => 'company_name',
'operator' => 'like',
'value' => '元禾原点'
],
[
'key' => 'company_name',
'operator' => 'like',
'value' => '元禾厚望'
],
[
'key' => 'company_name',
'operator' => 'like',
'value' => '元禾重元'
],
[
'key' => 'company_name',
'operator' => 'like',
'value' => '元禾璞华'
],
[
'key' => 'company_name',
'operator' => 'like',
'value' => '元禾谷风'
],
[
'key' => 'company_name',
'operator' => 'like',
'value' => '元禾绿柳'
],
[
'key' => 'company_name',
'operator' => 'like',
'value' => '元禾辰坤'
],
[
'key' => 'company_name',
'operator' => 'like',
'value' => '元禾沙湖'
],
[
'key' => 'company_name',
'operator' => 'like',
'value' => '禾裕集团'
],
[
'key' => 'company_name',
'operator' => 'like',
'value' => '苏州科服'
],
[
'key' => 'company_name',
'operator' => 'like',
'value' => '信诚管理咨询'
],
[
'key' => 'company_name',
'operator' => 'like',
'value' => '集成电路公司'
],
[
'key' => 'company_name',
'operator' => 'like',
'value' => '常州团队'
],
[
'key' => 'company_name',
'operator' => 'like',
'value' => '国企元禾'
]
]
],
'statistics' => [
'type' => 'count',
'order_by' => [
'field' => 'id',
'direction' => 'desc'
]
]
]
],
// 10. 全市干部参与企业 - 对应 homeV2 中的 yh_ganbu_total
[
'name' => '全市干部参与企业总数',
'key' => 'yh_ganbu_total',
'decimal_places' => 0,
'description' => '统计有"跟班学员"用户的企业总数',
'config_json' => [
'data_source' => [
'main_model' => 'company',
'relations' => ['users']
],
'conditions' => [
'logic' => 'and',
'items' => [
[
'key' => 'users.from',
'operator' => 'eq',
'value' => '跟班学员'
]
]
],
'statistics' => [
'type' => 'count',
'order_by' => [
'field' => 'id',
'direction' => 'desc'
]
]
]
],
// 11. 课程统计列表 - 对应 homeV2 中的 courseTypes已开设期数
[
'name' => '课程统计列表(已开设期数)',
'key' => 'course_types_list_periods',
'decimal_places' => 0,
'description' => '统计各课程类型的已开设期数(仅统计 is_chart=1 的课程类型)',
'config_json' => [
'data_source' => [
'main_model' => 'course',
'relations' => ['typeDetail']
],
'conditions' => [
'logic' => 'and',
'items' => [
[
'key' => 'typeDetail.is_chart',
'operator' => 'eq',
'value' => '1'
]
]
],
'statistics' => [
'type' => 'count',
'group_by' => 'typeDetail.id',
'order_by' => [
'field' => 'total',
'direction' => 'desc'
]
]
]
],
// 12. 课程统计列表 - 对应 homeV2 中的 courseTypes培养人数
[
'name' => '课程统计列表(培养人数)',
'key' => 'course_types_list_signs',
'decimal_places' => 0,
'description' => '统计各课程类型的培养人数2020-01-01至今审核通过仅统计 is_chart=1 的课程类型)',
'config_json' => [
'data_source' => [
'main_model' => 'course_sign',
'relations' => ['course']
],
'conditions' => [
'logic' => 'and',
'items' => [
[
'key' => 'status',
'operator' => 'eq',
'value' => '1'
],
[
'key' => 'created_at',
'operator' => 'between',
'value' => '2020-01-01,' . date('Y-m-d')
],
[
'key' => 'status',
'operator' => 'neq',
'value' => '4'
],
[
'key' => 'status',
'operator' => 'neq',
'value' => '5'
],
[
'key' => 'course.typeDetail.is_chart',
'operator' => 'eq',
'value' => '1'
]
]
],
'statistics' => [
'type' => 'count',
'group_by' => 'course.typeDetail.id',
'order_by' => [
'field' => 'total',
'direction' => 'desc'
]
]
]
],
// 13. 课程分类下的课程数量统计
[
'name' => '课程分类下的课程数量',
'key' => 'course_count_by_type',
'decimal_places' => 0,
'description' => '统计各课程分类下的所有课程数量',
'config_json' => [
'data_source' => [
'main_model' => 'course',
'relations' => ['typeDetail']
],
'conditions' => [
'logic' => 'and',
'items' => []
],
'statistics' => [
'type' => 'count',
'group_by' => 'typeDetail.id',
'order_by' => [
'field' => 'total',
'direction' => 'desc'
]
]
]
],
// 14. 课程分类列表及审核通过用户数量统计(按手机号去重)
[
'name' => '课程分类列表及审核通过用户数量统计',
'key' => 'course_type_list_with_student_count',
'decimal_places' => 0,
'description' => '获取课程分类列表,并统计每个分类下审核通过的报名用户数量(按照手机号去重)',
'config_json' => [
'data_source' => [
'main_model' => 'course_sign',
'relations' => ['course', 'user']
],
'conditions' => [
'logic' => 'and',
'items' => [
[
'key' => 'status',
'operator' => 'eq',
'value' => '1'
]
]
],
'statistics' => [
'type' => 'count_distinct',
'distinct_field' => 'user.mobile',
'group_by' => 'course.type',
'order_by' => [
'field' => 'group_value',
'direction' => 'asc'
]
]
]
],
// 15. 课程分类下按手机号去重的审核通过用户数量统计
[
'name' => '课程分类下按手机号去重的审核通过用户数量',
'key' => 'course_type_student_count_distinct',
'decimal_places' => 0,
'description' => '统计各课程分类下审核通过的报名用户数量,按照手机号去重',
'config_json' => [
'data_source' => [
'main_model' => 'course_sign',
'relations' => ['course', 'user']
],
'conditions' => [
'logic' => 'and',
'items' => [
[
'key' => 'status',
'operator' => 'eq',
'value' => '1'
]
]
],
'statistics' => [
'type' => 'count_distinct',
'distinct_field' => 'user.mobile',
'group_by' => 'course.type',
'order_by' => [
'field' => 'total',
'direction' => 'desc'
]
]
]
]
];
foreach ($configs as $config) {
StatisticsConfig::updateOrCreate(
['key' => $config['key']],
$config
);
}
$this->command->info('已生成 ' . count($configs) . ' 条统计数据配置测试数据(基于 homeV2 方法)');
}
}

@ -35,7 +35,10 @@ Route::group(["namespace" => "Admin", "prefix" => "admin"], function () {
Route::get('users/index', [\App\Http\Controllers\Admin\UserController::class, "index"]);
Route::get('other/table-fileds', [\App\Http\Controllers\Admin\OtherController::class, "tableFileds"]);
Route::get('other/home', [\App\Http\Controllers\Admin\OtherController::class, "home"]);
Route::get('other/home-v2', [\App\Http\Controllers\Admin\OtherController::class, "homeV2"]);
Route::get('other/courses-home', [\App\Http\Controllers\Admin\OtherController::class, "coursesHome"]);
Route::get('other/courses-home-export', [\App\Http\Controllers\Admin\OtherController::class, "coursesHomeExport"]);
// 验证码登陆
Route::get('auth/sms-login', [\App\Http\Controllers\Admin\AuthController::class, "smsLogin"]);
@ -228,6 +231,7 @@ Route::group(["namespace" => "Admin", "prefix" => "admin"], function () {
Route::post('email-record/excel-show', [\App\Http\Controllers\Admin\EmailRecordController::class, "excelShow"]);
// 企业管理
Route::get('company/config', [\App\Http\Controllers\Admin\CompanyController::class, "config"]);
Route::get('company/index', [\App\Http\Controllers\Admin\CompanyController::class, "index"]);
Route::get('company/show', [\App\Http\Controllers\Admin\CompanyController::class, "show"]);
Route::post('company/save', [\App\Http\Controllers\Admin\CompanyController::class, "save"]);
@ -238,6 +242,25 @@ Route::group(["namespace" => "Admin", "prefix" => "admin"], function () {
Route::get('course-content-check/show', [\App\Http\Controllers\Admin\CourseContentCheckController::class, "show"]);
Route::post('course-content-check/save', [\App\Http\Controllers\Admin\CourseContentCheckController::class, "save"]);
Route::get('course-content-check/destroy', [\App\Http\Controllers\Admin\CourseContentCheckController::class, "destroy"]);
// 文章管理
Route::get('article/index', [\App\Http\Controllers\Admin\ArticleController::class, "index"]);
Route::get('article/show', [\App\Http\Controllers\Admin\ArticleController::class, "show"]);
Route::post('article/save', [\App\Http\Controllers\Admin\ArticleController::class, "save"]);
Route::get('article/destroy', [\App\Http\Controllers\Admin\ArticleController::class, "destroy"]);
// 时间轴管理
Route::get('time-event/index', [\App\Http\Controllers\Admin\TimeEventController::class, "index"]);
Route::get('time-event/show', [\App\Http\Controllers\Admin\TimeEventController::class, "show"]);
Route::post('time-event/save', [\App\Http\Controllers\Admin\TimeEventController::class, "save"]);
Route::get('time-event/destroy', [\App\Http\Controllers\Admin\TimeEventController::class, "destroy"]);
// 统计数据配置管理
Route::get('statistics-configs/index', [\App\Http\Controllers\Admin\StatisticsConfigController::class, "index"]);
Route::get('statistics-config/show', [\App\Http\Controllers\Admin\StatisticsConfigController::class, "show"]);
Route::post('statistics-config/save', [\App\Http\Controllers\Admin\StatisticsConfigController::class, "save"]);
Route::get('statistics-config/destroy', [\App\Http\Controllers\Admin\StatisticsConfigController::class, "destroy"]);
Route::get('statistics-config/calculate', [\App\Http\Controllers\Admin\StatisticsConfigController::class, "calculate"]);
});
});
@ -287,14 +310,12 @@ Route::group(["namespace" => "Mobile", "prefix" => "mobile"], function () {
Route::post('user/update-donates', [\App\Http\Controllers\Mobile\UserController::class, "updateDonates"]);
// 课程信息
Route::get('course/evaluation-detail', [\App\Http\Controllers\Mobile\CourseController::class, "evaluationDetail"]);
Route::post('course/sign', [\App\Http\Controllers\Mobile\CourseController::class, "sign"]);
Route::get('course/my-course', [\App\Http\Controllers\Mobile\CourseController::class, "myCourse"]);
Route::get('course/my-course-content', [\App\Http\Controllers\Mobile\CourseController::class, "myCourseContent"]);
Route::post('course/course-form', [\App\Http\Controllers\Mobile\CourseController::class, "courseForm"]);
Route::get('course/get-sign', [\App\Http\Controllers\Mobile\CourseController::class, "getSign"]);
Route::post('course/update-sign', [\App\Http\Controllers\Mobile\CourseController::class, "updateSign"]);
Route::post('course/course-content-form', [\App\Http\Controllers\Mobile\CourseController::class, "courseContentForm"]);

@ -0,0 +1,550 @@
# 用户统计数据配置 JSON 结构说明
## 概述
`user_statistics_configs` 表的 `config_json` 字段用于存储动态统计配置,包含三个主要部分:数据来源、条件设置、统计方式。
## JSON 结构
```json
{
"data_source": {
"main_model": "user|company|course_sign|course|course_type",
"relations": ["user", "company", "course_sign", "course", "course_type"]
},
"conditions": {
"logic": "and|or",
"items": [
{
"key": "字段名",
"operator": "操作类型",
"value": "值"
}
]
},
"statistics": {
"type": "sum|max|min|count|count_distinct",
"field": "统计字段sum/max/min 时使用,可选)",
"distinct_field": "去重字段count_distinct 时使用,可选)",
"group_by": "分组字段(可选,不设置则不分组)",
"order_by": {
"field": "排序字段(可选)",
"direction": "asc|desc"
}
}
}
```
---
## 一、数据来源data_source
### 1.1 主模型main_model
**说明**:指定统计数据的主要来源模型。
**可选值**
- `user` - 用户模型
- `company` - 公司模型
- `course_sign` - 报名模型
- `course` - 课程模型
- `course_type` - 课程分类模型
**示例**
```json
{
"main_model": "user"
}
```
### 1.2 关联模型relations
**说明**:指定需要关联的其他模型,可以关联多个模型。
**可选值**(数组):
- `user` - 用户模型
- `company` - 公司模型
- `course_sign` - 报名模型
- `course` - 课程模型
- `course_type` - 课程分类模型
**注意**
- 关联模型不能包含主模型本身
- 可以关联多个模型
- 数组可以为空
**示例**
```json
{
"relations": ["company", "course_sign"]
}
```
---
## 二、条件设置conditions
### 2.1 逻辑关系logic
**说明**:指定多个条件之间的逻辑关系。
**可选值**
- `and` - 所有条件都必须满足AND
- `or` - 至少一个条件满足OR
**示例**
```json
{
"logic": "and"
}
```
### 2.2 条件项items
**说明**:条件数组,每个条件包含键名、操作类型和值。
**条件项结构**
```json
{
"key": "字段名",
"operator": "操作类型",
"value": "值"
}
```
#### 字段说明
- **key**(字符串):要查询的字段名
- 可以是主模型的字段
- 可以是关联模型的字段(使用点号分隔,如 `company.name`
- **operator**(字符串):操作类型
- `eq` - 等于
- `neq` - 不等于
- `gt` - 大于
- `egt` - 大于等于
- `lt` - 小于
- `elt` - 小于等于
- `like` - 模糊匹配
- `notlike` - 不匹配
- `in` - 在范围内(值为逗号分隔的字符串)
- `notin` - 不在范围内
- `between` - 在范围内(值为逗号分隔的两个值)
- `notbetween` - 不在范围内
- `isnull` - 为空value 可省略)
- `isnotnull` - 不为空value 可省略)
- **value**(字符串/数字/数组):条件值
- 根据操作类型不同,值的形式也不同
- `in` 操作:值为逗号分隔的字符串,如 `"1,2,3"`
- `between` 操作:值为逗号分隔的两个值,如 `"2024-01-01,2024-12-31"`
- `isnull``isnotnull` 操作value 可以省略
**示例**
```json
{
"logic": "and",
"items": [
{
"key": "is_schoolmate",
"operator": "eq",
"value": "1"
},
{
"key": "company.is_yh_invested",
"operator": "eq",
"value": "1"
},
{
"key": "created_at",
"operator": "between",
"value": "2024-01-01,2024-12-31"
}
]
}
```
---
## 三、统计方式statistics
### 3.1 统计类型type
**说明**:指定统计的方式。
**可选值**
- `sum` - 求和(需要指定 `field` 字段)
- `max` - 最大值(需要指定 `field` 字段)
- `min` - 最小值(需要指定 `field` 字段)
- `count` - 统计总数量(不需要指定 `field` 字段)
- `count_distinct` - 统计去重数量(需要指定 `distinct_field` 字段)
**示例**
```json
{
"type": "count"
}
```
### 3.2 统计字段field
**说明**:当统计类型为 `sum`、`max` 或 `min` 时,指定要统计的字段名。
**注意**
- `type``sum`、`max`、`min` 时必须指定 `field`
- `type``count` 时可以省略 `field`
- 可以是主模型的字段
- 可以是关联模型的字段(使用点号分隔,如 `company.company_fund`
**示例**
```json
{
"type": "sum",
"field": "company_fund"
}
```
```json
{
"type": "max",
"field": "company.company_fund"
}
```
```json
{
"type": "min",
"field": "created_at"
}
```
### 3.3 去重字段distinct_field
**说明**:当统计类型为 `count_distinct` 时,指定要去重的字段名。
**注意**
- `type``count_distinct` 时必须指定 `distinct_field`
- 可以是主模型的字段
- 可以是关联模型的字段(使用点号分隔,如 `user.mobile`
- **可以与 `group_by` 同时使用**:可以按某个字段分组,然后统计每个分组的去重数量
**示例1不分组去重统计**
```json
{
"type": "count_distinct",
"distinct_field": "mobile"
}
```
**示例2关联模型字段去重**
```json
{
"type": "count_distinct",
"distinct_field": "user.mobile"
}
```
**示例3分组 + 去重统计(组合使用)**
```json
{
"type": "count_distinct",
"distinct_field": "user.mobile",
"group_by": "course.type"
}
```
### 3.4 分组字段group_by
**说明**:指定按哪个字段进行分组统计。这是一个可选配置,可以选择不分组或选择具体的分组字段。
**配置选项**
- **不分组**:不设置 `group_by` 字段,或设置为 `null`,将返回所有符合条件的记录列表
- **按字段分组**:设置具体的分组字段,将按该字段进行分组统计
**分组字段格式**
- 可以是主模型的字段(如:`company_area`
- 可以是关联模型的字段(使用点号分隔,如 `company.company_area`
**示例1不分组统计**
```json
{
"statistics": {
"type": "count"
// 不设置 group_by表示不分组
}
}
```
**示例2按主模型字段分组**
```json
{
"statistics": {
"type": "count",
"group_by": "company_area"
}
}
```
**示例3按关联模型字段分组**
```json
{
"statistics": {
"type": "count",
"group_by": "company.company_area"
}
}
```
**示例4分组 + 去重统计(组合使用)**
```json
{
"statistics": {
"type": "count_distinct",
"distinct_field": "user.mobile",
"group_by": "course.type"
}
}
```
### 3.4 排序方式order_by
**说明**:指定结果的排序方式。
**结构**
```json
{
"field": "排序字段",
"direction": "asc|desc"
}
```
**字段说明**
- **field**(字符串):排序字段名
- 可以是主模型的字段
- 可以是关联模型的字段(使用点号分隔)
- 可以是统计结果字段(如 `total`、`count`
- **direction**(字符串):排序方向
- `asc` - 升序
- `desc` - 降序
**示例**
```json
{
"order_by": {
"field": "total",
"direction": "desc"
}
}
```
---
## 完整示例
### 示例1统计各区域的校友人数
```json
{
"data_source": {
"main_model": "user",
"relations": ["company"]
},
"conditions": {
"logic": "and",
"items": [
{
"key": "is_schoolmate",
"operator": "eq",
"value": "1"
},
{
"key": "created_at",
"operator": "between",
"value": "2024-01-01,2024-12-31"
}
]
},
"statistics": {
"type": "count",
"group_by": "company.company_area",
"order_by": {
"field": "count",
"direction": "desc"
}
}
}
```
### 示例2统计各课程类型的报名人数
```json
{
"data_source": {
"main_model": "course_sign",
"relations": ["course", "user"]
},
"conditions": {
"logic": "and",
"items": [
{
"key": "status",
"operator": "eq",
"value": "1"
},
{
"key": "created_at",
"operator": "between",
"value": "2024-01-01,2024-12-31"
}
]
},
"statistics": {
"type": "count",
"group_by": "course.type",
"order_by": {
"field": "count",
"direction": "desc"
}
}
}
```
### 示例3统计各公司的融资总额
```json
{
"data_source": {
"main_model": "company",
"relations": ["user"]
},
"conditions": {
"logic": "and",
"items": [
{
"key": "is_yh_invested",
"operator": "eq",
"value": "1"
},
{
"key": "company_fund",
"operator": "isnotnull"
}
]
},
"statistics": {
"type": "sum",
"field": "company_fund",
"group_by": "company_area",
"order_by": {
"field": "total",
"direction": "desc"
}
}
}
```
### 示例4统计审核通过或待审核的报名人数
```json
{
"data_source": {
"main_model": "course_sign",
"relations": []
},
"conditions": {
"logic": "or",
"items": [
{
"key": "status",
"operator": "eq",
"value": "0"
},
{
"key": "status",
"operator": "eq",
"value": "1"
}
]
},
"statistics": {
"type": "count",
"order_by": {
"field": "created_at",
"direction": "desc"
}
}
}
```
### 示例5统计各课程类型的去重培养人数按手机号去重
```json
{
"data_source": {
"main_model": "course_sign",
"relations": ["user", "course"]
},
"conditions": {
"logic": "and",
"items": [
{
"key": "status",
"operator": "eq",
"value": "1"
},
{
"key": "created_at",
"operator": "between",
"value": "2020-01-01," . date('Y-m-d')
}
]
},
"statistics": {
"type": "count_distinct",
"distinct_field": "user.mobile",
"group_by": "course.type",
"order_by": {
"field": "total",
"direction": "desc"
}
}
}
```
---
## 注意事项
1. **字段引用**
- 主模型字段直接使用字段名
- 关联模型字段使用 `模型名.字段名` 格式
- 例如:`company.name`、`course.type`
2. **数据类型**
- 所有值在 JSON 中都存储为字符串
- 系统会根据字段类型自动转换
3. **条件逻辑**
- `and` 表示所有条件都必须满足
- `or` 表示至少一个条件满足
- 条件数组可以为空(表示无条件)
4. **统计字段**
- `sum` 类型必须指定 `field`
- `count` 类型不需要 `field`
- 分组字段可以为空(表示不分组)
5. **排序字段**
- 可以按任意字段排序
- 可以按统计结果字段排序(如 `total`、`count`
- 排序字段可以为空(使用默认排序)
---
## 文档版本
- **创建日期**2025-11-19
- **最后更新**2025-11-19
Loading…
Cancel
Save