package service import ( "context" "crypto/rand" "encoding/hex" "errors" "log/slog" "time" "pay-bridge/internal/channel" "pay-bridge/internal/model" "pay-bridge/internal/repository" ) // merchantRepo 定义 MerchantService 所需的数据访问方法,便于测试时注入 mock type merchantRepo interface { Create(ctx context.Context, m *model.Merchant) error GetByMerchantID(ctx context.Context, merchantID string) (*model.Merchant, error) GetByMerchantIDAndAppID(ctx context.Context, merchantID, appID string) (*model.Merchant, error) UpdateStatus(ctx context.Context, merchantID string, status model.MerchantStatus, updates map[string]any) error List(ctx context.Context, status model.MerchantStatus, limit, offset int) ([]*model.Merchant, error) ListByAppID(ctx context.Context, appID string, status model.MerchantStatus, limit, offset int) ([]*model.Merchant, error) ListAnomalous(ctx context.Context) ([]*model.Merchant, error) CreateApplication(ctx context.Context, app *model.MerchantApplication) error GetLatestApplication(ctx context.Context, merchantID string) (*model.MerchantApplication, error) GetApprovedApplicationByChannel(ctx context.Context, merchantID, channelCode string) (*model.MerchantApplication, error) UpdateApplication(ctx context.Context, applicationID string, updates map[string]any) error } // MerchantService 商户进件与管理服务 type MerchantService struct { merchantRepo merchantRepo channelSvc *ChannelService } func NewMerchantService( merchantRepo *repository.MerchantRepository, channelSvc *ChannelService, ) *MerchantService { return &MerchantService{ merchantRepo: merchantRepo, channelSvc: channelSvc, } } func genApplicationID() string { b := make([]byte, 16) rand.Read(b) return "APP" + hex.EncodeToString(b)[:16] } // Apply 提交商户进件申请 // bizContent 为完整的入网申请业务参数(对应 001 文档的 biz_content 结构) func (s *MerchantService) Apply(ctx context.Context, merchantID, channelCode string, bizContent map[string]any) (string, error) { merchant, err := s.merchantRepo.GetByMerchantID(ctx, merchantID) if err != nil { return "", err } if merchant == nil { return "", errors.New("merchant not found") } if merchant.Status == model.MerchantStatusFrozen { return "", errors.New("merchant is frozen") } ch, err := s.channelSvc.GetChannel(ctx, "", channelCode) if err != nil { return "", err } resp, err := ch.MerchantApply(ctx, &channel.MerchantApplyReq{ MerchantID: merchantID, BizContent: bizContent, }) if err != nil { return "", err } applicationID := genApplicationID() app := &model.MerchantApplication{ ApplicationID: applicationID, MerchantID: merchantID, ChannelCode: channelCode, SubmitData: model.JSONMap(bizContent), AuditStatus: model.AuditStatusSubmitting, SubmittedAt: time.Now(), } // 持久化渠道返回的 request_no,用于后续查询/修改 if resp.RequestNo != "" { app.SubmitData["_channel_request_no"] = resp.RequestNo } if err := s.merchantRepo.CreateApplication(ctx, app); err != nil { return "", err } slog.InfoContext(ctx, "merchant application submitted", "merchant_id", merchantID, "application_id", applicationID, "channel_code", channelCode, "channel_request_no", resp.RequestNo, ) return applicationID, nil } // UploadFile 上传文件到指定渠道,返回渠道 file_id func (s *MerchantService) UploadFile(ctx context.Context, channelCode string, req *channel.UploadFileReq) (string, error) { ch, err := s.channelSvc.GetChannel(ctx, "", channelCode) if err != nil { return "", err } resp, err := ch.UploadFile(ctx, req) if err != nil { return "", err } return resp.FileID, nil } // QueryAuditStatus 查询进件审核状态 func (s *MerchantService) QueryAuditStatus(ctx context.Context, merchantID string) (*model.MerchantApplication, error) { app, err := s.merchantRepo.GetLatestApplication(ctx, merchantID) if err != nil { return nil, err } if app == nil { return nil, nil } // 如果仍在审核中,向渠道查询最新状态 if app.AuditStatus == model.AuditStatusSubmitting || app.AuditStatus == model.AuditStatusReviewing { // 从 submit_data 中读取渠道返回的 request_no channelRequestNo, _ := app.SubmitData["_channel_request_no"].(string) if channelRequestNo != "" { ch, err := s.channelSvc.GetChannel(ctx, "", app.ChannelCode) if err == nil { resp, err := ch.QueryMerchantStatus(ctx, channelRequestNo) if err == nil { merchant, _ := s.merchantRepo.GetByMerchantID(ctx, merchantID) s.syncMerchantStatus(ctx, merchantID, app.ApplicationID, merchant, resp) app, _ = s.merchantRepo.GetLatestApplication(ctx, merchantID) } } } } return app, nil } // syncMerchantStatus 同步渠道返回的审核状态到本地 func (s *MerchantService) syncMerchantStatus(ctx context.Context, merchantID, applicationID string, merchant *model.Merchant, resp *channel.MerchantStatusResp) { now := time.Now() appUpdates := map[string]any{} switch resp.Status { case "APPROVED": appUpdates["audit_status"] = model.AuditStatusApproved appUpdates["audited_at"] = now if resp.ChannelMerchantID != "" { appUpdates["channel_merchant_id"] = resp.ChannelMerchantID } s.merchantRepo.UpdateStatus(ctx, merchantID, model.MerchantStatusActive, nil) case "REJECTED": appUpdates["audit_status"] = model.AuditStatusRejected appUpdates["reject_reason"] = resp.RejectReason appUpdates["audited_at"] = now s.merchantRepo.UpdateStatus(ctx, merchantID, model.MerchantStatusRejected, nil) case "REVIEWING": appUpdates["audit_status"] = model.AuditStatusReviewing case "FROZEN": s.merchantRepo.UpdateStatus(ctx, merchantID, model.MerchantStatusFrozen, nil) } if len(appUpdates) > 0 { s.merchantRepo.UpdateApplication(ctx, applicationID, appUpdates) } } // GetChannelMerchantID 返回指定商户在指定渠道进件审核通过后的渠道商户ID // 若该商户未在该渠道进件或审核未通过,返回空字符串 func (s *MerchantService) GetChannelMerchantID(ctx context.Context, merchantID, channelCode string) (string, error) { app, err := s.merchantRepo.GetApprovedApplicationByChannel(ctx, merchantID, channelCode) if err != nil { return "", err } if app == nil { return "", nil } return app.ChannelMerchantID, nil } // CreateMerchantForApp 业务侧创建商户,强制绑定 appID func (s *MerchantService) CreateMerchantForApp(ctx context.Context, appID string, m *model.Merchant) error { m.AppID = appID return s.merchantRepo.Create(ctx, m) } // GetMerchantForApp 业务侧查询,校验 appID 归属 func (s *MerchantService) GetMerchantForApp(ctx context.Context, appID, merchantID string) (*model.Merchant, error) { m, err := s.merchantRepo.GetByMerchantIDAndAppID(ctx, merchantID, appID) if err != nil { return nil, err } if m == nil { return nil, errors.New("30001") // merchant not found } return m, nil } // ListMerchantsForApp 业务侧列表,只返回该 appID 下的商户 func (s *MerchantService) ListMerchantsForApp(ctx context.Context, appID string, status model.MerchantStatus, limit, offset int) ([]*model.Merchant, error) { return s.merchantRepo.ListByAppID(ctx, appID, status, limit, offset) } // ApplyForApp 业务侧进件,校验 appID 归属后委托 Apply func (s *MerchantService) ApplyForApp(ctx context.Context, appID, merchantID, channelCode string, bizContent map[string]any) (string, error) { if _, err := s.GetMerchantForApp(ctx, appID, merchantID); err != nil { return "", err } return s.Apply(ctx, merchantID, channelCode, bizContent) } // QueryAuditStatusForApp 业务侧查审核状态,校验 appID 归属 func (s *MerchantService) QueryAuditStatusForApp(ctx context.Context, appID, merchantID string) (*model.MerchantApplication, error) { if _, err := s.GetMerchantForApp(ctx, appID, merchantID); err != nil { return nil, err } return s.QueryAuditStatus(ctx, merchantID) } // CheckAnomalies 检查状态异常的商户(由 cron 调用) func (s *MerchantService) CheckAnomalies(ctx context.Context) error { merchants, err := s.merchantRepo.ListAnomalous(ctx) if err != nil { return err } slog.InfoContext(ctx, "anomalous merchants found", "count", len(merchants)) // 实际业务中可在此发送告警通知 return nil } // CreateMerchant 创建商户基础信息 func (s *MerchantService) CreateMerchant(ctx context.Context, m *model.Merchant) error { return s.merchantRepo.Create(ctx, m) } // GetMerchant 查询商户信息 func (s *MerchantService) GetMerchant(ctx context.Context, merchantID string) (*model.Merchant, error) { return s.merchantRepo.GetByMerchantID(ctx, merchantID) } // ListMerchants 查询商户列表 func (s *MerchantService) ListMerchants(ctx context.Context, status model.MerchantStatus, limit, offset int) ([]*model.Merchant, error) { return s.merchantRepo.List(ctx, status, limit, offset) } // FreezeMerchant 冻结商户 func (s *MerchantService) FreezeMerchant(ctx context.Context, merchantID string) error { return s.merchantRepo.UpdateStatus(ctx, merchantID, model.MerchantStatusFrozen, nil) } // UnfreezeMerchant 解冻商户 func (s *MerchantService) UnfreezeMerchant(ctx context.Context, merchantID string) error { return s.merchantRepo.UpdateStatus(ctx, merchantID, model.MerchantStatusActive, nil) }