shinnosuke
2026-08-13 f7b915a08e3407b54a589ad0999fdd4c69118557
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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
package com.talentcard.server.business.merchant;
 
import java.math.BigDecimal;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
import com.alibaba.fastjson.JSON;
import com.talentcard.server.business.DatabaseRows;
import com.talentcard.server.business.PageResult;
import com.talentcard.server.business.merchant.auth.MerchantPrincipal;
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 MerchantManagementService {
    private static final List<String> NOTICE_FIELDS = List.of("validity", "availableTime", "usage", "otherRules");
 
    private final JdbcTemplate jdbc;
 
    public MerchantManagementService(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }
 
    @Transactional(readOnly = true)
    public Map<String, Object> profile(MerchantPrincipal principal) {
        Map<String, Object> merchant = merchant(principal.merchantId());
        Map<String, Object> store = store(principal);
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("merchantId", principal.merchantId());
        result.put("storeId", principal.storeId());
        result.put("merchantName", merchant.get("merchantName"));
        result.put("storeName", store.get("storeName"));
        result.put("address", value(store.get("address"), value(merchant.get("address"), "")));
        result.put("coverUrl", value(merchant.get("coverUrl"), ""));
        result.put("businessStatus", isBusinessOpen(merchant, principal.storeId()) ? "open" : "closed");
        result.put("updatedAt", merchant.get("updatedAt"));
        Long productCount = jdbc.queryForObject("select count(*) from merchant_product where merchant_id = ? and deleted = 0 and status = 1 and audit_status = 2", Long.class, principal.merchantId());
        result.put("onlineProductCount", productCount == null ? 0 : productCount);
        return result;
    }
 
    @Transactional
    public Map<String, Object> updateBusinessStatus(MerchantPrincipal principal, MerchantBusinessStatusRequest request) {
        String status = normalizeBusinessStatus(request.status());
        merchant(principal.merchantId());
        store(principal);
        jdbc.update("update merchant set business_status = ? where id = ? and deleted = 0", status.toUpperCase(), principal.merchantId());
        return Map.of(
                "businessStatus", isBusinessOpen(merchant(principal.merchantId()), principal.storeId()) ? "open" : "closed",
                "updatedAt", java.time.LocalDateTime.now().toString());
    }
 
    @Transactional(readOnly = true)
    public Map<String, Object> businessHours(MerchantPrincipal principal) {
        store(principal);
        List<Map<String, Object>> periods = rows("""
                select start_time, end_time, sort_no
                from merchant_business_hours
                where merchant_id = ? and store_id = ?
                order by sort_no asc, id asc
                """, principal.merchantId(), principal.storeId()).stream().map(row -> Map.<String, Object>of(
                "start", value(row.get("startTime"), ""),
                "end", value(row.get("endTime"), "")
        )).toList();
        return Map.of("periods", periods);
    }
 
    @Transactional
    public Map<String, Object> updateBusinessHours(MerchantPrincipal principal, MerchantBusinessHoursRequest request) {
        store(principal);
        List<MerchantBusinessHoursRequest.Period> periods = new ArrayList<>(request.periods());
        validatePeriods(periods);
        periods.sort((left, right) -> left.start().compareTo(right.start()));
        jdbc.update("delete from merchant_business_hours where merchant_id = ? and store_id = ?", principal.merchantId(), principal.storeId());
        for (int index = 0; index < periods.size(); index++) {
            MerchantBusinessHoursRequest.Period period = periods.get(index);
            jdbc.update("""
                    insert into merchant_business_hours(merchant_id, store_id, start_time, end_time, sort_no)
                    values (?, ?, ?, ?, ?)
                    """, principal.merchantId(), principal.storeId(), period.start(), period.end(), (index + 1) * 10);
        }
        return businessHours(principal);
    }
 
    @Transactional(readOnly = true)
    public PageResult<Map<String, Object>> products(MerchantPrincipal principal, String scope, int page, int size) {
        store(principal);
        int safePage = Math.max(page, 1);
        int safeSize = Math.min(Math.max(size, 1), 50);
        ProductScope productScope = ProductScope.from(scope);
        String conditions = " where merchant_id = ? and deleted = 0" + productScope.conditions();
        List<Map<String, Object>> records = rows("""
                select id, product_no, product_name, cover_url, sale_price, status, audit_status, audit_remark, submit_time, updated_at
                from merchant_product
                """ + conditions + " order by updated_at desc, id desc limit ? offset ?", principal.merchantId(), safeSize, (safePage - 1) * safeSize)
                .stream().map(this::toProductListItem).toList();
        Long total = jdbc.queryForObject("select count(*) from merchant_product" + conditions, Long.class, principal.merchantId());
        return new PageResult<>(records, safePage, safeSize, total == null ? 0 : total);
    }
 
    @Transactional(readOnly = true)
    public Map<String, Object> product(MerchantPrincipal principal, Long productId) {
        store(principal);
        List<Map<String, Object>> records = rows("""
                select id, product_no, product_name, product_type, cover_url, original_price, sale_price, detail_text,
                       purchase_notice, product_images, rights_config, benefit_desc, status, audit_status, audit_remark,
                       submit_time, updated_at
                from merchant_product where id = ? and merchant_id = ? and deleted = 0
                """, productId, principal.merchantId());
        if (records.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "商品不存在");
        return toProductDetail(records.get(0));
    }
 
    @Transactional
    public Map<String, Object> updateProduct(MerchantPrincipal principal, Long productId, MerchantProductUpdateRequest request) {
        store(principal);
        product(principal, productId);
        String coverUrl = request.coverUrl() == null ? "" : request.coverUrl().trim();
        if (coverUrl.isEmpty()) throw new BusinessException(ErrorCode.BAD_REQUEST, "请上传商品封面图");
        String images = jsonArray(request.productImages() == null ? List.of() : request.productImages());
        Map<String, String> notice = new LinkedHashMap<>();
        for (String field : NOTICE_FIELDS) notice.put(field, request.purchaseNotice() == null ? "" : value(request.purchaseNotice().get(field), ""));
        jdbc.update("""
                update merchant_product
                set product_name = ?, sale_price = ?, cover_url = ?, product_images = ?, detail_text = ?, purchase_notice = ?,
                    audit_status = 1, status = 0, submit_time = now(), audit_remark = null
                where id = ? and merchant_id = ? and deleted = 0
                """, request.productName().trim(), request.salePrice(), coverUrl, images,
                value(request.detailText(), ""), jsonObject(notice), productId, principal.merchantId());
        return product(principal, productId);
    }
 
    @Transactional
    public Map<String, Object> updateProductStatus(MerchantPrincipal principal, Long productId, MerchantProductStatusRequest request) {
        store(principal);
        Map<String, Object> product = product(principal, productId);
        String status = request.status().trim().toLowerCase();
        if (!List.of("online", "offline").contains(status)) throw new BusinessException(ErrorCode.BAD_REQUEST, "商品状态无效");
        if ("online".equals(status) && !"approved".equals(product.get("auditStatus"))) {
            throw new BusinessException(ErrorCode.CONFLICT, "商品审核通过后才能上架");
        }
        jdbc.update("update merchant_product set status = ? where id = ? and merchant_id = ? and deleted = 0",
                "online".equals(status) ? 1 : 0, productId, principal.merchantId());
        return product(principal, productId);
    }
 
    @Transactional(readOnly = true)
    public List<Map<String, Object>> benefits(MerchantPrincipal principal) {
        store(principal);
        List<Map<String, Object>> configured = configuredBenefits(principal.merchantId());
        if (!configured.isEmpty()) return configured;
        return rows("""
                select card_level, card_name, benefit_text, theme
                from merchant_card_benefit
                where merchant_id = ? and status = 1
                order by sort_no desc, id desc
                """, principal.merchantId()).stream().map(row -> Map.<String, Object>of(
                "cardLevel", value(row.get("cardLevel"), ""),
                "cardName", value(row.get("cardName"), ""),
                "benefitText", value(row.get("benefitText"), ""),
                "theme", value(row.get("theme"), "excellent")
        )).toList();
    }
 
    private List<Map<String, Object>> configuredBenefits(Long merchantId) {
        List<Map<String, Object>> merchants = rows("select merchant_rights_config from merchant where id = ? and deleted = 0 limit 1", merchantId);
        if (merchants.isEmpty()) return List.of();
        Object raw = merchants.get(0).get("merchantRightsConfig");
        if (raw == null || value(raw, "").isBlank()) return List.of();
        List<Map<String, Object>> rights = new ArrayList<>();
        try {
            if (raw instanceof List<?> list) {
                for (Object item : list) if (item instanceof Map<?, ?> map) rights.add(copyMap(map));
            } else {
                for (Object item : JSON.parseArray(value(raw, ""), Map.class)) if (item instanceof Map<?, ?> map) rights.add(copyMap(map));
            }
        } catch (Exception ignored) {
            return List.of();
        }
        Map<String, Map<String, Object>> byLevel = new LinkedHashMap<>();
        for (Map<String, Object> right : rights) {
            if (!booleanValue(right.get("enabled"))) continue;
            String level = normalizeLevel(value(right.get("level"), ""));
            if (!List.of("EXCELLENT", "OUTSTANDING", "ELITE").contains(level)) continue;
            byLevel.putIfAbsent(level, right);
        }
        return List.of("EXCELLENT", "OUTSTANDING", "ELITE").stream()
                .filter(byLevel::containsKey).map(level -> toBenefit(byLevel.get(level)))
                .filter(item -> !value(item.get("benefitText"), "").isBlank()).toList();
    }
 
    private Map<String, Object> toBenefit(Map<String, Object> right) {
        String level = value(right.get("level"), "");
        String normalized = normalizeLevel(level);
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("cardLevel", normalized);
        result.put("cardName", value(right.get("cardName"), cardName(normalized, level)));
        String text = value(right.get("detail"), "");
        if (text.isBlank()) text = value(right.get("consumeDesc"), "");
        if (text.isBlank()) text = configuredConsumeText(right);
        result.put("benefitText", text);
        result.put("theme", theme(normalized));
        return result;
    }
 
    private String configuredConsumeText(Map<String, Object> right) {
        String type = value(right.get("consumeType"), "");
        if ("优惠".equals(type) && !value(right.get("consumeCoupon"), "").isBlank()) return "到店消费立减" + right.get("consumeCoupon") + "元";
        String discount = "折扣".equals(type) ? value(right.get("consumeDiscount"), "") : value(right.get("discount"), "");
        return discount.isBlank() ? "" : "到店消费" + discount + "折";
    }
 
    private String normalizeLevel(String value) {
        String upper = value.toUpperCase();
        if (upper.contains("EXCELLENT") || value.contains("卓越")) return "EXCELLENT";
        if (upper.contains("OUTSTANDING") || value.contains("杰出")) return "OUTSTANDING";
        if (upper.contains("ELITE") || value.contains("菁英") || value.contains("精英")) return "ELITE";
        return upper;
    }
 
    private String cardName(String normalized, String fallback) {
        return switch (normalized) {
            case "EXCELLENT" -> "卓越人才卡";
            case "OUTSTANDING" -> "杰出人才卡";
            case "ELITE" -> "菁英人才卡";
            default -> fallback;
        };
    }
 
    private String theme(String normalized) {
        return switch (normalized) {
            case "OUTSTANDING" -> "outstanding";
            case "ELITE" -> "elite";
            default -> "excellent";
        };
    }
 
    private boolean booleanValue(Object value) {
        if (value instanceof Boolean bool) return bool;
        if (value instanceof Number number) return number.intValue() != 0;
        String text = value(value, "");
        return "true".equalsIgnoreCase(text) || "1".equals(text) || "yes".equalsIgnoreCase(text);
    }
 
    private Map<String, Object> copyMap(Map<?, ?> source) {
        Map<String, Object> result = new LinkedHashMap<>();
        for (Map.Entry<?, ?> entry : source.entrySet()) result.put(String.valueOf(entry.getKey()), entry.getValue());
        return result;
    }
 
    private Map<String, Object> merchant(Long merchantId) {
        List<Map<String, Object>> merchants = rows("""
                select id, merchant_name, address, cover_url, business_status, status, updated_at
                from merchant where id = ? and deleted = 0
                """, merchantId);
        if (merchants.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "商户不存在");
        return merchants.get(0);
    }
 
    private Map<String, Object> store(MerchantPrincipal principal) {
        List<Map<String, Object>> stores = rows("""
                select id, merchant_id, store_name, address
                from merchant_store where id = ? and merchant_id = ? and status = 1 and deleted = 0
                """, principal.storeId(), principal.merchantId());
        if (stores.isEmpty()) throw new BusinessException(ErrorCode.FORBIDDEN, "当前门店不可操作");
        return stores.get(0);
    }
 
    private Map<String, Object> toProductListItem(Map<String, Object> row) {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("id", String.valueOf(row.get("id")));
        result.put("productNo", value(row.get("productNo"), ""));
        result.put("productName", value(row.get("productName"), ""));
        result.put("coverUrl", value(row.get("coverUrl"), ""));
        result.put("salePrice", decimal(row.get("salePrice")));
        result.put("status", number(row.get("status")) == 1 ? "online" : "offline");
        result.put("auditStatus", auditStatus(number(row.get("auditStatus"))));
        result.put("auditRemark", value(row.get("auditRemark"), ""));
        result.put("submitTime", row.get("submitTime"));
        result.put("updatedAt", row.get("updatedAt"));
        return result;
    }
 
    private Map<String, Object> toProductDetail(Map<String, Object> row) {
        Map<String, Object> result = toProductListItem(row);
        result.put("productType", value(row.get("productType"), ""));
        result.put("originalPrice", decimal(row.get("originalPrice")));
        result.put("detailText", value(row.get("detailText"), ""));
        result.put("purchaseNotice", object(row.get("purchaseNotice")));
        result.put("productImages", strings(row.get("productImages")));
        result.put("rightsConfig", object(row.get("rightsConfig")));
        result.put("benefitDesc", value(row.get("benefitDesc"), ""));
        return result;
    }
 
    private void validatePeriods(List<MerchantBusinessHoursRequest.Period> periods) {
        periods.sort((left, right) -> left.start().compareTo(right.start()));
        LocalTime previousEnd = null;
        for (MerchantBusinessHoursRequest.Period period : periods) {
            LocalTime start = LocalTime.parse(period.start());
            LocalTime end = LocalTime.parse(period.end());
            if (!start.isBefore(end)) throw new BusinessException(ErrorCode.BAD_REQUEST, "结束时间需晚于开始时间");
            if (previousEnd != null && start.isBefore(previousEnd)) throw new BusinessException(ErrorCode.BAD_REQUEST, "营业时段不能重叠");
            previousEnd = end;
        }
    }
 
    private String normalizeBusinessStatus(String input) {
        String status = input.trim().toLowerCase();
        if ("open".equals(status) || "营业中".equals(status)) return "open";
        if ("closed".equals(status) || "休息中".equals(status)) return "closed";
        throw new BusinessException(ErrorCode.BAD_REQUEST, "营业状态无效");
    }
 
    private String businessStatus(Object value) {
        String status = value(value, "").trim().toLowerCase();
        return "closed".equals(status) || "休息中".equals(status) ? "closed" : "open";
    }
 
    private boolean isBusinessOpen(Map<String, Object> merchant, Long storeId) {
        if (!Long.valueOf(1).equals(number(merchant.get("status"))) || "closed".equals(businessStatus(merchant.get("businessStatus")))) {
            return false;
        }
        Long activePeriods = jdbc.queryForObject("""
                select count(*)
                from merchant_business_hours
                where merchant_id = ? and store_id = ?
                  and (
                    (start_time <= end_time and current_time() >= start_time and current_time() < end_time)
                    or (start_time > end_time and (current_time() >= start_time or current_time() < end_time))
                  )
                """, Long.class, merchant.get("id"), storeId);
        return activePeriods != null && activePeriods > 0;
    }
 
    private String auditStatus(Long status) {
        if (status != null && status == 2) return "approved";
        if (status != null && status == 3) return "rejected";
        return "pending";
    }
 
    private List<Map<String, Object>> rows(String sql, Object... arguments) {
        return DatabaseRows.normalize(jdbc.queryForList(sql, arguments));
    }
 
    private Map<String, String> object(Object value) {
        Map<String, String> result = new LinkedHashMap<>();
        String json = value(value, "");
        for (String field : NOTICE_FIELDS) {
            String parsed = jsonField(json, field);
            if (!parsed.isEmpty()) result.put(field, parsed);
        }
        return result;
    }
 
    private List<String> strings(Object value) {
        String json = value(value, "");
        if (!json.startsWith("[")) return List.of();
        List<String> result = new ArrayList<>();
        Matcher matcher = Pattern.compile("\\\"((?:\\\\.|[^\\\"\\\\])*)\\\"").matcher(json);
        while (matcher.find()) result.add(unescape(matcher.group(1)));
        return result;
    }
 
    private String jsonObject(Map<String, String> values) {
        List<String> entries = new ArrayList<>();
        for (Map.Entry<String, String> entry : values.entrySet()) {
            entries.add("\"" + escape(entry.getKey()) + "\":\"" + escape(entry.getValue()) + "\"");
        }
        return "{" + String.join(",", entries) + "}";
    }
 
    private String jsonArray(List<String> values) {
        List<String> entries = new ArrayList<>();
        for (String value : values) entries.add("\"" + escape(value == null ? "" : value.trim()) + "\"");
        return "[" + String.join(",", entries) + "]";
    }
 
    private String jsonField(String json, String field) {
        Pattern pattern = Pattern.compile("\\\"" + Pattern.quote(field) + "\\\"\\s*:\\s*\\\"((?:\\\\.|[^\\\"\\\\])*)\\\"");
        Matcher matcher = pattern.matcher(json);
        return matcher.find() ? unescape(matcher.group(1)) : "";
    }
 
    private String escape(String value) {
        return value.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t");
    }
 
    private String unescape(String value) {
        StringBuilder result = new StringBuilder();
        boolean escaped = false;
        for (int index = 0; index < value.length(); index++) {
            char item = value.charAt(index);
            if (!escaped) {
                if (item == '\\') escaped = true;
                else result.append(item);
                continue;
            }
            if (item == 'n') result.append('\n');
            else if (item == 'r') result.append('\r');
            else if (item == 't') result.append('\t');
            else result.append(item);
            escaped = false;
        }
        if (escaped) result.append('\\');
        return result.toString();
    }
 
    private String value(Object value, String fallback) {
        if (value == null || "null".equals(String.valueOf(value))) return fallback;
        String text = String.valueOf(value).trim();
        return text.isEmpty() ? fallback : text;
    }
 
    private Long number(Object value) {
        return value instanceof Number number ? number.longValue() : null;
    }
 
    private BigDecimal decimal(Object value) {
        if (value instanceof BigDecimal decimal) return decimal;
        if (value instanceof Number number) return BigDecimal.valueOf(number.doubleValue());
        try {
            return new BigDecimal(String.valueOf(value));
        } catch (Exception ignored) {
            return BigDecimal.ZERO;
        }
    }
 
    private enum ProductScope {
        ONLINE(" and status = 1 and audit_status = 2"),
        OFFLINE(" and status = 0 and audit_status = 2"),
        REVIEW("");
 
        private final String conditions;
 
        ProductScope(String conditions) {
            this.conditions = conditions;
        }
 
        String conditions() {
            return conditions;
        }
 
        static ProductScope from(String value) {
            if ("offline".equals(value)) return OFFLINE;
            if ("review".equals(value)) return REVIEW;
            return ONLINE;
        }
    }
}