shinnosuke
2026-09-21 7844f5af457ff3b7d3f8eeac8799e8d281e635a1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
package com.talentcard.server.business.portal;
 
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
 
import com.alibaba.fastjson.JSON;
import com.talentcard.server.business.DatabaseRows;
import com.talentcard.server.common.BusinessException;
import com.talentcard.server.common.ErrorCode;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
 
@Service
public class MedicalPackagePortalService {
 
    private final JdbcTemplate jdbc;
 
    public MedicalPackagePortalService(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }
 
    @Transactional(readOnly = true)
    public List<Map<String, Object>> packages(Long merchantId) {
        medicalMerchant(merchantId);
        return query("""
                select p.id, p.product_name as package_name, p.cover_url as hero_image,
                       case when coalesce(p.sale_price, 0) = 0 then 1 else 0 end as free_status,
                       '' as applicable_cards,
                       coalesce(
                           nullif(json_unquote(json_extract(p.purchase_notice, '$.availableTime')), ''),
                           nullif(json_unquote(json_extract(p.purchase_notice, '$.validPeriod')), ''),
                           ''
                       ) as open_time,
                       p.stock, p.status, '' as unavailable_reason
                from merchant_product p
                where p.merchant_id = ? and p.deleted = 0 and p.audit_status = 2 and p.status = 1
                order by p.sort_no desc, p.id desc
                """, merchantId).stream().map(this::packageListView).toList();
    }
 
    @Transactional(readOnly = true)
    public Map<String, Object> detail(Long packageId) {
        Map<String, Object> row = packageRow(packageId);
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("packageId", row.get("packageId"));
        result.put("name", row.get("packageName"));
        result.put("heroImage", detailHeroImage(row));
        // 商户在「图文详情」里可以传多张:顶部大图取第一张,剩下的(含第一张)在这里整组返回,
        // 详情页按顺序铺出来,否则商户传了几张也只能看到一张。
        result.put("detailImages", imageUrls(row.get("productImages")).stream()
                .filter(url -> !url.isBlank())
                .toList());
        result.put("free", number(row.get("freeStatus")) == 1);
        result.put("freeLabel", number(row.get("freeStatus")) == 1 ? "免费" : "优惠");
        result.put("applicableCards", row.get("applicableCards"));
        result.put("available", available(row));
        result.put("unavailableReason", unavailableReason(row));
        result.put("hospital", Map.of(
                "hospitalId", row.get("merchantId"), "name", row.get("hospitalName"), "address", text(row.get("hospitalAddress")),
                "mobile", text(row.get("hospitalMobile")), "latitude", row.get("hospitalLatitude"), "longitude", row.get("hospitalLongitude")
        ));
        return result;
    }
 
    @Transactional(readOnly = true)
    public List<Map<String, Object>> items(Long packageId) {
        Map<String, Object> row = packageRow(packageId);
        String detailText = text(row.get("detailText")).trim();
        if (detailText.isBlank()) return List.of();
        try {
            List<Map<String, Object>> records = JSON.parseObject(detailText, new com.alibaba.fastjson.TypeReference<>() {});
            if (records == null) return List.of();
            java.util.ArrayList<Map<String, Object>> result = new java.util.ArrayList<>();
            for (int index = 0; index < records.size(); index++) {
                Map<String, Object> item = records.get(index);
                List<String> genders = JSON.parseArray(JSON.toJSONString(item.get("gender")), String.class);
                Map<String, Object> normalized = new LinkedHashMap<>();
                normalized.put("id", index + 1);
                normalized.put("name", text(item.get("name")));
                normalized.put("meaning", text(item.get("meaning")));
                normalized.put("male", genders != null && genders.contains("男"));
                normalized.put("female", genders != null && genders.contains("女"));
                result.add(normalized);
            }
            return result;
        } catch (RuntimeException ignored) {
            return List.of();
        }
    }
 
    private Map<String, Object> medicalMerchant(Long merchantId) {
        if (merchantId == null || merchantId <= 0) throw new BusinessException(ErrorCode.BAD_REQUEST, "体检医院信息不能为空");
        List<Map<String, Object>> rows = query("""
                select m.id from merchant m
                where m.id = ? and m.merchant_type_code = 'medical' and m.deleted = 0
                """, merchantId);
        if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "体检医院不存在");
        return rows.get(0);
    }
 
    private Map<String, Object> packageRow(Long packageId) {
        if (packageId == null || packageId <= 0) throw new BusinessException(ErrorCode.BAD_REQUEST, "体检套餐信息不能为空");
        List<Map<String, Object>> rows = query("""
                select p.id as package_id, p.merchant_id, p.product_name as package_name, p.cover_url as hero_image,
                       p.product_images as product_images,
                       case when coalesce(p.sale_price, 0) = 0 then 1 else 0 end as free_status,
                       '' as applicable_cards,
                       coalesce(
                           nullif(json_unquote(json_extract(p.purchase_notice, '$.availableTime')), ''),
                           nullif(json_unquote(json_extract(p.purchase_notice, '$.validPeriod')), ''),
                           ''
                       ) as open_time,
                       p.stock, p.status, '' as unavailable_reason, p.detail_text,
                       m.merchant_name as hospital_name,
                       m.address as hospital_address, m.contact_mobile as hospital_mobile, m.latitude as hospital_latitude,
                       m.longitude as hospital_longitude
                from merchant_product p join merchant m on m.id = p.merchant_id and m.deleted = 0 and m.merchant_type_code = 'medical'
                where p.id = ? and p.deleted = 0 and p.audit_status = 2
                """, packageId);
        if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "体检套餐不存在");
        return rows.get(0);
    }
 
    /**
     * 详情页大图:优先用商户在「商品详情 → 图文详情」上传的第一张图({@code product_images}),
     * 没配时才回落商品封面图。
     *
     * <p>图文详情才是商户为详情页配的图,封面图只是列表里的缩略图。详情页直接取封面,
     * 商户改了图文详情也看不到任何变化,表现成"详情图片没生效"。
     */
    private String detailHeroImage(Map<String, Object> row) {
        for (String image : imageUrls(row.get("productImages"))) {
            if (!image.isBlank()) return image;
        }
        return text(row.get("heroImage"));
    }
 
    /** {@code product_images} 是 json 数组列(驱动按字符串返回);内容不是数组或格式坏掉时按"没配"处理。 */
    private List<String> imageUrls(Object value) {
        String json = text(value).trim();
        if (!json.startsWith("[")) return List.of();
        try {
            List<String> parsed = JSON.parseArray(json, String.class);
            if (parsed == null) return List.of();
            return parsed.stream().map(item -> item == null ? "" : item.trim()).toList();
        } catch (RuntimeException ignored) {
            return List.of();
        }
    }
 
    private Map<String, Object> packageListView(Map<String, Object> row) {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("id", row.get("id"));
        result.put("name", row.get("packageName"));
        result.put("heroImage", row.get("heroImage"));
        result.put("openTime", row.get("openTime"));
        result.put("free", number(row.get("freeStatus")) == 1);
        result.put("applicableCards", row.get("applicableCards"));
        result.put("available", available(row));
        result.put("unavailableReason", unavailableReason(row));
        return result;
    }
 
    private boolean available(Map<String, Object> row) {
        return number(row.get("status")) == 1 && number(row.get("stock")) != 0;
    }
 
    private String unavailableReason(Map<String, Object> row) {
        if (number(row.get("status")) != 1) return text(row.get("unavailableReason"), "套餐已停用");
        if (number(row.get("stock")) == 0) return text(row.get("unavailableReason"), "当前库存不足");
        return "";
    }
 
    private List<Map<String, Object>> query(String sql, Object... args) {
        return DatabaseRows.normalize(jdbc.queryForList(sql, args));
    }
 
    private int number(Object value) { return value instanceof Number number ? number.intValue() : Integer.parseInt(String.valueOf(value)); }
    private String text(Object value) { return value == null ? "" : String.valueOf(value); }
    private String text(Object value, String fallback) { String valueText = text(value).trim(); return valueText.isBlank() ? fallback : valueText; }
}