Skip to content

Commit 8d30ae5

Browse files
876908Hunger61yulian-artWhisper985springbot2025
authored
Feat/buyer order packaging fee (#100)
* fix(errand): GetDistributingTaskDetail 透传 purchased_quantity DistributingItem 已新增 purchased_quantity 字段(proto + SQL + Row), 但 service 组装处漏了透传,前端收到 nil 导致实购量显示 0、 未采购判断失效。补齐组装透传。 * 列表响应批量补 productTemplate;详情/创建响应用已有内部 catalog/user client 补 productTemplate 和 seller * 汇总时间截断 * fix errand payment totals and cancel rollback * fix(payment): prevent PayBill timestamp conflicts (#92) Co-authored-by: yulian-art <1119253058@qq.com> * fix lint * fix lint * fix:time * Spot 下单前直接拒绝“买家就是卖家”,避免先扣库存/写订单再跨服务失败;把支付端创建账单的参数校验拆成明确错误,避免所有非法参数都显示成 invalid bill status 和 unknown message * feat(errand): add packaging_fee_share_cents to buyer order detail - proto: BuyerErrandOrderProductItem 新增 packaging_fee_share_cents (field 10) - repo: 新增 CountDistinctPurchasers 统计任务下唯一付款人数 - service: BuyerOrderProductItem 加字段,calcPackagingFeeShare 按人头分摊 - handler: 映射到 proto * feat(errand): add packaging_fee_share_cents to buyer order detail (#97) - proto: BuyerErrandOrderProductItem 新增 packaging_fee_share_cents (field 10) - repo: 新增 CountDistinctPurchasers 统计任务下唯一付款人数 - service: BuyerOrderProductItem 加字段,calcPackagingFeeShare 按人头分摊 - handler: 映射到 proto * fix: add PackagingFeeShareCents field to BuyerOrderProductItem struct * fix: remove local replace directive committed by accident * fix: golines formatting in spotservice * fix: remove packaging fee from totalActualAmountCents calculation --------- Co-authored-by: Hunger <3287426755@qq.com> Co-authored-by: yulian-art <1119253058@qq.com> Co-authored-by: Whisper985 <243932443+Whisper985@users.noreply.github.com> Co-authored-by: Spring <2922562941@qq.com>
1 parent ecb0668 commit 8d30ae5

13 files changed

Lines changed: 309 additions & 27 deletions

File tree

internal/pkg/feishu/client.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ func Init() {
2626
cfg := config.AppConfig
2727
if cfg.Feishu_AppID == "" || cfg.Feishu_AppSecret == "" ||
2828
cfg.Feishu_AppID == constant.FeishuDefaultAppID || cfg.Feishu_AppSecret == constant.FeishuDefaultAppSecret {
29+
if cfg.AppEnv == config.Development {
30+
return
31+
}
2932
panic("feishu: FEISHU_APP_ID / FEISHU_APP_SECRET must be configured with real credentials")
3033
}
3134
AppClient = &Client{

internal/service/errandservice/go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ module github.com/NJUPT-SAST/sast-shop-v2/internal/services/errandservice
33
go 1.26.3
44

55
require (
6-
buf.build/gen/go/sast/sast-shop-v2/connectrpc/go v1.20.0-20260729122644-8226c83aee9b.1
7-
buf.build/gen/go/sast/sast-shop-v2/protocolbuffers/go v1.36.11-20260729122644-8226c83aee9b.1
6+
buf.build/gen/go/sast/sast-shop-v2/connectrpc/go v1.20.0-20260803055026-0b894163deec.1
7+
buf.build/gen/go/sast/sast-shop-v2/protocolbuffers/go v1.36.11-20260803055026-0b894163deec.1
88
connectrpc.com/connect v1.20.0
99
github.com/NJUPT-SAST/sast-shop-v2/internal/pkg v0.0.0-20260703081920-1b187050f3a3
1010
github.com/labstack/echo/v5 v5.1.1

internal/service/errandservice/go.sum

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260709200747-435963d16310.1 h1:fXh8CsdNpjRr8R5vFdqtIxPt/Lno2IIJlYOdZBIZn0w=
2-
buf.build/gen/go/sast/sast-shop-v2/connectrpc/go v1.20.0-20260729122644-8226c83aee9b.1 h1:K+gDa3l5rI8Nd3lom4PWtIjRLqLi7I4z1TpXqtnWWMo=
3-
buf.build/gen/go/sast/sast-shop-v2/protocolbuffers/go v1.36.11-20260729122644-8226c83aee9b.1 h1:GkVktkeYitIhLdYrbBn7dhryQMQ9M8ucCK+b+JU1e+A=
2+
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260709200747-435963d16310.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
3+
buf.build/gen/go/sast/sast-shop-v2/connectrpc/go v1.20.0-20260803055026-0b894163deec.1 h1:UxXjA2ZoiLmW5tqR7QL7awKN3YXmXSwD8DDtGdYFZO0=
4+
buf.build/gen/go/sast/sast-shop-v2/connectrpc/go v1.20.0-20260803055026-0b894163deec.1/go.mod h1:ofBdA7CEpZY4zd/lR5KVu6DPUu+S9LHLwSfnA8KLU/0=
5+
buf.build/gen/go/sast/sast-shop-v2/protocolbuffers/go v1.36.11-20260803055026-0b894163deec.1 h1:tVnCjKxG/0YzR6pUKA0clntFUgwXK8PLmrAcHWKtGnA=
6+
buf.build/gen/go/sast/sast-shop-v2/protocolbuffers/go v1.36.11-20260803055026-0b894163deec.1/go.mod h1:aFm9mnorhJFXvjjh7GbevEuSVdVtd/KD4bgCogHCH40=
47
buf.build/go/protovalidate v1.2.0 h1:DQVrUWkmGTBij+kOYv/x2LLxwcLaGKMdzShj1/6/3H0=
58
buf.build/go/protovalidate v1.2.0/go.mod h1:7rYiQEhqvAipoazpVNBBH2S2f8bjG4huMVy1V2Yofn4=
69
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=

internal/service/errandservice/internal/handler/v1/buyer_errand_order.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,9 @@ func (s *BuyerErrandOrderServiceServer) GetBuyerErrandOrderDetail(
125125
NonPurchaseReason: &pi.NonPurchaseReason,
126126
DistributedQuantity: pi.DistributedQuantity,
127127
ServiceFeePerUnitCents: pi.ServiceFeePerUnitCents,
128-
SubtotalCents: 0,
128+
SubtotalCents: pi.SubtotalCents,
129129
ErrandDemandItemId: pi.ErrandDemandItemID,
130+
PackagingFeeShareCents: &pi.PackagingFeeShareCents,
130131
})
131132
}
132133

internal/service/errandservice/internal/repository/errand_task_payment.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,3 +689,59 @@ func UpdateTaskRelatedDemandItemsToCancelled(ctx context.Context, db bun.IDB, ta
689689
Exec(ctx)
690690
return err
691691
}
692+
693+
func ReopenTaskRelatedDemands(ctx context.Context, db bun.IDB, taskID int64, now time.Time) error {
694+
_, err := db.NewUpdate().
695+
Model((*model.ErrandDemand)(nil)).
696+
Set("status = ?", model.ErrandDemandStatusOpen).
697+
Set("task_id = NULL").
698+
Set("shopping_start_at = NULL").
699+
Set("shopping_completed_at = NULL").
700+
Set("distribution_completed_at = NULL").
701+
Set("payment_completed_at = NULL").
702+
Set("cancelled_at = NULL").
703+
Set("updated_at = ?", now).
704+
Where(`id IN (
705+
SELECT DISTINCT edi.errand_demand_id
706+
FROM errand.errand_task_assignment AS eta
707+
JOIN errand.errand_demand_item AS edi ON edi.id = eta.demand_item_id
708+
WHERE eta.task_id = ?
709+
)`, taskID).
710+
Where("status NOT IN (?)", bun.List([]model.ErrandDemandStatus{
711+
model.ErrandDemandStatusCompleted,
712+
model.ErrandDemandStatusCancelled,
713+
})).
714+
Exec(ctx)
715+
return err
716+
}
717+
718+
func ReopenTaskRelatedDemandItems(ctx context.Context, db bun.IDB, taskID int64, now time.Time) error {
719+
_, err := db.NewUpdate().
720+
Model((*model.ErrandDemandItem)(nil)).
721+
Set("status = ?", model.ErrandDemandItemStatusOpen).
722+
Set("updated_at = ?", now).
723+
Where(`id IN (
724+
SELECT eta.demand_item_id
725+
FROM errand.errand_task_assignment AS eta
726+
WHERE eta.task_id = ?
727+
)`, taskID).
728+
Where("status NOT IN (?)", bun.List([]model.ErrandDemandItemStatus{
729+
model.ErrandDemandItemStatusCompleted,
730+
model.ErrandDemandItemStatusCancelled,
731+
})).
732+
Exec(ctx)
733+
return err
734+
}
735+
736+
func DeleteReopenedTaskAssignments(ctx context.Context, db bun.IDB, taskID int64) error {
737+
_, err := db.NewDelete().
738+
Model((*model.ErrandTaskAssignment)(nil)).
739+
Where("task_id = ?", taskID).
740+
Where(`demand_item_id IN (
741+
SELECT edi.id
742+
FROM errand.errand_demand_item AS edi
743+
WHERE edi.status = ?
744+
)`, model.ErrandDemandItemStatusOpen).
745+
Exec(ctx)
746+
return err
747+
}

internal/service/errandservice/internal/repository/task.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ func MarkDemandsCompletedIfAllItemsDoneByItemIDs(
9797
return res.RowsAffected()
9898
}
9999

100+
// CountDistinctPurchasers 统计任务下唯一付款人数量,用于包装费分摊。
101+
func CountDistinctPurchasers(ctx context.Context, db bun.IDB, taskID int64) (int, error) {
102+
return db.NewSelect().
103+
ColumnExpr("COUNT(DISTINCT purchaser_id)").
104+
TableExpr("errand.errand_task_assignment").
105+
Where("task_id = ?", taskID).
106+
Count(ctx)
107+
}
108+
100109
// MarkTaskCompleted 完成团长任务的状态流转
101110
func MarkTaskCompleted(ctx context.Context, db bun.IDB, taskID int64, now time.Time) (int64, error) {
102111
res, err := db.NewUpdate().

internal/service/errandservice/internal/service/buyer_order.go

Lines changed: 110 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ type BuyerOrderProductItem struct {
3434
NonPurchaseReason string
3535
DistributedQuantity *int32
3636
ServiceFeePerUnitCents int32
37+
SubtotalCents int32
3738
EstimatedUnitPriceCents int32
3839
ErrandDemandItemID int64
40+
PackagingFeeShareCents int32
3941
}
4042

4143
type BuyerOrderDetail struct {
@@ -204,7 +206,23 @@ func GetBuyerOrderDetail(ctx context.Context, requesterID, demandID int64) (*Buy
204206
captainInfo := loadCaptain(ctx, task)
205207
billID := findBillID(assignByItem)
206208

207-
productItems, originCents, serviceCents := buildProductItems(items, assignByItem, taskItemByProduct, productMap)
209+
packagingShareCents, err := loadBuyerPackagingShareCents(ctx, task, requesterID)
210+
if err != nil {
211+
log.Error().Err(err).Int64("demand_id", demandID).Msg("calculate buyer packaging share failed")
212+
return nil, ErrInternal
213+
}
214+
215+
productItems, originCents, actualCents, serviceCents, err := buildProductItems(
216+
items,
217+
assignByItem,
218+
taskItemByProduct,
219+
productMap,
220+
packagingShareCents,
221+
)
222+
if err != nil {
223+
log.Error().Err(err).Int64("demand_id", demandID).Msg("build buyer order product items failed")
224+
return nil, ErrInternal
225+
}
208226

209227
return &BuyerOrderDetail{
210228
ErrandDemandID: demand.ID,
@@ -213,6 +231,7 @@ func GetBuyerOrderDetail(ctx context.Context, requesterID, demandID int64) (*Buy
213231
Status: demand.Status,
214232
ProductItems: productItems,
215233
TotalOriginAmountCents: originCents,
234+
TotalActualAmountCents: actualCents,
216235
TotalServiceFeeCents: serviceCents,
217236
StoreInfo: store,
218237
CaptainInfo: captainInfo,
@@ -279,35 +298,120 @@ func findBillID(assignByItem map[int64]*model.ErrandTaskAssignment) *int64 {
279298
return nil
280299
}
281300

301+
func loadBuyerPackagingShareCents(ctx context.Context, task *model.ErrandTask, requesterID int64) (int32, error) {
302+
if task == nil || task.PackagingFeeCents <= 0 {
303+
return 0, nil
304+
}
305+
306+
rows, err := repository.ListTaskPaymentBillAssignments(ctx, postgres.DB, task.ID)
307+
if err != nil {
308+
return 0, err
309+
}
310+
311+
payerIDs := make(map[int64]struct{})
312+
requesterHasBillableItem := false
313+
for _, row := range rows {
314+
payerIDs[row.PayerID] = struct{}{}
315+
if row.PayerID == requesterID {
316+
requesterHasBillableItem = true
317+
}
318+
}
319+
if !requesterHasBillableItem {
320+
return 0, nil
321+
}
322+
323+
payerCount, err := safeInt32FromInt(len(payerIDs))
324+
if err != nil {
325+
return 0, err
326+
}
327+
return ceilDivide(task.PackagingFeeCents, payerCount), nil
328+
}
329+
282330
func buildProductItems(
283331
items []*model.ErrandDemandItem,
284332
assignByItem map[int64]*model.ErrandTaskAssignment,
285333
taskItemByProduct map[int64]*model.ErrandTaskItem,
286334
productMap map[int64]*catalogv1.ProductTemplate,
287-
) ([]*BuyerOrderProductItem, int32, int32) {
335+
packagingShareCents int32,
336+
) ([]*BuyerOrderProductItem, int32, *int32, int32, error) {
288337
productItems := make([]*BuyerOrderProductItem, 0, len(items))
289-
var originCents, serviceCents int32
338+
actualTotalsReady := len(items) > 0
339+
packagingApplied := false
340+
var originCents, actualProductCents, serviceCents int64
290341

291342
for _, item := range items {
292-
originCents += item.EstimatedUnitPriceCents * item.Quantity
293-
serviceCents += item.ServiceFeePerUnitCents * item.Quantity
343+
originCents += int64(item.EstimatedUnitPriceCents) * int64(item.Quantity)
294344

295345
pi := &BuyerOrderProductItem{
296346
ProductTemplate: productMap[item.ProductTemplateID],
297347
RequiredQuantity: item.Quantity,
298348
ServiceFeePerUnitCents: item.ServiceFeePerUnitCents,
299349
EstimatedUnitPriceCents: item.EstimatedUnitPriceCents,
300350
ErrandDemandItemID: item.ID,
351+
PackagingFeeShareCents: packagingShareCents,
301352
}
353+
354+
quantity := item.Quantity
302355
if a, ok := assignByItem[item.ID]; ok {
303356
pi.DistributedQuantity = a.DistributedQuantity
357+
if a.DistributedQuantity != nil {
358+
quantity = *a.DistributedQuantity
359+
} else {
360+
actualTotalsReady = false
361+
}
362+
} else {
363+
actualTotalsReady = false
304364
}
365+
366+
actualUnitPriceCents := item.EstimatedUnitPriceCents
305367
if ti, ok := taskItemByProduct[item.ProductTemplateID]; ok {
306368
pi.ActualUnitPriceCents = ti.ActualUnitPriceCents
307369
pi.PurchasedQuantity = ti.PurchasedQuantity
308370
pi.NonPurchaseReason = ti.NonPurchaseReason
371+
if ti.ActualUnitPriceCents != nil {
372+
actualUnitPriceCents = *ti.ActualUnitPriceCents
373+
} else if quantity > 0 {
374+
actualTotalsReady = false
375+
}
376+
} else if quantity > 0 {
377+
actualTotalsReady = false
378+
}
379+
380+
productAmount := int64(actualUnitPriceCents) * int64(quantity)
381+
serviceFeeAmount := int64(item.ServiceFeePerUnitCents) * int64(quantity)
382+
subtotalCents := productAmount + serviceFeeAmount
383+
if !packagingApplied && packagingShareCents > 0 {
384+
subtotalCents += int64(packagingShareCents)
385+
packagingApplied = true
309386
}
387+
388+
subtotal, err := safeInt32FromInt64(subtotalCents)
389+
if err != nil {
390+
return nil, 0, nil, 0, err
391+
}
392+
pi.SubtotalCents = subtotal
393+
actualProductCents += productAmount
394+
serviceCents += serviceFeeAmount
310395
productItems = append(productItems, pi)
311396
}
312-
return productItems, originCents, serviceCents
397+
398+
origin, err := safeInt32FromInt64(originCents)
399+
if err != nil {
400+
return nil, 0, nil, 0, err
401+
}
402+
service, err := safeInt32FromInt64(serviceCents)
403+
if err != nil {
404+
return nil, 0, nil, 0, err
405+
}
406+
407+
var actual *int32
408+
if actualTotalsReady {
409+
actualSummary, err := safeInt32FromInt64(actualProductCents)
410+
if err != nil {
411+
return nil, 0, nil, 0, err
412+
}
413+
actual = &actualSummary
414+
}
415+
416+
return productItems, origin, actual, service, nil
313417
}

internal/service/errandservice/internal/service/errand_task.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2585,18 +2585,25 @@ func CancelTask(ctx context.Context, captainID int64, req *errandv1.CancelTaskRe
25852585
Msg("failed to update task to cancelled")
25862586
return newErrandInternalError("")
25872587
}
2588-
if err := repository.UpdateTaskRelatedDemandsToCancelled(ctx, tx, task.TaskID, now); err != nil {
2588+
if err := repository.ReopenTaskRelatedDemands(ctx, tx, task.TaskID, now); err != nil {
25892589
log.Error().
25902590
Err(err).
25912591
Int64("errand_task_id", task.TaskID).
2592-
Msg("failed to update related demands to cancelled")
2592+
Msg("failed to reopen related demands")
25932593
return newErrandInternalError("")
25942594
}
2595-
if err := repository.UpdateTaskRelatedDemandItemsToCancelled(ctx, tx, task.TaskID, now); err != nil {
2595+
if err := repository.ReopenTaskRelatedDemandItems(ctx, tx, task.TaskID, now); err != nil {
25962596
log.Error().
25972597
Err(err).
25982598
Int64("errand_task_id", task.TaskID).
2599-
Msg("failed to update related demand items to cancelled")
2599+
Msg("failed to reopen related demand items")
2600+
return newErrandInternalError("")
2601+
}
2602+
if err := repository.DeleteReopenedTaskAssignments(ctx, tx, task.TaskID); err != nil {
2603+
log.Error().
2604+
Err(err).
2605+
Int64("errand_task_id", task.TaskID).
2606+
Msg("failed to delete reopened task assignments")
26002607
return newErrandInternalError("")
26012608
}
26022609

internal/service/paymentservice/internal/service/bill.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ var (
2323
ErrConcurrencyConflict = errors.New("concurrency conflict: bill was modified by another request")
2424
ErrBillNotFound = errors.New("bill not found")
2525
ErrInvalidBillStatus = errors.New("invalid bill status")
26+
ErrInvalidBillRequest = errors.New("invalid create bill request")
27+
ErrSelfPayment = errors.New("payer and payee must be different")
2628
ErrInvalidChannel = errors.New("invalid channel")
2729
ErrDuplicateBill = errors.New("duplicate bill")
2830
)
@@ -298,8 +300,11 @@ func CreateBillForOrder(
298300
payerID, payeeID int64,
299301
amountCents int32,
300302
) (*paymentv1.Bill, error) {
301-
if sourceType == "" || sourceID <= 0 || payerID <= 0 || payeeID <= 0 || amountCents < 0 || payerID == payeeID {
302-
return nil, ErrInvalidBillStatus
303+
if sourceType == "" || sourceID <= 0 || payerID <= 0 || payeeID <= 0 || amountCents < 0 {
304+
return nil, ErrInvalidBillRequest
305+
}
306+
if payerID == payeeID {
307+
return nil, ErrSelfPayment
303308
}
304309

305310
bill, err := repository.GetBillBySource(ctx, sourceType, sourceID, payerID)

internal/service/spotservice/internal/client/client.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212

1313
var (
1414
CatalogInternalServiceClient catalogv1connect.CatalogInternalServiceClient
15-
BillServiceClient paymentv1connect.BillServiceClient
1615
PaymentInternalServiceClient paymentv1connect.PaymentInternalServiceClient
1716
UserInternalServiceClient userv1connect.UserInternalServiceClient
1817
)
@@ -26,10 +25,6 @@ func Init() {
2625
http.DefaultClient,
2726
fmt.Sprintf("%s:%d", config.AppConfig.PaymentServiceURL, config.AppConfig.PaymentServicePort),
2827
)
29-
BillServiceClient = paymentv1connect.NewBillServiceClient(
30-
http.DefaultClient,
31-
fmt.Sprintf("%s:%d", config.AppConfig.PaymentServiceURL, config.AppConfig.PaymentServicePort),
32-
)
3328
UserInternalServiceClient = userv1connect.NewUserInternalServiceClient(
3429
http.DefaultClient,
3530
fmt.Sprintf("%s:%d", config.AppConfig.UserServiceURL, config.AppConfig.UserServicePort),

0 commit comments

Comments
 (0)