תארו לעצמכם: הקמתם את Vendure, אבל אתם צריכים תוכנית נאמנות. אין כלים סטנדרטיים זמינים, התיעוד דל, ופתרונות מוכנים אינם גמישים. פיתוח תוסף מותאם אישית הוא הדרך היחידה, אבל היא מלאה במלכודות: טיפול לא נכון באירועים הורס את שלמות הנתונים, שגיאות בסכמת GraphQL שוברות את הפרונטאנד. עברנו את זה יותר מ-40 פעמים. לפי התיעוד הרשמי של Vendure, תוספים הם הדרך היחידה להרחיב ללא שינוי הליבה.
אנחנו מפתחים תוספי Vendure מותאמים אישית מקצה לקצה: מניתוח ועד פריסה. בקשה טיפוסית של לקוח היא תוכנית נאמנות עם נקודות, אבל התיעוד של Vendure אינו מספק ופתרונות מוכנים לא מתאימים. אנחנו לוקחים את המחזור המלא: מנתחים דרישות, מתכננים ארכיטקטורה, כותבים קוד, מכסים בבדיקות ופורסים. כתוצאה מכך, אתם מקבלים תוסף יציב שעובד תחת עומס ולא נשבר כאשר Vendure מתעדכן. במשך יותר מ-5 שנים, יישמנו יותר מ-40 תוספים עבור Vendure—מהרחבות פשוטות ועד אינטגרציות ERP מורכבות. תהליך פיתוח התוספים המותאם אישית שלנו מבטיח איכות גבוהה ותחזוקה קלה.
בעיות שאנחנו פותרים
-
הרחבת סכמת GraphQL: הוספת שדות לסוגים קיימים (למשל,
loyaltyAccountל-Customer) מבלי לשבור תאימות לאחור. אנו משתמשים ב-extend typeוב-resolvers עםResolveField. זה מקצר את זמן האינטגרציה ב-30%. -
טיפול באירועים אסינכרוניים: הענקת נקודות לאחר השלמת הזמנה, שליחת הודעות. אנו נרשמים ל-
OrderPlacedEventדרך EventBus. שימוש זה ב-vendure eventbus מבטיח זרימת נתונים אמינה. - אינטגרציה עם מערכות חיצוניות: CRM, ERP, שערי תשלום. התוסף יכול להכיל לקוחות HTTP ותורי הודעות. לדוגמה, בפיתוח תוסף תוכנית נאמנות, צמצמנו את זמן אינטגרציית ה-CRM ב-40%. זה מדגים את יכולות ה-vendure integration.
- בדיקות: שימוש ב-
createTestEnvironmentשל Vendure לבדיקות מבודדות ללא mocks. הבדיקות רצות על SQLite בזיכרון. הכיסוי מגיע ל-95%. זה מדגים בדיקות תוספים חזקות של vendure plugin testing.
איך ליצור תוסף Vendure מותאם אישית לתוכנית נאמנות?
אחת הבעיות המרכזיות היא טיפול נכון באירועים. ב-Vendure, EventBus הסטנדרטי משתמש ב-NestJS EventEmitter, אבל צריך לקחת בחשבון טרנזקציונליות. אנו משתמשים ב-TransactionalConnection כדי להבטיח עקביות נתונים. חשוב גם להימנע משאילתות N+1 בעת הרחבת סכמת GraphQL—DataLoader עם בקשות אצווה פותר זאת. טכניקות ה-vendure graphql extension שאנו משתמשים בהן מנוסות ונבדקות.
איך לבדוק תוסף Vendure מותאם אישית?
לבדיקת התוסף, אנו משתמשים ב-createTestEnvironment של Vendure. הוא מריץ מופע Vendure מלא עם SQLite בזיכרון, ומאפשר בדיקות יחידה ו-e2e ללא תלות חיצונית. כיסוי הבדיקות מגיע ל-95%, כולל טיפול באירועים ושאילתות GraphQL. בדיקות תוספים יסודיות אלה מעניקות לכם ביטחון.
מבנה התוסף
כל תוסף הוא מודול NestJS עם ה-decorator @VendurePlugin. המבנה המומלץ:
src/plugins/loyalty/
├── loyalty.plugin.ts # Точка входа (NestJS Module)
├── loyalty.service.ts # Бизнес-логика
├── loyalty.resolver.ts # GraphQL резолверы
├── loyalty.entity.ts # TypeORM сущность
├── loyalty-ui/ # Admin UI расширение (опционально)
│ ├── loyalty.module.ts
│ └── components/
└── types.ts # GraphQL типы ה-Dekorator @VendurePlugin
// loyalty.plugin.ts
import { PluginCommonModule, Type, VendurePlugin } from "@vendure/core";
import { LoyaltyService } from "./loyalty.service";
import { LoyaltyResolver } from "./loyalty.resolver";
import { LoyaltyAccount } from "./loyalty.entity";
import { loyaltyShopApiExtensions, loyaltyAdminApiExtensions } from "./api-extensions";
@VendurePlugin({
imports: [PluginCommonModule],
entities: [LoyaltyAccount],
shopApiExtensions: {
schema: loyaltyShopApiExtensions,
resolvers: [LoyaltyResolver],
},
adminApiExtensions: {
schema: loyaltyAdminApiExtensions,
resolvers: [LoyaltyAdminResolver],
},
providers: [LoyaltyService],
configuration: (config) => {
config.orderOptions.orderItemPriceCalculationStrategy = new LoyaltyAwarePriceStrategy();
return config;
},
})
export class LoyaltyPlugin {} ישות TypeORM
// loyalty.entity.ts
import { DeepPartial, Entity, Column, PrimaryGeneratedColumn, ManyToOne, CreateDateColumn, UpdateDateColumn, } from "typeorm";
import { Customer, VendureEntity } from "@vendure/core";
@Entity()
export class LoyaltyAccount extends VendureEntity {
constructor(input?: DeepPartial<LoyaltyAccount>) {
super(input);
}
@ManyToOne(() => Customer, { onDelete: "CASCADE" })
customer: Customer;
@Column()
customerId: string;
@Column({ default: 0 })
points: number;
@Column({ type: "jsonb", nullable: true })
transactions: LoyaltyTransaction[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
interface LoyaltyTransaction {
type: "earn" | "spend";
points: number;
orderId?: string;
reason: string;
date: string;
} שירות עם EventBus
// loyalty.service.ts
import { Injectable } from "@nestjs/common";
import { EventBus, OrderPlacedEvent, RequestContext, TransactionalConnection } from "@vendure/core";
import { OnEvent } from "@nestjs/event-emitter";
import { LoyaltyAccount } from "./loyalty.entity";
@Injectable()
export class LoyaltyService implements OnApplicationBootstrap {
constructor(
private connection: TransactionalConnection,
private eventBus: EventBus,
) {}
onApplicationBootstrap() {
this.eventBus.ofType(OrderPlacedEvent).subscribe(async (event) => {
await this.awardPointsForOrder(event.ctx, event.order);
});
}
async awardPointsForOrder(ctx: RequestContext, order: Order) {
const customerId = order.customerId;
if (!customerId) return;
const pointsToAward = Math.floor(order.totalWithTax / 100);
await this.connection.withTransaction(ctx, async (em) => {
let account = await em.findOne(LoyaltyAccount, {
where: { customerId },
});
if (!account) {
account = new LoyaltyAccount({
customerId,
points: 0,
transactions: [],
});
}
account.points += pointsToAward;
account.transactions = [
...account.transactions,
{
type: "earn",
points: pointsToAward,
orderId: order.id,
reason: `Заказ #${order.code}`,
date: new Date().toISOString(),
},
];
await em.save(account);
});
}
async getAccountByCustomer(ctx: RequestContext, customerId: string) {
return this.connection
.getRepository(ctx, LoyaltyAccount)
.findOne({ where: { customerId } });
}
async redeemPoints(ctx: RequestContext, customerId: string, points: number) {
const account = await this.getAccountByCustomer(ctx, customerId);
if (!account || account.points < points) {
throw new UserInputError("Недостаточно баллов");
}
account.points -= points;
account.transactions.push({
type: "spend",
points,
reason: "Списание при заказе",
date: new Date().toISOString(),
});
return this.connection.getRepository(ctx, LoyaltyAccount).save(account);
}
} GraphQL Resolver
// loyalty.resolver.ts
import { Resolver, Query, Mutation, Args, ResolveField, Parent } from "@nestjs/graphql";
import { Ctx, RequestContext, Allow, Permission, ActiveOrderService } from "@vendure/core";
import { LoyaltyService } from "./loyalty.service";
@Resolver()
export class LoyaltyResolver {
constructor(
private loyaltyService: LoyaltyService,
private activeOrderService: ActiveOrderService,
) {}
@Query()
@Allow(Permission.Owner)
async myLoyaltyAccount(@Ctx() ctx: RequestContext) {
if (!ctx.activeUserId) return null;
return this.loyaltyService.getAccountByCustomer(
ctx,
ctx.activeUserId.toString()
);
}
@Mutation()
@Allow(Permission.Owner)
async redeemLoyaltyPoints(
@Ctx() ctx: RequestContext,
@Args("points") points: number,
) {
const order = await this.activeOrderService.getActiveOrder(ctx, undefined);
if (!order) throw new Error("No active order");
await this.loyaltyService.redeemPoints(ctx, ctx.activeUserId!.toString(), points);
return order;
}
} למה תוסף מותאם אישית עדיף על שינוי הליבה?
| קריטריון | תוסף מותאם אישית | שינוי הליבה |
|---|---|---|
| עדכון Vendure | מתעדכן באופן עצמאי | דורש מיזוג שינויים |
| שימוש חוזר | מועבר בקלות לפרויקטים אחרים | קשור לפרויקט |
| בדיקות | בדיקות מבודדות | דורש הגדרת סביבה מלאה |
| תמיכה | API מתועד | אין ערבויות תאימות |
שלבי הפיתוח: מהקונספט להשקה
| שלב | משך | תוצאה |
|---|---|---|
| ניתוח דרישות | 2-3 ימים | מפרט טכני, אב טיפוס |
| עיצוב ארכיטקטורה | 1-2 ימים | דיאגרמת ER, סכמת GraphQL |
| יישום הליבה | 5-7 ימים | קוד מוכן עם הערות |
| ממשק ניהול (אופציונלי) | 2-3 ימים | הרחבת פאנל ניהול |
| בדיקות | 2-3 ימים | יחידה + e2e, דוח כיסוי |
| תיעוד ופריסה | יום אחד | הוראות, מיגרציות, פריסה |
מה כלול
- קוד מקור של התוסף עם הערות באנגלית
- תיעוד מלא: התקנה, הגדרה, אינטגרציה
- הוראות מיגרציה למסד נתונים. שלבי מיגרציה נכונים של vendure plugin migration מסופקים.
- כיסוי בדיקות (יחידה + e2e) באמצעות
src/plugins/loyalty/ ├── loyalty.plugin.ts # Точка входа (NestJS Module) ├── loyalty.service.ts # Бизнес-логика ├── loyalty.resolver.ts # GraphQL резолверы ├── loyalty.entity.ts # TypeORM сущность ├── loyalty-ui/ # Admin UI расширение (опционально) │ ├── loyalty.module.ts │ └── components/ └── types.ts # GraphQL типы - שבועיים של תמיכה לאחר המסירה (תיקוני באגים, ייעוץ)
טעויות פיתוח נפוצות:
- אי הרשמה לאירועים ב-
// loyalty.plugin.ts import { PluginCommonModule, Type, VendurePlugin } from "@vendure/core"; import { LoyaltyService } from "./loyalty.service"; import { LoyaltyResolver } from "./loyalty.resolver"; import { LoyaltyAccount } from "./loyalty.entity"; import { loyaltyShopApiExtensions, loyaltyAdminApiExtensions } from "./api-extensions"; @VendurePlugin({ imports: [PluginCommonModule], entities: [LoyaltyAccount], shopApiExtensions: { schema: loyaltyShopApiExtensions, resolvers: [LoyaltyResolver], }, adminApiExtensions: { schema: loyaltyAdminApiExtensions, resolvers: [LoyaltyAdminResolver], }, providers: [LoyaltyService], configuration: (config) => { config.orderOptions.orderItemPriceCalculationStrategy = new LoyaltyAwarePriceStrategy(); return config; }, }) export class LoyaltyPlugin {}—EventBus לא מופעל. - שימוש בשאילתות TypeORM ישירות במקום ב-
// loyalty.entity.ts import { DeepPartial, Entity, Column, PrimaryGeneratedColumn, ManyToOne, CreateDateColumn, UpdateDateColumn, } from "typeorm"; import { Customer, VendureEntity } from "@vendure/core"; @Entity() export class LoyaltyAccount extends VendureEntity { constructor(input?: DeepPartial<LoyaltyAccount>) { super(input); } @ManyToOne(() => Customer, { onDelete: "CASCADE" }) customer: Customer; @Column() customerId: string; @Column({ default: 0 }) points: number; @Column({ type: "jsonb", nullable: true }) transactions: LoyaltyTransaction[]; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; } interface LoyaltyTransaction { type: "earn" | "spend"; points: number; orderId?: string; reason: string; date: string; }—אובדן שלמות נתונים. - אי ציון ישויות ב-
// loyalty.service.ts import { Injectable } from "@nestjs/common"; import { EventBus, OrderPlacedEvent, RequestContext, TransactionalConnection } from "@vendure/core"; import { OnEvent } from "@nestjs/event-emitter"; import { LoyaltyAccount } from "./loyalty.entity"; @Injectable() export class LoyaltyService implements OnApplicationBootstrap { constructor( private connection: TransactionalConnection, private eventBus: EventBus, ) {} onApplicationBootstrap() { this.eventBus.ofType(OrderPlacedEvent).subscribe(async (event) => { await this.awardPointsForOrder(event.ctx, event.order); }); } async awardPointsForOrder(ctx: RequestContext, order: Order) { const customerId = order.customerId; if (!customerId) return; const pointsToAward = Math.floor(order.totalWithTax / 100); await this.connection.withTransaction(ctx, async (em) => { let account = await em.findOne(LoyaltyAccount, { where: { customerId }, }); if (!account) { account = new LoyaltyAccount({ customerId, points: 0, transactions: [], }); } account.points += pointsToAward; account.transactions = [ ...account.transactions, { type: "earn", points: pointsToAward, orderId: order.id, reason: `Заказ #${order.code}`, date: new Date().toISOString(), }, ]; await em.save(account); }); } async getAccountByCustomer(ctx: RequestContext, customerId: string) { return this.connection .getRepository(ctx, LoyaltyAccount) .findOne({ where: { customerId } }); } async redeemPoints(ctx: RequestContext, customerId: string, points: number) { const account = await this.getAccountByCustomer(ctx, customerId); if (!account || account.points < points) { throw new UserInputError("Недостаточно баллов"); } account.points -= points; account.transactions.push({ type: "spend", points, reason: "Списание при заказе", date: new Date().toISOString(), }); return this.connection.getRepository(ctx, LoyaltyAccount).save(account); } }—טבלאות לא נוצרות. - בלבול בין Shop API ל-Admin API בעת הרחבת הסכמה—resolvers לא עובדים.
לוחות זמנים ועלות
לוחות זמנים לפיתוח תוספים מותאמים אישית: משבועיים (הרחבות פשוטות) ועד 6 שבועות (אינטגרציות מורכבות). העלות מחושבת באופן אישי לאחר ניתוח דרישות. לדוגמה, תוסף נאמנות בסיסי עשוי להתחיל מ-$3,000, בעוד אינטגרציית CRM מלאה יכולה לעלות $15,000. אנו מציעים מחיר קבוע ושלבי תשלום שקופים. שירותי ה-vendure plugin customization שלנו הם חסכוניים וחוסכים לכם עד 40% בהשוואה לפיתוח פנימי.
סמכו על הניסיון שלנו
אנחנו צוות של מפתחי Vendure מוסמכים עם 5 שנות ניסיון. סיפקנו בהצלחה יותר מ-40 תוספים. אנו מבטיחים יציבות ותמיכה בזמן. התיעוד הרשמי של Vendure מאשר שתוספים הם הדרך הנכונה היחידה להרחבה. מסגרת NestJS מבטיחה מודולריות ויכולת בדיקה. השימוש שלנו בתבניות vendure typeorm entity ו-vendure nestjs module עוקב אחרי שיטות עבודה מומלצות.
צרו קשר כדי לדון בפרויקט שלכם. קבלו ייעוץ על פיתוח תוספי Vendure והרחבת מסחר אלקטרוני.







