新增代理注册申请管理
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m41s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m41s
This commit is contained in:
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
)
|
||||
|
||||
// RegistrationResult 返回已落库的待审批注册记录与审批实例引用。
|
||||
@@ -33,24 +34,86 @@ type RegistrationService struct {
|
||||
db *gorm.DB
|
||||
verifier VerificationCodeVerifier
|
||||
approval approvalapp.Port
|
||||
recovery approvalapp.RecoveryPort
|
||||
audit AuditWriter
|
||||
}
|
||||
|
||||
// NewRegistrationService 创建公开扫码注册用例。
|
||||
func NewRegistrationService(
|
||||
db *gorm.DB,
|
||||
verifier VerificationCodeVerifier,
|
||||
approval approvalapp.Port,
|
||||
audit AuditWriter,
|
||||
) *RegistrationService {
|
||||
func NewRegistrationService(db *gorm.DB, verifier VerificationCodeVerifier, approval approvalapp.Port, audit AuditWriter) *RegistrationService {
|
||||
return &RegistrationService{db: db, verifier: verifier, approval: approval, audit: audit}
|
||||
}
|
||||
|
||||
// Register 创建待审批注册记录。
|
||||
// 分销码无效、上级店铺停用、上级店铺缺少启用的主账号、短信验证码无效分别返回各自的错误码与提示。
|
||||
// 短信验证码只在注册记录与审批实例落库成功后消费:落库前的任何失败都不消费验证码,重试无需重新获取。
|
||||
// 手机号、用户名或店铺编号与既有账号/店铺冲突时返回对应已存在错误码;
|
||||
// 与其它待审批注册记录冲突时返回资源冲突错误并指明冲突字段。
|
||||
// SetRecoveryPort 注入通用审批原实例恢复接缝。
|
||||
func (s *RegistrationService) SetRecoveryPort(recovery approvalapp.RecoveryPort) {
|
||||
s.recovery = recovery
|
||||
}
|
||||
|
||||
// RecoverApproval 恢复代理注册原审批提交事实,不创建新审批实例或业务实体。
|
||||
func (s *RegistrationService) RecoverApproval(ctx context.Context, id uint) error {
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
if userType == constants.UserTypeEnterprise || (userType != constants.UserTypeSuperAdmin && userType != constants.UserTypePlatform && userType != constants.UserTypeAgent) {
|
||||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
if middleware.GetUserIDFromContext(ctx) == 0 {
|
||||
return errors.New(errors.CodeUnauthorized)
|
||||
}
|
||||
if s == nil || s.db == nil || s.recovery == nil {
|
||||
return errors.New(errors.CodeServiceUnavailable, "代理注册审批恢复能力尚未配置")
|
||||
}
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
registration, err := lockRegistrationForUpdate(ctx, tx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if registration.Status != constants.AgentDistributionRegistrationStatusPending {
|
||||
return errors.New(errors.CodeConflict, "终态申请不可恢复")
|
||||
}
|
||||
var parent model.Shop
|
||||
if err := tx.Where("id = ?", registration.ParentShopID).First(&parent).Error; err != nil || parent.Status != constants.StatusEnabled {
|
||||
return errors.New(errors.CodeConflict, "上级店铺已停用,不能恢复审批")
|
||||
}
|
||||
if userType == constants.UserTypeAgent && (middleware.GetShopIDFromContext(ctx) == 0 || middleware.GetShopIDFromContext(ctx) != registration.ParentShopID) {
|
||||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
if registration.ApprovalInstanceID == nil || *registration.ApprovalInstanceID == 0 {
|
||||
return errors.New(errors.CodeConflict, "注册申请未关联审批实例")
|
||||
}
|
||||
instanceID := *registration.ApprovalInstanceID
|
||||
var instance model.ApprovalInstance
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND business_type = ? AND business_id = ?", instanceID, constants.ApprovalBusinessTypeAgentDistribution, registration.ID).First(&instance).Error; err != nil {
|
||||
return errors.New(errors.CodeConflict, "审批实例业务关联不一致")
|
||||
}
|
||||
var wc model.WeComApprovalContext
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("approval_instance_id = ? AND business_type = ?", instanceID, constants.ApprovalBusinessTypeAgentDistribution).First(&wc).Error; err != nil {
|
||||
return errors.New(errors.CodeConflict, "企业微信审批上下文不存在")
|
||||
}
|
||||
branch := "active"
|
||||
var recoveryErr error
|
||||
if wc.SPNo != "" {
|
||||
branch = "sp_no"
|
||||
recoveryErr = s.recovery.EnqueueSubmittedSync(ctx, tx, instanceID)
|
||||
} else if wc.SubmissionStatus == constants.WeComSubmissionStatusUnknown || instance.Status == constants.ApprovalStatusSubmissionUnknown {
|
||||
branch = "unknown"
|
||||
recoveryErr = s.recovery.EnqueueUnknownConfirm(ctx, tx, instanceID)
|
||||
} else if instance.Status == constants.ApprovalStatusSubmissionFailed && wc.SubmissionStatus == constants.WeComSubmissionStatusFailed {
|
||||
branch = "replay"
|
||||
_, recoveryErr = s.recovery.RecoverSubmissionEvent(ctx, tx, instanceID)
|
||||
} else if instance.Status == constants.ApprovalStatusSubmitting || instance.Status == constants.ApprovalStatusPending || wc.SubmissionStatus == constants.WeComSubmissionStatusReady || wc.SubmissionStatus == constants.WeComSubmissionStatusSending {
|
||||
} else {
|
||||
return errors.New(errors.CodeConflict, "当前审批提交状态不允许恢复")
|
||||
}
|
||||
if recoveryErr != nil {
|
||||
return errors.Wrap(errors.CodeConflict, recoveryErr, "恢复审批提交失败")
|
||||
}
|
||||
if s.audit != nil {
|
||||
if err := s.audit.WriteDistributionWithdrawal(ctx, tx, AuditChange{EventID: "agent-distribution:" + uintText(id) + ":recover:" + branch, ActionCode: constants.AuditActionApprovalSubmissionRecovered, Summary: "人工恢复代理注册审批提交", Registration: registration, ParentShop: &parent, BeforeData: map[string]any{"submission_status": wc.SubmissionStatus, "branch": branch}, AfterData: map[string]any{"result": "accepted"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// 已通过或已驳回的终态记录不阻塞重新注册;同一关键字段的并发提交由事务级 advisory lock 串行裁决。
|
||||
func (s *RegistrationService) Register(
|
||||
ctx context.Context,
|
||||
|
||||
@@ -267,7 +267,10 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
||||
WithdrawalQualification: admin.NewWithdrawalQualificationHandler(
|
||||
svc.WithdrawalQualification, distributionwithdrawalQuery.NewQuery(deps.DB), validate,
|
||||
),
|
||||
AgentDistribution: app.NewAgentDistributionHandler(svc.DistributionRegistration, validate),
|
||||
AgentDistribution: app.NewAgentDistributionHandler(svc.DistributionRegistration, validate),
|
||||
AgentDistributionRegistration: admin.NewAgentDistributionRegistrationHandler(
|
||||
svc.DistributionRegistration, distributionwithdrawalQuery.NewQuery(deps.DB), validate,
|
||||
),
|
||||
CommissionWithdrawal: admin.NewCommissionWithdrawalHandler(svc.CommissionWithdrawal, validate),
|
||||
CommissionWithdrawalSetting: admin.NewCommissionWithdrawalSettingHandler(svc.CommissionWithdrawalSetting),
|
||||
Enterprise: admin.NewEnterpriseHandler(svc.Enterprise),
|
||||
|
||||
@@ -422,6 +422,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
distributionRegistration := distributionwithdrawalApp.NewRegistrationService(
|
||||
deps.DB, deps.VerificationService, approvalCreationService, auditWriter,
|
||||
)
|
||||
distributionRegistration.SetRecoveryPort(recoveryPort)
|
||||
withdrawalQualification := distributionwithdrawalApp.NewQualificationService(
|
||||
deps.DB, approvalCreationService, auditWriter,
|
||||
)
|
||||
|
||||
@@ -11,86 +11,87 @@ import (
|
||||
)
|
||||
|
||||
type Handlers struct {
|
||||
Auth *authHandler.Handler
|
||||
Account *admin.AccountHandler
|
||||
Role *admin.RoleHandler
|
||||
Permission *admin.PermissionHandler
|
||||
PersonalCustomer *app.PersonalCustomerHandler
|
||||
ClientAuth *app.ClientAuthHandler
|
||||
ClientAsset *app.ClientAssetHandler
|
||||
ClientWallet *app.ClientWalletHandler
|
||||
ClientOrder *app.ClientOrderHandler
|
||||
ClientExchange *app.ClientExchangeHandler
|
||||
ClientRealname *app.ClientRealnameHandler
|
||||
ClientDevice *app.ClientDeviceHandler
|
||||
ClientRechargeOrder *app.ClientRechargeOrderHandler
|
||||
ClientNotification *app.ClientNotificationHandler
|
||||
ClientPopup *app.ClientPopupHandler
|
||||
Shop *admin.ShopHandler
|
||||
ShopRole *admin.ShopRoleHandler
|
||||
AdminAuth *admin.AuthHandler
|
||||
ShopCommission *admin.ShopCommissionHandler
|
||||
WithdrawalQualification *admin.WithdrawalQualificationHandler
|
||||
AgentDistribution *app.AgentDistributionHandler
|
||||
CommissionWithdrawal *admin.CommissionWithdrawalHandler
|
||||
CommissionWithdrawalSetting *admin.CommissionWithdrawalSettingHandler
|
||||
Enterprise *admin.EnterpriseHandler
|
||||
EnterpriseCard *admin.EnterpriseCardHandler
|
||||
EnterpriseDevice *admin.EnterpriseDeviceHandler
|
||||
Authorization *admin.AuthorizationHandler
|
||||
IotCard *admin.IotCardHandler
|
||||
IotCardImport *admin.IotCardImportHandler
|
||||
ExportTask *admin.ExportTaskHandler
|
||||
Notification *admin.NotificationHandler
|
||||
H5PopupConfiguration *admin.H5PopupConfigurationHandler
|
||||
Device *admin.DeviceHandler
|
||||
DeviceImport *admin.DeviceImportHandler
|
||||
AssetAllocationRecord *admin.AssetAllocationRecordHandler
|
||||
Storage *admin.StorageHandler
|
||||
Carrier *admin.CarrierHandler
|
||||
PackageSeries *admin.PackageSeriesHandler
|
||||
Package *admin.PackageHandler
|
||||
PackageUsage *admin.PackageUsageHandler
|
||||
ShopPackageBatchAllocation *admin.ShopPackageBatchAllocationHandler
|
||||
ShopPackageBatchPricing *admin.ShopPackageBatchPricingHandler
|
||||
ShopSeriesGrant *admin.ShopSeriesGrantHandler
|
||||
AdminOrder *admin.OrderHandler
|
||||
AdminExchange *admin.ExchangeHandler
|
||||
PaymentCallback *callback.PaymentHandler
|
||||
CTCCRealnameCallback *callback.CTCCRealnameHandler
|
||||
CMCCRealnameCallback *callback.CMCCRealnameHandler
|
||||
CUCCRealnameCallback *callback.CUCCRealnameHandler
|
||||
CUCCRealnameRemovalCallback *callback.CUCCRealnameRemovalHandler
|
||||
WeComApprovalCallback *callback.WeComApprovalHandler
|
||||
PollingConfig *admin.PollingConfigHandler
|
||||
PollingConcurrency *admin.PollingConcurrencyHandler
|
||||
PollingMonitoring *admin.PollingMonitoringHandler
|
||||
PollingAlert *admin.PollingAlertHandler
|
||||
PollingCleanup *admin.PollingCleanupHandler
|
||||
PollingManualTrigger *admin.PollingManualTriggerHandler
|
||||
PollingPriority *admin.PriorityPollingHandler
|
||||
Asset *admin.AssetHandler
|
||||
AssetLifecycle *admin.AssetLifecycleHandler
|
||||
AssetWallet *admin.AssetWalletHandler
|
||||
WechatConfig *admin.WechatConfigHandler
|
||||
PaymentMerchant *admin.PaymentMerchantHandler
|
||||
EmployeeCollection *admin.EmployeeCollectionHandler
|
||||
AgentRecharge *admin.AgentRechargeHandler
|
||||
Refund *admin.RefundHandler
|
||||
OrderPackageInvalidate *admin.OrderPackageInvalidateHandler
|
||||
AssetPackageBatchOrder *admin.AssetPackageBatchOrderHandler
|
||||
BusinessUserGroup *admin.BusinessUserGroupHandler
|
||||
ShopBusinessOwnerImport *admin.ShopBusinessOwnerImportHandler
|
||||
PhoneAssetAssociation *admin.PhoneAssetAssociationHandler
|
||||
PackageTrafficAlert *admin.PackageTrafficAlertHandler
|
||||
AssetAutoRenewal *admin.AssetAutoRenewalConfigHandler
|
||||
OperationsReport *admin.OperationsReportHandler
|
||||
ClientWechat *app.ClientWechatHandler
|
||||
SuperAdmin *admin.SuperAdminHandler
|
||||
SystemConfig *admin.SystemConfigHandler
|
||||
Audit *admin.AuditHandler
|
||||
WeCom *admin.WeComHandler
|
||||
AgentOpenAPI *openapiHandler.Handler
|
||||
Auth *authHandler.Handler
|
||||
Account *admin.AccountHandler
|
||||
Role *admin.RoleHandler
|
||||
Permission *admin.PermissionHandler
|
||||
PersonalCustomer *app.PersonalCustomerHandler
|
||||
ClientAuth *app.ClientAuthHandler
|
||||
ClientAsset *app.ClientAssetHandler
|
||||
ClientWallet *app.ClientWalletHandler
|
||||
ClientOrder *app.ClientOrderHandler
|
||||
ClientExchange *app.ClientExchangeHandler
|
||||
ClientRealname *app.ClientRealnameHandler
|
||||
ClientDevice *app.ClientDeviceHandler
|
||||
ClientRechargeOrder *app.ClientRechargeOrderHandler
|
||||
ClientNotification *app.ClientNotificationHandler
|
||||
ClientPopup *app.ClientPopupHandler
|
||||
Shop *admin.ShopHandler
|
||||
ShopRole *admin.ShopRoleHandler
|
||||
AdminAuth *admin.AuthHandler
|
||||
ShopCommission *admin.ShopCommissionHandler
|
||||
WithdrawalQualification *admin.WithdrawalQualificationHandler
|
||||
AgentDistribution *app.AgentDistributionHandler
|
||||
AgentDistributionRegistration *admin.AgentDistributionRegistrationHandler
|
||||
CommissionWithdrawal *admin.CommissionWithdrawalHandler
|
||||
CommissionWithdrawalSetting *admin.CommissionWithdrawalSettingHandler
|
||||
Enterprise *admin.EnterpriseHandler
|
||||
EnterpriseCard *admin.EnterpriseCardHandler
|
||||
EnterpriseDevice *admin.EnterpriseDeviceHandler
|
||||
Authorization *admin.AuthorizationHandler
|
||||
IotCard *admin.IotCardHandler
|
||||
IotCardImport *admin.IotCardImportHandler
|
||||
ExportTask *admin.ExportTaskHandler
|
||||
Notification *admin.NotificationHandler
|
||||
H5PopupConfiguration *admin.H5PopupConfigurationHandler
|
||||
Device *admin.DeviceHandler
|
||||
DeviceImport *admin.DeviceImportHandler
|
||||
AssetAllocationRecord *admin.AssetAllocationRecordHandler
|
||||
Storage *admin.StorageHandler
|
||||
Carrier *admin.CarrierHandler
|
||||
PackageSeries *admin.PackageSeriesHandler
|
||||
Package *admin.PackageHandler
|
||||
PackageUsage *admin.PackageUsageHandler
|
||||
ShopPackageBatchAllocation *admin.ShopPackageBatchAllocationHandler
|
||||
ShopPackageBatchPricing *admin.ShopPackageBatchPricingHandler
|
||||
ShopSeriesGrant *admin.ShopSeriesGrantHandler
|
||||
AdminOrder *admin.OrderHandler
|
||||
AdminExchange *admin.ExchangeHandler
|
||||
PaymentCallback *callback.PaymentHandler
|
||||
CTCCRealnameCallback *callback.CTCCRealnameHandler
|
||||
CMCCRealnameCallback *callback.CMCCRealnameHandler
|
||||
CUCCRealnameCallback *callback.CUCCRealnameHandler
|
||||
CUCCRealnameRemovalCallback *callback.CUCCRealnameRemovalHandler
|
||||
WeComApprovalCallback *callback.WeComApprovalHandler
|
||||
PollingConfig *admin.PollingConfigHandler
|
||||
PollingConcurrency *admin.PollingConcurrencyHandler
|
||||
PollingMonitoring *admin.PollingMonitoringHandler
|
||||
PollingAlert *admin.PollingAlertHandler
|
||||
PollingCleanup *admin.PollingCleanupHandler
|
||||
PollingManualTrigger *admin.PollingManualTriggerHandler
|
||||
PollingPriority *admin.PriorityPollingHandler
|
||||
Asset *admin.AssetHandler
|
||||
AssetLifecycle *admin.AssetLifecycleHandler
|
||||
AssetWallet *admin.AssetWalletHandler
|
||||
WechatConfig *admin.WechatConfigHandler
|
||||
PaymentMerchant *admin.PaymentMerchantHandler
|
||||
EmployeeCollection *admin.EmployeeCollectionHandler
|
||||
AgentRecharge *admin.AgentRechargeHandler
|
||||
Refund *admin.RefundHandler
|
||||
OrderPackageInvalidate *admin.OrderPackageInvalidateHandler
|
||||
AssetPackageBatchOrder *admin.AssetPackageBatchOrderHandler
|
||||
BusinessUserGroup *admin.BusinessUserGroupHandler
|
||||
ShopBusinessOwnerImport *admin.ShopBusinessOwnerImportHandler
|
||||
PhoneAssetAssociation *admin.PhoneAssetAssociationHandler
|
||||
PackageTrafficAlert *admin.PackageTrafficAlertHandler
|
||||
AssetAutoRenewal *admin.AssetAutoRenewalConfigHandler
|
||||
OperationsReport *admin.OperationsReportHandler
|
||||
ClientWechat *app.ClientWechatHandler
|
||||
SuperAdmin *admin.SuperAdminHandler
|
||||
SystemConfig *admin.SystemConfigHandler
|
||||
Audit *admin.AuditHandler
|
||||
WeCom *admin.WeComHandler
|
||||
AgentOpenAPI *openapiHandler.Handler
|
||||
}
|
||||
|
||||
// Middlewares 封装所有中间件
|
||||
|
||||
70
internal/handler/admin/agent_distribution_registration.go
Normal file
70
internal/handler/admin/agent_distribution_registration.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
distributionapp "github.com/break/junhong_cmp_fiber/internal/application/distributionwithdrawal"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
distributionquery "github.com/break/junhong_cmp_fiber/internal/query/distributionwithdrawal"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// AgentDistributionRegistrationHandler 提供代理注册申请后台查询与原审批恢复。
|
||||
type AgentDistributionRegistrationHandler struct {
|
||||
service *distributionapp.RegistrationService
|
||||
query *distributionquery.Query
|
||||
validator *validator.Validate
|
||||
}
|
||||
|
||||
// NewAgentDistributionRegistrationHandler 创建代理注册申请后台 Handler。
|
||||
func NewAgentDistributionRegistrationHandler(service *distributionapp.RegistrationService, query *distributionquery.Query, validate *validator.Validate) *AgentDistributionRegistrationHandler {
|
||||
return &AgentDistributionRegistrationHandler{service: service, query: query, validator: validate}
|
||||
}
|
||||
|
||||
// List 查询代理注册申请列表。
|
||||
// GET /api/admin/agent-distribution-registrations
|
||||
func (h *AgentDistributionRegistrationHandler) List(c *fiber.Ctx) error {
|
||||
var req dto.AgentDistributionRegistrationListReq
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
if h.validator != nil {
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "代理注册申请查询参数不合法")
|
||||
}
|
||||
}
|
||||
result, err := h.query.ListAgentDistributionRegistrations(c.UserContext(), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.SuccessWithPagination(c, result.Items, result.Total, result.Page, result.Size)
|
||||
}
|
||||
|
||||
// Get 查询代理注册申请详情。
|
||||
// GET /api/admin/agent-distribution-registrations/:id
|
||||
func (h *AgentDistributionRegistrationHandler) Get(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的注册申请 ID")
|
||||
}
|
||||
result, err := h.query.AgentDistributionRegistrationDetail(c.UserContext(), uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, result)
|
||||
}
|
||||
|
||||
// RecoverApproval 恢复代理注册原审批提交。
|
||||
// POST /api/admin/agent-distribution-registrations/:id/recover-approval
|
||||
func (h *AgentDistributionRegistrationHandler) RecoverApproval(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的注册申请 ID")
|
||||
}
|
||||
if err := h.service.RecoverApproval(c.UserContext(), uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, &dto.RecoverAgentDistributionRegistrationResp{ID: uint(id), Message: "已受理原审批恢复"})
|
||||
}
|
||||
@@ -26,11 +26,11 @@ func (r *RecoveryPort) EnqueueUnknownConfirm(ctx context.Context, tx *gorm.DB, i
|
||||
}
|
||||
func (r *RecoveryPort) enqueueSync(ctx context.Context, tx *gorm.DB, instanceID uint) error {
|
||||
var instance model.ApprovalInstance
|
||||
if err := tx.WithContext(ctx).Where("id = ? AND business_type = ?", instanceID, constants.ApprovalBusinessTypeRefund).First(&instance).Error; err != nil {
|
||||
if err := tx.WithContext(ctx).First(&instance, instanceID).Error; err != nil || !recoverableBusinessType(instance.BusinessType) {
|
||||
return gorm.ErrInvalidData
|
||||
}
|
||||
var context model.WeComApprovalContext
|
||||
if err := tx.WithContext(ctx).Where("approval_instance_id = ? AND business_type = ?", instanceID, constants.ApprovalBusinessTypeRefund).First(&context).Error; err != nil || context.SPNo == "" || context.SubmissionStatus != constants.WeComSubmissionStatusSubmitted {
|
||||
if err := tx.WithContext(ctx).Where("approval_instance_id = ? AND business_type = ?", instanceID, instance.BusinessType).First(&context).Error; err != nil || context.SPNo == "" || context.SubmissionStatus != constants.WeComSubmissionStatusSubmitted {
|
||||
return gorm.ErrInvalidData
|
||||
}
|
||||
id := strconv.FormatUint(uint64(instanceID), 10)
|
||||
@@ -40,11 +40,11 @@ func (r *RecoveryPort) enqueueSync(ctx context.Context, tx *gorm.DB, instanceID
|
||||
}
|
||||
func (r *RecoveryPort) enqueueUnknown(ctx context.Context, tx *gorm.DB, instanceID uint) error {
|
||||
var instance model.ApprovalInstance
|
||||
if err := tx.WithContext(ctx).Where("id = ? AND business_type = ?", instanceID, constants.ApprovalBusinessTypeRefund).First(&instance).Error; err != nil || instance.Status != constants.ApprovalStatusSubmissionUnknown {
|
||||
if err := tx.WithContext(ctx).Where("id = ?", instanceID).First(&instance).Error; err != nil || !recoverableBusinessType(instance.BusinessType) || instance.Status != constants.ApprovalStatusSubmissionUnknown {
|
||||
return gorm.ErrInvalidData
|
||||
}
|
||||
var context model.WeComApprovalContext
|
||||
if err := tx.WithContext(ctx).Where("approval_instance_id = ? AND business_type = ?", instanceID, constants.ApprovalBusinessTypeRefund).First(&context).Error; err != nil || context.SubmissionStatus != constants.WeComSubmissionStatusUnknown || context.SPNo != "" {
|
||||
if err := tx.WithContext(ctx).Where("approval_instance_id = ? AND business_type = ?", instanceID, instance.BusinessType).First(&context).Error; err != nil || context.SubmissionStatus != constants.WeComSubmissionStatusUnknown || context.SPNo != "" {
|
||||
return gorm.ErrInvalidData
|
||||
}
|
||||
id := strconv.FormatUint(uint64(instanceID), 10)
|
||||
@@ -59,11 +59,11 @@ func (r *RecoveryPort) RecoverSubmissionEvent(ctx context.Context, tx *gorm.DB,
|
||||
if err := tx.WithContext(ctx).First(&instance, instanceID).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
if instance.Status != constants.ApprovalStatusSubmissionFailed {
|
||||
if !recoverableBusinessType(instance.BusinessType) || instance.Status != constants.ApprovalStatusSubmissionFailed {
|
||||
return false, gorm.ErrInvalidData
|
||||
}
|
||||
var context model.WeComApprovalContext
|
||||
if err := tx.WithContext(ctx).Where("approval_instance_id = ?", instanceID).First(&context).Error; err != nil || context.SubmissionStatus != constants.WeComSubmissionStatusFailed || context.SPNo != "" {
|
||||
if err := tx.WithContext(ctx).Where("approval_instance_id = ? AND business_type = ?", instanceID, instance.BusinessType).First(&context).Error; err != nil || context.SubmissionStatus != constants.WeComSubmissionStatusFailed || context.SPNo != "" {
|
||||
return false, gorm.ErrInvalidData
|
||||
}
|
||||
eventID := "approval:" + strconv.FormatUint(uint64(instanceID), 10) + ":submission"
|
||||
@@ -86,4 +86,8 @@ func (r *RecoveryPort) RecoverSubmissionEvent(ctx context.Context, tx *gorm.DB,
|
||||
return result.RowsAffected == 1, result.Error
|
||||
}
|
||||
|
||||
func recoverableBusinessType(value string) bool {
|
||||
return value == constants.ApprovalBusinessTypeRefund || value == constants.ApprovalBusinessTypeAgentDistribution
|
||||
}
|
||||
|
||||
var _ approvalapp.RecoveryPort = (*RecoveryPort)(nil)
|
||||
|
||||
@@ -153,3 +153,68 @@ type ShopWithdrawalRequestDetailResp struct {
|
||||
AnomalyReason string `json:"anomaly_reason,omitempty" description:"异常原因,供人工处理"`
|
||||
Attempts []WithdrawalRequestAttemptItem `json:"attempts" description:"审批尝试记录,按提交次序倒序;历史尝试与审批结果不被覆盖"`
|
||||
}
|
||||
|
||||
// AgentDistributionRegistrationListReq 代理注册申请列表查询请求。
|
||||
type AgentDistributionRegistrationListReq struct {
|
||||
Page int `query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码(默认1)"`
|
||||
PageSize int `query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量(默认20,最大100)"`
|
||||
Status *int `query:"status" validate:"omitempty,oneof=0 1 2" enum:"0,1,2" description:"注册业务状态 (0:待审批, 1:已通过, 2:已驳回)"`
|
||||
StartTime string `query:"start_time" description:"申请开始时间(RFC3339,闭区间)"`
|
||||
EndTime string `query:"end_time" description:"申请结束时间(RFC3339,闭区间)"`
|
||||
DecidedStartTime string `query:"decided_start_time" description:"决定开始时间(RFC3339,闭区间)"`
|
||||
DecidedEndTime string `query:"decided_end_time" description:"决定结束时间(RFC3339,闭区间)"`
|
||||
Phone string `query:"phone" validate:"omitempty,len=11" description:"手机号精确值"`
|
||||
Keyword string `query:"keyword" validate:"omitempty,max=100" maxLength:"100" description:"店铺名称、编号、用户名或联系人关键字"`
|
||||
ParentShopID *uint `query:"parent_shop_id" validate:"omitempty,min=1" description:"直接上级店铺ID(仅平台账号可用)"`
|
||||
SubmissionAnomaly *bool `query:"submission_anomaly" description:"是否仅筛选提交失败或结果未知"`
|
||||
}
|
||||
|
||||
// AgentDistributionRegistrationItem 代理注册申请后台投影。
|
||||
// 该投影不包含密码哈希、验证码及企业微信内部响应。
|
||||
type AgentDistributionRegistrationItem struct {
|
||||
ID uint `json:"id" description:"注册申请ID"`
|
||||
Status int `json:"status" description:"注册业务状态 (0:待审批, 1:已通过, 2:已驳回)"`
|
||||
StatusName string `json:"status_name" description:"注册业务状态名称"`
|
||||
ShopName string `json:"shop_name" description:"申请店铺名称"`
|
||||
ShopCode string `json:"shop_code" description:"申请店铺编号"`
|
||||
Username string `json:"username" description:"代理主账号用户名"`
|
||||
ContactName string `json:"contact_name" description:"联系人"`
|
||||
Phone string `json:"phone" description:"完整注册手机号"`
|
||||
Province string `json:"province" description:"省份"`
|
||||
City string `json:"city" description:"城市"`
|
||||
District string `json:"district" description:"区县"`
|
||||
Address string `json:"address" description:"详细注册地址"`
|
||||
ParentShopID uint `json:"parent_shop_id" description:"直接上级店铺ID"`
|
||||
ParentShopName string `json:"parent_shop_name" description:"直接上级店铺名称"`
|
||||
DistributionCode string `json:"distribution_code" description:"分销码快照"`
|
||||
CreatedAt string `json:"created_at" description:"申请时间"`
|
||||
DecidedAt string `json:"decided_at,omitempty" description:"决定时间"`
|
||||
RejectReason string `json:"reject_reason,omitempty" description:"驳回原因"`
|
||||
ApprovalInstanceID uint `json:"approval_instance_id" description:"通用审批实例ID"`
|
||||
ApprovalStatus int `json:"approval_status" description:"通用审批状态"`
|
||||
ApprovalStatusName string `json:"approval_status_name" description:"通用审批状态名称"`
|
||||
SubmissionStatus int `json:"submission_status" description:"企微提交状态 (0:待提交, 1:发送中, 2:已提交, 3:失败, 4:结果未知)"`
|
||||
SubmissionStatusName string `json:"submission_status_name" description:"企微提交状态名称"`
|
||||
FailureSummary string `json:"failure_summary,omitempty" description:"安全失败摘要"`
|
||||
Recoverable bool `json:"recoverable" description:"是否允许恢复原审批"`
|
||||
LastRecoveryAt string `json:"last_recovery_at,omitempty" description:"最近恢复时间"`
|
||||
}
|
||||
|
||||
// AgentDistributionRegistrationPageResult 代理注册申请分页响应。
|
||||
type AgentDistributionRegistrationPageResult struct {
|
||||
Items []AgentDistributionRegistrationItem `json:"items" description:"代理注册申请列表"`
|
||||
Total int64 `json:"total" description:"总记录数"`
|
||||
Page int `json:"page" description:"当前页码"`
|
||||
Size int `json:"size" description:"每页数量"`
|
||||
}
|
||||
|
||||
// AgentDistributionRegistrationDetailReq 代理注册申请详情路径参数。
|
||||
type AgentDistributionRegistrationDetailReq struct {
|
||||
ID uint `json:"-" params:"id" path:"id" validate:"required" description:"注册申请ID"`
|
||||
}
|
||||
|
||||
// RecoverAgentDistributionRegistrationResp 代理注册审批恢复响应。
|
||||
type RecoverAgentDistributionRegistrationResp struct {
|
||||
ID uint `json:"id" description:"注册申请ID"`
|
||||
Message string `json:"message" description:"恢复处理结果"`
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ package distributionwithdrawal
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
@@ -13,6 +15,8 @@ import (
|
||||
"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"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/utils"
|
||||
)
|
||||
|
||||
// Query 提供提现资料资格与提现申请详情的只读投影。
|
||||
@@ -286,3 +290,149 @@ func decodeAttemptAccount(payload []byte) (string, string) {
|
||||
}
|
||||
return info["account_name"], info["account_number"]
|
||||
}
|
||||
|
||||
// ListAgentDistributionRegistrations 查询代理注册申请列表,并按调用方固定数据范围过滤。
|
||||
func (q *Query) ListAgentDistributionRegistrations(ctx context.Context, req *dto.AgentDistributionRegistrationListReq) (*dto.AgentDistributionRegistrationPageResult, error) {
|
||||
if q == nil || q.db == nil {
|
||||
return nil, errors.New(errors.CodeInternalError, "代理注册申请查询能力未配置")
|
||||
}
|
||||
if req == nil {
|
||||
req = &dto.AgentDistributionRegistrationListReq{}
|
||||
}
|
||||
page, size := req.Page, req.PageSize
|
||||
if page <= 0 {
|
||||
page = constants.DefaultPage
|
||||
}
|
||||
if size <= 0 {
|
||||
size = constants.DefaultPageSize
|
||||
}
|
||||
if size > constants.MaxPageSize {
|
||||
size = constants.MaxPageSize
|
||||
}
|
||||
query, err := q.registrationScope(ctx, req.ParentShopID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.Status != nil {
|
||||
query = query.Where("r.status = ?", *req.Status)
|
||||
}
|
||||
if req.Phone != "" {
|
||||
query = query.Where("r.phone = ?", req.Phone)
|
||||
}
|
||||
if keyword := strings.TrimSpace(req.Keyword); keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
query = query.Where("(r.shop_name ILIKE ? OR r.shop_code ILIKE ? OR r.username ILIKE ? OR r.contact_name ILIKE ?)", like, like, like, like)
|
||||
}
|
||||
for _, f := range []struct{ start, end, columnStart, columnEnd string }{{req.StartTime, req.EndTime, "r.created_at >= ?", "r.created_at <= ?"}, {req.DecidedStartTime, req.DecidedEndTime, "r.decided_at >= ?", "r.decided_at <= ?"}} {
|
||||
start, end, err := utils.ParseTimeRange(f.start, f.end)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if start != nil {
|
||||
query = query.Where(f.columnStart, *start)
|
||||
}
|
||||
if end != nil {
|
||||
query = query.Where(f.columnEnd, *end)
|
||||
}
|
||||
}
|
||||
if req.SubmissionAnomaly != nil && *req.SubmissionAnomaly {
|
||||
query = query.Where("(wc.submission_status IN ? OR ai.status IN ?)", []int{constants.WeComSubmissionStatusFailed, constants.WeComSubmissionStatusUnknown}, []int{constants.ApprovalStatusSubmissionFailed, constants.ApprovalStatusSubmissionUnknown})
|
||||
}
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "统计代理注册申请失败")
|
||||
}
|
||||
var rows []registrationProjection
|
||||
if err := query.Order("r.created_at DESC, r.id DESC").Offset((page - 1) * size).Limit(size).Find(&rows).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询代理注册申请失败")
|
||||
}
|
||||
items := make([]dto.AgentDistributionRegistrationItem, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, projectRegistration(row))
|
||||
}
|
||||
return &dto.AgentDistributionRegistrationPageResult{Items: items, Total: total, Page: page, Size: size}, nil
|
||||
}
|
||||
|
||||
// AgentDistributionRegistrationDetail 查询单笔代理注册申请详情。
|
||||
func (q *Query) AgentDistributionRegistrationDetail(ctx context.Context, id uint) (*dto.AgentDistributionRegistrationItem, error) {
|
||||
query, err := q.registrationScope(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var row registrationProjection
|
||||
if err := query.Where("r.id = ?", id).First(&row).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "代理注册申请不存在")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询代理注册申请详情失败")
|
||||
}
|
||||
item := projectRegistration(row)
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
type registrationProjection struct {
|
||||
ID uint `gorm:"column:id"`
|
||||
Status int `gorm:"column:status"`
|
||||
ShopName string `gorm:"column:shop_name"`
|
||||
ShopCode string `gorm:"column:shop_code"`
|
||||
Username string `gorm:"column:username"`
|
||||
ContactName string `gorm:"column:contact_name"`
|
||||
Phone string `gorm:"column:phone"`
|
||||
Province string `gorm:"column:province"`
|
||||
City string `gorm:"column:city"`
|
||||
District string `gorm:"column:district"`
|
||||
Address string `gorm:"column:address"`
|
||||
ParentShopID uint `gorm:"column:parent_shop_id"`
|
||||
ParentShopName string `gorm:"column:parent_shop_name"`
|
||||
DistributionCode string `gorm:"column:distribution_code"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
DecidedAt *time.Time `gorm:"column:decided_at"`
|
||||
RejectReason string `gorm:"column:reject_reason"`
|
||||
ApprovalInstanceID uint `gorm:"column:approval_instance_id"`
|
||||
ApprovalStatus int `gorm:"column:approval_status"`
|
||||
SubmissionStatus int `gorm:"column:submission_status"`
|
||||
FailureSummary string `gorm:"column:failure_summary"`
|
||||
LastRecoveryAt *time.Time `gorm:"column:last_recovery_at"`
|
||||
ParentShopStatus int `gorm:"column:parent_shop_status"`
|
||||
}
|
||||
|
||||
func (q *Query) registrationScope(ctx context.Context, parentShopFilter *uint) (*gorm.DB, error) {
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
if userType == constants.UserTypeEnterprise || (userType != constants.UserTypeSuperAdmin && userType != constants.UserTypePlatform && userType != constants.UserTypeAgent) {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限访问该资源或资源不存在")
|
||||
}
|
||||
query := q.db.WithContext(ctx).Table("tb_agent_distribution_registration r").Select("r.id, r.status, r.shop_name, r.shop_code, r.username, r.contact_name, r.phone, r.province, r.city, r.district, r.address, r.parent_shop_id, COALESCE(s.shop_name, '') AS parent_shop_name, r.distribution_code, r.created_at, r.decided_at, r.reject_reason, COALESCE(r.approval_instance_id, 0) AS approval_instance_id, COALESCE(ai.status, -1) AS approval_status, COALESCE(wc.submission_status, 0) AS submission_status, COALESCE(wc.last_error, '') AS failure_summary, wc.last_recovery_at, COALESCE(s.status, 0) AS parent_shop_status").Joins("LEFT JOIN tb_shop s ON s.id = r.parent_shop_id").Joins("LEFT JOIN tb_approval_instance ai ON ai.id = r.approval_instance_id AND ai.business_type = ?", constants.ApprovalBusinessTypeAgentDistribution).Joins("LEFT JOIN tb_wecom_approval_context wc ON wc.approval_instance_id = r.approval_instance_id AND wc.business_type = ?", constants.ApprovalBusinessTypeAgentDistribution)
|
||||
if userType == constants.UserTypeAgent {
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
if shopID == 0 {
|
||||
return query.Where("1 = 0"), nil
|
||||
}
|
||||
query = query.Where("r.parent_shop_id = ?", shopID)
|
||||
} else if parentShopFilter != nil {
|
||||
if userType != constants.UserTypeSuperAdmin && userType != constants.UserTypePlatform {
|
||||
return query, nil
|
||||
}
|
||||
query = query.Where("r.parent_shop_id = ?", *parentShopFilter)
|
||||
}
|
||||
return query, nil
|
||||
}
|
||||
|
||||
func projectRegistration(row registrationProjection) dto.AgentDistributionRegistrationItem {
|
||||
failureSummary := ""
|
||||
if strings.TrimSpace(row.FailureSummary) != "" {
|
||||
failureSummary = "审批提交失败,请检查审批模板与渠道状态"
|
||||
}
|
||||
item := dto.AgentDistributionRegistrationItem{ID: row.ID, Status: row.Status, StatusName: constants.GetAgentDistributionRegistrationStatusName(row.Status), ShopName: row.ShopName, ShopCode: row.ShopCode, Username: row.Username, ContactName: row.ContactName, Phone: row.Phone, Province: row.Province, City: row.City, District: row.District, Address: row.Address, ParentShopID: row.ParentShopID, ParentShopName: row.ParentShopName, DistributionCode: row.DistributionCode, CreatedAt: row.CreatedAt.Format("2006-01-02 15:04:05"), RejectReason: row.RejectReason, ApprovalInstanceID: row.ApprovalInstanceID, ApprovalStatus: row.ApprovalStatus, ApprovalStatusName: constants.GetApprovalStatusName(row.ApprovalStatus), SubmissionStatus: row.SubmissionStatus, FailureSummary: failureSummary}
|
||||
if row.DecidedAt != nil {
|
||||
item.DecidedAt = row.DecidedAt.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
if row.LastRecoveryAt != nil {
|
||||
item.LastRecoveryAt = row.LastRecoveryAt.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
item.SubmissionStatusName = map[int]string{0: "待提交", 1: "发送中", 2: "已提交", 3: "失败", 4: "结果未知"}[row.SubmissionStatus]
|
||||
if item.SubmissionStatusName == "" {
|
||||
item.SubmissionStatusName = "未知"
|
||||
}
|
||||
item.Recoverable = row.Status == constants.AgentDistributionRegistrationStatusPending && row.ParentShopStatus == 1 && row.ApprovalInstanceID > 0 && (row.ApprovalStatus == constants.ApprovalStatusSubmitting || row.ApprovalStatus == constants.ApprovalStatusPending || row.ApprovalStatus == constants.ApprovalStatusSubmissionFailed || row.ApprovalStatus == constants.ApprovalStatusSubmissionUnknown || row.SubmissionStatus == constants.WeComSubmissionStatusFailed || row.SubmissionStatus == constants.WeComSubmissionStatusUnknown)
|
||||
return item
|
||||
}
|
||||
|
||||
@@ -42,6 +42,9 @@ func RegisterAdminRoutes(router fiber.Router, handlers *bootstrap.Handlers, midd
|
||||
if handlers.ShopCommission != nil {
|
||||
registerShopCommissionRoutes(authGroup, handlers.ShopCommission, doc, basePath)
|
||||
}
|
||||
if handlers.AgentDistributionRegistration != nil {
|
||||
registerAgentDistributionRegistrationRoutes(authGroup, handlers.AgentDistributionRegistration, doc, basePath)
|
||||
}
|
||||
if handlers.WithdrawalQualification != nil {
|
||||
registerWithdrawalQualificationRoutes(authGroup, handlers.WithdrawalQualification, doc, basePath)
|
||||
}
|
||||
|
||||
18
internal/routes/agent_distribution_registration.go
Normal file
18
internal/routes/agent_distribution_registration.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/admin"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/openapi"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// registerAgentDistributionRegistrationRoutes 注册代理注册申请后台路由。
|
||||
func registerAgentDistributionRegistrationRoutes(router fiber.Router, handler *admin.AgentDistributionRegistrationHandler, doc *openapi.Generator, basePath string) {
|
||||
if handler == nil {
|
||||
return
|
||||
}
|
||||
Register(router, doc, basePath, "GET", "/agent-distribution-registrations", handler.List, RouteSpec{Summary: "查询代理注册申请列表", Description: "平台账号查看全部,代理仅查看直接上级店铺申请;返回完整手机号与注册地址,不返回密码哈希及内部集成字段。", Input: new(dto.AgentDistributionRegistrationListReq), Output: new(dto.AgentDistributionRegistrationPageResult), Tags: []string{"代理注册管理"}, Auth: true})
|
||||
Register(router, doc, basePath, "GET", "/agent-distribution-registrations/:id", handler.Get, RouteSpec{Summary: "查询代理注册申请详情", Input: new(dto.AgentDistributionRegistrationDetailReq), Output: new(dto.AgentDistributionRegistrationItem), Tags: []string{"代理注册管理"}, Auth: true})
|
||||
Register(router, doc, basePath, "POST", "/agent-distribution-registrations/:id/recover-approval", handler.RecoverApproval, RouteSpec{Summary: "恢复代理注册原审批", Input: new(dto.AgentDistributionRegistrationDetailReq), Output: new(dto.RecoverAgentDistributionRegistrationResp), Tags: []string{"代理注册管理"}, Auth: true})
|
||||
}
|
||||
Reference in New Issue
Block a user