¥
立即购买

类模块结构生成

477 浏览
46 试用
13 购买
Nov 19, 2025更新

本提示词可根据用户需求生成完整类结构,包含关键属性、方法及参数与返回类型说明,支持前端组件化开发与面向对象建模,提升类设计效率与代码可维护性。

下面给出一个可用于 React/Vue/Svelte 等组件化开发的可复用 FormValidator 类结构实现。其具备字段/表单级校验、同步与异步混合规则、防抖控制、消息本地化与可插拔消息格式化、解耦的事件通知、动态增删规则、跨字段依赖与远程唯一性校验等能力。

代码(TypeScript):

// Supporting types
export type Locale = 'en' | 'zh' | 'es';

export type ValidationRule = {
  name: string;
  test: (value: unknown, ctx?: Record<string, unknown>) => boolean | Promise<boolean>;
  message?: string;
  severity?: 'error' | 'warning';
};

export type ValidationResult = {
  valid: boolean;
  errors: string[];
};

export interface EventEmitter {
  on(event: string, cb: (payload?: unknown) => void): void;
  off(event: string, cb: (payload?: unknown) => void): void;
  emit(event: string, payload?: unknown): void;
}

type MessageFormatter = (
  template: string,
  params?: Record<string, unknown>,
  locale?: Locale
) => string;

type Values = Record<string, unknown>;
type Ctx = Record<string, unknown>;

// Simple default emitter (decoupled from any UI framework)
class SimpleEmitter implements EventEmitter {
  private listeners = new Map<string, Set<(payload?: unknown) => void>>();
  on(event: string, cb: (payload?: unknown) => void) {
    if (!this.listeners.has(event)) this.listeners.set(event, new Set());
    this.listeners.get(event)!.add(cb);
  }
  off(event: string, cb: (payload?: unknown) => void) {
    this.listeners.get(event)?.delete(cb);
  }
  emit(event: string, payload?: unknown) {
    this.listeners.get(event)?.forEach((cb) => {
      try {
        cb(payload);
      } catch {
        /* swallow */
      }
    });
  }
}

export class FormValidator {
  // properties
  public rules: Map<string, ValidationRule[]> = new Map();
  public errors: Map<string, string[]> = new Map();
  public locale: Locale = 'en';
  public messages: Record<string, Record<string, string>>;
  public debounceMs: number = 250;
  public emitter: EventEmitter;
  public disposed: boolean = false;

  // pluggable message formatter
  private formatter: MessageFormatter;

  // internal state for debounce/coalescing
  private fieldTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
  private fieldLatestArgs: Map<string, { value: unknown; context?: Ctx }> = new Map();
  private fieldPending: Map<
    string,
    { promise: Promise<ValidationResult>; resolve: (r: ValidationResult) => void }
  > = new Map();

  constructor(options?: {
    locale?: Locale;
    messages?: Record<Locale, Record<string, string>>;
    debounceMs?: number;
    emitter?: EventEmitter;
    formatter?: MessageFormatter;
  }) {
    this.locale = options?.locale ?? 'en';
    this.debounceMs = options?.debounceMs ?? 250;
    this.emitter = options?.emitter ?? new SimpleEmitter();
    this.messages = {
      en: {
        required: 'This field is required.',
        match: 'Values do not match.',
        unique: 'This value is already taken.',
        minLength: 'Value is too short.',
        maxLength: 'Value is too long.',
        pattern: 'Invalid format.',
        exception: 'Validation failed',
      },
      zh: {
        required: '该字段为必填项。',
        match: '两次输入不一致。',
        unique: '该值已被占用。',
        minLength: '输入长度过短。',
        maxLength: '输入长度过长。',
        pattern: '格式不正确。',
        exception: '校验失败',
      },
      es: {
        required: 'Este campo es obligatorio.',
        match: 'Los valores no coinciden.',
        unique: 'Este valor ya está en uso.',
        minLength: 'El valor es demasiado corto.',
        maxLength: 'El valor es demasiado largo.',
        pattern: 'Formato inválido.',
        exception: 'Falló la validación',
      },
      ...(options?.messages ?? {}),
    };

    // 默认的消息格式化器:简单的 {key} 插值
    this.formatter =
      options?.formatter ??
      ((template: string, params?: Record<string, unknown>) => {
        if (!params) return template;
        return template.replace(/\{(\w+)\}/g, (_, k) =>
          params[k] !== undefined ? String(params[k]) : `{${k}}`
        );
      });
  }

  // methods

  /**
   * 为指定字段添加规则,支持批量和前置插入。
   * @param field 字段名
   * @param rule 单条或多条规则
   * @param options { prepend?: boolean } - 是否前置插入
   */
  public addRule(
    field: string,
    rule: ValidationRule | ValidationRule[],
    options?: { prepend?: boolean }
  ): void {
    this.ensureNotDisposed();
    const list = this.rules.get(field) ?? [];
    const incoming = Array.isArray(rule) ? rule : [rule];
    if (options?.prepend) {
      this.rules.set(field, [...incoming, ...list]);
    } else {
      this.rules.set(field, [...list, ...incoming]);
    }
  }

  /**
   * 移除字段的指定规则或全部规则,返回移除数量。
   * @param field 字段名
   * @param ruleName 可选,规则名称,不传则移除该字段全部规则
   */
  public removeRule(field: string, ruleName?: string): number {
    this.ensureNotDisposed();
    const list = this.rules.get(field);
    if (!list || list.length === 0) return 0;
    if (!ruleName) {
      const count = list.length;
      this.rules.delete(field);
      return count;
    }
    const remain = list.filter((r) => r.name !== ruleName);
    const removed = list.length - remain.length;
    if (remain.length > 0) this.rules.set(field, remain);
    else this.rules.delete(field);
    return removed;
  }

  /**
   * 校验单个字段。对包含异步规则的字段,会进行防抖合并:短时间多次调用将合并为一次最终执行,
   * 结果以“最后一次调用的最新值”为准(last-call-wins)。
   * @param field 字段名
   * @param value 当前字段值
   * @param context 额外上下文(可放入全部表单值以支持跨字段校验)
   * @returns Promise<ValidationResult>
   */
  public validateField(
    field: string,
    value: unknown,
    context?: Ctx
  ): Promise<ValidationResult> {
    this.ensureNotDisposed();
    const rules = this.rules.get(field) ?? [];
    if (rules.length === 0) {
      // 无规则则视为通过并清空错误
      this.errors.delete(field);
      const res: ValidationResult = { valid: true, errors: [] };
      this.emitter.emit('validated', {
        scope: 'field',
        field,
        ...res,
        warnings: [],
      });
      return Promise.resolve(res);
    }

    // 检测是否包含异步规则(粗略检查 async function)
    const hasAsync = rules.some((r) => r.test && r.test.constructor.name === 'AsyncFunction');

    // 合并 context 与默认参数
    const paramsBase = { field, value, ...(context ?? {}) };

    const runAll = async (
      v: unknown,
      ctx?: Ctx
    ): Promise<{ errors: string[]; warnings: string[] }> => {
      const errors: string[] = [];
      const warnings: string[] = [];
      for (const rule of rules) {
        try {
          const ok = await rule.test(v, ctx);
          if (!ok) {
            const template =
              rule.message ??
              this.messages[this.locale]?.[rule.name] ??
              this.messages['en']?.[rule.name] ??
              'Invalid.';
            const msg = this.formatter(template, { ...paramsBase, value: v }, this.locale);
            if (rule.severity === 'warning') warnings.push(msg);
            else errors.push(msg);
          }
        } catch (e) {
          const tmpl = this.messages[this.locale]?.['exception'] ?? 'Validation failed';
          const msg = `${tmpl}${(e as Error)?.message ? `: ${(e as Error).message}` : ''}`;
          errors.push(msg);
          this.emitter.emit('error', { scope: 'field', field, error: e });
        }
      }
      return { errors, warnings };
    };

    // 若全为同步规则,立即执行
    if (!hasAsync || this.debounceMs <= 0) {
      return runAll(value, context).then(({ errors, warnings }) => {
        this.setFieldErrors(field, errors);
        const res: ValidationResult = { valid: errors.length === 0, errors };
        this.emitter.emit('validated', { scope: 'field', field, ...res, warnings });
        return res;
      });
    }

    // 对包含异步规则的字段进行防抖合并
    // 记录最新值与上下文
    this.fieldLatestArgs.set(field, { value, context });

    // 复用 pending promise(多次频繁调用返回同一 promise,最终以最后一次值为准)
    if (!this.fieldPending.has(field)) {
      let resolver!: (r: ValidationResult) => void;
      const promise = new Promise<ValidationResult>((resolve) => {
        resolver = resolve;
      });
      this.fieldPending.set(field, { promise, resolve: resolver });
    }

    // 重置防抖计时器
    const existingTimer = this.fieldTimers.get(field);
    if (existingTimer) clearTimeout(existingTimer);

    const timer = setTimeout(async () => {
      // 取最后一次传入的值与上下文
      const latest = this.fieldLatestArgs.get(field);
      const latestVal = latest?.value;
      const latestCtx = latest?.context;
      const { errors, warnings } = await runAll(latestVal, latestCtx);
      this.setFieldErrors(field, errors);
      const res: ValidationResult = { valid: errors.length === 0, errors };
      this.emitter.emit('validated', { scope: 'field', field, ...res, warnings });

      // 完成并清理
      const pending = this.fieldPending.get(field);
      pending?.resolve(res);
      this.fieldPending.delete(field);
      this.fieldTimers.delete(field);
      this.fieldLatestArgs.delete(field);
    }, this.debounceMs);

    this.fieldTimers.set(field, timer);
    return this.fieldPending.get(field)!.promise;
  }

  /**
   * 校验整个表单,聚合所有字段错误。
   * 默认沿用字段级防抖行为(含异步规则的字段会等待防抖后执行)。
   * @param values 表单值
   * @param context 额外上下文(默认会与 values 合并,便于跨字段规则访问)
   * @returns Promise<{ valid: boolean; errors: Record<string, string[]> }>
   */
  public async validateForm(
    values: Values,
    context?: Ctx
  ): Promise<{ valid: boolean; errors: Record<string, string[]> }> {
    this.ensureNotDisposed();
    const ctx: Ctx = { ...(values ?? {}), ...(context ?? {}) };

    const fields = [...this.rules.keys()];
    const results = await Promise.all(
      fields.map((f) => this.validateField(f, values[f], ctx))
    );

    // 聚合错误(此处使用内部 errors Map 更稳妥)
    const errorRecord: Record<string, string[]> = {};
    for (const [field, msgs] of this.errors) {
      errorRecord[field] = msgs.slice();
    }
    const valid = results.every((r) => r.valid);

    this.emitter.emit('validated', {
      scope: 'form',
      valid,
      errors: errorRecord,
    });

    return { valid, errors: errorRecord };
  }

  /**
   * 切换语言,可增量注入消息模板(与现有模板合并)。
   * @param locale 目标语言
   * @param messages 可选,增量消息模板
   */
  public setLocale(locale: Locale, messages?: Record<string, string>): void {
    this.ensureNotDisposed();
    if (messages) {
      this.messages[locale] = { ...(this.messages[locale] ?? {}), ...messages };
    }
    this.locale = locale;
  }

  /**
   * 获取单字段或全部错误。
   * @param field 可选,字段名
   */
  public getErrors(field?: string): string[] | Record<string, string[]> {
    this.ensureNotDisposed();
    if (field) {
      return this.errors.get(field) ?? [];
    }
    const all: Record<string, string[]> = {};
    for (const [k, v] of this.errors) all[k] = v.slice();
    return all;
  }

  /**
   * 清空单字段或全部错误并发出 'validated' 事件。
   * @param field 可选,字段名
   */
  public clearErrors(field?: string): void {
    this.ensureNotDisposed();
    if (field) {
      this.errors.delete(field);
      this.emitter.emit('validated', {
        scope: 'field',
        field,
        valid: true,
        errors: [],
        warnings: [],
      });
      return;
    }
    this.errors.clear();
    this.emitter.emit('validated', { scope: 'form', valid: true, errors: {} });
  }

  /**
   * 设置异步校验防抖时间(毫秒)。
   * @param ms 毫秒数
   */
  public useDebounce(ms: number): void {
    this.ensureNotDisposed();
    this.debounceMs = Math.max(0, ms);
  }

  /**
   * 解绑事件、清理资源并标记 disposed。
   */
  public dispose(): void {
    if (this.disposed) return;
    this.disposed = true;
    // 清理防抖定时器
    for (const t of this.fieldTimers.values()) clearTimeout(t);
    this.fieldTimers.clear();
    // 使未决 promise 安全完成
    for (const [field, pending] of this.fieldPending) {
      try {
        pending.resolve({ valid: false, errors: ['Disposed'] });
      } catch {
        /* ignore */
      }
      this.emitter.emit('error', { scope: 'field', field, error: new Error('Disposed') });
    }
    this.fieldPending.clear();
    this.fieldLatestArgs.clear();
    // 不移除外部 emitter 的监听者,仅派发一次 disposed 事件
    this.emitter.emit('disposed', { disposed: true });
  }

  // 可选:允许在运行时替换消息格式化器(可插拔)
  public setMessageFormatter(formatter: MessageFormatter): void {
    this.ensureNotDisposed();
    this.formatter = formatter;
  }

  // ------ helpers ------

  private setFieldErrors(field: string, messages: string[]) {
    if (messages.length > 0) this.errors.set(field, messages);
    else this.errors.delete(field);
  }

  private ensureNotDisposed() {
    if (this.disposed) {
      throw new Error('FormValidator has been disposed.');
    }
  }
}

方法签名与返回类型说明:

  • addRule(field: string, rule: ValidationRule | ValidationRule[], options?: { prepend?: boolean }): void

    • 参数:字段名;规则或规则数组;可选是否前置插入
    • 返回:void
  • removeRule(field: string, ruleName?: string): number

    • 参数:字段名;可选的规则名称
    • 返回:移除的规则数量
  • validateField(field: string, value: unknown, context?: Record<string, unknown>): Promise

    • 参数:字段名;值;可选上下文(建议传整个表单值以支持跨字段校验)
    • 返回:Promise,包含 valid 与 errors
  • validateForm(values: Record<string, unknown>, context?: Record<string, unknown>): Promise<{ valid: boolean; errors: Record<string, string[]> }>

    • 参数:表单值;可选上下文(默认与 values 合并)
    • 返回:Promise,包含整体 valid 与按字段聚合的 errors
  • setLocale(locale: 'en' | 'zh' | 'es', messages?: Record<string, string>): void

    • 参数:目标语言;可选增量消息模板(会与已有模板合并)
    • 返回:void
  • getErrors(field?: string): string[] | Record<string, string[]>

    • 参数:可选字段名
    • 返回:若传字段名返回该字段错误数组,否则返回所有字段错误映射
  • clearErrors(field?: string): void

    • 参数:可选字段名
    • 返回:void,同时派发 validated 事件
  • useDebounce(ms: number): void

    • 参数:防抖间隔毫秒
    • 返回:void
  • dispose(): void

    • 参数:无
    • 返回:void,清理资源并标记 disposed

设计要点与说明:

  • 动态增删规则:rules 为 Map,addRule/removeRule 即时生效。
  • 同步/异步混合:统一在 validateField 中顺序 await;存在异步规则时采用字段级防抖合并,减少频繁远程请求(如用户名唯一性)。
  • 跨字段依赖:将整个 values 合并进 context,规则实现中可通过 ctx.otherField 读取其它字段。
  • 本地化与消息格式化:消息模板源自 messages[locale][rule.name];支持 rule.message 覆盖。formatter 可插拔,默认提供 {key} 插值。
  • 事件通知:emitter.emit('validated' | 'error' | 'disposed', payload) 解耦 UI;组件可订阅相应事件联动渲染。
  • 错误存储:errors 为 Map<string, string[]>,仅记录 severity !== 'warning' 的错误。warning 将通过事件 payload 返回,便于 UI 单独显示。
  • 防抖策略:含异步规则的字段采用 last-call-wins 合并,短时间多次 validateField 调用将复用同一 Promise,最终以最后一次的值为准。
  • 资源释放:dispose 会清理定时器、结束未决 Promise 并置位 disposed,防止内存泄漏与误用。

// Domain model for an e-commerce Order aggregate (DDD-oriented)

import java.math.BigDecimal; import java.math.RoundingMode; import java.time.Instant; import java.util.*; import java.util.stream.Collectors;

public final class OrderAggregate {

// --------- Key fields (Aggregate state) ---------
private final String orderId;                      // 订单唯一标识
private final String customerId;                   // 用户标识
private final List<OrderItem> items;               // 行项
private Money subtotal;                            // 小计
private Money discount;                            // 折扣金额
private Money total;                               // 应付总额
private OrderStatus status;                        // 状态
private Instant createdAt;                         // 创建时间
private Instant updatedAt;                         // 最近修改
private final List<DomainEvent> domainEvents;      // 未提交事件
private final Map<String, Object> metadata;        // 拓展信息(渠道等)

// ---------- Factory ----------
public static OrderAggregate create(String orderId, String customerId, String currency, Map<String, Object> metadata) {
    Objects.requireNonNull(orderId, "orderId");
    Objects.requireNonNull(customerId, "customerId");
    Objects.requireNonNull(currency, "currency");
    Instant now = Instant.now();
    return new OrderAggregate(
            orderId,
            customerId,
            new ArrayList<>(),
            Money.zero(currency),
            Money.zero(currency),
            Money.zero(currency),
            OrderStatus.CREATED,
            now,
            now,
            new ArrayList<>(),
            metadata == null ? new HashMap<>() : new HashMap<>(metadata)
    );
}

private OrderAggregate(
        String orderId,
        String customerId,
        List<OrderItem> items,
        Money subtotal,
        Money discount,
        Money total,
        OrderStatus status,
        Instant createdAt,
        Instant updatedAt,
        List<DomainEvent> domainEvents,
        Map<String, Object> metadata
) {
    this.orderId = orderId;
    this.customerId = customerId;
    this.items = items;
    this.subtotal = subtotal;
    this.discount = discount;
    this.total = total;
    this.status = status;
    this.createdAt = createdAt;
    this.updatedAt = updatedAt;
    this.domainEvents = domainEvents;
    this.metadata = metadata;
}

// ---------- Commands (intention-revealing) ----------

// 新增行项(仅允许在 CREATED 状态)
public OrderAggregate addItem(String productId, String name, Money price, int quantity) {
    assertState(OrderStatus.CREATED, "addItem only allowed in CREATED");
    Objects.requireNonNull(productId, "productId");
    Objects.requireNonNull(name, "name");
    Objects.requireNonNull(price, "price");
    if (quantity <= 0) throw new IllegalArgumentException("quantity must be > 0");
    ensureMoneyCurrency(price);

    Optional<OrderItem> existing = items.stream()
            .filter(it -> it.getProductId().equals(productId))
            .findFirst();

    if (existing.isPresent()) {
        OrderItem e = existing.get();
        int newQty = e.getQuantity() + quantity;
        OrderItem updated = new OrderItem(e.getProductId(), e.getName(), e.getPrice(), newQty);
        int idx = items.indexOf(e);
        items.set(idx, updated);
    } else {
        items.add(new OrderItem(productId, name, price, quantity));
    }
    calculateTotals();
    touch();
    return this;
}

// 移除行项(仅允许在 CREATED 状态)
public OrderAggregate removeItem(String productId) {
    assertState(OrderStatus.CREATED, "removeItem only allowed in CREATED");
    Objects.requireNonNull(productId, "productId");
    boolean removed = items.removeIf(it -> it.getProductId().equals(productId));
    if (!removed) return this; // idempotent
    calculateTotals();
    touch();
    return this;
}

// 应用折扣策略(仅允许在 CREATED 状态)
// 返回折扣金额(以 subtotal 的货币计)
public BigDecimal applyDiscountCode(String code, DiscountPolicy policy) {
    assertState(OrderStatus.CREATED, "applyDiscountCode only allowed in CREATED");
    Objects.requireNonNull(policy, "policy");

    // 透传上下文,可包含渠道、用户等级等
    Map<String, Object> ctx = new HashMap<>(metadata);
    ctx.put("orderId", orderId);
    ctx.put("customerId", customerId);
    ctx.put("itemsCount", items.size());
    ctx.put("subtotal", subtotal.getAmount());

    BigDecimal computed = policy.compute(this.subtotal, code, ctx);
    if (computed == null) computed = BigDecimal.ZERO;
    if (computed.signum() < 0) computed = BigDecimal.ZERO;

    Money candidate = Money.of(computed, subtotal.getCurrency());
    // 折扣不得超过小计
    if (candidate.compareTo(subtotal) > 0) {
        candidate = subtotal;
    }
    this.discount = candidate;
    calculateTotals();
    touch();
    return this.discount.getAmount();
}

// 重算金额(内部一致性)
public void calculateTotals() {
    // 小计
    Money newSubtotal = Money.zero(subtotal.getCurrency());
    for (OrderItem it : items) {
        ensureMoneyCurrency(it.getPrice());
        Money line = it.getPrice().multiply(it.getQuantity());
        newSubtotal = newSubtotal.add(line);
    }
    this.subtotal = newSubtotal;

    // 折扣边界
    if (discount == null) {
        this.discount = Money.zero(subtotal.getCurrency());
    } else {
        ensureMoneyCurrency(discount);
        if (discount.signum() < 0) discount = Money.zero(subtotal.getCurrency());
        if (discount.compareTo(subtotal) > 0) discount = subtotal;
    }

    // 目前 total = subtotal - discount(运费/税费可通过扩展点加入)
    Money result = subtotal.subtract(discount);
    if (result.signum() < 0) result = Money.zero(subtotal.getCurrency());
    this.total = result;

    touch();
}

// 支付(外部已确认库存与支付成功后调用)
public void pay(String paymentId, Instant paidAt) {
    Objects.requireNonNull(paymentId, "paymentId");
    if (paidAt == null) paidAt = Instant.now();

    if (status != OrderStatus.CREATED) {
        throw new IllegalStateException("Only CREATED can be paid");
    }
    if (items.isEmpty()) {
        throw new IllegalStateException("Cannot pay an order without items");
    }

    this.status = OrderStatus.PAID;
    touch();

    domainEvents.add(new PaymentSucceeded(
            orderId, customerId, paymentId, total, paidAt
    ));
}

// 发货(仅 PAID -> SHIPPED)
public void ship(String trackingNo, String carrier, Instant shippedAt) {
    Objects.requireNonNull(trackingNo, "trackingNo");
    Objects.requireNonNull(carrier, "carrier");
    if (shippedAt == null) shippedAt = Instant.now();

    if (status != OrderStatus.PAID) {
        throw new IllegalStateException("Only PAID can be shipped");
    }

    this.status = OrderStatus.SHIPPED;
    touch();

    domainEvents.add(new OrderShipped(orderId, trackingNo, carrier, shippedAt));
}

// 取消订单(CREATED/PAID -> CANCELED,记录原因)
public void cancel(String reason) {
    Objects.requireNonNull(reason, "reason");
    if (status == OrderStatus.SHIPPED) {
        throw new IllegalStateException("Shipped order cannot be canceled");
    }
    if (status == OrderStatus.CANCELED) {
        return; // idempotent
    }
    this.status = OrderStatus.CANCELED;
    touch();

    domainEvents.add(new OrderCanceled(orderId, customerId, reason, Instant.now()));
}

// 导出只读 DTO(供查询或 API 返回)
public OrderDTO toDTO() {
    OrderDTO dto = new OrderDTO();
    dto.orderId = this.orderId;
    dto.customerId = this.customerId;
    dto.items = Collections.unmodifiableList(new ArrayList<>(this.items));
    dto.total = this.total;
    dto.status = this.status;
    return dto;
}

// 获取未提交事件(仓储提交成功后应清空)
public List<DomainEvent> getUncommittedEvents() {
    return Collections.unmodifiableList(new ArrayList<>(domainEvents));
}

// 可选:由仓储在成功持久化与发布后调用
public void clearUncommittedEvents() {
    domainEvents.clear();
}

// ---------- Invariants & helpers ----------
private void assertState(OrderStatus expected, String message) {
    if (this.status != expected) throw new IllegalStateException(message);
}

private void ensureMoneyCurrency(Money m) {
    if (!Objects.equals(m.getCurrency(), this.subtotal.getCurrency())) {
        throw new IllegalArgumentException("Money currency mismatch: expected " + this.subtotal.getCurrency()
                + " but was " + m.getCurrency());
    }
}

private void touch() {
    this.updatedAt = Instant.now();
}

// ---------- Accessors ----------
public String getOrderId() { return orderId; }
public String getCustomerId() { return customerId; }
public List<OrderItem> getItems() { return Collections.unmodifiableList(items); }
public Money getSubtotal() { return subtotal; }
public Money getDiscount() { return discount; }
public Money getTotal() { return total; }
public OrderStatus getStatus() { return status; }
public Instant getCreatedAt() { return createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public Map<String, Object> getMetadata() { return Collections.unmodifiableMap(metadata); }

// ---------- Supporting types ----------

public static final class OrderItem {
    private final String productId;
    private final String name;
    private final Money price;
    private final int quantity;

    public OrderItem(String productId, String name, Money price, int quantity) {
        this.productId = Objects.requireNonNull(productId, "productId");
        this.name = Objects.requireNonNull(name, "name");
        this.price = Objects.requireNonNull(price, "price");
        if (quantity <= 0) throw new IllegalArgumentException("quantity must be > 0");
        this.quantity = quantity;
    }

    public String getProductId() { return productId; }
    public String getName() { return name; }
    public Money getPrice() { return price; }
    public int getQuantity() { return quantity; }

    public Money lineTotal() { return price.multiply(quantity); }
}

public static final class Money implements Comparable<Money> {
    private final BigDecimal amount;
    private final String currency;

    public static Money of(BigDecimal amount, String currency) {
        Objects.requireNonNull(amount, "amount");
        Objects.requireNonNull(currency, "currency");
        return new Money(amount.setScale(2, RoundingMode.HALF_UP), currency);
    }

    public static Money of(long minorUnits, String currency) {
        BigDecimal amt = BigDecimal.valueOf(minorUnits).movePointLeft(2);
        return of(amt, currency);
    }

    public static Money zero(String currency) {
        return new Money(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP), currency);
    }

    private Money(BigDecimal amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    public Money add(Money other) {
        checkCurrency(other);
        return of(this.amount.add(other.amount), currency);
    }

    public Money subtract(Money other) {
        checkCurrency(other);
        return of(this.amount.subtract(other.amount), currency);
    }

    public Money multiply(int times) {
        if (times < 0) throw new IllegalArgumentException("times must be >= 0");
        return of(this.amount.multiply(BigDecimal.valueOf(times)), currency);
    }

    public int compareTo(Money o) {
        checkCurrency(o);
        return this.amount.compareTo(o.amount);
    }

    public int signum() {
        return amount.signum();
    }

    public BigDecimal getAmount() { return amount; }
    public String getCurrency() { return currency; }

    private void checkCurrency(Money other) {
        if (!Objects.equals(this.currency, other.currency)) {
            throw new IllegalArgumentException("Currency mismatch: " + this.currency + " vs " + other.currency);
        }
    }

    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Money)) return false;
        Money money = (Money) o;
        return amount.compareTo(money.amount) == 0 && Objects.equals(currency, money.currency);
    }

    @Override public int hashCode() {
        return Objects.hash(amount.stripTrailingZeros(), currency);
    }

    @Override public String toString() {
        return amount.toPlainString() + " " + currency;
    }
}

public enum OrderStatus {
    CREATED, PAID, SHIPPED, CANCELED
}

public interface DomainEvent {
    String type();
    Instant occurredAt();
}

// --------- Domain events ---------
public static final class PaymentSucceeded implements DomainEvent {
    private final String orderId;
    private final String customerId;
    private final String paymentId;
    private final Money paidAmount;
    private final Instant occurredAt;

    public PaymentSucceeded(String orderId, String customerId, String paymentId, Money paidAmount, Instant occurredAt) {
        this.orderId = orderId;
        this.customerId = customerId;
        this.paymentId = paymentId;
        this.paidAmount = paidAmount;
        this.occurredAt = occurredAt;
    }

    @Override public String type() { return "PaymentSucceeded"; }
    @Override public Instant occurredAt() { return occurredAt; }

    public String getOrderId() { return orderId; }
    public String getCustomerId() { return customerId; }
    public String getPaymentId() { return paymentId; }
    public Money getPaidAmount() { return paidAmount; }
}

public static final class OrderShipped implements DomainEvent {
    private final String orderId;
    private final String trackingNo;
    private final String carrier;
    private final Instant occurredAt;

    public OrderShipped(String orderId, String trackingNo, String carrier, Instant occurredAt) {
        this.orderId = orderId;
        this.trackingNo = trackingNo;
        this.carrier = carrier;
        this.occurredAt = occurredAt;
    }

    @Override public String type() { return "OrderShipped"; }
    @Override public Instant occurredAt() { return occurredAt; }

    public String getOrderId() { return orderId; }
    public String getTrackingNo() { return trackingNo; }
    public String getCarrier() { return carrier; }
}

public static final class OrderCanceled implements DomainEvent {
    private final String orderId;
    private final String customerId;
    private final String reason;
    private final Instant occurredAt;

    public OrderCanceled(String orderId, String customerId, String reason, Instant occurredAt) {
        this.orderId = orderId;
        this.customerId = customerId;
        this.reason = reason;
        this.occurredAt = occurredAt;
    }

    @Override public String type() { return "OrderCanceled"; }
    @Override public Instant occurredAt() { return occurredAt; }

    public String getOrderId() { return orderId; }
    public String getCustomerId() { return customerId; }
    public String getReason() { return reason; }
}

// --------- Discount policy port ---------
public interface DiscountPolicy {
    // 返回折扣金额(与 subtotal 的 currency 一致)
    BigDecimal compute(Money subtotal, String code, Map<String, Object> ctx);
}

// --------- Read-only DTO ---------
public static final class OrderDTO {
    public String orderId;
    public String customerId;
    public List<OrderItem> items;
    public Money total;
    public OrderStatus status;
}

}

from __future__ import annotations

import datetime
import hashlib
import json
import logging
import re
import threading
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Tuple, Union


@dataclass(frozen=True)
class Rule:
    """
    单条评估规则。
    - name: 规则名称,便于调试与导出
    - predicate: 上下文判断函数,入参为 context: Dict[str, Any],返回 True/False
    - rollout: 可选的按比例灰度(0-100),当 predicate 为 True 且 identity 命中 rollout 桶时才算匹配
    """
    name: str
    predicate: Callable[[Dict[str, Any]], bool]
    rollout: Optional[int] = None

    def matches(self, context: Optional[Dict[str, Any]], key: str) -> bool:
        """
        计算规则对给定上下文是否命中。
        - 若设置 rollout,需要上下文中可解析的 identity;若缺失 identity,则不命中。
        """
        ctx = context or {}
        try:
            if not self.predicate(ctx):
                return False
        except Exception:
            # 避免业务 predicate 抛错影响整体评估
            return False

        if self.rollout is None:
            return True

        identity = FeatureToggleManager._extract_identity(ctx)
        if identity is None:
            return False  # 没有 identity 时,带 rollout 的规则不命中,避免意外放量

        bucket = FeatureToggleManager.compute_rollout_bucket(key, identity)
        return bucket < int(self.rollout)


@dataclass
class Toggle:
    """
    单个功能开关的定义。
    - key: 唯一键
    - enabled: 顶层开关(全局门禁)。False 时无论规则如何均返回 False;True 时结合规则评估。
    - rules: 可选规则列表;存在时需至少一条规则命中才返回 True。
    """
    key: str
    enabled: bool
    rules: Optional[List[Rule]] = None


class FeatureToggleManager:
    """
    统一管理功能开关的定义、加载、刷新与评估,支持:
    - 本地配置与远程拉取(fetcher)
    - 用户分群与灰度(Rule.predicate + Rule.rollout)
    - 可插拔钩子通知(开关 enabled 基态变化)
    - 线程安全缓存(TTL + RLock)
    适用场景:按环境发布、A/B 实验预埋、回滚策略等。
    """

    # -------------------------
    # 公共属性(按要求暴露)
    # -------------------------
    toggles: Dict[str, Toggle]
    default_state: bool
    fetcher: Optional[Callable[[], Dict[str, Any]]]
    cache_ttl_seconds: int
    last_refresh_at: Optional[datetime.datetime]
    hooks: List[Callable[[str, bool], None]]
    lock: threading.RLock
    logger: logging.Logger

    # -------------------------
    # 初始化
    # -------------------------
    def __init__(
        self,
        default_state: bool = False,
        fetcher: Optional[Callable[[], Dict[str, Any]]] = None,
        cache_ttl_seconds: int = 30,
        logger: Optional[logging.Logger] = None,
        initial_toggles: Optional[Dict[str, Toggle]] = None,
    ) -> None:
        """
        参数:
        - default_state: bool,未命中时的默认值
        - fetcher: 可选远程拉取函数,返回 Dict[str, Any] 的最新开关定义
        - cache_ttl_seconds: 本地缓存 TTL,过期后触发刷新
        - logger: 日志记录器,默认使用模块 logger
        - initial_toggles: 可选初始开关集合
        """
        self.default_state = bool(default_state)
        self.fetcher = fetcher
        self.cache_ttl_seconds = int(cache_ttl_seconds)
        self.logger = logger or logging.getLogger(__name__)
        self.toggles = initial_toggles.copy() if initial_toggles else {}
        self.last_refresh_at = None
        self.hooks = []
        self.lock = threading.RLock()

    # -------------------------
    # 核心 API
    # -------------------------
    def is_enabled(
        self,
        key: str,
        context: Optional[Dict[str, Any]] = None,
        default: Optional[bool] = None,
    ) -> bool:
        """
        基于规则与上下文计算开关是否开启(线程安全)。
        评估规则:
        - 若 key 不存在 => 返回 fallback: default 或 default_state
        - 若 toggle.enabled 为 False => 返回 False
        - 若 toggle.enabled 为 True:
            - 无规则 => True
            - 存在规则 => 任一规则命中且(若配置 rollout)命中桶 => True;否则 False

        参数:
        - key: str,开关键
        - context: 可选 Dict,上下文(如 user_id、country、app_version 等)
        - default: 可选 bool,当 key 不存在时的回退值(优先于 default_state)

        返回:
        - bool,是否开启
        """
        # 自动确保在 TTL 过期时刷新缓存
        self._ensure_fresh()

        with self.lock:
            toggle = self.toggles.get(key)
            if toggle is None:
                return bool(default if default is not None else self.default_state)

            if not toggle.enabled:
                return False

            if not toggle.rules:
                return True

            for rule in toggle.rules:
                try:
                    if rule.matches(context, key):
                        return True
                except Exception as e:
                    self.logger.exception("Rule evaluation failed for %s/%s: %s", key, rule.name, e)
            return False

    def set_toggle(
        self,
        key: str,
        state: bool,
        rules: Optional[List[Rule]] = None,
    ) -> None:
        """
        设置或更新单个开关,触发变更钩子(仅当 enabled 基态变化时)。
        参数:
        - key: str
        - state: bool,enabled 基态
        - rules: 可选规则列表
        返回:
        - None
        """
        with self.lock:
            old = self.toggles.get(key)
            old_enabled = old.enabled if old else None

            self.toggles[key] = Toggle(key=key, enabled=bool(state), rules=rules[:] if rules else None)

            hooks_to_call = []
            if old_enabled is None or bool(state) != bool(old_enabled):
                hooks_to_call = self.hooks[:]

        # 钩子在锁外调用,避免阻塞/死锁
        for cb in hooks_to_call:
            try:
                cb(key, bool(state))
            except Exception as e:
                self.logger.exception("Hook callback failed for %s: %s", key, e)

    def refresh(self, force: bool = False) -> bool:
        """
        在 TTL 过期或强制模式下调用 fetcher 拉取最新开关,成功返回 True。
        参数:
        - force: bool,是否强制刷新(忽略 TTL)
        返回:
        - bool,是否发生了刷新且拉取成功
        """
        if self.fetcher is None:
            return False

        now = datetime.datetime.utcnow()
        with self.lock:
            if not force and self.last_refresh_at is not None:
                age = (now - self.last_refresh_at).total_seconds()
                if age < self.cache_ttl_seconds:
                    return False  # 未过期,无需刷新

        # 调用远程拉取在锁外执行,避免长时间占用锁
        try:
            raw = self.fetcher() or {}
        except Exception as e:
            self.logger.exception("Fetcher failed: %s", e)
            return False

        # 解析并计算变化
        new_toggles, changed_keys = self._parse_remote_payload(raw)

        with self.lock:
            # 对比 enabled 基态的变化(用于钩子)
            enabled_changes = self._diff_enabled_changes(self.toggles, new_toggles)
            self.toggles = new_toggles
            self.last_refresh_at = now
            hooks_to_call = self.hooks[:]
            changed_keys = enabled_changes  # 仅对 enabled 基态变化触发钩子

        for key, enabled in changed_keys:
            for cb in hooks_to_call:
                try:
                    cb(key, enabled)
                except Exception as e:
                    self.logger.exception("Hook callback failed for %s: %s", key, e)

        return True

    def load_from_file(self, path: str) -> int:
        """
        从本地 JSON/YAML 文件加载开关定义,返回加载数量。
        文件格式支持两种顶层结构:
        1) 直接 key -> toggleDef
        2) {"toggles": { key -> toggleDef }}

        toggleDef 支持:
        - 布尔值:true/false
        - 对象:
          {
            "enabled": true/false,
            "rules": [
               {
                 "name": "rule-name",
                 "predicate": "always" | { ... 见 _compile_predicate 文档 ... },
                 "rollout": 30
               }
            ]
          }

        参数:
        - path: str,文件路径(.json/.yaml/.yml)
        返回:
        - int,成功加载的开关数量
        """
        content: Dict[str, Any]
        if path.lower().endswith(".json"):
            with open(path, "r", encoding="utf-8") as f:
                content = json.load(f)
        elif path.lower().endswith((".yml", ".yaml")):
            try:
                import yaml  # type: ignore
            except Exception as e:
                raise RuntimeError("PyYAML is required to load YAML files") from e
            with open(path, "r", encoding="utf-8") as f:
                content = yaml.safe_load(f) or {}
        else:
            raise ValueError("Unsupported file extension. Use .json, .yaml or .yml")

        toggles_obj = content.get("toggles", content)
        if not isinstance(toggles_obj, dict):
            raise ValueError("Invalid toggle file format")

        new_toggles: Dict[str, Toggle] = {}
        for key, val in toggles_obj.items():
            t = self._parse_toggle_entry(str(key), val)
            new_toggles[key] = t

        with self.lock:
            enabled_changes = self._diff_enabled_changes(self.toggles, new_toggles)
            self.toggles.update(new_toggles)
            hooks_to_call = self.hooks[:]

        for key, enabled in enabled_changes:
            for cb in hooks_to_call:
                try:
                    cb(key, enabled)
                except Exception as e:
                    self.logger.exception("Hook callback failed for %s: %s", key, e)

        return len(new_toggles)

    def register_hook(self, cb: Callable[[str, bool], None]) -> None:
        """
        注册开关 enabled 基态变化的通知回调。
        参数:
        - cb: Callable[[str, bool], None],签名为 (key, enabled)
        返回:
        - None
        """
        if not callable(cb):
            raise ValueError("hook must be callable")
        with self.lock:
            self.hooks.append(cb)

    def export_state(self) -> Dict[str, Any]:
        """
        导出当前开关快照,便于调试或持久化(不导出 predicate 可执行体,仅导出规则元信息)。
        返回:
        - Dict[str, Any],包含 toggles、default_state、cache_ttl_seconds、last_refresh_at
        """
        with self.lock:
            snapshot: Dict[str, Any] = {
                "default_state": self.default_state,
                "cache_ttl_seconds": self.cache_ttl_seconds,
                "last_refresh_at": self.last_refresh_at.isoformat() if self.last_refresh_at else None,
                "toggles": {},
            }
            for k, t in self.toggles.items():
                snapshot["toggles"][k] = {
                    "enabled": t.enabled,
                    "rules": [{"name": r.name, "rollout": r.rollout} for r in (t.rules or [])],
                }
        return snapshot

    # -------------------------
    # 线程安全缓存辅助
    # -------------------------
    def _ensure_fresh(self) -> None:
        """
        若 TTL 过期尝试刷新(非强制),fetcher 可能为 None。
        """
        if self.fetcher is None or self.cache_ttl_seconds <= 0:
            return

        need_refresh = False
        now = datetime.datetime.utcnow()

        with self.lock:
            if self.last_refresh_at is None:
                need_refresh = True
            else:
                age = (now - self.last_refresh_at).total_seconds()
                if age >= self.cache_ttl_seconds:
                    need_refresh = True

        if need_refresh:
            try:
                self.refresh(force=False)
            except Exception as e:
                self.logger.exception("Auto refresh failed: %s", e)

    # -------------------------
    # 解析与编译
    # -------------------------
    def _parse_remote_payload(self, raw: Dict[str, Any]) -> Tuple[Dict[str, Toggle], List[Tuple[str, bool]]]:
        """
        将 fetcher 返回的原始字典解析为 Toggle 集合。
        返回: (new_toggles, changed_enabled_pairs),其中 changed_enabled_pairs 在此阶段不计算,
        实际“变化”由 _diff_enabled_changes 决定;此处返回空列表即可。
        """
        toggles_obj = raw.get("toggles", raw)
        new_toggles: Dict[str, Toggle] = {}
        if not isinstance(toggles_obj, dict):
            self.logger.warning("Fetcher payload is not a dict")
            return {}, []

        for key, val in toggles_obj.items():
            try:
                t = self._parse_toggle_entry(str(key), val)
                new_toggles[key] = t
            except Exception as e:
                self.logger.exception("Failed to parse toggle %s: %s", key, e)

        return new_toggles, []

    def _parse_toggle_entry(self, key: str, val: Any) -> Toggle:
        """
        解析单个 toggle 条目。
        支持:
        - bool => enabled=val, rules=None
        - dict => 需要 enabled 字段,可含 rules
        """
        if isinstance(val, bool):
            return Toggle(key=key, enabled=val, rules=None)

        if isinstance(val, dict):
            if "enabled" not in val:
                raise ValueError(f"Toggle {key} missing 'enabled'")
            enabled = bool(val.get("enabled"))
            raw_rules = val.get("rules") or []
            rules: List[Rule] = []
            for r in raw_rules:
                if not isinstance(r, dict):
                    raise ValueError(f"Toggle {key} rules item must be object")
                name = str(r.get("name") or "rule")
                rollout = r.get("rollout")
                if rollout is not None:
                    rollout = int(rollout)
                    if rollout < 0 or rollout > 100:
                        raise ValueError(f"Toggle {key} rule {name} rollout must be 0..100")

                pred_spec = r.get("predicate", "always")
                predicate = self._compile_predicate(pred_spec)
                rules.append(Rule(name=name, predicate=predicate, rollout=rollout))

            return Toggle(key=key, enabled=enabled, rules=rules or None)

        raise ValueError(f"Unsupported toggle definition for {key}")

    # 简易谓词编译器:支持常见条件组合,满足多数灰度/分群场景
    def _compile_predicate(self, spec: Any) -> Callable[[Dict[str, Any]], bool]:
        """
        支持的 spec 形式:
        - "always" => 恒真
        - {"eq": {"field": "country", "value": "US"}}
        - {"in": {"field": "country", "values": ["US", "CA"]}}
        - {"regex": {"field": "app_version", "pattern": "^2\\."}}
        - {"gte": {"field": "build", "value": 120}}  数字比较
        - {"any": [spec1, spec2, ...]}
        - {"all": [spec1, spec2, ...]}
        - {"not": spec}
        """
        if spec == "always" or spec is True or spec is None:
            return lambda ctx: True

        if not isinstance(spec, dict) or len(spec) != 1:
            raise ValueError(f"Unsupported predicate spec: {spec}")

        op, arg = next(iter(spec.items()))

        if op == "eq":
            field, value = arg["field"], arg["value"]
            return lambda ctx: ctx.get(field) == value

        if op == "in":
            field, values = arg["field"], set(arg["values"])
            return lambda ctx: ctx.get(field) in values

        if op == "regex":
            field, pattern = arg["field"], arg["pattern"]
            regex = re.compile(pattern)
            return lambda ctx: isinstance(ctx.get(field), str) and bool(regex.search(ctx[field]))

        if op == "gte":
            field, value = arg["field"], arg["value"]
            return lambda ctx: FeatureToggleManager._to_number(ctx.get(field)) >= FeatureToggleManager._to_number(value)

        if op == "any":
            subs = [self._compile_predicate(s) for s in (arg or [])]
            return lambda ctx: any(p(ctx) for p in subs)

        if op == "all":
            subs = [self._compile_predicate(s) for s in (arg or [])]
            return lambda ctx: all(p(ctx) for p in subs)

        if op == "not":
            sub = self._compile_predicate(arg)
            return lambda ctx: not sub(ctx)

        raise ValueError(f"Unknown predicate operator: {op}")

    # -------------------------
    # 工具方法
    # -------------------------
    @staticmethod
    def _to_number(v: Any) -> float:
        try:
            return float(v)
        except Exception:
            return float("nan")

    @staticmethod
    def _extract_identity(context: Dict[str, Any]) -> Optional[str]:
        """
        从上下文中提取稳定 identity,用于一致性灰度。
        常用字段:user_id/uid/id/device_id/session_id/account/email
        """
        candidates = ("user_id", "uid", "id", "device_id", "session_id", "account", "email")
        for k in candidates:
            if k in context and context[k] is not None:
                return str(context[k])
        return None

    @staticmethod
    def compute_rollout_bucket(key: str, identity: str, buckets: int = 100) -> int:
        """
        基于 key + identity 生成 0..(buckets-1) 的稳定桶值(默认 0..99)。
        """
        seed = f"{key}:{identity}".encode("utf-8")
        digest = hashlib.sha256(seed).hexdigest()
        as_int = int(digest[:8], 16)  # 取前 32bit 足够
        return as_int % buckets

    @staticmethod
    def _diff_enabled_changes(
        old: Dict[str, Toggle],
        new: Dict[str, Toggle],
    ) -> List[Tuple[str, bool]]:
        """
        计算 enabled 基态的变化,用于触发钩子。
        返回: [(key, new_enabled), ...]
        """
        changes: List[Tuple[str, bool]] = []
        old_keys = set(old.keys())
        new_keys = set(new.keys())
        for k in new_keys.union(old_keys):
            old_enabled = old.get(k).enabled if k in old else None
            new_enabled = new.get(k).enabled if k in new else None
            if old_enabled is None and new_enabled is not None:
                changes.append((k, bool(new_enabled)))
            elif new_enabled is None:
                # 新集合中不存在,认为删除,不触发(也可选择触发 False)
                continue
            elif bool(old_enabled) != bool(new_enabled):
                changes.append((k, bool(new_enabled)))
        return changes

示例详情

解决的问题

帮助用户快速生成清晰、专业的类模块结构,包括属性和方法的详细说明,以满足软件开发中的架构设计需求。

适用用户

软件开发工程师

帮助工程师根据业务需求快速生成完整的类设计结构,减少前期架构设计时间,聚焦核心代码实现。

系统架构设计师

为设计师提供自动化工具,快速验证复杂系统的类模块关系与职责分布,提升设计效率与质量。

技术团队管理者

支持管理者快速生成标准化模板,便于团队协作,规范代码设计,同时缩短项目交付周期。

特征总结

根据用户需求,自动化生成清晰的类与模块结构,提高开发效率。
支持主流编程语言,轻松适配多种开发场景与项目需求。
智能推荐关键属性和方法,确保设计的类结构覆盖主要功能。
详细说明方法参数与返回类型,助力代码逻辑更规范易懂。
灵活定义类责任范围,满足复杂业务场景中的个性化需求。
强调面向对象设计原则,帮助用户构建高复用性和可扩展性系统。
一键生成标准化设计模板,便于团队协作与代码审查。
支持精准描述复杂属性与方法逻辑,助力高级开发者高效设计。
从零开始到完整生成,快速搭建符合需求的类结构。

如何使用购买的提示词模板

1. 直接在外部 Chat 应用中使用

将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。

2. 发布为 API 接口调用

把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。

3. 在 MCP Client 中配置使用

在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。

AI 提示词价格
¥20.00元
先用后买,用好了再付款,超安全!

您购买后可以获得什么

获得完整提示词模板
- 共 121 tokens
- 5 个可调节参数
{ 编程语言 } { 类名称 } { 类职责描述 } { 属性信息 } { 方法信息 }
获得社区贡献内容的使用权
- 精选社区优质案例,助您快速上手提示词
使用提示词兑换券,低至 ¥ 9.9
了解兑换券 →
限时半价

不要错过!

半价获取高级提示词-优惠即将到期

17
:
23
小时
:
59
分钟
:
59