@robsun/create-keystone-app 0.2.12 → 0.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/create-keystone-app.js +0 -0
- package/dist/create-module.js +584 -2
- package/package.json +22 -23
- package/template/.claude/skills/keystone-dev/SKILL.md +90 -103
- package/template/.claude/skills/keystone-dev/references/ADVANCED_PATTERNS.md +716 -0
- package/template/.claude/skills/keystone-dev/references/APPROVAL.md +47 -0
- package/template/.claude/skills/keystone-dev/references/CAPABILITIES.md +60 -5
- package/template/.claude/skills/keystone-dev/references/CHECKLIST.md +285 -0
- package/template/.claude/skills/keystone-dev/references/GOTCHAS.md +390 -0
- package/template/.claude/skills/keystone-dev/references/PATTERNS.md +605 -0
- package/template/.claude/skills/keystone-dev/references/TEMPLATES.md +2562 -384
- package/template/.codex/skills/keystone-dev/SKILL.md +90 -103
- package/template/.codex/skills/keystone-dev/references/ADVANCED_PATTERNS.md +716 -0
- package/template/.codex/skills/keystone-dev/references/APPROVAL.md +47 -0
- package/template/.codex/skills/keystone-dev/references/CAPABILITIES.md +60 -5
- package/template/.codex/skills/keystone-dev/references/CHECKLIST.md +285 -0
- package/template/.codex/skills/keystone-dev/references/GOTCHAS.md +390 -0
- package/template/.codex/skills/keystone-dev/references/PATTERNS.md +605 -0
- package/template/.codex/skills/keystone-dev/references/TEMPLATES.md +2562 -384
- package/template/README.md +8 -1
- package/template/apps/server/go.mod +97 -97
- package/template/apps/server/go.sum +283 -283
- package/template/docs/CONVENTIONS.md +11 -8
- package/template/package.json +3 -3
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
# Keystone 高级开发模式
|
|
2
|
+
|
|
3
|
+
> 本文档补充复杂业务场景的设计模式和最佳实践。
|
|
4
|
+
|
|
5
|
+
## 模块间依赖管理
|
|
6
|
+
|
|
7
|
+
### 1. 跨模块服务调用
|
|
8
|
+
|
|
9
|
+
当模块 A 需要调用模块 B 的服务时,通过依赖注入避免循环依赖。
|
|
10
|
+
|
|
11
|
+
```go
|
|
12
|
+
// apps/server/internal/app/container.go
|
|
13
|
+
type Container struct {
|
|
14
|
+
OrderService *order.OrderService
|
|
15
|
+
ProductService *product.ProductService
|
|
16
|
+
// ...
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func NewContainer(db *gorm.DB) *Container {
|
|
20
|
+
// 先创建基础服务
|
|
21
|
+
productRepo := product.NewProductRepository(db)
|
|
22
|
+
productSvc := product.NewProductService(productRepo)
|
|
23
|
+
|
|
24
|
+
// 再创建依赖其他模块的服务
|
|
25
|
+
orderRepo := order.NewOrderRepository(db)
|
|
26
|
+
orderSvc := order.NewOrderService(orderRepo, productSvc) // 注入 productSvc
|
|
27
|
+
|
|
28
|
+
return &Container{
|
|
29
|
+
OrderService: orderSvc,
|
|
30
|
+
ProductService: productSvc,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. 接口隔离原则
|
|
36
|
+
|
|
37
|
+
在 Service 中只依赖需要的接口,而非整个服务:
|
|
38
|
+
|
|
39
|
+
```go
|
|
40
|
+
// order/domain/service/service.go
|
|
41
|
+
package service
|
|
42
|
+
|
|
43
|
+
// 只定义需要的方法接口
|
|
44
|
+
type ProductPricer interface {
|
|
45
|
+
GetPrice(ctx context.Context, productID uint) (float64, error)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type OrderService struct {
|
|
49
|
+
repo OrderRepository
|
|
50
|
+
pricer ProductPricer // 只依赖接口
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func NewOrderService(repo OrderRepository, pricer ProductPricer) *OrderService {
|
|
54
|
+
return &OrderService{repo: repo, pricer: pricer}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. 事件驱动解耦
|
|
59
|
+
|
|
60
|
+
对于松耦合场景,使用事件总线:
|
|
61
|
+
|
|
62
|
+
```go
|
|
63
|
+
// infra/events/bus.go
|
|
64
|
+
type Event interface {
|
|
65
|
+
Name() string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type Handler func(ctx context.Context, event Event) error
|
|
69
|
+
|
|
70
|
+
type Bus struct {
|
|
71
|
+
handlers map[string][]Handler
|
|
72
|
+
mu sync.RWMutex
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func (b *Bus) Subscribe(eventName string, handler Handler) {
|
|
76
|
+
b.mu.Lock()
|
|
77
|
+
defer b.mu.Unlock()
|
|
78
|
+
b.handlers[eventName] = append(b.handlers[eventName], handler)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func (b *Bus) Publish(ctx context.Context, event Event) error {
|
|
82
|
+
b.mu.RLock()
|
|
83
|
+
handlers := b.handlers[event.Name()]
|
|
84
|
+
b.mu.RUnlock()
|
|
85
|
+
|
|
86
|
+
for _, h := range handlers {
|
|
87
|
+
if err := h(ctx, event); err != nil {
|
|
88
|
+
slog.Error("事件处理失败", "event", event.Name(), "error", err)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return nil
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 使用示例
|
|
95
|
+
type OrderCreatedEvent struct {
|
|
96
|
+
OrderID uint
|
|
97
|
+
TenantID uint
|
|
98
|
+
Amount float64
|
|
99
|
+
CreatedAt time.Time
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func (e OrderCreatedEvent) Name() string { return "order.created" }
|
|
103
|
+
|
|
104
|
+
// 在 Order 模块发布事件
|
|
105
|
+
func (s *OrderService) Create(ctx context.Context, ...) (*Order, error) {
|
|
106
|
+
// ... 创建订单逻辑
|
|
107
|
+
|
|
108
|
+
s.bus.Publish(ctx, OrderCreatedEvent{
|
|
109
|
+
OrderID: order.ID,
|
|
110
|
+
TenantID: order.TenantID,
|
|
111
|
+
Amount: order.TotalAmount,
|
|
112
|
+
CreatedAt: order.CreatedAt,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
return order, nil
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 在 Statistics 模块订阅事件
|
|
119
|
+
func (m *StatisticsModule) RegisterEventHandlers(bus *events.Bus) {
|
|
120
|
+
bus.Subscribe("order.created", func(ctx context.Context, e events.Event) error {
|
|
121
|
+
event := e.(OrderCreatedEvent)
|
|
122
|
+
return m.svc.UpdateDailyStats(ctx, event.TenantID, event.Amount)
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 状态机模式
|
|
130
|
+
|
|
131
|
+
适用于有复杂状态流转的业务实体(订单、审批、工单等)。
|
|
132
|
+
|
|
133
|
+
### 1. 状态机定义
|
|
134
|
+
|
|
135
|
+
```go
|
|
136
|
+
// domain/models/states.go
|
|
137
|
+
package models
|
|
138
|
+
|
|
139
|
+
type OrderStatus string
|
|
140
|
+
|
|
141
|
+
const (
|
|
142
|
+
OrderStatusDraft OrderStatus = "draft"
|
|
143
|
+
OrderStatusSubmitted OrderStatus = "submitted"
|
|
144
|
+
OrderStatusApproved OrderStatus = "approved"
|
|
145
|
+
OrderStatusRejected OrderStatus = "rejected"
|
|
146
|
+
OrderStatusPaid OrderStatus = "paid"
|
|
147
|
+
OrderStatusShipped OrderStatus = "shipped"
|
|
148
|
+
OrderStatusCompleted OrderStatus = "completed"
|
|
149
|
+
OrderStatusCancelled OrderStatus = "cancelled"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
// 状态转换规则
|
|
153
|
+
var orderTransitions = map[OrderStatus][]OrderStatus{
|
|
154
|
+
OrderStatusDraft: {OrderStatusSubmitted, OrderStatusCancelled},
|
|
155
|
+
OrderStatusSubmitted: {OrderStatusApproved, OrderStatusRejected},
|
|
156
|
+
OrderStatusApproved: {OrderStatusPaid, OrderStatusCancelled},
|
|
157
|
+
OrderStatusRejected: {OrderStatusDraft},
|
|
158
|
+
OrderStatusPaid: {OrderStatusShipped},
|
|
159
|
+
OrderStatusShipped: {OrderStatusCompleted},
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
func (s OrderStatus) CanTransitionTo(target OrderStatus) bool {
|
|
163
|
+
allowed, exists := orderTransitions[s]
|
|
164
|
+
if !exists {
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
for _, a := range allowed {
|
|
168
|
+
if a == target {
|
|
169
|
+
return true
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return false
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
func (o *Order) TransitionTo(newStatus OrderStatus) error {
|
|
176
|
+
if !o.Status.CanTransitionTo(newStatus) {
|
|
177
|
+
return fmt.Errorf("无法从 %s 转换到 %s", o.Status, newStatus)
|
|
178
|
+
}
|
|
179
|
+
o.Status = newStatus
|
|
180
|
+
return nil
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### 2. 状态操作服务
|
|
185
|
+
|
|
186
|
+
```go
|
|
187
|
+
// domain/service/transitions.go
|
|
188
|
+
package service
|
|
189
|
+
|
|
190
|
+
func (s *OrderService) Submit(ctx context.Context, tenantID, id, userID uint) error {
|
|
191
|
+
return s.transition(ctx, tenantID, id, userID, models.OrderStatusSubmitted, nil)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
func (s *OrderService) Approve(ctx context.Context, tenantID, id, userID uint) error {
|
|
195
|
+
return s.transition(ctx, tenantID, id, userID, models.OrderStatusApproved, nil)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
func (s *OrderService) Reject(ctx context.Context, tenantID, id, userID uint, reason string) error {
|
|
199
|
+
return s.transition(ctx, tenantID, id, userID, models.OrderStatusRejected, func(o *models.Order) {
|
|
200
|
+
o.RejectReason = reason
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
func (s *OrderService) transition(
|
|
205
|
+
ctx context.Context,
|
|
206
|
+
tenantID, id, userID uint,
|
|
207
|
+
newStatus models.OrderStatus,
|
|
208
|
+
beforeSave func(*models.Order),
|
|
209
|
+
) error {
|
|
210
|
+
order, err := s.repo.FindByID(tenantID, id)
|
|
211
|
+
if err != nil {
|
|
212
|
+
return err
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
oldStatus := order.Status
|
|
216
|
+
if err := order.TransitionTo(newStatus); err != nil {
|
|
217
|
+
return err
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if beforeSave != nil {
|
|
221
|
+
beforeSave(order)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 记录状态变更日志
|
|
225
|
+
log := &models.OrderStatusLog{
|
|
226
|
+
OrderID: order.ID,
|
|
227
|
+
FromStatus: oldStatus,
|
|
228
|
+
ToStatus: newStatus,
|
|
229
|
+
OperatorID: userID,
|
|
230
|
+
CreatedAt: time.Now(),
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return s.db.Transaction(func(tx *gorm.DB) error {
|
|
234
|
+
if err := tx.Save(order).Error; err != nil {
|
|
235
|
+
return err
|
|
236
|
+
}
|
|
237
|
+
return tx.Create(log).Error
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 多租户高级模式
|
|
245
|
+
|
|
246
|
+
### 1. 自动租户过滤中间件
|
|
247
|
+
|
|
248
|
+
```go
|
|
249
|
+
// infra/middleware/tenant.go
|
|
250
|
+
package middleware
|
|
251
|
+
|
|
252
|
+
func TenantScope(db *gorm.DB) gin.HandlerFunc {
|
|
253
|
+
return func(c *gin.Context) {
|
|
254
|
+
tenantID, ok := hcommon.GetTenantID(c)
|
|
255
|
+
if !ok {
|
|
256
|
+
response.Unauthorized(c, "租户信息缺失")
|
|
257
|
+
c.Abort()
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 创建带租户作用域的 DB 实例
|
|
262
|
+
scopedDB := db.Scopes(func(d *gorm.DB) *gorm.DB {
|
|
263
|
+
return d.Where("tenant_id = ?", tenantID)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
// 存入 context
|
|
267
|
+
c.Set("scopedDB", scopedDB)
|
|
268
|
+
c.Next()
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 在 handler 中使用
|
|
273
|
+
func GetScopedDB(c *gin.Context) *gorm.DB {
|
|
274
|
+
if db, ok := c.Get("scopedDB"); ok {
|
|
275
|
+
return db.(*gorm.DB)
|
|
276
|
+
}
|
|
277
|
+
return nil
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### 2. 租户数据隔离验证
|
|
282
|
+
|
|
283
|
+
```go
|
|
284
|
+
// 在 Repository 中添加验证
|
|
285
|
+
func (r *Repository) FindByID(tenantID, id uint) (*models.Entity, error) {
|
|
286
|
+
var entity models.Entity
|
|
287
|
+
err := r.db.Where("id = ?", id).First(&entity).Error
|
|
288
|
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
289
|
+
return nil, service.ErrNotFound
|
|
290
|
+
}
|
|
291
|
+
if err != nil {
|
|
292
|
+
return nil, err
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 验证租户归属
|
|
296
|
+
if entity.TenantID != tenantID {
|
|
297
|
+
slog.Warn("跨租户访问尝试",
|
|
298
|
+
"requestedTenant", tenantID,
|
|
299
|
+
"actualTenant", entity.TenantID,
|
|
300
|
+
"entityID", id,
|
|
301
|
+
)
|
|
302
|
+
return nil, service.ErrNotFound // 不暴露真实错误
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return &entity, nil
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## 缓存模式
|
|
312
|
+
|
|
313
|
+
### 1. 读穿透缓存
|
|
314
|
+
|
|
315
|
+
```go
|
|
316
|
+
// infra/cache/cache.go
|
|
317
|
+
package cache
|
|
318
|
+
|
|
319
|
+
import (
|
|
320
|
+
"context"
|
|
321
|
+
"encoding/json"
|
|
322
|
+
"time"
|
|
323
|
+
|
|
324
|
+
"github.com/redis/go-redis/v9"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
type Cache struct {
|
|
328
|
+
client *redis.Client
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
func (c *Cache) GetOrLoad[T any](
|
|
332
|
+
ctx context.Context,
|
|
333
|
+
key string,
|
|
334
|
+
ttl time.Duration,
|
|
335
|
+
loader func() (T, error),
|
|
336
|
+
) (T, error) {
|
|
337
|
+
var result T
|
|
338
|
+
|
|
339
|
+
// 尝试从缓存读取
|
|
340
|
+
data, err := c.client.Get(ctx, key).Bytes()
|
|
341
|
+
if err == nil {
|
|
342
|
+
if err := json.Unmarshal(data, &result); err == nil {
|
|
343
|
+
return result, nil
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 缓存未命中,加载数据
|
|
348
|
+
result, err = loader()
|
|
349
|
+
if err != nil {
|
|
350
|
+
return result, err
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 写入缓存
|
|
354
|
+
data, _ = json.Marshal(result)
|
|
355
|
+
c.client.Set(ctx, key, data, ttl)
|
|
356
|
+
|
|
357
|
+
return result, nil
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 使用示例
|
|
361
|
+
func (s *ProductService) GetByID(ctx context.Context, tenantID, id uint) (*models.Product, error) {
|
|
362
|
+
key := fmt.Sprintf("product:%d:%d", tenantID, id)
|
|
363
|
+
|
|
364
|
+
return s.cache.GetOrLoad(ctx, key, 5*time.Minute, func() (*models.Product, error) {
|
|
365
|
+
return s.repo.FindByID(tenantID, id)
|
|
366
|
+
})
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### 2. 缓存失效策略
|
|
371
|
+
|
|
372
|
+
```go
|
|
373
|
+
// 更新时主动失效
|
|
374
|
+
func (s *ProductService) Update(ctx context.Context, tenantID, id uint, input UpdateInput) (*models.Product, error) {
|
|
375
|
+
product, err := s.repo.Update(ctx, tenantID, id, input)
|
|
376
|
+
if err != nil {
|
|
377
|
+
return nil, err
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// 删除缓存
|
|
381
|
+
key := fmt.Sprintf("product:%d:%d", tenantID, id)
|
|
382
|
+
s.cache.Delete(ctx, key)
|
|
383
|
+
|
|
384
|
+
// 发布缓存失效事件(用于分布式场景)
|
|
385
|
+
s.bus.Publish(ctx, CacheInvalidatedEvent{Key: key})
|
|
386
|
+
|
|
387
|
+
return product, nil
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## 并发控制模式
|
|
394
|
+
|
|
395
|
+
### 1. 分布式锁
|
|
396
|
+
|
|
397
|
+
```go
|
|
398
|
+
// infra/lock/redis.go
|
|
399
|
+
package lock
|
|
400
|
+
|
|
401
|
+
import (
|
|
402
|
+
"context"
|
|
403
|
+
"time"
|
|
404
|
+
|
|
405
|
+
"github.com/redis/go-redis/v9"
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
type RedisLock struct {
|
|
409
|
+
client *redis.Client
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
func (l *RedisLock) TryLock(ctx context.Context, key string, ttl time.Duration) (bool, error) {
|
|
413
|
+
return l.client.SetNX(ctx, "lock:"+key, "1", ttl).Result()
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
func (l *RedisLock) Unlock(ctx context.Context, key string) error {
|
|
417
|
+
return l.client.Del(ctx, "lock:"+key).Err()
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 使用示例:防止重复提交
|
|
421
|
+
func (s *OrderService) Submit(ctx context.Context, tenantID, id, userID uint) error {
|
|
422
|
+
lockKey := fmt.Sprintf("order:submit:%d:%d", tenantID, id)
|
|
423
|
+
|
|
424
|
+
acquired, err := s.lock.TryLock(ctx, lockKey, 30*time.Second)
|
|
425
|
+
if err != nil {
|
|
426
|
+
return err
|
|
427
|
+
}
|
|
428
|
+
if !acquired {
|
|
429
|
+
return ErrOperationInProgress
|
|
430
|
+
}
|
|
431
|
+
defer s.lock.Unlock(ctx, lockKey)
|
|
432
|
+
|
|
433
|
+
// 执行提交逻辑
|
|
434
|
+
return s.doSubmit(ctx, tenantID, id, userID)
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### 2. 幂等性处理
|
|
439
|
+
|
|
440
|
+
```go
|
|
441
|
+
// infra/idempotency/store.go
|
|
442
|
+
package idempotency
|
|
443
|
+
|
|
444
|
+
type Store interface {
|
|
445
|
+
Check(ctx context.Context, key string) (exists bool, result interface{}, err error)
|
|
446
|
+
Save(ctx context.Context, key string, result interface{}, ttl time.Duration) error
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// 使用示例
|
|
450
|
+
func (h *Handler) Create(c *gin.Context) {
|
|
451
|
+
// 从请求头获取幂等键
|
|
452
|
+
idempotencyKey := c.GetHeader("X-Idempotency-Key")
|
|
453
|
+
if idempotencyKey == "" {
|
|
454
|
+
response.BadRequest(c, "缺少幂等键")
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// 检查是否已处理
|
|
459
|
+
key := fmt.Sprintf("idempotency:%s:%s", tenantID, idempotencyKey)
|
|
460
|
+
exists, result, err := h.idempotency.Check(c.Request.Context(), key)
|
|
461
|
+
if err != nil {
|
|
462
|
+
response.InternalError(c, err.Error())
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
if exists {
|
|
466
|
+
response.Success(c, result) // 返回之前的结果
|
|
467
|
+
return
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// 处理请求
|
|
471
|
+
entity, err := h.svc.Create(c.Request.Context(), tenantID, input)
|
|
472
|
+
if err != nil {
|
|
473
|
+
response.Error(c, err)
|
|
474
|
+
return
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// 保存结果
|
|
478
|
+
h.idempotency.Save(c.Request.Context(), key, entity, 24*time.Hour)
|
|
479
|
+
response.Created(c, entity)
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
485
|
+
## 异步任务编排
|
|
486
|
+
|
|
487
|
+
### 1. 任务链模式
|
|
488
|
+
|
|
489
|
+
```go
|
|
490
|
+
// domain/jobs/chain.go
|
|
491
|
+
package jobs
|
|
492
|
+
|
|
493
|
+
type ChainJob struct {
|
|
494
|
+
steps []Step
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
type Step struct {
|
|
498
|
+
Name string
|
|
499
|
+
Execute func(ctx context.Context, params jobs.Params, prevResult interface{}) (interface{}, error)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
func (j *ChainJob) Execute(ctx context.Context, params jobs.Params, progress jobs.ProgressReporter) error {
|
|
503
|
+
var prevResult interface{}
|
|
504
|
+
totalSteps := len(j.steps)
|
|
505
|
+
|
|
506
|
+
for i, step := range j.steps {
|
|
507
|
+
progress.Update(
|
|
508
|
+
int(float64(i)/float64(totalSteps)*100),
|
|
509
|
+
fmt.Sprintf("执行步骤: %s", step.Name),
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
result, err := step.Execute(ctx, params, prevResult)
|
|
513
|
+
if err != nil {
|
|
514
|
+
return fmt.Errorf("步骤 %s 失败: %w", step.Name, err)
|
|
515
|
+
}
|
|
516
|
+
prevResult = result
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
progress.Update(100, "任务完成")
|
|
520
|
+
progress.SetResult(prevResult)
|
|
521
|
+
return nil
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// 使用示例:订单导出任务链
|
|
525
|
+
func NewOrderExportChain(repo *OrderRepository, storage storage.Service) *ChainJob {
|
|
526
|
+
return &ChainJob{
|
|
527
|
+
steps: []Step{
|
|
528
|
+
{
|
|
529
|
+
Name: "查询数据",
|
|
530
|
+
Execute: func(ctx context.Context, params jobs.Params, _ interface{}) (interface{}, error) {
|
|
531
|
+
tenantID := params.GetUint("tenant_id")
|
|
532
|
+
return repo.ListAll(ctx, tenantID)
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
Name: "生成 Excel",
|
|
537
|
+
Execute: func(ctx context.Context, params jobs.Params, prev interface{}) (interface{}, error) {
|
|
538
|
+
orders := prev.([]models.Order)
|
|
539
|
+
return generateExcel(orders)
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
Name: "上传文件",
|
|
544
|
+
Execute: func(ctx context.Context, params jobs.Params, prev interface{}) (interface{}, error) {
|
|
545
|
+
data := prev.([]byte)
|
|
546
|
+
return storage.Upload(ctx, "orders.xlsx", data)
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
## 前端状态管理模式
|
|
557
|
+
|
|
558
|
+
### 1. 模块级状态 Store
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
// stores/orderStore.ts
|
|
562
|
+
import { create } from 'zustand'
|
|
563
|
+
import { persist } from 'zustand/middleware'
|
|
564
|
+
import type { Order, OrderFilter } from '../types'
|
|
565
|
+
import { listOrders, getOrder, createOrder } from '../services/api'
|
|
566
|
+
|
|
567
|
+
interface OrderState {
|
|
568
|
+
// 数据
|
|
569
|
+
orders: Order[]
|
|
570
|
+
currentOrder: Order | null
|
|
571
|
+
total: number
|
|
572
|
+
|
|
573
|
+
// 加载状态
|
|
574
|
+
loading: boolean
|
|
575
|
+
error: string | null
|
|
576
|
+
|
|
577
|
+
// 过滤和分页
|
|
578
|
+
filter: OrderFilter
|
|
579
|
+
page: number
|
|
580
|
+
pageSize: number
|
|
581
|
+
|
|
582
|
+
// Actions
|
|
583
|
+
fetchOrders: () => Promise<void>
|
|
584
|
+
fetchOrder: (id: number) => Promise<void>
|
|
585
|
+
createOrder: (input: CreateOrderInput) => Promise<Order>
|
|
586
|
+
setFilter: (filter: Partial<OrderFilter>) => void
|
|
587
|
+
setPage: (page: number) => void
|
|
588
|
+
reset: () => void
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export const useOrderStore = create<OrderState>()(
|
|
592
|
+
persist(
|
|
593
|
+
(set, get) => ({
|
|
594
|
+
orders: [],
|
|
595
|
+
currentOrder: null,
|
|
596
|
+
total: 0,
|
|
597
|
+
loading: false,
|
|
598
|
+
error: null,
|
|
599
|
+
filter: {},
|
|
600
|
+
page: 1,
|
|
601
|
+
pageSize: 10,
|
|
602
|
+
|
|
603
|
+
fetchOrders: async () => {
|
|
604
|
+
const { filter, page, pageSize } = get()
|
|
605
|
+
set({ loading: true, error: null })
|
|
606
|
+
|
|
607
|
+
try {
|
|
608
|
+
const result = await listOrders({ ...filter, page, page_size: pageSize })
|
|
609
|
+
set({ orders: result.items, total: result.total })
|
|
610
|
+
} catch (err) {
|
|
611
|
+
set({ error: err instanceof Error ? err.message : '加载失败' })
|
|
612
|
+
} finally {
|
|
613
|
+
set({ loading: false })
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
|
|
617
|
+
fetchOrder: async (id: number) => {
|
|
618
|
+
set({ loading: true, error: null })
|
|
619
|
+
try {
|
|
620
|
+
const order = await getOrder(id)
|
|
621
|
+
set({ currentOrder: order })
|
|
622
|
+
} catch (err) {
|
|
623
|
+
set({ error: err instanceof Error ? err.message : '加载失败' })
|
|
624
|
+
} finally {
|
|
625
|
+
set({ loading: false })
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
|
|
629
|
+
createOrder: async (input) => {
|
|
630
|
+
set({ loading: true, error: null })
|
|
631
|
+
try {
|
|
632
|
+
const order = await createOrder(input)
|
|
633
|
+
// 刷新列表
|
|
634
|
+
get().fetchOrders()
|
|
635
|
+
return order
|
|
636
|
+
} finally {
|
|
637
|
+
set({ loading: false })
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
|
|
641
|
+
setFilter: (newFilter) => {
|
|
642
|
+
set((state) => ({
|
|
643
|
+
filter: { ...state.filter, ...newFilter },
|
|
644
|
+
page: 1, // 重置页码
|
|
645
|
+
}))
|
|
646
|
+
get().fetchOrders()
|
|
647
|
+
},
|
|
648
|
+
|
|
649
|
+
setPage: (page) => {
|
|
650
|
+
set({ page })
|
|
651
|
+
get().fetchOrders()
|
|
652
|
+
},
|
|
653
|
+
|
|
654
|
+
reset: () => {
|
|
655
|
+
set({
|
|
656
|
+
orders: [],
|
|
657
|
+
currentOrder: null,
|
|
658
|
+
total: 0,
|
|
659
|
+
filter: {},
|
|
660
|
+
page: 1,
|
|
661
|
+
error: null,
|
|
662
|
+
})
|
|
663
|
+
},
|
|
664
|
+
}),
|
|
665
|
+
{
|
|
666
|
+
name: 'order-store',
|
|
667
|
+
partialize: (state) => ({ filter: state.filter, pageSize: state.pageSize }),
|
|
668
|
+
}
|
|
669
|
+
)
|
|
670
|
+
)
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### 2. 在组件中使用
|
|
674
|
+
|
|
675
|
+
```tsx
|
|
676
|
+
// pages/OrderListPage.tsx
|
|
677
|
+
import { useEffect } from 'react'
|
|
678
|
+
import { useOrderStore } from '../stores/orderStore'
|
|
679
|
+
|
|
680
|
+
export function OrderListPage() {
|
|
681
|
+
const {
|
|
682
|
+
orders,
|
|
683
|
+
total,
|
|
684
|
+
loading,
|
|
685
|
+
error,
|
|
686
|
+
page,
|
|
687
|
+
pageSize,
|
|
688
|
+
filter,
|
|
689
|
+
fetchOrders,
|
|
690
|
+
setFilter,
|
|
691
|
+
setPage
|
|
692
|
+
} = useOrderStore()
|
|
693
|
+
|
|
694
|
+
useEffect(() => {
|
|
695
|
+
fetchOrders()
|
|
696
|
+
}, [fetchOrders])
|
|
697
|
+
|
|
698
|
+
// ... 渲染逻辑
|
|
699
|
+
}
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
## 快速参考
|
|
705
|
+
|
|
706
|
+
| 模式 | 使用场景 | 关键点 |
|
|
707
|
+
|------|---------|--------|
|
|
708
|
+
| 跨模块调用 | 模块间服务依赖 | 依赖注入 + 接口隔离 |
|
|
709
|
+
| 事件驱动 | 松耦合通知 | 事件总线 + 异步处理 |
|
|
710
|
+
| 状态机 | 复杂状态流转 | 转换规则 + 状态日志 |
|
|
711
|
+
| 多租户 | 数据隔离 | 中间件 + 双重验证 |
|
|
712
|
+
| 缓存 | 性能优化 | 读穿透 + 主动失效 |
|
|
713
|
+
| 分布式锁 | 并发控制 | Redis SETNX + TTL |
|
|
714
|
+
| 幂等性 | 重复请求 | 幂等键 + 结果缓存 |
|
|
715
|
+
| 任务链 | 复杂异步任务 | 步骤拆分 + 进度汇报 |
|
|
716
|
+
| 状态管理 | 前端复杂状态 | Zustand + 持久化 |
|