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.
53 lines
1.8 KiB
JavaScript
53 lines
1.8 KiB
JavaScript
/**
|
||
* 善缘服务站(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,
|
||
};
|