Ver código fonte

fix: handle missing teacher income fields without estimated totals

彭峰 6 horas atrás
pai
commit
3ce6b39b2e

+ 4 - 0
docs/features/teacher-earnings-2026-10-15.md

@@ -59,6 +59,10 @@ p75 查询阶段下降约 71%。这是本地 SQL 和序列化测量,排除了
 
 仍需关注:累计课酬读取规范化工资来源及历史规则,特定学员复习仍可能等待旧数据同步;本轮未将旧同步改成无监控的后台任务,也未宣称这两条路径已提速。
 
+## 收入字段兼容修正
+
+2026-10-15 再次只读检查,线上仍为 `2026-10-14-member-credit-v11`。旧版统计仅提供按课次数估算的 `t_yongji`,不能作为真实总收入。首页优先显示 `totalIncome`,或将有效的实际 `lessonIncome` 与 `reviewIncome` 按分合计;两者均不可用时明确显示“暂不可用”,不再只显示横线。真实金额仍依赖兼容数据库迁移与收入网关发布;当前执行环境未配置发布主密钥及管理员会话,未执行发布。
+
 ## 线上只读核对
 
 2026-10-15 11:43 北京时间只读检查公开 migration_status:HTTP 200,现网仍为 `2026-10-14-member-credit-v11`,尚无 app_review_complete。只检查版本,不触碰账号或业务数据。

+ 1 - 1
projects/xiaoshu-mobile/src/app/features/home/home-page.component.html

@@ -20,7 +20,7 @@
           <div><strong>{{ statsLoading() ? '—' : (stats()['t_tiyan'] || 0) }}</strong><span>体验课</span></div>
           <div><strong>{{ statsLoading() ? '—' : (stats()['t_total'] || 0) }}</strong><span>上课总次数</span></div>
         </div>
-        <div class="coach-footer"><span>累计时长 {{ statsLoading() ? '—' : (stats()['t_shichang'] || 0) }}</span><span class="commission">累计收入 <span class="commission-amount">{{ commissionHidden() ? '***' : (statsLoading() || stats()['totalIncome'] == null ? '—' : '¥' + (stats()['totalIncome'] | number:'1.2-2')) }}</span><button type="button" (click)="commissionHidden.set(!commissionHidden())" [attr.aria-label]="commissionHidden() ? '显示累计收入' : '隐藏累计收入'"><lucide-icon [img]="commissionHidden() ? icons.EyeOff : icons.Eye" [size]="20" /></button></span></div>
+        <div class="coach-footer"><span>累计时长 {{ statsLoading() ? '—' : (stats()['t_shichang'] || 0) }}</span><span class="commission">累计收入 <span class="commission-amount">{{ commissionHidden() ? '***' : (statsLoading() ? '加载中' : totalIncome() === null ? '暂不可用' : '¥' + (totalIncome() | number:'1.2-2')) }}</span><button type="button" (click)="commissionHidden.set(!commissionHidden())" [attr.aria-label]="commissionHidden() ? '显示累计收入' : '隐藏累计收入'"><lucide-icon [img]="commissionHidden() ? icons.EyeOff : icons.Eye" [size]="20" /></button></span></div>
       }
     </section>
 

+ 13 - 0
projects/xiaoshu-mobile/src/app/features/home/home-page.component.ts

@@ -32,6 +32,19 @@ export class HomePageComponent implements OnInit, OnDestroy {
   readonly balanceLoading=signal(false);
   readonly balanceRefreshedAt=signal('');
   readonly commissionHidden = signal(true);
+  readonly totalIncome = computed(() => {
+    const amount = (value: unknown): number | null => {
+      if ((typeof value !== 'number' && typeof value !== 'string') || String(value).trim() === '') return null;
+      const parsed = Number(value);
+      return Number.isFinite(parsed) ? parsed : null;
+    };
+    const stats = this.stats();
+    const total = amount(stats['totalIncome']);
+    if (total !== null) return total;
+    const lesson = amount(stats['lessonIncome']), review = amount(stats['reviewIncome']);
+    // The legacy t_yongji is a fixed-rate estimate, not confirmed earnings.
+    return lesson !== null && review !== null ? (Math.round(lesson * 100) + Math.round(review * 100)) / 100 : null;
+  });
   readonly antiForgetItems = signal<Item[]>([]);
   readonly antiForgetLoading = signal(false);
   readonly antiForgetError = signal('');

+ 11 - 2
scripts/check-teacher-earnings-browser.mjs

@@ -12,7 +12,7 @@ const browser=await chromium.launch({headless:true,executablePath:'/Applications
 const checks=[],errors=[],calls=[];
 const day=new Date().toLocaleDateString('sv-SE',{timeZone:'Asia/Shanghai'}),yesterday=new Date(Date.now()-86400000).toLocaleDateString('sv-SE',{timeZone:'Asia/Shanghai'});
 const tasks=[{GeneralID:102,xxjlid:900,kcid:801,yhid:100,honeyName:'测试学员',kc_title:'高中单词课',kywrq:day,kywsj:day+' 09:00',fxzt:0},{GeneralID:103,xxjlid:900,kcid:801,yhid:100,honeyName:'测试学员',kc_title:'高中单词课',kywrq:yesterday,kywsj:yesterday+' 09:00',fxzt:0}];
-let completed=false,failReview=false;
+let completed=false,failReview=false,incomeMode='total';
 try{
  const context=await browser.newContext({viewport:{width:390,height:844}});
  await context.addInitScript(()=>localStorage.setItem('xiaoshu.angular.session',JSON.stringify({token:'r:local-fixture-only',user:{userId:300,groupId:3,honeyName:'测试老师'},student:{userId:100,groupId:1,honeyName:'测试学员'}})));
@@ -21,7 +21,8 @@ try{
   if(!u.pathname.startsWith('/cloud-functions/'))return route.continue();
   const {params:p}=route.request().postDataJSON();calls.push(p);let result={};
   if(p.action==='e_order_tongji')result={t30:1,t60:2,t_total:3,t_shichang:2.5,lessonIncome:120,reviewIncome:completed?3:2,totalIncome:completed?123:122,pendingPayCount:1};
-  else if(p.action==='app_live_versions')result={version:String(completed),scopeVersions:{},refreshedAt:new Date().toISOString(),syncStatus:{state:'current'}};
+  if(p.action==='e_order_tongji'&&incomeMode!=='total'){delete result.totalIncome;if(incomeMode==='legacy'){delete result.lessonIncome;delete result.reviewIncome;result.t_yongji='7800';}if(incomeMode==='zero'){result.lessonIncome=0;result.reviewIncome=0;}}
+  if(p.action==='app_live_versions')result={version:String(completed),scopeVersions:{},refreshedAt:new Date().toISOString(),syncStatus:{state:'current'}};
   else if(p.action==='app_review_page'){
    if(failReview)return route.fulfill({status:500,json:{retcode:-1,retmsg:'模拟读取失败'}});
    const items=p.scope==='today'?tasks.slice(0,1):p.scope==='overdue'?tasks.slice(1):p.status==='completed'?tasks.filter(t=>t.fxzt===1):tasks.filter(t=>!p.date||t.kywrq===p.date);
@@ -58,6 +59,14 @@ try{
  await page.goto(origin+'/pages/member/ke_xiao');await page.locator('.lesson-pay').first().waitFor();assert.match(await page.locator('body').innerText(),/带课工资 ¥45\.00/);assert.match(await page.locator('body').innerText(),/带课工资 ¥0\.00/);assert.match(await page.locator('body').innerText(),/工资待核对/);assert.ok(await page.locator('.tabbar').isVisible());
  for(const width of [390,360]){await page.setViewportSize({width,height:844});assert.ok(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+1));assert.ok(await page.locator('.schedule-copy').first().evaluate(e=>e.getBoundingClientRect().width>=80));await page.screenshot({path:out+'/history-'+width+'.png',fullPage:true});}
  checks.push('销课45元、合法零元、待核对分开显示;360/390宽度无横向溢出且固定导航可见');
+ for(const mode of ['legacy','split','zero']){
+  incomeMode=mode;await page.goto(origin+'/pages/home/index');await page.locator('.coach-memory-list article').waitFor();
+  await page.getByRole('button',{name:'显示累计收入'}).click();
+  const expected=mode==='legacy'?'暂不可用':mode==='zero'?'¥0.00':'¥123.00';
+  await page.waitForFunction(value=>document.querySelector('.commission-amount')?.textContent?.trim()===value,expected);
+  if(mode==='legacy')assert.doesNotMatch(await page.locator('.commission').innerText(),/7800/);
+ }
+ incomeMode='total';checks.push('旧接口缺金额明确显示不可用,不展示估算佣金;实际收入分项可合计,合法零元显示正常');
  failReview=true;await page.goto(origin+'/pages/home/index');await page.getByText('今天的复习任务已全部完成').waitFor();await page.getByRole('button',{name:'刷新抗遗忘任务',exact:true}).click();await page.getByText('抗遗忘计划暂时无法加载,点击重试').waitFor();assert.equal(await page.getByText('今天没有复习任务').count(),0);checks.push('接口失败不会显示没有任务');
  assert.deepEqual(errors,[]);writeFileSync(out+'/browser-result.json',JSON.stringify({passed:true,environment:'local build + isolated fixture API, not live backend or WeChat',checks,requests:calls.length,errors},null,2));console.log(JSON.stringify({passed:true,checks},null,2));
 }finally{await browser.close();await new Promise(r=>server.close(r));}