From 98d1455efaa4cd3d5d3213e047ccc0ac187ba05c Mon Sep 17 00:00:00 2001
From: shinnosuke <tantao@1hoper.com>
Date: 星期一, 03 八月 2026 17:30:53 +0800
Subject: [PATCH] feat(admin): 添加政策文章管理和商户功能增强

---
 talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java |  121 ++++++++++++++++++++++++++++++++++++---
 1 files changed, 110 insertions(+), 11 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 d8ab5a6..374f605 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
@@ -10,6 +10,7 @@
 import com.talentcard.server.business.BusinessIdGenerator;
 import com.talentcard.server.business.DatabaseRows;
 import com.talentcard.server.business.PageResult;
+import com.talentcard.server.business.merchant.auth.MerchantPasswordHasher;
 import com.talentcard.server.common.BusinessException;
 import com.talentcard.server.common.ErrorCode;
 import org.jsoup.Jsoup;
@@ -155,6 +156,52 @@
 	}
 
 	@Transactional(readOnly = true)
+	public PageResult<Map<String, Object>> policies(String title, String author, Integer status, String publishStart, String publishEnd, int page, int size) {
+		StringBuilder sql = new StringBuilder("select id, policy_no, title, subtitle, cover_url, author, source, view_count, is_top, sort_no, publish_time, status, created_at, updated_at from policy_article where deleted = 0");
+		StringBuilder countSql = new StringBuilder("select count(*) from policy_article where deleted = 0");
+		List<Object> args = new ArrayList<>();
+		if (title != null && !title.isBlank()) { sql.append(" and title like ?"); countSql.append(" and title like ?"); args.add("%" + title.trim() + "%"); }
+		if (author != null && !author.isBlank()) {
+			sql.append(" and (author like ? or source like ?)"); countSql.append(" and (author like ? or source like ?)");
+			String like = "%" + author.trim() + "%"; args.add(like); args.add(like);
+		}
+		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");
+		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);
+	}
+
+	@Transactional
+	public Map<String, Object> createPolicy(PolicyArticleRequest request) {
+		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(request.author()), blankToNull(request.source()), sanitizeNewsContent(request.content()), 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) {
+		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);
+		ensureUpdated(updated);
+		return policyDetail(id);
+	}
+
+	@Transactional
+	public Map<String, Object> updatePolicyStatus(Long id, Byte status) {
+		int updated = jdbcTemplate.update("update policy_article set status = ?, publish_time = case when ? = 1 then coalesce(publish_time, now()) else publish_time end where deleted = 0 and id = ?", status, status, id);
+		ensureUpdated(updated);
+		return policyDetail(id);
+	}
+
+	@Transactional(readOnly = true)
 	public PageResult<Map<String, Object>> merchants(
 			String keyword,
 			String contactMobile,
@@ -169,7 +216,21 @@
 		StringBuilder sql = new StringBuilder("""
 				select m.id, m.merchant_name, m.merchant_type, m.contact_name, m.contact_mobile,
 				       m.logo_url, m.cover_url, m.address, m.is_recommend, m.sort_no, m.status,
-				       m.business_status, m.created_at,
+				       case
+				         when m.status <> 1 then '休息中'
+				         when lower(coalesce(m.business_status, 'open')) in ('closed', '休息中') then '休息中'
+				         when exists (
+				           select 1
+				           from merchant_business_hours h
+				           join merchant_store s on s.id = h.store_id and s.status = 1 and s.deleted = 0
+				           where h.merchant_id = m.id
+				             and (
+				               (h.start_time <= h.end_time and current_time() >= h.start_time and current_time() < h.end_time)
+				               or (h.start_time > h.end_time and (current_time() >= h.start_time or current_time() < h.end_time))
+				             )
+				         ) then '营业中'
+				         else '休息中'
+				       end as business_status, m.created_at,
 				       (select count(*) from merchant_product p where p.merchant_id = m.id and p.deleted = 0) as product_count,
 				       (select count(*) from benefit_order o where o.merchant_id = m.id and o.deleted = 0) as order_count,
 				       0 as reservation_count
@@ -224,20 +285,34 @@
 	public Map<String, Object> merchant(Long id) {
 		return one("""
 				select id, merchant_name, merchant_type, contact_name, contact_mobile, logo_url, cover_url,
-				       address, business_status, longitude, latitude, group_buying_enabled, usage_instructions,
-				       rights_text, service_label, is_recommend, sort_no, status, created_at, updated_at
+				       address, business_status, longitude, latitude, can_login_merchant_app, group_buying_enabled, usage_instructions,
+				       rights_text, merchant_rights_config, service_label, is_recommend, sort_no, status, created_at, updated_at,
+				       (select o.account from merchant_operator o
+				        where o.merchant_id = merchant.id and o.role_code = 'owner' and o.deleted = 0
+				        order by o.id asc limit 1) as login_account
 				from merchant
 				where deleted = 0 and id = ?
 				""", id);
+	}
+
+	@Transactional(readOnly = true)
+	public Map<String, Object> merchantLoginAccountAvailable(String account) {
+		String normalizedAccount = account == null ? "" : account.trim();
+		if (!normalizedAccount.matches("^[A-Za-z0-9]{1,32}$")) {
+			return Map.of("available", false);
+		}
+		Long count = jdbcTemplate.queryForObject(
+				"select count(*) from merchant_operator where account = ?", Long.class, normalizedAccount);
+		return Map.of("available", count == null || count == 0);
 	}
 
 	@Transactional
 	public Map<String, Object> createMerchant(MerchantCreateRequest request) {
 		jdbcTemplate.update("""
 				insert into merchant(merchant_name, merchant_type, contact_name, contact_mobile, address,
-				                     logo_url, cover_url, business_status, longitude, latitude, group_buying_enabled,
-				                     usage_instructions, rights_text, service_label, is_recommend, status, sort_no)
-				values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0)
+					                     logo_url, cover_url, business_status, longitude, latitude, can_login_merchant_app, group_buying_enabled,
+					                     usage_instructions, rights_text, merchant_rights_config, service_label, is_recommend, status, sort_no)
+				values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0)
 				""",
 				request.merchantName(),
 				request.merchantType(),
@@ -249,13 +324,36 @@
 				request.businessStatus(),
 				request.longitude(),
 				request.latitude(),
+				request.canLoginMerchantApp() == null ? 1 : request.canLoginMerchantApp(),
 				request.groupBuyingEnabled() == null ? 1 : request.groupBuyingEnabled(),
 				request.usageInstructions(),
 				request.rightsText(),
+				toJson(request.merchantRightsConfig()),
 				request.serviceLabel(),
 				request.recommend() == null ? 0 : request.recommend());
 		Long id = jdbcTemplate.queryForObject("select last_insert_id()", Long.class);
+		if (isFoodMerchantWithAppAccess(request)) {
+			String account = request.loginAccount().trim();
+			if (!account.matches("^[A-Za-z0-9]{1,32}$")) {
+				throw new BusinessException(ErrorCode.BAD_REQUEST, "商户登录账号只能包含英文字母和数字");
+			}
+			if (request.loginPassword() == null || request.loginPassword().length() < 6 || request.loginPassword().length() > 64) {
+				throw new BusinessException(ErrorCode.BAD_REQUEST, "商户登录密码长度须为6至64位");
+			}
+			Integer occupied = jdbcTemplate.queryForObject("select count(*) from merchant_operator where account = ?", Integer.class, account);
+			if (occupied != null && occupied > 0) throw new BusinessException(ErrorCode.CONFLICT, "商户登录账号已存在");
+			jdbcTemplate.update("insert into merchant_store(merchant_id, store_name, address, status) values (?, ?, ?, 1)", id, request.merchantName().trim(), request.address());
+			Long storeId = jdbcTemplate.queryForObject("select last_insert_id()", Long.class);
+			jdbcTemplate.update("insert into merchant_operator(merchant_id, account, password_hash, display_name, role_code, status) values (?, ?, ?, ?, 'owner', 1)",
+					id, account, MerchantPasswordHasher.encode(request.loginPassword()), request.merchantName().trim());
+			Long operatorId = jdbcTemplate.queryForObject("select last_insert_id()", Long.class);
+			jdbcTemplate.update("insert into merchant_operator_store(operator_id, store_id) values (?, ?)", operatorId, storeId);
+		}
 		return one("select * from merchant where id = ?", id);
+	}
+
+	private boolean isFoodMerchantWithAppAccess(MerchantCreateRequest request) {
+		return "餐饮服务".equals(request.merchantType()) && (request.canLoginMerchantApp() == null || request.canLoginMerchantApp() == 1);
 	}
 
 	@Transactional
@@ -263,8 +361,8 @@
 		int updated = jdbcTemplate.update("""
 				update merchant
 				set merchant_name = ?, merchant_type = ?, contact_name = ?, contact_mobile = ?,
-				    address = ?, logo_url = ?, cover_url = ?, business_status = ?, longitude = ?, latitude = ?,
-				    group_buying_enabled = ?, usage_instructions = ?, rights_text = ?, service_label = ?,
+				    address = ?, logo_url = ?, cover_url = ?, business_status = ?, longitude = ?, latitude = ?, can_login_merchant_app = ?,
+				    group_buying_enabled = ?, usage_instructions = ?, rights_text = ?, merchant_rights_config = ?, service_label = ?,
 				    is_recommend = ?, sort_no = ?, status = ?
 				where deleted = 0 and id = ?
 				""",
@@ -278,9 +376,11 @@
 				request.businessStatus(),
 				request.longitude(),
 				request.latitude(),
+				request.canLoginMerchantApp() == null ? 1 : request.canLoginMerchantApp(),
 				request.groupBuyingEnabled() == null ? 1 : request.groupBuyingEnabled(),
 				request.usageInstructions(),
 				request.rightsText(),
+				toJson(request.merchantRightsConfig()),
 				request.serviceLabel(),
 				request.recommend() == null ? 0 : request.recommend(),
 				request.sortNo() == null ? 0 : request.sortNo(),
@@ -367,10 +467,9 @@
 			args.add("%" + contactMobile.trim() + "%");
 		}
 		if (keyword != null && !keyword.isBlank()) {
-			sql.append(" and (p.product_name like ? or p.product_no like ?)");
-			countSql.append(" and (p.product_name like ? or p.product_no like ?)");
+			sql.append(" and p.product_name like ?");
+			countSql.append(" and p.product_name like ?");
 			String like = "%" + keyword.trim() + "%";
-			args.add(like);
 			args.add(like);
 		}
 		if (type != null && !type.isBlank()) {

--
Gitblit v1.7.1