周末修订
build / build-push (push) Successful in 3m59s

This commit is contained in:
chenxing
2026-09-15 23:41:34 +08:00
parent e4b843cf00
commit 1cb95225a6
52 changed files with 1250 additions and 3788 deletions
@@ -1,57 +0,0 @@
package com.yunhai.cms.controller.api.svcitem;
import com.yunhai.cms.model.dto.DeleteRequest;
import com.yunhai.cms.svcitem.model.dto.SvcDictSaveRequest;
import com.yunhai.cms.svcitem.model.vo.SvcDeptVo;
import com.yunhai.cms.svcitem.model.vo.SvcDictVo;
import com.yunhai.cms.svcitem.service.SvcItemQuery;
import com.yunhai.cms.svcitem.service.impl.SvcDictService;
import com.yunhai.platform.utils.model.BoaUser;
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;
/**
* 事项字典 + 服务部门选项(/api/svc/dict/**、/api/svc/dept/tree,事项库详细设计 §3.3)。
*/
@RestController
@RequestMapping("/api/svc")
@RequiredArgsConstructor
public class SvcDictController {
private final SvcDictService svcDictService;
private final SvcItemQuery svcItemQuery;
/** 字典列表(dictType 空=全部;referenced=被引用条数) */
@GetMapping("/dict/list")
public List<SvcDictVo> list(BoaUser authUserWithDept,
@RequestParam("siteId") Integer siteId,
@RequestParam(value = "dictType", required = false) String dictType) {
return svcDictService.list(authUserWithDept, siteId, dictType);
}
/** 保存(新建/改名;稳定键被引用后不可改、被引用不可删——服务端校验) */
@PostMapping("/dict/save")
public Integer save(BoaUser authUserWithDept, @Valid @RequestBody SvcDictSaveRequest request) {
return svcDictService.save(authUserWithDept, request);
}
@PostMapping("/dict/delete")
public void delete(BoaUser authUserWithDept, @Valid @RequestBody DeleteRequest request) {
svcDictService.delete(authUserWithDept, request);
}
/** 服务部门选项(UserBase 组树代理;平台不可达降级空列表) */
@GetMapping("/dept/tree")
public List<SvcDeptVo> deptTree(BoaUser authUserWithDept,
@RequestParam("siteId") Integer siteId) {
return svcItemQuery.deptTree(authUserWithDept, siteId);
}
}
@@ -1,105 +0,0 @@
package com.yunhai.cms.controller.api.svcitem;
import com.yunhai.cms.model.dto.DeleteRequest;
import com.yunhai.cms.svcitem.model.dto.IdSortItem;
import com.yunhai.cms.svcitem.model.dto.SvcItemSaveRequest;
import com.yunhai.cms.svcitem.model.query.SvcItemPageQuery;
import com.yunhai.cms.svcitem.model.vo.SvcItemAttachmentVo;
import com.yunhai.cms.svcitem.model.vo.SvcItemEditVo;
import com.yunhai.cms.svcitem.model.vo.SvcItemSimpleVo;
import com.yunhai.cms.svcitem.service.SvcItemQuery;
import com.yunhai.platform.utils.model.BoaUser;
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/**,事项库详细设计 §3.3)。
* 包必须落 controller.api 之下以获得 ApiParamExceptionHandler 的参数绑定异常翻译(§3.1 修正)。
* 消费方纪律:只依赖 {@link SvcItemQuery} 接缝,不注入实现类(拆分时替换实现即可整体切换)。
*/
@RestController
@RequestMapping("/api/svc/item")
@RequiredArgsConstructor
public class SvcItemController {
private final SvcItemQuery svcItemQuery;
@GetMapping("/page")
public Map<String, Object> page(BoaUser authUserWithDept, SvcItemPageQuery query) {
return svcItemQuery.adminPage(authUserWithDept, query);
}
/** 编辑回显(ext_json.apply_url 已提取) */
@GetMapping("/get")
public SvcItemEditVo get(BoaUser authUserWithDept,
@RequestParam("siteId") Integer siteId,
@RequestParam("id") Integer id) {
return svcItemQuery.adminGet(authUserWithDept, siteId, id);
}
/** 保存(新建草稿/编辑;富文本 jsoup 清洗,apply_url 写 ext_json) */
@PostMapping("/save")
public Integer save(BoaUser authUserWithDept, @Valid @RequestBody SvcItemSaveRequest request) {
return svcItemQuery.adminSave(authUserWithDept, request);
}
@PostMapping("/publish/{id}")
public void publish(BoaUser authUserWithDept,
@PathVariable("id") Integer id,
@RequestParam("siteId") Integer siteId) {
svcItemQuery.adminPublish(authUserWithDept, siteId, id);
}
@PostMapping("/offline/{id}")
public void offline(BoaUser authUserWithDept,
@PathVariable("id") Integer id,
@RequestParam("siteId") Integer siteId) {
svcItemQuery.adminOffline(authUserWithDept, siteId, id);
}
/** 删除(附件 svc_item 引用同步解引用) */
@PostMapping("/delete")
public void delete(BoaUser authUserWithDept, @Valid @RequestBody DeleteRequest request) {
svcItemQuery.adminDelete(authUserWithDept, request);
}
/** 自定义排序(按传入 (id, sortNo) 落库) */
@PostMapping("/sort")
public void sort(BoaUser authUserWithDept,
@RequestParam("siteId") Integer siteId,
@RequestBody List<IdSortItem> items) {
svcItemQuery.adminSort(authUserWithDept, siteId, items);
}
/** 同步部门名快照(可选动作:UserBase 组树改名后刷新 dept_name) */
@PostMapping("/syncDeptName")
public Map<String, Object> syncDeptName(BoaUser authUserWithDept,
@RequestParam("siteId") Integer siteId) {
return svcItemQuery.syncDeptName(authUserWithDept, siteId);
}
/** 选择器精简列表 */
@GetMapping("/selList")
public List<SvcItemSimpleVo> selList(BoaUser authUserWithDept,
@RequestParam("siteId") Integer siteId) {
return svcItemQuery.adminSelList(authUserWithDept, siteId);
}
/** 指南附件回显(ref_type=svc_item, ref_id=id,编辑页列表) */
@GetMapping("/attList")
public List<SvcItemAttachmentVo> attList(BoaUser authUserWithDept,
@RequestParam("siteId") Integer siteId,
@RequestParam("id") Integer id) {
return svcItemQuery.itemAttachments(authUserWithDept, siteId, id);
}
}
@@ -0,0 +1,60 @@
package com.yunhai.cms.controller.oapi.site;
import com.yunhai.cms.entity.CmsSite;
import com.yunhai.cms.repository.CmsSiteRepository;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.utils.exception.ErrorCode;
import lombok.Data;
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;
/**
* 站点目录开放接口(/oapi/site/list,appKey 签名访问;V2.0 Part C.3)。
* 消费方=办事大厅 svc-hall(CmsSiteDirectoryImpl:resolveSite 反解 siteCode→schId、
* 站点选择器按校拉取;本地缓存 5min)。schId 可选:不传=全部启用站点(跨校全量,
* 供 siteCode 反解);传了=按校过滤(P0-1 契约对齐,两侧注释口径一致)。
*/
@RestController
@RequestMapping("/oapi/site")
@RequiredArgsConstructor
public class SiteOapiController {
private final CmsSiteRepository siteRepository;
@GetMapping("/list")
public List<SiteOptionVo> list(@RequestParam(value = "schId", required = false) Integer schId) {
if (schId == null) {
return siteRepository.findByStatusOrderByIdAsc(1).stream()
.map(SiteOapiController::toVo)
.collect(java.util.stream.Collectors.toList());
}
if (schId <= 0) {
throw new BizException(ErrorCode.PARAM_ERROR, "schId 非法");
}
return siteRepository.findBySchId(schId).stream()
.filter(site -> site.getStatus() != null && site.getStatus() == 1)
.map(SiteOapiController::toVo)
.collect(java.util.stream.Collectors.toList());
}
private static SiteOptionVo toVo(CmsSite site) {
SiteOptionVo vo = new SiteOptionVo();
vo.setSiteCode(site.getCode());
vo.setName(site.getName());
vo.setSchId(site.getSchId() == null ? null : site.getSchId().longValue());
return vo;
}
/** 站点选项(siteCode 为办事大厅侧定位键;schId 用于权限与租户定界) */
@Data
public static class SiteOptionVo {
private String siteCode;
private String name;
private Long schId;
}
}
@@ -1,40 +0,0 @@
package com.yunhai.cms.controller.oapi.svcitem;
import com.yunhai.cms.svcitem.model.vo.SvcItemSnapshotVo;
import com.yunhai.cms.svcitem.service.SvcItemQuery;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.utils.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.util.StringUtils;
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;
/**
* 事项跨服务快照(/oapi/**,appKey 签名访问;事项库详细设计 §3.4 冻结契约)。
* 错误一律走 BizException+信封 code(平台口径 HTTP 恒 200),未签名由签名过滤器返回 401;
* 不做"仅按 itemCode 全局搜"的降级查询——跨服务必须 (siteCode,itemCode) 成对。
* 接口不缓存(调用频次=办件量级,直查 DB)。
*/
@RestController
@RequestMapping("/oapi/svc/item")
@RequiredArgsConstructor
public class SvcItemOapiController {
private final SvcItemQuery svcItemQuery;
/** 发布态要点快照:供 svhall 发起办件时落 flw_instance(跨库不 join,办件不受后续变更影响) */
@GetMapping("/snapshot")
public SvcItemSnapshotVo 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 不得为空");
}
SvcItemSnapshotVo vo = svcItemQuery.snapshot(siteCode.trim(), itemCode.trim());
if (vo == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "事项不存在或未发布");
}
return vo;
}
}
@@ -26,6 +26,7 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
@@ -350,7 +351,30 @@ public class PortalController {
query.setDeptGroupId(dept);
query.setKeyword(trimToNull(q));
query.setPage(page == null || page < 1 ? 1 : page);
PortalServiceListData serviceList = svcItemQuery.portalPage(query);
// P1-4:svc-hall 停机/重启窗口(连接类异常,含 DHHttpException code=0)降级空态渲染 200,
// 不再连坐访客 500;业务性缺失语义维持 §2.3 修复后的行为(portalPage 无 code=2 分支)
PortalServiceListData serviceList;
boolean svcUnavailable;
try {
serviceList = svcItemQuery.portalPage(query);
svcUnavailable = false;
} catch (RuntimeException e) {
log.warn("[portal] 事项服务不可达(降级空态渲染)siteId={}:{}", site.getId(), e.toString());
serviceList = new PortalServiceListData();
serviceList.setSiteId(site.getId());
serviceList.setPage(1);
serviceList.setPageSize(10);
serviceList.setTotal(0L);
serviceList.setTotalPages(0);
svcUnavailable = true;
}
model.addAttribute("svcUnavailable", svcUnavailable);
if (svcUnavailable) {
addChromeAttributes(model, site, request, effectiveTheme, url, "服务中心 - " + site.getName());
model.addAttribute("canonical", url.serviceList());
return "themes/" + effectiveTheme + "/service";
}
// 筛选/分页 URL 预生成(§5.4):勾选=替换本维、取消=移除本维,其余维度与关键词保留,page 归 1
Map<String, Object> currentFilters = currentServiceFilters(obj, cat, dept, q);
@@ -400,14 +424,34 @@ public class PortalController {
if (site == null) {
return null;
}
PortalServiceItem item = svcItemQuery.portalItem(itemId, site.getId());
if (item == null) {
// P1-4:svc-hall 连接类异常降级空态渲染 200;业务性缺失(code=2→null)维持 404 语义
PortalServiceItem item;
boolean svcUnavailable;
try {
item = svcItemQuery.portalItem(itemId, site.getId());
svcUnavailable = false;
} catch (RuntimeException e) {
log.warn("[portal] 事项服务不可达(指南页降级空态渲染)siteId={} itemId={}:{}",
site.getId(), itemId, e.toString());
item = null;
svcUnavailable = true;
}
if (item == null && !svcUnavailable) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return null;
}
svcItemQuery.countView(itemId);
String effectiveTheme = pageRenderer.resolveEffectiveTheme(site, "guide.html");
PortalUrlHelper url = buildUrl(site, request, effectiveTheme);
model.addAttribute("svcUnavailable", svcUnavailable);
if (svcUnavailable) {
addChromeAttributes(model, site, request, effectiveTheme, url, "服务中心 - " + site.getName());
return "themes/" + effectiveTheme + "/guide";
}
try {
svcItemQuery.countView(itemId);
} catch (RuntimeException e) {
log.debug("[portal] 浏览量累加失败(忽略)itemId={}:{}", itemId, e.toString());
}
model.addAttribute("url", url);
model.addAttribute("site", site);
@@ -421,10 +465,9 @@ public class PortalController {
url.rewriteUploadUrls(item.getProcessHtml()),
url.rewriteUploadUrls(item.getMaterialHtml()),
url.rewriteUploadUrls(item.getNoticeHtml())));
model.addAttribute("attachments", svcItemQuery.attachments(itemId));
model.addAttribute("attachments", attachmentsQuietly(itemId));
List<Map<String, Object>> related = new ArrayList<>();
for (PortalServiceItem rel : svcItemQuery.relatedItems(site.getId(), itemId,
item.getCategoryKey(), RELATED_ITEMS_SIZE)) {
for (PortalServiceItem rel : relatedQuietly(site.getId(), itemId, item.getCategoryKey())) {
Map<String, Object> row = new HashMap<>();
row.put("id", rel.getId());
row.put("title", rel.getTitle());
@@ -444,9 +487,13 @@ public class PortalController {
model.addAttribute("ruleCategoryUrl", url.category(item.getRuleCategoryId()));
}
// "我要办理"三态(§5.3):① ext_json.apply_url 手填外链(最高优先)
// ② 未填且站点配了 svc_hall_base → 生成 svhall 发起页 URL ③ 都没有 → 不注入(模板不渲染按钮)
// ② 未填且站点配了 svc_hall_base 且事项开通在线办理(onlineFlag=1 且绑流程,§3.7——
// 与 svc-hall 侧 submit 校验"未开通在线办理"口径对齐,线下/纯指南事项不再渲染按钮)
// ③ 其余 → 不注入(模板不渲染按钮)
String applyUrl = item.getApplyUrl();
if ((applyUrl == null || applyUrl.trim().isEmpty())
boolean onlineHandlable = item.getOnlineFlag() != null && item.getOnlineFlag() == 1
&& StringUtils.hasText(item.getFlowGuid());
if ((applyUrl == null || applyUrl.trim().isEmpty()) && onlineHandlable
&& site.getSvcHallBase() != null && !site.getSvcHallBase().trim().isEmpty()) {
String base = site.getSvcHallBase().trim();
while (base.endsWith("/")) {
@@ -468,6 +515,39 @@ public class PortalController {
return "themes/" + effectiveTheme + "/guide";
}
/** 门户页面公共骨架属性(P1-4 降级路径与正常路径共用;q/seoDescription 等页面特有属性由调用方补) */
private void addChromeAttributes(Model model, PortalSiteSnapshot site, HttpServletRequest request,
String effectiveTheme, PortalUrlHelper url, String pageTitle) {
model.addAttribute("url", url);
model.addAttribute("site", site);
model.addAttribute("themeParams", site.getThemeParams());
model.addAttribute("themeVarsCss", site.getThemeVarsCss());
model.addAttribute("customCss", site.getCustomCss());
model.addAttribute("themeBase", "themes/" + effectiveTheme);
model.addAttribute("navCategories", site.getCategories());
model.addAttribute("pageTitle", pageTitle);
}
/** 附件查询静默降级(svc-hall 抖动不阻断已可渲染的指南页;业务性缺失已在查询层转空) */
private List<com.yunhai.cms.svcitem.model.vo.SvcItemAttachmentVo> attachmentsQuietly(Integer itemId) {
try {
return svcItemQuery.attachments(itemId);
} catch (RuntimeException e) {
log.warn("[portal] 附件查询失败(降级为空)itemId={}:{}", itemId, e.toString());
return new ArrayList<>();
}
}
/** 相关事项查询静默降级(同上) */
private List<PortalServiceItem> relatedQuietly(Integer siteId, Integer itemId, String categoryKey) {
try {
return svcItemQuery.relatedItems(siteId, itemId, categoryKey, RELATED_ITEMS_SIZE);
} catch (RuntimeException e) {
log.warn("[portal] 相关事项查询失败(降级为空)itemId={}:{}", itemId, e.toString());
return new ArrayList<>();
}
}
/** 当前有效筛选参数(obj/cat/dept/q,空值剔除)——分页与 canonical URL 的公共底稿 */
private Map<String, Object> currentServiceFilters(String obj, String cat, Integer dept, String q) {
Map<String, Object> filters = new LinkedHashMap<>();