fix: calculate merchant status from business hours
| | |
| | | rightsText?: string |
| | | merchantRightsConfig?: Array<Record<string, unknown>> | string |
| | | serviceLabel?: string |
| | | businessHours?: Array<{ startTime: string; endTime: string }> |
| | | isRecommend?: number |
| | | sortNo?: number |
| | | status?: number |
| | |
| | | rightsText?: string |
| | | merchantRightsConfig?: Array<Record<string, unknown>> |
| | | serviceLabel?: string |
| | | businessHours?: Array<{ startTime: string; endTime: string }> |
| | | recommend?: number |
| | | sortNo?: number |
| | | status?: number |
| | |
| | | function hasValidBusinessHours() { |
| | | return businessHours.length > 0 && businessHours.every((item) => { |
| | | const [startTime, endTime] = item.value |
| | | return Boolean(startTime && endTime && startTime < endTime) |
| | | return Boolean(startTime && endTime && startTime !== endTime) |
| | | }) |
| | | } |
| | | |
| | | function businessHoursPayload() { |
| | | return businessHours.map((item) => ({ startTime: item.value[0], endTime: item.value[1] })) |
| | | } |
| | | |
| | | function resetBusinessHours(hours?: Array<{ startTime: string; endTime: string }>) { |
| | | const normalized = (hours || []).filter((item) => item.startTime && item.endTime) |
| | | businessHours.splice(0, businessHours.length, ...(normalized.length > 0 |
| | | ? normalized.map((item, index) => ({ id: Date.now() + index, value: [item.startTime, item.endTime] })) |
| | | : [{ id: Date.now(), value: [] }])) |
| | | } |
| | | |
| | | function selectedCoordinates() { |
| | |
| | | rightsText: form.rightsText, |
| | | merchantRightsConfig: rightsConfigs.map((item) => ({ ...item })), |
| | | serviceLabel: form.serviceLabel, |
| | | businessHours: businessHoursPayload(), |
| | | recommend: form.recommend, |
| | | sortNo: Number(form.sortNo || 0), |
| | | status: form.status, |
| | |
| | | rightsText: form.rightsText, |
| | | merchantRightsConfig: rightsConfigs.map((item) => ({ ...item })), |
| | | serviceLabel: form.serviceLabel, |
| | | businessHours: businessHoursPayload(), |
| | | recommend: form.recommend, |
| | | sortNo: Number(form.sortNo || 0), |
| | | status: form.status, |
| | |
| | | form.canLoginMerchantApp = Number(merchant.canLoginMerchantApp ?? 1) |
| | | form.recommend = Number(merchant.isRecommend ?? 0) |
| | | accountForm.loginAccount = merchant.loginAccount || '' |
| | | resetBusinessHours(merchant.businessHours) |
| | | resetMerchantRights(merchant.merchantRightsConfig) |
| | | }) |
| | | } |
| | |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> merchant(Long id) { |
| | | return one(""" |
| | | 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, |
| | |
| | | from merchant |
| | | where deleted = 0 and id = ? |
| | | """, id); |
| | | result.put("businessHours", DatabaseRows.normalize(jdbcTemplate.queryForList(""" |
| | | select start_time, end_time |
| | | from merchant_business_hours |
| | | where merchant_id = ? |
| | | order by sort_no, id |
| | | """, id))); |
| | | return result; |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | |
| | | request.recommend() == null ? defaultRecommend(request.merchantType()) : request.recommend(), |
| | | request.sortNo() == null ? 0 : request.sortNo()); |
| | | Long id = jdbcTemplate.queryForObject("select last_insert_id()", Long.class); |
| | | Long storeId = createDefaultStore(id, request.merchantName(), request.address()); |
| | | syncMerchantBusinessHours(id, storeId, request.businessHours()); |
| | | if (hasMerchantAppAccess(request)) { |
| | | String account = request.loginAccount().trim(); |
| | | if (!account.matches("^[A-Za-z0-9]{1,32}$")) { |
| | |
| | | } |
| | | 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); |
| | |
| | | private boolean hasMerchantAppAccess(MerchantCreateRequest request) { |
| | | return ("餐饮服务".equals(request.merchantType()) || "景区服务".equals(request.merchantType())) |
| | | && (request.canLoginMerchantApp() == null || request.canLoginMerchantApp() == 1); |
| | | } |
| | | |
| | | private Long createDefaultStore(Long merchantId, String merchantName, String address) { |
| | | jdbcTemplate.update("insert into merchant_store(merchant_id, store_name, address, status) values (?, ?, ?, 1)", |
| | | merchantId, merchantName.trim(), address); |
| | | return jdbcTemplate.queryForObject("select last_insert_id()", Long.class); |
| | | } |
| | | |
| | | private Long findOrCreateDefaultStore(Long merchantId, String merchantName, String address) { |
| | | List<Long> stores = jdbcTemplate.queryForList(""" |
| | | select id from merchant_store |
| | | where merchant_id = ? and deleted = 0 |
| | | order by id |
| | | limit 1 |
| | | """, Long.class, merchantId); |
| | | if (stores.isEmpty()) return createDefaultStore(merchantId, merchantName, address); |
| | | Long storeId = stores.get(0); |
| | | jdbcTemplate.update("update merchant_store set store_name = ?, address = ? where id = ?", merchantName.trim(), address, storeId); |
| | | return storeId; |
| | | } |
| | | |
| | | private void syncMerchantBusinessHours(Long merchantId, Long storeId, List<MerchantBusinessHourRequest> businessHours) { |
| | | if (businessHours == null) return; |
| | | if (businessHours.isEmpty()) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "请至少配置一个营业时间段"); |
| | | } |
| | | if (businessHours.stream().anyMatch(item -> item.startTime().equals(item.endTime()))) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "营业开始时间和结束时间不能相同"); |
| | | } |
| | | jdbcTemplate.update("delete from merchant_business_hours where merchant_id = ? and store_id = ?", merchantId, storeId); |
| | | for (int index = 0; index < businessHours.size(); index++) { |
| | | MerchantBusinessHourRequest item = businessHours.get(index); |
| | | jdbcTemplate.update(""" |
| | | insert into merchant_business_hours(merchant_id, store_id, start_time, end_time, sort_no) |
| | | values (?, ?, ?, ?, ?) |
| | | """, merchantId, storeId, item.startTime(), item.endTime(), index + 1); |
| | | } |
| | | } |
| | | |
| | | private byte defaultRecommend(String merchantType) { |
| | |
| | | request.status() == null ? 1 : request.status(), |
| | | id); |
| | | ensureUpdated(updated); |
| | | Long storeId = findOrCreateDefaultStore(id, request.merchantName(), request.address()); |
| | | syncMerchantBusinessHours(id, storeId, request.businessHours()); |
| | | return merchant(id); |
| | | } |
| | | |
| New file |
| | |
| | | package com.talentcard.server.business.admin; |
| | | |
| | | import jakarta.validation.constraints.NotBlank; |
| | | import jakarta.validation.constraints.Pattern; |
| | | |
| | | public record MerchantBusinessHourRequest( |
| | | @NotBlank |
| | | @Pattern(regexp = "^(?:[01]\\d|2[0-3]):[0-5]\\d$") |
| | | String startTime, |
| | | @NotBlank |
| | | @Pattern(regexp = "^(?:[01]\\d|2[0-3]):[0-5]\\d$") |
| | | String endTime |
| | | ) { |
| | | } |
| | |
| | | import jakarta.validation.constraints.NotBlank; |
| | | import jakarta.validation.constraints.NotNull; |
| | | import jakarta.validation.constraints.Size; |
| | | import jakarta.validation.Valid; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | |
| | | List<Map<String, Object>> merchantRightsConfig, |
| | | @Size(max = 64) |
| | | String serviceLabel, |
| | | List<@Valid MerchantBusinessHourRequest> businessHours, |
| | | Byte canLoginMerchantApp, |
| | | @Size(max = 32) String loginAccount, |
| | | @Size(max = 64) String loginPassword |
| | |
| | | |
| | | import jakarta.validation.constraints.NotBlank; |
| | | import jakarta.validation.constraints.Size; |
| | | import jakarta.validation.Valid; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | |
| | | List<Map<String, Object>> merchantRightsConfig, |
| | | @Size(max = 64) |
| | | String serviceLabel, |
| | | List<@Valid MerchantBusinessHourRequest> businessHours, |
| | | Byte canLoginMerchantApp |
| | | ) { |
| | | } |