From 75e20ed49063feac161beca13aba24147424d5a7 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期四, 17 九月 2026 18:43:03 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/main'
---
talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java | 178 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 files changed, 170 insertions(+), 8 deletions(-)
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java
index b6a4fb1..c02a939 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java
@@ -180,28 +180,45 @@
if (status != null) { sql.append(" and status = ?"); countSql.append(" and status = ?"); args.add(status); }
if (publishStart != null && !publishStart.isBlank()) { sql.append(" and date(publish_time) >= ?"); countSql.append(" and date(publish_time) >= ?"); args.add(publishStart); }
if (publishEnd != null && !publishEnd.isBlank()) { sql.append(" and date(publish_time) <= ?"); countSql.append(" and date(publish_time) <= ?"); args.add(publishEnd); }
- sql.append(" order by is_top desc, sort_no desc, publish_time desc, id desc");
+ // 置顶优先,置顶组内按置顶时间倒序(越后置顶越靠前);非置顶按排序值倒序。
+ sql.append(" order by is_top desc, case when is_top = 1 then top_time end desc, sort_no desc, publish_time desc, id desc");
return page(sql.toString(), countSql.toString(), page, size, args.toArray());
}
@Transactional(readOnly = true)
public Map<String, Object> policyDetail(Long id) {
- return one("select id, policy_no, title, subtitle, cover_url, author, source, content, view_count, is_top, sort_no, publish_time, status, created_at, updated_at from policy_article where deleted = 0 and id = ?", id);
+ // created_by/updated_by 存的是管理员 ID,这里一并查出姓名,前端直接展示发布人/修改人。
+ // 历史数据没有记录操作人时回落到 author(创建人)。
+ return one("""
+ select p.id, p.policy_no, p.title, p.subtitle, p.cover_url, p.author,
+ p.created_by, p.updated_by, p.is_top, p.top_time,
+ coalesce(creator.display_name, p.author) as created_by_name,
+ coalesce(updater.display_name, p.author) as updated_by_name,
+ p.source, p.content, p.view_count, p.sort_no, p.publish_time, p.status,
+ p.created_at, p.updated_at
+ from policy_article p
+ left join admin_operator creator on creator.id = p.created_by and creator.deleted = 0
+ left join admin_operator updater on updater.id = p.updated_by and updater.deleted = 0
+ where p.deleted = 0 and p.id = ?
+ """, id);
}
@Transactional
- public Map<String, Object> createPolicy(PolicyArticleRequest request, String operatorName) {
+ public Map<String, Object> createPolicy(PolicyArticleRequest request, Long operatorId, String operatorName) {
int status = defaultNewsStatus(request.status());
- jdbcTemplate.update("insert into policy_article(policy_no, title, subtitle, cover_url, author, source, content, is_top, sort_no, publish_time, status) values (?, ?, ?, ?, ?, ?, ?, ?, ?, case when ? = 1 then now() else null end, ?)",
- BusinessIdGenerator.next("POLICY"), request.title().trim(), blankToNull(request.subtitle()), blankToNull(request.coverUrl()), blankToNull(operatorName), blankToNull(request.source()), sanitizeNewsContent(request.content()), Boolean.TRUE.equals(request.isTop()) ? 1 : 0, request.sortNo() == null ? 0 : request.sortNo(), status, status);
+ // top_time 只在勾选置顶时写入,列表用它做置顶组内的排序依据。
+ jdbcTemplate.update("insert into policy_article(policy_no, title, subtitle, cover_url, author, created_by, updated_by, source, content, is_top, top_time, sort_no, publish_time, status) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, case when ? = 1 then now() else null end, ?)",
+ BusinessIdGenerator.next("POLICY"), request.title().trim(), blankToNull(request.subtitle()), blankToNull(request.coverUrl()), blankToNull(operatorName), operatorId, operatorId, blankToNull(request.source()), sanitizeNewsContent(request.content()), Boolean.TRUE.equals(request.isTop()) ? 1 : 0, Boolean.TRUE.equals(request.isTop()) ? 1 : 0, request.sortNo() == null ? 0 : request.sortNo(), status, status);
return policyDetail(jdbcTemplate.queryForObject("select last_insert_id()", Long.class));
}
@Transactional
- public Map<String, Object> updatePolicy(Long id, PolicyArticleRequest request) {
+ public Map<String, Object> updatePolicy(Long id, PolicyArticleRequest request, Long operatorId) {
int status = defaultNewsStatus(request.status());
- int updated = jdbcTemplate.update("update policy_article set title = ?, subtitle = ?, cover_url = ?, author = ?, source = ?, content = ?, is_top = ?, sort_no = ?, status = ?, publish_time = case when ? = 1 then coalesce(publish_time, now()) else publish_time end where deleted = 0 and id = ?",
- request.title().trim(), blankToNull(request.subtitle()), blankToNull(request.coverUrl()), blankToNull(request.author()), blankToNull(request.source()), sanitizeNewsContent(request.content()), Boolean.TRUE.equals(request.isTop()) ? 1 : 0, request.sortNo() == null ? 0 : request.sortNo(), status, status, id);
+ // author 保持原样(发布人只在创建时确定);updated_by 记录本次保存的管理员,空值不覆盖。
+ // 新勾选置顶时记录置顶时间;取消置顶时清空,保证置顶组内按"最近置顶"排序。
+ int updated = jdbcTemplate.update("update policy_article set title = ?, subtitle = ?, cover_url = ?, author = coalesce(nullif(?, ''), author), updated_by = coalesce(?, updated_by), source = ?, content = ?, is_top = ?, top_time = case when ? = 1 and coalesce(is_top, 0) = 0 then now() when ? = 0 then null else top_time end, sort_no = ?, status = ?, publish_time = case when ? = 1 then coalesce(publish_time, now()) else publish_time end where deleted = 0 and id = ?",
+ request.title().trim(), blankToNull(request.subtitle()), blankToNull(request.coverUrl()), blankToNull(request.author()), operatorId, blankToNull(request.source()), sanitizeNewsContent(request.content()), Boolean.TRUE.equals(request.isTop()) ? 1 : 0, Boolean.TRUE.equals(request.isTop()) ? 1 : 0, Boolean.TRUE.equals(request.isTop()) ? 1 : 0, request.sortNo() == null ? 0 : request.sortNo(), status, status, id);
ensureUpdated(updated);
return policyDetail(id);
}
@@ -348,6 +365,21 @@
where merchant_id = ? and deleted = 0 and enabled = 1
order by sort_no asc, id asc
""", id)));
+ // 加油服务详情页顶部轮播图:后台在商家编辑页维护,保存时整表替换。
+ result.put("heroImages", DatabaseRows.normalize(jdbcTemplate.queryForList("""
+ select id, image_url, sort_no, enabled
+ from transport_fuel_hero_image
+ where merchant_id = ? and deleted = 0
+ order by sort_no asc, id asc
+ """, id)));
+ } else if (HERO_IMAGE_MERCHANT_TYPES.contains(String.valueOf(result.get("merchantTypeCode")))) {
+ // 子女托管 / 行政审批的详情页轮播图同样按商家配置。
+ result.put("heroImages", DatabaseRows.normalize(jdbcTemplate.queryForList("""
+ select id, image_url, sort_no, enabled
+ from merchant_hero_image
+ where merchant_id = ? and deleted = 0
+ order by sort_no asc, id asc
+ """, id)));
}
return result;
}
@@ -410,6 +442,7 @@
ensureParkingBenefit(id, merchantType.code());
ensureFuelBenefit(id, merchantType.code(), request.serviceLabel());
syncFuelStores(id, merchantType.code(), request.applicableStores());
+ syncMerchantHeroImages(id, merchantType.code(), request.heroImages());
syncPremiumTopicMerchant(id, merchantType, request.sortNo());
Long storeId = createDefaultStore(id, request.merchantName(), request.address());
syncMerchantBusinessHours(id, storeId, request.businessHours());
@@ -555,6 +588,7 @@
ensureParkingBenefit(id, merchantType.code());
ensureFuelBenefit(id, merchantType.code(), request.serviceLabel());
syncFuelStores(id, merchantType.code(), request.applicableStores());
+ syncMerchantHeroImages(id, merchantType.code(), request.heroImages());
syncPremiumTopicMerchant(id, merchantType, request.sortNo());
Long storeId = findOrCreateDefaultStore(id, request.merchantName(), request.address());
syncMerchantBusinessHours(id, storeId, request.businessHours());
@@ -669,10 +703,80 @@
}
}
+ /**
+ * 子女托管 / 行政审批的详情页图:这两类服务的权益是全局一条,图片也全局共用。
+ */
+ @Transactional(readOnly = true)
+ public Map<String, Object> serviceHeroImages() {
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("children", firstRowOrEmpty("select id, hero_image from children_entrustment_benefit where deleted = 0 order by id asc limit 1"));
+ result.put("approval", firstRowOrEmpty("select id, hero_image from admin_approval_benefit where deleted = 0 order by id asc limit 1"));
+ return result;
+ }
+
+ @Transactional
+ public Map<String, Object> saveServiceHeroImage(String type, String imageUrl) {
+ String table = switch (type == null ? "" : type.trim()) {
+ case "children" -> "children_entrustment_benefit";
+ case "approval" -> "admin_approval_benefit";
+ default -> throw new BusinessException(ErrorCode.BAD_REQUEST, "不支持的服务类型");
+ };
+ String url = imageUrl == null || imageUrl.isBlank() ? null : imageUrl.trim();
+ int updated = jdbcTemplate.update("update " + table + " set hero_image = ? where deleted = 0 order by id asc limit 1", url);
+ if (updated == 0) throw new BusinessException(ErrorCode.NOT_FOUND, "该服务的权益暂未配置");
+ return serviceHeroImages();
+ }
+
+ private Map<String, Object> firstRowOrEmpty(String sql, Object... args) {
+ List<Map<String, Object>> rows = DatabaseRows.normalize(jdbcTemplate.queryForList(sql, args));
+ return rows.isEmpty() ? new LinkedHashMap<>() : rows.get(0);
+ }
+
+ private boolean enabledFlag(Object value) {
+ if (value == null) return true;
+ if (value instanceof Boolean bool) return bool;
+ if (value instanceof Number number) return number.intValue() != 0;
+ String text = String.valueOf(value).trim();
+ return "true".equalsIgnoreCase(text) || "1".equals(text);
+ }
+
private Double numberValue(Object value) {
if (value instanceof Number number) return number.doubleValue();
try { return value == null ? null : Double.valueOf(String.valueOf(value)); }
catch (NumberFormatException ignored) { return null; }
+ }
+
+ /** 详情页轮播图按商家配置的服务类型:其余类型顶部图仍用封面图或团单封面。 */
+ private static final List<String> HERO_IMAGE_MERCHANT_TYPES = List.of("children", "approval");
+
+ /**
+ * 同步商家详情页轮播图。
+ *
+ * 后台提交整份列表,这里按商家整表替换;列表为 null 表示本次提交没有这个字段,保持原样。
+ * 加油服务沿用原有的 transport_fuel_hero_image,子女托管/行政审批使用通用的 merchant_hero_image。
+ */
+ private void syncMerchantHeroImages(Long merchantId, String merchantType, List<Map<String, Object>> images) {
+ if (images == null) return;
+ boolean fuel = "fuel".equals(merchantType);
+ if (!fuel && !HERO_IMAGE_MERCHANT_TYPES.contains(merchantType)) return;
+ List<Map<String, Object>> normalized = new ArrayList<>();
+ for (Map<String, Object> image : images) {
+ if (image == null) continue;
+ Object urlValue = image.containsKey("imageUrl") ? image.get("imageUrl") : image.get("image_url");
+ String url = urlValue == null ? "" : String.valueOf(urlValue).trim();
+ if (url.isBlank()) continue;
+ Map<String, Object> item = new LinkedHashMap<>();
+ item.put("imageUrl", url);
+ item.put("enabled", enabledFlag(image.get("enabled")) ? 1 : 0);
+ normalized.add(item);
+ }
+ String table = fuel ? "transport_fuel_hero_image" : "merchant_hero_image";
+ jdbcTemplate.update("delete from " + table + " where merchant_id = ?", merchantId);
+ String insertSql = "insert into " + table + "(merchant_id, image_url, sort_no, enabled, deleted) values (?, ?, ?, ?, 0)";
+ for (int index = 0; index < normalized.size(); index++) {
+ Map<String, Object> image = normalized.get(index);
+ jdbcTemplate.update(insertSql, merchantId, image.get("imageUrl"), (index + 1) * 10, image.get("enabled"));
+ }
}
private void syncPremiumTopicMerchant(Long merchantId, MerchantType merchantType, Integer sortNo) {
@@ -734,6 +838,64 @@
return one("select * from benefit where id = ?", id);
}
+ /**
+ * 商家编辑页面的团单列表。
+ *
+ * 按商品维度展示(一条团单一行),修改团单只更新原记录的状态为待审核;
+ * 审核记录(merchant_product_audit_record)仍只在团单审核页面展示。
+ */
+ @Transactional(readOnly = true)
+ public PageResult<Map<String, Object>> merchantProducts(
+ Long merchantId,
+ String keyword,
+ Integer auditStatus,
+ String createdStart,
+ String createdEnd,
+ int page,
+ int size
+ ) {
+ StringBuilder sql = new StringBuilder("""
+ select p.id, p.id as product_id, p.merchant_id, m.merchant_name, m.merchant_type, m.merchant_type_code, m.contact_mobile,
+ p.product_no, p.product_type, p.product_name, p.subtitle, p.cover_url,
+ p.original_price, p.sale_price, p.subsidy_amount, p.is_government_subsidy,
+ p.stock, p.sales_count, p.detail_text, p.benefit_desc, p.purchase_notice,
+ p.valid_start_time, p.valid_end_time, p.product_images, p.rights_config,
+ p.audit_status, p.audit_remark, p.status, p.sort_no, p.submit_time, p.created_at, p.updated_at
+ from merchant_product p
+ left join merchant m on m.id = p.merchant_id
+ where p.deleted = 0 and p.merchant_id = ?
+ """);
+ StringBuilder countSql = new StringBuilder("""
+ select count(*)
+ from merchant_product p
+ where p.deleted = 0 and p.merchant_id = ?
+ """);
+ List<Object> args = new ArrayList<>();
+ args.add(merchantId);
+ if (keyword != null && !keyword.isBlank()) {
+ sql.append(" and p.product_name like ?");
+ countSql.append(" and p.product_name like ?");
+ args.add("%" + keyword.trim() + "%");
+ }
+ if (auditStatus != null) {
+ sql.append(" and p.audit_status = ?");
+ countSql.append(" and p.audit_status = ?");
+ args.add(auditStatus);
+ }
+ if (createdStart != null && !createdStart.isBlank()) {
+ sql.append(" and date(coalesce(p.submit_time, p.created_at)) >= ?");
+ countSql.append(" and date(coalesce(p.submit_time, p.created_at)) >= ?");
+ args.add(createdStart);
+ }
+ if (createdEnd != null && !createdEnd.isBlank()) {
+ sql.append(" and date(coalesce(p.submit_time, p.created_at)) <= ?");
+ countSql.append(" and date(coalesce(p.submit_time, p.created_at)) <= ?");
+ args.add(createdEnd);
+ }
+ sql.append(" order by coalesce(p.updated_at, p.submit_time, p.created_at) desc, p.id desc");
+ return page(sql.toString(), countSql.toString(), page, size, args.toArray());
+ }
+
@Transactional(readOnly = true)
public PageResult<Map<String, Object>> products(
Long merchantId,
--
Gitblit v1.7.1