免费1年服务器,部署个ChatGPT专属网页版! - 掘金

mikel阅读(544)

来源: 免费1年服务器,部署个ChatGPT专属网页版! – 掘金

免费1年服务器,部署个ChatGPT专属网页版!

作者:小傅哥
博客:bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!😄

白皮袄个免费1年服务器,部署个ChatGPT专属网页版!

api.openai.com port 443: Connection timed out 你是不在调用 ChatGPT 接口的时候报错 443 了,哈哈哈,我的也是。当小傅哥发现自己开发的《ChatGPT AI 问答助手》已经不工作了,搜索了下为啥会 443 呢,原来是 openAI 的 API 域名已经 DNS 污染了,所以没法使用。

所以如果你仍需要学习测试使用,要不是找其他人做好的代理接口,要不就是买一台能调用 openAI 的海外服务器。但其实对于学习来说能白皮袄还花费这个钱干嘛!所以小傅哥今天给粉丝伙伴分享个,能免费使用1年稳定的云服务器。第二年再重新申请就可以。

这里小傅哥先部署了一个 ChatGPT Webchatgpt.itedus.cn/ – 以下文章有专门的部署教程。

一、这是什么服务器?

国内我们知道有阿里云、腾讯云、华为云、百度云、京东云,而国外有亚马逊旗下的 AWS 云,并且重点来了,AWS 是有免费的云服务的,任何人都可以获得一台。其实以前就听说有 AWS 免费云,但一直没当回事,现在才感觉真香!

在 AWS 活动页,有一个免费套餐,这里有很多产品都可以免费使用,包括;云服务、数据库、函数计算、机器学习等。地址:aws.amazon.com/cn/free

这里小傅哥感兴趣的就是 Amazon EC2 云服务器,每个月免费750个小时。这一个月31天✖️24小时不也才744小时,所以这和免费有啥区别?🤔 难道一天能加出来25个小时班?

另外这里有一些注意点

  1. EC2 云服务器,只有1核1G,在安装 Docker、Portainer、以及一个 Java SpringBoot 应用程序后,运行正常&稳定。
  2. 申请时需要用到银行卡,我自己有一个 VISA 的信用卡,直接注册的时候使用就可以。其他银行卡也OK。有些营销文,为了卖服务器说乱扣费,为此我专门去搜索了下关于 AWS 免费服务器会有坑不,以及自己验证了一段时间,检查账单以及服务器的运行程度,发现使用的很顺畅,不会有恶意扣费。
  3. 使用到快到1年的时候,可以把服务停掉,并重新申请个账号就可以继续用1年了。

如果以上这些点你觉得没问题,那么就可以按照接下来的教程进行申请和使用了。因为 Amazon EC2 和国内的一些服务器使用风格略有差异,所以需要按照教程创建并使用实例。

二、怎么免费白皮袄?

1. 创建账号

地址:portal.aws.amazon.com/billing/sig…

2. 登录账号

地址:console.aws.amazon.com/console/hom…

3. 控制面板

地址:us-east-1.console.aws.amazon.com/iamv2/home?… – 搜索 IAM 并进入,就是控制面板了。

在创建 Amazon EC2 云服务器前,需要在控制面板中创建;用户组、用户、角色,这样在后面创建服务器的时候才能把用户信息给关联上。

3.1 创建用户

3.2 创建用户组

4. 部署实例

4.1 创建密钥对

这里需要先创建一个密钥对,之后再创建应用实例的时候把密钥对关联上去。同时密钥对也是用作本地 SSH 登录的一个重要凭证,否则不能登录。另外这里创建完密钥对,会自动下载到本地,你需要保存好。

地址:us-east-2.console.aws.amazon.com/ec2/home?re… – EC2 管理控制台。

  • 创建完成后,会自动下载一个名为 admin_key.pem 的文件,保存好它,后面用。

4.2 创建安装组

用于访问服务器的端口都需要在安全组中开通,比如 SSH 22、Portainer 9000、Docker 2375 所以需要自己创建一个安全组。

地址:us-east-2.console.aws.amazon.com/ec2/home?re…

  • 以后你想控制各个端口的访问,就在安全组中操作就可以了。

4.4 启动新实例

如果你比较粗鲁,刚一进来 AWS 就创建了实例,但发现用户也不对,权限也不对用不了。没关系可以把它终止掉,之后重新启动新实例。

地址:us-east-2.console.aws.amazon.com/ec2/home?re…

如果在使用中把自己的应用实例搞坏了,没关系。直接干掉它,重新启动新实例就可以了。

5. 连接实例

Amazon EC2 云服务器提供了 EC2 在线连接、会话管理器、SSH 客户端、EC2 串行管理器。我们这里主要用在线的和 SSH 客户端。

EC2 Instance Connect SSH 客户端
  • EC2 在线连接,直接点上连接就可以了。SSH 客户端需要按照引导进行操作。

通过此方式就可以本地连接了,还是非常方便的。如果你有连接工具也可以配置到工具中操作。

三、部署应用耍起来!

1. 安装 Docker

sudo yum install -y yum-utils device-mapper-persistent-data lvm2

sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

yum list docker --showduplicates | sort -r

sudo yum install docker

sudo systemctl start docker

systemctl enable docker

docker --version
复制代码

2. 配置远程链接

vim /usr/lib/systemd/system/docker.service
在ExecStart=/usr/bin/dockerd-current 后面加上 -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock \
# 重新加载配置文件
systemctl daemon-reload
# 重启启动
systemctl restart docker
复制代码

3. 部署网页版 ChatGPT

方式 免费? 可靠性 质量
ChatGPTAPI(gpt-3.5-turbo-0301) 可靠 相对较笨
ChatGPTUnofficialProxyAPI(网页 accessToken) 相对不可靠 聪明

通过以下方式,你可以自己部署一个网页版的 ChatGPT,让自己使用。 注意:OPENAI_API_KEY 需要申请

  • 海外服务器;docker run --name chatgpt-web -d -p 3002:3002 --env OPENAI_API_KEY=sk-6cPOiRgKmIcMA**** chenzhaoyu94/chatgpt-web
  • 海内服务器;docker run --name chatgpt-web -d -p 3002:3002 --env OPENAI_API_KEY=sk-6cPOiRgKmIcMA**** --env OPENAI_API_BASE_URL=https://open2.aiproxy.xyz chenzhaoyu94/chatgpt-web – 需要添加代理

也可以不使用Key方式部署,这个方式的好处是免费,都免费!

  • docker run –name chatgpt-web -d -p 3002:3002 –env OPENAI_ACCESS_TOKEN=【从这里获取 chat.openai.com/api/auth/se… chenzhaoyu94/chatgpt-web

4. 部署 ChatGPT 问答助手

TradingView--最专业的走势图表,收下吧,也许你会用到 - 知乎

mikel阅读(863)

来源: TradingView–最专业的走势图表,收下吧,也许你会用到 – 知乎

前言

最近在做交易所项目里的K线图,得些经验,与大家分享。
代码居多,流量预警!!!!
点赞 收藏 不迷路。

技术选型

  • echrats
    • 基于canvas绘制,种类齐全的可视化图表库。
    • 官方地址:
  • highcharts
    • 基于svg绘制,可定制化内容很多,操纵dom更方便。
    • 官方地址:
  • tradingview
    • 基于canvas的专业全球化走势图表。
    • 官方地址:
  • 优缺点
    • hightcharts: 前些日子有仔细研究过 hightcharts

发现svg中的dom操作,以及定制化内容更好实现,但几乎都需要手动实现的这个特性在开发周期短的压迫下屈服了。上面的这个项目在慢慢摸索下也做了小三个月的样子,但还是很有成就感的。

    • echrats: echarts的官方案例很多,经常在做一些后台管理系统,展现数据时候会用到,方便,易用,使用者也足够多,搜索引擎鸡本能够解决你的任何问题。但对一些在图上划线,等操作,就显得略微疲软。不够能满足需求。
    • tradingview: 只要进入官网,就可见其专业性,他完全就是为了专业交易儿打造的,您只需要想里面填充数据就可以了,甚至在一些常用的交易内容上,可以使用tradingview自己的数据推送。
  • 小记
    • 所以,专业的交易图表,就交给专业的库来做吧
    • 手动狗头~~~~(∩_∩)

准备工作

  • 申请账号(key)
    • 在官网注册后会有邮件提示的,一步一步跟着做就可以了,这里就不做赘述了。
  • 环境搭建
    • 我使用的是自己搭建的React+webpack4脚手架,你也可以使用原生JS,或者你喜欢的任何框架(后面贴出来的代码都是在React环境下的)。
    • 从官方下载代码库
  • 了解websocket通讯协议
    • 发送请求
    • 接收数据
  • 大纲
    • 这里附上tradingview中文开发文档
    • 以及api示例

(此处需要自备梯子)

  • 一位大神的Demo

准备开始吧


创建

    page
    |--kLine // k线内容文件夹
    |--|--api // 需要使用的方法
    |--|--|--datafees.js // 定义了一些公用方法
    |--|--|--dataUpdater.js // 更新时调用的内容
    |--|--|--socket.js // websocket方法
    |--|--index.js // 自己代码开发
    |--|--index.scss // 样式开发
  • datafees.js加入如下代码
       /**
         * JS API
         */
        import React from 'react'
        import DataUpdater from './dataUpdater'
        
        class datafeeds extends React.Component {
        /**
         * JS API
         * @param {*Object} react react实例
         */
            constructor(self) {
                super(self)
                this.self = self
                this.barsUpdater = new DataUpdater(this)
                this.defaultConfiguration = this.defaultConfiguration.bind(this)
            }
            /**
             * @param {*Function} callback  回调函数
             * `onReady` should return result asynchronously.
             */
            onReady(callback) {
                // console.log('=============onReady running')
                return new Promise((resolve) => {
                    let configuration = this.defaultConfiguration()
                    if (this.self.getConfig) {
                        configuration = Object.assign(this.defaultConfiguration(), this.self.getConfig())
                    }
                    resolve(configuration)
                }).then(data => callback(data))
            }
            /**
             * @param {*Object} symbolInfo  商品信息对象
             * @param {*String} resolution  分辨率
             * @param {*Number} rangeStartDate  时间戳、最左边请求的K线时间
             * @param {*Number} rangeEndDate  时间戳、最右边请求的K线时间
             * @param {*Function} onDataCallback  回调函数
             * @param {*Function} onErrorCallback  回调函数
             */
            getBars(symbolInfo, resolution, rangeStartDate, rangeEndDate, onDataCallback) {
                const onLoadedCallback = (data) => {
                    data && data.length ? onDataCallback(data, { noData: false }) : onDataCallback([], { noData: true })
                }
                this.self.getBars(symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback)
                /* eslint-enable */
            }
            /**
             * @param {*String} symbolName  商品名称或ticker
             * @param {*Function} onSymbolResolvedCallback 成功回调
             * @param {*Function} onResolveErrorCallback   失败回调
             * `resolveSymbol` should return result asynchronously.
             */
            resolveSymbol(symbolName, onSymbolResolvedCallback, onResolveErrorCallback) {
                return new Promise((resolve) => {
                    // reject
                    let symbolInfoName
                    if (this.self.symbolName) {
                        symbolInfoName = this.self.symbolName
                    }
                    let symbolInfo = {
                        name: symbolInfoName,
                        ticker: symbolInfoName,
                        pricescale: 10000,
                    }
                    const { points } = this.props.props
                    const array = points.filter(item => item.name === symbolInfoName)
                    if (array) {
                        symbolInfo.pricescale = 10 ** array[0].pricePrecision
                    }
                    symbolInfo = Object.assign(this.defaultConfiguration(), symbolInfo)
                    resolve(symbolInfo)
                }).then(data => onSymbolResolvedCallback(data)).catch(err => onResolveErrorCallback(err))
            }
            /**
             * 订阅K线数据。图表库将调用onRealtimeCallback方法以更新实时数据
             * @param {*Object} symbolInfo 商品信息
             * @param {*String} resolution 分辨率
             * @param {*Function} onRealtimeCallback 回调函数
             * @param {*String} subscriberUID 监听的唯一标识符
             * @param {*Function} onResetCacheNeededCallback (从1.7开始): 将在bars数据发生变化时执行
             */
            subscribeBars(symbolInfo, resolution, onRealtimeCallback, subscriberUID, onResetCacheNeededCallback) {
                this.barsUpdater.subscribeBars(symbolInfo, resolution, onRealtimeCallback, subscriberUID, onResetCacheNeededCallback)
            }
            /**
             * 取消订阅K线数据
             * @param {*String} subscriberUID 监听的唯一标识符
             */
            unsubscribeBars(subscriberUID) {
                this.barsUpdater.unsubscribeBars(subscriberUID)
            }
            /**
             * 默认配置
             */
            defaultConfiguration = () => {
                const object = {
                    session: '24x7',
                    timezone: 'Asia/Shanghai',
                    minmov: 1,
                    minmov2: 0,
                    description: 'www.coinoak.com',
                    pointvalue: 1,
                    volume_precision: 4,
                    hide_side_toolbar: false,
                    fractional: false,
                    supports_search: false,
                    supports_group_request: false,
                    supported_resolutions: ['1', '15', '60', '1D'],
                    supports_marks: false,
                    supports_timescale_marks: false,
                    supports_time: true,
                    has_intraday: true,
                    intraday_multipliers: ['1', '15', '60', '1D'],
                }
                return object
            }
        }
        
        export default datafeeds 
  • dataUpdater加入如下代码
    class dataUpdater {
        constructor(datafeeds) {
            this.subscribers = {}
            this.requestsPending = 0
            this.historyProvider = datafeeds
        }
        subscribeBars(symbolInfonwq, resolutionInfo, newDataCallback, listenerGuid) {
            this.subscribers[listenerGuid] = {
                lastBarTime: null,
                listener: newDataCallback,
                resolution: resolutionInfo,
                symbolInfo: symbolInfonwq
            }
        }
        unsubscribeBars(listenerGuid) {
            delete this.subscribers[listenerGuid]
        }
        updateData() {
            if (this.requestsPending) return
            this.requestsPending = 0
            for (let listenerGuid in this.subscribers) {
                this.requestsPending++
                this.updateDataForSubscriber(listenerGuid).then(() => this.requestsPending--).catch(() => this.requestsPending--)
            }
        }
        updateDataForSubscriber(listenerGuid) {
            return new Promise(function (resolve, reject) {
              var subscriptionRecord = this.subscribers[listenerGuid];
              var rangeEndTime = parseInt((Date.now() / 1000).toString());
              var rangeStartTime = rangeEndTime - this.periodLengthSeconds(subscriptionRecord.resolution, 10);
              this.historyProvider.getBars(subscriptionRecord.symbolInfo, subscriptionRecord.resolution, rangeStartTime, rangeEndTime, function (bars) {
                this.onSubscriberDataReceived(listenerGuid, bars);
                resolve();
              }, function () {
                reject();
              });
            });
        }
        onSubscriberDataReceived(listenerGuid, bars) {
            if (!this.subscribers.hasOwnProperty(listenerGuid)) return
            if (!bars.length) return
            const lastBar = bars[bars.length - 1]
            const subscriptionRecord = this.subscribers[listenerGuid]
            if (subscriptionRecord.lastBarTime !== null && lastBar.time < subscriptionRecord.lastBarTime) return
            const isNewBar = subscriptionRecord.lastBarTime !== null && lastBar.time > subscriptionRecord.lastBarTime
            if (isNewBar) {
                if (bars.length < 2) {
                    throw new Error('Not enough bars in history for proper pulse update. Need at least 2.');
                }
                const previousBar = bars[bars.length - 2]
                subscriptionRecord.listener(previousBar)
            }
            subscriptionRecord.lastBarTime = lastBar.time
            console.log(lastBar)
            subscriptionRecord.listener(lastBar)
        }
        periodLengthSeconds =(resolution, requiredPeriodsCount) => {
            let daysCount = 0
            if (resolution === 'D' || resolution === '1D') {
                daysCount = requiredPeriodsCount
            } else if (resolution === 'M' || resolution === '1M') {
                daysCount = 31 * requiredPeriodsCount
            } else if (resolution === 'W' || resolution === '1W') {
                daysCount = 7 * requiredPeriodsCount
            } else {
                daysCount = requiredPeriodsCount * parseInt(resolution) / (24 * 60)
            }
            return daysCount * 24 * 60 * 60
        }
    }
    export default dataUpdater

  • socket.js加入如下代码(也可以使用自己的websocket模块)
     class socket {
            constructor(options) {
                this.heartBeatTimer = null
                this.options = options
                this.messageMap = {}
                this.connState = 0
                this.socket = null
            }
            doOpen() {
                if (this.connState) return
                this.connState = 1
                this.afterOpenEmit = []
                const BrowserWebSocket = window.WebSocket || window.MozWebSocket
                const socketArg = new BrowserWebSocket(this.url)
                socketArg.binaryType = 'arraybuffer'
                socketArg.onopen = evt => this.onOpen(evt)
                socketArg.onclose = evt => this.onClose(evt)
                socketArg.onmessage = evt => this.onMessage(evt.data)
                // socketArg.onerror = err => this.onError(err)
                this.socket = socketArg
            }
            onOpen() {
                this.connState = 2
                this.heartBeatTimer = setInterval(this.checkHeartbeat.bind(this), 20000)
                this.onReceiver({ Event: 'open' })
            }
            checkOpen() {
                return this.connState === 2
            }
            onClose() {
                this.connState = 0
                if (this.connState) {
                    this.onReceiver({ Event: 'close' })
                }
            }
            send(data) {
                this.socket.send(JSON.stringify(data))
            }
            emit(data) {
                return new Promise((resolve) => {
                    this.socket.send(JSON.stringify(data))
                    this.on('message', (dataArray) => {
                        resolve(dataArray)
                    })
                })
            }
            onMessage(message) {
                try {
                    const data = JSON.parse(message)
                    this.onReceiver({ Event: 'message', Data: data })
                } catch (err) {
                    // console.error(' >> Data parsing error:', err)
                }
            }
            checkHeartbeat() {
                const data = {
                    cmd: 'ping',
                    args: [Date.parse(new Date())]
                }
                this.send(data)
            }
            onReceiver(data) {
                const callback = this.messageMap[data.Event]
                if (callback) callback(data.Data)
            }
            on(name, handler) {
                this.messageMap[name] = handler
            }
            doClose() {
                this.socket.close()
            }
            destroy() {
                if (this.heartBeatTimer) {
                    clearInterval(this.heartBeatTimer)
                    this.heartBeatTimer = null
                }
                this.doClose()
                this.messageMap = {}
                this.connState = 0
                this.socket = null
            }
        }
        export default socket 

初始化图表

  • 可以同时请求websocket数据。
  • 新建init函数,并在onready/mounted/mounted等时候去调用(代码的含义在注释里,我尽量写的详细一点)
  init = () => {
        var resolution = this.interval; // interval/resolution 当前时间维度
        var chartType = (localStorage.getItem('tradingview.chartType') || '1')*1;
        var locale = this.props.lang; // 当前语言
        var skin = this.props.theme; // 当前皮肤(黑/白)
        if (!this.widgets) {
            this.widgets = new TradingView.widget({ // 创建图表
                autosize: true, // 自动大小(适配,宽高百分百)
                symbol:this.symbolName, // 商品名称
                interval: resolution,
                container_id: 'tv_chart_container', // 容器ID
                datafeed: this.datafeeds, // 配置,即api文件夹下的datafees.js文件
                library_path: '/static/TradingView/charting_library/', // 图表库的位置,我这边放在了static,因为已经压缩过
                enabled_features: ['left_toolbar'],
                timezone: 'Asia/Shanghai', // 图表的内置时区(常用UTC+8)
                // timezone: 'Etc/UTC', // 时区为(UTC+0)
                custom_css_url: './css/tradingview_'+skin+'.css', //样式位置
                locale, // 语言
                debug: false,
                disabled_features: [ // 在默认情况下禁用的功能
                    'edit_buttons_in_legend',
                    'timeframes_toolbar',
                    'go_to_date',
                    'volume_force_overlay',
                    'header_symbol_search',
                    'header_undo_redo',
                    'caption_button_text_if_possible',
                    'header_resolutions',
                    'header_interval_dialog_button',
                    'show_interval_dialog_on_key_press',
                    'header_compare',
                    'header_screenshot',
                    'header_saveload'
                ],
                overrides: this.getOverrides(skin), // 定制皮肤,默认无盖默认皮肤
                studies_overrides: this.getStudiesOverrides(skin) // 定制皮肤,默认无盖默认皮肤
            })
            var thats = this.widgets;
            // 当图表内容准备就绪时触发
            thats.onChartReady(function() {
                createButton(buttons);
            })
            var buttons = [
                {title:'1m',resolution:'1',chartType:1},
                {title:'15m',resolution:'15',chartType:1},
                {title:'1h',resolution:'60',chartType:1},
                {title:'1D',resolution:'1D',chartType:1},
            ];
            // 创建按钮(这里是时间维度),并对选中的按钮加上样式
            function createButton(buttons){
                for(var i = 0; i < buttons.length; i++){
                    (function(button){
                        let defaultClass =
                        thats.createButton()
                        .attr('title', button.title).addClass(`mydate ${button.resolution === '15' ? 'active' : ''}`)
                        .text(button.title)
                        .on('click', function(e) {
                            if (this.className.indexOf('active')> -1){// 已经选中
                                return false
                            }
                            let curent =e.currentTarget.parentNode.parentElement.childNodes
                            for(let index of curent) {
                                if (index.className.indexOf('my-group')> -1 && index.childNodes[0].className.indexOf('active')> -1) {
                                    index.childNodes[0].className = index.childNodes[0].className.replace('active', '')
                                }
                            }
                            this.className = `${this.className} active`
                            thats.chart().setResolution(button.resolution, function onReadyCallback() {})
                        }).parent().addClass('my-group'+(button.resolution == paramary.resolution ? ' active':''))
                    })(buttons[i])
                }
            }
        }
    }

请求数据

  • 新建initMessage函数—在需要去获取数据的时候,调取initMessage。
     initMessage = (symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback) => {
            let that = this
            //保留当前回调
            that.cacheData['onLoadedCallback'] = onLoadedCallback;
            //获取需要请求的数据数目
            let limit = that.initLimit(resolution, rangeStartDate, rangeEndDate)
            //如果当前时间节点已经改变,停止上一个时间节点的订阅,修改时间节点值
            if(that.interval !== resolution){
                that.interval = resolution
                paramary.endTime = parseInt((Date.now() / 1000), 10)
            } else {
                paramary.endTime = rangeEndDate
            }
            //获取当前时间段的数据,在onMessage中执行回调onLoadedCallback
            paramary.limit = limit
            paramary.resolution = resolution
            let param
            // 分批次获取历史(这边区分了历史记录分批加载的请求)
            if (isHistory.isRequestHistory) {
                param = {
                    // 获取历史记录时的参数(与全部主要区别是时间戳)
                }
            } else {
                param = {
                    // 获取全部记录时的参数
                }
            }
            this.getklinelist(param)
        } 
  • 在请求历史数据时,由于条件不满足,会一直请求后台接口,所以需要加上 函数节流
    • 在lodash这个库里面是有节流的方法的
    • 首先引入节流函数—-import throttle from 'lodash/throttle'
    • 使用非常简单,只要在函数前面套一层—–this.initMessage = throttle(this.initMessage, 1000);
      • throttle()函数里面,第一个参数是需要截留的函数,第二个为节流时间。

收到数据,渲染图表

  • 可以在接收数据的地方调用socket.on('message', this.onMessage(res.data))
  • onMessage函数,是为渲染数据进入图表内容
// 渲染数据
    onMessage = (data) => { // 通过参数将数据传递进来
        let thats = this
        if (data === []) {
            return
        }
        // 引入新数据的原因,是我想要加入缓存,这样在大数据量的时候,切换时间维度可以大大的优化请求时间
        let newdata = []
        if(data && data.data) {
            newdata = data.data
        }
        const ticker = `${thats.symbolName}-${thats.interval}`
        // 第一次全部更新(增量数据是一条一条推送,等待全部数据拿到后再请求)
        if (newdata && newdata.length >= 1 && !thats.cacheData[ticker] && data.firstHisFlag === 'true') {
            // websocket返回的值,数组代表时间段历史数据,不是增量
            var tickerstate = `${ticker}state`
            // 如果没有缓存数据,则直接填充,发起订阅
            if(!thats.cacheData[ticker]){
                thats.cacheData[ticker] = newdata
                thats.subscribe()   // 这里去订阅增量数据!!!!!!!
            }
            // 新数据即当前时间段需要的数据,直接喂给图表插件
            // 如果出现历史数据不见的时候,就说明 onLoadedCallback 是undefined
            if(thats.cacheData['onLoadedCallback']){ // ToDo
                thats.cacheData['onLoadedCallback'](newdata)
            }
            //请求完成,设置状态为false
            thats.cacheData[tickerstate] = false
            //记录当前缓存时间,即数组最后一位的时间
            thats.lastTime = thats.cacheData[ticker][thats.cacheData[ticker].length - 1].time
        }
        // 更新历史数据 (这边是添加了滑动按需加载,后面我会说明)
        if(newdata && newdata.length > 1 && data.firstHisFlag === 'true' && paramary.klineId === data.klineId && paramary.resolution === data.resolution && thats.cacheData[ticker] && isHistory.isRequestHistory) {
            thats.cacheData[ticker] = newdata.concat(thats.cacheData[ticker])
            isHistory.isRequestHistory = false
        }
        // 单条数据()
        if (newdata && newdata.length === 1 && data.hasOwnProperty('firstHisFlag') === false && data.klineId === paramary.klineId  && paramary.resolution === data.resolution) {
            //构造增量更新数据
            let barsData = newdata[0]
            //如果增量更新数据的时间大于缓存时间,而且缓存有数据,数据长度大于0
            if (barsData.time > thats.lastTime && thats.cacheData[ticker] && thats.cacheData[ticker].length) {
                //增量更新的数据直接加入缓存数组
                thats.cacheData[ticker].push(barsData)
                //修改缓存时间
                thats.lastTime = barsData.time
            } else if(barsData.time == thats.lastTime && thats.cacheData[ticker] && thats.cacheData[ticker].length){
                //如果增量更新的时间等于缓存时间,即在当前时间颗粒内产生了新数据,更新当前数据
                thats.cacheData[ticker][thats.cacheData[ticker].length - 1] = barsData
            }
            // 通知图表插件,可以开始增量更新的渲染了
            thats.datafeeds.barsUpdater.updateData()
        }
    } 

逻辑中心===>getbars

  • 新建getbars函数(该函数会在图表有变化时自动调用)
     getBars = (symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback) => {
            const timeInterval = resolution // 当前时间维度
            this.interval = resolution
            let ticker = `${this.symbolName}-${resolution}`
            let tickerload = `${ticker}load`
            var tickerstate = `${ticker}state`
            this.cacheData[tickerload] = rangeStartDate
            //如果缓存没有数据,而且未发出请求,记录当前节点开始时间
            // 切换时间或币种
            if(!this.cacheData[ticker] && !this.cacheData[tickerstate]){
                this.cacheData[tickerload] = rangeStartDate
                //发起请求,从websocket获取当前时间段的数据
                this.initMessage(symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback)
                //设置状态为true
                this.cacheData[tickerstate] = true
            }
            if(!this.cacheData[tickerload] || this.cacheData[tickerload] > rangeStartDate){
                //如果缓存有数据,但是没有当前时间段的数据,更新当前节点时间
                this.cacheData[tickerload] = rangeStartDate;
                //发起请求,从websocket获取当前时间段的数据
                this.initMessage(symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback);
                //设置状态为true
                this.cacheData[tickerstate] = !0;
            }
            //正在从websocket获取数据,禁止一切操作
            if(this.cacheData[tickerstate]){
                return false
            }
            // 拿到历史数据,更新图表
            if (this.cacheData[ticker] && this.cacheData[ticker].length > 1) {
                this.isLoading = false
                onLoadedCallback(this.cacheData[ticker])
            } else {
                let self = this
                this.getBarTimer = setTimeout(function() {
                    self.getBars(symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback)
                }, 10)
            }
            // 这里很重要,画圈圈----实现了往前滑动,分次请求历史数据,减小压力
            // 根据可视窗口区域最左侧的时间节点与历史数据第一个点的时间比较判断,是否需要请求历史数据
            if (this.cacheData[ticker] && this.cacheData[ticker].length > 1 && this.widgets && this.widgets._ready && !isHistory.isRequestHistory && timeInterval !== '1D') {
                const rangeTime = this.widgets.chart().getVisibleRange()  // 可视区域时间值(秒) {from, to}
                const dataTime = this.cacheData[ticker][0].time // 返回数据第一条时间
                if (rangeTime.from * 1000 <= dataTime + 28800000) { // true 不用请求 false 需要请求后续
                    isHistory.endTime = dataTime / 1000
                    isHistory.isRequestHistory = true
                    // 发起历史数据的请求
                    this.initMessage(symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback)
                }
            }
        }   

小记

  • tradingview主要就是这几个函数之间的搭配。
  • 使用onLoadedCallback(this.cacheData[ticker])或者this.datafeeds.barsUpdater.updateData()去更新数据。
  • 滑动加载时,可以先加载200条,后面每次150条,这样大大缩小了数据量,增加了渲染时间。
  • 滑动加载时的节流会经常用到。

进阶websocket

  • 二进制传输数据
  • websocket在传输数据的时候是明文传输,而且像K线上的历史数据,一般数据量比较大。为了安全性以及更快的加载出图表,我们决定使用二进制的方式传输数据。
    • 可以通过使用pako.js解压二进制数据
    • 引入pako.jsyarn add pako -S
    • 使用方法
      if (res.data instanceof Blob) { // 看下收到的数据是不是Blob对象
            const blob = res.data
            // 读取二进制文件
            const reader = new FileReader()
            reader.readAsBinaryString(blob)
            reader.onload = () => {
                // 首先对结果进行pako解压缩,类型是string,再转换成对象
                data = JSON.parse(pako.inflate(reader.result, { to: 'string' }))
            }
        }
    • 转换后,数据大小大概减少了20%。

差不多了


写在最后

  • 这里只分享些简单的内容,细节可以参照原生js版本的Demo github.com/tenggouwa/tr
  • 关于滚动加载,以及二进制的内容有问题的可以评论留言。
  • 如果这篇文章对你有帮助,或者是让您对tradingview有些了解,欢迎留言或点赞,我会一一回复。
  • 笔者最大的希望就是您能从我的文章里获得点什么,我就很开心啦。。。
  • 后面,至少每个月更新一篇文章。点赞关注不迷路啊,老铁。

Cursor 全自动代码生成器?你还没用过?接下来我就来介绍智能 AI 代码生成工具 Cursor 安装和使用 - 掘金

mikel阅读(1385)

来源: Cursor 全自动代码生成器?你还没用过?接下来我就来介绍智能 AI 代码生成工具 Cursor 安装和使用 – 掘金

一、概述

Cursor.so 是一个集成了 GPT-4 的国内直接可以访问的,优秀而强大的免费代码生成器,可以帮助你快速编写、编辑和讨论代码。 它支持多种编程语言,如 Python, Java, C#, JavaScript 等,并且可以根据你的输入和需求自动生成代码片段。 Cursor.so 还可以帮助你重构、理解和优化代码,提高开发效率。

强烈建议程序员们都要安装使用,将带来革命性的编码效率提升。本文简单介绍下 Cursor 的安装和使用。

二、安装

2.1 安装 cursor

官网地址:www.cursor.so/
支持 Mac 、Windows 和 Linux 操作系统,根据自己电脑的系统进行下载。

image.png

界面

首先,Cursor目前是一款独立的应用,界面看 上去像是VS Code, IDE的功能上也明显弱于 VS Code。界面上就三个菜单栏: File、 Edit、 View,然后就是右.上角的4个图标了。这里着 重关注一下View菜单下的Command Palette以 及右.上角的设置图标。

image.png

打开Command Palette之后,可以看到总共有 六个选项,重要的就是其中的4个选项: ●Ctrl+K: 负责代码生成与编辑

●Ctrl+L:负责代码理解和注释

image.png

点击setting按钮,出现一个设置的配置,需要 注意的就是Cursor编辑器支持vim、emacs; 支持绑定COpilot;支持安装不同语言的 server。

image.png

三、使用

image.png

3.1 生成代码

先创建一个 Java 文,然后使用 Ctrl + k 输入想让它生成怎样的代码

image.png 生成出来的代码:

image.png 注意:如果代码生成一半终止,可以重新呼出对话框,输入 “继续” 即可。

3.2 和代码“对话”

可以选择生成的部分代码,去问任何你想问题的问题,让它对代码进行优化。

选择后有两个选项,一个是 Edit ,即告诉它你的诉求,让它帮你修改;一个是 Chat , 即问它相关问题,让它给你解答。

image.png

3.2.1 对话

image.png

image.png 注意:你不仅可以和生成的代码“对话”,也可以将 JDK 、开源项目和自己业务代码贴到这里,进行“对话”,让它解释帮你理解,也可以让它给你改进建议,让它帮你找潜在BUG 等。

3.2.2 修改

image.png

它会根据你的意思进行修改,如果你认为符合要求,点击 Accept 即可生效,否则点击 Reject 拒绝修改。

image.png

点击 Accept 修改成功:

image.png

这个修改不一定是最优的但是基本符合需求。

四、总结

本文只是一个使用 Cursor 的简单示例,帮助大家如何安装和使用。 大家可以根据自己的业务,让它帮你写一些基础的代码,利用好 Cursor 可以大大提高工作效率。

需要注意的是,虽然可以通过语言来生成代码,但目前还不完美,如果生成的代码不太符合要求,可以优化自己的表达,交代再清楚一些;也可以尝试重新生成;还可以通过对话让它不断优化调整以便符合你的要求。 估计未来人工智能将带来开发工具的更大变革。 还有注意数据安全,不要将敏感数据和代码贴出来,以减少泄露风险。而且这些修改和建议只是参考,自己要注意甄别!

作者:小摩尔
链接:https://juejin.cn/post/7214288126222696508
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

layui table组件edit编辑事件获取单元格改之前的值,edit事件中给单元格赋值(附 判断一个js变量是否为数字)_layui edit 判断_面向顶风的博客-CSDN博客

mikel阅读(593)

来源: layui table组件edit编辑事件获取单元格改之前的值,edit事件中给单元格赋值(附 判断一个js变量是否为数字)_layui edit 判断_面向顶风的博客-CSDN博客

layui table组件edit编辑事件获取单元格改之前的值
table.on(‘edit(demo)’,function (obj) {
// 获取单元格编辑之前td的选择器
var selector = obj.tr.selector+’ td[data-field=”‘+obj.field+'”] div’;
// 单元格编辑之前的值
var oldtext = $(selector).text();
alert(oldtext);
});

obj.field就是表头参数的field 这个选择器同样可以用来改变单元格的值
.
.

edit单元格编辑事件中改变单元格的值

如果想要改变在edit单元格编辑事件中改变单元格的值,需要这样
例子
//判断输入的值是否为数字,不是数字提示并赋值回原本的值
table.on(‘edit(demo)’,function (obj) {
//获取编辑前的值
var selector = obj.tr.selector+’ td[data-field=”‘+obj.field+'”] div’;
var oldtext = $(selector).text();
//判断数据类型
if(!isNumber(obj.value)) {
layer.msg(‘请输入数字’);
console.log(oldtext);
// 重点 赋值
$(obj.tr.selector + ‘ td[data-field=”‘ + obj.field + ‘”] input’).val(oldtext);
}
});

之所以要这样赋值,是因为在点击layui table单元格的时候 layui会在td标签下添加一个input输入框,
在输入完成后调用你绑定的edit事件,最后在将input输入框的值覆盖到单元格div 的text里面

说以这样的写法是没有用的:

// 注意这是错误例子
table.on(‘edit(demo)’,function (obj) {
//获取编辑前的值
var selector = obj.tr.selector+’ td[data-field=”‘+obj.field+'”] div’;
var oldtext = $(selector).text();
//判断数据类型
if(!isNumber(obj.value)) {
layer.msg(‘请输入数字’);
console.log(oldtext);
// 赋值
$(obj.tr.selector + ‘ td[data-field=”‘ + obj.field + ‘”] div’).text(oldtext);
}
});

原因是执行顺序,在你edit事件调用

$(obj.tr.selector + ‘ td[data-field=”‘ + obj.field + ‘”] div’).text(oldtext);
1
改变单元格值之后,layui又把input的值覆盖到div,将你对单元格div的赋值覆盖掉了
.
.
对了,附上判断变量是否为数字的isNumber方法

/判断是非数字
function isNumber(val) {
var regPos = /^\d+(\.\d+)?$/; //非负浮点数
var regNeg = /^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$/; //负浮点数
if (regPos.test(val) || regNeg.test(val)) {
return true;
} else {
return false;
}
}
————————————————
版权声明:本文为CSDN博主「面向顶风」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_41746337/article/details/97146486

layui用table.render加载数据 用table.reload重载里面的数据---解决table.render重新加载闪动的问题_itfallrain的博客-CSDN博客

mikel阅读(645)

来源: layui用table.render加载数据 用table.reload重载里面的数据—解决table.render重新加载闪动的问题_itfallrain的博客-CSDN博客

今天在用layui 展示数据的时候,首先想到了table.render这个插件进行数据的展示,因为数据要实时刷新,说到实时刷新,你最低要三秒刷新一次表格的数据吧!!!一开始写了个定时把table.render放到定时函数里面,三秒执行一次函数,那么问题来了,虽然效果是实现了,但这是重新加载表格啊,三秒闪一次,别说是用户了,我都看不下去了,闪的眼疼,就想有没有只让数据重新加载,表格不动。终于功夫不负有心人!!!
tablePlug感觉就是救命稻草,就是解决这个问题的,这也是贤心人性补充吧
下面教你怎么用
1:首先奉上下载这个插件的地址
https://gitee.com/sun_zoro/layuiTablePlug
2:然后找到“下载”文件夹下把tablePlug文件夹复制到你项目的合适位置
3:敲黑板!!!把你项目一开始引入layui modules文件夹下的table.js给替换了(或者你手动修改源码),替换成你从git上下载的那个table.js(这个可能后续不需要修改,现在贤心还没整合!!!)
然后就是引入了

layui.config({base:’static/tablePlug/’}).use([‘table’,’tablePlug’], function(){
var table = layui.table;
var $ = layer.$;
var tablePlug=layui.tablePlug;
tablePlug.smartReload.enable(true);//处理不闪动的关键代码
table.render({
elem: ‘#testone’
, method : ‘POST’
,contentType: ‘application/json’
,url:UrlSchool.oneWen()
,id:”testoneTable”
,smartReloadModel:true
,cellMinWidth: 80 //全局定义常规单元格的最小宽度,layui 2.2.1 新增
,cols: [
[
{field:’deviceName’, title:’设备名称’, width:100}
,{field:’valveStatusName’, title:’阀门状态’, width:120, edit: ‘text’}
]
]
});
// var t1 = window.setInterval(hahahah,3000);
function hahahah(){
// 表格重载
table.reload(‘testoneTable’,{
url:UrlSchool.oneWen()
});
}
});

重点:需要重新加载数据要在table.render中加入 ,smartReloadModel:true(开启)
友情提示:table.reload这个不需要任何改动,组件会根据你reload里面的参数去判断是重新请求数据还是重载!!!
写的应该很详细了,觉得有问题的可以直接参考贤心原文解释!!!
最后的最后给你个贤心博文地址,如果遇到问题这里面有解决办法
https://fly.layui.com/jie/43423/
从这里往下看!!!

————————————————
版权声明:本文为CSDN博主「itfallrain」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_38215042/article/details/102705606

 EditPlus 格式化HTML JS CSS 教程及edtools分享链接_editplus html代码格式化_Seleey的博客-CSDN博客

mikel阅读(393)

 

来源: (1条消息) EditPlus 格式化HTML JS CSS 教程及edtools分享链接_editplus html代码格式化_Seleey的博客-CSDN博客

首先你得安装一个EditPlus,然后下载你想格式化的xxxformatter.js文件,也就是网上说的EDTOOLS工具包集合,包括html,css,js格式化工具文件。
我的edtools分享链接:
https://pan.baidu.com/s/12fUx8XhkeWsaqucAgLH9Jg 提取码: dm3h

1.:在tools栏—>打开Preferences窗口,点击User tools.

2.按照上图进行,设置。点击Group Name填入”前端开发工具”,
然后Add Tool,填入“html格式化”,填入Command的值
cscript /nologo “D:\software\edTools\htmlFormatter.js”
后面的就是文件工具路径

然后选择Action,

选择标出来的Action的值(第四个选项),
最后操作6,7就可以了。
继续tools

完成了吗,没有,还会出现错误,哈哈,

解决方法:创建reg文件 内容如下 点击运行:

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\.js]
@=”JSFile”
“Content Type”=”text/plain”
“PerceivedType”=”text”

[HKEY_CLASSES_ROOT\.js\OpenWithProgids]
“VisualStudio.js.b2b43195″=””
1
2
3
4
5
6
7
8
9
添加注册表成功后,重试一下,就我上面图一样格式化正常了,
尼玛,又乱码了。
解决方法:
1.出现乱码后按Ctrl+Z,回退到乱码前的正常显示状态

2.先把本文档按ANSI编码的先保存一遍(此时不会乱码,此步骤是保证中文不乱码正常显示)

(Editplus的编码设置步骤:

英文版在上边的工具栏找到Document——File Encoding——Convert Encoding——ANSI,

中文版:文件——文件编码——转换编码——选ANSI)

3.再按快捷键让edtools格式化代码(此时不会乱码)

4.再把文档按UTF-8的编码保存一遍
————————————————
版权声明:本文为CSDN博主「Seleey」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_44094872/article/details/101988341

 转载editplus格式化js,xml等____附输入错误: 没有文件扩展“.js”的脚本引擎解决_jsformatter.js_大脑袋硕鼠的博客-CSDN博客

mikel阅读(397)

来源: (1条消息) 转载editplus格式化js,xml等____附输入错误: 没有文件扩展“.js”的脚本引擎解决_jsformatter.js_大脑袋硕鼠的博客-CSDN博客

本人一般用editplus写一些小的测试代码或者来研究学习别人的代码,但经常会遇到这些问题:下载过来的HTML/CSS代码混乱,JS代码被压缩,或者是我们想把我们的代码做一下压缩混淆以供发布时使用。当然,对于代码的格式化和代码压缩等,我们都可以使用专有的工具,或者使用一些在线的工作来做,既然EditPlus可以扩展插件,那我们何不利用这一功能来把这些工具集成到EP中呢?

其实很早我都有这些想法,一直没有深究其做法,EP的用户工具组,我一般也是挂些帮助手册之类的供开发时参考。今天在BlueIdea看到有人发了一篇名为“让Editplus自动格式化css和js”的文章,看完后觉得写的很好,我也突然来了灵感,为什么不把前端开发常用工具都集成进去呢?

说做就做,我在网上找了一些相关的工具代码,按照作者的方式开始改造(作者是使用“本地cScript调用JS+EP的文本过滤器”的方式来实现的)。

 

     下面来说下集成的方法(以下以EditPlus3中文版本为例):

      1、下载工具包:edtools.rar ,解压后放到磁盘的一个目录,如D:/edTools。

      2、打开ED,打开“工具”-“用户工具组”,在弹出的对象框中,在“组和工具项目”下拉框中选择一个工具组,点击“组名称”,为该组工具设定一个名称,如“前端开发工具”,如下图所示:

 

 

3、下面开始加入工具,这里主要以JSFormat为例讲解,其它类似。
点击“添加”–“应用程序”,在新建的项中,菜单文本写上名称,如”jsFormat”,在命令里面输入:

 

 

  1. cscript /nologo “D:/edTools/jsFormatter.js”

 

 

后面引号中的内容要修改你磁盘上对应的文件的路径。
在下面的几个选项中,选择“运行为文本过滤”。如下图所示:

 

 

4、其它几个的安装方式与jsFormat的安装类似,这里不再重复。点击ED ,工具你会发现:jsFormat   Ctrl+1,打开一个js点击后你会发现奥秘。

 

 

下载地址: http://download.csdn.net/source/1907530

转自:CSDN:http://blog.csdn.net/misswuyang/article/details/5018143

然后crtl+1格式化错误”输入错误: 没有文件扩展“.js”的脚本引擎。”是因为你js格式文件被别的软件关联了

你可以

如系统中安装了ULTRAEDIT或者E钻加密软件等,就需要去掉关联;

或者如下解决方法:

在运行中输入“regedit”进入注册表,

只需要把[HKEY_CLASSES_ROOT\.js] 项下的那个默认值改成 “JSFile” 就可以正常运行JS 文件了。

ThinkPHP3.2.3扩展之生成PDF文件(MPDF) - 博文 - 手册网

mikel阅读(567)

来源: ThinkPHP3.2.3扩展之生成PDF文件(MPDF) – 博文 – 手册网

先安照路径放好如图。

2016-03-14_144416.gif

下面是使用方法

  1. public function pdf(){
  2.     //引入类库
  3.     Vendor(‘mpdf.mpdf’);
  4.     //设置中文编码
  5.     $mpdf=new \mPDF(‘zh-cn’,‘A4’, 0, ‘宋体’, 0, 0);
  6.     //html内容
  7.     $html=‘<h1><a name=”top”></a>一个PDF文件</h1>’;
  8.     $mpdf->WriteHTML($html);
  9.     $mpdf->Output();
  10.     exit;
  11. }

在浏览器中输入

  1. xxx/index.php?s=/Home/Index/pdf    //换成你的url路径

效果如下

2016-03-14_144911.gif

下面是高级一些的使用方法:

  1. public function pdf(){
  2.         //引入类库
  3.         Vendor(‘mpdf.mpdf’);
  4.         //设置中文编码
  5.         $mpdf=new \mPDF(‘zh-cn’,‘A4’, 0, ‘宋体’, 0, 0);
  6.         $mpdf->SetWatermarkText(‘中国水印’,0.1);
  7.         $strContent = ‘我是带水印的PDF文件’;
  8.         $mpdf->showWatermarkText = true;
  9.         $mpdf->SetHTMLHeader( ‘头部’ );
  10.         $mpdf->SetHTMLFooter( ‘底部’ );
  11.         //$stylesheet =file_get_contents(‘themes/wei/css/bootstrap.min.css’);
  12.         //$mpdf->WriteHTML($stylesheet, 1);
  13.         $mpdf->WriteHTML($strContent);
  14.         //保存ss.pdf文件
  15.         $mpdf->Output(‘ss.pdf’);
  16.         //直接浏览器输出pdf
  17.         $mpdf->Output(‘tmp.pdf’,true);
  18.         $mpdf->Output(‘tmp.pdf’,‘d’);
  19.         $mpdf->Output();
  20.         exit;
  21. }

2016-03-14_150552.gif

mpdf下载可以到官网下载http://www.mpdf1.com 或者在下面的百度云里下载

tcpdf开发文档(中文翻译版) - zh7314 - 博客园

mikel阅读(574)

来源: tcpdf开发文档(中文翻译版) – zh7314 – 博客园

2017年5月3日15:06:15

这个是英文翻译版,我看过作者的文档其实不太友善或者不方便阅读,不如wiki方便

后面补充一些,结构性文档翻译

这是一部官方网站文档,剩余大部分都是开发的时候和网络总结来的

项目官网:https://tcpdf.org/

github:https://github.com/tecnickcom/TCPDF

都没比较完整的api文档,所以最后的demo需要总去总结,好吧

(发现所有例子全部翻译消耗时间过于长,只把其中不同的部分分离出来,也方便读者使用)

 

2018年1月24日09:27:20

发现一个比较大的问题,不算bug,但是是一个比较蛋疼的 问题,当你需要直接将word或者excel的东西,直接获取出来,贴到编辑器,在拿出来,替换内容标签做pdf的时候,

tcpdf需要的是必须闭合的标签,要可以100%转换成xml,不然就报错 Undefined index: startcolumn in tcpdf.php (line 19456)

 

2018年1月29日17:43:18

因为新需要,合同页面变得很复杂,需要支持后端编辑合同模板,又得兼容合同编辑之后,在生成合同

解决办法,找前端直接使用table布局或则div+css,直接把页面写出来在存起来,不然就会报错,虽然麻烦一点,而且也不是那么灵活,但是如果在不随意改变合同模板样式的架构的情况下,还是可以使用的

笔者现在已经尝试了dompdf 但是目前看来这个的中文支持不好,css也好像支持不好,

如果你需要弄复杂页面的合同,请参看 :http://www.cnblogs.com/zx-admin/p/8352003.html

mpdf目前对table布局和div+css布局支持唯一个支持的比较好的插件

 

新增composer 使用tcpdf

composer require tecnickcom/tcpdf
//引入tcpdf
use TCPDF;

我使用laravel是5.4,但是服务上php是5.6但是laravel 这个组件 doctrine/inflector 1.2.x-dev requires php ^7.0

需要的是7,又不好直接服务器上php7,thinkphp最新版的话就直接上7,5.6版本不会出现类似的问题,好吧,我还是觉得tp好使

如果把laravl的计划任务模块,移植到tp上就爽了,有时间在弄这个

 

 

 

Fonts设置字体

注意:以下信息仅对旧的TCPDF库有效。 新的tc-lib-pdf库使用能够即时转换字体的新的tc-lib-pdf-字体库。

TCPDF支持TrueTypeUnicode(UTF-8 Unicode),OpenTypeUnicode,TrueType,OpenType,Type1,CID-0和Core(标准)字体。
有两种使用新字体的方法:将其嵌入PDF(有或没有子集)。 当未嵌入字体时,将在系统中进行搜索。 优点是PDF文件较轻; 另一方面,如果不可用,则使用替换字体。 因此,最好确保在客户端系统上安装所需的字体。 如果该文件要被大量观众查看,建议嵌入。

TCPDF支持字体子集,以减少大型unicode字体文件的文档大小。 如果您将整个字体嵌入到PDF中,则另一端的人即使没有您的字体也可以进行更改。 如果您对字体进行子集,则PDF的文件大小会更小,但是接收PDF的人需要具有相同的字体才能更改PDF。 有关启用/禁用字体子集的选项,请参见SetFont()和AddFont()方法的源代码文档。

可以不嵌入的字体只是标准的核心字体和CID-0字体。

PDF Core(标准)字体是:

  • courier : Courier
  • courierB : Courier Bold
  • courierBI : Courier Bold Italic
  • courierI : Courier Italic
  • helvetica : Helvetica
  • helveticaB : Helvetica Bold
  • helveticaBI : Helvetica Bold Italic
  • helveticaI : Helvetica Italic
  • symbol : Symbol
  • times : Times New Roman
  • timesB : Times New Roman Bold
  • timesBI : Times New Roman Bold Italic
  • timesI : Times New Roman Italic
  • zapfdingbats : Zapf Dingbats

转换TCPDF的字体

使用addTTFfont()方法,您可以直接从TrueType,OpenType或Type1字体创建一个TCPDF字体。
注意:’fonts’文件夹必须由webserver可写。

实例:

$fontname = $pdf->addTTFfont(‘/path-to-font/DejaVuSans.ttf’, ‘TrueTypeUnicode’, “, 32);

检查addTTFfont()的源代码文档以获取更多信息。

设置字体

在配置文件(config / tcpdf_config.php)上将K_PATH_FONTS常量设置为TCPDF字体路径。
在TCPDF类构造函数的第四个参数上,如果使用Unicode字体(true)或旧字体(false),则必须指定。

要在脚本中设置字体,只需调用SetFont()方法即可。 在打印文本或生成的文档无效之前,必须至少调用此方法一次。 该方法可以在创建第一个页面之前调用,字体从页面到页面保留:

SetFont(string family[,string style[,string size]])
  • family : 字体属性 它可以是字体名称或标准系列之一(不区分大小写):
    • Courier (fixed-width)
    • Helvetica or Arial (synonymous; sans serif)
    • Times (serif)
    • Symbol (symbolic)
    • ZapfDingbats (symbolic)

    也可以传递一个空字符串。 在这种情况下,现有的家庭被保留。

  • style : 字体样式。 可能的值是(不区分大小写):
    • empty string: regular
    • B: bold
    • I: italic
    • U: underline

    或任何组合。 默认值为常规值。

  • size:字体大小分。 默认值为当前大小。 如果文档开头没有指定大小,则取值为12。
  • fontfile : 字体定义文件。 默认情况下,名称是从家庭和样式构建的,小写没有空格。
  • subset :如果true只嵌入字体的一个子集(仅存储与所使用字符相关的信息); 如果假嵌入完整字体; 如果’default’使用setFontSubsetting()设置的默认值。 此选项仅对TrueTypeUnicode字体有效。 如果要启用用户更改文档,请将此参数设置为false。 如果您对字体进行分类,那么接收PDF的人将需要使用相同的字体才能更改PDF。 PDF的文件大小也会较小,因为您仅嵌入字体的一部分。

Example:

$pdf->SetFont(‘times’, ‘BI’, 20, “, ‘false’);

Performances(性能)

  • 安装和配置PHP操作码cacher,如XCache;
  • 编辑php.ini文件并增加脚本可能消耗的最大内存量(memory_limit);
  • 编辑php.ini文件并增加每个脚本的最大执行时间(max_execution_time);
  • 编辑config / tcpdf_config.php文件:手动设置$ _SERVER [‘DOCUMENT_ROOT’],K_PATH_MAIN和K_PATH_URL常量,并删除自动计算部分;
  • 如果您不使用泰语,请编辑config / tcpdf_config.php文件并将K_THAI_TOPCHARS常量设置为false;
  • 如果您不需要扩展字符,请编辑config / tcpdf_config.php文件,并将默认字体设置为核心字体;
  • 如果不需要UTF-8 Unicode,则将TCPID构造函数上的unicodefalse�������参数设置为�����,将 encoding参数设置为“ISO-8859-1”或其他字符映射。
  • 默认情况下,TCPDF允许字体子集减少嵌入式Unicode TTF字体的大小,这个过程非常慢,需要大量内存,可以使用setFontSubsetting(false)方法关闭;
  • 尽可能使用核心字体而不是嵌入字体;
  • 如果不严格要求,避免使用HTML语法(writeHTML和writeHTMLCell方法)
  • 拆分较小的大块HTML块;
  • 如果不严格要求,避免使用翻译;
  • 更改后重新启动网络服务器。

 

api使用分析实例

 

 

//去掉默认的页头页脚。比如那个横线
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(false);

 

//设置一个单元格,可以按比例缩放单元格大小
$pdf->Cell(0, 0, 'TEST CELL STRETCH: no stretch', 1, 1, 'C', 0, '', 0);

 

 

复制代码
//设置一个单元格,可以按比例缩放单元格大小
$pdf->Cell(0, 0, 'TEST CELL STRETCH: no stretch', 1, 1, 'C', 0, '', 0);


// 设置背景填充色
$pdf->SetFillColor(220, 255, 220);

/*
*此方法允许以换行符打印文本。
*它们可以是自动的(一旦文本到达单元格的右边界)或显式(通过\ n字符)。 输出所需的多个单元格,一个低于另一个。<br />
*文本可以对齐,居中或对齐。 单元格块可以框架并绘制背景
 */
//设置一个text文本块
$pdf->MultiCell(55, 5, '[LEFT] '.$txt, 1, 'L', 1, 0, '', '', true);
复制代码

 

 

 个人demo实例

注意pdf不是完全支持html标签,所以如果你的写入pdf的是html内容请使用txt或者qq对话框,过滤掉多数的html,

不然写入pdf就会异常,但是是openXML就不会有这个问题,写入word就没任何问题

 

复制代码
$pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
            $pdf->setPrintHeader(false);    //页面头部横线取消
            $pdf->setPrintFooter(false); //页面底部更显取消
            $pdf->SetAutoPageBreak(TRUE, PDF_MARGIN_BOTTOM);//自动分页
            $pdf->SetMargins(PDF_MARGIN_LEFT, PDF_MARGIN_TOP, PDF_MARGIN_RIGHT);//设置页面margin
            $pdf->SetFont('stsongstdlight', '', 12);//设置字体,注意在循环里面一定要把new都一起放在循环里面,不然会报错,没有设置字体,因为这个需要上下文来读取配置
            $pdf->AddPage();//增加一个页面
            $pdf->setCellPaddings(0, 0, 0, 0);//设置一个单元格的padding
            //追加用户亲笔签名到文件底部
            $identify = $v['identify'];
            $contract = M("contract")->where(array("identify" => "$identify"))->find();
            $file = M("file")->where(array("file_id" => $contract['contract_sign_pic_file_id']))->find();
            $sign_pic_url = app_standard_path_new($file['file_path']);

            //tcpdf支持远程图片,所以不用麻烦

            $pdf->writeHTML($contract_content, $ln = true, $fill = false, $reseth = false, $cell = false, $align = '1');//这个如果里面有远程图片,不能直接获取,需要在代码本地才行,把

            $pdf->writeHTML('<b>用户签名</b><br>', $ln = true, $fill = false, $reseth = false, $cell = false, $align = '1');

            $pdf->Image($sign_pic_url, '', '', '40%', '40%', '', '', 'T', false, 300, '', false, false, 1, false, false, false);
//这个可以获得远程图片地址,但是注意它一定是可以在公网可以访问或者授权的
//如果怕图片太大也是可以安比例缩小,放大的

// i 输出到浏览器,D下来php://output S保存
contractdata=������������=pdf->Output($file_name, ‘S’);

file_put_contents(tmpname,�������,contract_data);//所以写入你想写入的地方的文件

 


复制代码

 

新增实例,吧图片章浮动到文字上面demo

2018年1月17日11:34:27

 

例子的官方地址  https://tcpdf.org/examples/

 

Example 001 : first example with default Header and Footer  第一个例子设置默认页眉和页脚的

 

复制代码
<?php

// Include the main TCPDF library (search for installation path).
require_once('./tcpdf/tcpdf.php');

// create new PDF document
$pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);

// set document information
$pdf->SetCreator(PDF_CREATOR); //设置创建者
$pdf->SetAuthor('Nicola Asuni'); //设置作者
$pdf->SetTitle('TCPDF Example 001'); //设置文件的title
$pdf->SetSubject('TCPDF Tutorial'); //设置主题
$pdf->SetKeywords('TCPDF, PDF, example, test, guide'); //设置关键词
// set default header data
$pdf->SetHeaderData(PDF_HEADER_LOGO, PDF_HEADER_LOGO_WIDTH, PDF_HEADER_TITLE . ' 001', PDF_HEADER_STRING, array(0, 64, 255), array(0, 64, 128)); //设置头部,比如header_logo,header_title,header_string及其属性
$pdf->setFooterData(array(0, 64, 0), array(0, 64, 128));

// set header and footer fonts
$pdf->setHeaderFont(Array(PDF_FONT_NAME_MAIN, '', PDF_FONT_SIZE_MAIN)); //设置页头字体
$pdf->setFooterFont(Array(PDF_FONT_NAME_DATA, '', PDF_FONT_SIZE_DATA)); //设置页尾字体
// set default monospaced font
$pdf->SetDefaultMonospacedFont(PDF_FONT_MONOSPACED); //设置默认等宽字体
// set margins
$pdf->SetMargins(PDF_MARGIN_LEFT, PDF_MARGIN_TOP, PDF_MARGIN_RIGHT); //设置margins 参考css的margins
$pdf->SetHeaderMargin(PDF_MARGIN_HEADER); //设置页头margins
$pdf->SetFooterMargin(PDF_MARGIN_FOOTER); //设置页脚margins
// set auto page breaks
$pdf->SetAutoPageBreak(TRUE, PDF_MARGIN_BOTTOM); //设置自动分页
// set image scale factor
$pdf->setImageScale(PDF_IMAGE_SCALE_RATIO); //设置调整图像自适应比例
// set some language-dependent strings (optional) 设置一些与语言相关的字符串
if (@file_exists(dirname(__FILE__) . '/lang/eng.php')) {
    require_once(dirname(__FILE__) . '/lang/eng.php');
    $pdf->setLanguageArray($l);
}

// ---------------------------------------------------------
// set default font subsetting mode
$pdf->setFontSubsetting(true); //设置默认字体子集模式
// Set font
// dejavusans is a UTF-8 Unicode font, if you only need to
// print standard ASCII chars, you can use core fonts like
// helvetica or times to reduce file size.
$pdf->SetFont('dejavusans', '', 14, '', true); //设置字体
// Add a page
// This method has several options, check the source code documentation for more information.
$pdf->AddPage(); //增加一个页面
// set text shadow effect  设置文字阴影效果
$pdf->setTextShadow(array('enabled' => true, 'depth_w' => 0.2, 'depth_h' => 0.2, 'color' => array(196, 196, 196), 'opacity' => 1, 'blend_mode' => 'Normal'));

// Set some content to print
$html = <<<EOD
<h1>Welcome to <a href="http://www.tcpdf.org" style="text-decoration:none;background-color:#CC0000;color:black;">&nbsp;<span style="color:black;">TC</span><span style="color:white;">PDF</span>&nbsp;</a>!</h1>
<i>This is the first example of TCPDF library.</i>
<p>This text is printed using the <i>writeHTMLCell()</i> method but you can also use: <i>Multicell(), writeHTML(), Write(), Cell() and Text()</i>.</p>
<p>Please check the source code documentation and other examples for further information.</p>
<p style="color:#CC0000;">TO IMPROVE AND EXPAND TCPDF I NEED YOUR SUPPORT, PLEASE <a href="http://sourceforge.net/donate/index.php?group_id=128076">MAKE A DONATION!</a></p>
EOD;

/*
 * 
  此方法允许以换行符打印文本。
  它们可以是自动的(一旦文本到达单元格的右边界)或显式(通过\ n字符)。 输出所需的许多单元,一个低于另一个。
    文本可以对齐,居中或对齐。 单元格块可以框架并绘制背景。
 */

// Print text using writeHTMLCell() 
$pdf->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, true, '', true); //使用writeHTMLCell打印文本
// ---------------------------------------------------------
// Close and output PDF document
// This method has several options, check the source code documentation for more information.
$pdf->Output('example_001.pdf', 'I'); //I输出在浏览器上

//============================================================+
// END OF FILE
//============================================================+
复制代码

 

Example 002 : without Header and Footer  取消页眉和页脚

 

Example 003 : custom Header and Footer  自定义页眉和页脚

Example 004 : text Stretching with Cell()

Example 005 : Multicell()

Example 006 : WriteHTML()

Example 007 : independent columns with WriteHTMLCell()

Example 008 : external UTF-8 Unicode text file

Example 009 : Image()

Example 010 : text on multiple columns

Example 011 : table with primitive methods

Example 012 : graphic methods

Example 013 : graphic transformations

Example 014 : forms and JavaScript

Example 015 : index with Bookmarks()

Example 016 : document encryption

Example 017 : independent columns with MultiCell()

Example 018 : Persian and Arabic language on RTL document

Example 019 : alternative config file

Example 020 : complex alignment with Multicell()

Example 021 : writeHTML() text flow

Example 022 : CMYK colors

Example 023 : page groups

Example 024 : object visibility with setVisibility() and layers with startLayer()

Example 025 : object transparency with SetAlpha()

Example 026 : text clipping

Example 027 : 1D barcodes

Example 028 : multiple page formats

Example 029 : Set PDF viewer display preferences with setViewerPreferences()

Example 030 : colour gradients

Example 031 : pie chart graphic

Example 032 : EPS/AI vectorial image with ImageEPS()

Example 033 : mixed font types (TrueType Unicode, core, CID-0)

Example 034 : clipping masks

Example 035 : border styles with SetLineStyle()

Example 036 : PDF text annotations

Example 037 : spot colors

Example 038 : unembedded CID-0 CJK font

Example 039 : HTML text justification

Example 040 : booklet mode (double-sided pages)

Example 041 : file attachment

Example 042 : image with transparency (alpha channel)

Example 043 : disk caching

Example 044 : move, copy and delete pages

Example 045 : table of contents

Example 046 : text hyphenation

Example 047 : transactions and UNDO

Example 048 : HTML tables with header and rowspan

Example 049 : call TCPDF methods in HTML

Example 050 : 2D barcodes (QR-Code, Datamatrix ECC200 and PDF417)

Example 051 : image as a page background

Example 052 : digital signature certification

Example 053 : JavaScript functions

Example 054 : XHTML form

Example 055 : core fonts dump

Example 056 : crop marks and registration marks

Example 057 : vertical alignment and metrics on Cell()

Example 058 : SVG vectorial image with ImageSVG()

Example 059 : table of contents with HTML templates

Example 060 : advanced page settings

Example 061 : XHTML + CSS

Example 062 : XObject templates

Example 063 : text stretching and spacing (tracking/kerning)

Example 064 : no-write page regions

Example 065 : PDF/A-1b (ISO 19005-1:2005) document

Dapper简明教程 - 简书

mikel阅读(865)

来源: Dapper简明教程 – 简书

Dapper是一款轻量级的ORM框架,有关Dapper优缺点的文章网上一大堆,这里小编就不再赘述啦。下面直接进入正题:

使用前准备

  • 添加对Dapper的引用

在使用Dapper之前,我们要首先添加对Dapper的引用,这里小编使用NuGet来添加引用。因为小编使用的是MySQL数据库,所以也要在项目中添加对MySQL.Data的引用。

Dapper是一款ORM框架,用于数据表和实体模型间的映射,所以在使用前我们还需要创建数据表和实体模型。

  • 创建数据表

CREATE TABLE `t_schools` (
`Id`  int(11) NOT NULL AUTO_INCREMENT ,
`Name`  varchar(20)  NOT NULL ,
`Address`  varchar(50)  NOT NULL ,
PRIMARY KEY (`Id`)
)

CREATE TABLE `t_students` (
`Id`  int(11) NOT NULL AUTO_INCREMENT ,
`Name`  varchar(15) NOT NULL ,
`Number`  varchar(15)  NOT NULL ,
`SchoolId`  int(11) NOT NULL ,
`Gender`  enum('男','女','保密') NOT NULL ,
PRIMARY KEY (`Id`)
)
  • 创建模型

class School
{
    /*
      若属性名和数据库字段不一致(不区分大小写)则查询不出数据,如果使用EF则可以通过Column特性
      建立属性和数据表字段之间的映射关系,Dapper则不行
    */
    //[Column("Name")]
    public string Title { set; get; }
    public string Address { set; get; }
}

class Student
{
    public string Name { set; get; }
    public string Number { set; get; }
    public int SchoolId { set; get; }
}

Dapper的基本用法

const string _connectionString = "Database=Dapper;Data Source=127.0.0.1;User Id=root;Password=root;pooling=false;CharSet=utf8;port=3306;";
using (IDbConnection dbConnection = new MySqlConnection(_connectionString))
{
    dbConnection.Open();
    //通过匿名类型插入单条数据
    dbConnection.Execute("insert into t_schools(Name,Address) values(@Name,@Address)", new { Name = "西南大学", Address = "重庆市北碚区天生路2号" });
    //批量插入数据
    List<School> schools = new List<School>()
    {
      new School() {Address="China·BeiJing",Title="清华大学" },
      new School() {Address="杭州",Title="浙江大学" },
      new School() {Address="不知道,US?",Title="哈弗大学" }
    };
    //在执行参数化的SQL时,SQL中的参数(如@title可以和数据表中的字段不一致,但要和实体类型的属性Title相对应)
    dbConnection.Execute("insert into t_schools(Address,Name) values(@address,@title);", schools);
    //通过匿名类型批量插入数据
    dbConnection.Execute("insert into t_schools(Address,Name) values(@address,@name)", 
    new[] {
      new {Address="杨浦区四平路1239号",Name="同济大学"},
      new {Address="英国",Name="剑桥"},
      new {Address="美国·硅谷",Name="斯坦福大学"}
    });
}

使用Dapper进行查询操作

默认情况下Dapper会将查询到的整个数据集放到内存中,可以在Query方法中通过参数buffered来设置是否将查询结果存放到内存中

  • 查询结果映射到强类型

var schools = dbConnection.Query<School>("select * from t_schools where Name=@name", new { Name = "西南大学" });
foreach (var school in schools)
{
    Console.WriteLine(school.Address);
}
查询变量

有上图我们可以看到,因为School类中的Title属性在数据库中没有与之对应的字段,所以Title的值为null。

查询结果
  • 查询结果映射到匿名类型

在上面的查询中,我们将查询结果映射到了自定义的类型上。除了将查询结果映射到强类型之外,Dapper中还提供了匿名查询方式。

//查询结果result是匿名类型
var result = dbConnection.Query("select * from t_schools limit 0,3");
var resultList = result.AsList();
foreach (var l in resultList)
{
    Console.WriteLine(l.Name);
}
查询结果
  • in

var result = dbConnection.Query<Student>("select * from t_students where SchoolId in @schoolId", new { schoolId = new int[] { 2, 3 } });
foreach (var r in result)
{
    var ps = r.GetType().GetProperties();
    foreach (var p in ps)
    {
        Console.Write(p.Name + "=" + p.GetValue(r) + " ");
    }
    Console.WriteLine();
}
查询结果
  • between

var result = dbConnection.Query<School>("select Name,Address from t_schools where Id between @start and @end", new { start = 2, end = 4 });
foreach (var r in result)
{
    var ps = r.GetType().GetProperties();
    foreach (var p in ps)
    {
        Console.Write(p.Name + "=" + p.GetValue(r) + " ");
    }
    Console.WriteLine();
}
查询结果
  • join

使用join查询时需要用到Query方法中的splitOn参数,话说这个参数让小编纠结了很久才弄明白。关于splitOn参数的说明,可参考stackoverflow上的一篇文章Correct use of Multimapping in Dapper

var result = dbConnection.Query<Student, School, string>("select s.Name,sc.Address from t_students s,t_schools sc where s.SchoolId=sc.Id and binary sc.Address like '%BeiJing%'",
    (s, sc) =>
    {
        return s.Name + " " + sc.Address;
    }, 
    /*
    还有一点需要特别注意,泛型参数的顺序必须和SQL语句查询数据表的顺序一致,
    即Student对应t_students表的查询结果s.Name,否则Query方法的查询结果
    可能为null(这点也是困扰小编很久......)
    */
    splitOn: "Address"
    );
foreach (var r in result)
{
    Console.WriteLine(r);
}
查询结果

Dapper执行多条SQL语句

string sql = "select Address from t_schools;select SchoolId from t_students;select Name from t_students";
using (var multipleReader = dbConnection.QueryMultiple(sql))
{
    //一次执行N条SQL语句则最多只能调用N次Read方法,否则抛异常:the reader has been disposed.
    //Dapper读取查询结果数据的顺序必须要和查询语句中的查询顺序一致,否则可能读取不到数据
    var schoolList = multipleReader.Read<School>();
    foreach (var s in schoolList)
    {
        Console.Write(s.Address + " ");
    }
    Console.WriteLine();
    var studentSchools = multipleReader.Read<Student>();
    foreach (var s in studentSchools)
    {
        Console.Write(s.SchoolId + " ");
    }
    Console.WriteLine();
    var studentNames = multipleReader.Read<Student>();
    foreach (var s in studentNames)
    {
        Console.Write(s.Name + " ");
    }
}
查询结果

事务

使用Dapper执行事务倒是没有什么需要特别说明的。

using (IDbTransaction tran = dbConnection.BeginTransaction())
{
    try
    {
        dbConnection.Execute("delete from t_schools where Id=3", transaction: tran);
        throw new Exception();
        tran.Commit();
    }
    catch
    {
        tran.Rollback();
    }
}

存储过程

首先先创建一个存储过程

DROP PROCEDURE IF EXISTS `GetSchoolName`;
CREATE PROCEDURE `GetSchoolName`(in schoolId int,out scname varchar(20))
BEGIN
select `Name` into scname from t_schools where Id=schoolId;
select scname; 
END;

然后在程序中调用存储过程

//在程序中调用存储过程时,存储过程名要小写,传递的参数名要和存储过程中的参数名一致(不区分大小写)
//连接字符串中的数据库名也要小写,否则抛异常:在数据库***中找不到存储过程×××
var parameters = new DynamicParameters();
parameters.Add("@scname", dbType: DbType.String, direction: ParameterDirection.Output);
parameters.Add("schoolid", 6, direction: ParameterDirection.Input);
var result = dbConnection.Query("getschoolname", parameters, commandType: CommandType.StoredProcedure);
执行结果

从上图可以看出,返回值类型是Dapper中定义的DapperRow类型。

结语

  • Dapper是一个轻量级的ORM框架,它是通过扩展IDbConnection接口来实现一系列的功能的。相比EF、NHibernate,它的功能较为简单。
  • Dapper在执行查询语句时会缓存SQL语句的相关信息,这样就保证了Dapper拥有较高的性能(原文:Dapper caches information about every query it runs, this allow it to materialize objects quickly and process parameters quickly)。

以上内容是小编自己的一个学习总结,写出来一是作为自己的学习笔记,二是和广大网友分享。文中若有错误之处,还望各位读者能够指正。

参考文章:

Dapper
Dapper快速学习
Dapper中的一些复杂操作和inner join应该注意的坑
Dapper异常汇总
Correct use of Multimapping in Dapper

16人点赞

作者:雪飞鸿
链接:https://www.jianshu.com/p/b6cd7778567e
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。