热门角色不仅是灵感来源,更是你的效率助手。通过精挑细选的角色提示词,你可以快速生成高质量内容、提升创作灵感,并找到最契合你需求的解决方案。让创作更轻松,让价值更直接!
我们根据不同用户需求,持续更新角色库,让你总能找到合适的灵感入口。
本提示词可根据用户需求生成完整类结构,包含关键属性、方法及参数与返回类型说明,支持前端组件化开发与面向对象建模,提升类设计效率与代码可维护性。
下面给出一个可用于 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
removeRule(field: string, ruleName?: string): number
validateField(field: string, value: unknown, context?: Record<string, unknown>): Promise
validateForm(values: Record<string, unknown>, context?: Record<string, unknown>): Promise<{ valid: boolean; errors: Record<string, string[]> }>
setLocale(locale: 'en' | 'zh' | 'es', messages?: Record<string, string>): void
getErrors(field?: string): string[] | Record<string, string[]>
clearErrors(field?: string): void
useDebounce(ms: number): void
dispose(): void
设计要点与说明:
// 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
帮助用户快速生成清晰、专业的类模块结构,包括属性和方法的详细说明,以满足软件开发中的架构设计需求。
帮助工程师根据业务需求快速生成完整的类设计结构,减少前期架构设计时间,聚焦核心代码实现。
为设计师提供自动化工具,快速验证复杂系统的类模块关系与职责分布,提升设计效率与质量。
支持管理者快速生成标准化模板,便于团队协作,规范代码设计,同时缩短项目交付周期。
将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。
把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。
在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。
半价获取高级提示词-优惠即将到期