七月迭代短暂完结,还有很多后端的关键东西没有弄,这是一版赶时间做的东西
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s
This commit is contained in:
119
internal/application/agentrecharge/approval_decision.go
Normal file
119
internal/application/agentrecharge/approval_decision.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package agentrecharge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
approvalapp "github.com/break/junhong_cmp_fiber/internal/application/approval"
|
||||
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// ApprovalDecisionHandler 将渠道无关审批终态应用到员工线下代充值业务。
|
||||
type ApprovalDecisionHandler struct {
|
||||
db *gorm.DB
|
||||
posting *walletapp.PostingService
|
||||
}
|
||||
|
||||
// NewApprovalDecisionHandler 创建员工线下代充值审批终态消费者。
|
||||
func NewApprovalDecisionHandler(db *gorm.DB, posting *walletapp.PostingService) *ApprovalDecisionHandler {
|
||||
return &ApprovalDecisionHandler{db: db, posting: posting}
|
||||
}
|
||||
|
||||
// Handle 幂等处理标准审批终态;只有 approved 首次入账,其他终态不修改钱包。
|
||||
func (h *ApprovalDecisionHandler) Handle(ctx context.Context, event approvalapp.TerminalDecisionEvent) error {
|
||||
if h == nil || h.db == nil || h.posting == nil {
|
||||
return errors.New(errors.CodeInternalError, "员工线下代充值审批终态能力未配置")
|
||||
}
|
||||
if event.BusinessType != constants.ApprovalBusinessTypeOfflineRecharge || event.BusinessID == 0 || event.InstanceID == 0 {
|
||||
return errors.New(errors.CodeInvalidParam, "员工线下代充值审批终态参数无效")
|
||||
}
|
||||
return h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var record model.AgentRechargeRecord
|
||||
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("id = ? AND payment_method = ?", event.BusinessID, constants.RechargeMethodOffline).
|
||||
First(&record).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "锁定员工线下代充值申请失败")
|
||||
}
|
||||
if record.ApprovalInstanceID == nil || *record.ApprovalInstanceID != event.InstanceID {
|
||||
return errors.New(errors.CodeConflict, "线下代充值申请关联的审批实例不一致")
|
||||
}
|
||||
switch event.Decision {
|
||||
case constants.ApprovalDecisionApproved:
|
||||
return h.applyApproved(ctx, tx, &record, event)
|
||||
case constants.ApprovalDecisionRejected:
|
||||
return closeOfflineRecharge(ctx, tx, &record, constants.RechargeStatusRejected, "企业微信审批已拒绝")
|
||||
case constants.ApprovalDecisionCancelled:
|
||||
return closeOfflineRecharge(ctx, tx, &record, constants.RechargeStatusClosed, "企业微信审批已撤销")
|
||||
case constants.ApprovalDecisionDeleted:
|
||||
return closeOfflineRecharge(ctx, tx, &record, constants.RechargeStatusClosed, "企业微信审批已删除")
|
||||
case constants.ApprovalDecisionRevokedAfterApproved:
|
||||
return nil
|
||||
default:
|
||||
return errors.New(errors.CodeInvalidParam, "不支持的线下代充值审批终态")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (h *ApprovalDecisionHandler) applyApproved(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
record *model.AgentRechargeRecord,
|
||||
event approvalapp.TerminalDecisionEvent,
|
||||
) error {
|
||||
if record.Status != constants.RechargeStatusPending && record.Status != constants.RechargeStatusCompleted {
|
||||
return errors.New(errors.CodeInvalidStatus, "线下代充值申请状态不允许审批入账")
|
||||
}
|
||||
if record.Status == constants.RechargeStatusPending {
|
||||
result := tx.WithContext(ctx).Model(&model.AgentRechargeRecord{}).
|
||||
Where("id = ? AND status = ?", record.ID, constants.RechargeStatusPending).
|
||||
Updates(map[string]any{
|
||||
"status": constants.RechargeStatusCompleted, "paid_at": event.OccurredAt,
|
||||
"completed_at": event.OccurredAt, "updated_at": event.OccurredAt,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "完成线下代充值审批申请失败")
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return errors.New(errors.CodeConflict, "线下代充值申请状态已变化")
|
||||
}
|
||||
}
|
||||
_, err := h.posting.PostInTx(ctx, tx, walletapp.PostingCommand{
|
||||
ShopID: record.ShopID, WalletID: record.AgentWalletID, Amount: record.Amount,
|
||||
ReferenceType: constants.ReferenceTypeTopup, ReferenceID: record.ID,
|
||||
TransactionType: constants.AgentTransactionTypeRecharge,
|
||||
UserID: event.SubmitterAccountID, Creator: event.SubmitterAccountID,
|
||||
Remark: "企业微信审批通过线下充值", CorrelationID: event.CorrelationID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func closeOfflineRecharge(ctx context.Context, tx *gorm.DB, record *model.AgentRechargeRecord, status int, reason string) error {
|
||||
if record.Status == status {
|
||||
return nil
|
||||
}
|
||||
if record.Status != constants.RechargeStatusPending {
|
||||
return errors.New(errors.CodeInvalidStatus, "线下代充值申请状态不允许结束审批")
|
||||
}
|
||||
reason = strings.TrimSpace(reason)
|
||||
result := tx.WithContext(ctx).Model(&model.AgentRechargeRecord{}).
|
||||
Where("id = ? AND status = ?", record.ID, constants.RechargeStatusPending).
|
||||
Updates(map[string]any{"status": status, "rejection_reason": reason})
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "结束线下代充值审批申请失败")
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return errors.New(errors.CodeConflict, "线下代充值申请状态已变化")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ approvalapp.BusinessDecisionHandler = (*ApprovalDecisionHandler)(nil)
|
||||
188
internal/application/agentrecharge/offline_creation.go
Normal file
188
internal/application/agentrecharge/offline_creation.go
Normal file
@@ -0,0 +1,188 @@
|
||||
// Package agentrecharge 收口员工线下代充值申请和审批终态业务用例。
|
||||
package agentrecharge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"gorm.io/gorm"
|
||||
|
||||
approvalapp "github.com/break/junhong_cmp_fiber/internal/application/approval"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// CreateOfflineCommand 描述员工创建线下代充值审批申请的稳定输入。
|
||||
type CreateOfflineCommand struct {
|
||||
SubmitterAccountID uint
|
||||
SubmitterUserType int
|
||||
ShopID uint
|
||||
RechargeNo string
|
||||
Amount int64
|
||||
PaymentVoucherKeys []string
|
||||
Remark string
|
||||
}
|
||||
|
||||
// CreateOfflineResult 返回已原子保存的业务申请和初始审批状态。
|
||||
type CreateOfflineResult struct {
|
||||
Record *model.AgentRechargeRecord
|
||||
ShopName string
|
||||
SubmitterName string
|
||||
ApprovalStatus int
|
||||
}
|
||||
|
||||
// OfflineCreationService 创建员工线下代充值申请及唯一通用审批实例。
|
||||
type OfflineCreationService struct {
|
||||
db *gorm.DB
|
||||
approval approvalapp.Port
|
||||
}
|
||||
|
||||
// NewOfflineCreationService 创建员工线下代充值申请用例。
|
||||
func NewOfflineCreationService(db *gorm.DB, approval approvalapp.Port) *OfflineCreationService {
|
||||
return &OfflineCreationService{db: db, approval: approval}
|
||||
}
|
||||
|
||||
// Execute 在业务写入前校验审批渠道,并在同一事务保存充值申请、审批实例和提交 Outbox。
|
||||
func (s *OfflineCreationService) Execute(ctx context.Context, command CreateOfflineCommand) (*CreateOfflineResult, error) {
|
||||
if s == nil || s.db == nil || s.approval == nil {
|
||||
return nil, errors.New(errors.CodeServiceUnavailable, "员工线下代充值审批能力未配置")
|
||||
}
|
||||
if err := validateCreateOfflineCommand(command); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
account, shop, wallet, err := s.loadCreationFacts(ctx, command)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
preparation, err := s.approval.Prepare(ctx, approvalapp.PrepareRequest{
|
||||
BusinessType: constants.ApprovalBusinessTypeOfflineRecharge, SubmitterAccountID: command.SubmitterAccountID,
|
||||
CorrelationID: strings.TrimSpace(command.RechargeNo),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
submitterSnapshot, requestSnapshot, err := offlineApprovalSnapshots(command, account.Username, shop.ShopName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paymentChannel := constants.RechargeMethodOffline
|
||||
record := &model.AgentRechargeRecord{
|
||||
UserID: command.SubmitterAccountID, AgentWalletID: wallet.ID, ShopID: command.ShopID,
|
||||
RechargeNo: strings.TrimSpace(command.RechargeNo), Amount: command.Amount,
|
||||
PaymentMethod: constants.RechargeMethodOffline, PaymentChannel: &paymentChannel,
|
||||
PaymentVoucherKey: model.StringJSONBArray(command.PaymentVoucherKeys), Remark: strings.TrimSpace(command.Remark),
|
||||
Status: constants.RechargeStatusPending, ShopIDTag: wallet.ShopIDTag, EnterpriseIDTag: wallet.EnterpriseIDTag,
|
||||
}
|
||||
var approvalStatus int
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.WithContext(ctx).Create(record).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建员工线下代充值申请失败")
|
||||
}
|
||||
reference, err := s.approval.CreateInTx(ctx, tx, approvalapp.CreateRequest{
|
||||
Preparation: preparation, BusinessType: constants.ApprovalBusinessTypeOfflineRecharge,
|
||||
BusinessID: record.ID, SubmitterAccountID: command.SubmitterAccountID,
|
||||
SubmitterSnapshot: submitterSnapshot, RequestSnapshot: requestSnapshot,
|
||||
CorrelationID: record.RechargeNo,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result := tx.WithContext(ctx).Model(&model.AgentRechargeRecord{}).
|
||||
Where("id = ? AND approval_instance_id IS NULL", record.ID).
|
||||
Update("approval_instance_id", reference.InstanceID)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "关联员工线下代充值审批实例失败")
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return errors.New(errors.CodeConflict, "员工线下代充值审批实例关联已变化")
|
||||
}
|
||||
record.ApprovalInstanceID = &reference.InstanceID
|
||||
approvalStatus = reference.Status
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &CreateOfflineResult{
|
||||
Record: record, ShopName: shop.ShopName, SubmitterName: account.Username, ApprovalStatus: approvalStatus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateCreateOfflineCommand(command CreateOfflineCommand) error {
|
||||
if command.SubmitterUserType != constants.UserTypePlatform && command.SubmitterUserType != constants.UserTypeSuperAdmin {
|
||||
return errors.New(errors.CodeForbidden, "线下充值仅平台管理员可操作")
|
||||
}
|
||||
if command.SubmitterAccountID == 0 || command.ShopID == 0 || strings.TrimSpace(command.RechargeNo) == "" {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
if command.Amount < constants.AgentRechargeMinAmount || command.Amount > constants.AgentRechargeMaxAmount {
|
||||
return errors.New(errors.CodeInvalidParam, "充值金额超出允许范围")
|
||||
}
|
||||
if len(command.PaymentVoucherKeys) == 0 || len(command.PaymentVoucherKeys) > 5 {
|
||||
return errors.New(errors.CodeInvalidParam, "线下充值必须上传 1 至 5 个支付凭证")
|
||||
}
|
||||
for _, key := range command.PaymentVoucherKeys {
|
||||
if strings.TrimSpace(key) == "" {
|
||||
return errors.New(errors.CodeInvalidParam, "线下充值支付凭证不能为空")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OfflineCreationService) loadCreationFacts(
|
||||
ctx context.Context,
|
||||
command CreateOfflineCommand,
|
||||
) (*model.Account, *model.Shop, *model.AgentWallet, error) {
|
||||
var account model.Account
|
||||
if err := s.db.WithContext(ctx).Where("id = ? AND status = ?", command.SubmitterAccountID, constants.StatusEnabled).First(&account).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil, nil, errors.New(errors.CodeForbidden, "提交人账号不可用")
|
||||
}
|
||||
return nil, nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询线下代充值提交人失败")
|
||||
}
|
||||
var shop model.Shop
|
||||
if err := s.db.WithContext(ctx).Where("id = ?", command.ShopID).First(&shop).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil, nil, errors.New(errors.CodeNotFound, "目标店铺不存在")
|
||||
}
|
||||
return nil, nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询目标店铺失败")
|
||||
}
|
||||
var wallet model.AgentWallet
|
||||
if err := s.db.WithContext(ctx).
|
||||
Where("shop_id = ? AND wallet_type = ? AND status = ?", command.ShopID, constants.AgentWalletTypeMain, constants.AgentWalletStatusNormal).
|
||||
First(&wallet).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil, nil, errors.New(errors.CodeWalletNotFound, "目标店铺主钱包不存在或不可用")
|
||||
}
|
||||
return nil, nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询目标店铺主钱包失败")
|
||||
}
|
||||
return &account, &shop, &wallet, nil
|
||||
}
|
||||
|
||||
func offlineApprovalSnapshots(command CreateOfflineCommand, submitterName, shopName string) ([]byte, []byte, error) {
|
||||
submitterSnapshot, err := sonic.Marshal(map[string]any{
|
||||
"account_id": command.SubmitterAccountID, "account_name": submitterName,
|
||||
"user_type": command.SubmitterUserType,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(errors.CodeInternalError, err, "编码线下代充值提交人快照失败")
|
||||
}
|
||||
requestSnapshot, err := sonic.Marshal(map[string]any{
|
||||
constants.ApprovalFieldRechargeNo: strings.TrimSpace(command.RechargeNo),
|
||||
constants.ApprovalFieldShopID: command.ShopID,
|
||||
constants.ApprovalFieldShopName: shopName,
|
||||
constants.ApprovalFieldAmount: fmt.Sprintf("%d.%02d", command.Amount/100, command.Amount%100),
|
||||
constants.ApprovalFieldAmountCent: command.Amount,
|
||||
constants.ApprovalFieldPaymentVoucherKey: command.PaymentVoucherKeys,
|
||||
constants.ApprovalFieldRemark: strings.TrimSpace(command.Remark),
|
||||
constants.ApprovalFieldSubmitterID: command.SubmitterAccountID,
|
||||
constants.ApprovalFieldSubmitterName: submitterName,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(errors.CodeInternalError, err, "编码线下代充值审批业务快照失败")
|
||||
}
|
||||
return submitterSnapshot, requestSnapshot, nil
|
||||
}
|
||||
49
internal/application/exchange/shipping_notification.go
Normal file
49
internal/application/exchange/shipping_notification.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// Package exchange 提供换货用例的可靠通知编排。
|
||||
package exchange
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// ShippingCreatedEvent 是物流换货单创建后通知个人客户的稳定业务事件。
|
||||
type ShippingCreatedEvent struct {
|
||||
ExchangeID uint
|
||||
ExchangeNo string
|
||||
CustomerID uint
|
||||
AssetType string
|
||||
AssetID uint
|
||||
AssetIdentifier string
|
||||
RequestID string
|
||||
CorrelationID string
|
||||
}
|
||||
|
||||
// ShippingCreatedEventWriter 将物流换货创建通知写入可靠事件基础设施。
|
||||
type ShippingCreatedEventWriter interface {
|
||||
Append(ctx context.Context, tx *gorm.DB, event ShippingCreatedEvent) error
|
||||
}
|
||||
|
||||
// ShippingCreatedNotifier 在换货业务事务内编排个人客户通知事件。
|
||||
type ShippingCreatedNotifier struct {
|
||||
writer ShippingCreatedEventWriter
|
||||
}
|
||||
|
||||
// NewShippingCreatedNotifier 创建物流换货通知用例。
|
||||
func NewShippingCreatedNotifier(writer ShippingCreatedEventWriter) *ShippingCreatedNotifier {
|
||||
return &ShippingCreatedNotifier{writer: writer}
|
||||
}
|
||||
|
||||
// Notify 在换货单事务内追加指定个人客户的可靠通知事件。
|
||||
func (n *ShippingCreatedNotifier) Notify(ctx context.Context, tx *gorm.DB, event ShippingCreatedEvent) error {
|
||||
if n == nil || n.writer == nil {
|
||||
return errors.New(errors.CodeInternalError, "物流换货通知事件 Writer 未配置")
|
||||
}
|
||||
if tx == nil || event.ExchangeID == 0 || event.ExchangeNo == "" || event.CustomerID == 0 ||
|
||||
event.AssetType == "" || event.AssetID == 0 || event.AssetIdentifier == "" {
|
||||
return errors.New(errors.CodeInvalidParam, "物流换货通知事件参数不完整")
|
||||
}
|
||||
return n.writer.Append(ctx, tx, event)
|
||||
}
|
||||
@@ -103,8 +103,8 @@ func personalReadScope(db *gorm.DB, customerID uint, now time.Time) *gorm.DB {
|
||||
AND category IN ? AND type IN ? AND (expires_at IS NULL OR expires_at > ?)`,
|
||||
constants.NotificationRecipientKindPersonalCustomer,
|
||||
customerID,
|
||||
[]string{constants.NotificationCategoryApproval, constants.NotificationCategoryExpiry},
|
||||
[]string{constants.NotificationTypePackageExpiring},
|
||||
[]string{constants.NotificationCategoryApproval, constants.NotificationCategoryExpiry, constants.NotificationCategorySystem},
|
||||
[]string{constants.NotificationTypePackageExpiring, constants.NotificationTypeExchangeShippingCreated},
|
||||
now,
|
||||
)
|
||||
}
|
||||
|
||||
45
internal/application/packageexpiry/reminder.go
Normal file
45
internal/application/packageexpiry/reminder.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Package packageexpiry 编排套餐临期节点提醒用例。
|
||||
package packageexpiry
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// ReminderScanner 查询当天命中临期节点的资产。
|
||||
type ReminderScanner interface {
|
||||
ReminderCandidates(ctx context.Context) ([]dto.ExpiringAssetItem, error)
|
||||
}
|
||||
|
||||
// ReminderPublisher 批量发布个人客户临期通知。
|
||||
type ReminderPublisher interface {
|
||||
Publish(ctx context.Context, candidates []dto.ExpiringAssetItem) error
|
||||
}
|
||||
|
||||
// ReminderService 扫描并发布每日套餐临期节点提醒。
|
||||
type ReminderService struct {
|
||||
scanner ReminderScanner
|
||||
publisher ReminderPublisher
|
||||
}
|
||||
|
||||
// NewReminderService 创建套餐临期提醒用例。
|
||||
func NewReminderService(scanner ReminderScanner, publisher ReminderPublisher) *ReminderService {
|
||||
return &ReminderService{scanner: scanner, publisher: publisher}
|
||||
}
|
||||
|
||||
// Run 执行当天 15、7、3 天节点扫描并可靠发布通知。
|
||||
func (s *ReminderService) Run(ctx context.Context) error {
|
||||
if s == nil || s.scanner == nil || s.publisher == nil {
|
||||
return errors.New(errors.CodeInternalError, "套餐临期提醒用例未配置")
|
||||
}
|
||||
candidates, err := s.scanner.ReminderCandidates(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.publisher.Publish(ctx, candidates)
|
||||
}
|
||||
151
internal/application/refundapproval/creation.go
Normal file
151
internal/application/refundapproval/creation.go
Normal file
@@ -0,0 +1,151 @@
|
||||
// Package refundapproval 收口退款申请与渠道无关审批的事务边界。
|
||||
package refundapproval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"gorm.io/gorm"
|
||||
|
||||
approvalapp "github.com/break/junhong_cmp_fiber/internal/application/approval"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// CreateCommand 描述已通过订单与金额校验的退款审批申请。
|
||||
type CreateCommand struct {
|
||||
Refund *model.RefundRequest
|
||||
SubmitterAccountID uint
|
||||
}
|
||||
|
||||
// CreateResult 返回原子保存后的退款申请和初始审批状态。
|
||||
type CreateResult struct {
|
||||
Refund *model.RefundRequest
|
||||
SubmitterName string
|
||||
ApprovalStatus int
|
||||
}
|
||||
|
||||
// CreationService 原子创建退款申请、通用审批实例、企微上下文和提交 Outbox。
|
||||
type CreationService struct {
|
||||
db *gorm.DB
|
||||
approval approvalapp.Port
|
||||
}
|
||||
|
||||
// NewCreationService 创建退款审批申请用例。
|
||||
func NewCreationService(db *gorm.DB, approval approvalapp.Port) *CreationService {
|
||||
return &CreationService{db: db, approval: approval}
|
||||
}
|
||||
|
||||
// Execute 在业务写入前校验审批渠道,并在同一事务冻结退款事实和审批事实。
|
||||
func (s *CreationService) Execute(ctx context.Context, command CreateCommand) (*CreateResult, error) {
|
||||
if s == nil || s.db == nil || s.approval == nil {
|
||||
return nil, errors.New(errors.CodeServiceUnavailable, "退款审批能力未配置")
|
||||
}
|
||||
if command.Refund == nil || command.Refund.OrderID == 0 || command.SubmitterAccountID == 0 ||
|
||||
command.Refund.Creator != command.SubmitterAccountID || strings.TrimSpace(command.Refund.RefundNo) == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
account, err := s.loadSubmitter(ctx, command.SubmitterAccountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
preparation, err := s.approval.Prepare(ctx, approvalapp.PrepareRequest{
|
||||
BusinessType: constants.ApprovalBusinessTypeRefund, SubmitterAccountID: command.SubmitterAccountID,
|
||||
CorrelationID: command.Refund.RefundNo,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
submitterSnapshot, requestSnapshot, err := refundSnapshots(command.Refund, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var approvalStatus int
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Exec("SELECT pg_advisory_xact_lock(?)", int64(command.Refund.OrderID)).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "锁定退款订单申请边界失败")
|
||||
}
|
||||
var activeCount int64
|
||||
if err := tx.WithContext(ctx).Model(&model.RefundRequest{}).
|
||||
Where("order_id = ? AND status IN ?", command.Refund.OrderID, []int{model.RefundStatusPending, model.RefundStatusApproved}).
|
||||
Count(&activeCount).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "复核订单活跃退款申请失败")
|
||||
}
|
||||
if activeCount > 0 {
|
||||
return errors.New(errors.CodeConflict, "该订单已存在退款申请")
|
||||
}
|
||||
if err := tx.WithContext(ctx).Create(command.Refund).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建退款申请失败")
|
||||
}
|
||||
reference, err := s.approval.CreateInTx(ctx, tx, approvalapp.CreateRequest{
|
||||
Preparation: preparation, BusinessType: constants.ApprovalBusinessTypeRefund,
|
||||
BusinessID: command.Refund.ID, SubmitterAccountID: command.SubmitterAccountID,
|
||||
SubmitterSnapshot: submitterSnapshot, RequestSnapshot: requestSnapshot,
|
||||
CorrelationID: command.Refund.RefundNo,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result := tx.WithContext(ctx).Model(&model.RefundRequest{}).
|
||||
Where("id = ? AND approval_instance_id IS NULL", command.Refund.ID).
|
||||
Update("approval_instance_id", reference.InstanceID)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "关联退款审批实例失败")
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return errors.New(errors.CodeConflict, "退款审批实例关联已变化")
|
||||
}
|
||||
command.Refund.ApprovalInstanceID = &reference.InstanceID
|
||||
approvalStatus = reference.Status
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &CreateResult{Refund: command.Refund, SubmitterName: account.Username, ApprovalStatus: approvalStatus}, nil
|
||||
}
|
||||
|
||||
func (s *CreationService) loadSubmitter(ctx context.Context, accountID uint) (*model.Account, error) {
|
||||
var account model.Account
|
||||
if err := s.db.WithContext(ctx).Where("id = ? AND status = ?", accountID, constants.StatusEnabled).First(&account).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeForbidden, "退款提交人账号不可用")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询退款提交人失败")
|
||||
}
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
func refundSnapshots(refund *model.RefundRequest, account *model.Account) ([]byte, []byte, error) {
|
||||
submitterSnapshot, err := sonic.Marshal(map[string]any{
|
||||
"account_id": account.ID, "account_name": account.Username, "user_type": account.UserType,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(errors.CodeInternalError, err, "编码退款提交人快照失败")
|
||||
}
|
||||
requestSnapshot, err := sonic.Marshal(map[string]any{
|
||||
constants.ApprovalFieldRefundNo: refund.RefundNo,
|
||||
constants.ApprovalFieldOrderID: refund.OrderID,
|
||||
constants.ApprovalFieldOrderNo: refund.OrderNo,
|
||||
constants.ApprovalFieldAssetIdentifier: refund.AssetIdentifier,
|
||||
constants.ApprovalFieldAssetType: refund.OrderType,
|
||||
constants.ApprovalFieldActualReceivedAmount: formatCentAmount(refund.ActualReceivedAmount),
|
||||
constants.ApprovalFieldRequestedRefundAmount: formatCentAmount(refund.RequestedRefundAmount),
|
||||
constants.ApprovalFieldRefundVoucherKey: []string(refund.RefundVoucherKey),
|
||||
constants.ApprovalFieldRefundReason: refund.RefundReason,
|
||||
constants.ApprovalFieldPackageUsageID: refund.PackageUsageID,
|
||||
constants.ApprovalFieldSubmitterID: account.ID,
|
||||
constants.ApprovalFieldSubmitterName: account.Username,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(errors.CodeInternalError, err, "编码退款审批业务快照失败")
|
||||
}
|
||||
return submitterSnapshot, requestSnapshot, nil
|
||||
}
|
||||
|
||||
func formatCentAmount(amount int64) string {
|
||||
return fmt.Sprintf("%d.%02d", amount/100, amount%100)
|
||||
}
|
||||
@@ -219,7 +219,8 @@ func newShopResponse(shop *model.Shop, parentName string) *dto.ShopResponse {
|
||||
ParentShopName: parentName, Level: shop.Level, ContactName: shop.ContactName,
|
||||
ContactPhone: shop.ContactPhone, Province: shop.Province, City: shop.City,
|
||||
District: shop.District, Address: shop.Address, Status: shop.Status,
|
||||
StatusName: constants.GetStatusName(shop.Status), CreatedAt: shop.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
ClientLoginDisabled: shop.ClientLoginDisabled,
|
||||
StatusName: constants.GetStatusName(shop.Status), CreatedAt: shop.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: shop.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ func (s *UpdateService) Update(ctx context.Context, shopID uint, request *dto.Up
|
||||
if userType == constants.UserTypeAgent && request.BusinessOwnerAccountIDSet {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限设置店铺业务员")
|
||||
}
|
||||
if userType == constants.UserTypeAgent && request.ClientLoginDisabled != nil && middleware.GetShopIDFromContext(ctx) != shopID {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限修改其他店铺的C端登录限制")
|
||||
}
|
||||
if userType != constants.UserTypeSuperAdmin && userType != constants.UserTypePlatform && userType != constants.UserTypeAgent {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
@@ -59,6 +62,9 @@ func (s *UpdateService) Update(ctx context.Context, shopID uint, request *dto.Up
|
||||
}
|
||||
shop.BusinessOwnerAccountID = ownerID
|
||||
}
|
||||
if request.ClientLoginDisabled != nil {
|
||||
shop.ClientLoginDisabled = *request.ClientLoginDisabled
|
||||
}
|
||||
shop.ShopName = request.ShopName
|
||||
shop.ContactName = request.ContactName
|
||||
shop.ContactPhone = request.ContactPhone
|
||||
|
||||
303
internal/application/wecom/connection.go
Normal file
303
internal/application/wecom/connection.go
Normal file
@@ -0,0 +1,303 @@
|
||||
// Package wecom 提供企业微信配置简单写与连接测试用例。
|
||||
package wecom
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdErrors "errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
systemconfigapp "github.com/break/junhong_cmp_fiber/internal/application/systemconfig"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
)
|
||||
|
||||
// ApplicationRepository 定义企业微信应用配置持久化边界。
|
||||
type ApplicationRepository interface {
|
||||
FindByIdentityForUpdate(ctx context.Context, tx *gorm.DB, corpID string, agentID int64) (*model.WeComApplication, error)
|
||||
Create(ctx context.Context, tx *gorm.DB, application *model.WeComApplication) error
|
||||
Update(ctx context.Context, tx *gorm.DB, application *model.WeComApplication) error
|
||||
List(ctx context.Context, page, pageSize int) ([]model.WeComApplication, int64, error)
|
||||
GetEnabled(ctx context.Context, applicationID uint) (*model.WeComApplication, error)
|
||||
UpdateDefaultCreator(ctx context.Context, tx *gorm.DB, applicationID uint, userID, name string, operatorID uint, updatedAt time.Time) error
|
||||
}
|
||||
|
||||
// DefaultCreatorMemberFinder 定义默认审批发起人的可见成员查询边界。
|
||||
type DefaultCreatorMemberFinder interface {
|
||||
GetVisible(ctx context.Context, applicationID uint, userID string) (*model.WeComMember, error)
|
||||
}
|
||||
|
||||
// CredentialCipher 定义企业微信敏感凭据加密边界。
|
||||
type CredentialCipher interface {
|
||||
Encrypt(plaintext string) ([]byte, error)
|
||||
Decrypt(ciphertext []byte) (string, error)
|
||||
}
|
||||
|
||||
// AccessTokenProvider 定义按应用取得及失效 access_token 的边界。
|
||||
type AccessTokenProvider interface {
|
||||
GetAccessToken(ctx context.Context, applicationID uint) (string, error)
|
||||
Invalidate(ctx context.Context, applicationID uint)
|
||||
}
|
||||
|
||||
// ConnectionService 保存加密配置并测试企业微信连接。
|
||||
type ConnectionService struct {
|
||||
db *gorm.DB
|
||||
repo ApplicationRepository
|
||||
cipher CredentialCipher
|
||||
tokens AccessTokenProvider
|
||||
audit systemconfigapp.AuditWriter
|
||||
members DefaultCreatorMemberFinder
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
// SetDefaultCreatorMemberFinder 注入默认审批发起人的可见成员查询边界。
|
||||
func (s *ConnectionService) SetDefaultCreatorMemberFinder(finder DefaultCreatorMemberFinder) {
|
||||
s.members = finder
|
||||
}
|
||||
|
||||
// NewConnectionService 创建企业微信连接用例。
|
||||
func NewConnectionService(db *gorm.DB, repo ApplicationRepository, cipher CredentialCipher, tokens AccessTokenProvider, audit systemconfigapp.AuditWriter) *ConnectionService {
|
||||
return &ConnectionService{db: db, repo: repo, cipher: cipher, tokens: tokens, audit: audit, now: time.Now}
|
||||
}
|
||||
|
||||
// Save 创建或更新企业微信应用,并保证数据库仅保存密文凭据。
|
||||
func (s *ConnectionService) Save(ctx context.Context, request dto.SaveWeComApplicationRequest) (*dto.WeComApplicationResponse, error) {
|
||||
if s == nil || s.db == nil || s.repo == nil || s.cipher == nil {
|
||||
return nil, errors.New(errors.CodeWeComCredentialInvalid)
|
||||
}
|
||||
if middleware.GetUserTypeFromContext(ctx) != constants.UserTypeSuperAdmin {
|
||||
return nil, errors.New(errors.CodeForbidden)
|
||||
}
|
||||
operatorID := middleware.GetUserIDFromContext(ctx)
|
||||
if operatorID == 0 {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
secret, err := s.cipher.Encrypt(request.Secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
callbackToken, err := s.cipher.Encrypt(request.CallbackToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encodingAESKey, err := s.cipher.Encrypt(request.EncodingAESKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := s.now().UTC()
|
||||
var saved *model.WeComApplication
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Exec("SELECT pg_advisory_xact_lock(hashtext(?))", fmt.Sprintf("wecom:%s:%d", request.CorpID, request.AgentID)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
existing, findErr := s.repo.FindByIdentityForUpdate(ctx, tx, request.CorpID, request.AgentID)
|
||||
if findErr != nil {
|
||||
return findErr
|
||||
}
|
||||
before := map[string]any{"configured": false}
|
||||
if existing == nil {
|
||||
existing = &model.WeComApplication{
|
||||
Model: gorm.Model{CreatedAt: now},
|
||||
CorpID: request.CorpID,
|
||||
AgentID: request.AgentID,
|
||||
CreatedBy: operatorID,
|
||||
}
|
||||
} else {
|
||||
before = applicationAuditSnapshot(existing)
|
||||
}
|
||||
existing.Name = request.Name
|
||||
existing.SecretCiphertext = secret
|
||||
existing.CallbackTokenCiphertext = callbackToken
|
||||
existing.EncodingAESKeyCiphertext = encodingAESKey
|
||||
existing.Status = request.Status
|
||||
existing.UpdatedBy = operatorID
|
||||
existing.UpdatedAt = now
|
||||
if existing.ID == 0 {
|
||||
if err := s.repo.Create(ctx, tx, existing); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := s.repo.Update(ctx, tx, existing); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.audit != nil {
|
||||
requestID := ""
|
||||
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
|
||||
requestID = *value
|
||||
}
|
||||
if err := s.audit.WriteConfigChange(ctx, tx, systemconfigapp.ChangeAudit{
|
||||
OperatorID: operatorID, OperationType: "wecom_application_save", Description: "保存企业微信应用安全配置",
|
||||
ConfigKey: fmt.Sprintf("wecom.application.%d", existing.ID), BeforeData: before,
|
||||
AfterData: applicationAuditSnapshot(existing), RequestID: requestID, CorrelationID: requestID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
saved = existing
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
var appErr *errors.AppError
|
||||
if stdErrors.As(err, &appErr) {
|
||||
return nil, appErr
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "保存企业微信应用配置失败")
|
||||
}
|
||||
if s.tokens != nil {
|
||||
s.tokens.Invalidate(ctx, saved.ID)
|
||||
}
|
||||
response := toApplicationResponse(*saved, request.Secret, request.CallbackToken, request.EncodingAESKey)
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// List 返回企业微信应用列表,并向超级管理员返回可直接编辑的明文凭据。
|
||||
func (s *ConnectionService) List(ctx context.Context, request dto.WeComApplicationListRequest) (*dto.WeComApplicationListResponse, error) {
|
||||
if s == nil || s.repo == nil || s.cipher == nil {
|
||||
return nil, errors.New(errors.CodeWeComCredentialInvalid)
|
||||
}
|
||||
if middleware.GetUserTypeFromContext(ctx) != constants.UserTypeSuperAdmin {
|
||||
return nil, errors.New(errors.CodeForbidden)
|
||||
}
|
||||
if request.Page <= 0 {
|
||||
request.Page = constants.DefaultPage
|
||||
}
|
||||
if request.PageSize <= 0 {
|
||||
request.PageSize = constants.DefaultPageSize
|
||||
}
|
||||
if request.PageSize > constants.MaxPageSize {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
applications, total, err := s.repo.List(ctx, request.Page, request.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]dto.WeComApplicationResponse, 0, len(applications))
|
||||
for _, application := range applications {
|
||||
secret, err := s.cipher.Decrypt(application.SecretCiphertext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
callbackToken, err := s.cipher.Decrypt(application.CallbackTokenCiphertext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encodingAESKey, err := s.cipher.Decrypt(application.EncodingAESKeyCiphertext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, toApplicationResponse(application, secret, callbackToken, encodingAESKey))
|
||||
}
|
||||
return &dto.WeComApplicationListResponse{
|
||||
Items: result, Total: total, Page: request.Page, PageSize: request.PageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Test 强制失效旧缓存后取得一次 access_token,但绝不向调用方返回 token。
|
||||
func (s *ConnectionService) Test(ctx context.Context, applicationID uint) error {
|
||||
if s == nil {
|
||||
return errors.New(errors.CodeServiceUnavailable, "企业微信连接服务未配置")
|
||||
}
|
||||
if middleware.GetUserTypeFromContext(ctx) != constants.UserTypeSuperAdmin {
|
||||
return errors.New(errors.CodeForbidden)
|
||||
}
|
||||
if s.tokens == nil {
|
||||
return errors.New(errors.CodeServiceUnavailable, "企业微信连接服务未配置")
|
||||
}
|
||||
s.tokens.Invalidate(ctx, applicationID)
|
||||
_, err := s.tokens.GetAccessToken(ctx, applicationID)
|
||||
return err
|
||||
}
|
||||
|
||||
// SaveDefaultCreator 从应用当前可见成员中保存代理等账号使用的默认审批发起人。
|
||||
func (s *ConnectionService) SaveDefaultCreator(ctx context.Context, applicationID uint, request dto.SaveWeComDefaultCreatorRequest) (*dto.WeComApplicationResponse, error) {
|
||||
if s == nil || s.db == nil || s.repo == nil || s.cipher == nil || s.members == nil {
|
||||
return nil, errors.New(errors.CodeServiceUnavailable, "企业微信默认审批发起人服务未配置")
|
||||
}
|
||||
if middleware.GetUserTypeFromContext(ctx) != constants.UserTypeSuperAdmin {
|
||||
return nil, errors.New(errors.CodeForbidden)
|
||||
}
|
||||
operatorID := middleware.GetUserIDFromContext(ctx)
|
||||
if applicationID == 0 || operatorID == 0 {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
application, err := s.repo.GetEnabled(ctx, applicationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
member, err := s.members.GetVisible(ctx, applicationID, request.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := s.now().UTC()
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
before := applicationAuditSnapshot(application)
|
||||
if err := s.repo.UpdateDefaultCreator(ctx, tx, applicationID, member.UserID, member.Name, operatorID, now); err != nil {
|
||||
return err
|
||||
}
|
||||
application.DefaultCreatorUserID = member.UserID
|
||||
application.DefaultCreatorName = member.Name
|
||||
application.UpdatedBy = operatorID
|
||||
application.UpdatedAt = now
|
||||
if s.audit != nil {
|
||||
requestID := ""
|
||||
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
|
||||
requestID = *value
|
||||
}
|
||||
return s.audit.WriteConfigChange(ctx, tx, systemconfigapp.ChangeAudit{
|
||||
OperatorID: operatorID, OperationType: "wecom_default_creator_save", Description: "保存企业微信默认审批发起人",
|
||||
ConfigKey: fmt.Sprintf("wecom.application.%d.default_creator", applicationID), BeforeData: before,
|
||||
AfterData: applicationAuditSnapshot(application), RequestID: requestID, CorrelationID: requestID,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
var appErr *errors.AppError
|
||||
if stdErrors.As(err, &appErr) {
|
||||
return nil, appErr
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "保存企业微信默认审批发起人失败")
|
||||
}
|
||||
secret, err := s.cipher.Decrypt(application.SecretCiphertext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
callbackToken, err := s.cipher.Decrypt(application.CallbackTokenCiphertext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encodingAESKey, err := s.cipher.Decrypt(application.EncodingAESKeyCiphertext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := toApplicationResponse(*application, secret, callbackToken, encodingAESKey)
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func applicationAuditSnapshot(application *model.WeComApplication) map[string]any {
|
||||
return map[string]any{
|
||||
"id": application.ID, "corp_id": application.CorpID, "agent_id": application.AgentID,
|
||||
"name": application.Name, "status": application.Status, "credentials_configured": true,
|
||||
"default_creator_userid": application.DefaultCreatorUserID,
|
||||
"default_creator_name": application.DefaultCreatorName,
|
||||
}
|
||||
}
|
||||
|
||||
func toApplicationResponse(application model.WeComApplication, secret, callbackToken, encodingAESKey string) dto.WeComApplicationResponse {
|
||||
statusName := "禁用"
|
||||
if application.Status == constants.StatusEnabled {
|
||||
statusName = "启用"
|
||||
}
|
||||
return dto.WeComApplicationResponse{
|
||||
ID: application.ID, CorpID: application.CorpID, AgentID: application.AgentID, Name: application.Name,
|
||||
Secret: secret, CallbackToken: callbackToken, EncodingAESKey: encodingAESKey,
|
||||
DefaultCreatorUserID: application.DefaultCreatorUserID, DefaultCreatorName: application.DefaultCreatorName,
|
||||
Status: application.Status, StatusName: statusName,
|
||||
CredentialsSet: len(application.SecretCiphertext) > 0 && len(application.CallbackTokenCiphertext) > 0 && len(application.EncodingAESKeyCiphertext) > 0,
|
||||
LastConnectedAt: application.LastConnectedAt, CreatedAt: application.CreatedAt, UpdatedAt: application.UpdatedAt,
|
||||
}
|
||||
}
|
||||
129
internal/application/wecom/directory.go
Normal file
129
internal/application/wecom/directory.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package wecom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
)
|
||||
|
||||
// DirectoryMember 是企微 Adapter 交给 Application 的最小成员快照。
|
||||
type DirectoryMember struct {
|
||||
UserID string
|
||||
Name string
|
||||
DepartmentIDs []int64
|
||||
}
|
||||
|
||||
// DirectoryProvider 定义拉取应用可见成员的外部端口。
|
||||
type DirectoryProvider interface {
|
||||
ListVisibleMembers(ctx context.Context, applicationID uint) ([]DirectoryMember, error)
|
||||
}
|
||||
|
||||
// MemberRepository 定义可见成员快照同步和分页查询边界。
|
||||
type MemberRepository interface {
|
||||
ReplaceVisible(ctx context.Context, applicationID uint, members []model.WeComMember, syncedAt time.Time) error
|
||||
ListVisible(ctx context.Context, applicationID uint, page, pageSize int, keyword string) ([]model.WeComMember, int64, error)
|
||||
}
|
||||
|
||||
// DirectoryService 同步并分页查询企业微信应用可见成员。
|
||||
type DirectoryService struct {
|
||||
applications interface {
|
||||
GetEnabled(ctx context.Context, applicationID uint) (*model.WeComApplication, error)
|
||||
}
|
||||
provider DirectoryProvider
|
||||
members MemberRepository
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
// NewDirectoryService 创建企业微信通讯录同步用例。
|
||||
func NewDirectoryService(applications interface {
|
||||
GetEnabled(ctx context.Context, applicationID uint) (*model.WeComApplication, error)
|
||||
}, provider DirectoryProvider, members MemberRepository) *DirectoryService {
|
||||
return &DirectoryService{applications: applications, provider: provider, members: members, now: time.Now}
|
||||
}
|
||||
|
||||
// Sync 拉取并替换指定应用当前可见成员快照。
|
||||
func (s *DirectoryService) Sync(ctx context.Context, applicationID uint) (*dto.WeComMemberSyncResponse, error) {
|
||||
if s == nil || s.applications == nil || s.provider == nil || s.members == nil || applicationID == 0 {
|
||||
return nil, errors.New(errors.CodeServiceUnavailable, "企业微信通讯录服务未配置")
|
||||
}
|
||||
if !canManageWeComDirectory(ctx) {
|
||||
return nil, errors.New(errors.CodeForbidden)
|
||||
}
|
||||
application, err := s.applications.GetEnabled(ctx, applicationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
remoteMembers, err := s.provider.ListVisibleMembers(ctx, applicationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
syncedAt := s.now().UTC()
|
||||
members := make([]model.WeComMember, 0, len(remoteMembers))
|
||||
for _, member := range remoteMembers {
|
||||
departments, err := sonic.Marshal(member.DepartmentIDs)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "序列化企业微信成员部门失败")
|
||||
}
|
||||
members = append(members, model.WeComMember{
|
||||
ApplicationID: applicationID, CorpID: application.CorpID, UserID: member.UserID,
|
||||
Name: member.Name, DepartmentIDs: departments, Visible: true, SyncedAt: syncedAt,
|
||||
CreatedAt: syncedAt, UpdatedAt: syncedAt,
|
||||
})
|
||||
}
|
||||
if err := s.members.ReplaceVisible(ctx, applicationID, members, syncedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.WeComMemberSyncResponse{ApplicationID: applicationID, SyncedCount: len(members), SyncedAt: syncedAt}, nil
|
||||
}
|
||||
|
||||
// List 分页返回本地最近一次同步的应用可见成员。
|
||||
func (s *DirectoryService) List(ctx context.Context, applicationID uint, request dto.WeComMemberListRequest) (*dto.WeComMemberListResponse, error) {
|
||||
if s == nil || s.applications == nil || s.members == nil || applicationID == 0 {
|
||||
return nil, errors.New(errors.CodeServiceUnavailable, "企业微信通讯录服务未配置")
|
||||
}
|
||||
if !canManageWeComDirectory(ctx) {
|
||||
return nil, errors.New(errors.CodeForbidden)
|
||||
}
|
||||
if request.Page <= 0 {
|
||||
request.Page = constants.DefaultPage
|
||||
}
|
||||
if request.PageSize <= 0 {
|
||||
request.PageSize = constants.DefaultPageSize
|
||||
}
|
||||
if request.PageSize > constants.MaxPageSize {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
if _, err := s.applications.GetEnabled(ctx, applicationID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
members, total, err := s.members.ListVisible(ctx, applicationID, request.Page, request.PageSize, request.Keyword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]dto.WeComMemberResponse, 0, len(members))
|
||||
for _, member := range members {
|
||||
var departmentIDs []int64
|
||||
if len(member.DepartmentIDs) > 0 {
|
||||
if err := sonic.Unmarshal(member.DepartmentIDs, &departmentIDs); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "解析企业微信成员部门失败")
|
||||
}
|
||||
}
|
||||
items = append(items, dto.WeComMemberResponse{
|
||||
ApplicationID: member.ApplicationID, CorpID: member.CorpID, UserID: member.UserID,
|
||||
Name: member.Name, DepartmentIDs: departmentIDs, SyncedAt: member.SyncedAt,
|
||||
})
|
||||
}
|
||||
return &dto.WeComMemberListResponse{Items: items, Total: total, Page: request.Page, PageSize: request.PageSize}, nil
|
||||
}
|
||||
|
||||
func canManageWeComDirectory(ctx context.Context) bool {
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
return userType == constants.UserTypeSuperAdmin || userType == constants.UserTypePlatform
|
||||
}
|
||||
322
internal/application/wecom/scene.go
Normal file
322
internal/application/wecom/scene.go
Normal file
@@ -0,0 +1,322 @@
|
||||
package wecom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
stdErrors "errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"gorm.io/gorm"
|
||||
|
||||
systemconfigapp "github.com/break/junhong_cmp_fiber/internal/application/systemconfig"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
)
|
||||
|
||||
// TemplateControl 是模板详情中可供业务映射的最小控件结构。
|
||||
type TemplateControl struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Required bool `json:"required"`
|
||||
OptionKeys []string `json:"option_keys"`
|
||||
}
|
||||
|
||||
// TemplateDefinition 是企微 Adapter 返回的模板最小结构。
|
||||
type TemplateDefinition struct {
|
||||
TemplateID string `json:"template_id"`
|
||||
Name string `json:"name"`
|
||||
Controls []TemplateControl `json:"controls"`
|
||||
}
|
||||
|
||||
// TemplateProvider 定义审批模板详情外部端口。
|
||||
type TemplateProvider interface {
|
||||
GetTemplateDetail(ctx context.Context, applicationID uint, templateID string) (TemplateDefinition, error)
|
||||
}
|
||||
|
||||
// SceneRepository 定义审批场景当前配置持久化边界。
|
||||
type SceneRepository interface {
|
||||
FindForUpdate(ctx context.Context, tx *gorm.DB, businessType string) (*model.WeComApprovalScene, error)
|
||||
Create(ctx context.Context, tx *gorm.DB, scene *model.WeComApprovalScene) error
|
||||
Update(ctx context.Context, tx *gorm.DB, scene *model.WeComApprovalScene) error
|
||||
List(ctx context.Context, page, pageSize int) ([]model.WeComApprovalScene, int64, error)
|
||||
}
|
||||
|
||||
// SceneService 保存经企微模板详情校验的业务场景当前映射。
|
||||
type SceneService struct {
|
||||
db *gorm.DB
|
||||
provider TemplateProvider
|
||||
repo SceneRepository
|
||||
audit systemconfigapp.AuditWriter
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
// NewSceneService 创建企业微信审批场景配置用例。
|
||||
func NewSceneService(db *gorm.DB, provider TemplateProvider, repo SceneRepository, audit systemconfigapp.AuditWriter) *SceneService {
|
||||
return &SceneService{db: db, provider: provider, repo: repo, audit: audit, now: time.Now}
|
||||
}
|
||||
|
||||
// Save 校验模板控件后创建或替换指定稳定业务场景映射。
|
||||
func (s *SceneService) Save(ctx context.Context, businessType string, request dto.SaveWeComApprovalSceneRequest) (*dto.WeComApprovalSceneResponse, error) {
|
||||
if s == nil || s.db == nil || s.provider == nil || s.repo == nil {
|
||||
return nil, errors.New(errors.CodeServiceUnavailable, "企业微信审批场景服务未配置")
|
||||
}
|
||||
if middleware.GetUserTypeFromContext(ctx) != constants.UserTypeSuperAdmin {
|
||||
return nil, errors.New(errors.CodeForbidden)
|
||||
}
|
||||
businessType = strings.TrimSpace(businessType)
|
||||
if !validApprovalBusinessType(businessType) {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "不支持的审批业务类型")
|
||||
}
|
||||
if request.ApplicationID == 0 || strings.TrimSpace(request.TemplateID) == "" || len(request.ControlMapping) == 0 ||
|
||||
(request.Status != constants.StatusDisabled && request.Status != constants.StatusEnabled) {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
definition, err := s.provider.GetTemplateDetail(ctx, request.ApplicationID, strings.TrimSpace(request.TemplateID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateSceneMapping(businessType, request.ControlMapping, definition.Controls); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request.ControlMapping = normalizeSceneMapping(request.ControlMapping)
|
||||
mappingJSON, err := sonic.Marshal(request.ControlMapping)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "编码企业微信控件映射失败")
|
||||
}
|
||||
snapshotJSON, err := sonic.Marshal(definition)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "编码企业微信模板快照失败")
|
||||
}
|
||||
fingerprintBytes := sha256.Sum256(snapshotJSON)
|
||||
fingerprint := hex.EncodeToString(fingerprintBytes[:])
|
||||
operatorID := middleware.GetUserIDFromContext(ctx)
|
||||
if operatorID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized)
|
||||
}
|
||||
now := s.now().UTC()
|
||||
var saved *model.WeComApprovalScene
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Exec("SELECT pg_advisory_xact_lock(hashtext(?))", "wecom-scene:"+businessType).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
existing, err := s.repo.FindForUpdate(ctx, tx, businessType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
before := map[string]any{"configured": false, "business_type": businessType}
|
||||
if existing == nil {
|
||||
existing = &model.WeComApprovalScene{BusinessType: businessType, CreatedBy: operatorID, CreatedAt: now}
|
||||
} else {
|
||||
before = sceneAuditSnapshot(existing)
|
||||
}
|
||||
existing.ApplicationID = request.ApplicationID
|
||||
existing.TemplateID = definition.TemplateID
|
||||
existing.TemplateName = definition.Name
|
||||
existing.ControlMapping = mappingJSON
|
||||
existing.TemplateSnapshot = snapshotJSON
|
||||
existing.TemplateFingerprint = fingerprint
|
||||
existing.Status = request.Status
|
||||
existing.LastVerifiedAt = now
|
||||
existing.UpdatedBy = operatorID
|
||||
existing.UpdatedAt = now
|
||||
if existing.ID == 0 {
|
||||
if err := s.repo.Create(ctx, tx, existing); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := s.repo.Update(ctx, tx, existing); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.audit != nil {
|
||||
requestID := ""
|
||||
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
|
||||
requestID = *value
|
||||
}
|
||||
if err := s.audit.WriteConfigChange(ctx, tx, systemconfigapp.ChangeAudit{
|
||||
OperatorID: operatorID, OperationType: "wecom_approval_scene_save", Description: "保存企业微信审批模板映射",
|
||||
ConfigKey: "wecom.approval_scene." + businessType, BeforeData: before,
|
||||
AfterData: sceneAuditSnapshot(existing), RequestID: requestID, CorrelationID: requestID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
saved = existing
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
var appErr *errors.AppError
|
||||
if stdErrors.As(err, &appErr) {
|
||||
return nil, appErr
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "保存企业微信审批场景失败")
|
||||
}
|
||||
return sceneResponse(*saved)
|
||||
}
|
||||
|
||||
// List 分页查询企业微信审批场景当前配置。
|
||||
func (s *SceneService) List(ctx context.Context, request dto.WeComApprovalSceneListRequest) (*dto.WeComApprovalSceneListResponse, error) {
|
||||
if s == nil || s.repo == nil {
|
||||
return nil, errors.New(errors.CodeServiceUnavailable, "企业微信审批场景服务未配置")
|
||||
}
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
if userType != constants.UserTypeSuperAdmin && userType != constants.UserTypePlatform {
|
||||
return nil, errors.New(errors.CodeForbidden)
|
||||
}
|
||||
if request.Page <= 0 {
|
||||
request.Page = constants.DefaultPage
|
||||
}
|
||||
if request.PageSize <= 0 {
|
||||
request.PageSize = constants.DefaultPageSize
|
||||
}
|
||||
if request.PageSize > constants.MaxPageSize {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
scenes, total, err := s.repo.List(ctx, request.Page, request.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]dto.WeComApprovalSceneResponse, 0, len(scenes))
|
||||
for _, scene := range scenes {
|
||||
item, err := sceneResponse(scene)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, *item)
|
||||
}
|
||||
return &dto.WeComApprovalSceneListResponse{Items: items, Total: total, Page: request.Page, PageSize: request.PageSize}, nil
|
||||
}
|
||||
|
||||
func validateSceneMapping(businessType string, mapping []dto.WeComControlMappingItem, controls []TemplateControl) error {
|
||||
controlByID := make(map[string]TemplateControl, len(controls))
|
||||
for _, control := range controls {
|
||||
controlByID[control.ID] = control
|
||||
}
|
||||
mappedControls := make(map[string]struct{}, len(mapping))
|
||||
businessFields := make(map[string]struct{}, len(mapping))
|
||||
for _, item := range mapping {
|
||||
item.BusinessField = strings.TrimSpace(item.BusinessField)
|
||||
item.ControlID = strings.TrimSpace(item.ControlID)
|
||||
if item.BusinessField == "" || item.ControlID == "" {
|
||||
return errors.New(errors.CodeInvalidParam, "企业微信控件映射字段不能为空")
|
||||
}
|
||||
if !allowedSceneBusinessField(businessType, item.BusinessField) {
|
||||
return errors.New(errors.CodeInvalidParam, "企业微信控件映射包含当前业务不支持的字段")
|
||||
}
|
||||
if _, exists := businessFields[item.BusinessField]; exists {
|
||||
return errors.New(errors.CodeInvalidParam, "企业微信业务字段映射重复")
|
||||
}
|
||||
if _, exists := mappedControls[item.ControlID]; exists {
|
||||
return errors.New(errors.CodeInvalidParam, "企业微信模板控件不能重复映射")
|
||||
}
|
||||
control, exists := controlByID[item.ControlID]
|
||||
if !exists || !strings.EqualFold(control.Type, strings.TrimSpace(item.ControlType)) {
|
||||
return errors.New(errors.CodeInvalidParam, "企业微信模板控件 ID 或类型已失效")
|
||||
}
|
||||
validOptions := make(map[string]struct{}, len(control.OptionKeys))
|
||||
for _, key := range control.OptionKeys {
|
||||
validOptions[key] = struct{}{}
|
||||
}
|
||||
for _, key := range item.OptionMapping {
|
||||
if _, exists := validOptions[key]; !exists {
|
||||
return errors.New(errors.CodeInvalidParam, "企业微信模板选择项 key 已失效")
|
||||
}
|
||||
}
|
||||
businessFields[item.BusinessField] = struct{}{}
|
||||
mappedControls[item.ControlID] = struct{}{}
|
||||
}
|
||||
for _, control := range controls {
|
||||
if control.Required {
|
||||
if _, exists := mappedControls[control.ID]; !exists {
|
||||
return errors.New(errors.CodeInvalidParam, "企业微信模板存在未映射的必填控件: "+control.Title)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func allowedSceneBusinessField(businessType, businessField string) bool {
|
||||
allowed := map[string]struct{}{}
|
||||
switch businessType {
|
||||
case constants.ApprovalBusinessTypeOfflineRecharge:
|
||||
allowed = map[string]struct{}{
|
||||
constants.ApprovalFieldRechargeNo: {},
|
||||
constants.ApprovalFieldShopID: {},
|
||||
constants.ApprovalFieldShopName: {},
|
||||
constants.ApprovalFieldAmount: {},
|
||||
constants.ApprovalFieldAmountCent: {},
|
||||
constants.ApprovalFieldPaymentVoucherKey: {},
|
||||
constants.ApprovalFieldRemark: {},
|
||||
constants.ApprovalFieldSubmitterID: {},
|
||||
constants.ApprovalFieldSubmitterName: {},
|
||||
}
|
||||
case constants.ApprovalBusinessTypeRefund:
|
||||
allowed = map[string]struct{}{
|
||||
constants.ApprovalFieldRefundNo: {},
|
||||
constants.ApprovalFieldOrderID: {},
|
||||
constants.ApprovalFieldOrderNo: {},
|
||||
constants.ApprovalFieldAssetIdentifier: {},
|
||||
constants.ApprovalFieldAssetType: {},
|
||||
constants.ApprovalFieldActualReceivedAmount: {},
|
||||
constants.ApprovalFieldRequestedRefundAmount: {},
|
||||
constants.ApprovalFieldRefundVoucherKey: {},
|
||||
constants.ApprovalFieldRefundReason: {},
|
||||
constants.ApprovalFieldPackageUsageID: {},
|
||||
constants.ApprovalFieldSubmitterID: {},
|
||||
constants.ApprovalFieldSubmitterName: {},
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
_, exists := allowed[businessField]
|
||||
return exists
|
||||
}
|
||||
|
||||
func normalizeSceneMapping(mapping []dto.WeComControlMappingItem) []dto.WeComControlMappingItem {
|
||||
result := make([]dto.WeComControlMappingItem, 0, len(mapping))
|
||||
for _, item := range mapping {
|
||||
item.BusinessField = strings.TrimSpace(item.BusinessField)
|
||||
item.ControlID = strings.TrimSpace(item.ControlID)
|
||||
item.ControlType = strings.TrimSpace(item.ControlType)
|
||||
if item.OptionMapping == nil {
|
||||
item.OptionMapping = map[string]string{}
|
||||
}
|
||||
result = append(result, item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func validApprovalBusinessType(businessType string) bool {
|
||||
return businessType == constants.ApprovalBusinessTypeRefund || businessType == constants.ApprovalBusinessTypeOfflineRecharge
|
||||
}
|
||||
|
||||
func sceneAuditSnapshot(scene *model.WeComApprovalScene) map[string]any {
|
||||
return map[string]any{
|
||||
"business_type": scene.BusinessType, "application_id": scene.ApplicationID,
|
||||
"template_id": scene.TemplateID, "status": scene.Status, "last_verified_at": scene.LastVerifiedAt,
|
||||
"template_fingerprint": scene.TemplateFingerprint,
|
||||
}
|
||||
}
|
||||
|
||||
func sceneResponse(scene model.WeComApprovalScene) (*dto.WeComApprovalSceneResponse, error) {
|
||||
var mapping []dto.WeComControlMappingItem
|
||||
if err := sonic.Unmarshal(scene.ControlMapping, &mapping); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "解析企业微信控件映射失败")
|
||||
}
|
||||
name := "员工线下代充值审批"
|
||||
if scene.BusinessType == constants.ApprovalBusinessTypeRefund {
|
||||
name = "退款审批"
|
||||
}
|
||||
return &dto.WeComApprovalSceneResponse{
|
||||
ID: scene.ID, BusinessType: scene.BusinessType, BusinessTypeName: name,
|
||||
ApplicationID: scene.ApplicationID, TemplateID: scene.TemplateID, TemplateName: scene.TemplateName,
|
||||
TemplateFingerprint: scene.TemplateFingerprint,
|
||||
ControlMapping: mapping, Status: scene.Status, StatusName: constants.GetStatusName(scene.Status),
|
||||
LastVerifiedAt: scene.LastVerifiedAt, UpdatedAt: scene.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user