This commit is contained in:
2026-07-26 23:14:52 +08:00
parent 750be2a305
commit 7244df5ae1
40 changed files with 4774 additions and 260 deletions

View File

@@ -2,10 +2,14 @@ package services
import (
"context"
"errors"
"time"
"tyapi-server/internal/config"
"tyapi-server/internal/domains/api/entities"
repo "tyapi-server/internal/domains/api/repositories"
"gorm.io/gorm"
)
type ApiUserAggregateService interface {
@@ -30,6 +34,15 @@ func NewApiUserAggregateService(repo repo.ApiUserRepository, cfg *config.Config)
}
func (s *ApiUserAggregateServiceImpl) CreateApiUser(ctx context.Context, apiUserId string) error {
// 已存在则幂等成功,避免重复 INSERT 导致 PostgreSQL 事务中止SQLSTATE 25P02
existing, err := s.repo.FindByUserId(ctx, apiUserId)
if err == nil && existing != nil {
return nil
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
apiUser, err := entities.NewApiUser(apiUserId, s.cfg.Wallet.BalanceAlert.DefaultEnabled, s.cfg.Wallet.BalanceAlert.DefaultThreshold)
if err != nil {
return err

View File

@@ -3,6 +3,7 @@ package entities
import (
"errors"
"fmt"
"strings"
"time"
"tyapi-server/internal/domains/certification/entities/value_objects"
@@ -28,15 +29,17 @@ type Certification struct {
CompletedAt *time.Time `json:"completed_at,omitempty" comment:"认证完成时间"`
ContractFileCreatedAt *time.Time `json:"contract_file_created_at,omitempty" comment:"合同文件生成时间"`
// === e签宝相关信息 ===
// === 签署平台esign | fadada===
SignPlatform enums.SignPlatform `gorm:"type:varchar(20);index" json:"sign_platform,omitempty" comment:"签署平台: esign|fadada"`
// === 第三方认证/签署信息(字段名历史兼容,语义为平台无关)===
AuthFlowID string `gorm:"type:varchar(500)" json:"auth_flow_id,omitempty" comment:"企业认证流程ID"`
AuthURL string `gorm:"type:varchar(500)" json:"auth_url,omitempty" comment:"企业认证链接"`
AuthURL string `gorm:"type:text" json:"auth_url,omitempty" comment:"企业认证链接"`
ContractCode string `gorm:"type:varchar(100)" json:"contract_code,omitempty" comment:"合同/协议编号"`
ContractFileID string `gorm:"type:varchar(500)" json:"contract_file_id,omitempty" comment:"合同文件ID"`
EsignFlowID string `gorm:"type:varchar(500)" json:"esign_flow_id,omitempty" comment:"签署流程ID"`
ContractURL string `gorm:"type:varchar(500)" json:"contract_url,omitempty" comment:"合同文件访问链接"`
ContractSignURL string `gorm:"type:varchar(500)" json:"contract_sign_url,omitempty" comment:"合同签署链接"`
// ContractCode 合作协议编号(与电子合同模板控件 xybh 一致,签署完成后写入用户域合同)
ContractCode string `gorm:"type:varchar(255)" json:"contract_code,omitempty" comment:"合作协议编号"`
EsignFlowID string `gorm:"type:varchar(500)" json:"esign_flow_id,omitempty" comment:"签署流程ID(兼容字段名)"`
ContractURL string `gorm:"type:text" json:"contract_url,omitempty" comment:"合同文件访问链接(法大大下载链可能很长)"`
ContractSignURL string `gorm:"type:text" json:"contract_sign_url,omitempty" comment:"合同签署链接(短链/长链)"`
// === 失败信息 ===
FailureReason enums.FailureReason `gorm:"type:varchar(100)" json:"failure_reason,omitempty" comment:"失败原因"`
@@ -319,8 +322,12 @@ func (c *Certification) ApplyContract(EsignFlowID string, ContractSignURL string
if err := c.TransitionTo(enums.StatusContractApplied, enums.ActorTypeUser, c.UserID, "用户申请合同签署"); err != nil {
return err
}
c.EsignFlowID = EsignFlowID
c.ContractSignURL = ContractSignURL
if EsignFlowID != "" {
c.EsignFlowID = EsignFlowID
}
if ContractSignURL != "" {
c.ContractSignURL = ContractSignURL
}
now := time.Now()
c.ContractFileCreatedAt = &now
// 添加业务事件
@@ -333,7 +340,19 @@ func (c *Certification) ApplyContract(EsignFlowID string, ContractSignURL string
return nil
}
// SetContractCode 设置合作协议编号(首次生成合同时写入,后续换文件复用
// BindSignTask 企业认证完成后预创建签署任务(不改状态;乙方可先免签
func (c *Certification) BindSignTask(esignFlowID, contractSignURL string) {
if esignFlowID != "" {
c.EsignFlowID = esignFlowID
}
if contractSignURL != "" {
c.ContractSignURL = contractSignURL
}
now := time.Now()
c.ContractFileCreatedAt = &now
}
// SetContractCode 设置合同/协议编号
func (c *Certification) SetContractCode(code string) {
c.ContractCode = code
}
@@ -509,13 +528,114 @@ func (c *Certification) CompleteCertification() error {
return nil
}
// ResolvedSignPlatform 解析签署平台(空则默认 e签宝兼容历史数据
func (c *Certification) ResolvedSignPlatform() enums.SignPlatform {
if enums.IsValidSignPlatform(c.SignPlatform) {
return c.SignPlatform
}
return enums.DefaultSignPlatform()
}
// ResetAfterSignTaskCreateFailed 签署任务未创建却已到 enterprise_verified 时,回退到待选平台
func (c *Certification) ResetAfterSignTaskCreateFailed() error {
if c.Status != enums.StatusEnterpriseVerified {
return fmt.Errorf("当前状态 %s 无需回退", enums.GetStatusName(c.Status))
}
if strings.TrimSpace(c.EsignFlowID) != "" {
return fmt.Errorf("签署任务已存在,不能回退到选平台")
}
c.Status = enums.StatusInfoPendingReview
c.SignPlatform = ""
c.AuthURL = ""
c.AuthFlowID = ""
c.ContractFileID = ""
c.ContractURL = ""
c.ContractSignURL = ""
c.EsignFlowID = ""
c.EnterpriseVerifiedAt = nil
c.UpdatedAt = time.Now()
return nil
}
// ResetForResignContract 管理员触发重新认证:回退到 pending用户可重新填写企业信息并走完整入驻流程
// 不改动用户域认证标记、钱包、API Key、历史合同记录
func (c *Certification) ResetForResignContract(actorID, reason string) error {
switch c.Status {
case enums.StatusCompleted, enums.StatusContractSigned, enums.StatusContractApplied,
enums.StatusContractRejected, enums.StatusContractExpired,
enums.StatusEnterpriseVerified, enums.StatusInfoSubmitted, enums.StatusInfoPendingReview:
// 允许从已进入流程的各状态重置,供用户重新填写企业信息
default:
if c.Status == enums.StatusPending {
return fmt.Errorf("当前已是待提交企业信息状态,无需重置")
}
return fmt.Errorf("当前状态 %s 不允许重新认证", enums.GetStatusName(c.Status))
}
oldStatus := c.Status
now := time.Now()
c.Status = enums.StatusPending
c.SignPlatform = ""
c.AuthFlowID = ""
c.AuthURL = ""
c.ContractCode = ""
c.ContractFileID = ""
c.EsignFlowID = ""
c.ContractURL = ""
c.ContractSignURL = ""
c.InfoSubmittedAt = nil
c.EnterpriseVerifiedAt = nil
c.ContractAppliedAt = nil
c.ContractSignedAt = nil
c.CompletedAt = nil
c.ContractFileCreatedAt = nil
c.RetryCount = 0
c.clearFailureInfo()
c.updateTransitionAudit(enums.ActorTypeAdmin, actorID)
c.UpdatedAt = now
c.addDomainEvent(&CertificationStatusChangedEvent{
CertificationID: c.ID,
UserID: c.UserID,
FromStatus: oldStatus,
ToStatus: enums.StatusPending,
Actor: enums.ActorTypeAdmin,
ActorID: actorID,
Reason: reason,
TransitionedAt: now,
})
return nil
}
// SetSignPlatform 设置签署平台(选定后锁定)
func (c *Certification) SetSignPlatform(platform enums.SignPlatform) error {
if !enums.IsValidSignPlatform(platform) {
return fmt.Errorf("无效的签署平台: %s", platform)
}
if c.SignPlatform != "" && c.SignPlatform != platform {
return fmt.Errorf("签署平台已选择为 %s不可更改", enums.GetSignPlatformName(c.SignPlatform))
}
c.SignPlatform = platform
return nil
}
// ClearSignPlatform 清除已选签署平台(内部回退用)
func (c *Certification) ClearSignPlatform() {
c.SignPlatform = ""
}
// ================ 查询方法 ================
// GetDataByStatus 根据当前状态获取对应的数据
func (c *Certification) GetDataByStatus() map[string]interface{} {
data := map[string]interface{}{}
if c.SignPlatform != "" {
data["sign_platform"] = string(c.SignPlatform)
data["sign_platform_name"] = enums.GetSignPlatformName(c.SignPlatform)
}
switch c.Status {
case enums.StatusInfoPendingReview:
// 待审核,额外数据
// 待审核/待选平台,额外数据由应用层补充
case enums.StatusInfoSubmitted:
data["auth_url"] = c.AuthURL
case enums.StatusInfoRejected:
@@ -523,11 +643,24 @@ func (c *Certification) GetDataByStatus() map[string]interface{} {
data["failure_message"] = c.FailureMessage
case enums.StatusEnterpriseVerified:
data["ContractURL"] = c.ContractURL
data["contract_url"] = c.ContractURL
if c.EsignFlowID != "" {
data["esign_flow_id"] = c.EsignFlowID
data["sign_task_ready"] = true
}
case enums.StatusContractApplied:
data["contract_sign_url"] = c.ContractSignURL
data["ContractURL"] = c.ContractURL
data["contract_url"] = c.ContractURL
if c.EsignFlowID != "" {
data["esign_flow_id"] = c.EsignFlowID
}
case enums.StatusContractSigned:
case enums.StatusCompleted:
data["completed_at"] = c.CompletedAt
if c.ContractURL != "" {
data["contract_url"] = c.ContractURL
}
case enums.StatusContractRejected:
data["failure_reason"] = c.FailureReason
data["failure_message"] = c.FailureMessage

View File

@@ -15,7 +15,9 @@ type ContractInfo struct {
ContractFileID string `json:"contract_file_id"` // 合同文件ID
EsignFlowID string `json:"esign_flow_id"` // e签宝签署流程ID
ContractURL string `json:"contract_url"` // 合同文件访问链接
ContractSignURL string `json:"contract_sign_url"` // 合同签署链接
ContractSignURL string `json:"contract_sign_url"` // 合同签署链接iframe 长链,可能短期有效)
// ContractSignShortURL 短链(法大大不可 iframe可选
ContractSignShortURL string `json:"contract_sign_short_url,omitempty"`
// 合同元数据
ContractTitle string `json:"contract_title"` // 合同标题

View File

@@ -0,0 +1,36 @@
package enums
// SignPlatform 签署/认证平台
type SignPlatform string
const (
SignPlatformEsign SignPlatform = "esign"
SignPlatformFadada SignPlatform = "fadada"
)
// IsValidSignPlatform 是否为合法平台
func IsValidSignPlatform(p SignPlatform) bool {
switch p {
case SignPlatformEsign, SignPlatformFadada:
return true
default:
return false
}
}
// GetSignPlatformName 平台中文名
func GetSignPlatformName(p SignPlatform) string {
switch p {
case SignPlatformEsign:
return "e签宝"
case SignPlatformFadada:
return "法大大"
default:
return string(p)
}
}
// DefaultSignPlatform 历史数据默认平台
func DefaultSignPlatform() SignPlatform {
return SignPlatformEsign
}

View File

@@ -0,0 +1,152 @@
package ports
import (
"context"
"tyapi-server/internal/domains/certification/enums"
)
// EnterpriseAuthRequest 企业认证链接请求
type EnterpriseAuthRequest struct {
ClientCorpID string
ClientUserID string
CompanyName string
UnifiedSocialCode string
LegalPersonName string
LegalPersonID string
TransactorName string
TransactorMobile string
TransactorID string
}
// AuthLinkResult 企业认证链接结果
type AuthLinkResult struct {
AuthFlowID string
AuthURL string
AuthShortURL string
}
// OrgIdentityQuery 企业实名查询
type OrgIdentityQuery struct {
OrgName string
OrgIdentNo string
OpenCorpID string
ClientCorpID string
}
// ContractGenerateRequest 合同模板填单
type ContractGenerateRequest struct {
AgreementNo string
CompanyName string
UnifiedSocialCode string
EnterpriseAddress string
AuthorizedRepName string
SignDate string
FileName string
}
// ContractFileResult 合同文件结果
type ContractFileResult struct {
FileID string
FileDownloadURL string
}
// SignFlowCreateRequest 创建签署流程
type SignFlowCreateRequest struct {
Subject string
FileID string
DocName string
PartyAOpenCorpID string
PartyAName string
PartyAUSCC string
TransactorName string
TransactorMobile string
TransactorID string
TransReferenceID string
Fill *ContractGenerateRequest
// SkipActorURL 为企业认证后预创建任务时跳过取签署链接(避免消耗 10 分钟单次 EmbedURL
SkipActorURL bool
}
// SignFlowResult 签署流程创建结果
type SignFlowResult struct {
SignFlowID string
// SignURL 可供 iframe 嵌入的签署长链(法大大 actorSignTaskEmbedUrl / e签宝 url
SignURL string
// ShortURL 短链(法大大 actorSignTaskUrl不可 iframe
ShortURL string
}
// GetActorSignURLRequest 重新获取参与方签署链接
type GetActorSignURLRequest struct {
SignFlowID string
TransactorMobile string
PartyAName string
}
// ActorSignURLResult 参与方签署链接(长链可 iframe短链不可
type ActorSignURLResult struct {
SignFlowID string
EmbedURL string // iframe 用
ShortURL string // 短链,一年有效,需登录
}
// SignTaskPreviewURLResult 签署任务预览链接
type SignTaskPreviewURLResult struct {
SignFlowID string
PreviewURL string
}
// SignStatusResult 签署状态
type SignStatusResult struct {
SignFlowID string
Status string
Completed bool
Rejected bool
Expired bool
Message string
}
// SignedFile 已签文件
type SignedFile struct {
DownloadURL string
DownloadID string
}
// CallbackEvent 回调事件
type CallbackEvent struct {
Event string
AuthPassed bool
SignCompleted bool
SignRejected bool
SignFlowID string
AuthFlowID string
OrgName string
Raw map[string]interface{}
}
// SignPlatformProvider 签署平台统一能力e签宝 / 法大大)
type SignPlatformProvider interface {
Platform() enums.SignPlatform
GenerateEnterpriseAuth(ctx context.Context, req *EnterpriseAuthRequest) (*AuthLinkResult, error)
QueryOrgVerified(ctx context.Context, req *OrgIdentityQuery) (bool, error)
GenerateContractFile(ctx context.Context, req *ContractGenerateRequest) (*ContractFileResult, error)
CreateSignFlow(ctx context.Context, req *SignFlowCreateRequest) (*SignFlowResult, error)
// GetActorSignURL 按已有签署任务重新获取参与方链接(法大大长链 10 分钟/单次,进入签署页时应刷新)
GetActorSignURL(ctx context.Context, req *GetActorSignURLRequest) (*ActorSignURLResult, error)
// GetSignTaskPreviewURL 获取签署任务预览链接(法大大 EUI约 2 小时/单次;勿用 PDF 直链做预览)
GetSignTaskPreviewURL(ctx context.Context, signFlowID string) (*SignTaskPreviewURLResult, error)
QuerySignStatus(ctx context.Context, flowID string) (*SignStatusResult, error)
DownloadSignedFiles(ctx context.Context, flowID string) ([]*SignedFile, error)
VerifyCallback(ctx context.Context, headers map[string]string, body []byte) error
ParseCallback(ctx context.Context, headers map[string]string, body []byte) (*CallbackEvent, error)
}
// SignPlatformRegistry 按平台路由 Provider
type SignPlatformRegistry interface {
Get(platform enums.SignPlatform) (SignPlatformProvider, error)
List() []SignPlatformProvider
}

View File

@@ -52,16 +52,20 @@ func NewWalletAggregateService(
}
}
// CreateWallet 创建钱包
// CreateWallet 创建钱包(已存在则幂等返回已有钱包,避免重复 INSERT 中止事务)
func (s *WalletAggregateServiceImpl) CreateWallet(ctx context.Context, userID string) (*entities.Wallet, error) {
// 检查是否已存在
w, _ := s.walletRepo.GetByUserID(ctx, userID)
if w != nil {
return nil, fmt.Errorf("用户已存在钱包")
w, err := s.walletRepo.GetByUserID(ctx, userID)
if err == nil && w != nil {
return w, nil
}
wallet := entities.NewWallet(userID, decimal.NewFromFloat(s.cfg.Wallet.DefaultCreditLimit))
created, err := s.walletRepo.Create(ctx, *wallet)
if err != nil {
// 并发下可能已创建成功,再查一次
if existing, getErr := s.walletRepo.GetByUserID(ctx, userID); getErr == nil && existing != nil {
return existing, nil
}
s.logger.Error("创建钱包失败", zap.Error(err))
return nil, err
}