From 3bbe727343a3dff8c74a045e286b9185b0aab024 Mon Sep 17 00:00:00 2001
From: cody <648753004@qq.com>
Date: Tue, 3 Mar 2026 09:30:56 +0800
Subject: [PATCH] update
---
.../Controllers/Mobile/OtherController.php | 16 +-
.../Controllers/Mobile/UserController.php | 16 +-
app/Http/functions.php | 31 +-
app/Repositories/DoorRepository.php | 22 +-
doc/业界动态-爬虫与AI方案探讨.md | 216 ++++++++++++++
doc/积分系统与商城结合促进校友活跃度方案.md | 132 +++++++++
doc/线上课程平台功能设计与承载量评估.md | 266 ++++++++++++++++++
doc/邮件打开率与回执统计实现方案.md | 131 +++++++++
科技商学院学员信息-匹配人才标签_匹配是否.xlsx | Bin 31130 -> 0 bytes
9 files changed, 798 insertions(+), 32 deletions(-)
create mode 100644 doc/业界动态-爬虫与AI方案探讨.md
create mode 100644 doc/积分系统与商城结合促进校友活跃度方案.md
create mode 100644 doc/线上课程平台功能设计与承载量评估.md
create mode 100644 doc/邮件打开率与回执统计实现方案.md
delete mode 100644 科技商学院学员信息-匹配人才标签_匹配是否.xlsx
diff --git a/app/Http/Controllers/Mobile/OtherController.php b/app/Http/Controllers/Mobile/OtherController.php
index 03d6413..825bab7 100755
--- a/app/Http/Controllers/Mobile/OtherController.php
+++ b/app/Http/Controllers/Mobile/OtherController.php
@@ -195,14 +195,14 @@ class OtherController extends CommonController
->whereHas('users', function ($query) {
$query->where('is_schoolmate', 1);
})->with([
- 'users' => function ($query) {
- $query->select('id', 'name', 'username', 'company_position', 'company_id')->where('is_schoolmate', 1)->with([
- 'courseSigns' => function ($query) {
- $query->select('id', 'status', 'user_id', 'course_id')->with('course')->orderBy('fee_status', 'desc');
- }
- ]);
- }
- ]);
+ 'users' => function ($query) {
+ $query->select('id', 'name', 'username', 'company_position', 'company_id')->where('is_schoolmate', 1)->with([
+ 'courseSigns' => function ($query) {
+ $query->select('id', 'status', 'user_id', 'course_id')->with('course')->orderBy('fee_status', 'desc');
+ }
+ ]);
+ }
+ ]);
// 根据排序字段进行排序
if ($sortName !== 'distance') {
$query->orderBy($sortName, $sortType);
diff --git a/app/Http/Controllers/Mobile/UserController.php b/app/Http/Controllers/Mobile/UserController.php
index 31a19b0..75844bf 100755
--- a/app/Http/Controllers/Mobile/UserController.php
+++ b/app/Http/Controllers/Mobile/UserController.php
@@ -240,10 +240,10 @@ class UserController extends CommonController
{
$user = User::with('appointments')
->withCount([
- 'appointments as pass_appointments' => function ($query) {
- $query->whereIn('status', [0, 1]);
- }
- ])->with([
+ 'appointments as pass_appointments' => function ($query) {
+ $query->whereIn('status', [0, 1]);
+ }
+ ])->with([
'courseSigns' => function ($query) {
$query->whereHas('course')->with('course.typeDetail')->where('status', 1)->where('fee_status', 1);
}
@@ -255,7 +255,9 @@ class UserController extends CommonController
->orderBy('id', 'desc')
->first();
if ($door_appointments) {
- $door_appointments->qrcode = $doorRepository->getEmpQrCode($door_appointments, $out);
+ $out = null;
+ $qrcode = $doorRepository->getEmpQrCode($door_appointments, $out, 2);
+ $door_appointments->qrcode = ($qrcode !== false && $qrcode !== null) ? $qrcode : null;
}
// 进行中的课程
$course_signs = CourseSign::where('user_id', $this->getUserId())
@@ -265,7 +267,9 @@ class UserController extends CommonController
$query->where('start_date', '<=', $nowDate)->where('end_date', '>=', $nowDate);
})->first();
if ($course_signs) {
- $course_signs->qrcode = $doorRepository->getEmpQrCodeByCourse($course_signs, $out);
+ $out = null;
+ $qrcode = $doorRepository->getEmpQrCodeByCourse($course_signs, $out, 2);
+ $course_signs->qrcode = ($qrcode !== false && $qrcode !== null) ? $qrcode : null;
}
// 是否有资格进入校友库
$enter_schoolmate = User::whereHas('courseSigns', function ($query) {
diff --git a/app/Http/functions.php b/app/Http/functions.php
index bacbb45..5f8d727 100755
--- a/app/Http/functions.php
+++ b/app/Http/functions.php
@@ -135,7 +135,7 @@ function find_children($array, $key, $parent_key, $val, $return = [])
*/
function id_number2age($IDnumber)
{
- $year = (int)substr($IDnumber, 6, 4);
+ $year = (int) substr($IDnumber, 6, 4);
return date("Y") - $year;
}
@@ -175,7 +175,7 @@ function number2chinese($num, $lower = false)
}
$i = $i + 1;
$num = $num / 10;
- $num = (int)$num;
+ $num = (int) $num;
if ($num == 0) {
break;
}
@@ -216,7 +216,7 @@ function string2secret($string = NULL)
return NULL;
}
$length = mb_strlen($string);
- $visibleCount = (int)round($length / 4);
+ $visibleCount = (int) round($length / 4);
$hiddenCount = $length - ($visibleCount * 2);
return mb_substr($string, 0, $visibleCount) . str_repeat('*', $hiddenCount) . mb_substr($string, ($visibleCount * -1), $visibleCount);
}
@@ -316,10 +316,10 @@ function multiline2radio($text, $value = "", $block = false)
$block = $block === true ? "d-block" : "";
for ($i = 0; $i < count($text); $i++) {
$checked = "";
- if ($value === (string)$text[$i]) {
+ if ($value === (string) $text[$i]) {
$checked = "checked";
}
- if ((string)$text[$i] == "splitter") {
+ if ((string) $text[$i] == "splitter") {
$res .= "
";
} else {
$res .= '';
@@ -511,9 +511,12 @@ function getDomain()
function getFieldWithName($detail)
{
$array = [];
- if (isset($detail['local_key'])) $array[] = $detail['local_key'];
- if (isset($detail['link_table_name'])) $array[] = $detail['link_table_name'];
- if (isset($detail['foreign_key'])) $array[] = $detail['foreign_key'];
+ if (isset($detail['local_key']))
+ $array[] = $detail['local_key'];
+ if (isset($detail['link_table_name']))
+ $array[] = $detail['link_table_name'];
+ if (isset($detail['foreign_key']))
+ $array[] = $detail['foreign_key'];
$array[] = 'relation';
return implode('_', $array);
}
@@ -528,15 +531,15 @@ function getBatchNo()
return date('YmdHis') . $millisecond;
}
-function httpCurl($url, $method = 'GET', $params = [], $header = [])
+function httpCurl($url, $method = 'GET', $params = [], $header = [], $timeout = 3)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); //将获取的信息以字符串返回,而不是直接输出
curl_setopt($ch, CURLOPT_URL, $method == "POST" ? $url : $url . '?' . http_build_query($params)); //http_build_query数组转Url格式参数
//设置超时时间
- curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 60);
- curl_setopt($ch, CURLOPT_TIMEOUT, 60);
+ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
+ curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
//如果是https协议,取消检测SSL证书
if (stripos($url, "https://") !== FALSE) {
@@ -620,7 +623,7 @@ function idCardToSex($idcard)
if (empty($idcard) || !isIdCard($idcard)) {
return '男';
}
- $sexint = (int)substr($idcard, 16, 1);
+ $sexint = (int) substr($idcard, 16, 1);
return $sexint % 2 === 0 ? '女' : '男';
}
@@ -643,7 +646,7 @@ function isIdCard($idCard)
$sigma = 0;
# 提取前17位的其中一位,并将变量类型转为实数
for ($i = 0; $i < 17; $i++) {
- $b = (int)$idcard[$i];
+ $b = (int) $idcard[$i];
# 提取相应的加权因子
$w = $wi[$i];
# 把从身份证号码中提取的一位数字和加权因子相乘,并累加
@@ -678,7 +681,7 @@ function isMultiDimensionalArray($array)
**/
function getDistance($lat1, $lng1, $lat2, $lng2)
{
-//将角度转为狐度
+ //将角度转为狐度
$radLat1 = deg2rad($lat1);//deg2rad()函数将角度转换为弧度
$radLat2 = deg2rad($lat2);
$radLng1 = deg2rad($lng1);
diff --git a/app/Repositories/DoorRepository.php b/app/Repositories/DoorRepository.php
index 3709bd5..1572703 100755
--- a/app/Repositories/DoorRepository.php
+++ b/app/Repositories/DoorRepository.php
@@ -207,8 +207,9 @@ class DoorRepository
/**
* 获取员工开门二维码
+ * @param int|null $timeout 超时秒数,不传则使用 httpCurl 默认
*/
- public function getEmpQrCode($model, &$out)
+ public function getEmpQrCode($model, &$out, $timeout = null)
{
$url = $this->baseUrl . '/api/AccessInterface/GetEmpQrCode';
$header[] = 'Content-Type: application/json;charset=UTF-8';
@@ -221,8 +222,14 @@ class DoorRepository
$finally = 0;
try {
$params = json_encode($params);
- $result = httpCurl($url, 'POST', $params, $header);
+ $result = $timeout !== null
+ ? httpCurl($url, 'POST', $params, $header, $timeout)
+ : httpCurl($url, 'POST', $params, $header);
$result = json_decode($result, true);
+ if (!is_array($result) || !array_key_exists('code', $result)) {
+ $out = '请求超时或返回异常';
+ return false;
+ }
if (empty($result['code'])) {
$model->qrcode = $result['data'];
$model->save();
@@ -340,8 +347,11 @@ class DoorRepository
/**
* 获取员工开门二维码(报名渠道)
+ * @param mixed $model
+ * @param mixed $out
+ * @param int|null $timeout 超时秒数,不传则使用默认 60 秒
*/
- public function getEmpQrCodeByCourse($model, &$out)
+ public function getEmpQrCodeByCourse($model, &$out, $timeout = null)
{
$user = User::find($model->user_id);
$url = $this->baseUrl . '/api/AccessInterface/GetEmpQrCode';
@@ -355,8 +365,12 @@ class DoorRepository
$finally = 0;
try {
$params = json_encode($params);
- $result = httpCurl($url, 'POST', $params, $header);
+ $result = httpCurl($url, 'POST', $params, $header, $timeout ?? 60);
$result = json_decode($result, true);
+ if (!is_array($result) || !array_key_exists('code', $result)) {
+ $out = '请求超时或返回异常';
+ return false;
+ }
if (empty($result['code'])) {
$model->qrcode = $result['data'];
$model->save();
diff --git a/doc/业界动态-爬虫与AI方案探讨.md b/doc/业界动态-爬虫与AI方案探讨.md
new file mode 100644
index 0000000..6757166
--- /dev/null
+++ b/doc/业界动态-爬虫与AI方案探讨.md
@@ -0,0 +1,216 @@
+# 业界动态解决方案探讨:爬虫与 AI 技术方案
+
+## 一、需求与目标
+
+**核心需求**:定时爬取、分析指定公众号与网站的文章,将内容纳入系统供内部参考,形成可检索、可分析的「业界动态」信息库。
+
+**目标**:
+- 自动采集:按计划抓取指定公众号、网站的最新文章
+- 智能分析:用 AI 对文章做摘要、分类、关键词提取、趋势判断
+- 系统沉淀:结构化入库,支持检索、订阅、报表与决策参考
+
+---
+
+## 二、方案可行性结论
+
+**结论**:**可以**通过爬虫 + AI 实现定时爬取与分析,但公众号与普通网站在实现难度和合规性上差异较大,需分场景设计。
+
+| 数据源类型 | 技术可行性 | 主要难点与约束 |
+|----------------|------------|---------------------------|
+| 普通网站/博客 | 高 | 反爬、频率控制、版权 |
+| 微信公众号文章 | 中~高 | 无公开 API,可通过后台「查找文章」接口(需自有订阅号) |
+
+---
+
+## 三、技术方案概览
+
+### 3.1 整体流程
+
+```
+[定时任务] → [爬虫采集] → [清洗与去重] → [AI 分析] → [入库与索引] → [系统展示/检索]
+ ↑ ↑
+ 公众号 / 网站 摘要、分类、标签、趋势
+```
+
+### 3.2 爬虫技术方案
+
+#### 3.2.0 借助 AI 智能获取「只想要的内容」
+
+**可以**。在爬取网站时,用 AI 参与「抓什么、取哪块」,能减少为每个站点写死规则,并只保留真正需要的内容。
+
+| 思路 | 做法 | 适用场景 |
+|------|------|----------|
+| **AI 辅助正文/字段提取** | 爬虫先拿到整页 HTML(或简化后的 DOM/文本),把「页面片段 + 需求描述」交给大模型,用自然语言说明要提取的字段(如:标题、正文、发布时间、作者)。由 AI 直接返回结构化结果,或指出对应区块的语义描述,再映射到 DOM。 | 站点多、页面结构各异,不想为每个站维护 XPath/CSS |
+| **AI 判断「是否想要」** | 先抓列表页或整页文本,对每个链接/段落用 AI 做一次相关性判断(如:是否属于「业界动态」、是否与指定主题相关)。只对判定为「想要」的链接再请求正文,或只入库通过筛选的段落。 | 目标站内容杂,需要过滤噪音、只留相关文章 |
+| **AI 生成/推荐选择器** | 给 AI 一张样例页的 HTML 或 DOM 树摘要,让 AI 输出「标题 / 正文 / 时间」的 XPath 或 CSS 选择器,人工审核后写入配置,爬虫仍按传统方式用选择器抓取。 | 希望减少手写规则,但保留规则可审、可改、可回滚 |
+
+**实现要点**:
+- **输入控制**:整页 HTML 可能超长,可先做**正文预提取**(如用 readability、trafilatura 等库)再送 AI,或只送主要区块的 HTML 片段,以节省 token、提高稳定性。
+- **输出约定**:与 AI 约定好输出格式(如 JSON:`title`、`body`、`publish_time`),便于直接落库;必要时加一层校验或回退逻辑(如解析失败时 fallback 到规则或整页存储)。
+- **成本与延迟**:每条页面调用一次 AI 会带来时延与费用,可对「新站点/新模板」用 AI 抽取,对已稳定站点用缓存的选择器或规则,或仅对列表页用 AI 做「要不要抓」的过滤。
+
+这样即可在**爬取阶段**就借助 AI,只获取想要的内容,减少无关信息和后续清洗成本。
+
+---
+
+#### (1)普通网站 / 博客
+
+- **方式一:自建爬虫**
+ - 使用 **Scrapy**、**Playwright** 或 **Puppeteer** 抓取目标站点
+ - 针对不同站点写解析规则(XPath/CSS/正则),提取标题、正文、时间、作者等
+ - 建议:遵守 `robots.txt`,控制请求频率(如 1~5 秒/页),设置合理 User-Agent
+
+#### (1.1)Vue / React 等 SPA:能爬取到动态数据吗?
+
+**可以**。Vue、React、Angular 等前端框架做的是**单页应用(SPA)**:首屏 HTML 往往只有一个壳,正文、列表等是页面加载后由 JavaScript 请求接口、再渲染到 DOM 的。用只发 HTTP 请求、不执行 JS 的爬虫(如仅用 requests + 解析 HTML)拿到的多是空壳,**拿不到动态渲染出来的内容**。要拿到动态数据,有两种常见做法:
+
+| 方式 | 做法 | 优点 | 注意点 |
+|------|------|------|--------|
+| **无头浏览器** | 用 **Playwright**、**Puppeteer** 或 **Selenium** 打开页面,等待 JS 执行、接口返回、DOM 渲染完成,再对当前 DOM 做选择器提取(或把最终 HTML 交给 AI 抽取)。 | 与用户看到的完全一致,不关心前端用的什么框架。 | 耗资源、较慢,需设置合理的等待条件(如等某元素出现、或固定延迟),并控制并发。 |
+| **直接请求接口** | 在浏览器开发者工具里看该 Vue 站点的「Network」:文章列表、正文往往来自某个 REST 或 GraphQL 接口。用爬虫**直接请求这些接口**,拿到 JSON,再解析入库。 | 速度快、省资源、数据已结构化。 | 需人工分析接口格式与鉴权(如 token、cookie);接口变更或鉴权加强时需维护。 |
+
+**建议**:目标站若是 Vue/React 等 SPA,优先看能否抓到其**数据接口**并直接请求;若接口难找、鉴权复杂或经常变,再用 **Playwright/Puppeteer** 做无头渲染后从 DOM 里取动态内容。二者都可与前述「AI 智能提取只想要的内容」结合使用。
+
+#### (2)微信公众号
+
+微信公众号**没有官方开放的文章列表/正文 API**。采用**公众号后台「查找文章」接口**获取文章数据:
+
+- **思路**:拥有一个微信**个人订阅号**,登录 [微信公众平台](https://mp.weixin.qq.com/) 后,利用后台「新建图文素材」里的**超链接 → 查找文章**功能:输入目标公众号名称即可拉取该号的**历史文章列表**。该能力对应后台的搜索与文章列表接口,可用于程序化获取最新及历史数据。
+- **操作路径**:登录公众号 → 管理 → 素材管理 → 新建图文素材 → 工具栏「超链接」→「查找文章」→ 输入要查找的公众号。
+- **技术实现要点**(供自建爬虫参考):
+ 1. **登录与 Cookie**:用 Selenium/Playwright 等打开 `https://mp.weixin.qq.com/`,完成扫码或账号密码登录,将登录后的 **cookies** 持久化(如保存到本地),后续请求携带。
+ 2. **获取 token**:访问首页后,从跳转 URL 中解析出 `token=(\d+)`,后续接口均需携带该 token。
+ 3. **搜索公众号得到 fakeid**:请求 `https://mp.weixin.qq.com/cgi-bin/searchbiz?`,参数含 `action=search_biz`、`query=目标公众号名`、`token` 等,从返回 JSON 的 `list[0].fakeid` 得到该公众号的 **fakeid**。
+ 4. **分页拉取文章列表**:请求 `https://mp.weixin.qq.com/cgi-bin/appmsg?`,参数含 `action=list_ex`、`fakeid`、`begin`(分页偏移,每页 5 条递增)、`count=5`、`token` 等,可循环翻页直至取完;返回中有标题、链接、摘要等,可直接入库或再抓正文。
+- **优点**:可拉取**更多甚至全部历史文章**,数据来自微信官方后台,结构稳定、易解析。
+- **注意点**:依赖自有订阅号登录态,需保管好 Cookie/账号安全;请求频率需控制(如分页间隔 2~5 秒),避免触发后台风控;仅限内部使用,遵守平台服务条款。
+- **参考**:[python 之抓取微信公众号文章系列 2 - 腾讯云开发者社区](https://cloud.tencent.com/developer/article/1406410)(含个人公众号接口抓取思路与示例)。
+
+### 3.3 AI 分析能力
+
+在文章入库前/后接入 AI,实现「从原文到可参考信息」的转化:
+
+| 能力 | 说明 | 典型实现 |
+|--------------|--------------------------|------------------------------|
+| 摘要生成 | 一段话概括文章要点 | 大模型 API(如 GPT/国产大模型)|
+| 分类/打标签 | 行业、主题、业务线 | 提示词 + 分类接口或 embedding |
+| 关键词提取 | 便于检索与聚合 | NLP 关键词 / 大模型 |
+| 情感/倾向 | 正面/中性/负面等 | 情感分析模型或大模型 |
+| 与内部关联 | 与本单位业务、项目的关联 | 自定义 prompt + 知识库 |
+| 趋势与周报 | 按主题聚合、生成周报 | 定时任务 + 大模型汇总 |
+
+**技术选型建议**:
+- 摘要/分类/标签:调用 **OpenAI API**、**通义千问**、**文心一言**、**智谱** 等
+- 若需私有化:可部署 **Llama**、**ChatGLM**、**Qwen** 等,或使用支持私有化的大模型服务
+
+### 3.4 系统架构示意
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ 调度层(定时任务) │
+│ Cron / Celery Beat / 云函数 等,按日/周触发采集与分析 │
+└─────────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────────┐
+│ 采集层 │
+│ 网站爬虫(Scrapy/Playwright) │ 公众号(后台「查找文章」接口) │
+└─────────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────────┐
+│ 清洗与去重 │
+│ 正文提取、时间归一化、URL/标题去重、简单反作弊检测 │
+└─────────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────────┐
+│ AI 分析层 │
+│ 摘要 | 分类 | 关键词 | 情感 | 与业务关联度 │
+└─────────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────────┐
+│ 存储与检索 │
+│ 数据库(MySQL/PostgreSQL) + 全文检索(Elasticsearch/Meilisearch) │
+└─────────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────────┐
+│ 应用层 │
+│ 业界动态列表、检索、订阅、看板、周报/月报导出 │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 四、实现要点与注意事项
+
+### 4.1 合规与版权
+
+- **版权**:爬取内容仅供**内部参考**,不对外全文再发表;建议在系统内标注来源、链接,并控制传播范围
+- ** robots.txt**:遵守目标站点的 robots 规则
+- **服务条款**:注意目标站与公众号平台的服务条款,避免违反约定用途
+- **个人信息**:若抓取到评论、作者信息等,需符合《个人信息保护法》等要求(脱敏或取得同意)
+
+### 4.2 反爬与稳定性
+
+- 控制访问频率,避免对目标站造成压力
+- 必要时使用代理池、更换 User-Agent,但需权衡合规与伦理
+- 页面结构变更时,解析规则需可配置、可快速调整(如规则配置表 + 版本管理)
+
+### 4.3 公众号特殊说明
+
+- 通过**公众号后台「查找文章」**所对应的接口获取目标号的文章列表(见 3.2 节),再抓正文与做 AI 分析;需保管好订阅号登录态并控制请求频率,仅限内部使用、遵守平台服务条款。
+
+### 4.4 成本粗算
+
+- **爬虫**:开发与维护人力;若用云主机/代理,有少量机器与带宽成本
+- **AI**:按调用量计费(摘要/分类等),可先对部分文章采样,再全量开启
+- **第三方数据**:若目标站有数据 API(如新榜等),可按条或按订阅量采购;公众号采用后台「查找文章」接口则无此项费用
+
+---
+
+## 五、推荐实施路径
+
+### 阶段一:MVP(最小可行)
+
+1. **先做网站 + 链接录入**
+ - 支持「添加目标网站 + 简单爬虫规则」或「手动/半自动录入文章 URL」
+ - 爬虫只抓正文,落库(标题、来源、URL、正文、抓取时间)
+2. **接入一种 AI**
+ - 仅做「摘要 + 3~5 个标签」,便于快速浏览与检索
+3. **简单列表与检索**
+ - 按时间、来源、标签筛选,关键词全文检索
+
+### 阶段二:增强
+
+1. 增加**分类、关键词、情感**等 AI 能力
+2. 引入**公众号**:采用后台「查找文章」接口(需自有个人订阅号)获取目标号文章列表,再抓正文与做 AI 分析
+3. 增加**订阅与通知**(如按标签/来源推送日报、周报)
+
+### 阶段三:深度用起来
+
+1. **趋势与报表**:按主题/行业聚合,自动生成周报/月报
+2. **与业务关联**:用 AI 判断文章与本单位业务、项目的相关度,并打标
+3. 对爬虫规则、AI 提示词做**配置化与版本管理**,便于扩展更多公众号与站点
+
+---
+
+## 六、小结
+
+| 问题 | 结论 |
+|------------------------------|----------------------------------------------------------------------|
+| 能否用爬虫+AI 做业界动态? | **可以**,技术上可行,需区分「网站」与「公众号」分别设计。 |
+| 爬取时能否借 AI 只取想要内容?| **可以**:AI 辅助字段提取、AI 过滤相关性、或 AI 生成选择器再人工审核。|
+| Vue/SPA 动态站能爬吗? | **可以**:用无头浏览器(Playwright/Puppeteer)等渲染后再取 DOM,或直接请求前端调用的数据接口。|
+| 公众号如何获取? | 用**公众号后台「查找文章」接口**:自有个人订阅号登录后,通过后台接口获取目标号文章列表,再抓正文与做 AI 分析。|
+| 直接请求接口 vs 无头浏览器? | 直接请求接口易触发风控;无头/有头浏览器更接近真人,相对更顺利,但仍需控频与反检测,无头并非万能。|
+| 如何发挥 AI 价值? | 摘要、分类、标签、关键词、趋势汇总、与业务关联度分析等。 |
+| 主要风险 | 版权与合规、公众号来源不稳定、反爬与维护成本。 |
+
+将上述方案落地为「**定时爬取 + 清洗去重 + AI 分析 + 入库检索**」的闭环,即可在系统内形成可持续更新的业界动态参考库;实施时建议从网站与少量公众号试点,再逐步扩大数据源与 AI 能力。
+
+---
+
+*文档版本:v1.0 | 建议放置于 doc 目录,供项目评审与开发参考*
diff --git a/doc/积分系统与商城结合促进校友活跃度方案.md b/doc/积分系统与商城结合促进校友活跃度方案.md
new file mode 100644
index 0000000..625f6a0
--- /dev/null
+++ b/doc/积分系统与商城结合促进校友活跃度方案.md
@@ -0,0 +1,132 @@
+# 积分系统与商城功能结合促进校友活跃度方案
+
+## 一、现状与目标
+
+### 1.1 系统现有能力
+
+| 模块 | 现状 | 说明 |
+|------|------|------|
+| **积分** | 已具备基础能力 | `users.score` 字段、`score_logs` 表,`ScoreLog::add()` 方法;当前仅「分享获得」一种获取方式,依赖配置项 `share_score` |
+| **校友** | 已完善 | `is_schoolmate`、校友库、校友企业、课程审核后自动入校友库、校友捐赠(donates)、校友动态(articles) |
+| **移动端** | 课程/预约/供需/图书 | 课程报名与签到、课表签到(content_check)、预约、供需信息、图书、用户信息、捐赠 |
+| **商城** | 未建设 | 无商品、兑换、订单模块 |
+
+### 1.2 目标
+
+- **积分与行为挂钩**:将课程、签到、供需、捐赠等校友相关行为纳入积分体系,明确「做什么得多少分」。
+- **积分可消费**:建设积分商城(礼品/权益),支持积分兑换,让积分有实际价值。
+- **校友身份加成**:部分积分规则或商品仅对校友开放,激励完成认证、持续参与。
+- **提升活跃**:通过「赚积分→换好礼」闭环,提高打开率、参与课程与活动、使用供需等功能的频率。
+
+---
+
+## 二、积分获取规则设计(与现有功能结合)
+
+在保留现有「分享获得」基础上,新增以下规则,均可在后台通过 Config 或独立配置表配置分值,便于运营调整。
+
+| 行为 | 触发节点 | 建议分值(示例) | 说明 |
+|------|----------|----------------|------|
+| 分享获新用户 | 小程序 `applet-login` 带 pid,新用户首次登录 | 沿用 `share_score` | 已实现 |
+| 完善资料 | 用户首次补全姓名/公司/职位等关键信息 | 50~200 | 需在 `updateUser` 或资料页增加「是否首次完善」判断 |
+| 课程报名成功 | 报名审核通过(CourseSign status 通过) | 20~50/次 | 在审核通过逻辑中调用 `ScoreLog::add` |
+| 单次课签到 | 课表签到成功(CourseContentCheck) | 5~15/次 | 签到成功回调中发积分 |
+| 课程评价 | 提交课程/单次课评价 | 10~30/次 | 评价提交接口中发积分 |
+| 供需发布 | 供需信息审核通过 | 30~80/条 | 审核通过时给发布者积分 |
+| 供需留言/对接 | 在供需下有效留言或标记对接成功 | 5~20/次 | 需定义「有效」规则 |
+| 校友捐赠 | 捐赠信息审核通过(donates) | 100~500/次 | 审核通过时发积分(可与捐赠金额档位挂钩) |
+| 预约到场 | 预约完成且到场核销 | 10~30/次 | 需有到场核销节点 |
+| 每日登录 | 当日首次打开小程序并完成接口请求 | 1~5/天 | 需防刷(同 user 同日仅一次) |
+| 校友专属任务 | 如「邀请一名校友注册并认证」 | 100~300 | 需定义「校友」与「认证」口径 |
+
+**实现要点:**
+
+- 所有新增发分点统一调用 `ScoreLog::add($user_id, $score, $remark)`,`remark` 建议用统一文案如「课程报名」「课表签到」「供需发布」等,便于对账与统计。
+- 建议新增 Config 键:`score_register`、`score_course_sign`、`score_content_check`、`score_evaluation`、`score_supply_demand`、`score_donate`、`score_daily_login` 等,后台可调。
+- 部分规则需加「仅校友可得」:在发分前判断 `User::find($user_id)->is_schoolmate == 1`,不满足则不发或发减半(由产品决定)。
+
+---
+
+## 三、积分商城功能设计
+
+### 3.1 模块范围
+
+- **商品/礼品管理**(后台):商品名称、图片、所需积分、库存、是否仅校友可兑换、上下架、排序。
+- **兑换记录**(前后台):谁、何时、兑换何商品、扣减积分、订单状态(待发货/已发货/已完成)。
+- **积分明细与余额**(移动端):当前积分、积分明细列表(现有 `score_logs` 可复用,需区分 type:1 获得 2 消耗)。
+
+### 3.2 数据表建议(与现有库表风格一致)
+
+```text
+# 积分商品表 score_goods
+id, name, image_id, score_price, stock, is_schoolmate_only(0/1), sort, status(0下架1上架), created_at, updated_at, deleted_at
+
+# 积分兑换订单表 score_orders
+id, user_id, score_good_id, score_amount, status(0待发货1已发货2已完成3已取消), address_snapshot, created_at, updated_at, deleted_at
+```
+
+- 兑换时:扣减 `users.score`,写入 `score_logs`(score 为负、remark 如「兑换-商品名」),扣减 `score_goods.stock`,生成 `score_orders`。
+- 若需物流:在 `score_orders` 或扩展表记录收货人、手机、地址(可 address_snapshot JSON),后台发货后更新 status。
+
+### 3.3 与校友身份结合
+
+- **商品维度**:部分商品设置「仅校友可兑换」,移动端列表与详情中展示「仅校友」标签,未认证校友点击兑换时提示「请先完成校友认证」。
+- **价格维度**(可选):同一商品可设「校友价」与「非校友价」,用积分差体现校友优势,激励认证与保持校友身份。
+
+---
+
+## 四、促进校友活跃的具体策略
+
+### 4.1 拉新与认证
+
+- 分享得积分(已有)+ 被邀请人完成「校友认证」后给邀请人额外积分,形成二次激励。
+- 新用户引导任务:完善资料→报名一门课→完成首次签到,每步给积分,最后一步可给较大额,引导完成从「注册」到「校友行为」的路径。
+
+### 4.2 课程与活动
+
+- 报名、签到、评价均给积分,并可在课程详情/签到成功页展示「本次获得 XX 积分」,强化即时反馈。
+- 重要活动(如开学典礼、年会)可设「参与即得积分」或「签到得双倍积分」,由后台配置或活动开关控制。
+
+### 4.3 内容与互动
+
+- 供需发布、有效留言给积分,鼓励发布需求与对接。
+- 校友动态(articles)可扩展「点赞/评论得积分」或「优质动态被采纳得积分」,需防刷与内容审核。
+
+### 4.4 捐赠与品牌
+
+- 捐赠审核通过后给积分,并可设「捐赠专属礼品」仅可用积分+捐赠记录兑换,增强荣誉感与品牌绑定。
+
+### 4.5 积分商城本身
+
+- 上架校友专属商品(如学院周边、活动名额、线下权益),仅校友可兑换或校友享受更低积分价。
+- 限时兑换、限量商品,制造稀缺感,促进行为前置(先攒积分再抢兑)。
+
+---
+
+## 五、实施节奏建议
+
+| 阶段 | 内容 | 依赖 |
+|------|------|------|
+| **第一阶段** | 扩展积分获取:在现有课程报名通过、课表签到、分享处增加 `ScoreLog::add`,并增加 Config 配置项 | 现有 CourseSign/CourseContentCheck/UserController |
+| **第二阶段** | 移动端「我的积分」:展示当前积分与明细列表(拉取 score_logs,区分正负),必要时在 score_logs 增加 type 字段 | score_logs、users.score |
+| **第三阶段** | 积分商城后台:score_goods、score_orders 表与 CRUD,商品管理、订单管理、发货 | 新建表与 Admin 控制器/菜单 |
+| **第四阶段** | 积分商城移动端:商品列表/详情、兑换下单、订单列表与状态 | 新建 Mobile 控制器与小程序页 |
+| **第五阶段** | 校友专属与运营策略:仅校友可兑换商品、校友价、捐赠/活动专项积分与礼品 | 上述全部 + 运营配置 |
+
+---
+
+## 六、与现有代码的衔接点
+
+- **发积分**:统一用 `App\Models\ScoreLog::add($user_id, $score, $remark)`;若需支持「消耗」积分,可扩展 `ScoreLog::consume($user_id, $score, $remark)`(内部 decrement users.score 并写入一条负 score 记录)。
+- **配置**:积分分值继续用 `Config::getValueByKey('xxx')` 读取,在后台 Config 管理中加入 `score_*` 系列 key。
+- **校友判断**:`User::is_schoolmate == 1`,与现有校友库、课程自动入库逻辑一致。
+- **移动端鉴权**:兑换、积分明细等接口放在现有 `mobile` 路由组内,使用现有 `getUserId()` 等鉴权即可。
+
+---
+
+## 七、风险与注意
+
+- **防刷**:每日登录、签到等按人按日去重;分享需确认为新用户且未重复给同一 pid 发过该新用户的奖励;兑换需校验库存与积分余额,并发下用事务与行锁。
+- **积分一致性**:发分/扣分务必「改 users.score + 写 score_logs」在同一事务中,避免只写日志不改余额或只改余额不写日志。
+- **合规**:积分与兑换若涉及实物或权益,需在规则页说明解释权、过期与清零政策(若有),避免纠纷。
+
+本方案在不改变现有校友与课程主流程的前提下,通过「积分获取扩展 + 积分商城 + 校友专属」三条线,与系统现有功能深度结合,达到促进校友活跃度的目标。实施时可按阶段落地,先做积分规则与展示,再做商城与运营策略。
diff --git a/doc/线上课程平台功能设计与承载量评估.md b/doc/线上课程平台功能设计与承载量评估.md
new file mode 100644
index 0000000..12c84d5
--- /dev/null
+++ b/doc/线上课程平台功能设计与承载量评估.md
@@ -0,0 +1,266 @@
+# 线上课程平台功能设计与承载量评估
+
+## 一、需求概述与目标
+
+**核心需求**:为客户建设一套可用的线上课程平台,支持课程发布、学习管理、直播/录播、互动与数据统计,并能在预期用户规模下稳定运行。
+
+**目标**:
+- **功能完整**:覆盖课程生命周期、学习行为、互动与运营统计
+- **可落地**:技术方案与现有技术栈兼容,便于实施与迭代
+- **可评估**:对并发、带宽、存储等承载量有清晰估算与扩展路径
+
+---
+
+## 二、功能模块设计
+
+### 2.1 功能总览
+
+| 模块 | 功能要点 | 优先级 |
+|------|----------|--------|
+| 课程管理 | 课程/章节/课时创建、上下架、排序、分类与标签 | P0 |
+| 学习进度 | 报名、学习记录、进度同步、完成规则(按课时/按章节) | P0 |
+| 视频能力 | 录播点播(上传、转码、CDN 分发)、倍速/拖拽 | P0 |
+| 直播能力 | 创建直播、推流/拉流、回放生成、签到/互动(可选) | P1 |
+| 练习与考试 | 练习题/试卷、答题记录、自动批改、成绩与错题本 | P1 |
+| 互动 | 评论/问答、笔记、讨论区、弹幕(可选) | P1 |
+| 运营与统计 | 学习报表、完课率、热门课程、行为分析、导出 | P0 |
+
+### 2.2 核心业务流程
+
+```
+[管理员] 创建课程 → 添加章节/课时 → 上传视频或配置直播
+ ↓
+[学员] 选课/报名 → 按章节学习 → 观看视频/直播 → 做题/考试 → 获得完成凭证
+ ↓
+[系统] 记录学习进度、行为日志 → 统计报表、完课率、热门分析
+```
+
+---
+
+## 三、使用第三方平台的方案
+
+除自建系统外,可选用**第三方线上课程/知识付费平台**,由平台提供课程、视频、直播、练习、统计等能力,客户在平台内创建课程、招募学员即可,无需自建服务器与开发业务系统。
+
+### 3.1 典型第三方平台(示例)
+
+| 类型 | 代表产品 | 能力概览 | 适用场景 |
+|------|----------|----------|----------|
+| 综合型知识付费/网校 | **小鹅通**、**有赞教育**、**千聊** | 课程/专栏、录播/直播、打卡、考试、分销、数据统计 | 培训、知识付费、企业内训 |
+| 云课堂/公开课 | **腾讯课堂**、**网易云课堂**、**慕课网** | 课程体系、视频、直播、作业/考试、证书、学习数据 | 公开招生、职业技能培训 |
+| 会议/互动教室 | **ClassIn**、**腾讯会议**、**钉钉**(云课堂/在线课堂) | 实时互动、直播授课、白板、录播回放、考勤与数据 | 强互动、小班课、会议式授课 |
+| 企业学习平台 | **云学堂**、**魔学院**、**问鼎** | 学习计划、必修/选修、考试、证书、报表、与 OA/HR 对接 | 企业内训、合规学习、人才发展 |
+
+以上仅为示例,选型时需以当前产品能力、报价与客户业务匹配度为准。
+
+### 3.2 第三方方案的优势与约束
+
+| 维度 | 说明 |
+|------|------|
+| **上线快** | 注册/开通即可创建课程、上传视频、开直播,无需开发与部署,几天内可对外开课 |
+| **无基础设施负担** | 带宽、转码、存储、并发由平台承担,无需自建 CDN、流媒体与承载量评估 |
+| **功能现成** | 报名、学习进度、打卡、考试、证书、报表等一般已具备,按需开通与配置 |
+| **成本结构** | 多为按年/按人数/按流量付费,初期投入低;规模大时需看阶梯价格与定制费用 |
+| **定制与集成** | 界面、流程、业务规则受平台限制;与自有系统(如 CRM、公众号、支付)集成需看是否提供 API/ webhook/ 嵌入能力 |
+| **数据与合规** | 学习数据在平台侧,需确认导出能力、留存周期及是否满足客户的数据主权与合规要求 |
+
+### 3.3 选型建议
+
+- **适合优先考虑第三方平台的场景**:希望快速上线、团队无研发/运维资源、以标准课程+直播+简单练习为主、对深度定制与系统对接要求不高。
+- **适合自建的场景**:需与现有业务系统深度打通、有强定制与品牌统一要求、对数据与合规有明确自管要求、或长期规模与成本测算后自建更优。
+- **混合方式**:核心授课与学习在第三方完成,通过 API/ 嵌入/ 单点登录与自有系统做账号与门户统一、订单与数据回传,形成「第三方授课 + 自建门户/运营」的折中方案。
+
+若选择**自建**,则采用下文**第四、五、六节**的技术方案、承载量评估与实施建议;若选择**第三方或混合**,可仅将第四~六节作为技术参考或对接设计依据。
+
+---
+
+## 四、技术方案与实现要点(自建方案)
+
+### 4.1 整体架构建议
+
+```
+ ┌─────────────────────────────────────────┐
+ │ 用户端(Web / 小程序 / H5) │
+ └───────────────────┬─────────────────────┘
+ │
+ ┌───────────────────▼─────────────────────┐
+ │ API 网关 / 负载均衡 │
+ └───────────────────┬─────────────────────┘
+ │
+ ┌───────────────┬───────────────┼───────────────┬───────────────┐
+ ▼ ▼ ▼ ▼ ▼
+ [业务服务] [点播/直播] [文件/转码] [搜索/统计] [消息/通知]
+ │ │ │ │ │
+ └───────────────┴───────┬───────┴───────────────┴───────────────┘
+ ▼
+ ┌─────────────────────────────────────────┐
+ │ MySQL / Redis / 对象存储 / 流媒体 │
+ └─────────────────────────────────────────┘
+```
+
+- **业务服务**:课程、报名、进度、练习、考试、互动等,可用现有 Laravel 等框架扩展。
+- **点播/直播**:建议对接成熟云厂商(阿里云、腾讯云、七牛等)的「视频点播 + 直播」产品,自建成本高、运维重。
+- **文件/转码**:视频上传至对象存储,转码与多清晰度由云点播完成;仅元数据与播放地址自管。
+- **搜索/统计**:列表与简单检索用 DB;若需全文/复杂筛选可引入 Elasticsearch(可选)。
+
+### 4.2 数据库设计要点
+
+#### 4.2.1 课程与内容
+
+| 表名 | 主要字段 | 说明 |
+|------|----------|------|
+| `courses` | id, title, cover, intro, category_id, tags, status, sort, created_at | 课程主表 |
+| `course_chapters` | id, course_id, title, sort | 章节 |
+| `course_lessons` | id, chapter_id, title, type(video/live/text), duration, video_id, live_id, sort | 课时;type 区分录播/直播/图文 |
+| `course_videos` | id, file_path, cover, duration, transcoded(多清晰度信息 JSON) | 视频元数据,实际文件在对象存储/点播 |
+| `course_lives` | id, title, start_at, end_at, stream_name, replay_video_id, status | 直播场次及回放关联 |
+
+#### 4.2.2 学习与进度
+
+| 表名 | 主要字段 | 说明 |
+|------|----------|------|
+| `course_enrollments` | id, user_id, course_id, enrolled_at, status | 选课/报名 |
+| `lesson_progress` | id, user_id, lesson_id, progress_seconds, completed_at, status | 课时学习进度(可细化到秒) |
+| `course_progress` | user_id, course_id, completed_lessons_count, finished_at | 课程维度进度汇总(可冗余或统计得出) |
+
+**完成规则**:可在 `courses` 增加 `completion_rule`(如:需完成 80% 课时 / 全部必修课时),在 `lesson_progress` 与 `course_progress` 上计算是否达标。
+
+#### 4.2.3 练习与考试
+
+| 表名 | 主要字段 | 说明 |
+|------|----------|------|
+| `course_exams` | id, course_id, title, type(practice/exam), total_score, pass_score, time_limit_minutes | 练习/试卷定义 |
+| `exam_questions` | id, exam_id, type(single/multi/judge/text), content, options, answer, score | 题目 |
+| `exam_attempts` | id, user_id, exam_id, started_at, submitted_at, score, passed, answers_snapshot | 作答记录 |
+| `exam_attempt_answers` | attempt_id, question_id, user_answer, is_correct, score | 每题作答明细(错题本来源) |
+
+#### 4.2.4 互动与运营
+
+| 表名 | 主要字段 | 说明 |
+|------|----------|------|
+| `lesson_comments` | id, lesson_id, user_id, content, parent_id, created_at | 评论/问答(可带 parent 做楼中楼) |
+| `lesson_notes` | id, user_id, lesson_id, content, video_timestamp_seconds | 笔记(可关联时间点) |
+| `learning_logs` | id, user_id, lesson_id, event(play/pause/complete), progress_seconds, created_at | 行为日志(用于统计与报表) |
+
+索引建议:`lesson_progress(user_id, lesson_id)` 唯一;`learning_logs(user_id, created_at)`、`(lesson_id, created_at)`;`course_enrollments(user_id, course_id)` 唯一。
+
+### 4.3 关键接口与实现要点
+
+| 能力 | 接口/实现方式 | 要点 |
+|------|----------------|------|
+| 课程列表/详情 | GET /api/courses, GET /api/courses/{id} | 分页、分类/标签筛选、只返回上架课程 |
+| 章节与课时树 | GET /api/courses/{id}/chapters | 一次返回章节+课时树,前端按需展示 |
+| 报名 | POST /api/courses/{id}/enroll | 幂等;校验是否已报名、课程是否可报名 |
+| 进度上报 | POST /api/lessons/{id}/progress | body: progress_seconds, completed;防刷(如单次上报跨度不超过总时长) |
+| 进度查询 | GET /api/courses/{id}/my-progress | 当前用户该课程下各课时进度与完成状态 |
+| 视频播放地址 | GET /api/lessons/{id}/play-info | 返回带签名的点播 URL 或 HLS 地址(由云点播生成,避免盗链) |
+| 直播地址 | GET /api/lives/{id}/stream-info | 返回推流/拉流地址与鉴权参数 |
+| 练习/考试 | GET/POST /api/exams/{id}/attempt, POST submit | 计时在服务端校验;交卷后算分、写 attempt 与 answers |
+| 评论/笔记 | CRUD /api/lessons/{id}/comments, /notes | 分页、敏感词过滤(可选) |
+| 学习报表 | GET /api/admin/stats/learning | 按课程/时间维度:报名数、完课率、学习时长分布等 |
+
+**视频与直播**:不推荐自建转码与流媒体;使用云点播/直播的 API 完成「上传 → 转码 → 获取播放/推拉流地址」,平台只存 `course_videos`、`course_lives` 与对应 `video_id`/`stream_name`。
+
+---
+
+## 五、承载量评估
+
+### 5.1 评估假设(需按客户实际修正)
+
+| 指标 | 假设值 | 说明 |
+|------|--------|------|
+| 注册/日活用户 | 5,000 / 2,000 | 日活为估算并发与带宽的基准 |
+| 同时在线学习 | 500 | 同一时刻在看视频/直播的人数上界 |
+| 峰值系数 | 2~3× | 活动日/开课日相对平日 |
+| 单课程并发观看 | 200 | 单一直播或单课同时观看上限假设 |
+| 视频码率 | 1~2 Mbps | 标清~高清,按 1.5 Mbps 估算 |
+| 人均学习时长 | 30 min/日 | 用于估算存储与日志量 |
+
+以下计算均基于上述假设,实际需替换为客户真实或目标数据。
+
+### 5.2 并发与 QPS
+
+| 场景 | 估算方式 | 结果(示例) |
+|------|----------|--------------|
+| 列表/详情/进度查询 | 日活 × 5 次/天,按 8 小时摊 | 约 1,250 req/h → 0.35 req/s,取整 **5 QPS** 可留余量 |
+| 进度上报 | 同时在线 × 每 30 s 一次 | 500 × (1/30) ≈ **17 QPS** |
+| 评论/笔记/提交试卷 | 按同时在线 10% 有写操作 | 50 × 0.5 次/min ≈ **1 QPS** |
+| **业务 API 合计** | 留 2~3 倍余量 | **50~80 QPS** 可覆盖当前假设 |
+
+若日活或同时在线提升,可按比例放大;单机 Laravel(PHP-FPM)在简单接口下通常可支撑数百 QPS,瓶颈多在 DB 与外部服务。
+
+### 5.3 视频带宽(关键)
+
+- **点播**:同时观看 500 人 × 1.5 Mbps ≈ **750 Mbps** 下行(约 94 MB/s)。
+- **直播**:单场 200 人 × 1.5 Mbps ≈ **300 Mbps** 下行。
+
+建议全部走 **CDN + 云点播/直播**,带宽由云厂商按量计费,平台侧不直接出带宽;需在评估报告中写清「预计峰值带宽约 750 Mbps(点播)+ 300 Mbps(直播单场)」,便于客户与云厂商选型与计费。
+
+### 5.4 存储估算
+
+| 类型 | 估算方式 | 结果(示例) |
+|------|----------|--------------|
+| 视频文件 | 不落本地,在对象存储/点播;仅元数据在 DB | 按课程数量与时长估云侧费用 |
+| 数据库 | 用户/课程/进度/答题/日志,按 1 万用户、100 课程、每人 50 条进度 | 约 **1~5 GB** 量级,随日志与行为表增长 |
+| 行为/学习日志 | 2000 日活 × 30 条/天 × 0.5 KB | 约 **30 MB/天**,可定期归档或冷热分离 |
+| Redis | 会话、进度缓存、限流 | **1~2 GB** 足够 |
+
+### 5.5 压测与容量建议
+
+- **业务 API**:对「课程列表、进度上报、我的进度」等接口做压测,目标 **80 QPS 下 P99 < 500 ms**;数据库加索引、避免 N+1。
+- **视频**:使用云点播/直播后,由云厂商保证可用性与带宽扩展,平台侧重点保证「获取播放地址」接口稳定与鉴权正确。
+- **扩展**:
+ - 应用:单机可先支撑上述量级;超过可加机器 + 负载均衡,会话与缓存用 Redis 集中。
+ - 数据库:主从读写分离、慢查询优化;日志表可按时分区或归档。
+ - 带宽与转码:随量在云上扩容,无需自建。
+
+### 5.6 承载量结论表(示例)
+
+| 维度 | 当前假设下容量 | 扩展方式 |
+|------|----------------|----------|
+| 业务 API | 50~80 QPS,P99 < 500 ms | 增加应用节点、DB 优化/读写分离 |
+| 同时在线学习 | 约 500(点播)+ 单场 200(直播) | 视频全部走 CDN/云服务,按量扩容 |
+| 存储(DB) | 1~5 GB,30 MB/天日志 | 分区/归档、冷热分离 |
+| 预期可支撑 | 约 5,000 注册、2,000 日活、500 同时学习 | 按客户目标替换假设后重算 |
+
+---
+
+## 六、实施建议与风险
+
+### 6.1 实施阶段建议
+
+| 阶段 | 内容 | 周期参考 |
+|------|------|----------|
+| 一期 | 课程管理、录播点播、学习进度、基础报表;对接云点播 | 4~6 周 |
+| 二期 | 直播、练习/考试、评论与笔记 | 3~4 周 |
+| 三期 | 运营报表增强、错题本、消息通知、体验优化 | 2~3 周 |
+
+先保证「能上课、能记进度、能看数据」,再上直播与练习,可控制风险与返工。
+
+### 6.2 风险与应对
+
+| 风险 | 应对 |
+|------|------|
+| 视频带宽/成本超预期 | 明确用 CDN + 云点播/直播,按量计费;设置清晰度与码率策略 |
+| 进度与完成规则复杂 | 先实现「按课时完成比例」的简单规则,再迭代条件(如必须做完练习) |
+| 高并发下 DB 成为瓶颈 | 索引优化、读写分离、热点表(如 progress)可考虑分表或缓存 |
+| 直播延迟与稳定性 | 完全依赖云直播方案,选成熟厂商并做单场压测与回放验证 |
+
+### 6.3 与客户对焦事项
+
+- **用户规模**:确认日活、同时在线、单课程最大并发,用于替换 4.1 的假设并重算承载量。
+- **完成规则**:是否必须做题、是否必须看完全部视频、是否有证书/学分。
+- **直播需求**:是否必须、场次频率、是否需要连麦/白板等,以决定是否采用「云直播 + 回放」即可。
+- **合规与版权**:课程版权、个人信息与学习数据留存策略、审计日志要求。
+
+---
+
+## 七、文档与交付物建议
+
+- **功能规格**:在本文第二、四节基础上细化成「课程管理」「学习进度」「视频/直播」「练习考试」「统计报表」等子规格,便于开发与验收。
+- **承载量报告**:将 5.1 的假设改为客户确认数据后,重新计算 5.2~5.5,输出一页「承载量评估结论表」和「扩展建议」,便于运维与采购。
+- **接口文档**:对 4.3 中接口出 OpenAPI/Swagger,便于前后端联调与测试。
+
+---
+
+**文档版本**:v1.0
+**适用场景**:线上课程平台客户需求的技术实现方案与承载量评估,可作为投标/立项文档的附录或正文依据。
diff --git a/doc/邮件打开率与回执统计实现方案.md b/doc/邮件打开率与回执统计实现方案.md
new file mode 100644
index 0000000..96bc9f2
--- /dev/null
+++ b/doc/邮件打开率与回执统计实现方案.md
@@ -0,0 +1,131 @@
+# 邮件打开率统计功能实现方案
+
+## 一、需求概述
+
+在现有邮件发送能力(`EmailRecord` / `EmailRecordUser`、定时任务 `send_email`)基础上,增加:
+
+- **打开率统计**:统计每封邮件是否被收件人「打开」(基于追踪像素),并在后台展示汇总与明细。
+
+便于评估邮件触达效果,为后续运营与内容优化提供数据支撑。
+
+## 二、现状简要分析
+
+| 模块 | 现状 |
+|------|------|
+| 发送记录 | `email_records`:主题、模版、发送时间、状态等 |
+| 收件人明细 | `email_record_users`:邮箱、status(0 待发送 / 1 成功 / 2 失败)、send_time、reason |
+| 发送方式 | `SendEmail` 命令 + `EmailRecordUser::email()`,使用 Laravel `Mail::send()` |
+| 统计 | 仅有「成功/失败」数量,无打开相关字段与逻辑 |
+
+## 三、技术方案
+
+### 打开率统计
+
+**思路**:采用「追踪像素 + 唯一链接」方式。
+
+1. **追踪像素(Open Tracking)**
+ 在 HTML 邮件正文末尾插入一张 1×1 透明图片,`src` 指向本站追踪接口,并携带 `email_record_user_id` 作为唯一标识。
+
+2. **接口行为**
+ - 收到 GET 请求后,根据参数识别到对应的 `email_record_user`。
+ - 若该条尚未记录打开,则写入「首次打开时间」并计数;可同时记录打开次数、最近打开时间(视需求扩展)。
+ - 返回 1×1 透明 GIF 图片响应(或 204 No Content),避免影响邮件展示。
+
+3. **打开率计算**
+ - 单条邮件任务:`打开率 = 有过打开记录的 email_record_user 数 / 发送成功的 email_record_user 数`。
+ - 列表/汇总:在 `EmailRecord` 维度聚合「已打开人数」「发送成功人数」后计算百分比。
+
+**注意**:
+
+- 部分邮件客户端或隐私保护会屏蔽远程图片,打开率会偏保守。
+- 需做好参数校验与防刷(同一 user 可多次请求只计一次或按业务规则限频)。
+
+## 四、数据库设计
+
+在现有表结构上做最小扩展,便于与当前 `EmailRecord` / `EmailRecordUser` 一致。
+
+### 4.1 `email_record_users` 表增加字段(推荐)
+
+| 字段名 | 类型 | 说明 |
+|--------|------|------|
+| `opened_at` | `dateTime` nullable | 首次打开时间(用于判断是否「已打开」) |
+| `open_count` | `int` default 0 | 打开次数(可选,用于重复打开统计) |
+
+**说明**:
+- 仅当 status = 1(发送成功)时,才参与打开率计算。
+- 若只需「是否打开」,仅 `opened_at` 即可;`open_count` 可用于「最近打开时间」等扩展。
+
+### 4.2 迁移示例
+
+```php
+// database/migrations/xxxx_add_open_tracking_to_email_record_users_table.php
+Schema::table('email_record_users', function (Blueprint $table) {
+ $table->dateTime('opened_at')->nullable()->after('send_time')->comment('首次打开时间');
+ $table->unsignedInteger('open_count')->default(0)->after('opened_at')->comment('打开次数');
+});
+```
+
+无需新建表即可满足「按人维度打开 + 按邮件记录聚合打开率」的需求。
+
+## 五、后端实现要点
+
+### 5.1 追踪像素接口(公开,无需登录)
+
+- **路由**:例如 `GET /api/email/open/{id}` 或 `GET /track/email/open?id=xxx`,其中 `id` 为 `email_record_user_id`。
+- **逻辑**:
+ 1. 根据 `email_record_user_id` 查出 `EmailRecordUser`,校验 status = 1(发送成功)。
+ 2. 若 `opened_at` 为空,则写入 `opened_at = now()`,并可选 `open_count += 1`;若已存在,可按策略只更新 `open_count` 或忽略。
+ 3. 返回 1×1 透明 GIF(可把一张静态 GIF 放在 `public` 或 `storage`,或使用二进制输出)。
+
+### 5.2 邮件内容中插入追踪像素
+
+- 在 `SendEmail` 命令(或统一封装「生成邮件 HTML」的地方)中,对每条 `EmailRecordUser`:
+ - 生成该条记录对应的追踪 URL(带 `email_record_user_id`),如:`https://your-domain.com/api/email/open/{{ $recordUser->id }}`。
+ - 在 HTML 正文末尾追加:`
`。
+- 若当前邮件是纯文本,可考虑转为 multipart(text + html),仅在 html 部分加像素;若暂时只发 HTML,则直接拼接即可。
+
+### 5.3 统计查询
+
+- **列表(EmailRecord 维度)**:
+ - 已有:`withCount('emailRecordUsers')`、成功数、失败数。
+ - 新增:`withCount(['emailRecordUsers as opened_count' => function ($q) { $q->where('status', 1)->whereNotNull('opened_at'); }])`,或单独查询「发送成功数」与「已打开数」,再算打开率。
+- **详情**:在 `emailRecordUsers` 中直接展示 `opened_at`、`open_count`,列表页可展示「已打开 / 已发送」与打开率百分比。
+
+### 5.4 接口返回建议
+
+- 列表接口在每条 `EmailRecord` 上增加:
+ - `sent_count`:发送成功数(status=1)。
+ - `opened_count`:已打开数(status=1 且 opened_at 非空)。
+ - `open_rate`:百分比,如 `sent_count > 0 ? round(opened_count / sent_count * 100, 2) : 0`。
+- 详情接口在每条 `EmailRecordUser` 上增加 `opened_at`、`open_count`(若存在)。
+
+## 六、前端展示建议
+
+- **邮件记录列表**:
+ - 增加列:「发送成功数」「已打开数」「打开率%(如 65.00%)」。
+ - 可选:筛选「打开率 > 某值」或「未打开」。
+
+- **邮件记录详情(收件人明细)**:
+ - 表格列:邮箱、发送状态、发送时间、**是否已打开**、**首次打开时间**、打开次数。
+ - 可导出为 Excel,便于线下分析。
+
+- **仪表盘/统计页(可选)**:
+ - 按时间范围汇总:总发送数、总打开数、平均打开率;或按某次邮件任务排行。
+
+## 七、安全与隐私
+
+- **防刷**:同一 `email_record_user_id` 多次请求只记一次「打开」或按业务限频,避免恶意请求虚高打开率。
+- **隐私**:追踪像素仅记录「是否/何时打开」,不在方案中记录 IP、User-Agent 等(若后续需要可单独评估合规性)。
+
+## 八、实施步骤建议
+
+| 步骤 | 内容 |
+|------|------|
+| 1 | 新增迁移:`email_record_users` 增加 `opened_at`、`open_count` |
+| 2 | 实现追踪接口(路由 + 控制器,按 `email_record_user_id` 查询并更新 + 返回 1×1 图片) |
+| 3 | 在发送逻辑中为每条收件人生成追踪 URL,并在 HTML 邮件末尾插入追踪像素 |
+| 4 | 在 `EmailRecordController` 的 index/show 中增加 `opened_count`、`open_rate` 及明细字段的查询与返回 |
+| 5 | 前端:列表与详情页增加打开率、已打开数、首次打开时间等展示与导出 |
+| 6 | (可选)增加简单防刷与限频、日志 |
+
+按上述步骤即可在现有项目上完成邮件打开率统计功能。
diff --git a/科技商学院学员信息-匹配人才标签_匹配是否.xlsx b/科技商学院学员信息-匹配人才标签_匹配是否.xlsx
deleted file mode 100644
index 103a04b5423130a26d1282c780c66f87e4416ee9..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 31130
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