Files
danyang-match/miniapp/pages/mine/mine.ts
T
WorkBuddy 72b4e9b846 fix(ops-backend/service-station): close Phase 19 KEEP-VERIFY — 25 defects (unreachable announcements + station write privilege escalation; banner 500 + soft-delete leak; content publish dead-end; program approval dead-end/draft leak/zero counts/missing withdraw; zero-coverage test schema) ; add V042 announcement chain + 34 guard tests; full regression 602 green
Backend
- Service Station: expose the 「临时公告」 chain end-to-end (V042 table/service existed
  but had ZERO endpoints -> both admin and miniapp could never reach it): 5 operator
  endpoints (list/create/update/publish/archive) + 1 user-facing read endpoint.
- Fix privilege escalation: create/update were guarded only by the class-level
  service_location.view, making V015's service_location.manage a never-used dead
  permission -> read-only roles could edit stations. Permissions moved to method level.
- Add ServiceLocationOperatorController to OperatorEndpointPermissionCoverageTest
  (it was NOT scanned, which is why the escalation stayed invisible).
- Add create/update validation (address NOT NULL + status CHECK only blew up on PG).
- Banner: validation failures threw bare RuntimeException -> 500; now VALIDATION_ERROR
  -> 400. Add deleted_at IS NULL to both home queries (soft-deleted banners stayed live).
- Content: operator list/detail split (admin «publish» was a dead end because it reused
  the published-only public API); category deactivate/reactivate; article body on edit.
- Service Program: operator member list now includes pending (approval reachable);
  active-only public visibility (draft leak); memberCount/pendingCount aggregation;
  withdraw/removeMember; reuse row on re-apply (UNIQUE(program_id,user_id) crash).
- MeController: add the missing GET /me/service-history route.

Schema / guards
- schema-h2.sql: add service_location_announcements (+ chk_announcement_status) and
  content_categories (+ uk_content_category_code, chk_category_status); move both into
  REQUIRED_IN_TEST_SCHEMA. MigrationValidationTest baseline 41 -> 42.
- V11PermissionMatrixContractTest now scans ALL migrations instead of hardcoding V015+V029.
- New: ServiceLocationAnnouncementPhase19IntegrationTest (24),
  ServiceLocationControllerPermissionContractTest (5),
  ServiceLocationAnnouncementConstraintParityTest (5, prod DDL <-> test schema <->
  backend whitelist <-> admin labels).

Frontend
- admin-web: ServiceStationView gains announcement management (statuses, create/edit,
  publish/archive with confirm, expiry); write buttons gated by service_location.manage.
- miniapp: U50 renders the 临时公告 block via new serviceStationService; page no longer
  talks to utils/api directly; announcement failure degrades to [] instead of erroring.
- ui-tests: U37 four-group + U50 announcement contracts; /home assertion retargeted to
  the service layer.

Verification
- Negative verification: 3 injected defects (drop soft-delete filter / drop test-schema
  CHECK / downgrade write endpoint to view) -> 4 named tests fail precisely, no
  collateral failures; restored, NEG-VAL residue = 0.
- Backend full regression: 602 tests, 0 failures, 0 errors, 3 skipped (baseline 420).
- admin-web: vue-tsc --noEmit EXIT=0, vite build EXIT=0. Miniapp: tsc --noEmit EXIT=0.
- ui-tests 14/14 PASS.
2026-09-17 22:21:31 +08:00

187 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 我的页 U30-U33
*
* 不变量(AGENTS.md):
* - 第 8 条:敏感数据默认拒绝——Profile 不展示身份证号/手机号/完整生日
* - 第 2 条:Required 不可自动放宽
* - 第 9 条:PostgreSQL 是 Source of Truth
*/
import { userStore, authStore, matchStore, appStore } from '../../stores';
import { ROUTE, DANGER_ACTION, RELATIONSHIP_STATUS, PROFILE_REVIEW } from '../../constants';
const RELATIONSHIP_TEXT: Record<string, string> = {
[RELATIONSHIP_STATUS.SEARCHING]: '正在寻找',
[RELATIONSHIP_STATUS.GETTING_TO_KNOW]: '正在了解',
[RELATIONSHIP_STATUS.PAUSED]: '已暂停寻找',
[RELATIONSHIP_STATUS.MATCHED_SUCCESSFULLY]: '已成功牵手',
[RELATIONSHIP_STATUS.SERVICE_EXITED]: '已退出服务',
};
Page({
data: {
isLoggedIn: false,
profile: null as any,
userState: null as any,
verificationStatus: 'not_submitted',
loadStatus: 'idle',
isRealNameVerified: false,
isProfileApproved: false,
reviewStatusText: '',
relationshipText: '',
canPause: false,
canResume: false,
likersCount: 0,
matchCount: 0,
showLogoutDialog: false,
logoutAction: DANGER_ACTION.LOGOUT,
loginActions: [{ text: '微信登录', event: 'login', variant: 'primary' }],
hasOperatorToken: false,
profileCompleteness: 0,
},
onLoad() {
(this as any)._unsubUser = userStore.subscribe((state: any) => this._syncFromUserStore(state));
this._syncFromUserStore(userStore.state);
(this as any)._unsubMatch = matchStore.subscribe((state: any) => {
this.setData({
likersCount: state.likersCount || 0,
matchCount: Array.isArray(state.mutualList) ? state.mutualList.length : 0,
});
});
},
onShow() {
if (authStore.state.isLoggedIn) {
userStore.fetchStatus().catch((err: any) => appStore.toastError(err));
userStore.fetchProfile().catch(() => {});
userStore.fetchVerificationStatus().catch(() => {});
matchStore.fetchLikersCount().catch(() => {});
matchStore.fetchMutualList().catch(() => {});
}
},
onUnload() {
if ((this as any)._unsubUser) (this as any)._unsubUser();
if ((this as any)._unsubMatch) (this as any)._unsubMatch();
},
_syncFromUserStore(state: any) {
const us = state.userState;
const isLoggedIn = !!us || authStore.state.isLoggedIn;
const isRealNameVerified = !!us && !!us.isRealNameVerified;
const reviewStatus = us ? us.profileReviewStatus : null;
const isProfileApproved = reviewStatus === PROFILE_REVIEW.APPROVED;
let reviewStatusText = '未提交';
if (reviewStatus === PROFILE_REVIEW.PENDING) reviewStatusText = '审核中';
else if (reviewStatus === PROFILE_REVIEW.APPROVED) reviewStatusText = '已审核';
else if (reviewStatus === PROFILE_REVIEW.NEED_EDIT) reviewStatusText = '需修改';
else if (reviewStatus === PROFILE_REVIEW.REJECTED) reviewStatusText = '未通过';
const rel = us ? us.relationshipStatus : null;
const relationshipText = rel ? RELATIONSHIP_TEXT[rel] || '' : '';
const canPause = rel === RELATIONSHIP_STATUS.SEARCHING || rel === RELATIONSHIP_STATUS.GETTING_TO_KNOW;
const canResume = rel === RELATIONSHIP_STATUS.PAUSED;
this.setData({
isLoggedIn,
profile: state.profile,
userState: us,
verificationStatus: state.verificationStatus,
loadStatus: state.loadStatus,
isRealNameVerified,
isProfileApproved,
reviewStatusText,
relationshipText,
canPause,
canResume,
profileCompleteness: state.profile?.profileCompletion || 0,
});
},
// ============ 登录 ============
onLoginAction(e: any) {
if ((e.detail || {}).event === 'login') wx.navigateTo({ url: ROUTE.AUTH.LOGIN });
},
// ============ 资料编辑 ============
onEditProfile() { wx.navigateTo({ url: ROUTE.ONBOARDING.PROFILE_INTRO }); },
onEditPreference() { wx.navigateTo({ url: ROUTE.ONBOARDING.PROFILE_PREFERENCE }); },
onEditPhotos() { wx.navigateTo({ url: ROUTE.ONBOARDING.PROFILE_PHOTOS }); },
// ============ 匹配 ============
onGoMessages() { wx.switchTab({ url: ROUTE.TAB.MESSAGES }); },
onGoLikers() { wx.navigateTo({ url: ROUTE.DETAIL.LIKERS }); },
// ============ 账户与安全 ============
onGoVerification() { wx.navigateTo({ url: ROUTE.AUTH.VERIFICATION }); },
onGoPrivacy() { wx.navigateTo({ url: ROUTE.SETTINGS.PRIVACY }); },
onGoBlacklist() { wx.navigateTo({ url: ROUTE.SETTINGS.BLACKLIST }); },
onGoAccountStatus() { wx.navigateTo({ url: ROUTE.SETTINGS.ACCOUNT_STATUS_PAGE }); },
// ============ 关系状态切换 ============
async onPause() {
try {
appStore.showLoading('处理中…');
await userStore.pauseSearching();
appStore.hideLoading();
appStore.toastSuccess('已暂停寻找');
} catch (err: any) { appStore.hideLoading(); appStore.toastError(err); }
},
async onResume() {
try {
appStore.showLoading('处理中…');
await userStore.resumeSearching();
appStore.hideLoading();
appStore.toastSuccess('已恢复寻找');
} catch (err: any) { appStore.hideLoading(); appStore.toastError(err); }
},
onGoFate() { wx.navigateTo({ url: ROUTE.MAIN.FATE_CENTER }); },
onGoMatchmaker() { wx.navigateTo({ url: ROUTE.MAIN.MATCHMAKER }); },
onGoCounseling() { wx.navigateTo({ url: ROUTE.SERVICE.COUNSELING }); },
onGoActivities() { wx.navigateTo({ url: ROUTE.DETAIL.MY_ACTIVITIES }); },
onGoAppointments() { wx.navigateTo({ url: ROUTE.SERVICE.MY_APPOINTMENTS }); },
onGoServiceHistory() { wx.navigateTo({ url: ROUTE.SERVICE.HISTORY }); },
onGoSafety() { wx.navigateTo({ url: ROUTE.SETTINGS.MY_REPORTS }); },
onGoClassroom() { wx.navigateTo({ url: ROUTE.SERVICE.CLASSROOM }); },
onGoAntiFraud() { wx.navigateTo({ url: ROUTE.SERVICE.ANTI_FRAUD }); },
onGoStation() { wx.navigateTo({ url: ROUTE.SERVICE.STATION }); },
// Phase 19:U49 公益服务中心此前只有 app.json 注册、全仓无任何入口(用户不可达)。
onGoServiceCenter() { wx.navigateTo({ url: ROUTE.SERVICE.CENTER }); },
// ============ 其他 ============
onGoSettings() { wx.navigateTo({ url: ROUTE.SETTINGS.SETTINGS }); },
onGoHelp() { wx.navigateTo({ url: ROUTE.SETTINGS.HELP_CENTER }); },
onGoAbout() { wx.navigateTo({ url: ROUTE.SETTINGS.ABOUT }); },
// ============ 身份切换 ============
onSwitchRole() {
const token = wx.getStorageSync('operatorToken');
if (token) wx.navigateTo({ url: ROUTE.OPERATOR.WORKBENCH });
else wx.navigateTo({ url: ROUTE.OPERATOR.LOGIN });
},
onLogout() { this.setData({ showLogoutDialog: true }); },
async onConfirmLogout() {
this.setData({ showLogoutDialog: false });
try {
appStore.showLoading('退出中…');
await authStore.logout();
appStore.hideLoading();
wx.reLaunch({ url: ROUTE.AUTH.LOGIN });
} catch (err: any) { appStore.hideLoading(); appStore.toastError(err); }
},
onCancelLogout() { this.setData({ showLogoutDialog: false }); },
});