feat: 办事大厅后端源码首次入库(事项/字典/表单/申报 + 流程回调)
build / build-push (push) Failing after 39s

This commit is contained in:
chenxing
2026-09-15 11:00:00 +08:00
parent 1eeb7a4276
commit d47951abd7
70 changed files with 7569 additions and 0 deletions
@@ -0,0 +1,76 @@
package com.yunhai.svchall.controller.api;
import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.svchall.model.svc.SvcModels.ApplySubmitRequest;
import com.yunhai.svchall.model.svc.SvcModels.ApplyVo;
import com.yunhai.svchall.service.impl.ServiceApplyService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.Map;
/**
* 用户面办件(/api/apply/**,CAS 登录):发起/重试/我的办件/详情。
* 审批动作(撤回/催办/轨迹)由前端直调流程中心 /api/flow/*(同主域 CAS cookie),
* 本控制器只管办事大厅侧的申请记录。
*/
@RestController
@RequestMapping("/api/apply")
@RequiredArgsConstructor
public class ApplyUserController {
private final ServiceApplyService applyService;
/** 发起(表单数据强校验 → 快照落库 → 代发起;流程中心未接入时落待提交态) */
@PostMapping
public ApplyVo submit(BoaUser authUserWithDept, @Valid @RequestBody ApplySubmitRequest request) {
return applyService.submit(authUserWithDept, request);
}
/** 重试(仅待提交态;复用同一 bizId,流程中心 uk 幂等) */
@PostMapping("/{id}/retry")
public ApplyVo retry(BoaUser authUserWithDept, @PathVariable("id") Integer id) {
return applyService.retry(authUserWithDept, id);
}
/** 一键重提(仅驳回/撤回态;复用同一 bizId,flow-center R4 重启推进,轨迹跨轮连续) */
@PostMapping("/{id}/resubmit")
public ApplyVo resubmit(BoaUser authUserWithDept, @PathVariable("id") Integer id) {
return applyService.resubmit(authUserWithDept, id);
}
/** 我的办件 */
@GetMapping("/my")
public Map<String, Object> my(BoaUser authUserWithDept,
@RequestParam(value = "flowStatus", required = false) Integer flowStatus,
@RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "pageSize", required = false) Integer pageSize) {
return applyService.myApplies(authUserWithDept, flowStatus, page, pageSize);
}
/** 详情(提交人本人或站点管理面) */
@GetMapping("/{id}")
public ApplyVo detail(BoaUser authUserWithDept,
@PathVariable("id") Integer id,
@RequestParam(value = "siteCode", required = false) String siteCode) {
return applyService.detail(authUserWithDept, siteCode, id);
}
/**
* 发起页数据(CAS,本校用户):事项发布态要点 + 绑定表单的已发布 schema(前端渲染动态表单)。
* 未发布/不存在报业务错;未绑流程报"未开通在线办理"。
*/
@GetMapping("/item")
public java.util.Map<String, Object> applyItem(BoaUser authUserWithDept,
@RequestParam("siteCode") String siteCode,
@RequestParam("itemCode") String itemCode) {
return applyService.applyItem(authUserWithDept, siteCode, itemCode);
}
}
@@ -0,0 +1,80 @@
package com.yunhai.svchall.controller.api;
import com.yunhai.platform.openapi.client.YschBaseOpenApi;
import com.yunhai.platform.utils.model.BoaModule;
import com.yunhai.platform.utils.model.BoaUser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 平台代理域接口(Notice/CMS BaseApiController 同款契约):GetUserInfo / GetUserMod。
* GetUserInfo 直接返回 CAS 会话注入的 BoaUser;GetUserMod 经 Tier1 SDK 取当前用户在
* 本应用(appKey 按 yh.open 身份)下已授权模块并组树,是前端左侧菜单与页头用户信息的数据源。
* 返回裸对象,由统一响应包装为 {code, msg, data}。
*/
@RestController
@RequestMapping("/api/base")
public class BaseApiController {
/** 当前登录用户信息(页头用户名/头像) */
@GetMapping("GetUserInfo")
public BoaUser getUserInfo(BoaUser authUserWithDept) {
return authUserWithDept;
}
/** 当前用户已授权模块列表(左侧菜单树) */
@GetMapping("GetUserMod")
public List<Map<String, Object>> getUserMod(BoaUser authUserWithDept) {
Integer schId = authUserWithDept.getCurrentRole() != null
? authUserWithDept.getCurrentRole().getSchId() : null;
List<BoaModule> modules = YschBaseOpenApi.getUserModule(authUserWithDept.getId(), schId);
if (modules == null || modules.isEmpty()) {
return Collections.emptyList();
}
return buildModuleTree(modules);
}
/** 扁平模块列表组树(Notice UserCenterService.buildModuleTree 同构:modId/page/children) */
private static List<Map<String, Object>> buildModuleTree(List<BoaModule> modules) {
Map<Integer, BoaModule> moduleMap = new LinkedHashMap<>();
for (BoaModule m : modules) {
moduleMap.put(m.getId(), m);
}
Map<Integer, Map<String, Object>> nodeMap = new LinkedHashMap<>();
for (BoaModule m : modules) {
Map<String, Object> node = new LinkedHashMap<>();
node.put("id", m.getId());
node.put("modId", m.getId());
node.put("name", m.getName());
node.put("parentId", m.getParentId() != null ? m.getParentId() : 0);
node.put("page", m.getPath() != null ? m.getPath() : "");
node.put("level", m.getLevel() != null ? m.getLevel() : 0);
node.put("icon", m.getIcon() != null ? m.getIcon() : "");
node.put("orderNumber", m.getSortNo() != null ? m.getSortNo() : 0);
node.put("usable", true);
node.put("children", new ArrayList<>());
nodeMap.put(m.getId(), node);
}
List<Map<String, Object>> roots = new ArrayList<>();
for (BoaModule m : modules) {
Map<String, Object> node = nodeMap.get(m.getId());
int parentId = m.getParentId() != null ? m.getParentId() : 0;
if (parentId == 0 || !moduleMap.containsKey(parentId)) {
roots.add(node);
} else {
@SuppressWarnings("unchecked")
List<Map<String, Object>> children =
(List<Map<String, Object>>) nodeMap.get(parentId).get("children");
children.add(node);
}
}
return roots;
}
}
@@ -0,0 +1,49 @@
package com.yunhai.svchall.controller.api;
import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.svchall.entity.ServiceAttachment;
import com.yunhai.svchall.service.impl.AttachmentService;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* 文件上传(/api/file/upload,Notice 上传通道契约):完整 URL 直接返回(直连访问),
* DB 存相对段;业务绑定(指南附件/办件材料)由保存与提交动作完成。
*/
@RestController
@RequestMapping("/api/file")
@RequiredArgsConstructor
public class FileController {
private final AttachmentService attachmentService;
@PostMapping("/upload")
public UploadVo upload(BoaUser authUserWithDept,
@RequestParam("file") MultipartFile file,
@RequestParam(value = "type", defaultValue = "file") String type,
@RequestParam(value = "siteCode", required = false) String siteCode) {
ServiceAttachment attachment = attachmentService.upload(authUserWithDept, siteCode, file, type);
UploadVo vo = new UploadVo();
vo.setId(attachment.getId());
vo.setFileName(attachment.getFileName());
vo.setFileUrl(com.yunhai.svchall.utils.FileUtil.buildUrl(attachment.getFileUrl()));
vo.setFileSize(attachment.getFileSize());
vo.setMimeType(attachment.getMimeType());
return vo;
}
/** 上传返回体(fileUrl 为完整可达地址) */
@Data
public static class UploadVo {
private Integer id;
private String fileName;
private String fileUrl;
private Long fileSize;
private String mimeType;
}
}
@@ -0,0 +1,46 @@
package com.yunhai.svchall.controller.api;
import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.svchall.service.SiteDirectory;
import com.yunhai.svchall.service.SiteDirectory.SiteInfo;
import com.yunhai.svchall.service.SiteGuard;
import com.yunhai.svchall.service.impl.ServiceFormService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 站点选项(/api/svc/site/options):管理端站点选择器数据源,代理 CMS 站点目录
* (/oapi/site/list,本地缓存 5 分钟)。当前用户角色学校口径;超管同样按其角色学校下发
* (跨校站点管理走超管在该校的角色,与 CMS requireManageable 的使用方式一致)。
*/
@RestController
@RequestMapping("/api/svc/site")
@RequiredArgsConstructor
public class SiteOptionsController {
private final SiteDirectory siteDirectory;
private final SiteGuard siteGuard;
private final ServiceFormService formService;
@GetMapping("/options")
public List<com.yunhai.svchall.model.svc.SvcModels.SiteOptionVo> options(BoaUser authUserWithDept) {
if (authUserWithDept == null || authUserWithDept.getCurrentRole() == null
|| authUserWithDept.getCurrentRole().getSchId() == null) {
throw new com.yunhai.platform.apistd.BizException(
com.yunhai.platform.utils.exception.ErrorCode.PARAM_ERROR, "当前用户无学校归属");
}
Long schId = authUserWithDept.getCurrentRole().getSchId().longValue();
return siteDirectory.listSites(schId);
}
/** 站点解析校验(编辑页初始化时前端可调,确保站点可达且有权) */
@GetMapping("/check")
public SiteInfo check(BoaUser authUserWithDept, @RequestParam("siteCode") String siteCode) {
return siteGuard.requireSiteManager(authUserWithDept, siteCode);
}
}
@@ -0,0 +1,25 @@
package com.yunhai.svchall.controller.api.svc;
import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.svchall.model.svc.SvcModels.ApplyPageQuery;
import com.yunhai.svchall.service.impl.ServiceApplyService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/** 办件记录管理面(/api/svc/apply/**):本校在途与历史办件分页。 */
@RestController
@RequestMapping("/api/svc/apply")
@RequiredArgsConstructor
public class SvcApplyAdminController {
private final ServiceApplyService applyService;
@GetMapping("/page")
public Map<String, Object> page(BoaUser authUserWithDept, ApplyPageQuery query) {
return applyService.adminPage(authUserWithDept, query);
}
}
@@ -0,0 +1,56 @@
package com.yunhai.svchall.controller.api.svc;
import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.svchall.model.svc.SvcModels.DeleteRequest;
import com.yunhai.svchall.model.svc.SvcModels.SvcDeptVo;
import com.yunhai.svchall.model.svc.SvcModels.SvcDictSaveRequest;
import com.yunhai.svchall.model.svc.SvcModels.SvcDictVo;
import com.yunhai.svchall.service.impl.ServiceDictService;
import com.yunhai.svchall.service.impl.ServiceItemService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.List;
import java.util.Map;
/**
* 事项字典 + 服务部门选项(/api/svc/dict/**、/api/svc/dept/tree,迁移自 CMS SvcDictController)。
*/
@RestController
@RequestMapping("/api/svc")
@RequiredArgsConstructor
public class SvcDictController {
private final ServiceDictService dictService;
private final ServiceItemService itemService;
@GetMapping("/dict/list")
public List<SvcDictVo> list(BoaUser authUserWithDept,
@RequestParam("siteCode") String siteCode,
@RequestParam(value = "dictType", required = false) String dictType) {
return dictService.list(authUserWithDept, siteCode, dictType);
}
@PostMapping("/dict/save")
public Integer save(BoaUser authUserWithDept, @Valid @RequestBody SvcDictSaveRequest request) {
return dictService.save(authUserWithDept, request);
}
@PostMapping("/dict/delete")
public void delete(BoaUser authUserWithDept, @Valid @RequestBody DeleteRequest request) {
dictService.delete(authUserWithDept, request);
}
/** 服务部门选项(UserBase 组树代理;平台不可达降级空列表) */
@GetMapping("/dept/tree")
public List<SvcDeptVo> deptTree(BoaUser authUserWithDept,
@RequestParam("siteCode") String siteCode) {
return itemService.deptTree(authUserWithDept, siteCode);
}
}
@@ -0,0 +1,36 @@
package com.yunhai.svchall.controller.api.svc;
import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.svchall.model.svc.SvcModels.FlowSummaryVo;
import com.yunhai.svchall.service.FlowClient;
import com.yunhai.svchall.service.SiteDirectory;
import com.yunhai.svchall.service.SiteGuard;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 流程选择器(/api/svc/flow/**):事项编辑页"办理流程"数据源,代理流程中心
* /oapi/flow/list。流程中心未接入(svchall.flow.enabled=false)时抛业务异常,
* 前端据此降级提示(事项只能配为线下/纯指南)。
*/
@RestController
@RequestMapping("/api/svc/flow")
@RequiredArgsConstructor
public class SvcFlowController {
private final FlowClient flowClient;
private final SiteGuard siteGuard;
private final SiteDirectory siteDirectory;
@GetMapping("/list")
public List<FlowSummaryVo> list(BoaUser authUserWithDept, @RequestParam("siteCode") String siteCode) {
siteGuard.requireSiteManager(authUserWithDept, siteCode);
SiteDirectory.SiteInfo site = siteDirectory.requireSite(siteCode);
return flowClient.listFlows(site.getSchId());
}
}
@@ -0,0 +1,69 @@
package com.yunhai.svchall.controller.api.svc;
import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.svchall.model.svc.SvcModels.SvcFormSaveRequest;
import com.yunhai.svchall.model.svc.SvcModels.SvcFormVo;
import com.yunhai.svchall.service.impl.ServiceFormService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.List;
/**
* 自定义表单管理(/api/svc/form/**,V2.0 Part B.3):列表/版本轨迹/保存(发布版自动升版)/
* 发布(静态校验)/删除(被绑定即拦)。表单为 sch 级,端点仍以 siteCode 定位做权限收口。
*/
@RestController
@RequestMapping("/api/svc/form")
@RequiredArgsConstructor
public class SvcFormController {
private final ServiceFormService formService;
@GetMapping("/list")
public List<SvcFormVo> list(BoaUser authUserWithDept, @RequestParam("siteCode") String siteCode) {
return formService.list(authUserWithDept, siteCode);
}
@GetMapping("/versions")
public List<SvcFormVo> versions(BoaUser authUserWithDept,
@RequestParam("siteCode") String siteCode,
@RequestParam("code") String code) {
return formService.versions(authUserWithDept, siteCode, code);
}
@GetMapping("/get")
public SvcFormVo get(BoaUser authUserWithDept,
@RequestParam("siteCode") String siteCode,
@RequestParam("id") Integer id) {
return formService.get(authUserWithDept, siteCode, id);
}
@PostMapping("/save")
public Integer save(BoaUser authUserWithDept,
@RequestParam("siteCode") String siteCode,
@Valid @RequestBody SvcFormSaveRequest request) {
return formService.save(authUserWithDept, siteCode, request);
}
@PostMapping("/publish/{id}")
public void publish(BoaUser authUserWithDept,
@PathVariable("id") Integer id,
@RequestParam("siteCode") String siteCode) {
formService.publish(authUserWithDept, siteCode, id);
}
@PostMapping("/delete")
public void delete(BoaUser authUserWithDept,
@RequestParam("siteCode") String siteCode,
@RequestParam("code") String code) {
formService.delete(authUserWithDept, siteCode, code);
}
}
@@ -0,0 +1,101 @@
package com.yunhai.svchall.controller.api.svc;
import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.svchall.model.svc.SvcModels.DeleteRequest;
import com.yunhai.svchall.model.svc.SvcModels.IdSortItem;
import com.yunhai.svchall.model.svc.SvcModels.SvcItemAttachmentVo;
import com.yunhai.svchall.model.svc.SvcModels.SvcItemEditVo;
import com.yunhai.svchall.model.svc.SvcModels.SvcItemPageItem;
import com.yunhai.svchall.model.svc.SvcModels.SvcItemPageQuery;
import com.yunhai.svchall.model.svc.SvcModels.SvcItemSaveRequest;
import com.yunhai.svchall.model.svc.SvcModels.SvcItemSimpleVo;
import com.yunhai.svchall.service.impl.ServiceItemService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.List;
import java.util.Map;
/**
* 事项管理(/api/svc/item/**,迁移自 CMS SvcItemController;siteId → siteCode)。
* 权限:站点管理面(SiteGuard:超管或本校角色,站点经 CMS 目录解析 fail-closed)。
*/
@RestController
@RequestMapping("/api/svc/item")
@RequiredArgsConstructor
public class SvcItemController {
private final ServiceItemService itemService;
@GetMapping("/page")
public Map<String, Object> page(BoaUser authUserWithDept, SvcItemPageQuery query) {
return itemService.adminPage(authUserWithDept, query);
}
@GetMapping("/get")
public SvcItemEditVo get(BoaUser authUserWithDept,
@RequestParam("siteCode") String siteCode,
@RequestParam("id") Integer id) {
return itemService.adminGet(authUserWithDept, siteCode, id);
}
/** 保存(新建草稿/编辑;富文本 jsoup 清洗;flowGuid 绑定校验:onlineFlag=1 ⇒ 必绑流程) */
@PostMapping("/save")
public Integer save(BoaUser authUserWithDept, @Valid @RequestBody SvcItemSaveRequest request) {
return itemService.adminSave(authUserWithDept, request);
}
@PostMapping("/publish/{id}")
public void publish(BoaUser authUserWithDept,
@PathVariable("id") Integer id,
@RequestParam("siteCode") String siteCode) {
itemService.adminPublish(authUserWithDept, siteCode, id);
}
@PostMapping("/offline/{id}")
public void offline(BoaUser authUserWithDept,
@PathVariable("id") Integer id,
@RequestParam("siteCode") String siteCode) {
itemService.adminOffline(authUserWithDept, siteCode, id);
}
@PostMapping("/delete")
public void delete(BoaUser authUserWithDept, @Valid @RequestBody DeleteRequest request) {
itemService.adminDelete(authUserWithDept, request);
}
@PostMapping("/sort")
public void sort(BoaUser authUserWithDept,
@RequestParam("siteCode") String siteCode,
@RequestBody List<IdSortItem> items) {
itemService.adminSort(authUserWithDept, siteCode, items);
}
/** 同步部门名快照(UserBase 组树改名后刷新 dept_name) */
@PostMapping("/syncDeptName")
public Map<String, Object> syncDeptName(BoaUser authUserWithDept,
@RequestParam("siteCode") String siteCode) {
return itemService.syncDeptName(authUserWithDept, siteCode);
}
@GetMapping("/selList")
public List<SvcItemSimpleVo> selList(BoaUser authUserWithDept,
@RequestParam("siteCode") String siteCode) {
return itemService.adminSelList(authUserWithDept, siteCode);
}
/** 指南附件回显(编辑页列表) */
@GetMapping("/attList")
public List<SvcItemAttachmentVo> attList(BoaUser authUserWithDept,
@RequestParam("siteCode") String siteCode,
@RequestParam("id") Integer id) {
return itemService.itemAttachments(authUserWithDept, siteCode, id);
}
}
@@ -0,0 +1,47 @@
package com.yunhai.svchall.controller.callback;
import com.yunhai.svchall.service.impl.FlowCallbackService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.nio.charset.StandardCharsets;
/**
* 流程中心回调接收端点(/callback/flow;不在 CAS pattern(/api/*)内、也不在 /oapi/*
* (appKey 签名)内——独立走 FlowCallbackVerifyFilter 的 X-Flw-* 四头验签后才到达这里)。
* <p>
* <b>返回语义(契约冻结 D2,免对账前提)</b>:状态推进成功 / 重复投递(去重命中)/
* 协议毒消息 / 乱序丢弃 / 办件不属本应用 → <b>200</b>(毒消息回 200 防无限重放);
* 瞬时失败(DB 异常/锁冲突,{@link FlowCallbackService.TransientCallbackException})/
* 意外 RuntimeException → <b>5xx</b>——flow-center 持续重试直至送达,消费方恢复自动追平
* (替代旧"恒 200 + 每日对账"架构,验收报告 §五遗留批次)。
*/
@Slf4j
@RestController
@RequestMapping("/callback")
@RequiredArgsConstructor
public class FlowCallbackController {
private final FlowCallbackService callbackService;
@PostMapping("/flow")
public ResponseEntity<String> flow(@RequestBody byte[] body) {
try {
callbackService.process(new String(body, StandardCharsets.UTF_8));
return ResponseEntity.ok("{\"ok\":true}");
} catch (FlowCallbackService.TransientCallbackException e) {
log.error("[svc-hall] 回调瞬时失败(回 5xx 触发流程中心重试): {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("{\"ok\":false}");
} catch (Exception e) {
// 意外异常同样按瞬时失败处理(宁可多重试一次,不可丢事件)
log.error("[svc-hall] 回调意外异常(回 5xx 触发流程中心重试)", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("{\"ok\":false}");
}
}
}
@@ -0,0 +1,136 @@
package com.yunhai.svchall.controller.oapi.svc;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.utils.exception.ErrorCode;
import com.yunhai.svchall.model.oapi.OapiModels;
import com.yunhai.svchall.service.SiteDirectory;
import com.yunhai.svchall.service.impl.ServiceItemService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 门户供数开放接口(/oapi/svc/**,appKey 签名访问,消费方=CMS;V2.0 Part C.2 契约冻结件)。
* 错误纪律:未签名 401(RequireAppSignFilter);缺参/站点或事项不存在/未发布 →
* HTTP 200 + 信封 code=2(BizException);detail 支持 itemId/itemCode 双键。
* 站点解析 fail-closed:站点目录不可达按"站点不存在"报 code=2,不降级全局查询。
*/
@Slf4j
@RestController
@RequestMapping("/oapi/svc")
@RequiredArgsConstructor
public class SvcOapiController {
private final ServiceItemService itemService;
private final SiteDirectory siteDirectory;
/** 服务中心列表(分页 + 三维筛选 + 关键词;URL 由 CMS 生成,接口不返回 url) */
@GetMapping("/items")
public OapiModels.ListData items(@RequestParam(value = "siteCode", required = false) String siteCode,
@RequestParam(value = "objectKey", required = false) String objectKey,
@RequestParam(value = "categoryKey", required = false) String categoryKey,
@RequestParam(value = "deptGroupId", required = false) Integer deptGroupId,
@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "pageSize", required = false) Integer pageSize) {
SiteDirectory.SiteInfo site = requireSite(siteCode);
return itemService.oapiItems(site.getSchId(), site.getSiteCode(), emptyToNull(objectKey),
emptyToNull(categoryKey), deptGroupId, emptyToNull(keyword), page, pageSize);
}
/** 三组筛选项 + 计数(已发布口径) */
@GetMapping("/filters")
public OapiModels.Filters filters(@RequestParam(value = "siteCode", required = false) String siteCode) {
SiteDirectory.SiteInfo site = requireSite(siteCode);
return itemService.oapiFilters(site.getSchId(), site.getSiteCode());
}
/** 指南页详情(itemId 或 itemCode 二选一;三段 HTML/封面/附件已绝对化;含关联事项与 applyUrl) */
@GetMapping("/item/detail")
public OapiModels.Detail detail(@RequestParam(value = "siteCode", required = false) String siteCode,
@RequestParam(value = "itemId", required = false) Integer itemId,
@RequestParam(value = "itemCode", required = false) String itemCode) {
SiteDirectory.SiteInfo site = requireSite(siteCode);
if (itemId == null && !StringUtils.hasText(itemCode)) {
throw new BizException(ErrorCode.PARAM_ERROR, "itemId/itemCode 不得同时为空");
}
OapiModels.Detail detail = itemService.oapiDetail(site.getSchId(), site.getSiteCode(),
itemId, emptyToNull(itemCode));
if (detail == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "事项不存在或未发布");
}
return detail;
}
/** 浏览量累加(CMS fire-and-forget;支持 itemId 直取;找不到事项静默忽略,保持信封 code=0) */
@PostMapping("/item/view")
public void view(@RequestParam(value = "siteCode", required = false) String siteCode,
@RequestParam(value = "itemCode", required = false) String itemCode,
@RequestParam(value = "itemId", required = false) Integer itemId) {
if (itemId != null) {
itemService.countViewByItemId(itemId);
return;
}
SiteDirectory.SiteInfo site = requireSite(siteCode);
if (!StringUtils.hasText(itemCode)) {
throw new BizException(ErrorCode.PARAM_ERROR, "itemCode/itemId 不得同时为空");
}
itemService.countView(site.getSchId(), site.getSiteCode(), itemCode.trim());
}
/** 指南附件(按 itemId 直取;fileUrl 已绝对化) */
@GetMapping("/item/attachments")
public java.util.List<OapiModels.AttachmentRow> attachments(
@RequestParam(value = "itemId", required = false) Integer itemId) {
if (itemId == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "itemId 不得为空");
}
return itemService.attachmentsByItemId(itemId);
}
/** 关联事项(按 itemId 直取:同类别已发布 Top N,排除自身) */
@GetMapping("/item/related")
public java.util.List<OapiModels.RelatedItemRow> related(
@RequestParam(value = "itemId", required = false) Integer itemId,
@RequestParam(value = "limit", required = false) Integer limit) {
if (itemId == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "itemId 不得为空");
}
return itemService.relatedByItemId(itemId, limit);
}
/** 跨服务要点快照(docs/17 §1.1 契约平移;原消费方流程中心已取消,保留为对外标准口) */
@GetMapping("/item/snapshot")
public OapiModels.Snapshot snapshot(@RequestParam(value = "siteCode", required = false) String siteCode,
@RequestParam(value = "itemCode", required = false) String itemCode) {
if (!StringUtils.hasText(siteCode) || !StringUtils.hasText(itemCode)) {
throw new BizException(ErrorCode.PARAM_ERROR, "siteCode/itemCode 不得为空");
}
OapiModels.Snapshot vo = itemService.oapiSnapshot(siteCode.trim(), itemCode.trim());
if (vo == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "事项不存在或未发布");
}
return vo;
}
/** 站点解析 fail-closed:不可达或不存在一律 code=2(门户侧不应渲染未知站点) */
private SiteDirectory.SiteInfo requireSite(String siteCode) {
if (!StringUtils.hasText(siteCode)) {
throw new BizException(ErrorCode.PARAM_ERROR, "siteCode 不得为空");
}
SiteDirectory.SiteInfo site = siteDirectory.resolveSite(siteCode.trim());
if (site == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "站点不存在或站点服务不可达:" + siteCode);
}
return site;
}
private static String emptyToNull(String value) {
return StringUtils.hasText(value) ? value.trim() : null;
}
}