269 lines
9.2 KiB
Go
269 lines
9.2 KiB
Go
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)
|
||
}
|