Giter Club home page Giter Club logo

wechatpay-go's People

Contributors

a8520238 avatar alanfc666 avatar alexkie007 avatar bobbyz3g avatar bytechen avatar cadmusjiang avatar chaoyuxie avatar colindhlin avatar emmetzc avatar lddssxy avatar lianup avatar sexybear avatar shaojunda avatar tonytony2020 avatar v-jinyewang avatar whrss9527 avatar whuwxl avatar xavierzho avatar xiaokang-chen avatar xy-peng avatar zhenyonghou avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

wechatpay-go's Issues

是否有计划支持服务商模式

  • Go 版本:1.64
  • wechatpay-go 版本:v0.2.3

想咨询下golang版本是否有计划支持服务商模式,现在的sdk看起来只支持直充模式,如果支持未来排期大概是什么样子的?

client的实例初始化一定要证书么

微信支付的证书在文档说明里并不是太多的操作需要,可是在仓库的说明里初始化client又需要代入证书信息。
是v3的接口要求必须开通证书么?

如何调试沙箱环境

在这里请只反馈跟微信支付 Go 语言开发库 (wechatpay-go)的 bugfeature request

如果你在接入微信支付的过程中遇到了业务错误,推荐通过 腾讯客服自助服务专区 或者 微信支付在线技术支持 获取帮助,你也可以在微信开放社区的 开发者专区 反馈业务问题。

在反馈问题时,请提供你所使用的 Go 版本和 wechatpay-go 的版本,以及尽可能详细的日志和细节(如调用代码),以便于我们能更快的找到问题。
目前是不支持沙箱环境吗?

  • Go 版本:1.15
  • wechatpay-go 版本:v0.21

希望增加转账和分账接口

非常需要两个接口:

  1. 转账到微信零钱(用于提现)
  2. 服务商分账

希望有排期时间的告知,如果没有,只能用老版本了

服务商模式NewClient失败

服务商模式初始化客户端失败

信息都是正确的,特约商户模式可以。服务商模式就异常。

"init client setting err:decrypt downloaded certificate failed: crypto/aes: invalid key size 0"

  • Go 版本:go version go1.16.3 darwin/amd64
  • wechatpay-go 版本:0.2.5

validate verify fail

调用client.Post方法返回错误
validate verify fail serial=3BA696623B16A8D204DAC4EBFDA4703C7A89EEDB request-id=08E78DE5830610CB0718C2B8B74C20C55128C68803-0 err=no serial number:3BA696623B16A8D204DAC4EBFDA4703C7A89EEDB corresponding certificate
虽然显示了错误信息,但是也正常返回了相应结果

func main() {
	// 增加客户端配置
	opts := []option.ClientOption{
		option.WithMerchant(mchID, certificateSerialNumber, privateKey), // 设置商户信息,用于生成签名信息
		option.WithWechatPay(wechatPayCertificateList),                  // 设置微信支付平台证书信息,对回包进行校验
		option.WithHTTPClient(&http.Client{}),                           // 可以不设置
		option.WithTimeout(2 * time.Second),                             // 自行进行超时时间配置
		option.WithHeader(&http.Header{}),                               // 可以自行设置Header
	}
	client, err := core.NewClient(ctx, opts...)
	if err != nil {
		log.Printf("new wechat pay client err:%s", err.Error())
		return
	}
	// 后面可以开始写你的逻辑
	nm := NativePayModel{
		Appid:       "wx347d8b996e0b2d71",
		Mchid:       mchID,
		Description: "测试商品-2021-04-16",
		OutTradeNo:  gconv.String(gtime.Now().UnixNano()),
		NotifyUrl:   "http://api.hcyjk.com/apiv1/notify",
		Amount:      AmountModel{
			Total: 100,
		},
	}
	_response, _err := client.Post(ctx, "https://api.mch.weixin.qq.com/v3/pay/transactions/native", nm)
	if _err != nil {
		fmt.Println(fmt.Sprintf("请求出错,%s\n", _err.Error()))
	}

	fmt.Println("请求完成")
	if body, err := ioutil.ReadAll(_response.Body); err != nil{
		fmt.Println(err.Error())
	} else {
		fmt.Println(string(body))
	}
}

WechatPayNotifyValidator Validate 传参建议

  • Go 版本:1.6.4
  • wechatpay-go 版本:v0.2.5

WechatPayNotifyValidatorValidate 直传*http.Request是不是太粗暴了 如果我用的别的 web 框架库将无法支持比如fasthttp 这个库内建了 Request 如果是需要取HTTP Header 可以构造个结构传递到方法内呀... 现在我只能将 Validate 内部方法提取出来重新实现一遍

使用示例的demo无法初始化client

使用例子中的代码无法初始化client
{"file":"/Users/apple/business/kuzi-app/common/weixinPay/weixin_pay_client.go:64","func":"kuzi/app/common/weixinPay.NewWeixinPayClient","level":"error","msg":"new wechat pay client err:init client setting err:generate authorization err:you must set privatekey to use SHA256WithRSASigner","time":"2021-09-02T09:09:29+08:00"}

`import (
"context"
"crypto/rsa"
"net/http"

//"kuzi/app/app/common"
"kuzi/app/configs"

"github.com/sirupsen/logrus"
"github.com/wechatpay-apiv3/wechatpay-go/core"
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
"github.com/wechatpay-apiv3/wechatpay-go/utils"

)

var weixinPayClient *core.Client

// 示例参数,实际使用时请自行初始化
var (
mchID string // 商户号
mchCertificateSerialNumber string // 商户证书序列号
mchPrivateKey *rsa.PrivateKey // 商户私钥
mchAPIv3Key string // 商户APIv3密钥
customHTTPClient *http.Client // 可选,自定义客户端实例
)

func InitWeixinPayClient() {
mchID = configs.AppConfig.Weixin.MchId
mchCertificateSerialNumber = configs.AppConfig.Weixin.MchCertificateSerialNumber
mchPrivateKey = GetPrivateKey()
mchAPIv3Key = configs.AppConfig.Weixin.MchAPIv3Key
weixinPayClient = NewWeixinPayClient()

}

//获取商户私钥
func GetPrivateKey() *rsa.PrivateKey {
// 加载商户私钥
privateKeyPath := configs.AppConfig.Weixin.MchPrivateKeyPath
privateKey, err := utils.LoadPrivateKeyWithPath(privateKeyPath)

if err != nil {
	logrus.Errorf("load private err:%s", err.Error())
	return nil
}

return privateKey

}

func NewWeixinPayClient() *core.Client {
ctx := context.Background()
opts := []core.ClientOption{
// 一次性设置 签名/验签/敏感字段加解密,并注册 平台证书下载器,自动定时获取最新的平台证书
option.WithWechatPayAutoAuthCipher(mchID, mchCertificateSerialNumber, mchPrivateKey, mchAPIv3Key),
// 设置自定义 HTTPClient 实例,不设置时默认使用 http.Client{},并设置超时时间为 30s
//option.WithHTTPClient(customHTTPClient),
}
client, err := core.NewClient(ctx, opts...)
if err != nil {
logrus.Errorf("new wechat pay client err:%s", err.Error())
return nil
}
return client
}`

  • Go 版本:
  • wechatpay-go 版本:0.2.6

【官方调查问卷】微信支付 API v3 Go SDK 开发者体验调查

为了向广大开发者提供更好的使用体验,微信支付诚挚邀请您将使用微信支付 API v3 SDK中的感受反馈给我们。本问卷可能会占用您不超过2分钟的时间,感谢您的支持。

问卷系统使用的腾讯问卷,您可以点击这里,或者扫描以下二维码参与调查。

qrcode

升级2.0时mod tidy错误

  • Go 版本:

go version go1.16.3 windows/amd64

  • wechatpay-go 版本:

go get -u

main imports
        github.com/wechatpay-apiv3/wechatpay-go/core/option: cannot find module providing package github.com/wechatpay-apiv3/wechatpay-go/core/option

手动改go.mod后
github.com/wechatpay-apiv3/wechatpay-go v0.2.0

go mod tidy

go: finding module for package github.com/wechatpay-apiv3/wechatpay-go/core/option
main imports
        github.com/wechatpay-apiv3/wechatpay-go/core/option: module github.com/wechatpay-apiv3/wechatpay-go@latest found (v0.2.0), but does not contain package github.com/wechatpay-apiv3/wechatpay-go/core/option

[QUESTION] 有哪些组件是协程安全的?

  • Go 版本: go version go1.16.4 linux/amd64
  • wechatpay-go 版本: v0.2.5

README里面介绍:

  • core.Client初始化完成后,可以在多个goroutine中并发使用。
    微信图片_20210818160100

还有哪些组件是协程安全的呢?比如可以在不同的协程里面安全使用同一个 notify.Handler 么?

notify.Handler 调用了GetBody函数,http server端拿到的request此函数值真是nil,文档也说明只有client端才能用此函数

在这里请只反馈跟微信支付 Go 语言开发库 (wechatpay-go)的 bugfeature request

如果你在接入微信支付的过程中遇到了业务错误,推荐通过 腾讯客服自助服务专区 或者 微信支付在线技术支持 获取帮助,你也可以在微信开放社区的 开发者专区 反馈业务问题。

在反馈问题时,请提供你所使用的 Go 版本和 wechatpay-go 的版本,以及尽可能详细的日志和细节(如调用代码),以便于我们能更快的找到问题。

  • Go 版本:1.16.3
  • wechatpay-go 版本:v0.2.2

调用jsapi返回报错如下

	resp, result, err := svc.Prepay(ctx,
		jsapi.PrepayRequest{
			Appid:       core.String(appId),
			Mchid:       core.String(mchId),
			Description: core.String(desc),
			OutTradeNo:  core.String(tradeNo),
			Attach:      core.String(attach),
			NotifyUrl:   core.String(notify),
			Amount: &jsapi.Amount{
				Total:    core.Int32(total),
				Currency: core.String("CNY"),
			},
			Payer: &jsapi.Payer{
				Openid: core.String(openId),
			},
		},
	)

prepay  validate verify fail serial=xxxxx    request-id=xxxxx-0 err=no serial number:xxxxxx    corresponding certificate  56111111787422951772160

小程序微信支付结果通知未返回所需的 request-id

微信通知header如下

{
  host: '127.0.0.1:8099',
  connection: 'close',
  'content-length': '915',
  'wechatpay-nonce': 'zz0Hx4BPQJ5M6vmHfveBOuTABJVjLjE6',
  'wechatpay-serial': '5854C75E7AD35B656A1C9B3E20700A0323D5BC8F',
  'content-type': 'application/json',
  'wechatpay-signature': 'K8BNkGoGkzfvQ5n26NHzoj267CAHVDltBP6uGB8R5iU3sQSkDooKp9/yBxtlrXkfyvnrY6DBjBJbUcMuj4yEXuNuDDNcU2k5cMyjISI3aP9A+7fn8qS/pY2s8ZD0c0Noy0/Zs4p7TVawjIOwCivmgFp3D0sGRgMUfQU0yM/fKfjvb72v/8utE83xaBciwDzoGizI7EjsioFQB7T9kJpu9apV4dFBsfxxiULOhZvLYuqrtp9PJaUQbQdRHnipJHhiERwx26i7Xv940NFCm0xIwy2asl6/JZm98+gOFFwbfpj6BAJUgsArRZcTM15PHlvTRZXB737MTHHpM8jUh3KNKA==',
  'wechatpay-timestamp': '1625638767',
  pragma: 'no-cache',
  'user-agent': 'Mozilla/4.0',
  accept: '*/*'
}

微信通知body如下

{
  id: 'f994ecf7-60a3-537e-85d1-7dad28f7bfe8',
  create_time: '2021-07-07T14:19:27+08:00',
  resource_type: 'encrypt-resource',
  event_type: 'TRANSACTION.SUCCESS',
  summary: '支付成功',
  resource: {
    original_type: 'transaction',
    algorithm: 'AEAD_AES_256_GCM',
    ciphertext: 'ZetL1s9e/3nhpfCdyLEKobHv9JpPYmZJb9H2VglLoRo+IH+I8OgkzisraaH6oca/yKNGw67z8FQvakGO2M2K329ef4jvGymPXehFjHJNW8cOGH7xlrEbsj7Pg79P1dA1lXXXte6IJcTfHZLLpohUbqNFq5MTKCFZP4tjZcdU4izis7YNkQyUM1nSbS+ia0kg9AOy7rmN7nyFQ+UNeWU5nHHa5j7QDwp32DHzHyZHTY4GgWHqJ2NK4NI42o8IbDeHsraBodFItDMI/kd4NFYry38cFcfSSAN23CopHetud/Wog/8x4ufA3+2G2DW5ogbsVWUVUaPc5n5vZxRRpO/DulXlfEnnD0Bw7whxd2ZjB3T6uWPHqpfgnadjgmj+KydZBqbuL5tEUeC/wody2+G0uCAqGUF/an1LBwpZeGKg2UOmN9CJ7XFGXM5e4Dnh2Fgu/lUDiWd/okkrLhbkjePiMkTHUuc7RNBFg09saa1CmsaPNWeqIv3GuKumhh6D/zruEfy7NFPo183v/7S2yfnHla08tzYA1273WYVrxMN/Z7qiS9Tv71RuRJqaLKm4ShCa/5DxfgRVlA==',
    associated_data: 'transaction',
    nonce: 'uGIie22uk1w9'
  }
}

调用通知回调错误如下

not valid wechatpay notify request: validate verify fail serial=5854C75E7AD35B656A1C9B3E20700A0323D5BC8F request-id=5d781374-9d73-381a-8851-673de8004cbb err=certificate[5854C75E7AD35B656A1C9B3E20700A0323D5BC8F] not found in verifier

自定义 Downloader 使用的 Client

在使用 WithWechatPayAutoAuthCipher 创建 core.Client 时,会自动在 MgrInstance() 中新增一个 Downloader(如果不存在该商户的Downloader的话)。

这个Downloader是独立新建了一个 core.Client,这个时候如果开发者想要自定义 Downloader 使用的 core.Client(一般情况下是希望自定义其使用的 http.Client),就需要再次调用 MgrInstance().RegisterDownloader 方法重新注册一次 Downloader

这个过程很不友好,而且需要对SDK有充分的了解才会知道应该这么做。

需要有办法让这个过程,至少是「知道应该怎么做」的过程,更加清楚。

堂堂微信支付,为何SDK如此之烂?

你们瞧瞧自己写的 sdk ,shi 一样的代码,你们就不能封装的好点吗?
拿完 KPI 之后,项目就没人管了么?
你要么就别做,留这么烂的代码,shi 一样的说明文档,让人怎么用?

商户批量付款至用户零钱中的Demo不完善

TransferDetailList: []transferbatch.TransferDetailInput{transferbatch.TransferDetailInput{
  Openid:         core.String("o-MYE42l80oelYMDE34nYD456Xoy"),
  OutDetailNo:    core.String("x23zy545Bd5436"),
  TransferAmount: core.Int64(200000),
  TransferRemark: core.String("2020年4月报销"),
  UserIdCard:     nil,
  UserName:       nil,
}},

其中UserName和UserIdCard如何加密并赋值,可以给出示例吗

Tag包和Master包的请求地址不一致

在本地debug时,发现v0.2.6中调用始终会出现以下错误:
call Prepay err:error http response:[StatusCode: 400 Code: "PARAM_ERROR"
Message: 输入源“/body/appid”映射到字段“公众号ID”必填性规则校验失败,此字段为必填项
Detail:
{
"location": "body",
"value": ""
}
Header:

  • Server=[nginx]
  • Keep-Alive=[timeout=8]
  • Wechatpay-Nonce=[aa5373a8719bd1d99cb13c8061bfbcda]
  • Wechatpay-Serial=[75F61B74B4D16B45E920861D4194BF27CB1425C6]
  • Date=[Sat, 18 Sep 2021 06:39:01 GMT]
  • Connection=[keep-alive]
  • Cache-Control=[no-cache, must-revalidate]
  • Request-Id=[088593968A0610EB0418E2A6E44820A62128956F-268435461]
  • Wechatpay-Timestamp=[1631947141]
  • Content-Type=[application/json; charset=utf-8]
  • Content-Length=[184]
  • Wechatpay-Signature=[FP/wZjL4EGROq9z2xT1Fpprus8/+SJ5+LvbhmkHBpMkeGUA+7DPWvlOYcdEAj96p7+RCxtBxiut7t01zbBvLL7JwX8GYkLheg3xGHOLDqxmqU/BOsfgIuSriA3FufNLe3C5JJhfbD5I5CvXzqlZnsiowDokrAaoz94JA6hqm56l09lpyKO6x9mlxc/0RKQeV6bUqb2PWhazT205iPqly1X57uUWgktixYgXFJIgrbmX1Co50fXTB6x9a9NdBA0O4z99j+1ZZJyMC9qCbDu0zB+nxFQVyI3pE61XSEsBU1tQPHNa0ILaksxH60BbUj3NFZ0t0PztCTsPhZsx5Qyc6IA==]
  • X-Content-Type-Options=[nosniff]
  • Content-Language=[zh-CN]]

然后用git clone拉下代码使用Master包调用就能正常使用,最后对比v0.2.6与Master包代码发现:wechatpay-go/services/partnerpayments/jsapi/api_jsapi.go:83两者路径出现差异一下为具体路径:
Master: localVarPath := consts.WechatPayAPIServer + "/v3/pay/partner/transactions/jsapi"
v0.2.6: localVarPath := consts.WechatPayAPIServer + "/v3/pay/transactions/jsapi"

请求是否为两者路径差异导致的这个错误?已经对比过两次调用的入参完全一致。

Enum类型 UnmarshalJSON 的潜在不可兼容问题

目前model中的 Enum 类型自定义了 UnmarshalJSON 方法,会对应答中的 enum 值进行检查。这种检查会在服务增加枚举值时导致不兼容问题,如果商户不进行SDK升级会导致应答反序列化(Unmarshal)失败。
建议移除该检查。

  • Go 版本:any
  • wechatpay-go 版本:v0.2.5

使用七牛代理后无法go get

使用七牛国内代理后 无法拉取代码

GOPROXY="http://goproxy.cn" # go env 代理配置
go get github.com/wechatpay-apiv3/wechatpay-go v0.2.6 官方命令 如果不指定版本号那么默认拉取的是2.5版本

指定版本号则会出现404情况,还望告知如何获取最新0.26版本

  • Go 版本:go version go1.15 darwin/amd64
  • wechatpay-go 版本:v3

小提议:可否重新设计 auth.Validator?

  • Go 版本:1.16
  • wechatpay-go 版本:0.2.2

我在修复 notify/handler 的过程中发现有些坏味道代码,可能当初的设计是非常好的,只是加上 notify 后变得不那么合适了。
我个人感觉可以更新一下 Validator 的定义。
目前的定义是这样:

// Validator 应答报文验证器
type Validator interface {
	Validate(ctx context.Context, response *http.Response) error // 对 HTTP 应答报文进行验证
}

可否改为类似下面的 :

// Validator 验证器
type Validator interface {
	Validate(v Validatable) error {
}

// 可验证对象(表示业务上下文)
type Validatable interface {
	Verify(verifier auth.Verifier)
	// or
	GetVerifyArgs() (serialNo, message, sign string)
}

// 默认的验证器实现
type DefaultValidator struct {
	verifier auth.Verifier
}

func (dv *DefaultValidator) Validate(v Validatable) error {
	return  v.Verify(dv.verifier)
	// or
	serialNo, msg, sign := v.GetVerifyArgs()
	return dv.verifier.Verify(serialNo, msg, sign)
}

然后不同的业务上下文各自实现自己的 Validatable ,比如 http.Response 和 notify 就可以拆到各自的模块,互不相干。
两者在业务上确实也没啥关联,仅仅只是重用了一套验证逻辑(当然,也能重用一些小代码)。

你好

你好 我的返回的值了 但是类型是*payments.Transaction 这个我要怎么解析

支付回调提示not valid wechatpay notify

用户在支付以后,回调接口报错

2021-08-26T07:44:46.190+0800	�[34mINFO�[0m	rest/user.go:208	resp:&{FAIL not valid wechatpay notify request: validate verify fail serial=[62602949721CAEC38677A4170A80C97DDB5A580C] request-id=[] err=certificate[62602949721CAEC38677A4170A80C97DDB5A580C] not found in verifier}

这是我回调函数的代码

	certPath := zconfig.GetString("weixin.cert")

	cert, err := utils.LoadCertificateWithPath(certPath)
	if err != nil {
		return nil, err
	}

	apiV3Key := zconfig.GetString("weixin.apiv3Key")

	handler := notify.NewNotifyHandler(apiV3Key, verifiers.NewSHA256WithRSAVerifier(core.NewCertificateMapWithList([]*x509.Certificate{cert})))

	content := new(contentType)

	notifyReq, err := handler.ParseNotifyRequest(ctx, req, content)

我看一下一下serial的值,这个62602949721CAEC38677A4170A80C97DDB5A580C跟证书申请的时候证书序列号并不一样,头部拿到值,与支付平台提供的证书序列号的值并不一致。

  • Go 版本:1.16
  • wechatpay-go 版本:v0.2.5

关于 微信支付退款的通知 NotifyUrl

我请问 我通过 refunddomestic 可以申请退款了, 可是我准备使用NotifyUrl ,接收退款通知的时候, 发现好像, 没有相关的解析模块。
另外, 我通过 支付模块 获取的 NotifyUrl 是通知一次的, 可是 我的退款NotifyUrl 接口的 是不断重复的。 是不是 支付模块的通知包里面有反馈 说已接受到通知, 可以不用在重复发送了。 而没有退款模块并且我只是简单打印了一下 ,故在不断发送给我?

  • Go 版本:最新
  • wechatpay-go 版本: 最新

实例化client时,商户私钥应该如何配置

chPrivateKey *rsa.PrivateKey // 商户私钥
这个字段实在是不知道应该如何配置进行赋值,让您见笑了,我现在有
apiclient_key.pem, apiclient_cert.pem 两个文件,但是在代码中我实在是不知道如何实现这个字段
烦请告知如何赋值,感激不尽!

  • Go 版本:1.15
  • wechatpay-go 版本:0.25

v3沙箱可以通过改sandboxnew实现吗

尝试fork和更改const的url,后缀加上 "/sandboxnew"
但是初始化client报错, Prepay也显示serialize错误

问题:

  1. 是v3版本现在不支持sandbox吗,什么时候能支持呢

  2. 如果只是golang sdk还未提供支持,换一种语言的sdk能否解决?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.