diff --git a/app/Http/Controllers/Admin/ArticleController.php b/app/Http/Controllers/Admin/ArticleController.php new file mode 100644 index 0000000..b950b4b --- /dev/null +++ b/app/Http/Controllers/Admin/ArticleController.php @@ -0,0 +1,218 @@ +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(); + } + +} diff --git a/app/Http/Controllers/Admin/CalendarsController.php b/app/Http/Controllers/Admin/CalendarsController.php index ba5cea1..1f54e14 100644 --- a/app/Http/Controllers/Admin/CalendarsController.php +++ b/app/Http/Controllers/Admin/CalendarsController.php @@ -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", diff --git a/coursesHome统计逻辑说明.md b/coursesHome统计逻辑说明.md new file mode 100644 index 0000000..03a264a --- /dev/null +++ b/coursesHome统计逻辑说明.md @@ -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. **汇总数据**:区域汇总可能包含跨区域的重复学员,请注意区分 + diff --git a/database/migrations/2025_11_18_160801_add_days_to_calendars_table.php b/database/migrations/2025_11_18_160801_add_days_to_calendars_table.php new file mode 100644 index 0000000..0de0832 --- /dev/null +++ b/database/migrations/2025_11_18_160801_add_days_to_calendars_table.php @@ -0,0 +1,32 @@ +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'); + }); + } +}; diff --git a/routes/api.php b/routes/api.php index fe03fd5..f4e213b 100755 --- a/routes/api.php +++ b/routes/api.php @@ -240,6 +240,12 @@ 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"]); }); }); @@ -295,7 +301,7 @@ Route::group(["namespace" => "Mobile", "prefix" => "mobile"], function () { 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::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"]);