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

81 lines
2.5 KiB
JavaScript
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.
/**
* 服务预约(U51 服务预约 / U52 我的预约)
*
* 归属与契约边界(Phase 19):
* - 用户端只能读写本人预约:详情/列表/取消全部由服务端按登录主体过滤,
* 越权访问一律返回空或 404,前端不得以任何参数指定他人 userId。
* - 提交预约**只传服务内容**。来源(source)由服务端强制为 user_self,
* 前端禁止传 requestSource / createdByOperatorId / userId(伪造运营代登记身份)。
* - 运营代登记走独立端点(后端 /service-appointments/operator/register),前端不在本层暴露。
*/
const api = require('../utils/api');
/**
* 读取当前服务站点(U51 预约上下文)
*
* 服务站页(U50)跳转时会带 `?serviceLocationId=xxx`,本方法用于把该 ID 还原成
* 可展示的站点名。仅**展示用途**:真正的归属由服务端按登录主体校验,
* 展示失败(无站点 / 接口异常)不影响提交,返回 null。
* @returns {Promise<{id: string, name: string} | null>}
*/
async function getCurrentServiceLocation() {
try {
const home = await api.get('/home');
const location = home && home.serviceLocation;
if (!location || !location.id) {
return null;
}
return { id: location.id, name: location.name || '已选服务站点' };
} catch (err) {
return null;
}
}
/**
* 我的预约列表
* @returns {Promise<Array<object>>}
*/
function getMyAppointments() {
return api.get('/service-appointments');
}
/**
* 预约详情(越权由服务端返回 404)
* @param {string} id
*/
function getAppointmentDetail(id) {
return api.get(`/service-appointments/${id}`);
}
/**
* 提交预约
* @param {{ serviceLocationId?: string, serviceType: string, preferredDate: string,
* preferredTimeSlot?: string, userNote?: string }} payload
*/
function createAppointment(payload) {
return api.post('/service-appointments', {
serviceLocationId: payload.serviceLocationId,
serviceType: payload.serviceType,
preferredDate: payload.preferredDate,
preferredTimeSlot: payload.preferredTimeSlot,
userNote: payload.userNote,
});
}
/**
* 取消我的预约
* @param {string} id
* @param {string} [reason]
*/
function cancelAppointment(id, reason) {
return api.post(`/service-appointments/${id}/cancel`, { reason: reason || '' });
}
module.exports = {
getCurrentServiceLocation,
getMyAppointments,
getAppointmentDetail,
createAppointment,
cancelAppointment,
};