shinnosuke
2026-08-21 bddcb4ef710d8bcad722a8e062ee9e0855d7042c
又又更新
7个文件已修改
119 ■■■■■ 已修改文件
rencaika_merchant/pages/withdrawal-detail/withdrawal-detail.uvue 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/style.css 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MerchantCreateView.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/ProductAuditDetailView.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkService.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/order/OrderService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/pages/withdrawal-detail/withdrawal-detail.uvue
@@ -2,19 +2,18 @@
    <view class="page">
        <view class="navigation-bar"><view class="back-button" @tap="goBack"><image class="back-icon" src="/static/icons/lucide/back-left.svg" mode="aspectFit"></image></view><text class="page-title">商户提现</text></view>
        <scroll-view class="form-scroll" scroll-y="true" :show-scrollbar="false" :enhanced="false">
            <view class="available-row"><text class="available-label">当前可提</text><text class="available-value">¥{{availableText}}</text></view>
            <view class="form-card">
                <view class="available-row"><text class="available-label">当前可提</text><text class="available-value">¥{{availableText}}</text></view>
                <view class="field-block">
                    <view class="field-row">
                    <view class="field-row amount-field-row">
                        <text class="field-label">提现金额</text>
                        <view class="amount-input-shell" :class="error.length > 0 ? 'field-invalid' : ''">
                            <text class="amount-prefix">¥</text>
                            <input class="amount-input" v-model="amount" type="digit" maxlength="10" placeholder="请输入" placeholder-class="input-placeholder" @input="clearError" />
                            <text class="amount-prefix">¥</text>
                            <input class="amount-input" v-model="amount" type="digit" maxlength="10" placeholder="请输入提现金额" placeholder-class="input-placeholder" @input="clearError" />
                        </view>
                    </view>
                    <text v-if="error.length > 0" class="field-error">{{error}}</text>
                </view>
                <view class="field-divider"></view>
                <view class="field-block bank-field-block">
                    <view class="field-row bank-field-row">
                        <text class="field-label">提现银行卡</text>
@@ -28,8 +27,8 @@
                        <view class="select-bank-button" @tap="chooseBank"><text>{{selected == null ? '选择' : '更换'}}</text></view>
                    </view>
                </view>
                <view class="submit-button" :class="submitting ? 'submit-disabled' : ''" @tap="submit"><text>{{submitting ? '提交中' : '发起提现'}}</text></view>
            </view>
            <view class="submit-button" :class="submitting ? 'submit-disabled' : ''" @tap="submit"><text>{{submitting ? '提交中' : '发起提现'}}</text></view>
            <view class="safe-space"></view>
        </scroll-view>
    </view>
@@ -79,4 +78,27 @@
    page { background-color: #F4F6F9; }.page { display: flex; flex: 1; width: 100%; height: 100vh; flex-direction: column; overflow: hidden; background-color: #F4F6F9; }.navigation-bar { display: flex; height: 174rpx; padding: 68rpx 38rpx 0; flex: none; flex-direction: row; align-items: center; position: relative; box-sizing: border-box; }.back-button { display: flex; width: 64rpx; height: 64rpx; justify-content: center; align-items: center; position: absolute; left: 38rpx; top: 89rpx; z-index: 2; }.back-icon { width: 38rpx; height: 38rpx; transform: rotate(180deg); }.page-title { width: 100%; color: #202B3E; font-size: 34rpx; line-height: 48rpx; text-align: center; font-weight: 700; }.form-scroll { flex: 1; height: 0; padding: 0 34rpx; box-sizing: border-box; }.available-row { display: flex; height: 72rpx; padding: 0 12rpx; align-items: center; }.available-label { color: #7D8797; font-size: 25rpx; }.available-value { margin-left: 12rpx; color: #2C3748; font-size: 27rpx; font-weight: 700; }.form-card { padding: 6rpx 28rpx; border-radius: 16rpx; background-color: #FFFFFF; box-shadow: 0 8rpx 24rpx rgba(49,55,66,.05); }.field-block { display: flex; flex-direction: column; }.bank-field-block { margin-top: 0; }.field-row { display: flex; min-height: 104rpx; align-items: center; }.bank-field-row { align-items: center; }.field-label { width: 178rpx; padding-top: 0; flex: none; color: #5F6978; font-size: 27rpx; font-weight: 400; }.amount-input-shell { display: flex; height: 70rpx; padding: 0 18rpx; flex: 1; align-items: center; border: 1rpx solid #E3E7ED; border-radius: 8rpx; background-color: #FFFFFF; box-sizing: border-box; }.amount-prefix { color: #8B94A2; font-size: 27rpx; font-weight: 400; }.amount-input { height: 68rpx; margin-left: 8rpx; flex: 1; color: #2C3748; font-size: 27rpx; }.input-placeholder { color: #B5BDC9; font-size: 25rpx; }.field-invalid { border-color: #F0746D; background-color: #FFF9F8; }.field-error { margin: 0 0 14rpx 178rpx; color: #E95B55; font-size: 22rpx; }.field-divider { height: 1rpx; margin-left: 178rpx; background-color: #EEF0F3; }.bank-value { display: flex; min-width: 0; flex: 1; }.bank-placeholder { color: #B5BDC9; font-size: 25rpx; }.selected-bank-copy { display: flex; min-width: 0; flex: 1; flex-direction: column; }.selected-bank-name { color: #2C3748; font-size: 25rpx; }.selected-card-number { margin-top: 4rpx; color: #8A93A1; font-size: 22rpx; }.select-bank-button { display: flex; width: 84rpx; height: 52rpx; padding: 0; margin: 0 0 0 14rpx; flex: none; align-items: center; justify-content: center; border: 1rpx solid #58A5F8; border-radius: 8rpx; color: #318CF0; background-color: #FFFFFF; font-size: 23rpx; font-weight: 400; }.submit-button { display: flex; width: 360rpx; height: 72rpx; margin: 34rpx auto 0; align-items: center; justify-content: center; border-radius: 8rpx; color: #FFFFFF; background-color: #409EFF; font-size: 27rpx; font-weight: 500; box-shadow: none; }.submit-disabled { opacity: .58; }.safe-space { height: 70rpx; }
    .back-icon { transform: none; }
    .submit-button { width: 100%; margin-left: 0; margin-right: 0; }
    /* Match the withdrawal prototype: balance above the card and vertically stacked fields. */
    .form-scroll { padding-top: 0; }
    .available-row { height: 128rpx; padding: 20rpx 0 24rpx; flex-direction: column; justify-content: center; align-items: center; box-sizing: border-box; }
    .available-label { font-size: 25rpx; line-height: 36rpx; }
    .available-value { margin: 2rpx 0 0; color: #E87535; font-size: 30rpx; line-height: 42rpx; }
    .form-card { padding: 54rpx 28rpx 34rpx; border-radius: 28rpx; }
    .amount-field-row { min-height: 0; flex-direction: column; align-items: stretch; }
    .amount-field-row .field-label { width: auto; margin-bottom: 10rpx; text-align: center; color: #273345; font-size: 30rpx; line-height: 42rpx; font-weight: 700; }
    .amount-input-shell { width: 100%; height: 160rpx; padding: 10rpx 18rpx 0; flex: none; flex-direction: column; align-items: center; box-sizing: border-box; }
    .amount-prefix { line-height: 34rpx; }
    .amount-input { width: 100%; height: 100rpx; margin: 0; flex: none; text-align: center; }
    .input-placeholder { text-align: center; }
    .field-divider { display: none; }
    .bank-field-block { margin-top: 56rpx; }
    .bank-field-row { min-height: 0; flex-direction: column; align-items: flex-start; }
    .bank-field-row .field-label { width: auto; margin-bottom: 18rpx; color: #273345; font-size: 30rpx; line-height: 42rpx; font-weight: 700; }
    .bank-value { width: 100%; flex: none; }
    .bank-placeholder { font-size: 25rpx; }
    .bank-placeholder { display: none; }
    .select-bank-button { width: 240rpx; height: 76rpx; margin: 18rpx 0 0; border-color: #F0D1B9; border-radius: 38rpx; color: #D97643; background-color: #FFF9F4; font-size: 28rpx; }
    .submit-button { width: 100%; height: 88rpx; margin: 62rpx 0 0; border-radius: 44rpx; color: #FFFFFF; background-image: linear-gradient(90deg, #FF7625 0%, #FF9630 100%); font-size: 29rpx; line-height: 41rpx; font-weight: 700; box-shadow: 0 12rpx 24rpx rgba(255, 107, 36, 0.20); }
    .safe-space { height: 70rpx; }
</style>
talent-card-admin/src/style.css
@@ -738,6 +738,17 @@
  padding-left: 12px;
}
.merchant-settings-form .created-time-row > label {
  box-sizing: border-box;
  padding-left: 12px;
}
.merchant-settings-form .merchant-form-row > label,
.merchant-settings-form .merchant-hours-block > label {
  box-sizing: border-box;
  padding-left: 12px;
}
.merchant-form-row.recommend-row .el-switch {
  justify-self: start;
}
talent-card-admin/src/views/MerchantCreateView.vue
@@ -2248,8 +2248,8 @@
        <h3>商家信息</h3>
        <el-form ref="merchantFormRef" :model="form" :rules="merchantRules" class="merchant-settings-form" label-width="58px" label-position="left">
        <div class="merchant-create-content" :class="{ 'service-merchant-content': isServiceMerchant || isApartmentMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant }">
          <div class="merchant-create-left" :style="isApartmentMerchant || isFoodMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant ? 'flex: 1' : ''">
        <div class="merchant-create-content" :class="{ 'service-merchant-content': isServiceMerchant || isApartmentMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant || usesSimplifiedMerchantSettings }">
          <div class="merchant-create-left" :style="isApartmentMerchant || isFoodMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant || usesSimplifiedMerchantSettings ? 'flex: 1' : ''">
            <el-form-item class="merchant-form-row three-columns" label="商家名称" prop="merchantName" required>
              <el-input v-model="form.merchantName" />
            </el-form-item>
@@ -2273,7 +2273,7 @@
            </el-form-item>
          </div>
          <div v-if="!isServiceMerchant && !isApartmentMerchant && !isHotelMerchant && !isCarWashMerchant && !isHealthCheckMerchant && !isParkingMerchant && !isChildcareMerchant && !isFinancialMerchant && !isMovieMerchant && !isSpouseEmploymentMerchant && !isApprovalMerchant" class="merchant-create-middle">
          <div v-if="!isServiceMerchant && !isApartmentMerchant && !isHotelMerchant && !isCarWashMerchant && !isHealthCheckMerchant && !isParkingMerchant && !isChildcareMerchant && !isFinancialMerchant && !isMovieMerchant && !isSpouseEmploymentMerchant && !isApprovalMerchant && !usesSimplifiedMerchantSettings" class="merchant-create-middle">
            <el-form-item class="merchant-form-row" label="联系电话" prop="contactMobile" required>
              <el-input v-model="form.contactMobile" />
            </el-form-item>
@@ -2297,12 +2297,12 @@
            </template>
          </div>
          <div class="merchant-create-right" :style="isApartmentMerchant || isFoodMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant ? 'flex: 1' : ''">
            <el-form-item v-if="isServiceMerchant || isApartmentMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant" class="merchant-form-row service-contact-row" label="联系电话" prop="contactMobile" required>
          <div class="merchant-create-right" :style="isApartmentMerchant || isFoodMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant || usesSimplifiedMerchantSettings ? 'flex: 1' : ''">
            <el-form-item v-if="isServiceMerchant || isApartmentMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant || usesSimplifiedMerchantSettings" class="merchant-form-row service-contact-row" label="联系电话" prop="contactMobile" required>
              <el-input v-model="form.contactMobile" placeholder="请输入联系电话" />
            </el-form-item>
            <div v-if="hasExistingMerchant" class="merchant-form-row">
            <div v-if="hasExistingMerchant" class="merchant-form-row created-time-row">
              <label>创建时间</label>
              <el-input v-model="form.createdAt" readonly />
            </div>
talent-card-admin/src/views/ProductAuditDetailView.vue
@@ -53,6 +53,10 @@
  const merchantType = product.value?.merchantType || ''
  return merchantType === '酒店服务' || merchantType === '酒店'
})
const hideTalentBenefitPanel = computed(() => {
  const merchantType = String(product.value?.merchantType || '').trim().toLowerCase()
  return ['酒店服务', '酒店', '医院服务', '医院', '医疗服务', '医疗', 'hotel', 'hospital', 'medical', 'health_check'].includes(merchantType)
})
const productAmountText = computed(() => {
  const salePrice = product.value?.salePrice
  const originalPrice = product.value?.originalPrice
@@ -283,7 +287,7 @@
      <img v-if="previewUrl" :src="previewUrl" alt="商品图片预览" class="product-audit-preview-image" />
    </el-dialog>
    <section class="audit-detail-panel talent-benefit-panel">
    <section v-if="!hideTalentBenefitPanel" class="audit-detail-panel talent-benefit-panel">
      <h3>人才权益</h3>
      <div class="benefit-config-grid">
        <div v-for="row in benefitRows" :key="row.level" class="benefit-config-row">
talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java
@@ -181,6 +181,7 @@
        result.put("name", result.get("ruleName"));
        result.put("discountRate", configuredDiscountRate(right.get("discount")));
        result.put("subsidyEnabled", booleanValue(right.get("governmentSubsidy")) ? 1 : 0);
        result.put("merchantDetail", right.get("detail"));
        result.put("consumeDesc", right.get("consumeDesc"));
        result.put("benefitText", configuredBenefitText(right));
        return result;
@@ -332,6 +333,13 @@
                .orElse("");
        if (configuredDescription.isBlank()) {
            configuredDescription = configuredRules(merchantId, cardLevel).stream()
                    .map(rule -> string(rule.get("merchantDetail")))
                    .filter(value -> !value.isBlank())
                    .findFirst()
                    .orElse("");
        }
        if (configuredDescription.isBlank()) {
            configuredDescription = configuredRules(merchantId, cardLevel).stream()
                    .map(rule -> string(rule.get("benefitText")))
                    .filter(value -> !value.isBlank())
                    .findFirst()
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkService.java
@@ -20,10 +20,14 @@
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
@Service
public class MerchantWorkService {
    private static final long REVOCATION_HOURS = 3;
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private static final TypeReference<List<Map<String, Object>>> RIGHTS_TYPE = new TypeReference<>() {};
    private final JdbcTemplate jdbcTemplate;
    private final ManualConsumptionService manualConsumptionService;
@@ -73,7 +77,8 @@
                select o.id, o.order_no, o.talent_id, tu.name as talent_name, tu.mobile as talent_mobile, tu.gender,
                       o.card_id, tc.card_no, coalesce(nullif(o.card_level_snapshot, ''), tc.card_level) as card_level,
                       o.benefit_id, b.benefit_name,
                       o.merchant_id, m.merchant_name, o.product_id, o.product_name_snapshot, p.rights_config,
                       o.merchant_id, m.merchant_name, m.merchant_rights_config,
                       o.product_id, o.product_name_snapshot, p.rights_config,
                       o.order_type, o.order_amount, o.base_pay_amount, o.coupon_amount,
                       o.subsidy_amount, o.pay_amount, o.order_status, o.verify_status, o.created_at,
                       vr.verify_time, vr.status as verify_record_status
@@ -314,6 +319,8 @@
    private String currentBenefitText(Map<String, Object> order, String category) {
        String cardLevel = text(order.get("cardLevel"));
        String merchantDetail = merchantBenefitDetail(order.get("merchantRightsConfig"), cardLevel);
        if (!merchantDetail.isBlank()) return merchantDetail;
        if ("store".equals(category)) {
            Long merchantId = number(order.get("merchantId"));
            return merchantId == null ? "以当前商户到店消费规则为准" : manualConsumptionService.benefitTextForMerchant(merchantId, cardLevel);
@@ -326,6 +333,40 @@
        return configured.isBlank() ? "以当前订单权益规则为准" : configured;
    }
    private String merchantBenefitDetail(Object rawConfig, String cardLevel) {
        if (rawConfig == null || cardLevel.isBlank()) return "";
        try {
            List<Map<String, Object>> configs = rawConfig instanceof List<?>
                    ? ((List<?>) rawConfig).stream().filter(Map.class::isInstance)
                            .map(item -> (Map<String, Object>) item).toList()
                    : OBJECT_MAPPER.readValue(String.valueOf(rawConfig), RIGHTS_TYPE);
            for (Map<String, Object> config : configs) {
                if (!booleanValue(config.get("enabled")) || !sameCardLevel(config.get("level"), cardLevel)) continue;
                String detail = text(config.get("detail"));
                if (!detail.isBlank()) return detail;
            }
        } catch (Exception ignored) {
            // Invalid legacy configuration should fall back to the structured rule text.
        }
        return "";
    }
    private boolean sameCardLevel(Object configured, String actual) {
        return normalizeCardLevel(configured).equals(normalizeCardLevel(actual));
    }
    private String normalizeCardLevel(Object value) {
        String raw = text(value).toUpperCase();
        if (raw.contains("卓越") || "EXCELLENT".equals(raw)) return "EXCELLENT";
        if (raw.contains("杰出") || "OUTSTANDING".equals(raw)) return "OUTSTANDING";
        if (raw.contains("菁英") || "ELITE".equals(raw)) return "ELITE";
        return raw;
    }
    private boolean booleanValue(Object value) {
        return value instanceof Boolean bool ? bool : "1".equals(text(value)) || "true".equalsIgnoreCase(text(value));
    }
    private String productBenefitText(ProductPriceCalculator.ProductRight right) {
        String discountText = discountText(right.discount());
        if (!right.free()) return discountText.isBlank() ? "以商品配置为准" : "按市价" + discountText;
talent-card-server/src/main/java/com/talentcard/server/business/order/OrderService.java
@@ -416,13 +416,16 @@
                       %s as talent_level,
                       o.card_id, tc.card_no, tc.card_type,
                       o.merchant_id, m.merchant_name, m.merchant_type,
                       o.benefit_id, b.benefit_name, o.order_type,
                       o.benefit_id, b.benefit_name,
                       coalesce(nullif(o.product_name_snapshot, ''), p.product_name, b.benefit_name) as product_name,
                       o.order_type,
                       o.order_amount, o.subsidy_amount, o.pay_amount,
                       o.order_status, o.verify_status, o.created_at, o.updated_at as pay_time
                from benefit_order o
                left join talent_user tu on tu.id = o.talent_id
                left join talent_card tc on tc.id = o.card_id
                left join benefit b on b.id = o.benefit_id
                left join merchant_product p on p.id = o.product_id and p.deleted = 0
                left join merchant m on m.id = o.merchant_id
                """.formatted(ORDER_CARD_LEVEL_SQL) + where + " order by o.id desc limit ? offset ?", pageArgs.toArray());
        Long total = jdbcTemplate.queryForObject("""