| | |
| | | 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, 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()); |
| | | } |
| | |
| | | } |
| | | |
| | | /** 详情页轮播图按商家配置的服务类型:其余类型顶部图仍用封面图或团单封面。 */ |
| | | private static final List<String> HERO_IMAGE_MERCHANT_TYPES = List.of("children", "approval"); |
| | | // 只有子女托管走通用的 merchant_hero_image;加油服务用 transport_fuel_hero_image。 |
| | | // 行政审批不再单独配详情页图片(人才端直接用商家封面图),所以不在这里。 |
| | | private static final List<String> HERO_IMAGE_MERCHANT_TYPES = List.of("children"); |
| | | |
| | | /** |
| | | * 同步商家详情页轮播图。 |
| | |
| | | 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.stock, %s, 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 = ? |
| | | """); |
| | | """.formatted(salesCountSelect("p.id", "p"))); |
| | | StringBuilder countSql = new StringBuilder(""" |
| | | select count(*) |
| | | from merchant_product p |
| | |
| | | select r.id, r.product_id, r.merchant_id, m.merchant_name, m.merchant_type, m.merchant_type_code, m.contact_mobile, |
| | | r.product_no, r.product_type, r.product_name, |
| | | r.subtitle, r.cover_url, r.original_price, r.sale_price, r.subsidy_amount, |
| | | r.is_government_subsidy, r.stock, r.sales_count, r.detail_text, r.benefit_desc, |
| | | r.is_government_subsidy, r.stock, %s, r.detail_text, r.benefit_desc, |
| | | r.purchase_notice, r.valid_start_time, r.valid_end_time, r.product_images, r.rights_config, |
| | | r.audit_status, r.audit_remark, r.audit_by, r.audit_time, r.submit_time, |
| | | r.source_created_at as created_at, p.status |
| | |
| | | left join merchant_product p on p.id = r.product_id |
| | | left join merchant m on m.id = r.merchant_id |
| | | where r.deleted = 0 |
| | | """); |
| | | and (r.product_id is null or r.id = ( |
| | | select max(newer.id) from merchant_product_audit_record newer |
| | | where newer.deleted = 0 and newer.product_id = r.product_id)) |
| | | """.formatted(salesCountSelect("r.product_id", "p"))); |
| | | StringBuilder countSql = new StringBuilder(""" |
| | | select count(*) |
| | | from merchant_product_audit_record r |
| | | left join merchant m on m.id = r.merchant_id |
| | | where r.deleted = 0 |
| | | and (r.product_id is null or r.id = ( |
| | | select max(newer.id) from merchant_product_audit_record newer |
| | | where newer.deleted = 0 and newer.product_id = r.product_id)) |
| | | """); |
| | | List<Object> args = new ArrayList<>(); |
| | | if (merchantId != null) { |
| | |
| | | select p.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.is_government_subsidy, p.stock, %s, 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.id = ? |
| | | """, id); |
| | | """.formatted(salesCountSelect("p.id", "p")), id); |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | |
| | | select r.id, r.product_id, r.merchant_id, m.merchant_name, m.merchant_type, m.merchant_type_code, m.contact_mobile, |
| | | r.product_no, r.product_type, r.product_name, r.subtitle, r.cover_url, |
| | | r.original_price, r.sale_price, r.subsidy_amount, r.is_government_subsidy, |
| | | r.stock, r.sales_count, r.detail_text, r.benefit_desc, r.purchase_notice, |
| | | r.stock, %s, r.detail_text, r.benefit_desc, r.purchase_notice, |
| | | r.valid_start_time, r.valid_end_time, r.product_images, r.rights_config, |
| | | r.audit_status, r.audit_remark, r.audit_by, r.audit_time, r.submit_time, |
| | | r.source_created_at as created_at, p.status |
| | |
| | | left join merchant_product p on p.id = r.product_id |
| | | left join merchant m on m.id = r.merchant_id |
| | | where r.deleted = 0 and r.id = ? |
| | | """, auditRecordId); |
| | | """.formatted(salesCountSelect("r.product_id", "p")), auditRecordId); |
| | | } |
| | | |
| | | @Transactional |
| | |
| | | ensureMerchantExists(request.merchantId()); |
| | | validateProductConfiguration(request.merchantId(), request.salePrice(), request.productImages()); |
| | | validateProductRights(request.merchantId(), request.rightsConfig()); |
| | | Long productId = resolveProductId(id); |
| | | BigDecimal subsidyAmount = valueOrZero(request.subsidyAmount()); |
| | | int updated = jdbcTemplate.update(""" |
| | | update merchant_product |
| | | set merchant_id = ?, product_type = ?, product_name = ?, subtitle = ?, cover_url = ?, |
| | | original_price = ?, sale_price = ?, subsidy_amount = ?, is_government_subsidy = ?, |
| | | stock = ?, detail_text = ?, benefit_desc = ?, purchase_notice = ?, valid_start_time = ?, |
| | | valid_end_time = ?, product_images = ?, rights_config = ?, status = 0, sort_no = ?, |
| | | audit_status = 1, audit_remark = null, audit_time = null, submit_time = now() |
| | | where deleted = 0 and id = ? |
| | | """, |
| | | int updated = jdbcTemplate.update(""" |
| | | update merchant_product |
| | | set merchant_id = ?, product_type = ?, product_name = ?, subtitle = ?, cover_url = ?, |
| | | original_price = ?, sale_price = ?, subsidy_amount = ?, is_government_subsidy = ?, |
| | | stock = ?, detail_text = ?, benefit_desc = ?, purchase_notice = ?, valid_start_time = ?, |
| | | valid_end_time = ?, product_images = ?, rights_config = ?, status = 0, sort_no = ?, |
| | | audit_status = 1, audit_remark = null, audit_time = null, submit_time = now() |
| | | where deleted = 0 and id = ? |
| | | """, |
| | | request.merchantId(), |
| | | request.productType(), |
| | | request.productName(), |
| | |
| | | toJson(request.productImages()), |
| | | toJson(request.rightsConfig()), |
| | | request.sortNo() == null ? 0 : request.sortNo(), |
| | | id); |
| | | productId); |
| | | ensureUpdated(updated); |
| | | appendProductAuditRecord(id); |
| | | return product(id); |
| | | appendProductAuditRecord(productId); |
| | | return product(productId); |
| | | } |
| | | |
| | | @Transactional |
| | | public Map<String, Object> updateProductStatus(Long id, Byte status) { |
| | | Number auditStatus = (Number) product(id).get("auditStatus"); |
| | | Long productId = resolveProductId(id); |
| | | Number auditStatus = (Number) product(productId).get("auditStatus"); |
| | | if (auditStatus == null || auditStatus.intValue() != 2) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "Only approved products can be listed or unlisted"); |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "只有审核通过的团单才能上架或下架"); |
| | | } |
| | | int updated = jdbcTemplate.update("update merchant_product set status = ? where deleted = 0 and id = ?", status, id); |
| | | int updated = jdbcTemplate.update("update merchant_product set status = ? where deleted = 0 and id = ?", status, productId); |
| | | ensureUpdated(updated); |
| | | return product(id); |
| | | return product(productId); |
| | | } |
| | | |
| | | @Transactional |
| | | public Map<String, Object> deleteProduct(Long id) { |
| | | Map<String, Object> existing = product(id); |
| | | int updated = jdbcTemplate.update("update merchant_product set deleted = 1 where deleted = 0 and id = ?", id); |
| | | Long productId = resolveProductId(id); |
| | | Map<String, Object> existing = product(productId); |
| | | int updated = jdbcTemplate.update("update merchant_product set deleted = 1 where deleted = 0 and id = ?", productId); |
| | | ensureUpdated(updated); |
| | | return existing; |
| | | } |
| | | |
| | | @Transactional |
| | | public Map<String, Object> auditProduct(Long id, ProductAuditRequest request) { |
| | | Long productId = resolveProductId(id); |
| | | List<Map<String, Object>> pendingRecords = DatabaseRows.normalize(jdbcTemplate.queryForList(""" |
| | | select id from merchant_product_audit_record |
| | | where product_id = ? and deleted = 0 and audit_status = 1 |
| | | order by id desc limit 1 |
| | | """, id)); |
| | | """, productId)); |
| | | if (!pendingRecords.isEmpty()) { |
| | | return auditProductAuditRecord(Long.valueOf(String.valueOf(pendingRecords.get(0).get("id"))), request); |
| | | } |
| | | if (request.auditStatus() != 2 && request.auditStatus() != 3) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "Invalid audit status"); |
| | | } |
| | | Number currentAuditStatus = (Number) product(id).get("auditStatus"); |
| | | Number currentAuditStatus = (Number) product(productId).get("auditStatus"); |
| | | if (currentAuditStatus == null || currentAuditStatus.intValue() != 1) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "Product is not pending audit"); |
| | | } |
| | |
| | | update merchant_product |
| | | set audit_status = ?, audit_remark = ?, audit_time = now(), status = ? |
| | | where deleted = 0 and id = ? |
| | | """, request.auditStatus(), request.auditRemark(), request.auditStatus() == 2 ? 1 : 0, id); |
| | | """, request.auditStatus(), request.auditRemark(), request.auditStatus() == 2 ? 1 : 0, productId); |
| | | ensureUpdated(updated); |
| | | return product(id); |
| | | return product(productId); |
| | | } |
| | | |
| | | @Transactional |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 团单在后台有两个 id 口径:商家编辑页的团单列表给的是商品 id,团单审核列表给的是审核记录 id。 |
| | | * 两个都认——否则拿另一口径的 id 去改团单,就只会在 update 影响 0 行之后抛出英文的 |
| | | * {@code Data not found},现场完全看不出是"id 口径不一致"。 |
| | | */ |
| | | private Long resolveProductId(Long id) { |
| | | if (id == null) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "团单参数不能为空"); |
| | | } |
| | | List<Long> direct = jdbcTemplate.queryForList( |
| | | "select id from merchant_product where deleted = 0 and id = ?", |
| | | Long.class, |
| | | id); |
| | | if (!direct.isEmpty()) { |
| | | return direct.get(0); |
| | | } |
| | | List<Long> byAuditRecord = jdbcTemplate.queryForList(""" |
| | | select p.id |
| | | from merchant_product_audit_record r |
| | | join merchant_product p on p.id = r.product_id and p.deleted = 0 |
| | | where r.deleted = 0 and r.id = ? |
| | | limit 1 |
| | | """, Long.class, id); |
| | | if (!byAuditRecord.isEmpty()) { |
| | | return byAuditRecord.get(0); |
| | | } |
| | | throw new BusinessException(ErrorCode.NOT_FOUND, "团单不存在或已被删除,请刷新列表后重试"); |
| | | } |
| | | |
| | | private void ensureMerchantExists(Long merchantId) { |
| | | Long count = jdbcTemplate.queryForObject( |
| | | "select count(*) from merchant where deleted = 0 and id = ?", |
| | |
| | | return Boolean.TRUE.equals(value == null ? defaultValue : value) ? (byte) 1 : (byte) 0; |
| | | } |
| | | |
| | | /** |
| | | * 团单销量 = 该商品的有效订单数(已支付,且未取消/未退款)。 |
| | | * |
| | | * 商品表上的 sales_count 只在部分流程(如酒店房型预约)里累加,普通团单下单后一直是 0, |
| | | * 后台列表就统计不出销量。这里直接按订单算,同时保留 sales_count 作为下限,兼容历史数据。 |
| | | */ |
| | | String salesCountSelect(String productIdExpression, String productAlias) { |
| | | return "greatest(coalesce(" + productAlias + ".sales_count, 0), (select count(*) from benefit_order bo" |
| | | + " where bo.deleted = 0 and bo.product_id = " + productIdExpression |
| | | + " and bo.order_status = 1 and (bo.refund_status is null or bo.refund_status = 0))) as sales_count"; |
| | | } |
| | | |
| | | private List<Long> couponProductIds(Long couponId) { |
| | | return jdbcTemplate.queryForList( |
| | | "select product_id from merchant_coupon_product where coupon_id = ? order by id", |
| | |
| | | |
| | | private List<Map<String, Object>> couponProducts(Long couponId) { |
| | | return DatabaseRows.normalize(jdbcTemplate.queryForList(""" |
| | | select p.id, p.product_name, p.cover_url, p.sale_price, p.sales_count, p.submit_time, p.created_at, |
| | | select p.id, p.product_name, p.cover_url, p.sale_price, %s, p.submit_time, p.created_at, |
| | | p.audit_status, p.status |
| | | from merchant_coupon_product cp |
| | | join merchant_product p on p.id = cp.product_id |
| | | where cp.coupon_id = ? and p.deleted = 0 |
| | | order by cp.id |
| | | """, couponId)); |
| | | """.formatted(salesCountSelect("p.id", "p")), couponId)); |
| | | } |
| | | |
| | | private void syncCouponProducts(Long couponId, List<Long> productIds, String productScope) { |