费用报销混搭插件开发指南
在开发费用报销混搭应用时,我们会用到多个工具和技术,下面为你详细介绍整个开发过程。
1. 所需工具和库
- PayPal Sandbox :这是一个独立的环境,开发者可以在其中对PayPal应用进行原型设计和测试。它几乎模拟了实际PayPal环境中的所有功能,我们将使用它来模拟工资和费用报销的混搭。
-
Google APIs
:我们会使用四种不同的Google API,分别是Google账户认证API、Google数据API、Google文档列表数据API和Google电子表格数据API。
-
Google账户认证
:第三方应用可通过此API向Google API进行认证,有两种认证方式:
- 使用ClientLogin API(主要用于桌面应用,需要访问用户的登录凭证)。
- 使用AuthSub API(用于Web应用,无需用户的登录凭证)。在本次混搭中,我们使用ClientLogin API。
- Google数据API :为读写Google服务的数据提供了简单、标准的协议,使用基于XML的Atom 1.0和RSS 2.0联合格式以及Atom发布协议。获取数据需发送HTTP Get请求,更新数据发送HTTP Put请求,删除数据发送HTTP Delete请求,返回的数据为RSS或Atom提要。
- Google文档列表数据API :允许第三方应用通过Google数据API提要访问Google Docs中存储的文档,我们将用它在“approved - claims”文件夹中搜索电子表格,并将文字处理文档作为PDF文档检索。
- Google电子表格数据API :允许第三方应用通过Google数据API提要查看和更新Google电子表格中的数据,我们会用它从费用报销电子表格中获取数据并填充到支付数据库中。
-
Google账户认证
:第三方应用可通过此API向Google API进行认证,有两种认证方式:
- Ruby - PayPal库 :是围绕PayPal NVP API的轻量级包装库,为PayPal NVP API的输入提供基本验证支持,减少输入错误时的处理时间,还能对PayPal的响应进行解释,为Ruby开发者提供简单接口。安装命令如下:
$gem install ruby - paypal
- Acts_as_state_machine插件 :虽不是本次混搭严格必需的,但我们会用它让Payment类表现为状态机,可减少编写模拟Payment对象状态变化的代码量。安装命令如下:
$./script/plugin install http://elitists.textdriven.com/svn/plugins/acts_as_state_machine/trunk/
- XmlSimple :是一个Ruby API,可轻松读写XML格式的数据,是Perl模块XML::Simple的Ruby翻译版本,基于REXML(Ruby发行版中包含的XML解析器)编写。安装命令如下:
$gem install xml - simple
2. 开发步骤
- 创建Rails应用 :
$rails Chapter8
我们不会使用该应用的大部分界面,而是运行位于
$RAILS_ROOT/lib/tasks
文件夹中的rake脚本,且脚本运行应定期自动化。
2.
设置数据库
:
- 首先创建数据迁移脚本:
$./script/generate migration create_payments
生成的
001_create_payments.rb
文件内容如下:
class CreatePayments < ActiveRecord::Migration
def self.up
create_table :payments do |t|
t.column 'type', :string, :default => 'salary' # either salary or claims
t.column 'name', :string
t.column 'description', :string
t.column 'email', :string
t.column 'amount', :float
t.column 'state', :string, :default => 'pending' # states are pending, suspended and paid
t.column 'expense_evidence', :binary, :limit => 10.megabytes
t.column 'created_on', :datetime
t.column 'updated_on', :datetime
end
create_table :claim_items do |t|
t.column 'claim_id', :integer
t.column 'expense_date', :date
t.column 'project', :string
t.column 'item', :string
t.column 'remarks', :string
t.column 'created_on', :datetime
t.column 'amount', :float
end
end
def self.down
drop_table :payments
drop_table :claim_items
end
end
- 运行迁移命令创建表:
$rake db:migrate
- 创建Payment和Claim Item支架 :
$./script/generate scaffold Payment
$./script/generate scaffold ClaimItem
-
修改Payment并创建其子类
:
修改$RAILS_ROOT/app/models文件夹中的payment.rb文件:
class Payment < ActiveRecord::Base
acts_as_state_machine :initial => :pending
state :pending, :enter => :add_to_account_payable
state :paid, :enter => :log_payment
state :suspended
event :pay do
transitions :from => :pending, :to => :paid
end
event :suspend do
transitions :from => :pending, :to => :suspended
end
event :unsuspend do
transitions :from => :suspended, :to => :pending
end
def add_to_account_payable
# hook up with accounts system to record expense in account payable
end
def log_payment
# hook up with accounts system to log actual payment
end
end
创建
Salary
和
Claim
子类:
class Salary < Payment
end
class Claim < Payment
has_many :claim_items
end
3. 创建Google API访问库
在
$RAILS_ROOT/lib
文件夹中创建
gdata.rb
文件:
require 'net/http'
require 'net/https'
require 'open-uri'
require 'xmlsimple'
GOOGLE_CLIENT_LOGIN_URL = 'www.google.com/accounts/ClientLogin'
GOOGLE_DOCS_URL = 'docs.google.com'
GOOGLE_SPREADSHEETS_URL = 'spreadsheets.google.com'
SPREADSHEET_CATEGORY = {"term"=>"http://schemas.google.com/docs/2007#spreadsheet",
"scheme"=>"http://schemas.google.com/g/2005#kind",
"label"=>"spreadsheet"}
# convenience module to use HTTP (mainly used for login)
module Net
class HTTPS < HTTP
def initialize(address, port = nil)
super(address, port)
self.use_ssl = true
end
end
end
class GData
def login(email, password)
@user_id = email
gdoc_params = { 'Email' => email,
'Passwd' => password,
'source' => 'saush-gdocs-01',
'accountType' => 'HOSTED_OR_GOOGLE',
'service' => 'writely'
}
gss_params = { 'Email' => email,
'Passwd' => password,
'source' => 'saush-gss-01',
'accountType' => 'HOSTED_OR_GOOGLE',
'service' => 'wise'
}
gdoc_response = Net::HTTPS.post_form(
URI.parse("https://#{GOOGLE_CLIENT_LOGIN_URL}"),
gdoc_params)
gdoc_response.error! unless gdoc_response.kind_of?
Net::HTTPSuccess
@gdoc_token = gdoc_response.body.split(/=/).last
gss_response = Net::HTTPS.post_form(
URI.parse("https://#{GOOGLE_CLIENT_LOGIN_URL}"),
gss_params)
gss_response.error! unless gss_response.kind_of?
Net::HTTPSuccess
@gss_token = gss_response.body.split(/=/).last
end
# Get a Google Docs feed
def gdoc_feed(feed)
results = ''
open("http://" + GOOGLE_DOCS_URL + feed, 'Authorization' =>
"GoogleLogin auth=#{@gdoc_token}") { |s|
results = XmlSimple::xml_in(s.read, 'force_array' => false)
}
return results
end
# Get a Google Spreadsheets feed
def gss_feed(feed)
results = ''
open("http://" + GOOGLE_SPREADSHEETS_URL + feed, 'Authorization'
=> "GoogleLogin auth=#{@gss_token}") { |s|
results = XmlSimple::xml_in(s.read, 'force_array' => false)
}
return results
end
# returns all spreadsheets in a given folder
# returns an array of GSpreadsheet objects
def spreadsheets_in_folder(folder)
feed = gdoc_feed("/feeds/documents/private/full/-/%7Bhttp:%2F%2Fschemas.google.com%2Fdocs%2F2007%2Ffolders%2F#{@user_id}%7D#{folder}")
spreadsheets = []
spreadsheet_data = []
if feed['totalResults'].to_i > 1 then
spreadsheet_data = spreadsheet_data + feed['entry']
else
spreadsheet_data = spreadsheet_data << feed['entry']
end
spreadsheet_data.each { |doc|
if doc['category'].include? SPREADSHEET_CATEGORY then
ss = Spreadsheet.new
ss.title = doc['title']['content']
ss.author = doc['author']['name']
ss.spreadsheet_id = doc['id']
doc['link'].each { |link|
case link['rel']
when "http://schemas.google.com/spreadsheets/2006#worksheetsfeed"
# hack to overcome bug in Google Spreadsheets http://code.google.com/p/gdata-issues/issues/detail?id=321
wks_link = link['href'].sub "trix.", ""
ss.worksheets = get_worksheets_from(wks_link)
when "alternate"
ss.link = link['href']
when "edit"
ss.edit_link = link['href']
end
}
ss.updated_on = doc['updated']
spreadsheets << ss
end
}
return spreadsheets
end
# get a list of worksheets in this spreadsheet
def get_worksheets_from(worksheetfeed)
uri = URI.parse worksheetfeed
worksheets = []
feed = gss_feed(uri.path)
if feed['totalResults'].to_i > 1 then
feed['entry'].each {|ws|
worksheets << populate_worksheet(ws)
}
else
worksheets << populate_worksheet(feed['entry'])
end
return worksheets
end
# populate a Worksheet object from a feed
def populate_worksheet(data)
ws = Worksheet.new
ws.title = data['title']['content']
ws.row_count = data['rowCount']
ws.col_count = data['colCount']
ws.worksheet_id = data['id']
data['link'].each { |link|
case link['rel']
when "http://schemas.google.com/spreadsheets/2006#listfeed"
ws.rows = get_rows(link['href'])
when "edit"
ws.edit_link = link['href']
end
}
return ws
end
# get row data from list feed
def get_rows(listfeed)
uri = URI.parse listfeed
rows = []
feed = gss_feed(uri.path)
if feed['totalResults'].to_i > 1 then
feed['entry'].each { |row|
rows << Row.new.merge(row)
}
elsif feed['totalResults'].to_i == 1 then
rows << Row.new.merge(feed['entry'])
end
return rows
end
def get_pdf_document(docid)
results = ''
url = "http://docs.google.com/MiscCommands?command=saveasdoc&exportformat=pdf&docID=#{docid}"
open(url, 'Authorization' => "GoogleLogin auth=#{@gdoc_token}") { |s|
results = s.read
}
return results
end
# delete the spreadsheet
def delete(spreadsheet)
url = URI.parse(spreadsheet.edit_link)
res = Net::HTTP.new(url.host, url.port).start {|http| http.delete(url.path, 'Authorization' => "GoogleLogin auth=#{@gdoc_token}") }
case res
when Net::HTTPSuccess, Net::HTTPRedirection
return true
else
return false
end
end
end
# Models a Google spreadsheet
class Spreadsheet
attr_accessor :title, :author, :spreadsheet_id,
:worksheets, :link, :edit_link,
:updated_on
end
# Models a worksheet in a spreadsheet under Google Spreadsheets
class Worksheet
attr_accessor :title, :row_count, :col_count,
:worksheet_id, :rows, :edit_link
end
# Models a row in a worksheet, in a spreadsheet under Google Spreadsheets
class Row < Hash
def method_missing(m,*a)
if m.to_s.upcase =~ /=$/
self[$`] = a[0]
elsif a.empty?
self[m.to_s]
else
raise NoMethodError, "#{m}"
end
end
end
这个库是从Google电子表格中提取信息的核心代码,下面为你详细解释其工作原理。
4. Google API访问库工作原理
- 登录方法 :通过ClientLogin API向Google账户认证API发送HTTP Post请求,获取认证令牌。由于不能对Google Docs和Google电子表格使用相同的令牌,所以需要登录两次。
def login(email, pwd)
@user_id = email
gdoc_params = { 'Email' => email,
'Passwd' => pwd,
'source' => 'saush-gdocs-01',
'accountType' => 'HOSTED_OR_GOOGLE',
'service' => 'writely'
}
gss_params = { 'Email' => email,
'Passwd' => pwd,
'source' => 'saush-gss-01',
'accountType' => 'HOSTED_OR_GOOGLE',
'service' => 'wise'
}
gdoc_response = Net::HTTPS.post_form(URI.parse("https://#{GOOGLE_URL}/accounts/ClientLogin"), gdoc_params)
gdoc_response.error! unless gdoc_response.kind_of? Net::HTTPSuccess
@gdoc_token = gdoc_response.body.split(/=/).last
gss_response = Net::HTTPS.post_form(URI.parse("https://#{GOOGLE_URL}/accounts/ClientLogin"), gss_params)
gss_response.error! unless gss_response.kind_of? Net::HTTPSuccess
@gss_token = gss_response.body.split(/=/).last
end
- 获取提要方法 :使用Open URI获取Google Docs和Google电子表格的提要,并通过XmlSimple进行解析。
def gdoc_feed(feed)
results = ''
open("http://" + GOOGLE_DOCS_URL + feed, 'Authorization' =>
"GoogleLogin auth=#{@gdoc_token}") { |s|
results = XmlSimple::xml_in(s.read, 'force_array' => false)
}
return results
end
def gss_feed(feed)
results = ''
open("http://" + GOOGLE_SPREADSHEETS_URL + feed, 'Authorization'
=> "GoogleLogin auth=#{@gss_token}") { |s|
results = XmlSimple::xml_in(s.read, 'force_array' => false)
}
return results
end
-
容器对象
:
- Spreadsheet类 :用于建模Google电子表格文档,包含标题、作者、电子表格ID、工作表数组等属性。
- Worksheet类 :建模电子表格中的工作表,包含标题、行数、列数、工作表ID、行数组等属性。
-
Row类
:继承自Hash,重写了
method_missing方法,可通过列名访问行数据。
5. 数据解析流程
我们可以用下面的mermaid流程图来展示从Google电子表格中解析数据的流程:
graph TD;
A[开始] --> B[登录Google API获取令牌];
B --> C[获取指定文件夹中的电子表格];
C --> D[遍历电子表格];
D --> E{是否为电子表格};
E -- 是 --> F[获取电子表格的工作表];
F --> G[遍历工作表];
G --> H[获取工作表的行数据];
H --> I[处理行数据];
E -- 否 --> D;
I --> J[删除已处理的电子表格];
J --> K[结束];
通过以上步骤,我们可以完成费用报销混搭应用的开发,实现从Google电子表格中提取费用报销数据并进行处理的功能。后续我们还会继续创建Manager类及其控制器和视图、费用报销解析rake脚本、批量支付rake脚本,并修改Payment和Claim Item控制器等。
费用报销混搭插件开发指南
6. 解析电子表格数据示例
为了更清晰地理解数据解析过程,我们来看一个具体的电子表格及其解析结果。假设我们有一个名为“Peter”的电子表格,其ATOM提要如下:
<entry>
<id>http://docs.google.com/feeds/documents/private/full/spreadsheet%3ApafEr_vqlVZWG3p3RrZQUeQ</id>
<updated>2008-02-02T00:54:37.383Z</updated>
<category scheme="http://schemas.google.com/docs/2007/folders/somemail@gmail.com" term="approved-claims" label="approved-claims"/>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/docs/2007#spreadsheet" label="spreadsheet"/>
<title type="text">Peter</title>
<content type="text/html" src="http://spreadsheets.google.com/fm?fmcmd=102&key=pafEr_vqlVZWG3p3RrZQUeQ"/>
<link rel="alternate" type="text/html" href="http://spreadsheets.google.com/ccc?key=pafEr_vqlVZWG3p3RrZQUeQ"/>
<link rel="http://schemas.google.com/spreadsheets/2006#worksheetsfeed" type="application/atom+xml" href="http://spreadsheets.google.com/feeds/worksheets/pafEr_vqlVZWG3p3RrZQUeQ/private/full"/>
<link rel="self" type="application/atom+xml" href="http://docs.google.com/feeds/documents/private/full/spreadsheet%3ApafEr_vqlVZWG3p3RrZQUeQ"/>
<link rel="edit" type="application/atom+xml" href="http://docs.google.com/feeds/documents/private/full/spreadsheet%3ApafEr_vqlVZWG3p3RrZQUeQ/fc5gdcuf"/>
<author>
<name>sausheong</name>
<email>username@gmail.com</email>
</author>
</entry>
通过XmlSimple解析后,得到的哈希数据如下:
{
"category" => [
{
"term" => "approved-claims",
"scheme" => "http://schemas.google.com/docs/2007/folders/somemail@gmail.com",
"label" => "approved-claims"
},
{
"term" => "http://schemas.google.com/docs/2007#spreadsheet",
"scheme" => "http://schemas.google.com/g/2005#kind",
"label" => "spreadsheet"
}
],
"title" => {
"type" => "text",
"content" => "Peter"
},
"author" => {
"name" => "sausheong",
"email" => "somemail@gmail.com"
},
"id" => "http://docs.google.com/feeds/documents/private/full/spreadsheet%3ApafEr_vqlVZWG3p3RrZQUeQ",
"content" => {
"src" => "http://spreadsheets.google.com/fm?fmcmd=102&key=pafEr_vqlVZWG3p3RrZQUeQ",
"type" => "text/html"
},
"link" => [
{
"href" => "http://spreadsheets.google.com/ccc?key=pafEr_vqlVZWG3p3RrZQUeQ",
"rel" => "alternate",
"type" => "text/html"
},
{
"href" => "http://spreadsheets.google.com/feeds/worksheets/pafEr_vqlVZWG3p3RrZQUeQ/private/full",
"rel" => "http://schemas.google.com/spreadsheets/2006#worksheetsfeed",
"type" => "application/atom+xml"
},
{
"href" => "http://docs.google.com/feeds/documents/private/full/spreadsheet%3ApafEr_vqlVZWG3p3RrZQUeQ",
"rel" => "self",
"type" => "application/atom+xml"
},
{
"href" => "http://docs.google.com/feeds/documents/private/full/spreadsheet%3ApafEr_vqlVZWG3p3RrZQUeQ/fc5gdcuf",
"rel" => "edit",
"type" => "application/atom+xml"
}
],
"updated" => "2008-02-02T00:54:37.383Z"
}
从这个哈希数据中,我们可以获取电子表格的各种信息,如标题、作者、ID、链接等。其中,我们关注的工作表提要链接可以通过
hash['link']
中
rel
为
http://schemas.google.com/spreadsheets/2006#worksheetsfeed
的元素获取。
接着,我们获取该电子表格的工作表提要,示例如下:
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:openSearch="http://a9.com/-/spec/opensearchrss/1.0/" xmlns:gs="http://schemas.google.com/spreadsheets/2006">
<id>http://spreadsheets.google.com/feeds/worksheets/pafEr_vqlVZWG3p3RrZQUeQ/private/full</id>
<updated>2008-02-02T00:54:37.383Z</updated>
<category scheme="http://schemas.google.com/spreadsheets/2006" term="http://schemas.google.com/spreadsheets/2006#worksheet"/>
<title type="text">Peter</title>
<link rel="alternate" type="text/html" href="http://spreadsheets.google.com/ccc?key=pafEr_vqlVZWG3p3RrZQUeQ"/>
<link rel="http://schemas.google.com/g/2005#feed" type="application/atom+xml" href="http://spreadsheets.google.com/feeds/worksheets/pafEr_vqlVZWG3p3RrZQUeQ/private/full"/>
<link rel="http://schemas.google.com/g/2005#post" type="application/atom+xml" href="http://spreadsheets.google.com/feeds/worksheets/pafEr_vqlVZWG3p3RrZQUeQ/private/full"/>
<link rel="self" type="application/atom+xml" href="http://spreadsheets.google.com/feeds/worksheets/pafEr_vqlVZWG3p3RrZQUeQ/private/full"/>
<author>
<name>sausheong</name>
<email>somemail@gmail.com</email>
</author>
<openSearch:totalResults>4</openSearch:totalResults>
<openSearch:startIndex>1</openSearch:startIndex>
<entry>
<id>http://spreadsheets.google.com/feeds/worksheets/pafEr_vqlVZWG3p3RrZQUeQ/private/full/od6</id>
<updated>2008-02-02T00:54:37.383Z</updated>
<category scheme="http://schemas.google.com/spreadsheets/2006" term="http://schemas.google.com/spreadsheets/2006#worksheet"/>
<title type="text">Items</title>
<content type="text">Items</content>
<link rel="http://schemas.google.com/spreadsheets/2006#listfeed" type="application/atom+xml" href="http://spreadsheets.google.com/feeds/list/pafEr_vqlVZWG3p3RrZQUeQ/od6/private/full"/>
<link rel="http://schemas.google.com/spreadsheets/2006#cellsfeed" type="application/atom+xml" href="http://spreadsheets.google.com/feeds/cells/pafEr_vqlVZWG3p3RrZQUeQ/od6/private/full"/>
<link rel="self" type="application/atom+xml" href="http://spreadsheets.google.com/feeds/worksheets/pafEr_vqlVZWG3p3RrZQUeQ/private/full/od6"/>
<link rel="edit" type="application/atom+xml" href="http://spreadsheets.google.com/feeds/worksheets/pafEr_vqlVZWG3p3RrZQUeQ/private/full/od6/bu1jir0swo"/>
<gs:rowCount>100</gs:rowCount>
<gs:colCount>20</gs:colCount>
</entry>
</feed>
我们关注的是列表提要链接,它指向工作表的行数据。获取列表提要中的一行数据示例如下:
<entry>
<id>http://spreadsheets.google.com/feeds/list/pafEr_vqlVZWG3p3RrZQUeQ/od6/private/full/cokwr</id>
<updated>2008-02-02T00:54:37.383Z</updated>
<category scheme="http://schemas.google.com/spreadsheets/2006" term="http://schemas.google.com/spreadsheets/2006#list"/>
<title type="text">3/10/2008</title>
<content type="text">amount: $9.50, item: Taxi, project: Project A, remarks: Taxi to the hotel</content>
<link rel="self" type="application/atom+xml" href="http://spreadsheets.google.com/feeds/list/pafEr_vqlVZWG3p3RrZQUeQ/od6/private/full/cokwr"/>
<link rel="edit" type="application/atom+xml" href="http://spreadsheets.google.com/feeds/list/pafEr_vqlVZWG3p3RrZQUeQ/od6/private/full/cokwr/gf6ji7n3f2d53"/>
<gsx:date>3/10/2008</gsx:date>
<gsx:amount>$9.50</gsx:amount>
<gsx:item>Taxi</gsx:item>
<gsx:project>Project A</gsx:project>
<gsx:remarks>Taxi to the hotel</gsx:remarks>
</entry>
通过XmlSimple解析后,得到的行数据哈希如下:
{
"remarks" => "Taxi to the hotel",
"category" => {
"term" => "http://schemas.google.com/spreadsheets/2006#list",
"scheme" => "http://schemas.google.com/spreadsheets/2006"
},
"title" => {
"type" => "text",
"content" => "3/10/2008"
},
"project" => "Project A",
"date" => "3/10/2008",
"id" => "http://spreadsheets.google.com/feeds/list/pafEr_vqlVZWG3p3RrZQUeQ/od6/private/full/cokwr",
"amount" => "$9.50",
"content" => {
"type" => "text",
"content" => "amount: $9.50, item: Taxi, project: Project A, remarks: Taxi to the hotel"
},
"item" => "Taxi",
"link" => [
{
"href" => "http://spreadsheets.google.com/feeds/list/pafEr_vqlVZWG3p3RrZQUeQ/od6/private/full/cokwr",
"rel" => "self",
"type" => "application/atom+xml"
},
{
"href" => "http://spreadsheets.google.com/feeds/list/pafEr_vqlVZWG3p3RrZQUeQ/od6/private/full/cokwr/gf6ji7n3f2d53",
"rel" => "edit",
"type" => "application/atom+xml"
}
],
"updated" => "2008-02-02T00:54:37.383Z"
}
我们可以看到,通过调用
hash['remarks']
等方式可以方便地获取对应列的数据,这正是
Row
类重写
method_missing
方法的作用。
7. 后续开发计划
接下来,我们将按照以下步骤继续完成费用报销混搭应用的开发:
1.
创建Manager类及其控制器和视图
:Manager类将负责管理费用报销的流程,控制器和视图将提供用户交互界面,方便管理人员进行操作。
2.
创建费用报销解析rake脚本
:该脚本将定期运行,自动从Google电子表格中解析费用报销数据,并将其存储到数据库中。
3.
创建批量支付rake脚本
:使用Ruby - PayPal库,该脚本将实现批量支付功能,将报销款项支付给员工。
4.
修改Payment和Claim Item控制器
:对控制器进行修改,以确保数据的正确处理和显示,同时与其他模块进行良好的交互。
8. 总结
通过本文,我们详细介绍了费用报销混搭应用的开发过程,包括所需的工具和库、开发步骤、Google API访问库的创建和工作原理,以及数据解析流程。我们还通过具体的示例展示了如何解析电子表格数据。后续的开发工作将围绕Manager类、rake脚本和控制器的创建和修改展开,最终实现一个完整的费用报销管理系统。在开发过程中,我们需要注意Google API的使用规范和数据处理的准确性,确保系统的稳定性和可靠性。以下是整个开发过程的关键步骤总结表格:
| 步骤 | 操作内容 |
| ---- | ---- |
| 1 | 创建Rails应用 |
| 2 | 设置数据库 |
| 3 | 创建Payment和Claim Item支架 |
| 4 | 修改Payment并创建其子类 |
| 5 | 创建Google API访问库 |
| 6 | 创建Manager类及其控制器和视图 |
| 7 | 创建费用报销解析rake脚本 |
| 8 | 创建批量支付rake脚本 |
| 9 | 修改Payment和Claim Item控制器 |
希望本文能为你开发类似的应用提供有价值的参考,让你顺利完成费用报销混搭应用的开发。
费用报销混搭插件开发
超级会员免费看
2935

被折叠的 条评论
为什么被折叠?



