Files
danyang-match/miniapp/services/serviceStationService.js
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

53 lines
1.8 KiB
JavaScript
Raw Permalink 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.
/**
* 善缘服务站(U50)服务层。
*
* Phase 19:U50 的内容清单包含「临时公告 / 开放时间 / 服务项目 / 预约到站」,
* 但公告此前**全仓没有任何存储与接口**,页面也无落点。本轮补齐后端
* (V042 + 运营端五个端点 + 用户端只读端点)后由此接入。
*
* 分层:按 services/index.js 的约定,页面不直接 require utils/api,
* 统一从本模块取数据(本站点主体信息来自首页聚合,公告来自服务站公告端点)。
*
* 契约:
* GET /home → 当前服务站主体信息
* GET /service-locations/{id}/announcements → 仅 status=published 且未过期
* 草稿/归档/过期由服务端过滤,前端不得再做"猜测式"过滤,
* 否则等于把未定稿内容拿到客户端。
*/
const api = require('../utils/api');
/**
* 当前展示的服务站(首页聚合给出的唯一站点)。
* @returns {Promise<object|null>}
*/
async function getStation() {
const home = await api.get('/home');
return (home && home.serviceLocation) || null;
}
/**
* 某服务站的可见临时公告。
*
* 公告属**增强信息**:请求失败时返回空数组而不抛出,
* 避免"公告接口挂了导致整个服务站详情页变成错误页"。
* @param {string} locationId
* @returns {Promise<Array<{id: string, title: string, content?: string}>>}
*/
async function getVisibleAnnouncements(locationId) {
if (!locationId) {
return [];
}
try {
const list = await api.get(`/service-locations/${encodeURIComponent(locationId)}/announcements`);
return Array.isArray(list) ? list : [];
} catch (err) {
console.warn('加载服务站公告失败', err);
return [];
}
}
module.exports = {
getStation,
getVisibleAnnouncements,
};