shinnosuke
2026-08-21 7150d5ac2fdb9ab5a6e9ae27e4b8ab9f129945a7
又又更新
7个文件已修改
163 ■■■■■ 已修改文件
talent-card-admin/src/style.css 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MerchantCreateView.vue 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MerchantView.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/PlatformSettingsView.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/StaffManagementView.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminStaffRequest.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/style.css
@@ -343,21 +343,6 @@
  max-width: 100%;
}
/* Keep required and optional label text aligned; the required marker sits outside the text flow. */
.admin-main .el-form-item__label {
  position: relative;
  box-sizing: border-box;
  padding-left: 12px;
  text-align: left;
  justify-content: flex-start;
}
.admin-main .el-form-item.is-required:not(.is-no-asterisk) > .el-form-item__label::before {
  position: absolute;
  left: 0;
  margin-right: 0;
}
.main-breadcrumb {
  display: flex;
  align-items: center;
@@ -1377,6 +1362,26 @@
  width: 104px;
}
.merchant-consume-row .merchant-discount-input {
  position: relative;
  width: 104px;
}
.merchant-consume-row .merchant-discount-input .el-input {
  width: 104px;
}
.merchant-consume-row .merchant-discount-input .field-error {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  width: max-content;
  color: #f56c6c;
  font-size: 12px;
  line-height: 16px;
  white-space: nowrap;
}
.hotel-voucher-row {
  grid-template-columns: 76px 42px 104px 24px 64px 104px 96px 42px;
}
@@ -1765,8 +1770,6 @@
.product-image-row label,
.product-detail-row label,
.product-notice-field label {
  box-sizing: border-box;
  padding-left: 12px;
  color: #334155;
  font-size: 12px;
  line-height: 28px;
@@ -3397,6 +3400,8 @@
/* 角色管理筛选项与标题左边缘对齐。 */
.role-management-filter-form .el-form-item__label {
  flex: 0 0 42px;
  width: 42px !important;
  justify-content: flex-start;
}
@@ -4235,6 +4240,11 @@
  line-height: 32px;
}
.platform-settings-form .platform-mini-program-link .el-form-item__label {
  font-size: 12px;
  white-space: nowrap;
}
.platform-settings-form .el-input {
  width: 178px;
}
talent-card-admin/src/views/MerchantCreateView.vue
@@ -88,7 +88,9 @@
const isParkingMerchant = computed(() => normalizedMerchantType.value === '停车服务')
const isChildcareMerchant = computed(() => normalizedMerchantType.value === '子女托管')
const isFinancialMerchant = computed(() => normalizedMerchantType.value === '金融服务')
const isMovieMerchant = computed(() => normalizedMerchantType.value === '观影服务')
const isMovieMerchant = computed(() => [
  '观影服务', 'cinema', 'theatre', '电影院', '影院', '影城', '剧院',
].includes(normalizedMerchantType.value))
const isSpouseEmploymentMerchant = computed(() => normalizedMerchantType.value === '配偶推荐就业')
const isApprovalMerchant = computed(() => normalizedMerchantType.value === '行政审批')
const isServiceMerchant = computed(() => isFoodMerchant.value || normalizedMerchantType.value === '景区服务')
@@ -966,6 +968,7 @@
}
const rightsConfigs = reactive<MerchantRightConfig[]>(defaultMerchantRights())
const merchantDiscountErrors = reactive<Record<string, string>>({})
const rightExampleImage =
  'https://images.unsplash.com/photo-1559847844-5315695dadae?auto=format&fit=crop&w=640&q=80'
@@ -1688,6 +1691,33 @@
  return amount > 0 && amount <= 10
}
function validMerchantDiscount(value: unknown) {
  const text = String(value ?? '').trim()
  if (!/^(?:[0-9]|10)(?:\.\d)?$/.test(text)) return false
  const amount = Number(text)
  return amount > 0 && amount <= 10
}
function normalizeMerchantDiscount(item: MerchantRightConfig) {
  let text = String(item.discount ?? '').replace(/[^\d.]/g, '')
  const dotIndex = text.indexOf('.')
  if (dotIndex >= 0) {
    text = `${text.slice(0, dotIndex + 1)}${text.slice(dotIndex + 1).replace(/\./g, '').slice(0, 1)}`
  }
  if (text.startsWith('.')) text = `0${text}`
  item.discount = text
  delete merchantDiscountErrors[item.level]
}
function validateMerchantDiscount(item: MerchantRightConfig) {
  if (validMerchantDiscount(item.discount)) {
    delete merchantDiscountErrors[item.level]
    return true
  }
  merchantDiscountErrors[item.level] = '\u8bf7\u8f93\u5165\u5927\u4e8e0\u4e14\u4e0d\u8d85\u8fc710\u7684\u4e00\u4f4d\u5c0f\u6570'
  return false
}
function validateProductExceedDiscount(item: { level: string, exceedDiscount: unknown }) {
  const key = item.level
  if (validExceedDiscount(item.exceedDiscount)) {
@@ -2006,9 +2036,12 @@
async function saveMerchant() {
  const merchantValid = await merchantFormRef.value?.validate().catch(() => false)
  if (!merchantValid) return
    if (isServiceMerchant.value && rightsConfigs.some((item) => item.storeConsume && !validExceedDiscount(item.discount))) {
        ElMessage.warning('折扣请输入大于 0 且不超过 10 的一位小数')
        return
    if (isServiceMerchant.value) {
        const invalidDiscount = rightsConfigs.filter((item) => item.storeConsume && !validateMerchantDiscount(item))
        if (invalidDiscount.length > 0) {
            ElMessage.warning('\u6298\u6263\u8bf7\u8f93\u5165\u5927\u4e8e0\u4e14\u4e0d\u8d85\u8fc710\u7684\u4e00\u4f4d\u5c0f\u6570')
            return
        }
    }
    const updatingMerchantPassword = isEditMode.value && accountForm.loginPassword.length > 0
    const accountValid = await accountFormRef.value?.validate().catch(() => false)
@@ -2523,7 +2556,16 @@
                <strong>到店消费</strong>
                <el-switch v-model="item.storeConsume" />
                <span>折扣</span>
                <el-input v-model="item.discount" placeholder="请输入" />
                <div class="merchant-discount-input">
                  <el-input
                    v-model="item.discount"
                    placeholder="请输入"
                    :class="{ 'is-error': merchantDiscountErrors[item.level] }"
                    @input="normalizeMerchantDiscount(item)"
                    @blur="validateMerchantDiscount(item)"
                  />
                  <span v-if="merchantDiscountErrors[item.level]" class="field-error">{{ merchantDiscountErrors[item.level] }}</span>
                </div>
                <span>折</span>
                <strong>是否政府补贴</strong>
                <el-switch v-model="item.governmentSubsidy" />
talent-card-admin/src/views/MerchantView.vue
@@ -249,7 +249,7 @@
      <el-form class="merchant-filter-form" :model="filters" label-width="74px">
        <el-form-item label="商家">
          <el-input v-model="filters.keyword" placeholder="请输入商家名称,支持模糊查询" clearable @keyup.enter="search" />
          <el-input v-model="filters.keyword" placeholder="请输入商家名称" clearable @keyup.enter="search" />
        </el-form-item>
        <el-form-item label="联系电话">
          <el-input v-model="filters.contactMobile" placeholder="请输入商家联系电话" clearable />
talent-card-admin/src/views/PlatformSettingsView.vue
@@ -49,7 +49,7 @@
        <span v-if="form.videoName || form.homeVideoUrl" class="platform-setting-file">{{ form.videoName || form.homeVideoUrl }}</span>
      </el-form-item>
      <el-form-item label="首页小程序跳转链接">
      <el-form-item class="platform-mini-program-link" label="首页小程序跳转链接">
        <el-input v-model="form.miniProgramUrl" placeholder="请输入" />
      </el-form-item>
talent-card-admin/src/views/StaffManagementView.vue
@@ -36,6 +36,14 @@
const accountPattern = /^[A-Za-z0-9]+$/;
const rules = computed<FormRules>(() => ({
  displayName: [{ required: true, message: "请输入人员名称", trigger: "blur" }],
  mobile: [{
    validator: (_rule, value, callback) => {
      const mobile = String(value || '').trim()
      if (!mobile || /^1\d{10}$/.test(mobile)) return callback()
      callback(new Error('请输入正确的联系电话'))
    },
    trigger: 'blur',
  }],
  account: [
    { required: true, message: "请输入登录账号", trigger: "blur" },
    {
@@ -328,7 +336,7 @@
      ><el-form ref="formRef" :model="form" :rules="rules" label-width="92px"
        ><el-form-item label="人员名称" prop="displayName" required
          ><el-input v-model="form.displayName" /></el-form-item
        ><el-form-item label="联系电话"
        ><el-form-item label="联系电话" prop="mobile"
          ><el-input
            v-model="form.mobile"
            inputmode="numeric"
talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java
@@ -238,11 +238,9 @@
        StringBuilder countSql = new StringBuilder("select count(*) from merchant m where m.deleted = 0");
        List<Object> args = new ArrayList<>();
        if (keyword != null && !keyword.isBlank()) {
            sql.append(" and (m.merchant_name like ? or m.contact_mobile like ? or m.address like ?)");
            countSql.append(" and (m.merchant_name like ? or m.contact_mobile like ? or m.address like ?)");
            sql.append(" and m.merchant_name like ?");
            countSql.append(" and m.merchant_name like ?");
            String like = "%" + keyword.trim() + "%";
            args.add(like);
            args.add(like);
            args.add(like);
        }
        if (contactMobile != null && !contactMobile.isBlank()) {
@@ -330,8 +328,8 @@
                       s.external_mini_program_path as car_wash_external_mini_program_path,
                       s.external_web_url as car_wash_external_web_url,
                       (select o.account from merchant_operator o
                        where o.merchant_id = merchant.id and o.role_code = 'owner' and o.deleted = 0
                        order by o.id asc limit 1) as login_account
                        where o.merchant_id = merchant.id and o.deleted = 0
                        order by case when o.role_code = 'owner' then 0 else 1 end, o.id asc limit 1) as login_account
                from merchant
                left join transport_car_wash_service s on s.merchant_id = merchant.id and s.deleted = 0
                where merchant.deleted = 0 and merchant.id = ?
@@ -361,6 +359,7 @@
        MerchantType merchantType = requireMerchantType(request.merchantType());
        validateMerchantAddress(merchantType.code(), request.address());
        validateRequiredMerchantCreateFields(request);
        validateMerchantRightsConfig(merchantType.code(), request.merchantRightsConfig());
        jdbcTemplate.update("""
                insert into merchant(merchant_name, merchant_type, merchant_type_code, contact_name, contact_mobile, address,
                                         logo_url, cover_url, business_status, longitude, latitude, can_login_merchant_app, group_buying_enabled,
@@ -392,8 +391,10 @@
        syncPremiumTopicMerchant(id, merchantType, request.sortNo());
        Long storeId = createDefaultStore(id, request.merchantName(), request.address());
        syncMerchantBusinessHours(id, storeId, request.businessHours());
        if (hasMerchantAppAccess(request)) {
            String account = request.loginAccount().trim();
        if (hasMerchantAppAccess(request)
                || (request.loginAccount() != null && !request.loginAccount().isBlank())
                || (request.loginPassword() != null && !request.loginPassword().isBlank())) {
            String account = request.loginAccount() == null ? "" : request.loginAccount().trim();
            if (!account.matches("^[A-Za-z0-9]{1,32}$")) {
                throw new BusinessException(ErrorCode.BAD_REQUEST, "商户登录账号只能包含英文字母和数字");
            }
@@ -436,6 +437,20 @@
        }
        if (request.loginPassword() == null || request.loginPassword().length() < 6 || request.loginPassword().length() > 64) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "商户登录密码长度须为6至64位");
        }
    }
    private void validateMerchantRightsConfig(String merchantType, List<Map<String, Object>> rightsConfig) {
        if (!Set.of("food", "scenic").contains(MerchantTypeCatalog.canonicalCode(merchantType)) || rightsConfig == null) return;
        for (Map<String, Object> right : rightsConfig) {
            Object storeConsume = right.get("storeConsume");
            if (!Boolean.TRUE.equals(storeConsume) && !"true".equalsIgnoreCase(String.valueOf(storeConsume))) continue;
            String discount = String.valueOf(right.getOrDefault("discount", "")).trim();
            if (!discount.matches("^(?:[0-9]|10)(?:\\.\\d)?$")
                    || new BigDecimal(discount).compareTo(BigDecimal.ZERO) <= 0
                    || new BigDecimal(discount).compareTo(BigDecimal.TEN) > 0) {
                throw new BusinessException(ErrorCode.BAD_REQUEST, "折扣请输入大于0且不超过10的一位小数");
            }
        }
    }
@@ -484,6 +499,7 @@
    public Map<String, Object> updateMerchant(Long id, MerchantUpdateRequest request) {
        MerchantType merchantType = resolveMerchantTypeForUpdate(id, request.merchantType());
        validateMerchantAddress(merchantType.code(), request.address());
        validateMerchantRightsConfig(merchantType.code(), request.merchantRightsConfig());
        int updated = jdbcTemplate.update("""
                update merchant
                set merchant_name = ?, merchant_type = ?, merchant_type_code = ?, contact_name = ?, contact_mobile = ?,
talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminStaffRequest.java
@@ -1,12 +1,17 @@
package com.talentcard.server.business.admin;
import jakarta.validation.constraints.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
public record AdminStaffRequest(
        @NotBlank @Size(max=32) @Pattern(regexp="[A-Za-z0-9]+", message="登录账号只能包含英文和数字") String account,
        @Size(max=64) String password,
        @NotBlank @Size(max=64) String displayName,
        @Pattern(regexp="^$|^1\\d{10}$", message="联系电话格式不正确") String mobile,
        @NotBlank @Size(max=32) String roleCode,
        @NotBlank @Size(max=64) String positionName,
        @Size(max=64) String customerWechat,
        @NotBlank @Size(max = 32) @Pattern(regexp = "[A-Za-z0-9]+", message = "登录账号只能包含英文和数字") String account,
        @Size(max = 64) String password,
        @NotBlank @Size(max = 64) String displayName,
        @Pattern(regexp = "^$|^1\\d{10}$", message = "请输入正确的联系电话") String mobile,
        @NotBlank @Size(max = 32) String roleCode,
        @NotBlank @Size(max = 64) String positionName,
        @Size(max = 64) String customerWechat,
        @NotNull Byte status
) {}