feat(员工代收款): 新增员工代收款账单闭环
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m20s

- 新增 6 张表与成对迁移 000212,扩展企业微信审批场景业务类型白名单
- 后台线下套餐订单与两条代理线下充值入账路径在来源成功事务内建账,来源唯一键幂等
- 核销申请、审批尝试记录、账单分摊预占与驳回重提,审批业务类型 employee_collection_approval
- 企业微信终态消费幂等:通过转已核销、驳回释放预占、通过后撤销不回滚并转异常终态
- 退款成功事务内按 bill_id+refund_id 幂等冲销账单或仅写退款关联提示
- 线下收款方式字典、账单查询/统计/关闭、申请查询与代办权限,均写入事务内审计

OpenSpec Change: add-employee-collection-bills
This commit is contained in:
2026-09-10 18:24:05 +08:00
parent dc4e0d4103
commit ce24d5612e
54 changed files with 5975 additions and 465 deletions

View File

@@ -14,6 +14,7 @@ import (
"gorm.io/gorm"
agentrechargeapp "github.com/break/junhong_cmp_fiber/internal/application/agentrecharge"
employeecollectionapp "github.com/break/junhong_cmp_fiber/internal/application/employeecollection"
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
@@ -48,6 +49,12 @@ type Service struct {
redis *redis.Client
logger *zap.Logger
rechargeAudit agentrechargeapp.RechargeAuditWriter
billCreation *employeecollectionapp.BillCreationService
}
// SetEmployeeCollectionBillCreation 注入员工代收款账单建账用例。
func (s *Service) SetEmployeeCollectionBillCreation(creation *employeecollectionapp.BillCreationService) {
s.billCreation = creation
}
// New 创建代理预充值服务实例
@@ -222,6 +229,13 @@ func (s *Service) OfflinePay(ctx context.Context, id uint, req *dto.AgentOffline
if err != nil {
return err
}
// 员工代收款建账:人工确认入账与企微终审入账同属“线下充值入账成功”事实。
if s.billCreation == nil {
return errors.New(errors.CodeInternalError, "员工代收款建账能力未配置")
}
if _, err := s.billCreation.CreateFromRechargeInTx(ctx, tx, record); err != nil {
return err
}
return s.appendCreditedAudit(ctx, tx, record, nil, constants.RechargeStatusCompleted, "线下充值确认已入账")
})

View File

@@ -11,8 +11,10 @@ import (
"time"
cardObservationApp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
employeecollectionapp "github.com/break/junhong_cmp_fiber/internal/application/employeecollection"
merchantpayment "github.com/break/junhong_cmp_fiber/internal/application/merchantpayment"
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
employeecollectiondomain "github.com/break/junhong_cmp_fiber/internal/domain/employeecollection"
packagedomain "github.com/break/junhong_cmp_fiber/internal/domain/package"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/commissiondelivery"
@@ -76,6 +78,12 @@ type Service struct {
observationSeriesEvents cardObservationApp.SeriesEventWriter
auditWriter *audit.Writer
paymentIntegration *integrationlog.Repository
billCreation *employeecollectionapp.BillCreationService
}
// SetEmployeeCollectionBillCreation 注入员工代收款账单建账用例。
func (s *Service) SetEmployeeCollectionBillCreation(creation *employeecollectionapp.BillCreationService) {
s.billCreation = creation
}
// SetObservationSeriesEventWriter 注入购包成功观测序列 Outbox Writer。
@@ -221,9 +229,6 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
return nil, err
}
}
if req.PaymentMethod == model.PaymentMethodOffline && len(req.PaymentVoucherKey) == 0 {
return nil, errors.New(errors.CodeInvalidParam, "线下支付必须上传支付凭证")
}
carrierType, carrierID := resolveAdminCarrierInfoFromVars(orderType, iotCardID, deviceID)
existingOrderID, lockToken, err := s.checkOrderIdempotency(ctx, buyerType, buyerID, orderType, carrierType, carrierID, req.PackageIDs)
@@ -494,6 +499,14 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
}
auditOrder = order
// 线下订单创建时付款凭证要求:会生成员工代收款账单的订单改由核销申请提供凭证,
// 其余既有线下订单保持原要求。判定复用建账判据,避免两处口径分叉。
createsCollectionBill := employeecollectiondomain.ShouldCreateBillForOrder(
employeecollectiondomain.OrderBillSubjectFromOrder(order, containsGift))
if req.PaymentMethod == model.PaymentMethodOffline && !createsCollectionBill && len(req.PaymentVoucherKey) == 0 {
return nil, errors.New(errors.CodeInvalidParam, "线下支付必须上传支付凭证")
}
// 线下支付订单写入支付凭证 file_key 列表
if req.PaymentMethod == model.PaymentMethodOffline {
order.PaymentVoucherKey = model.StringJSONBArray(req.PaymentVoucherKey)
@@ -510,7 +523,7 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
// 根据支付方式选择创建订单的方式
if req.PaymentMethod == model.PaymentMethodOffline {
// 平台代购:创建订单并立即激活套餐
if err := s.createOrderWithActivation(ctx, order, items); err != nil {
if err := s.createOrderWithActivation(ctx, order, items, containsGift); err != nil {
return nil, err
}
s.markOrderCreated(ctx, idempotencyKey, order.ID)
@@ -805,7 +818,7 @@ func (s *Service) CreateH5Order(ctx context.Context, req *dto.CreateOrderRequest
// 根据支付方式选择创建订单的方式
if req.PaymentMethod == model.PaymentMethodOffline {
// 平台代购:创建订单并立即激活套餐
if err := s.createOrderWithActivation(ctx, order, items); err != nil {
if err := s.createOrderWithActivation(ctx, order, items, packageprice.ContainsGiftPackage(validationResult.Packages)); err != nil {
return nil, err
}
s.markOrderCreated(ctx, idempotencyKey, order.ID)
@@ -1215,12 +1228,20 @@ func (s *Service) createOrderWithWalletPayment(ctx context.Context, order *model
return 0, nil
}
func (s *Service) createOrderWithActivation(ctx context.Context, order *model.Order, items []*model.OrderItem) error {
func (s *Service) createOrderWithActivation(ctx context.Context, order *model.Order, items []*model.OrderItem, hasGiftPackage bool) error {
if s.billCreation == nil {
return errors.New(errors.CodeInternalError, "员工代收款建账能力未配置")
}
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Create(order).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建代购订单失败")
}
// 员工代收款建账:按来源唯一键 order:{id} 幂等,重复事务或重放返回既有账单。
if _, err := s.billCreation.CreateFromOrderInTx(ctx, tx, order, hasGiftPackage); err != nil {
return err
}
for _, item := range items {
item.OrderID = order.ID
}

View File

@@ -9,6 +9,7 @@ import (
"gorm.io/gorm/clause"
approvalapp "github.com/break/junhong_cmp_fiber/internal/application/approval"
employeecollectionapp "github.com/break/junhong_cmp_fiber/internal/application/employeecollection"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/commissiondelivery"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
"github.com/break/junhong_cmp_fiber/internal/model"
@@ -101,6 +102,16 @@ func (s *Service) applyApprovedDecision(ctx context.Context, event approvalapp.T
if err := s.refundWalletPayment(ctx, tx, &refund, &order, approvedAmount, event.SubmitterAccountID); err != nil {
return err
}
// 员工代收款冲销:接入点只在既有退款成功事务内,以 bill_id+refund_id 唯一事实幂等,
// 不依赖下方 changed 门;来源订单未建账时直接跳过,不阻断退款。
if s.refundOffset == nil {
return errors.New(errors.CodeInternalError, "员工代收款退款冲销能力未配置")
}
if err := s.refundOffset.ApplyInTx(ctx, tx, employeecollectionapp.RefundOffsetSource{
RefundID: refund.ID, OrderID: order.ID, RefundAmount: approvedAmount,
}); err != nil {
return err
}
if err := s.appendCompletedNotification(ctx, tx, &refund); err != nil {
return err
}

View File

@@ -15,6 +15,7 @@ import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
employeecollectionapp "github.com/break/junhong_cmp_fiber/internal/application/employeecollection"
merchantpayment "github.com/break/junhong_cmp_fiber/internal/application/merchantpayment"
notificationapp "github.com/break/junhong_cmp_fiber/internal/application/notification"
refundapprovalapp "github.com/break/junhong_cmp_fiber/internal/application/refundapproval"
@@ -58,6 +59,12 @@ type Service struct {
auditWriter *audit.Writer
logger *zap.Logger
paymentMerchantRuntime *merchantpayment.RuntimeLoader
refundOffset *employeecollectionapp.RefundOffsetService
}
// SetEmployeeCollectionRefundOffset 注入员工代收款账单退款冲销用例。
func (s *Service) SetEmployeeCollectionRefundOffset(offset *employeecollectionapp.RefundOffsetService) {
s.refundOffset = offset
}
// New 创建退款业务服务实例