[开发]职业罗盘(career_compass)插件开发方案

👑Lv.11 元老 🌞 骨干会员
2026-10-26 00:41:34

本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/docs/职业罗盘插件开发方案_2026-10-19.md。

职业罗盘(career_compass)插件开发方案

版本:规划稿 v2.3(2026-10-19)——v2.1 回写 7 条锁定决策(§0.1);v2.2 订正「题序/混排」、新增 §5.5 部署与安全;v2.3 按 A-2 用户裁决把霍兰德算分改为 main/skill 两池(修原站 bug,取码只用 main);P1 已落地

实测依据:.workbuddy-ai/tmp/cc_extract/scoring_rules.md(P1 硬交付)与 report.txt(断言报告)——凡本方案与该文件冲突,以该文件为准

依据:docs/插件开发规范.md v1.6(2026-10-16)

数据源:d:\360极速浏览器下载\index-DEkqrWmP.js(931,492 字符,压缩产物)

现状:P1(数据提取 + 骨架)已落地(questions.json / results.json / scoring_rules.md / Plugin.php / plugin.json / hook 骨架,断言全绿);P1 的 scoring_rules.md 为 P3/P4 唯一依据,P2 起(入口页 / 答题 / 算分 / 结果)待确认后开工。


0. 结论速览(本次已实测,非推测)

项结论
题库数据✅ 4 个题库数组、7 个解析字典(含实测补出的 pue/gue/yue)全部真实存在,字节偏移与元素形状已逐个采样核实
题量✅ Xc=192(每维 32)、VF=60(每维 10)、IF=60(15 维 × 4)、Gc=120(每维 24,反向 45 题)
字典✅ EN=6 维(R/I/A/S/E/C)、ON=15 键、hue=96 键(974 条职业,v1 的「2,922」系字符/字节计数残留,已作废)、$d=5 维;另实测出方案遗漏的 pue/gue/yue 三本字典,已并入 results.json
数据形态⚠️ 是 JS 对象字面量(裸键、双引号字符串),不是可直接解析的 JSON → 必须清洗
图标✅ compass 在核心 $map 内(TemplateCompiler.php:832),可直接用
插件名⚠️ 目录名含下划线 → 必须在 Plugin.php 末尾加 class_alias 段(§4.1)
数据存放✅ 定案(D1):plugins/career_compass/data/。前提成立——官方自行发布、不投稿 → data/ 不被剥离
数据缺口✅ 已补齐(P1 硬交付 scoring_rules.md):选项/分值、霍兰德取码、大五阈值全部从 JS 下游消费点抄出并写死;大五阈值实测为 >50(0–100 百分比空间),D4 的 3.5 兜底未使用;霍兰德算分按用户裁决修正为 main/skill 两池(不照抄原站 P4 bug,取码只用 main)

0.1 已锁定决策(v2 定案 · 2026-10-19)

#决策定案口径影响面
D1静态 JSON 存放放 plugins/career_compass/data/questions.json 与 data/results.json。前提成立:官方自行发布、不投稿 → data/ 不被剥离。v1「改放 assets/data/」的建议作废。§2.4 路径、§8 发布
D2发布路径 / 页面范围官方自行发布、不投稿。后台管理页缓做(先不做);「我的记录」页保留。§5.1、§7 P5
D3192 题顺序订正(v2.2):数据层 questions.json 保留原顺序(P1 断言逐题可核对);渲染层不再混排(取消)——实测每页(60/60/60/12)已含 6 维各 10 题、天然混排,无需再打散。原「渲染层页内混排」表述作废。§2.2、§5.2
D4大五分档阈值优先从 JS 下游消费点抄原站阈值;抄不到则固定切点 3.5(≥3.5 高、<3.5 低)。不用均值 ±0.5σ。P1 的 scoring_rules.md 必须把结论写死,不留「待定」。实测结果:已抄到原站阈值 >50(0–100 百分比空间),故 3.5 兜底未启用。§2.1、§5.3
D5逐题作答入库cc_records 加 answers_json(TEXT,逐题作答)。用途:查看答卷 / 二次判分 / 以后调阈值不重测。其余字段按最小实现。§2.5
D6游客策略游客不能答题:入口页、答题页、提交接口一律 requireLogin(),未登录跳 loginUrl()(比 v1 建议更严)。§3、§5.1
D7积分先不接:不写积分钩子,表结构不受影响,以后要接再加。§5.3

P1 硬交付补一条:scoring_rules.md 不能拖到 P2 —— P1 就要从 JS 下游消费点抄出「选项定义/分值、霍兰德取码规则、大五分档阈值」并落盘,否则 P4 算分没有地基。

迁移备注(D1):将来若转投稿,此块须迁至 assets/data/;现在不动。


1. 数据源实测明细

变量字节偏移(实测)数量元素形状(原文采样)备注
Xc 霍兰德完整340,765192{id:"1",type:"R",text:"装配修理电器或玩具",category:"R"}id 是字符串→清洗为整数;type 与 category 值完全相同(冗余);每维 32 题;分页 60/60/60/12(见 §5.2)
IF 舒伯价值观479,87360{id:1,text:"能参与救灾济贫的工作",category:"利他主义"}15 个维度 × 4 题,维度名为中文
VF 霍兰德速测489,67460{type:"R",text:"我喜欢动手修理电器、家具或机械设备"}无 id 字段(清洗时按数组序补 1..60);每维 10 题
Gc 大五人格935,750120{id:1,text:"我对抽象的概念和理论充满好奇。",domain:"O",keyed:1}keyed:-1 共 45 题(反向计分标记就在数据里);每维 24 题
EN 霍兰德解析845,1226 键{R:{name:"实用型 (Realistic)",desc:"…",traits:[11 项],jobs:[10 项]}}键 = R/I/A/S/E/C
ON 舒伯解析847,23915 键{利他主义:{desc:"…",advice:"…"}}键是中文裸标识符(无引号),清洗时需补引号
hue 职业匹配850,54196 键{ACE:["艺术品拍卖师","画廊行政经理",…]}键 = 三字母霍兰德码;职业条目合计 974(v1 的 2,922 系计数残留)
pue 大学专业(单维)868,9266 键{R:["…共 15 项"]}键 = R/I/A/S/E/C;v1 遗漏,P1 补出
gue 大学专业(双维)870,28230 键{RI:["…共 3 项"]}键 = 二字母组合;v1 遗漏,P1 补出
yue 行动建议871,5066 键{R:["…共 5 项"]}键 = R/I/A/S/E/C;v1 遗漏,P1 补出
$d 大五解析943,8255 键{O:{name:"开放性 (Openness)",desc:"…",high:"…",low:"…"}}键 = O/C/E/A/N;只有高/低两档文案,无中档

1.1 两个必须正视的数据特征

  1. 题目在「块内」天然混排,不是按维度连续成块(订正 v2.1 及更早的「R×32 连成块」表述)

实测 Xc 维序为

R×10 A×10 I×10 S×10 E×10 C×10 | R×10 A×10 I×10 S×10 E×10 C×10 | R×10 S×10 I×10 E×10 A×10 C×10 | R I A S E C 各 ×2,

即 3 个 60 题块(每块 6 维各 10 题)+ 尾 12 题(6 维各 2 题);原站分页为 60/60/60/12(见 §5.2)。

→ 每一页都含全部 6 维各 10 题、天然混排——第 1 页并非「全是 R 题」。

→ D3 订正为:数据层保原顺序即可;渲染层页内混排「取消」(每页已天然混排,不需再打散)。

Gc(大五)虽按维度连续成块 O×24 → C×24 → E×24 → A×24 → N×24,但原站为 24 页 × 5 题(每页 5 题同维、

原站本就不打散),故大五同样无需渲染层混排;速测单页、舒伯 6 页 × 10 题(每页 10 题同维,原站亦然)同理。

  1. hue 缺 24 个组合

6 维取 3 的全部排列为 6×5×4 = 120,实际只有 96 键(缺失名单见 report.txt)。

→ 算分得到的三字母码可能查不到精准职业,必须设计降级链(实测原站链,见 scoring_rules.md §5):

hue[三字母码](精准职业)→ mue(原站为空对象,不迁移)→ pue[首维] ∪ gue[前两维](大学专业,去重取前 12)→ yue[首维](行动建议 5 条)→ 兜底文案。


2. 第一阶段交付物(本次要做的全部内容)

2.1 一次性提取脚本(不进插件目录)

  • 位置:.workbuddy-ai/tmp/cc_extract/(中间产物目录,不随插件发布,也不放 plugins/career_compass/ 内)。
  • 做法:读取 JS 原文 → 按偏移切片定位 8 个结构 → 正则转写(裸键补引号)→ 校验计数 → 写出两个 JSON。
  • P1 硬交付:scoring_rules.md(不得拖到 P2) —— 在 JS 中定位 Xc/VF/IF/Gc 的下游消费点(算分函数/渲染函数),抄出并落盘以下三项,作为 P4 移植的唯一依据:
  1. 各量表的选项定义与分值(霍兰德/速测是「是/否」还是 Likert 几级;IF/Gc 的 1–5 文案与对应分值);
  2. 霍兰德的结果码取法(取前 3 维?并列如何裁决?是否加权);
  3. 大五的分档阈值:优先抄原站阈值;抄不到则按 D4 写死为固定切点 3.5(≥3.5 高、<3.5 低),不留「待定」、不用均值 ±0.5σ。
  • 落盘位置:.workbuddy-ai/tmp/cc_extract/scoring_rules.md(与提取脚本同目录,属 P1 交付物),并把结论回填进本方案。
  • 不把算分逻辑留在前端(用户红线):P4 的 PHP 算分严格按该文件实现。

2.2 data/questions.json(拟用 schema)

{
  "holland_full":  [ {"id":1,"dim":"R","text":"…"}, … ]          // 192 条,dim ∈ R/I/A/S/E/C
  "holland_short": [ {"id":1,"dim":"R","text":"…"}, … ]          // 60 条,补出的 id
  "super_values":  [ {"id":1,"dim":"利他主义","text":"…"}, … ]    // 60 条,15 维 × 4
  "big_five":      [ {"id":1,"dim":"O","keyed":1,"text":"…"}, … ] // 120 条,keyed ∈ {1,-1}
}

清洗规则(逐条):

  1. 裸键补双引号:id / type / text / category / domain / keyed / name / desc / traits / jobs / advice / high / low。
  2. Xc 的 id:"1" → 整数 1;冗余字段 category 合并为单一 dim(值等于 type),只保留一个键。
  3. VF 无 id → 按数组顺序补 1..60(不改变顺序,便于与原站逐题核对)。
  4. IF 的 category 改名 dim(保持中文维度名,与 ON 的键一一对应)。
  5. Gc 的 domain 改名 dim,保留 keyed(±1)——这是反向计分的唯一依据,不可丢。
  6. 输出:UTF-8 无 BOM、LF、JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT;字符串内的 "、\n、HTML 实体等特殊字符在提取时统一规范化(提取后人工抽检 20 题确认无截断/粘连)。
  7. 保序(D3):数组顺序不得重排;四组 id 必须等于文件内位置(holland_full 1..192、holland_short 1..60、super_values 1..60、big_five 1..120 严格递增),保证「逐题与原站核对」成立。乱序只发生在渲染层 script.js,questions.json 一律不动。

2.3 data/results.json(拟用 schema)

{
  "holland":       { "R": {"name":"…","desc":"…","traits":[…],"jobs":[…]}, … },  // 6 键
  "super":         { "利他主义": {"desc":"…","advice":"…"}, … },                  // 15 键
  "hue":           { "ACE": ["…","…"], … },                                      // 96 键(974 条)
  "majors_single": { "R": ["…共 15 项"], … },                                    // 6 键(原 pue,P1 补出)
  "majors_pair":   { "RI": ["…共 3 项"], … },                                    // 30 键(原 gue,P1 补出)
  "action_plan":   { "R": ["…共 5 项"], … },                                     // 6 键(原 yue,P1 补出)
  "big_five":      { "O": {"name":"…","desc":"…","high":"…","low":"…"}, … }      // 5 键
}

说明:解析字典统一挂到一个 results.json 里、以 7 个顶层键区分(实测比 v1 的 4 键多出霍兰德结果页依赖的 majors_single/majors_pair/action_plan;原站的 mue 为空对象,不迁移),避免多文件多次读盘(配合 §4 的单例缓存)。

2.4 Plugin.php 骨架(本阶段只到数据层 + DDL,不含算分)

namespace Plugin\CareerCompass;

class Plugin {
    const TYPES = ['holland_full','holland_short','super_values','big_five'];

    public static function db(): \PDO            // Plugin::db('career_compass')
    public static function ddl(): string          // cc_records 建表
    public static function activate(): bool        // ensureSchema + 索引
    public static function deactivate(): bool      // 空实现(不删表)
    public static function uninstall(): void       // DROP TABLE

    private static array $dataCache = [];                       // ← 用户指定:单例缓存
    public static function loadData(string $filename): array    // 读 data/{file}.json
    public static function loadQuestions(string $type): array    // 校验白名单后走 loadData
    public static function loadResults(): array
}

// 文件末尾(类定义之外)——目录名含下划线,硬性要求:
if (!\class_exists('Plugin\\career_compass\\Plugin', false)) {
    \class_alias(Plugin::class, 'Plugin\\career_compass\\Plugin', false);
}

loadQuestions($type) 的三条硬约束:

  1. $type 必须在 TYPES 白名单内,非法值直接返回 [](防路径穿越:绝不把用户输入拼进文件路径);
  2. 走 $dataCache 静态缓存,同一请求内的 192 题读取与算分查字典最多各读盘一次;
  3. 文件缺失/JSON 解析失败返回 [],由调用方给出友好错误,不抛异常、不 die()。

2.5 cc_records DDL(SQLite 语法,幂等)

CREATE TABLE IF NOT EXISTS cc_records (
    id           INTEGER PRIMARY KEY,
    user_id      INTEGER NOT NULL,
    test_type    TEXT    NOT NULL,
    scores_json  TEXT    NOT NULL DEFAULT '{}',  -- 各维度得分:霍兰德 {"main":{…},"skill":{…}}(A-2 两池)/ 舒伯 15 维加权和 / 大五 5 维百分比
    result_code  TEXT    NOT NULL DEFAULT '',    -- 霍兰德 = 三字母码(如 AIS);舒伯 = 首要维度名;大五 = 五维档位串
    answers_json TEXT    NOT NULL DEFAULT '{}',  -- D5:逐题作答 {"题目id":分值,…},用于查看答卷 / 二次判分
    created_at   TEXT    NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_cc_records_user ON cc_records (user_id, test_type, created_at DESC);

用户指定的 6 个字段全部覆盖,另按 D5 追加 answers_json(TEXT)。写入一律 json_encode 后 prepare() + 绑定,不拼 SQL。

建表统一走 Plugin::ensureSchema('career_compass', self::ddl()) + init_after 兜底(§11.15),索引用 CREATE INDEX IF NOT EXISTS 单独执行。

2.6 第一阶段验收断言(脚本自动跑,不靠肉眼)

断言期望
questions.json 四组条数192 / 60 / 60 / 120
Xc 各维计数R=I=A=S=E=C=32
VF 各维计数每维 10
IF 去重维度数15,且每维 4 题
Gc 各维计数 / keyed:-1每维 24 / 反向 45
results.json 键数holland=6、super=15、hue=96、big_five=5
super 键 ⊇ IF 维度名15/15 全对应(缺一即数据错位)
EN/$d 每条均含 name/desc/high/low($d)与 traits/jobs(EN)无缺失字段
保序(D3)四组 id 自 1 起严格递增 1、无重号无跳号;数组顺序与原文逐题一致
抽样人工核对随机 20 题(含反向题、含 id 字符串题)与原文逐字一致
JSON 合法性php -r 'json_decode(file_get_contents(...), true)' 无错误、无 BOM


3. 规范合规要点(对照 v1.6,逐条落到本插件)

规范本插件落实
目录即插件plugins/career_compass/,命名空间 Plugin\CareerCompass
独立库仅 cc_records 落 data/career_compass.sqlite;题库/字典为 data/*.json 静态文件(D1:官方自行发布,data/ 不被剥离)
零内联(§7.3)答题页/结果页无 <script> 块、无 on、无静态 style="";交互全进 assets/script.js;下拉/进度/选项状态用 class 或 data- 承载;文案注入走 data-i18n='…json…'
主题变量全部色值用 --mn-* 变量(卡片/边框/文字/强调色),不硬编码 hex;雷达图颜色同样走变量
生命周期activate/deactivate/uninstall + init_after 建表兜底 + 末尾 class_alias
图标plugin.json 的 icon: "compass"(已在核心 $map 内);导航钩子内用 icon('compass', 16)
资源assets/style.css、assets/script.js 一律 ?v=filemtime,路径拼 BASE_PATH;用 section('css') 按页加载
语言包lang/{zh,en,zh_tw}.php,键前缀 plugin.career_compass.*
钩子init_after(建表兜底)、route_register(前台 6 条路由:入口/答题/取题/提交/结果/我的记录)、nav_plugin_links(PC 左栏 + 顶栏双触发,必须用 __nav_uri 守卫 + mn-cat-active + 16px)
红线 1/2无 unserialize/eval;路径全部基于 __DIR__/BASE_PATH
红线 3本插件不用渲染钩子输出内容(页面全自建),从根上规避 die()/exit()
红线 5/6/7写库全部 prepare() + 绑定;无库存扣减场景;不覆盖 PDO 属性
红线 8 + §11.28 + D6入口页 / 答题页 / 提交接口三处一律先 $this->requireLogin();提交接口再 Csrf::verifyOrDie()(顺序不可颠倒);登录链接用 $this->loginUrl()
游客策略(D6)游客不能答题:三处入口全部登录态,未登录跳 loginUrl();不注册后台路由(D2,后台缓做),permissions 保持 []
性能(§11.4)题库读盘走单例;「我的记录」列表一次查库(WHERE user_id=:id ORDER BY created_at DESC LIMIT),绝不 N+1
用户数据列表页用户名一律走 User::batchGetNames(),禁止直查核心 users 表


4. 数据访问与性能设计

  1. 服务端:loadData() 单例缓存(§2.4),一次请求最多各读一次盘 → 拉 192 题、算分查字典都命中缓存。
  2. 前端取题:答题页不内联题库(192 题约 13 KB,内联会让每份 HTML 变胖且不利于缓存)。

拟由 assets/script.js 向 GET /career-compass/api/questions?type=xxx 取 JSON(Content-Type: application/json,带 Cache-Control 缓存头),渲染交给 JS。

→ 服务端仍只读一次盘(命中单例),并统一输出 {code,message,data} 形状便于前端判错。

  1. 落库:只存记录与得分(cc_records),不建题库表(用户明确要求)。
  2. 图片:不使用任何位图,雷达图用内联 SVG path 由 PHP 生成/由 JS 按 data-* 绘制 → 无外部图片依赖。

5. 第二阶段蓝图(本阶段不实现,仅供确认口径)

5.1 路由(hook/route_register.php)

方法路径控制器登录(D6)说明
GET/career-compassFrontController::indexrequireLogin入口页:4 个测评卡片(题量/时长/维度说明)
GET/career-compass/testFrontController::testrequireLogin答题页,?type= 决定题库(白名单校验)
GET/career-compass/api/questionsFrontController::apiQuestionsrequireLogin题库 JSON(type 白名单),输出 {code,message,data}
POST/career-compass/submitFrontController::submitrequireLogin → Csrf::verifyOrDie()收作答 → PHP 算分(按 scoring_rules.md)→ 写 cc_records → 结果跳转
GET/career-compass/resultFrontController::resultrequireLogin结果页,?id= 读本人记录(user_id 归属校验,防 IDOR)
GET/career-compass/mineFrontController::minerequireLogin我的测评记录(D2 保留;一次查库无 N+1,用户名走 User::batchGetNames())

路径为连字符目录名,与站点根真实文件/目录不冲突(§11.34b 已核)。

不注册 admin_route_register(D2 后台缓做)→ plugin.json 的 permissions 保持 []。

5.2 答题交互(零内联约束下的写法)

  • script.js 通过 fetch 取题库 → 分页渲染 → 点击选项即计进度;

实测分页(订正 v1 的「48/页」):霍兰德完整版原站是 4 段 60 / 60 / 60 / 12,且四段题型不同

(P1–P3「是/否」=1/0,P4 能力自评 1–7 滑杆、默认 4);速测 60 题单页;舒伯 6 页 × 10 题;大五 24 页 × 5 题。

详见 scoring_rules.md §0/§1–§4(以此为准)。

分页区间取原站 IT():P1=id 1–60、P2=61–120、P3=121–180、P4=181–192;四页题干取原站 LF(「是否喜欢…活动」/「是否擅长…」/「是否喜欢…职业」/「能力评分 1-7 分」)。

  • 渲染顺序(D3 订正):按 questions.json 原顺序直接切段渲染即可,不再做页内打散。

实测每段(60/60/60/12)都含全部 6 维各 10 题(速测每页 6 维各 10;舒伯每页 10 题同维、大五每页 5 题同维,均为原站口径),

「第 1 页全是 R 题」的引导问题不存在。questions.json 一律不动,script.js 内删除任何 shuffle/打散逻辑。

  • 选项状态用 class 表达(.cc-opt-selected),不写 onclick,用事件委托 + data-qid / data-value;
  • 页面用 data-* 承载:CSRF token、type、i18n 文案(题序按原顺序由 JS 切段渲染,不落 HTML);
  • 提交时组装 {type, answers:{id:value,...}} 以 FormData(含 csrf)POST;
  • 前端不做任何算分:不出现维度累加、不出现反向计分、不出现结果码计算。

5.3 算分逻辑(PHP,Plugin::score*() 四个方法)

量表算法`result_code`
霍兰德完整/速测两池(A-2 修正):main 池按 dim 累加 1–180 作答值(每维上限 30);skill 池单独累加 181–192 的 1–7(每维上限 14),不并入主分。速测无 P4 → 只有 main取 main 池前 3 维拼接(并列按固定维度序 R/I/A/S/E/C 裁决),得三字母码
舒伯价值观每题 1–5 权重按 dim 求和(15 维)取最高分维度名(ON 的键);可选输出 Top3
大五人格keyed==1 ? score : (6 - score),按 dim 求和 → 0–100 百分比(round(Σ/(题数×5)×100),每维 24 题)5 维档位串(高/低);阈值 = >50(百分比)(等价原始均值 >2.5)——已抄到原站阈值,D4 的 3.5 兜底未使用;文案取 $d[*].high/low

  • 霍兰德四段计分口径(A-2,已修正原站 bug,写死):原站 C.category&&(R<=180,…) 因逗号运算符使 R<=180 失效 → 把 P4 的 1–7 分并入主维度,P4 仅 2/32 题量却拉出约 32% 上限分(技术债,本插件不留)。

落地:拆 main / skill 两池——main = 1–180(P1+P2+P3,0/1,每维上限 30);skill = 181–192(P4,1–7,每维上限 14)。取码只看 main;skill 在结果页作为「技能加成」单独展示。选此方案而非「P4×0.5 再并入」:前者是单层整数累加 + 一个 id<=180 判断,无浮点、无取整歧义,纯 PHP 里最干净最快。

连带:霍兰德雷达轴 fullMark 由 50 改为 30;scores_json 形如 {"main":{…},"skill":{…}};answers_json 仍存全部 192 题原始作答(不变)。

依据:scoring_rules.md §1.3.1(P4 阶段的实现细则以此为准)。

  • 结果码 → 文案:霍兰德先查 hue[code](缺失走 §1.1-2 降级链),再叠加 holland[dim] 的 traits/jobs;
  • 舒伯 → super[dim] 的 desc/advice;大五 → big_five[dim] 的 desc + high/low;
  • 作答完整性校验:缺答比例超阈值即拒绝提交并回显缺题号(服务端校验,信任边界在服务端)。
  • 唯一依据(D4):P4 严格按 P1 的 scoring_rules.md 实现,代码内不留第二套规则。
  • 积分(D7):不调用 Points::award();提交成功只写 cc_records 一行。

5.4 结果页

  • 雷达图:霍兰德 6 轴(main 池,fullMark=30)/ 大五 5 轴(fullMark=100)/ 舒伯 15 轴(改横向条形,避免雷达图退化)。用内联 SVG,颜色/描边走 --mn-;坐标数据经 data- 传给 script.js 绘制,或由 PHP 直接生成 <svg> 节点(均符合零内联)。
  • 文案区:性格描述 + 特质标签 + 职业推荐列表(精准职业 hue / 大学专业 majors_single∪majors_pair 去重取前 12 / 行动建议 action_plan / 维度详解 holland.jobs / super.advice)。
  • 技能加成区块(A-2 新增):霍兰德结果页单列一区,展示 skill 池每维值(上限 14),不参与取码/排序。
  • 历史记录入口 + 「再测一次」。
  • 容器统一 .cc-wrap { max-width: 1080px; margin: 0 auto; padding: 16px 20px 24px },窄屏收窄不归零;相邻卡片间距由容器 > + 统一(§6.4-2/5)。

5.5 部署与安全(C 组,硬要求)

  1. data/ 目录整目录 deny(双配):题库/字典/独立库都在 plugins/career_compass/data/ 下,禁止被 web 直读。
  • Apache:data/.htaccess 写 Require all denied(Plugin::db() 首次调用会自动补写,但我们仍显式落盘一份,防目录被清)。
  • nginx:在实际生效的 D:/phpstudy_pro/WWW/nginx-server.conf 内确认/补齐 location ~ ^/plugins/[^/]+/data/ { deny all; }(已有该规则,需实测验证)。
  • 前端只走 GET /career-compass/api/questions?type=... 取题库,不得让浏览器直接请求 data/*.json。
  1. data/career_compass.sqlite 必须挡死:不得通过任何 URL 命中;activate() 内做可直读自检——若探测到 data/ 可被匿名读取(如 HTTP 200 返回 JSON/SQLite 魔数),error_log() 告警(不阻断激活,仅记录)。
  2. 结果页 IDOR 双校验:查记录一律 WHERE id = :id AND user_id = :currentUserId,查不到即 404;mine 列表同样只按当前 user_id 过滤,杜绝横向越权。
  3. 分享功能暂不做:结果页不预留 share_token 等字段;cc_records 不加该列(将来要做再单开迁移)。

6. 决策定案记录(7/7 已定,P1 无待定项)

v1 的 7 个待确认项已于 2026-10-19 全部定案,口径以 §0.1 为准;此处只列落地动作,避免两处口径漂移。

#落地动作
D1loadData() 读 plugins/career_compass/data/*.json;不迁 assets/data/(转投稿时再迁,见 §0.1 迁移备注)。
D2官方自行发布、不投稿;不做后台管理页 → permissions: []、不注册 admin_route_register;「我的记录」页 mine 做。
D3清洗规则第 7 条保序 + §2.6 保序断言;渲染层不再混排——按 questions.json 原顺序切段(霍兰德 60/60/60/12、速测单页、舒伯 10/页、大五 5/页;订正 v1 的「48 题分页」;「先打散、后切页」表述作废,每段已天然混排)。
D4P1 落 scoring_rules.md 并写死实测阈值 >50(0–100 百分比)(原站阈值已抄到,3.5 兜底未启用);P4 按该文件实现,代码内不留第二套规则。
D5DDL 含 answers_json;提交时 json_encode + 参数绑定入库。
D6入口页/答题页/取题 API/提交/结果页/我的记录全部 requireLogin();提交接口顺序为 requireLogin() → Csrf::verifyOrDie()。
D7不调 Points::award();完成后仅写 cc_records。


7. 阶段计划

阶段内容交付/验证状态
P1-a一次性提取脚本 + 计数断言11 个结构提取成功、断言全绿、抽检 20 题一致✅ 已落地(report.txt)
P1-bquestions.json / results.json + scoring_rules.md(P1 硬交付,D4)JSON 合法、无 BOM、schema 与 §2.2/2.3 一致;保序断言全绿(D3);阈值结论已写死、无「待定」✅ 已落地(questions 54,418B / results 53,480B;阈值 >50)
P1-cPlugin.php 骨架(db/ddl/activate/deactivate/uninstall/loadData/loadQuestions + class_alias)php -l 通过;激活后 cc_records(含 answers_json)建表成功;init_after 兜底可用✅ 已落地(php -l 7/7 通过;激活断言 21/21)
P1-dplugin.json(icon: compass、permissions: [])+ hook/+views/+assets/+lang/ 骨架钩子名合法(审计无新增差异);plugin_case_lint.py 通过;i18n 三语一致✅ 已落地(lint / i18n / var_cjk / missing_use 全过)
P2入口页 + 路由 + 导航入口(6 条路由均登录态,D6)未登录访问任一入口均跳 loginUrl();三处导航入口(PC 左栏/顶栏/移动端)双 include 回归通过;页面无 &#xf128;待开工(P1 已留就绪闸:控制器落地前不出现入口/路由)
P3答题页(取题 API + 渲染 + 进度 + 原顺序切段 D3)零内联三查零命中;192/60/60/120 四种题库均可加载;分页 60/60/60/12(及速测单页/舒伯 10/页/大五 5/页)正确待开工
P4算分(按 scoring_rules.md,D4)+ 落库(含 answers_json,D5)+ 结果页(雷达图)四个量表各跑一遍真实答卷,得分与人工核对一致;IDOR 校验有效待开工
P5我的记录页(D2 保留) / 出包(官方自行发布、不投稿)fh_i18n_check、php_var_cjk_check、php_missing_use_check 全过;包内 data/ 保留(D1)待开工

P1 落地实况(2026-10-19):plugins/career_compass/ 现含 Plugin.php、plugin.json、hook/{init_after,route_register,nav_plugin_links}.php、lang/{zh,en,zh_tw}.php、assets/{style.css,script.js}、views/(空)、data/{questions,results}.json + career_compass.sqlite(激活时建)。

一键复验:php .workbuddy-ai/tmp/cc_extract/verify_p1.php(24 断言全绿)。

P2 前的刻意留白:hook/route_register.php 与 hook/nav_plugin_links.php 各带一道 class_exists('\Plugin\CareerCompass\FrontController') 就绪闸 —— 控制器未落地(P2)前不注册路由、不出现导航入口,避免中途出现「点了即 500」的按钮;P2 建好控制器后自动生效(闸门随之恒真,可保留)。


8. 出包前自检清单(沿用项目既有工具)

  • python .workbuddy-ai/tools/plugin_case_lint.py plugins/career_compass(命名空间逐字节匹配)
  • php .workbuddy-ai/tools/fh_i18n_check.php plugins/career_compass(语言键零缺失)
  • php .workbuddy-ai/tools/php_var_cjk_check.php plugins/career_compass(全角标点紧跟变量名)
  • php .workbuddy-ai/tools/php_missing_use_check.php plugins/career_compass(同命名空间漏 use → 运行期 500)
  • grep -nE '<style|<script>|on(click|change|submit|load)=' plugins/career_compass/views plugins/career_compass/hook → 零命中
  • php .workbuddy-ai/tools/plugin_hook_audit.php(钩子名与 §9.1 表双向对账)
  • 双 include 回归:php -r 'include $f; include $f;'(针对 nav_plugin_links)
  • plugin.json 的 icon: compass 已 grep 命中 | version 三处一致
  • 发布(D1/D2):官方自行发布、不投稿 → 包内保留 data/(两个 JSON + SQLite)。

build_submit_zip.py / verify_submit_zip.php 仅作未来转投稿的预案:投稿路径会自动排除 data/,届时须先按 §0.1 迁移备注把两个 JSON 迁到 assets/data/

轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 浏览 3 次 | 回复 0 次

全部回复 (0)

暂无回复

请 登录