Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c5e63e4
fix(errand): GetDistributingTaskDetail 透传 purchased_quantity
Hunger61 Aug 1, 2026
eb655c3
Merge pull request #87 from NJUPT-SAST/fix/distributing-item-purchase…
Hunger61 Aug 1, 2026
c2a27eb
Merge branch 'main' of https://github.com/NJUPT-SAST/sast-shop-v2 int…
Hunger61 Aug 2, 2026
fef4616
Merge branch 'main' into dev
Hunger61 Aug 2, 2026
ca45f2c
Merge branch 'main' into dev
Hunger61 Aug 3, 2026
f758d22
列表响应批量补 productTemplate;详情/创建响应用已有内部 catalog/user client 补 productTem…
yulian-art Aug 3, 2026
aa631e7
Merge branch 'dev' of https://github.com/NJUPT-SAST/sast-shop-v2 into…
yulian-art Aug 3, 2026
023040c
汇总时间截断
yulian-art Aug 3, 2026
4c8fbd1
fix errand payment totals and cancel rollback
Whisper985 Aug 3, 2026
5e876b8
fix(payment): prevent PayBill timestamp conflicts (#92)
springbot2025 Aug 3, 2026
ba50b75
fix lint
yulian-art Aug 3, 2026
18faf12
Merge branch 'dev' of https://github.com/NJUPT-SAST/sast-shop-v2 into…
yulian-art Aug 3, 2026
169507a
fix lint
yulian-art Aug 3, 2026
414c8b2
Merge pull request #94 from NJUPT-SAST/codex/errand-payment-cancel-fixes
Hunger61 Aug 3, 2026
1d4fe7f
fix:time
yulian-art Aug 3, 2026
6129ca5
Spot 下单前直接拒绝“买家就是卖家”,避免先扣库存/写订单再跨服务失败;把支付端创建账单的参数校验拆成明确错误,避免所有非法参数都显示…
yulian-art Aug 3, 2026
10c3e98
Merge branch 'dev' of https://github.com/NJUPT-SAST/sast-shop-v2 into…
yulian-art Aug 3, 2026
f76cda2
feat(errand): add packaging_fee_share_cents to buyer order detail
876908 Aug 3, 2026
f9c9fba
feat(errand): add packaging_fee_share_cents to buyer order detail (#97)
876908 Aug 3, 2026
5755014
merge: resolve conflicts with main for packaging fee PR
876908 Aug 3, 2026
a09cf4e
Merge branch 'dev' into feat/buyer-order-packaging-fee
876908 Aug 3, 2026
41e5630
fix: add PackagingFeeShareCents field to BuyerOrderProductItem struct
876908 Aug 3, 2026
feab803
fix: remove local replace directive committed by accident
876908 Aug 3, 2026
cecc3f3
fix: golines formatting in spotservice
876908 Aug 3, 2026
2a89b4d
fix: remove packaging fee from totalActualAmountCents calculation
876908 Aug 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions internal/pkg/feishu/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ func Init() {
cfg := config.AppConfig
if cfg.Feishu_AppID == "" || cfg.Feishu_AppSecret == "" ||
cfg.Feishu_AppID == constant.FeishuDefaultAppID || cfg.Feishu_AppSecret == constant.FeishuDefaultAppSecret {
if cfg.AppEnv == config.Development {
return
}
panic("feishu: FEISHU_APP_ID / FEISHU_APP_SECRET must be configured with real credentials")
}
AppClient = &Client{
Expand Down
4 changes: 2 additions & 2 deletions internal/service/errandservice/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ module github.com/NJUPT-SAST/sast-shop-v2/internal/services/errandservice
go 1.26.3

require (
buf.build/gen/go/sast/sast-shop-v2/connectrpc/go v1.20.0-20260729122644-8226c83aee9b.1
buf.build/gen/go/sast/sast-shop-v2/protocolbuffers/go v1.36.11-20260729122644-8226c83aee9b.1
buf.build/gen/go/sast/sast-shop-v2/connectrpc/go v1.20.0-20260803055026-0b894163deec.1
buf.build/gen/go/sast/sast-shop-v2/protocolbuffers/go v1.36.11-20260803055026-0b894163deec.1
connectrpc.com/connect v1.20.0
github.com/NJUPT-SAST/sast-shop-v2/internal/pkg v0.0.0-20260703081920-1b187050f3a3
github.com/labstack/echo/v5 v5.1.1
Expand Down
7 changes: 5 additions & 2 deletions internal/service/errandservice/go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260709200747-435963d16310.1 h1:fXh8CsdNpjRr8R5vFdqtIxPt/Lno2IIJlYOdZBIZn0w=
buf.build/gen/go/sast/sast-shop-v2/connectrpc/go v1.20.0-20260729122644-8226c83aee9b.1 h1:K+gDa3l5rI8Nd3lom4PWtIjRLqLi7I4z1TpXqtnWWMo=
buf.build/gen/go/sast/sast-shop-v2/protocolbuffers/go v1.36.11-20260729122644-8226c83aee9b.1 h1:GkVktkeYitIhLdYrbBn7dhryQMQ9M8ucCK+b+JU1e+A=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260709200747-435963d16310.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
buf.build/gen/go/sast/sast-shop-v2/connectrpc/go v1.20.0-20260803055026-0b894163deec.1 h1:UxXjA2ZoiLmW5tqR7QL7awKN3YXmXSwD8DDtGdYFZO0=
buf.build/gen/go/sast/sast-shop-v2/connectrpc/go v1.20.0-20260803055026-0b894163deec.1/go.mod h1:ofBdA7CEpZY4zd/lR5KVu6DPUu+S9LHLwSfnA8KLU/0=
buf.build/gen/go/sast/sast-shop-v2/protocolbuffers/go v1.36.11-20260803055026-0b894163deec.1 h1:tVnCjKxG/0YzR6pUKA0clntFUgwXK8PLmrAcHWKtGnA=
buf.build/gen/go/sast/sast-shop-v2/protocolbuffers/go v1.36.11-20260803055026-0b894163deec.1/go.mod h1:aFm9mnorhJFXvjjh7GbevEuSVdVtd/KD4bgCogHCH40=
buf.build/go/protovalidate v1.2.0 h1:DQVrUWkmGTBij+kOYv/x2LLxwcLaGKMdzShj1/6/3H0=
buf.build/go/protovalidate v1.2.0/go.mod h1:7rYiQEhqvAipoazpVNBBH2S2f8bjG4huMVy1V2Yofn4=
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,9 @@ func (s *BuyerErrandOrderServiceServer) GetBuyerErrandOrderDetail(
NonPurchaseReason: &pi.NonPurchaseReason,
DistributedQuantity: pi.DistributedQuantity,
ServiceFeePerUnitCents: pi.ServiceFeePerUnitCents,
SubtotalCents: 0,
SubtotalCents: pi.SubtotalCents,
ErrandDemandItemId: pi.ErrandDemandItemID,
PackagingFeeShareCents: &pi.PackagingFeeShareCents,
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -689,3 +689,59 @@ func UpdateTaskRelatedDemandItemsToCancelled(ctx context.Context, db bun.IDB, ta
Exec(ctx)
return err
}

func ReopenTaskRelatedDemands(ctx context.Context, db bun.IDB, taskID int64, now time.Time) error {
_, err := db.NewUpdate().
Model((*model.ErrandDemand)(nil)).
Set("status = ?", model.ErrandDemandStatusOpen).
Set("task_id = NULL").
Set("shopping_start_at = NULL").
Set("shopping_completed_at = NULL").
Set("distribution_completed_at = NULL").
Set("payment_completed_at = NULL").
Set("cancelled_at = NULL").
Set("updated_at = ?", now).
Where(`id IN (
SELECT DISTINCT edi.errand_demand_id
FROM errand.errand_task_assignment AS eta
JOIN errand.errand_demand_item AS edi ON edi.id = eta.demand_item_id
WHERE eta.task_id = ?
)`, taskID).
Where("status NOT IN (?)", bun.List([]model.ErrandDemandStatus{
model.ErrandDemandStatusCompleted,
model.ErrandDemandStatusCancelled,
})).
Exec(ctx)
return err
}

func ReopenTaskRelatedDemandItems(ctx context.Context, db bun.IDB, taskID int64, now time.Time) error {
_, err := db.NewUpdate().
Model((*model.ErrandDemandItem)(nil)).
Set("status = ?", model.ErrandDemandItemStatusOpen).
Set("updated_at = ?", now).
Where(`id IN (
SELECT eta.demand_item_id
FROM errand.errand_task_assignment AS eta
WHERE eta.task_id = ?
)`, taskID).
Where("status NOT IN (?)", bun.List([]model.ErrandDemandItemStatus{
model.ErrandDemandItemStatusCompleted,
model.ErrandDemandItemStatusCancelled,
})).
Exec(ctx)
return err
}

func DeleteReopenedTaskAssignments(ctx context.Context, db bun.IDB, taskID int64) error {
_, err := db.NewDelete().
Model((*model.ErrandTaskAssignment)(nil)).
Where("task_id = ?", taskID).
Where(`demand_item_id IN (
SELECT edi.id
FROM errand.errand_demand_item AS edi
WHERE edi.status = ?
)`, model.ErrandDemandItemStatusOpen).
Exec(ctx)
return err
}
9 changes: 9 additions & 0 deletions internal/service/errandservice/internal/repository/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,15 @@ func MarkDemandsCompletedIfAllItemsDoneByItemIDs(
return res.RowsAffected()
}

// CountDistinctPurchasers 统计任务下唯一付款人数量,用于包装费分摊。
func CountDistinctPurchasers(ctx context.Context, db bun.IDB, taskID int64) (int, error) {
return db.NewSelect().
ColumnExpr("COUNT(DISTINCT purchaser_id)").
TableExpr("errand.errand_task_assignment").
Where("task_id = ?", taskID).
Count(ctx)
}

// MarkTaskCompleted 完成团长任务的状态流转
func MarkTaskCompleted(ctx context.Context, db bun.IDB, taskID int64, now time.Time) (int64, error) {
res, err := db.NewUpdate().
Expand Down
116 changes: 110 additions & 6 deletions internal/service/errandservice/internal/service/buyer_order.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ type BuyerOrderProductItem struct {
NonPurchaseReason string
DistributedQuantity *int32
ServiceFeePerUnitCents int32
SubtotalCents int32
EstimatedUnitPriceCents int32
ErrandDemandItemID int64
PackagingFeeShareCents int32
}

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

productItems, originCents, serviceCents := buildProductItems(items, assignByItem, taskItemByProduct, productMap)
packagingShareCents, err := loadBuyerPackagingShareCents(ctx, task, requesterID)
if err != nil {
log.Error().Err(err).Int64("demand_id", demandID).Msg("calculate buyer packaging share failed")
return nil, ErrInternal
}

productItems, originCents, actualCents, serviceCents, err := buildProductItems(
items,
assignByItem,
taskItemByProduct,
productMap,
packagingShareCents,
)
if err != nil {
log.Error().Err(err).Int64("demand_id", demandID).Msg("build buyer order product items failed")
return nil, ErrInternal
}

return &BuyerOrderDetail{
ErrandDemandID: demand.ID,
Expand All @@ -213,6 +231,7 @@ func GetBuyerOrderDetail(ctx context.Context, requesterID, demandID int64) (*Buy
Status: demand.Status,
ProductItems: productItems,
TotalOriginAmountCents: originCents,
TotalActualAmountCents: actualCents,
TotalServiceFeeCents: serviceCents,
StoreInfo: store,
CaptainInfo: captainInfo,
Expand Down Expand Up @@ -279,35 +298,120 @@ func findBillID(assignByItem map[int64]*model.ErrandTaskAssignment) *int64 {
return nil
}

func loadBuyerPackagingShareCents(ctx context.Context, task *model.ErrandTask, requesterID int64) (int32, error) {
if task == nil || task.PackagingFeeCents <= 0 {
return 0, nil
}

rows, err := repository.ListTaskPaymentBillAssignments(ctx, postgres.DB, task.ID)
if err != nil {
return 0, err
}

payerIDs := make(map[int64]struct{})
requesterHasBillableItem := false
for _, row := range rows {
payerIDs[row.PayerID] = struct{}{}
if row.PayerID == requesterID {
requesterHasBillableItem = true
}
}
if !requesterHasBillableItem {
return 0, nil
}

payerCount, err := safeInt32FromInt(len(payerIDs))
if err != nil {
return 0, err
}
return ceilDivide(task.PackagingFeeCents, payerCount), nil
}

func buildProductItems(
items []*model.ErrandDemandItem,
assignByItem map[int64]*model.ErrandTaskAssignment,
taskItemByProduct map[int64]*model.ErrandTaskItem,
productMap map[int64]*catalogv1.ProductTemplate,
) ([]*BuyerOrderProductItem, int32, int32) {
packagingShareCents int32,
) ([]*BuyerOrderProductItem, int32, *int32, int32, error) {
productItems := make([]*BuyerOrderProductItem, 0, len(items))
var originCents, serviceCents int32
actualTotalsReady := len(items) > 0
packagingApplied := false
var originCents, actualProductCents, serviceCents int64

for _, item := range items {
originCents += item.EstimatedUnitPriceCents * item.Quantity
serviceCents += item.ServiceFeePerUnitCents * item.Quantity
originCents += int64(item.EstimatedUnitPriceCents) * int64(item.Quantity)

pi := &BuyerOrderProductItem{
ProductTemplate: productMap[item.ProductTemplateID],
RequiredQuantity: item.Quantity,
ServiceFeePerUnitCents: item.ServiceFeePerUnitCents,
EstimatedUnitPriceCents: item.EstimatedUnitPriceCents,
ErrandDemandItemID: item.ID,
PackagingFeeShareCents: packagingShareCents,
}

quantity := item.Quantity
if a, ok := assignByItem[item.ID]; ok {
pi.DistributedQuantity = a.DistributedQuantity
if a.DistributedQuantity != nil {
quantity = *a.DistributedQuantity
} else {
actualTotalsReady = false
}
} else {
actualTotalsReady = false
}

actualUnitPriceCents := item.EstimatedUnitPriceCents
if ti, ok := taskItemByProduct[item.ProductTemplateID]; ok {
pi.ActualUnitPriceCents = ti.ActualUnitPriceCents
pi.PurchasedQuantity = ti.PurchasedQuantity
pi.NonPurchaseReason = ti.NonPurchaseReason
if ti.ActualUnitPriceCents != nil {
actualUnitPriceCents = *ti.ActualUnitPriceCents
} else if quantity > 0 {
actualTotalsReady = false
}
} else if quantity > 0 {
actualTotalsReady = false
}

productAmount := int64(actualUnitPriceCents) * int64(quantity)
serviceFeeAmount := int64(item.ServiceFeePerUnitCents) * int64(quantity)
subtotalCents := productAmount + serviceFeeAmount
if !packagingApplied && packagingShareCents > 0 {
subtotalCents += int64(packagingShareCents)
packagingApplied = true
}

subtotal, err := safeInt32FromInt64(subtotalCents)
if err != nil {
return nil, 0, nil, 0, err
}
pi.SubtotalCents = subtotal
actualProductCents += productAmount
serviceCents += serviceFeeAmount
productItems = append(productItems, pi)
}
return productItems, originCents, serviceCents

origin, err := safeInt32FromInt64(originCents)
if err != nil {
return nil, 0, nil, 0, err
}
service, err := safeInt32FromInt64(serviceCents)
if err != nil {
return nil, 0, nil, 0, err
}

var actual *int32
if actualTotalsReady {
actualSummary, err := safeInt32FromInt64(actualProductCents)
if err != nil {
return nil, 0, nil, 0, err
}
actual = &actualSummary
}

return productItems, origin, actual, service, nil
}
15 changes: 11 additions & 4 deletions internal/service/errandservice/internal/service/errand_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -2585,18 +2585,25 @@ func CancelTask(ctx context.Context, captainID int64, req *errandv1.CancelTaskRe
Msg("failed to update task to cancelled")
return newErrandInternalError("")
}
if err := repository.UpdateTaskRelatedDemandsToCancelled(ctx, tx, task.TaskID, now); err != nil {
if err := repository.ReopenTaskRelatedDemands(ctx, tx, task.TaskID, now); err != nil {
log.Error().
Err(err).
Int64("errand_task_id", task.TaskID).
Msg("failed to update related demands to cancelled")
Msg("failed to reopen related demands")
return newErrandInternalError("")
}
if err := repository.UpdateTaskRelatedDemandItemsToCancelled(ctx, tx, task.TaskID, now); err != nil {
if err := repository.ReopenTaskRelatedDemandItems(ctx, tx, task.TaskID, now); err != nil {
log.Error().
Err(err).
Int64("errand_task_id", task.TaskID).
Msg("failed to update related demand items to cancelled")
Msg("failed to reopen related demand items")
return newErrandInternalError("")
}
if err := repository.DeleteReopenedTaskAssignments(ctx, tx, task.TaskID); err != nil {
log.Error().
Err(err).
Int64("errand_task_id", task.TaskID).
Msg("failed to delete reopened task assignments")
return newErrandInternalError("")
}

Expand Down
9 changes: 7 additions & 2 deletions internal/service/paymentservice/internal/service/bill.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ var (
ErrConcurrencyConflict = errors.New("concurrency conflict: bill was modified by another request")
ErrBillNotFound = errors.New("bill not found")
ErrInvalidBillStatus = errors.New("invalid bill status")
ErrInvalidBillRequest = errors.New("invalid create bill request")
ErrSelfPayment = errors.New("payer and payee must be different")
ErrInvalidChannel = errors.New("invalid channel")
ErrDuplicateBill = errors.New("duplicate bill")
)
Expand Down Expand Up @@ -298,8 +300,11 @@ func CreateBillForOrder(
payerID, payeeID int64,
amountCents int32,
) (*paymentv1.Bill, error) {
if sourceType == "" || sourceID <= 0 || payerID <= 0 || payeeID <= 0 || amountCents < 0 || payerID == payeeID {
return nil, ErrInvalidBillStatus
if sourceType == "" || sourceID <= 0 || payerID <= 0 || payeeID <= 0 || amountCents < 0 {
return nil, ErrInvalidBillRequest
}
if payerID == payeeID {
return nil, ErrSelfPayment
}

bill, err := repository.GetBillBySource(ctx, sourceType, sourceID, payerID)
Expand Down
5 changes: 0 additions & 5 deletions internal/service/spotservice/internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (

var (
CatalogInternalServiceClient catalogv1connect.CatalogInternalServiceClient
BillServiceClient paymentv1connect.BillServiceClient
PaymentInternalServiceClient paymentv1connect.PaymentInternalServiceClient
UserInternalServiceClient userv1connect.UserInternalServiceClient
)
Expand All @@ -26,10 +25,6 @@ func Init() {
http.DefaultClient,
fmt.Sprintf("%s:%d", config.AppConfig.PaymentServiceURL, config.AppConfig.PaymentServicePort),
)
BillServiceClient = paymentv1connect.NewBillServiceClient(
http.DefaultClient,
fmt.Sprintf("%s:%d", config.AppConfig.PaymentServiceURL, config.AppConfig.PaymentServicePort),
)
UserInternalServiceClient = userv1connect.NewUserInternalServiceClient(
http.DefaultClient,
fmt.Sprintf("%s:%d", config.AppConfig.UserServiceURL, config.AppConfig.UserServicePort),
Expand Down
Loading
Loading