| | |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalTime; |
| | | import java.time.ZoneId; |
| | | import java.time.format.DateTimeParseException; |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.HashSet; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | import com.talentcard.server.business.BusinessIdGenerator; |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.talentcard.server.business.DatabaseRows; |
| | | import com.talentcard.server.business.PageResult; |
| | | import com.talentcard.server.business.merchant.auth.MerchantPasswordHasher; |
| | |
| | | |
| | | @Service |
| | | public class AdminBusinessService { |
| | | |
| | | private static final Set<String> MERCHANT_APP_LOGIN_TYPES = Set.of( |
| | | "餐饮服务", "景区服务", "免费体检", "配偶推荐就业", "金融服务", "行政审批", "子女托管", |
| | | "酒店服务", "公寓租赁", "加油服务", "停车服务", "自助洗车", "购房优惠", "购车服务", |
| | | "家电服务", "数码服务", "文化服务", "电信服务", "口腔治疗保健" |
| | | ); |
| | | |
| | | private final JdbcTemplate jdbcTemplate; |
| | | |
| | |
| | | int size |
| | | ) { |
| | | StringBuilder sql = new StringBuilder(""" |
| | | select id, title, summary, cover_url, source, author, view_count, is_top, sort_no, |
| | | select id, title, summary, cover_url, source, author, view_count, is_top, top_time, sort_no, |
| | | publish_time, status, created_at, updated_at |
| | | from news_article |
| | | where deleted = 0 |
| | |
| | | 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, publish_time desc, id desc"); |
| | | return page(sql.toString(), countSql.toString(), page, size, args.toArray()); |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> newsDetail(Long id) { |
| | | return one(""" |
| | | select id, title, summary, cover_url, source, author, content, view_count, is_top, sort_no, |
| | | select id, title, summary, cover_url, source, author, content, view_count, is_top, top_time, sort_no, |
| | | publish_time, status, created_at, updated_at |
| | | from news_article |
| | | where deleted = 0 and id = ? |
| | |
| | | } |
| | | |
| | | @Transactional |
| | | public Map<String, Object> createNews(NewsArticleRequest request) { |
| | | public Map<String, Object> createNews(NewsArticleRequest request, String operatorName) { |
| | | int status = defaultNewsStatus(request.status()); |
| | | jdbcTemplate.update(""" |
| | | insert into news_article(title, summary, cover_url, source, author, content, is_top, sort_no, publish_time, status) |
| | | values (?, ?, ?, ?, ?, ?, ?, ?, case when ? = 1 then now() else null end, ?) |
| | | insert into news_article(title, summary, cover_url, source, author, content, is_top, top_time, sort_no, publish_time, status) |
| | | values (?, ?, ?, ?, ?, ?, ?, case when ? = 1 then now() else null end, ?, case when ? = 1 then now() else null end, ?) |
| | | """, |
| | | request.title().trim(), |
| | | blankToNull(request.summary()), |
| | | blankToNull(request.coverUrl()), |
| | | blankToNull(request.source()), |
| | | blankToNull(request.author()), |
| | | null, |
| | | blankToNull(operatorName), |
| | | sanitizeNewsContent(request.content()), |
| | | Boolean.TRUE.equals(request.isTop()) ? 1 : 0, |
| | | Boolean.TRUE.equals(request.isTop()) ? 1 : 0, |
| | | request.sortNo() == null ? 0 : request.sortNo(), |
| | | status, |
| | |
| | | int status = defaultNewsStatus(request.status()); |
| | | int updated = jdbcTemplate.update(""" |
| | | update news_article |
| | | set title = ?, summary = ?, cover_url = ?, source = ?, author = ?, content = ?, is_top = ?, sort_no = ?, |
| | | set title = ?, summary = ?, cover_url = ?, content = ?, |
| | | top_time = case when ? = 1 and coalesce(is_top, 0) = 0 then now() when ? = 0 then null else top_time end, |
| | | 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.summary()), |
| | | blankToNull(request.coverUrl()), |
| | | blankToNull(request.source()), |
| | | blankToNull(request.author()), |
| | | 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, |
| | |
| | | } |
| | | |
| | | @Transactional |
| | | public Map<String, Object> createPolicy(PolicyArticleRequest request) { |
| | | public Map<String, Object> createPolicy(PolicyArticleRequest request, 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(request.author()), blankToNull(request.source()), sanitizeNewsContent(request.content()), Boolean.TRUE.equals(request.isTop()) ? 1 : 0, request.sortNo() == null ? 0 : request.sortNo(), status, status); |
| | | 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); |
| | | return policyDetail(jdbcTemplate.queryForObject("select last_insert_id()", Long.class)); |
| | | } |
| | | |
| | |
| | | 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, |
| | | 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, |
| | | 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 |
| | |
| | | args.add(createdEnd); |
| | | } |
| | | sql.append(" order by m.is_recommend desc, m.sort_no desc, m.id desc"); |
| | | return page(sql.toString(), countSql.toString(), page, size, args.toArray()); |
| | | PageResult<Map<String, Object>> result = page(sql.toString(), countSql.toString(), page, size, args.toArray()); |
| | | applyBusinessStatus(result.records()); |
| | | return result; |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public List<Map<String, Object>> pendingNotices() { |
| | | List<Map<String, Object>> notices = new ArrayList<>(); |
| | | addPendingNotice(notices, "product-audit", "团单审核", "您有 %d 条团单待审核", "/products", |
| | | "select count(*) as pending_count, max(coalesce(submit_time, created_at)) as latest_time from merchant_product where deleted = 0 and audit_status = 1"); |
| | | addPendingNotice(notices, "family-card", "家属卡申请", "您有 %d 条家属卡申请待审核", "/family-card-applications", |
| | | "select count(*) as pending_count, max(apply_time) as latest_time from family_card_application where deleted = 0 and status = '待审核'"); |
| | | addPendingNotice(notices, "upgrade", "升级申请", "您有 %d 条升级申请待审核", "/upgrade-applications", |
| | | "select count(*) as pending_count, max(apply_time) as latest_time from upgrade_application where deleted = 0 and status = '待审核'"); |
| | | addPendingNotice(notices, "merchant-onboarding", "商户进件", "您有 %d 条商户进件待审核", "/merchant-settlement?kind=onboardings", |
| | | "select count(*) as pending_count, max(created_at) as latest_time from merchant_onboarding_application where status = 'pending'"); |
| | | addPendingNotice(notices, "merchant-bank-card", "银行卡申请", "您有 %d 条银行卡申请待审核", "/merchant-settlement?kind=bank-cards", |
| | | "select count(*) as pending_count, max(created_at) as latest_time from merchant_bank_card where status = 'pending'"); |
| | | addPendingNotice(notices, "merchant-withdrawal", "提现审核", "您有 %d 条提现申请待审核", "/merchant-settlement?kind=withdrawals", |
| | | "select count(*) as pending_count, max(created_at) as latest_time from merchant_withdrawal where status = 'pending'"); |
| | | addPendingNotice(notices, "medical-reservation", "体检预约", "您有 %d 条体检预约待处理", "/medical-reservations", |
| | | "select count(*) as pending_count, max(submitted_at) as latest_time from service_application where deleted = 0 and application_type = 'MEDICAL_RESERVATION' and status = '待处理'"); |
| | | addPendingNotice(notices, "medical-application", "就医申请", "您有 %d 条就医申请待处理", "/medical-applications", |
| | | "select count(*) as pending_count, max(submitted_at) as latest_time from service_application where deleted = 0 and application_type = 'MEDICAL_APPLICATION' and status = '待处理'"); |
| | | notices.sort((left, right) -> String.valueOf(right.get("time")).compareTo(String.valueOf(left.get("time")))); |
| | | return notices; |
| | | } |
| | | |
| | | private void addPendingNotice(List<Map<String, Object>> notices, String id, String category, String template, String path, String sql) { |
| | | Map<String, Object> summary = DatabaseRows.normalize(jdbcTemplate.queryForMap(sql)); |
| | | long count = summary.get("pendingCount") instanceof Number number ? number.longValue() : 0; |
| | | if (count == 0) return; |
| | | Map<String, Object> notice = new LinkedHashMap<>(); |
| | | notice.put("id", id); |
| | | notice.put("category", category); |
| | | notice.put("content", template.formatted(count)); |
| | | notice.put("time", summary.get("latestTime")); |
| | | notice.put("path", path); |
| | | notices.add(notice); |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> merchant(Long id) { |
| | | Map<String, Object> result = one(""" |
| | | select id, merchant_name, merchant_type, contact_name, contact_mobile, logo_url, cover_url, |
| | | 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 merchant.id, merchant.merchant_name, merchant.merchant_type, merchant.contact_name, merchant.contact_mobile, |
| | | merchant.logo_url, merchant.cover_url, merchant.address, merchant.business_status, merchant.longitude, |
| | | merchant.latitude, merchant.can_login_merchant_app, merchant.group_buying_enabled, merchant.usage_instructions, |
| | | merchant.rights_text, merchant.merchant_rights_config, merchant.service_label, merchant.is_recommend, |
| | | merchant.sort_no, merchant.status, merchant.created_at, merchant.updated_at, |
| | | s.external_link_type as car_wash_external_link_type, |
| | | s.external_mini_program_app_id as car_wash_external_mini_program_app_id, |
| | | s.external_mini_program_path as car_wash_external_mini_program_path, |
| | | s.external_web_url as car_wash_external_web_url, |
| | | (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 = ? |
| | | left join transport_car_wash_service s on s.merchant_id = merchant.id and s.deleted = 0 |
| | | where merchant.deleted = 0 and merchant.id = ? |
| | | """, id); |
| | | result.put("businessHours", DatabaseRows.normalize(jdbcTemplate.queryForList(""" |
| | | select start_time, end_time |
| | |
| | | |
| | | @Transactional |
| | | public Map<String, Object> createMerchant(MerchantCreateRequest request) { |
| | | validateScenicMerchantCreate(request); |
| | | validateMerchantAddress(request.merchantType(), request.address()); |
| | | validateRequiredMerchantCreateFields(request); |
| | | jdbcTemplate.update(""" |
| | | insert into merchant(merchant_name, merchant_type, contact_name, contact_mobile, address, |
| | | logo_url, cover_url, business_status, longitude, latitude, can_login_merchant_app, group_buying_enabled, |
| | |
| | | request.recommend() == null ? defaultRecommend(request.merchantType()) : request.recommend(), |
| | | request.sortNo() == null ? 0 : request.sortNo()); |
| | | Long id = jdbcTemplate.queryForObject("select last_insert_id()", Long.class); |
| | | ensureParkingBenefit(id, request.merchantType()); |
| | | Long storeId = createDefaultStore(id, request.merchantName(), request.address()); |
| | | syncMerchantBusinessHours(id, storeId, request.businessHours()); |
| | | if (hasMerchantAppAccess(request)) { |
| | |
| | | return one("select * from merchant where id = ?", id); |
| | | } |
| | | |
| | | void validateMerchantAddress(String merchantType, String address) { |
| | | if (!"公寓租赁".equals(merchantType) && (address == null || address.isBlank())) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "请输入商家地址"); |
| | | } |
| | | } |
| | | |
| | | private boolean hasMerchantAppAccess(MerchantCreateRequest request) { |
| | | return ("餐饮服务".equals(request.merchantType()) || "景区服务".equals(request.merchantType())) |
| | | return supportsMerchantAppLogin(request.merchantType()) |
| | | && (request.canLoginMerchantApp() == null || request.canLoginMerchantApp() == 1); |
| | | } |
| | | |
| | | private void validateScenicMerchantCreate(MerchantCreateRequest request) { |
| | | if (!"景区服务".equals(request.merchantType())) { |
| | | boolean supportsMerchantAppLogin(String merchantType) { |
| | | return MERCHANT_APP_LOGIN_TYPES.contains(merchantType); |
| | | } |
| | | |
| | | private void validateRequiredMerchantCreateFields(MerchantCreateRequest request) { |
| | | if (!Set.of("景区服务", "餐饮服务").contains(request.merchantType())) { |
| | | return; |
| | | } |
| | | if (request.businessHours() == null || request.businessHours().isEmpty() |
| | |
| | | request.status() == null ? 1 : request.status(), |
| | | id); |
| | | ensureUpdated(updated); |
| | | ensureParkingBenefit(id, request.merchantType()); |
| | | Long storeId = findOrCreateDefaultStore(id, request.merchantName(), request.address()); |
| | | syncMerchantBusinessHours(id, storeId, request.businessHours()); |
| | | if (request.loginPassword() != null && !request.loginPassword().isBlank()) { |
| | | if (request.loginPassword().length() < 6 || request.loginPassword().length() > 64) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "商户登录密码长度须为6至64位"); |
| | | } |
| | | int passwordUpdated = jdbcTemplate.update(""" |
| | | update merchant_operator |
| | | set password_hash = ? |
| | | where merchant_id = ? and role_code = 'owner' and deleted = 0 |
| | | """, MerchantPasswordHasher.encode(request.loginPassword()), id); |
| | | if (passwordUpdated == 0) { |
| | | createMerchantOperator(id, request); |
| | | } |
| | | } |
| | | if ((request.loginPassword() == null || request.loginPassword().isBlank()) && supportsMerchantAppLogin(request.merchantType()) |
| | | && (request.canLoginMerchantApp() == null || request.canLoginMerchantApp() == 1) |
| | | && (request.loginAccount() != null && !request.loginAccount().isBlank())) { |
| | | createMerchantOperator(id, request); |
| | | } |
| | | return merchant(id); |
| | | } |
| | | |
| | | private void createMerchantOperator(Long merchantId, MerchantUpdateRequest request) { |
| | | if (request.loginAccount() == null || !request.loginAccount().trim().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 = ? and deleted = 0", Integer.class, request.loginAccount().trim()); |
| | | if (occupied != null && occupied > 0) throw new BusinessException(ErrorCode.CONFLICT, "登录账号已存在"); |
| | | jdbcTemplate.update("insert into merchant_operator(merchant_id, account, password_hash, display_name, role_code, status) values (?, ?, ?, ?, 'owner', 1)", |
| | | merchantId, request.loginAccount().trim(), MerchantPasswordHasher.encode(request.loginPassword()), request.merchantName().trim()); |
| | | } |
| | | |
| | | private void ensureParkingBenefit(Long merchantId, String merchantType) { |
| | | if (!"停车服务".equals(merchantType) && !"parking".equalsIgnoreCase(merchantType)) return; |
| | | jdbcTemplate.update(""" |
| | | insert into transport_parking_benefit(merchant_id, talent_levels, benefit_text, action_mode, plate_form_mode, usage_instructions, rule_text, status, deleted) |
| | | select ?, '', '停车权益', 'plate', 'withParking', |
| | | '登记车牌后可享受当前停车场的人才专属停车权益。', |
| | | '具体免费时长、适用时段和停车场范围以现场说明为准。', 1, 0 |
| | | where not exists (select 1 from transport_parking_benefit where merchant_id = ? and deleted = 0) |
| | | """, merchantId, merchantId); |
| | | } |
| | | |
| | | @Transactional |
| | |
| | | @Transactional |
| | | public Map<String, Object> createProduct(ProductCreateRequest request) { |
| | | ensureMerchantExists(request.merchantId()); |
| | | validateProductRights(request.rightsConfig()); |
| | | validateProductConfiguration(request.merchantId(), request.salePrice(), request.productImages()); |
| | | validateProductRights(request.merchantId(), request.rightsConfig()); |
| | | String productNo = BusinessIdGenerator.next("P"); |
| | | BigDecimal subsidyAmount = valueOrZero(request.subsidyAmount()); |
| | | jdbcTemplate.update(""" |
| | |
| | | @Transactional |
| | | public Map<String, Object> updateProduct(Long id, ProductUpdateRequest request) { |
| | | ensureMerchantExists(request.merchantId()); |
| | | validateProductRights(request.rightsConfig()); |
| | | validateProductConfiguration(request.merchantId(), request.salePrice(), request.productImages()); |
| | | validateProductRights(request.merchantId(), request.rightsConfig()); |
| | | BigDecimal subsidyAmount = valueOrZero(request.subsidyAmount()); |
| | | int updated = jdbcTemplate.update(""" |
| | | update merchant_product |
| | |
| | | ) { |
| | | StringBuilder sql = new StringBuilder(""" |
| | | select c.id, c.merchant_id, m.merchant_name, c.coupon_name, c.condition_type, |
| | | c.condition_amount, c.coupon_amount, c.talent_levels, c.valid_start, c.valid_end, |
| | | c.condition_amount, c.coupon_amount, c.talent_levels, c.talent_level_quantities, c.valid_start, c.valid_end, |
| | | c.product_scope, c.store_consume, c.government_subsidy, c.use_count, c.status, |
| | | c.created_at, c.updated_at, |
| | | (select count(*) from merchant_coupon_product cp where cp.coupon_id = c.id) as product_count, |
| | |
| | | public Map<String, Object> coupon(Long id) { |
| | | Map<String, Object> result = one(""" |
| | | select c.id, c.merchant_id, m.merchant_name, c.coupon_name, c.condition_type, |
| | | c.condition_amount, c.coupon_amount, c.talent_levels, c.valid_start, c.valid_end, |
| | | c.condition_amount, c.coupon_amount, c.talent_levels, c.talent_level_quantities, c.valid_start, c.valid_end, |
| | | c.product_scope, c.store_consume, c.government_subsidy, c.use_count, c.status, |
| | | c.created_at, c.updated_at |
| | | from merchant_coupon c |
| | |
| | | public Map<String, Object> createCoupon(CouponCreateRequest request) { |
| | | ensureMerchantExists(request.merchantId()); |
| | | validateCouponConfiguration(request.validStart(), request.validEnd(), request.productScope(), request.productIds()); |
| | | validateCouponAmount(request.couponAmount()); |
| | | validateTalentLevelQuantities(request.talentLevelQuantities()); |
| | | jdbcTemplate.update(""" |
| | | insert into merchant_coupon(merchant_id, coupon_name, condition_type, condition_amount, |
| | | coupon_amount, talent_levels, valid_start, valid_end, |
| | | coupon_amount, talent_levels, talent_level_quantities, valid_start, valid_end, |
| | | product_scope, store_consume, government_subsidy, status) |
| | | values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1) |
| | | values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1) |
| | | """, |
| | | request.merchantId(), |
| | | request.couponName(), |
| | |
| | | request.conditionAmount(), |
| | | valueOrZero(request.couponAmount()), |
| | | joinTalentLevels(request.talentLevels()), |
| | | levelQuantitiesJson(request.talentLevelQuantities()), |
| | | blankToNull(request.validStart()), |
| | | blankToNull(request.validEnd()), |
| | | normalizeProductScope(request.productScope()), |
| | |
| | | public Map<String, Object> updateCoupon(Long id, CouponUpdateRequest request) { |
| | | ensureMerchantExists(request.merchantId()); |
| | | validateCouponConfiguration(request.validStart(), request.validEnd(), request.productScope(), request.productIds()); |
| | | validateCouponAmount(request.couponAmount()); |
| | | validateTalentLevelQuantities(request.talentLevelQuantities()); |
| | | int updated = jdbcTemplate.update(""" |
| | | update merchant_coupon |
| | | set merchant_id = ?, coupon_name = ?, condition_type = ?, condition_amount = ?, |
| | | coupon_amount = ?, talent_levels = ?, valid_start = ?, valid_end = ?, |
| | | coupon_amount = ?, talent_levels = ?, talent_level_quantities = ?, valid_start = ?, valid_end = ?, |
| | | product_scope = ?, store_consume = ?, government_subsidy = ?, status = ? |
| | | where deleted = 0 and id = ? |
| | | """, |
| | |
| | | request.conditionAmount(), |
| | | valueOrZero(request.couponAmount()), |
| | | joinTalentLevels(request.talentLevels()), |
| | | levelQuantitiesJson(request.talentLevelQuantities()), |
| | | blankToNull(request.validStart()), |
| | | blankToNull(request.validEnd()), |
| | | normalizeProductScope(request.productScope()), |
| | |
| | | return new PageResult<>(records, safePage, safeSize, total == null ? 0 : total); |
| | | } |
| | | |
| | | private void applyBusinessStatus(List<Map<String, Object>> merchants) { |
| | | if (merchants.isEmpty()) return; |
| | | List<Long> merchantIds = merchants.stream().map(row -> Long.valueOf(String.valueOf(row.get("id")))).toList(); |
| | | String placeholders = String.join(",", java.util.Collections.nCopies(merchantIds.size(), "?")); |
| | | List<Map<String, Object>> periods = DatabaseRows.normalize(jdbcTemplate.queryForList(""" |
| | | select h.merchant_id, h.start_time, h.end_time |
| | | from merchant_business_hours h |
| | | join merchant_store s on s.id = h.store_id |
| | | where s.status = 1 and s.deleted = 0 and h.merchant_id in (""" + placeholders + ")", merchantIds.toArray())); |
| | | LocalTime now = LocalTime.now(ZoneId.of("Asia/Shanghai")); |
| | | Set<Long> openMerchantIds = new HashSet<>(); |
| | | for (Map<String, Object> period : periods) { |
| | | if (isWithinBusinessPeriod(now, String.valueOf(period.get("startTime")), String.valueOf(period.get("endTime")))) { |
| | | openMerchantIds.add(Long.valueOf(String.valueOf(period.get("merchantId")))); |
| | | } |
| | | } |
| | | for (Map<String, Object> merchant : merchants) { |
| | | Long merchantId = Long.valueOf(String.valueOf(merchant.get("id"))); |
| | | boolean enabled = "1".equals(String.valueOf(merchant.get("status"))); |
| | | merchant.put("businessStatus", enabled && openMerchantIds.contains(merchantId) ? "营业中" : "休息中"); |
| | | } |
| | | } |
| | | |
| | | private boolean isWithinBusinessPeriod(LocalTime now, String startValue, String endValue) { |
| | | try { |
| | | LocalTime start = LocalTime.parse(startValue); |
| | | LocalTime end = LocalTime.parse(endValue); |
| | | return start.isBefore(end) |
| | | ? !now.isBefore(start) && now.isBefore(end) |
| | | : !now.isBefore(start) || now.isBefore(end); |
| | | } catch (DateTimeParseException ignored) { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | private void ensureUpdated(int updated) { |
| | | if (updated == 0) { |
| | | throw new BusinessException(ErrorCode.NOT_FOUND, "Data not found"); |
| | |
| | | private BigDecimal valueOrZero(BigDecimal value) { |
| | | return value == null ? BigDecimal.ZERO : value; |
| | | } |
| | | |
| | | private void validateCouponAmount(BigDecimal value) { |
| | | if (value == null || value.compareTo(new BigDecimal("0.1")) < 0 || value.compareTo(new BigDecimal("9999.9")) > 0) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "优惠券金额必须在0.1-9999.9元之间"); |
| | | } |
| | | } |
| | | private void validateTalentLevelQuantities(Map<String, Integer> values) { |
| | | if (values == null) return; |
| | | values.values().forEach(value -> { if (value == null || value < 1 || value > 99) throw new BusinessException(ErrorCode.BAD_REQUEST, "人才卡发放数量必须为1-99"); }); |
| | | } |
| | | private String levelQuantitiesJson(Map<String, Integer> values) { return values == null || values.isEmpty() ? null : JSON.toJSONString(values); } |
| | | |
| | | private String toJson(Object value) { |
| | | if (value == null) { |
| | |
| | | return "ALL".equalsIgnoreCase(value) ? "ALL" : "SELECTED"; |
| | | } |
| | | |
| | | private void validateProductRights(List<Map<String, Object>> rightsConfig) { |
| | | private void validateProductRights(Long merchantId, List<Map<String, Object>> rightsConfig) { |
| | | if (rightsConfig == null) { |
| | | return; |
| | | } |
| | | if (isHotelMerchant(merchantId) || isHealthCheckMerchant(merchantId)) { |
| | | return; |
| | | } |
| | | for (Map<String, Object> right : rightsConfig) { |
| | |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "免费次数必须为 1 至 999 的整数"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void validateProductConfiguration(Long merchantId, BigDecimal salePrice, List<String> productImages) { |
| | | if (isHealthCheckMerchant(merchantId)) { |
| | | return; |
| | | } |
| | | if (salePrice == null) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "商品金额不能为空"); |
| | | } |
| | | if (productImages == null || productImages.isEmpty()) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "请至少添加一张商品图片"); |
| | | } |
| | | } |
| | | |
| | | private boolean isHotelMerchant(Long merchantId) { |
| | | String type = merchantType(merchantId); |
| | | return "hotel".equalsIgnoreCase(type) || "酒店服务".equals(type); |
| | | } |
| | | |
| | | private boolean isHealthCheckMerchant(Long merchantId) { |
| | | String type = merchantType(merchantId); |
| | | return "healthCheck".equalsIgnoreCase(type) || "免费体检".equals(type); |
| | | } |
| | | |
| | | private String merchantType(Long merchantId) { |
| | | List<String> types = jdbcTemplate.queryForList( |
| | | "select merchant_type from merchant where deleted = 0 and id = ? limit 1", |
| | | String.class, |
| | | merchantId); |
| | | if (types.isEmpty()) { |
| | | return ""; |
| | | } |
| | | return types.get(0) == null ? "" : types.get(0).trim(); |
| | | } |
| | | |
| | | private void validateCouponConfiguration(String validStart, String validEnd, String productScope, List<Long> productIds) { |
| | |
| | | return null; |
| | | } |
| | | Safelist safelist = Safelist.relaxed() |
| | | .removeTags("style", "script") |
| | | .removeTags("script") |
| | | .addTags("span", "video", "source") |
| | | .addAttributes(":all", "style", "class") |
| | | .addAttributes("a", "target") |
| | | .addProtocols("a", "href", "https") |
| | | .addProtocols("img", "src", "https"); |
| | | .addAttributes("video", "src", "controls", "poster", "width", "height") |
| | | .addAttributes("source", "src", "type") |
| | | .addProtocols("a", "href", "http", "https") |
| | | .addProtocols("img", "src", "http", "https") |
| | | .addProtocols("video", "src", "http", "https") |
| | | .addProtocols("source", "src", "http", "https"); |
| | | return Jsoup.clean(content, safelist); |
| | | } |
| | | |