jiangsu-kaogong 1.2.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/index.js +177 -111
  2. package/package.json +1 -1
package/bin/index.js CHANGED
@@ -472,50 +472,163 @@ program
472
472
  const interviewScore = parseFloat(body.interview) || 0;
473
473
  const finalScore = (totalScore / 2) * 0.5 + interviewScore * 0.5;
474
474
 
475
- // 读取内置的盐城岗位数据
476
- let positions = [];
477
- try {
478
- const dataPath = path.join(__dirname, '..', 'data', 'positions.json');
479
- if (fs.existsSync(dataPath)) {
480
- positions = JSON.parse(fs.readFileSync(dataPath, 'utf8'));
481
- }
482
- } catch (e) {}
475
+ // 检查是否上传了文件
476
+ const hasFiles = req.files && (req.files.positions || req.files.cutoffs || req.files.interviews);
483
477
 
484
- // 真实筛选结果 - 2026盐城市A类岗位
485
- const allPositions = [
486
- // 🔴 冲档(15个)
487
- { rank: '冲', district: '射阳县', unit: '县司法局', position: '四明司法所四级主任科员及以下', minScore: 130, maxScore: 132.6, firstScore: 70.25, firstInterview: 75.5, gap: (70.25 - finalScore).toFixed(2), tag: '⭐', remark: '' },
488
- { rank: '冲', district: '射阳县', unit: '县人社局', position: '办公室四级主任科员及以下', minScore: 132.6, maxScore: 133.1, firstScore: 70.35, firstInterview: 73.5, gap: (70.35 - finalScore).toFixed(2), tag: '⭐', remark: '限女' },
489
- { rank: '冲', district: '射阳县', unit: '县自然资源和规划局', position: '兴桥自然资源所四级主任科员及以下(参照管理)', minScore: 129.7, maxScore: 133.6, firstScore: 70.46, firstInterview: 74.12, gap: (70.46 - finalScore).toFixed(2), tag: '⭐', remark: '限户籍' },
490
- { rank: '冲', district: '东台市', unit: '市政府办', position: '秘书八科四级主任科员及以下', minScore: 131.4, maxScore: 133.1, firstScore: 70.55, firstInterview: 75.4, gap: (70.55 - finalScore).toFixed(2), tag: '⭐', remark: '' },
491
- { rank: '冲', district: '东台市', unit: '市文广旅局', position: '文化旅游交流推广科四级主任科员及以下', minScore: 131.3, maxScore: 133.2, firstScore: 70.8, firstInterview: 75, gap: (70.8 - finalScore).toFixed(2), tag: '⭐', remark: '' },
492
- { rank: '冲', district: '阜宁县', unit: '县委社会工作部', position: '办公室一级科员', minScore: 130.7, maxScore: 134.7, firstScore: 70.975, firstInterview: 74.6, gap: (70.975 - finalScore).toFixed(2), tag: '⭐', remark: '限男' },
493
- { rank: '冲', district: '射阳县', unit: '县文广旅局', position: '办公室四级主任科员及以下', minScore: 131.3, maxScore: 135.6, firstScore: 71.005, firstInterview: 74.16, gap: (71.005 - finalScore).toFixed(2), tag: '⭐', remark: '限户籍' },
494
- { rank: '冲', district: '阜宁县', unit: '县发改委', position: '办公室一级科员', minScore: 127.9, maxScore: 136.3, firstScore: 71.425, firstInterview: 74.7, gap: (71.425 - finalScore).toFixed(2), tag: '', remark: '' },
495
- { rank: '冲', district: '射阳县', unit: '县人社局', position: '办公室四级主任科员及以下', minScore: 132.6, maxScore: 133.1, firstScore: 70.8, firstInterview: 74.6, gap: (70.8 - finalScore).toFixed(2), tag: '', remark: '限男' },
496
- { rank: '冲', district: '盐城市大丰区', unit: '区人大办', position: '备案审查科一级科员', minScore: 133.2, maxScore: 135, firstScore: 72.125, firstInterview: 77.2, gap: (72.125 - finalScore).toFixed(2), tag: '', remark: '' },
497
- { rank: '冲', district: '射阳县', unit: '县文广旅局', position: '办公室四级主任科员及以下', minScore: 131.3, maxScore: 135.6, firstScore: 72.615, firstInterview: 74.88, gap: (72.615 - finalScore).toFixed(2), tag: '', remark: '' },
498
- { rank: '冲', district: '响水县', unit: '县委统战部', position: '民族宗教工作科一级科员', minScore: 129.8, maxScore: 138.9, firstScore: 73.525, firstInterview: 77.6, gap: (73.525 - finalScore).toFixed(2), tag: '', remark: '限户籍' },
499
- { rank: '冲', district: '盐城市大丰区', unit: '区文广旅局', position: '公共文化科一级科员', minScore: 133.8, maxScore: 138.8, firstScore: 73.52, firstInterview: 78.44, gap: (73.52 - finalScore).toFixed(2), tag: '', remark: '' },
500
- { rank: '冲', district: '东台市', unit: '市委老干部局', position: '老干部活动中心四级主任科员及以下(参照管理)', minScore: 137.6, maxScore: 140, firstScore: 74.2, firstInterview: 78.4, gap: (74.2 - finalScore).toFixed(2), tag: '', remark: '' },
501
- { rank: '冲', district: '阜宁县', unit: '县委社会工作部', position: '办公室一级科员', minScore: 130.7, maxScore: 134.7, firstScore: 69.175, firstInterview: 76.6, gap: (finalScore - 69.175).toFixed(2), tag: '', remark: '限女' },
502
-
503
- // 🟡 稳档(6个)
504
- { rank: '稳', district: '东台市', unit: '市民政局', position: '救助管理站四级主任科员及以下(参照管理)', minScore: 123.1, maxScore: 130.6, firstScore: 69.54, firstInterview: 73.78, gap: (finalScore - 69.54).toFixed(2), tag: '⭐', remark: '限户籍' },
505
- { rank: '稳', district: '建湖县', unit: '县政府办', position: '总值班室四级主任科员及以下', minScore: 127, maxScore: 136.6, firstScore: 70.025, firstInterview: 73.9, gap: (finalScore - 70.025).toFixed(2), tag: '⭐', remark: '限男' },
506
- { rank: '稳', district: '滨海县', unit: '县司法局', position: '普法与依法治理科四级主任科员及以下', minScore: 124.1, maxScore: 133.1, firstScore: 71.325, firstInterview: 76.1, gap: (71.325 - finalScore).toFixed(2), tag: '', remark: '' },
507
- { rank: '稳', district: '盐城市大丰区', unit: '区自然资源和规划局', position: '草庙自然资源所一级科员(参照管理)', minScore: 124.5, maxScore: 127.2, firstScore: null, firstInterview: null, gap: null, tag: '', remark: '限户籍' },
508
- { rank: '稳', district: '盐城市盐都区', unit: '区供销合作总社(参照管理)', position: '资产管理科四级主任科员及以下', minScore: 125.1, maxScore: 129, firstScore: null, firstInterview: null, gap: null, tag: '', remark: '限户籍' },
509
- { rank: '稳', district: '响水县', unit: '响水工业经济区管委会', position: '党政办公室一级科员', minScore: 127.5, maxScore: 133, firstScore: null, firstInterview: null, gap: null, tag: '', remark: '限户籍' },
510
-
511
- // 🟢 保档(2个)
512
- { rank: '保', district: '滨海县', unit: '县自然资源和规划局', position: '五汛自然资源所四级主任科员及以下(参照管理)', minScore: 114, maxScore: 118.6, firstScore: 67.43, firstInterview: 75.56, gap: (finalScore - 67.43).toFixed(2), tag: '⭐', remark: '限户籍' },
513
- { rank: '保', district: '东台市', unit: '市自然资源和规划局', position: '唐洋自然资源所四级主任科员及以下(参照管理)', minScore: 114.4, maxScore: 126.8, firstScore: 68.45, firstInterview: 73.5, gap: (finalScore - 68.45).toFixed(2), tag: '⭐', remark: '限户籍' },
514
- ];
478
+ let allPositions = [];
479
+ let rushPositions = [];
480
+ let stablePositions = [];
481
+ let safePositions = [];
482
+ let noDataTip = '';
515
483
 
516
- const rushPositions = allPositions.filter(p => p.rank === '冲');
517
- const stablePositions = allPositions.filter(p => p.rank === '稳');
518
- const safePositions = allPositions.filter(p => p.rank === '保');
484
+ if (hasFiles) {
485
+ try {
486
+ // ========== 解析Excel职位表 ==========
487
+ if (req.files.positions && req.files.positions[0]) {
488
+ const posPath = req.files.positions[0].path;
489
+ const xlsx = require('xlsx');
490
+ const workbook = xlsx.readFile(posPath);
491
+ const sheetName = workbook.SheetNames[0];
492
+ const worksheet = workbook.Sheets[sheetName];
493
+ const excelData = xlsx.utils.sheet_to_json(worksheet, { header: 1 });
494
+
495
+ // 找到表头行(包含"地区"、"单位名称"等关键词的行)
496
+ let headerRowIndex = -1;
497
+ for (let i = 0; i < Math.min(excelData.length, 10); i++) {
498
+ const row = excelData[i].join('');
499
+ if (row.includes('地区') || row.includes('单位名称') || row.includes('职位名称') || row.includes('专业')) {
500
+ headerRowIndex = i;
501
+ break;
502
+ }
503
+ }
504
+
505
+ if (headerRowIndex >= 0) {
506
+ const headers = excelData[headerRowIndex].map(h => String(h || '').trim());
507
+ const districtIdx = headers.findIndex(h => h.includes('地区名称') || h.includes('地区') || h.includes('考区'));
508
+ const deptIdx = headers.findIndex(h => h.includes('单位名称') || h.includes('招录机关') || h.includes('部门'));
509
+ const posIdx = headers.findIndex(h => h.includes('职位名称') || h.includes('岗位名称') || h.includes('职位'));
510
+ const majorIdx = headers.findIndex(h => h.includes('专业') || h.includes('学科') || h.includes('专 业'));
511
+ const eduIdx = headers.findIndex(h => h.includes('学历') || h.includes('学 历'));
512
+ const otherIdx = headers.findIndex(h => h.includes('其它') || h.includes('其他') || h.includes('其 它'));
513
+ const categoryIdx = headers.findIndex(h => h.includes('考试类别') || h.includes('试卷类型'));
514
+ const countIdx = headers.findIndex(h => h.includes('招考人数') || h.includes('招录人数'));
515
+
516
+ // 解析数据行
517
+ for (let i = headerRowIndex + 1; i < excelData.length; i++) {
518
+ const row = excelData[i];
519
+ if (!row[deptIdx] || String(row[deptIdx]).trim() === '') continue;
520
+
521
+ const otherInfo = otherIdx >= 0 ? String(row[otherIdx] || '') : '';
522
+
523
+ const position = {
524
+ district: districtIdx >= 0 ? String(row[districtIdx] || '') : '',
525
+ unit: deptIdx >= 0 ? String(row[deptIdx] || '') : '',
526
+ position: posIdx >= 0 ? String(row[posIdx] || '') : '',
527
+ major: majorIdx >= 0 ? String(row[majorIdx] || '') : '',
528
+ education: eduIdx >= 0 ? String(row[eduIdx] || '') : '',
529
+ other: otherInfo,
530
+ political: (otherInfo.match(/党员|团员|群众/) || [''])[0],
531
+ isFresh: otherInfo.includes('应届') ? '是' : '否',
532
+ category: categoryIdx >= 0 ? String(row[categoryIdx] || '') : '',
533
+ count: countIdx >= 0 ? String(row[countIdx] || '') : '',
534
+ minScore: 0,
535
+ maxScore: 0,
536
+ firstScore: 0,
537
+ firstInterview: 0,
538
+ gap: 0,
539
+ rank: '稳',
540
+ tag: '',
541
+ remark: ''
542
+ };
543
+
544
+ // 根据用户条件过滤
545
+ let match = true;
546
+
547
+ // 地区过滤 - 只显示盐城地区
548
+ if (position.district) {
549
+ const yanchengDistricts = ['盐城', '亭湖', '盐都', '大丰', '响水', '滨海', '阜宁', '射阳', '建湖', '东台'];
550
+ const isYancheng = yanchengDistricts.some(d => position.district.includes(d));
551
+ if (!isYancheng) {
552
+ match = false;
553
+ }
554
+ }
555
+
556
+ // 学历过滤
557
+ if (body.education && body.education !== '本科及以上') {
558
+ if (position.education && !position.education.includes(body.education)) {
559
+ match = false;
560
+ }
561
+ }
562
+
563
+ // 专业匹配
564
+ if (body.major && position.major) {
565
+ const userMajor = String(body.major || '');
566
+ // 如果用户选了社会政治类,匹配社会政治类、公共管理类等
567
+ if (userMajor.includes('社会政治') || userMajor.includes('政治学')) {
568
+ const matchMajor = ['社会政治', '公共管理', '法学', '中文', '不限'].some(k =>
569
+ position.major.includes(k)
570
+ );
571
+ if (!matchMajor) {
572
+ match = false;
573
+ }
574
+ }
575
+ }
576
+
577
+ // 政治面貌过滤
578
+ if (body.political && body.political !== '不限') {
579
+ if (position.other && !position.other.includes('不限')) {
580
+ if (body.political === '群众' && (position.other.includes('党员') || position.other.includes('团员'))) {
581
+ match = false;
582
+ } else if (body.political === '团员' && position.other.includes('党员')) {
583
+ match = false;
584
+ }
585
+ }
586
+ }
587
+
588
+ // 考试类别过滤
589
+ if (body.category && position.category) {
590
+ if (position.category !== body.category) {
591
+ match = false;
592
+ }
593
+ }
594
+
595
+ // 应届过滤
596
+ if (body.freshman === '否' && position.isFresh === '是') {
597
+ match = false;
598
+ }
599
+
600
+ // 基层经验过滤(简单匹配)
601
+ if (body.experience === '是' && position.other) {
602
+ const hasExpReq = position.other.includes('年') || position.other.includes('工作经历');
603
+ if (!hasExpReq) {
604
+ // 用户有基层经验,但岗位没要求也算匹配
605
+ }
606
+ }
607
+
608
+ if (match) {
609
+ // 只使用Excel里的真实数据,不做任何模拟
610
+ allPositions.push(position);
611
+ }
612
+ }
613
+ }
614
+ }
615
+
616
+ // 分档逻辑去掉,只显示所有符合条件的岗位
617
+ rushPositions = allPositions;
618
+ stablePositions = [];
619
+ safePositions = [];
620
+
621
+ if (allPositions.length === 0) {
622
+ noDataTip = '📂 已解析文件,但未找到符合条件的岗位,请调整筛选条件';
623
+ }
624
+
625
+ } catch (err) {
626
+ console.error('解析错误:', err);
627
+ noDataTip = '❌ 解析文件时出错,请检查文件格式';
628
+ }
629
+ } else {
630
+ noDataTip = '📁 请先上传职位表Excel文件后再进行分析';
631
+ }
519
632
 
520
633
  res.send(`
521
634
  <!DOCTYPE html>
@@ -628,91 +741,44 @@ program
628
741
  </div>
629
742
  </div>
630
743
 
631
- <div class="card rank-rush">
632
- <h2>🔴 冲档(${rushPositions.length}个)— 与综合第一差距5分以内</h2>
633
- <table class="table">
634
- <thead>
635
- <tr><th>#</th><th>地区</th><th>单位 - 岗位</th><th>进面最低~最高</th><th>综合第一</th><th>综合第一面试</th><th>你与第一差距</th><th>备注</th></tr>
636
- </thead>
637
- <tbody>
638
- ${rushPositions.map((p, i) => `
639
- <tr>
640
- <td class="star">${p.tag}${i+1}</td>
641
- <td>${p.district}</td>
642
- <td>${p.unit} - ${p.position}</td>
643
- <td>${p.minScore}~${p.maxScore}</td>
644
- <td>${p.firstScore || '—'}</td>
645
- <td>${p.firstInterview || '—'}</td>
646
- <td><span class="badge badge-red">${p.gap !== null ? p.gap + ' 分' : '—'}</span></td>
647
- <td>${p.remark}</td>
648
- </tr>
649
- `).join('')}
650
- </tbody>
651
- </table>
744
+ ${noDataTip ? `
745
+ <div class="card" style="text-align: center; padding: 60px 40px;">
746
+ <div style="font-size: 48px; margin-bottom: 20px;">📁</div>
747
+ <h2 style="color: #667eea; margin-bottom: 15px;">${noDataTip}</h2>
748
+ <p style="color: #666;">上传你的25年/26年数据文件后,即可看到专属选岗分析结果</p>
652
749
  </div>
653
-
654
- <div class="card rank-stable">
655
- <h2>🟡 稳档(${stablePositions.length}个)— 笔试超进面线5~10分</h2>
750
+ ` : `
751
+ <div class="card rank-rush">
752
+ <h2>📋 符合条件的岗位(${allPositions.length}个)</h2>
656
753
  <table class="table">
657
754
  <thead>
658
- <tr><th>#</th><th>地区</th><th>单位 - 岗位</th><th>进面最低~最高</th><th>综合第一</th><th>综合第一面试</th><th>你与第一差距</th><th>备注</th></tr>
755
+ <tr><th>#</th><th>地区</th><th>单位</th><th>岗位</th><th>专业</th><th>学历</th><th>考试类别</th><th>其他条件</th></tr>
659
756
  </thead>
660
757
  <tbody>
661
- ${stablePositions.map((p, i) => `
758
+ ${allPositions.map((p, i) => `
662
759
  <tr>
663
- <td class="star">${p.tag}${i+1}</td>
760
+ <td>${i+1}</td>
664
761
  <td>${p.district}</td>
665
- <td>${p.unit} - ${p.position}</td>
666
- <td>${p.minScore}~${p.maxScore}</td>
667
- <td>${p.firstScore || '—'}</td>
668
- <td>${p.firstInterview || '—'}</td>
669
- <td><span class="badge badge-yellow">${p.gap !== null ? (parseFloat(p.gap) >= 0 ? '+' : '') + p.gap + ' 分' : '—'}</span></td>
670
- <td>${p.remark}</td>
762
+ <td>${p.unit}</td>
763
+ <td>${p.position}</td>
764
+ <td style="font-size: 12px; max-width: 150px;">${p.major}</td>
765
+ <td>${p.education}</td>
766
+ <td>${p.category}</td>
767
+ <td style="font-size: 11px; max-width: 200px;">${p.other}</td>
671
768
  </tr>
672
769
  `).join('')}
673
770
  </tbody>
674
771
  </table>
675
772
  </div>
676
773
 
677
- <div class="card rank-safe">
678
- <h2>🟢 保档(${safePositions.length}个)— 笔试超进面线10分以上</h2>
679
- <table class="table">
680
- <thead>
681
- <tr><th>#</th><th>地区</th><th>单位 - 岗位</th><th>进面最低~最高</th><th>综合第一</th><th>综合第一面试</th><th>你与第一差距</th><th>备注</th></tr>
682
- </thead>
683
- <tbody>
684
- ${safePositions.map((p, i) => `
685
- <tr>
686
- <td class="star">${p.tag}${i+1}</td>
687
- <td>${p.district}</td>
688
- <td>${p.unit} - ${p.position}</td>
689
- <td>${p.minScore}~${p.maxScore}</td>
690
- <td>${p.firstScore || '—'}</td>
691
- <td>${p.firstInterview || '—'}</td>
692
- <td><span class="badge badge-green">+${p.gap} 分</span></td>
693
- <td>${p.remark}</td>
694
- </tr>
695
- `).join('')}
696
- </tbody>
697
- </table>
774
+ <div class="card" style="display: none;">
775
+ <h2>🟡 稳档(0个)</h2>
698
776
  </div>
699
777
 
700
- <div class="card" style="border-left: 4px solid #ff4757;">
701
- <h2>❌ 未达线(1个)</h2>
702
- <table class="table">
703
- <thead>
704
- <tr><th>地区</th><th>单位 - 岗位</th><th>进面最低</th><th>差距</th></tr>
705
- </thead>
706
- <tbody>
707
- <tr>
708
- <td>盐城市盐都区</td>
709
- <td>区委老干部局 - 老干部活动中心四级主任科员及以下(参照管理)</td>
710
- <td>139.8</td>
711
- <td><span class="badge badge-red">差7.0分</span></td>
712
- </tr>
713
- </tbody>
714
- </table>
778
+ <div class="card" style="display: none;">
779
+ <h2>🟢 保档(0个)</h2>
715
780
  </div>
781
+ `}
716
782
 
717
783
  <div class="card">
718
784
  <h2>💡 报考策略建议</h2>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jiangsu-kaogong",
3
- "version": "1.2.0",
3
+ "version": "3.1.0",
4
4
  "description": "江苏省考选岗CLI工具 - 解析职位表、进面分数线,智能推荐岗位",
5
5
  "main": "index.js",
6
6
  "bin": {