This commit is contained in:
2026-03-13 15:51:59 +08:00
parent 4db2386bbf
commit 4e91f4cede
133 changed files with 19502 additions and 37 deletions

View File

@@ -0,0 +1,268 @@
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)
}