diff --git a/.gitignore b/.gitignore index f0f8579..f32fbf1 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ .idea/ .git/ .DS_Store/ -data/ \ No newline at end of file +data/ +docs/ diff --git a/api/controller/caV2.go b/api/controller/caV2.go new file mode 100644 index 0000000..b7574b2 --- /dev/null +++ b/api/controller/caV2.go @@ -0,0 +1,141 @@ +package controller + +import ( + "github.com/gin-gonic/gin" + "hospital-admin-api/api/requests" + "hospital-admin-api/api/responses" + cav2 "hospital-admin-api/extend/ca_v2" + "hospital-admin-api/global" + "hospital-admin-api/utils" +) + +type CaV2 struct{} + +// GetServiceURL 获取云证书生命周期管理界面链接地址 +func (r *CaV2) GetServiceURL(c *gin.Context) { + caRequest := requests.CaV2Request{} + req := caRequest.GetServiceURL + if err := c.ShouldBind(&req); err != nil { + responses.FailWithMessage(err.Error(), c) + return + } + + if err := global.Validate.Struct(req); err != nil { + responses.FailWithMessage(utils.Translate(err), c) + return + } + + if msg := req.ValidateBusinessFields(); msg != "" { + responses.FailWithMessage(msg, c) + return + } + + client := cav2.NewClientFromConfig() + result, err := client.GetServiceURL(cav2.GetServiceURLRequest{ + CertType: req.CertType, + Type: req.Type, + EntityID: req.EntityID, + UserName: req.UserName, + UserIDCard: req.UserIDCard, + UserPhone: req.UserPhone, + Email: req.Email, + EnterpriseName: req.EnterpriseName, + SocialCreditCode: req.SocialCreditCode, + DeptPerson: req.DeptPerson, + DeptPersonCode: req.DeptPersonCode, + DeptPersonPhone: req.DeptPersonPhone, + AgentName: req.AgentName, + AgentIDCard: req.AgentIDCard, + AuthMethod: req.AuthMethod, + }) + if err != nil { + responses.FailWithMessage(err.Error(), c) + return + } + + responses.OkWithData(result, c) +} + +// CertSign 云证书签名 +func (r *CaV2) CertSign(c *gin.Context) { + caRequest := requests.CaV2Request{} + req := caRequest.CertSign + if err := c.ShouldBind(&req); err != nil { + responses.FailWithMessage(err.Error(), c) + return + } + + if err := global.Validate.Struct(req); err != nil { + responses.FailWithMessage(utils.Translate(err), c) + return + } + + client := cav2.NewClientFromConfig() + result, err := client.CertSign(cav2.CertSignRequest{ + EntityID: req.EntityID, + RequestID: req.RequestID, + BusinessType: req.BusinessType, + ToSign: req.ToSign, + NotifyURL: req.NotifyURL, + JumpURL: req.JumpURL, + ExtParam: req.ExtParam, + }) + if err != nil { + responses.FailWithMessage(err.Error(), c) + return + } + + responses.OkWithData(result, c) +} + +// GetCertSignResult 获取云证书签名结果 +func (r *CaV2) GetCertSignResult(c *gin.Context) { + caRequest := requests.CaV2Request{} + req := caRequest.GetCertSignResult + if err := c.ShouldBind(&req); err != nil { + responses.FailWithMessage(err.Error(), c) + return + } + + if err := global.Validate.Struct(req); err != nil { + responses.FailWithMessage(utils.Translate(err), c) + return + } + + client := cav2.NewClientFromConfig() + result, err := client.GetCertSignResult(cav2.GetCertSignResultRequest{ + RequestID: req.RequestID, + }) + if err != nil { + responses.FailWithMessage(err.Error(), c) + return + } + + responses.OkWithData(result, c) +} + +// PasswordLessSignInfo 云证书免密签署状态查询 +func (r *CaV2) PasswordLessSignInfo(c *gin.Context) { + caRequest := requests.CaV2Request{} + req := caRequest.PasswordLessSignInfo + if err := c.ShouldBind(&req); err != nil { + responses.FailWithMessage(err.Error(), c) + return + } + + if err := global.Validate.Struct(req); err != nil { + responses.FailWithMessage(utils.Translate(err), c) + return + } + + client := cav2.NewClientFromConfig() + result, err := client.PasswordLessSignInfo(cav2.PasswordLessSignInfoRequest{ + EntityID: req.EntityID, + }) + if err != nil { + responses.FailWithMessage(err.Error(), c) + return + } + + responses.OkWithData(result, c) +} diff --git a/api/requests/caV2.go b/api/requests/caV2.go new file mode 100644 index 0000000..835070d --- /dev/null +++ b/api/requests/caV2.go @@ -0,0 +1,78 @@ +package requests + +import "strings" + +type CaV2Request struct { + GetServiceURL GetCaV2ServiceURL + CertSign GetCaV2CertSign + GetCertSignResult GetCaV2CertSignResult + PasswordLessSignInfo GetCaV2PasswordLessSignInfo +} + +type GetCaV2ServiceURL struct { + CertType string `json:"certType" form:"certType" validate:"required,oneof=Personal Organizational OrgPerson" label:"证书类型"` + Type string `json:"type" form:"type" validate:"required,oneof=BUSINESS_CENTER CERT_APPLY CERT_RENEW CERT_UPDATE CERT_REVOKE" label:"业务类型"` + EntityID string `json:"entityId" form:"entityId" validate:"required" label:"用户唯一标识"` + UserName string `json:"userName" form:"userName"` + UserIDCard string `json:"userIdCard" form:"userIdCard"` + UserPhone string `json:"userPhone" form:"userPhone"` + Email string `json:"email" form:"email"` + EnterpriseName string `json:"enterpriseName" form:"enterpriseName"` + SocialCreditCode string `json:"socialCreditCode" form:"socialCreditCode"` + DeptPerson string `json:"deptPerson" form:"deptPerson"` + DeptPersonCode string `json:"deptPersonCode" form:"deptPersonCode"` + DeptPersonPhone string `json:"deptPersonPhone" form:"deptPersonPhone"` + AgentName string `json:"agentName" form:"agentName"` + AgentIDCard string `json:"agentIdCard" form:"agentIdCard"` + AuthMethod string `json:"authMethod" form:"authMethod" validate:"omitempty,oneof=face SMS" label:"认证方式"` +} + +func (r GetCaV2ServiceURL) ValidateBusinessFields() string { + switch r.CertType { + case "Personal": + if strings.TrimSpace(r.UserName) == "" || strings.TrimSpace(r.UserIDCard) == "" || strings.TrimSpace(r.UserPhone) == "" { + return "个人证书必须填写 userName、userIdCard、userPhone" + } + case "Organizational": + if strings.TrimSpace(r.EnterpriseName) == "" || + strings.TrimSpace(r.SocialCreditCode) == "" || + strings.TrimSpace(r.DeptPerson) == "" || + strings.TrimSpace(r.DeptPersonCode) == "" || + strings.TrimSpace(r.DeptPersonPhone) == "" { + return "企业证书必须填写 enterpriseName、socialCreditCode、deptPerson、deptPersonCode、deptPersonPhone" + } + case "OrgPerson": + if strings.TrimSpace(r.UserName) == "" || + strings.TrimSpace(r.UserIDCard) == "" || + strings.TrimSpace(r.UserPhone) == "" { + return "企业个人证书必须填写 userName、userIdCard、userPhone" + } + if strings.TrimSpace(r.EnterpriseName) == "" || + strings.TrimSpace(r.SocialCreditCode) == "" || + strings.TrimSpace(r.DeptPerson) == "" || + strings.TrimSpace(r.DeptPersonCode) == "" || + strings.TrimSpace(r.DeptPersonPhone) == "" { + return "企业个人证书必须填写 enterpriseName、socialCreditCode、deptPerson、deptPersonCode、deptPersonPhone" + } + } + + return "" +} + +type GetCaV2CertSign struct { + EntityID string `json:"entityId" form:"entityId" validate:"required" label:"用户唯一标识"` + RequestID string `json:"requestId" form:"requestId" validate:"required" label:"业务流水号"` + BusinessType string `json:"businessType" form:"businessType" validate:"required,oneof=businessCenter certApply certRenewcertRenew certUpdate certRevoke certInfo certSign certUpdatePin" label:"业务类型"` + ToSign string `json:"toSign" form:"toSign" validate:"required" label:"签名原文"` + NotifyURL string `json:"notifyUrl" form:"notifyUrl"` + JumpURL string `json:"jumpUrl" form:"jumpUrl"` + ExtParam string `json:"extParam" form:"extParam"` +} + +type GetCaV2CertSignResult struct { + RequestID string `json:"requestId" form:"requestId" validate:"required" label:"业务流水号"` +} + +type GetCaV2PasswordLessSignInfo struct { + EntityID string `json:"entityId" form:"entityId" validate:"required" label:"用户唯一标识"` +} diff --git a/api/router/router.go b/api/router/router.go index ae43c68..dce397b 100644 --- a/api/router/router.go +++ b/api/router/router.go @@ -98,6 +98,15 @@ func publicRouter(r *gin.Engine, api controller.Api) { } } + caV2 := controller.CaV2{} + caV2Group := r.Group("/open/ca/v2") + { + caV2Group.POST("/service-url", caV2.GetServiceURL) + caV2Group.POST("/cert-sign", caV2.CertSign) + caV2Group.POST("/cert-sign/result", caV2.GetCertSignResult) + caV2Group.POST("/passwordless-sign-info", caV2.PasswordLessSignInfo) + } + } // adminRouter 公共路由-验证权限 diff --git a/config.yaml b/config.yaml index 6a826b8..05f8dae 100644 --- a/config.yaml +++ b/config.yaml @@ -53,6 +53,11 @@ ca-online: ca-online-app-secret: adf718ebc1fb4bb7b158de9117d1313a ca-online-api-url: http://testmicrosrv.scca.com.cn:9527 +ca-online-v2: + ca-online-app-id: SCCA1951838127584579586 + ca-online-app-secret: a26bf6c8b78b4099939ab09accbc9a80 + ca-online-api-url: http://testmicrosrv.scca.com.cn:9527 + # [腾讯im] #im: # im-app-id: 1400798221 @@ -118,4 +123,4 @@ amqp: port: 5672 user: gdxz_2022rabbitmq password: qwr2p&¥e@3.2p - vhost: gdxz_2022rabbitmq \ No newline at end of file + vhost: gdxz_2022rabbitmq diff --git a/config.yaml.template b/config.yaml.template index 6728a49..192c6d9 100644 --- a/config.yaml.template +++ b/config.yaml.template @@ -46,9 +46,14 @@ ca-online: ca-online-app-secret: ca-online-api-url: +ca-online-v2: + ca-online-app-id: + ca-online-app-secret: + ca-online-api-url: + # [腾讯im] im: im-app-id: im-secret: im-base-url: https://console.tim.qq.com/ - im-token: \ No newline at end of file + im-token: diff --git a/config/caOnlineV2.go b/config/caOnlineV2.go new file mode 100644 index 0000000..2eca302 --- /dev/null +++ b/config/caOnlineV2.go @@ -0,0 +1,7 @@ +package config + +type CaOnlineV2 struct { + CaOnlineAppID string `mapstructure:"ca-online-app-id" json:"ca-online-app-id" yaml:"ca-online-app-id"` + CaOnlineAppSecret string `mapstructure:"ca-online-app-secret" json:"ca-online-app-secret" yaml:"ca-online-app-secret"` + CaOnlineAPIURL string `mapstructure:"ca-online-api-url" json:"ca-online-api-url" yaml:"ca-online-api-url"` +} diff --git a/config/config.go b/config/config.go index d805f13..00685ce 100644 --- a/config/config.go +++ b/config/config.go @@ -3,18 +3,19 @@ package config var C Config type Config struct { - Port int `mapstructure:"port" json:"port" yaml:"port"` - Env string `mapstructure:"env" json:"Env" yaml:"Env"` - Mysql Mysql `mapstructure:"mysql" json:"mysql" yaml:"mysql"` - Log Log `mapstructure:"log" json:"log" yaml:"log"` - Redis Redis `mapstructure:"redis" json:"redis" yaml:"redis"` - Jwt Jwt `mapstructure:"jwt" json:"jwt" yaml:"jwt"` - Oss Oss `mapstructure:"oss" json:"oss" yaml:"oss"` - Snowflake int64 `mapstructure:"snowflake" json:"snowflake" yaml:"snowflake"` - CaOnline CaOnline `mapstructure:"ca-online" json:"ca-online" yaml:"ca-online"` - Im Im `mapstructure:"im" json:"im" yaml:"im"` - Dysms Dysms `mapstructure:"dysms" json:"dysms" yaml:"dysms"` - Wechat Wechat `mapstructure:"wechat" json:"wechat" yaml:"wechat"` - Pre Pre `mapstructure:"pre" json:"pre" yaml:"pre"` // 处方平台 - Amqp Amqp `mapstructure:"amqp" json:"amqp" yaml:"amqp"` + Port int `mapstructure:"port" json:"port" yaml:"port"` + Env string `mapstructure:"env" json:"Env" yaml:"Env"` + Mysql Mysql `mapstructure:"mysql" json:"mysql" yaml:"mysql"` + Log Log `mapstructure:"log" json:"log" yaml:"log"` + Redis Redis `mapstructure:"redis" json:"redis" yaml:"redis"` + Jwt Jwt `mapstructure:"jwt" json:"jwt" yaml:"jwt"` + Oss Oss `mapstructure:"oss" json:"oss" yaml:"oss"` + Snowflake int64 `mapstructure:"snowflake" json:"snowflake" yaml:"snowflake"` + CaOnline CaOnline `mapstructure:"ca-online" json:"ca-online" yaml:"ca-online"` + CaOnlineV2 CaOnlineV2 `mapstructure:"ca-online-v2" json:"ca-online-v2" yaml:"ca-online-v2"` + Im Im `mapstructure:"im" json:"im" yaml:"im"` + Dysms Dysms `mapstructure:"dysms" json:"dysms" yaml:"dysms"` + Wechat Wechat `mapstructure:"wechat" json:"wechat" yaml:"wechat"` + Pre Pre `mapstructure:"pre" json:"pre" yaml:"pre"` // 处方平台 + Amqp Amqp `mapstructure:"amqp" json:"amqp" yaml:"amqp"` } diff --git a/extend/ca_v2/auth.go b/extend/ca_v2/auth.go new file mode 100644 index 0000000..ebd9ff9 --- /dev/null +++ b/extend/ca_v2/auth.go @@ -0,0 +1,57 @@ +package cav2 + +import "net/url" + +func (c *Client) CheckCIdAndName(request CheckCIdAndNameRequest) error { + form := url.Values{} + setValue(form, "requestId", request.RequestID) + setValue(form, "cName", request.CName) + setValue(form, "cId", request.CID) + return c.postForm("/scca-ocr-server/authapi/checkCIdAndName", form, nil) +} + +func (c *Client) FetchH5FaceURL(request FetchH5FaceURLRequest) (string, error) { + form := url.Values{} + setValue(form, "cName", request.CName) + setValue(form, "cId", request.CID) + setValue(form, "orderNo", request.OrderNo) + setValue(form, "redirectUrl", request.RedirectURL) + return c.postFormForString("/scca-ocr-server/authapi/fetchH5FaceUrl", form) +} + +func (c *Client) FetchH5FaceResult(request FetchH5FaceResultRequest) (*FetchH5FaceResultResponse, error) { + form := url.Values{} + setValue(form, "orderNo", request.OrderNo) + + response := &FetchH5FaceResultResponse{} + if err := c.postForm("/scca-ocr-server/authapi/fetchH5FaceResult", form, response); err != nil { + return nil, err + } + return response, nil +} + +func (c *Client) CheckEnterprise(request CheckEnterpriseRequest) error { + form := url.Values{} + setValue(form, "cName", request.CName) + setValue(form, "cId", request.CID) + setValue(form, "registrationNumber", request.RegistrationNumber) + setValue(form, "enterpriseName", request.EnterpriseName) + return c.postForm("/scca-ocr-server/authapi/checkEnterprise", form, nil) +} + +func (c *Client) CheckEnterprise3(request CheckEnterprise3Request) error { + form := url.Values{} + setValue(form, "cName", request.CName) + setValue(form, "registrationNumber", request.RegistrationNumber) + setValue(form, "enterpriseName", request.EnterpriseName) + setValue(form, "requestId", request.RequestID) + return c.postForm("/scca-ocr-server/authapi/checkDeptAndIndividualInfo3", form, nil) +} + +func (c *Client) PhoneVerification(request PhoneVerificationRequest) error { + form := url.Values{} + setValue(form, "realName", request.RealName) + setValue(form, "idCard", request.IDCard) + setValue(form, "phoneNumber", request.PhoneNumber) + return c.postForm("/scca-ocr-server/authapi/phoneVerification", form, nil) +} diff --git a/extend/ca_v2/certificate.go b/extend/ca_v2/certificate.go new file mode 100644 index 0000000..f06c376 --- /dev/null +++ b/extend/ca_v2/certificate.go @@ -0,0 +1,67 @@ +package cav2 + +import "net/url" + +func (c *Client) GetServiceURL(request GetServiceURLRequest) (*GetServiceURLResponse, error) { + form := url.Values{} + setValue(form, "certType", request.CertType) + setValue(form, "type", request.Type) + setValue(form, "entityId", request.EntityID) + setValue(form, "userName", request.UserName) + setValue(form, "userIdCard", request.UserIDCard) + setValue(form, "userPhone", request.UserPhone) + setValue(form, "email", request.Email) + setValue(form, "enterpriseName", request.EnterpriseName) + setValue(form, "socialCreditCode", request.SocialCreditCode) + setValue(form, "deptPerson", request.DeptPerson) + setValue(form, "deptPersonCode", request.DeptPersonCode) + setValue(form, "deptPersonPhone", request.DeptPersonPhone) + setValue(form, "agentName", request.AgentName) + setValue(form, "agentIdCard", request.AgentIDCard) + setValue(form, "authMethod", request.AuthMethod) + + response := &GetServiceURLResponse{} + if err := c.postForm("/open/api/data/getServiceUrl", form, response); err != nil { + return nil, err + } + return response, nil +} + +func (c *Client) CertSign(request CertSignRequest) (*CertSignResponse, error) { + form := url.Values{} + setValue(form, "entityId", request.EntityID) + setValue(form, "requestId", request.RequestID) + setValue(form, "businessType", request.BusinessType) + setValue(form, "toSign", request.ToSign) + setValue(form, "notifyUrl", request.NotifyURL) + setValue(form, "jumpUrl", request.JumpURL) + setValue(form, "extParam", request.ExtParam) + + response := &CertSignResponse{} + if err := c.postForm("/open/api/data/certSign", form, response); err != nil { + return nil, err + } + return response, nil +} + +func (c *Client) GetCertSignResult(request GetCertSignResultRequest) (*GetCertSignResultResponse, error) { + form := url.Values{} + setValue(form, "requestId", request.RequestID) + + response := &GetCertSignResultResponse{} + if err := c.postForm("/open/api/data/getCertSignResult", form, response); err != nil { + return nil, err + } + return response, nil +} + +func (c *Client) PasswordLessSignInfo(request PasswordLessSignInfoRequest) (*PasswordLessSignInfoResponse, error) { + form := url.Values{} + setValue(form, "entityId", request.EntityID) + + response := &PasswordLessSignInfoResponse{} + if err := c.postForm("/open/api/data/passwordLessSignInfo", form, response); err != nil { + return nil, err + } + return response, nil +} diff --git a/extend/ca_v2/client.go b/extend/ca_v2/client.go new file mode 100644 index 0000000..b1f93dc --- /dev/null +++ b/extend/ca_v2/client.go @@ -0,0 +1,174 @@ +package cav2 + +import ( + "crypto/hmac" + "crypto/sha1" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "hospital-admin-api/config" + "io" + "net/http" + "net/url" + "sort" + "strings" + "time" +) + +type Client struct { + BaseURL string + AppID string + AppSecret string + HTTPClient *http.Client +} + +func NewClient(baseURL, appID, appSecret string) *Client { + return &Client{ + BaseURL: strings.TrimRight(baseURL, "/"), + AppID: appID, + AppSecret: appSecret, + HTTPClient: &http.Client{ + Timeout: 15 * time.Second, + }, + } +} + +func NewClientFromConfig() *Client { + baseURL := config.C.CaOnlineV2.CaOnlineAPIURL + appID := config.C.CaOnlineV2.CaOnlineAppID + appSecret := config.C.CaOnlineV2.CaOnlineAppSecret + + if baseURL == "" { + baseURL = config.C.CaOnline.CaOnlineApiUrl + } + if appID == "" { + appID = config.C.CaOnline.CaOnlineAppId + } + if appSecret == "" { + appSecret = config.C.CaOnline.CaOnlineAppSecret + } + + return NewClient( + baseURL, + appID, + appSecret, + ) +} + +func (c *Client) postForm(path string, form url.Values, out interface{}) error { + if c == nil { + return errors.New("ca v2 client is nil") + } + if c.BaseURL == "" || c.AppID == "" || c.AppSecret == "" { + return errors.New("ca v2 client config is incomplete") + } + if c.HTTPClient == nil { + c.HTTPClient = &http.Client{Timeout: 15 * time.Second} + } + + payload := strings.NewReader(form.Encode()) + req, err := http.NewRequest(http.MethodPost, c.BaseURL+path, payload) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("app_id", c.AppID) + req.Header.Set("signature", c.signature(form)) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return err + } + defer func() { + _ = resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("ca v2 request failed: http %d", resp.StatusCode) + } + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + var envelope responseEnvelope + if err := json.Unmarshal(respBody, &envelope); err != nil { + return err + } + + if envelope.ResultCode != 0 { + if envelope.ResultMsg != "" { + return errors.New(envelope.ResultMsg) + } + return fmt.Errorf("ca v2 request failed with code %d", envelope.ResultCode) + } + + if out == nil { + return nil + } + + body := strings.TrimSpace(string(envelope.Body)) + if body == "" || body == "null" { + return nil + } + + return json.Unmarshal(envelope.Body, out) +} + +func (c *Client) postFormForString(path string, form url.Values) (string, error) { + var raw json.RawMessage + if err := c.postForm(path, form, &raw); err != nil { + return "", err + } + body := strings.TrimSpace(string(raw)) + if body == "" || body == "null" { + return "", nil + } + + var text string + if err := json.Unmarshal(raw, &text); err == nil { + return text, nil + } + + var data map[string]string + if err := json.Unmarshal(raw, &data); err != nil { + return "", err + } + + for _, key := range []string{"url", "authUrl", "faceUrl", "h5Url"} { + if value := strings.TrimSpace(data[key]); value != "" { + return value, nil + } + } + + return "", errors.New("ca v2 string body not found") +} + +func (c *Client) signature(form url.Values) string { + keys := make([]string, 0, len(form)) + for key := range form { + keys = append(keys, key) + } + sort.Strings(keys) + + values := make([]string, 0, len(keys)) + for _, key := range keys { + for _, value := range form[key] { + values = append(values, value) + } + } + + h := hmac.New(sha1.New, []byte(c.AppSecret)) + h.Write([]byte(strings.Join(values, "&"))) + return hex.EncodeToString(h.Sum(nil)) +} + +func setValue(form url.Values, key, value string) { + if strings.TrimSpace(value) == "" { + return + } + form.Set(key, value) +} diff --git a/extend/ca_v2/client_test.go b/extend/ca_v2/client_test.go new file mode 100644 index 0000000..3232fd4 --- /dev/null +++ b/extend/ca_v2/client_test.go @@ -0,0 +1,116 @@ +package cav2 + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "testing" +) + +func TestGetServiceURL(t *testing.T) { + var gotForm url.Values + signClient := NewClient("http://unused", "test-app", "test-secret") + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/open/api/data/getServiceUrl" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + if r.Method != http.MethodPost { + t.Fatalf("unexpected method: %s", r.Method) + } + if err := r.ParseForm(); err != nil { + t.Fatalf("parse form: %v", err) + } + gotForm = r.PostForm + + expectedSignature := signClient.signature(r.PostForm) + if r.Header.Get("app_id") != "test-app" { + t.Fatalf("unexpected app_id: %s", r.Header.Get("app_id")) + } + if r.Header.Get("signature") != expectedSignature { + t.Fatalf("unexpected signature: %s", r.Header.Get("signature")) + } + + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "result_code": 0, + "result_msg": "", + "success": true, + "body": map[string]string{ + "webToken": "token-1", + "url": "https://example.test/h5", + "expireTime": "2026-05-01 10:00:00", + }, + }) + })) + defer server.Close() + + client := NewClient(server.URL, "test-app", "test-secret") + response, err := client.GetServiceURL(GetServiceURLRequest{ + CertType: "Personal", + Type: "CERT_APPLY", + EntityID: "user-1", + UserName: "Alice", + UserIDCard: "123456", + UserPhone: "13800000000", + AuthMethod: "face", + }) + if err != nil { + t.Fatalf("GetServiceURL error: %v", err) + } + + if gotForm.Get("entityId") != "user-1" { + t.Fatalf("unexpected entityId: %s", gotForm.Get("entityId")) + } + if response.WebToken != "token-1" || response.URL != "https://example.test/h5" { + t.Fatalf("unexpected response: %+v", response) + } +} + +func TestFetchH5FaceURLWithStringBody(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "result_code": 0, + "result_msg": "", + "success": true, + "body": "https://example.test/face-h5", + }) + })) + defer server.Close() + + client := NewClient(server.URL, "test-app", "test-secret") + link, err := client.FetchH5FaceURL(FetchH5FaceURLRequest{ + CName: "Alice", + CID: "123456", + OrderNo: "order-1", + RedirectURL: "https://callback.test/result", + }) + if err != nil { + t.Fatalf("FetchH5FaceURL error: %v", err) + } + if link != "https://example.test/face-h5" { + t.Fatalf("unexpected link: %s", link) + } +} + +func TestCheckEnterpriseSuccessWithoutBody(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "result_code": 0, + "result_msg": "", + "success": true, + "body": nil, + }) + })) + defer server.Close() + + client := NewClient(server.URL, "test-app", "test-secret") + err := client.CheckEnterprise(CheckEnterpriseRequest{ + CName: "法人A", + CID: "510000000000000000", + RegistrationNumber: "91510000TEST", + EnterpriseName: "测试企业", + }) + if err != nil { + t.Fatalf("CheckEnterprise error: %v", err) + } +} diff --git a/extend/ca_v2/types.go b/extend/ca_v2/types.go new file mode 100644 index 0000000..ebac22e --- /dev/null +++ b/extend/ca_v2/types.go @@ -0,0 +1,129 @@ +package cav2 + +import "encoding/json" + +type responseEnvelope struct { + ResultCode int `json:"result_code"` + ResultMsg string `json:"result_msg"` + Success bool `json:"success"` + Body json.RawMessage `json:"body"` +} + +type GetServiceURLRequest struct { + CertType string `json:"certType"` + Type string `json:"type"` + EntityID string `json:"entityId"` + UserName string `json:"userName,omitempty"` + UserIDCard string `json:"userIdCard,omitempty"` + UserPhone string `json:"userPhone,omitempty"` + Email string `json:"email,omitempty"` + EnterpriseName string `json:"enterpriseName,omitempty"` + SocialCreditCode string `json:"socialCreditCode,omitempty"` + DeptPerson string `json:"deptPerson,omitempty"` + DeptPersonCode string `json:"deptPersonCode,omitempty"` + DeptPersonPhone string `json:"deptPersonPhone,omitempty"` + AgentName string `json:"agentName,omitempty"` + AgentIDCard string `json:"agentIdCard,omitempty"` + AuthMethod string `json:"authMethod,omitempty"` +} + +type GetServiceURLResponse struct { + WebToken string `json:"webToken"` + URL string `json:"url"` + ExpireTime string `json:"expireTime"` +} + +type CertSignRequest struct { + EntityID string `json:"entityId"` + RequestID string `json:"requestId"` + BusinessType string `json:"businessType"` + ToSign string `json:"toSign"` + NotifyURL string `json:"notifyUrl,omitempty"` + JumpURL string `json:"jumpUrl,omitempty"` + ExtParam string `json:"extParam,omitempty"` +} + +type CertSignResponse struct { + RequestID string `json:"requestId"` + BusinessType string `json:"businessType"` + SignType string `json:"signType"` + SignP7 string `json:"signP7"` + ToSignEncoding string `json:"toSignEncoding"` + SignURL string `json:"signUrl"` +} + +type GetCertSignResultRequest struct { + RequestID string `json:"requestId"` +} + +type GetCertSignResultResponse struct { + RequestID string `json:"requestId"` + BusinessType string `json:"businessType"` + SignType string `json:"signType"` + SignP7 string `json:"signP7"` + ToSignEncoding string `json:"toSignEncoding"` +} + +type PasswordLessSignInfoRequest struct { + EntityID string `json:"entityId"` +} + +type PasswordLessSignInfoResponse struct { + CertDN string `json:"certDn"` + NotAfter string `json:"notafter"` + CertSN string `json:"certSn"` + EnableTime string `json:"enableTime"` + DisableTime string `json:"disableTime"` + DataStatus int `json:"dataStatus"` +} + +type CheckCIdAndNameRequest struct { + RequestID string `json:"requestId"` + CName string `json:"cName"` + CID string `json:"cId"` +} + +type FetchH5FaceURLRequest struct { + CName string `json:"cName"` + CID string `json:"cId"` + OrderNo string `json:"orderNo"` + RedirectURL string `json:"redirectUrl"` +} + +type FetchH5FaceResultRequest struct { + OrderNo string `json:"orderNo"` +} + +type FetchH5FaceResultResponse struct { + Code string `json:"code"` + Msg string `json:"msg"` + LiveRate string `json:"liveRate"` + Similarity string `json:"similarity"` + OccurredTime string `json:"occurredTime"` + OrderNo string `json:"orderNo"` + IDNo string `json:"idNo"` + Name string `json:"name"` + IDType string `json:"idType"` + Photo string `json:"photo"` + Video string `json:"video"` +} + +type CheckEnterpriseRequest struct { + CName string `json:"cName"` + CID string `json:"cId"` + RegistrationNumber string `json:"registrationNumber"` + EnterpriseName string `json:"enterpriseName"` +} + +type CheckEnterprise3Request struct { + CName string `json:"cName"` + RegistrationNumber string `json:"registrationNumber"` + EnterpriseName string `json:"enterpriseName"` + RequestID string `json:"requestId,omitempty"` +} + +type PhoneVerificationRequest struct { + RealName string `json:"realName"` + IDCard string `json:"idCard"` + PhoneNumber string `json:"phoneNumber"` +}