前段时间在导出商家结余表时,发现有些大商家数据比较多,而一直使用的PHP导数脚本是同步脚本,未改为多进程导数,导出效果特别差强人意,有一些甚至等待一天才勉强收到邮件通知。刚好这段时间公司项目上没有什么新需求要做,项目处于维护期和日常工作期,鉴于Go语言的异步优势,就用Go重写了该脚本。一来将Go的学习用于实际场景编写,巩固和运用;二来也比较一下动态语言和静态语言的思维方式和执行效率。
开始编写时,确实遇到很多难处。一开始想到的,是如何做数据库连接,幸好Go对这些常见的场景都有现成的库可使用,稍微在GitHub上搜索就能有好的选择。然后是对数据类型的描述上,确实跟动态语言有明显的不同,但是无法否认,静态语言在编写和编译期间就能减少很多低级的Bug。
也发现了一些公司的服务问题,比如邮件服务器总是x.509证书问题而无法使用,网易163也存在这样的问题,但是QQ邮箱可以正常使用。官方库中已经提供了CSV格式的支持,使用起来相当的方便,就不提供excel方式了。
下面简单梳理一下编写该脚本的过程和需要注意的地方。
1. 我们最终需要导出一个这样的excel表格给其他业务部门查看
表格中省略了部分内容,只是为了呈现最终的样式,同时为了方便查看,依照顺序显示,后续操作需要兼顾该顺序。当然因为csv格式与Excel的关系,直接用Excel打开会出现乱码,如果必须要使用Excel查看,需要将csv用文本文件打开,拷贝到Excel中,使用数据->分隔的方式重新显示。如操作频繁,可使用Go的Excel库来直接写入到Excel文件,略去转化过程。比如之前在公众号看到的这篇Golang 读写 Excel,可以参考其思路。
2. 表格标题项的处理
// 结算相关标题
var settleColumns = []string{
"供应商",
"结算单Id",
"有效付款订单金额",
"红包抵扣金额合计",
"手机端满减返款项的金额合计",
"钻石会员包邮运费返款",
"已确认退货金额",
"退货快递运费",
"违约订单金额",
"罚款金额",
"暂不结款订单金额",
"返点订单总金额",
"返点金额",
"费用调整",
"红包总金额",
"退款金额扣点",
"现金券使用的金额",
"虚假发货罚款金额",
"应付金额",
"结算时间",
"结算状态",
}
比较简单,直接定义为一个变量即可,也可以直接在写csv文件时在定义。
3. 需要导出的11种数据类型
一开始联想到PHP的变量函数,但是静态语言不支持该种思路,必须在编译阶段确定其类型,不能直接调用一个字符串,更无法操作一个字符串会像动态语言一样,解释器将其解释为一个函数,因此要按函数类型来定义,比如我的处理方式是:
// 使用计数器记录启动的goroutine个数
var count sync.WaitGroup
// 声明函数类型字典
names := map[string]func(group *sync.WaitGroup, string string){
"1、partner系统已结算单": partnerList,
"2、Partner未结算数据": partnerNoList,
"3、FSP已结算数据": fspList,
"4、FSP未结算数据": fspNoList,
"5、Finance已结算数据": financeList,
"6、未发货并且无退货的订单": noDeliveryNoRmaList,
"7、未结算订单": noSettleList,
"8、结算过暂不结算": onHoldList,
"9、结算过退货": settleRmaList,
"10、在结算单创建审核中产生退货": settlingRmaList,
}
for dist, name := range names {
count.Add(1)
go name(&count, dist)
}
// 等待所有的goroutine执行完毕
count.Wait()
之所以要用Go来写,就是因为该处的异步调用优势。每一个字符串描述的后面,都是一个个的真实函数。第11种比较特殊,需要依赖前面的几种返回值,因此无法将其加入到异步执行中,只能同步阻塞运行。
// 11、没有符合任何条件的订单
// 该数据依赖前面数据,不支持并发操作,该方法最后执行
noList()
PHP按照我们已有的方式,只能借助Pcntl多进程模型来完成,但是也需要将执行结果写入到一个文本文件中,同样要记录字典的键,然后等待所有进程都执行完毕后,在将该文本文件按展示顺序排列后在重新写入到Excel文件中,一样可以实现。但是考虑到多进程控制问题,如果不需要返回值,确实比较方便。当然也可以临时建一张表,没跑完一个进程就将数据按格式写入库中,待执行完毕后,在查询数据库得到结果,效果也不会差。
// PHP多进程执行方式
foreach ($dateSpanList as $dateList) {
$pid = pcntl_fork();
if ($pid == -1) {
error_log('Could not launch new job, exiting');
exit(3);
} else if ($pid) {
++$processCnt;
} else {
// 每一个独立的业务都在该进程中执行,不需要返回值,执行完毕后直接退出
exit(0);
}
}
for ($i = 0; $i < $processCnt; ++$i) {
printf("\t\t*****************return %d\n", pcntl_wait($status));
}
// 此时所有进程均已执行完毕,就可以得到所有的结果
但是进程的资源要昂贵得多,而Go的go关键字启动的消耗说特别的小,系统里完全可以有成千上万个协程在运行。Pcntl如果要接收子进程的结束信号,就要用到pcntl_signal()信号注册函数,而且编写和管理起来就要复杂的多。
4. 每一个方法的编写,其中以调试最多的方法结算过暂不结算为例
var soIdList settledSoIds
// 8、结算过暂不结算
func onHoldList(n *sync.WaitGroup, dist string) {
defer n.Done()
// 对字典进行锁
soIdList.lock.Lock()
defer soIdList.lock.Unlock()
// 是否已经获取过已结算的so_id集合
if len(soIdList.soIds) <= 0 {
var soId int32
var settleCnt int
rows, err := common.GetPopDb().Query(`
SELECT
so_id,
MAX(settlement_cnt) settlement_cnt
FROM t_pop_trans_list FORCE INDEX(idx_pid_oid)
WHERE partner_id = ?
AND settlement_note_id > 0
AND order_id > 1
AND type <> 'pre_settle'
GROUP BY so_id
`, partnerId)
common.Error(err)
defer rows.Close()
for rows.Next() {
if err := rows.Scan(&soId, &settleCnt); err != nil {
if _, file, line, ok := runtime.Caller(0); ok {
common.Info(err, file, line)
}
}
// 保存已结算的so_id到全局字典中
soIdList.soIds[settleCnt] = soId
}
common.RowsError(rows)
}
var p order
var records [][]string
records = append(records, []string{dist})
for cnt, id := range soIdList.soIds {
err := common.GetPopDb().QueryRow(`
SELECT
order_id,
sku_no,
valid_order_total_price,
red_envelope_user_delivery_fee,
mobile_user_delivery_fee,
diamond_user_delivery_fee,
rma_order_total_price,
rma_delivery_fee,
delay_order_total_price,
fine_fee,
onhold_order_total_price,
media_rebate_order_total_price,
media_rebate_fee,
adjustment_fee,
red_envelope_total_price,
rma_rebate_fee,
promo_card_used_order_total_price,
0 fake_shipping_fee,
need_paid_total,
UNIX_TIMESTAMP(date_line) check_time,
'' status
FROM t_pop_trans_list FORCE INDEX(idx_soid_cnt)
WHERE so_id = ?
AND settlement_cnt = ?
AND settlement_note_id = 0
AND re_type <> 'so'
AND type <> 'pre_settle'
`, id, cnt).Scan(
&p.orderId, &p.skuNo,
&p.common.validOrderTotalPrice, &p.common.redEnvelopeUserDeliveryFee, &p.common.mobileUserDeliveryFee,
&p.common.diamondUserDeliveryFee, &p.common.rmaOrderTotalPrice, &p.common.rmaDeliveryFee,
&p.common.delayOrderTotalPrice, &p.common.fineFee, &p.common.onHoldOrderTotalPrice,
&p.common.mediaRebateOrderTotalPrice, &p.common.mediaRebateFee, &p.common.adjustmentFee,
&p.common.redEnvelopeTotalPrice, &p.common.rmaRebateFee, &p.common.promoCardUsedOrderTotalPrice,
&p.common.fakeShippingFee, &p.common.needPaidTotal, &p.common.checkTime, &p.common.status,
)
switch {
case err == sql.ErrNoRows:
continue
case err != nil:
if _, file, line, ok := runtime.Caller(0); ok {
common.Info(err, file, line)
}
}
specSoIdList.lock.Lock()
specSoIdList.soIds[id] = id
specSoIdList.lock.Unlock()
// FSP新退保业务,记录日志
sur := surrender{
checkLogId: checkLogId,
partnerId: partnerId,
name: partnerName,
classify: 6,
orderId: p.orderId,
skuNo: p.skuNo,
needPaidTotal: p.common.needPaidTotal,
reason: fmt.Sprintf("结算过暂不结算的订单orderId: %d, skuNo: %d", p.orderId, p.skuNo),
}
surrenderLod(sur)
records = addOrderRecord(records, p)
}
target.lock.Lock()
target.goal[8] = records
target.lock.Unlock()
fmt.Println(dist, time.Now().Format("2006-01-02 15:04:05"))
}
其中包含了基本常用的错误处理方式,对于Go来说,查询结果集为空的错误需要特殊处理,
switch {
case err == sql.ErrNoRows:
continue
case err != nil:
if _, file, line, ok := runtime.Caller(0); ok {
common.Info(err, file, line)
}
}
sql.ErrNoRows的值是
// ErrNoRows is returned by Scan when QueryRow doesn't return a
// row. In such a case, QueryRow returns a placeholder *Row value that
// defers this error until a Scan.
var ErrNoRows = errors.New("sql: no rows in result set")
是一种特殊的情形,需要单独判断,其实是一种正常的返回,但是在Go中被定义为一种错误消息。
遍历也需要检查是否有错误产生。
// 结果集错误,该错误可能由db.query()遍历产生
func RowsError(rows *sql.Rows) {
if err := rows.Err(); err != nil {
log.Fatalln(err)
}
}
看到一些评论说,Go写什么都好,就是真正管用的代码没几行,但是跟错误相关的代码却是有用代码的好几倍,而且错误几乎都无法避开。
在这里遇到最大的一个问题还是,因为Go的数据库包 database/sql 的原因,如果返回的数据较多,会产生MySQL数据库太多连接的错误,这个没什么好办法,只能将sql语句调节到最优,每次处理完数据就直接关闭连接,而不需要等待到defer来延迟关闭结果集。
目前使用go1.8.3版本,并发写字典是需要加锁的,但是据说go1.9之后的新版本,内部已经加锁处理,可以直接调用了。
// 定义一个字典保存所有结算相关信息,字典的键为每一项结算信息的标识,即main中11种内容类型,字典的值为一个二维字符串数组
type desc struct {
goal map[int][][]string
lock sync.Mutex
}
该脚本使用的目标数据结构,其中就专门定义了锁机制。
Go查询的记录,即结果集,都需要有明确的变量去绑定接收,这是动态语言天然的优势,但静态语言无法做到。还有就是需要注意,数据库查询返回值中一定不能有NULL,这在Go中是不可接受的。如果无法改变,需要借助NULL类型来处理。但我觉得任何一个合格的DBA或者PHP开发者,只要不是第一天学习PHP,都应该知道数据库对NULL的控制,这种东西应该从建表的第一行SQL开始,就被扼杀在摇篮之中。如果不行你使用的旧的数据库中有这种类型存在,那么狠遗憾,你只能在SQL语句中大量使用 IS NULL
IF (valid_order_total_price IS NULL, 0, valid_order_total_price) valid_order_total_price,
这样的判断,你也看到了,我就是这个不幸的人。还有更多奇怪的问题,只能遇到了再挨个解决。以前一个小伙伴对我说,她们老大给她一个同事说,做编程是一门有意思的工作,你每天都是不停的发现问题,遇见新问题,然后解决问题,每一天都是新的。一定程度上的确如此,但要是遇见磕破脑袋都解决不了的问题,谁都会有发狂的时候,遇见了就忍忍吧,歇一会就想到解决办法了。
5. 数据处理完后,其它类型转为字符串类型
因为使用csv来读写数据,csv只接收字符串类型,故需要将其它非字符串类型转化为字符串类型。
// 对结算值进行绑定
func addSettleRecord(records [][]string, p settle) [][]string {
return append(records, []string{
partnerName,
fmt.Sprintf("%d", p.settlementNoteId),
fmt.Sprintf("%f", p.common.validOrderTotalPrice),
fmt.Sprintf("%f", p.common.redEnvelopeUserDeliveryFee),
fmt.Sprintf("%f", p.common.mobileUserDeliveryFee),
fmt.Sprintf("%f", p.common.diamondUserDeliveryFee),
fmt.Sprintf("%f", p.common.rmaOrderTotalPrice),
fmt.Sprintf("%f", p.common.rmaDeliveryFee),
fmt.Sprintf("%f", p.common.delayOrderTotalPrice),
fmt.Sprintf("%f", p.common.fineFee),
fmt.Sprintf("%f", p.common.onHoldOrderTotalPrice),
fmt.Sprintf("%f", p.common.mediaRebateOrderTotalPrice),
fmt.Sprintf("%f", p.common.mediaRebateFee),
fmt.Sprintf("%f", p.common.adjustmentFee),
fmt.Sprintf("%f", p.common.redEnvelopeTotalPrice),
fmt.Sprintf("%f", p.common.rmaRebateFee),
fmt.Sprintf("%f", p.common.promoCardUsedOrderTotalPrice),
fmt.Sprintf("%f", p.common.fakeShippingFee),
fmt.Sprintf("%f", p.common.needPaidTotal),
time.Unix(p.common.checkTime, 0).Format("2006-01-02 15:04:05"),
statusToDes(p.common.status),
})
}
这也是和动态脚本语言差异很大的地方,数据类型很多场景下是不能自动传化为所需要的格式的,必须明确将其转化为目标类型。也没有三元运算符,只能用类似字典断言的方式来判断具体的值。
// 结算单状态描述
func statusToDes(status string) string {
var statusList = map[string]string{
"confirmed": "初审通过",
"confirmNote": "待确认",
"new": "新建",
"invalid": "不可结算",
"merged": "合并",
"paid": "结款完成",
"rejected": "驳回",
"to_approve": "待审批",
"verified": "待结款",
"submitted": "待审核",
"approved": "审核通过",
"finished": "完成",
"supplier_approved": "商家审核通过",
"supplier_rejected": "商家驳回",
"wait_supplier_confirm": "待商家确认",
"received": "已收单",
"wait_finance_check": "等待finance检查",
"finance_check_ok": "finance检查ok",
}
if desc, ok := statusList[status]; ok {
return desc
}
return status
}
这要是在PHP中,下列方式
return isset($list[$skuCategory]) ? $list[$skuCategory] : '未知';
很简单的一句话就能做到,高版本的PHP,甚至还能用合并运算符,更精简
return $list[$skuCategory] ?? '未知';
直接省去了isset()的判断就能得到要表达的结果。Go没有这么多的语法糖,这根它的哲学有关,保持语言本身的简单。
6. 写csv文件
// 生成csv格式的附件
func attachment(name string) {
f, err := os.OpenFile(name, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0777)
defer f.Close()
common.Error(err)
var records [][]string
records = append(records, settleColumns)
for i := 1; i <= 11; i++ {
// 当属于第6个时,需要标识为订单相关项
if i == 6 {
records = append(records, orderColumns)
}
for _, v := range target.goal[i] {
records = append(records, v)
}
}
err = csv.NewWriter(f).WriteAll(records)
common.Error(err)
}
写csv文件非常简单,直接保存即可,默认均为utf-8编码,写完后用LibreOffice这些软件打开是没问题的,但是Windows系统就不是很方便了,需要做一些额外的工作。跨平台就是这样,只能迁就它。谁让他们家的办公软件这么流行和好用。苹果的Numbers打开也没问题,反正你知道就行,就是微软的Excel打开是一堆乱码,即便是PHP,也要主动对中文进行转码,将utf-8转化为gbk,你生气也没用,事就是要这样才能成的
return mb_convert_encoding($str, 'gbk', 'utf-8');
对,就是要这样处理之后,使用Windows的同学才能正常查看,但是这样处理后至少我的Mac是没法查看的,这没关系,他们能看就行,咱们自己都可以忍。
7. 发送邮件
// 发送附件
func email(name string, emails string) {
// smtp.qq.com,使用SSL,端口号465或587
// 公司邮件本次测试,使用mail.xxx.com:25可以发送邮件,但是正式线无法发送邮件
// x509: cannot validate certificate for 10.2.12.9 because it doesn't contain any IP SANs
// x509: certificate is valid for Barracuda/emailAddress=sales@barracuda.com, not mail.jumei.com
// 似乎是证书的问题
// 我的网易邮件zhgxun1989@163.com也存在一样的问题,但是腾讯QQ邮箱服务却可以正常使用
// 定位问题为公司服务器原因,不处理,属于个性问题
config := `{"username":"978771018@qq.com","password":"xxxxxx","host":"smtp.qq.com","port":587}`
mail := common.NewEMail(config)
mail.To = strings.Split(emails, ",")
mail.From = "978771018@qq.com"
mail.Subject = partnerName + " 商家结余附件导出"
//mail.Text = "Text Body is, of course, supported!"
// 尝试读取错误日志中的文件内容
f, err := os.Open(logFile)
common.Error(err)
defer f.Close()
con, err := ioutil.ReadAll(f)
common.Error(err)
mail.HTML = "<h3 style='color:red'>商家重复结算或结算后退货日志: </h3>" +
string(con) +
"<h3 style='color:blue'>以下为商家结余附件: </h3>"
mail.AttachFile(name)
err = mail.Send()
common.Error(err)
}
正如注释中所说,x509问题属于服务问题,就不处理。该邮件发送使用了Beego框架的邮件发送封装包。错误日志部分是另一个Go脚本,这里就不描述了,都差不多。代码只是工具,重要的是要跟业务结合起来,才有用处,才能转化为生产力。
8. 整个入口文件
// 商家结余表导出
func Export(pId int, fix int, emails string, cId int, n *sync.WaitGroup) {
defer n.Done()
partnerId = pId
checkLogId = cId
logFile = fmt.Sprintf("/home/www/finance/apps/finance/protected/runtime/fix_rma_after_settle_%d", partnerId)
if len(settleColumns)-len(orderColumns) != 0 {
log.Fatal("导出csv文件标题项目数不对应")
}
// 商家名称
partnerName = getPartnerName()
// 结算后退货或重复结算处理
if fix > 0 {
Fix()
}
// 时间格式化必须是该日期2006-01-02 15:04:05
fmt.Println("统计开始", time.Now().Format("2006-01-02 15:04:05"))
// 初始化字典
target.goal = make(map[int][][]string)
specSoIdList.soIds = make(map[int32]int32)
soIdList.soIds = make(map[int]int32)
// 使用计数器记录启动的goroutine个数
var count sync.WaitGroup
// 声明函数类型字典
names := map[string]func(group *sync.WaitGroup, string string){
"1、partner系统已结算单": partnerList,
"2、Partner未结算数据": partnerNoList,
"3、FSP已结算数据": fspList,
"4、FSP未结算数据": fspNoList,
"5、Finance已结算数据": financeList,
"6、未发货并且无退货的订单": noDeliveryNoRmaList,
"7、未结算订单": noSettleList,
"8、结算过暂不结算": onHoldList,
"9、结算过退货": settleRmaList,
"10、在结算单创建审核中产生退货": settlingRmaList,
}
for dist, name := range names {
count.Add(1)
go name(&count, dist)
}
// 等待所有的goroutine执行完毕
count.Wait()
// 11、没有符合任何条件的订单
// 该数据依赖前面数据,不支持并发操作,该方法最后执行
noList()
// 保存为csv格式的附件
name := fmt.Sprintf("/home/www/flogs/POP_PartnerId_%d.csv", partnerId)
attachment(name)
// 发送邮件
email(name, emails)
fmt.Println("统计结束", time.Now().Format("2006-01-02 15:04:05"))
}
使用Go确实比PHP写起来要严格一些,但是对数据库操作没有PHP那么方便和自由。PHP WEB框架目前非常的成熟,数据库等操作都是开箱即用,手到擒来,唯一的就是因为语言的关系,异步编程不方便,需要借助扩展来实现。
偶尔还看到说Go的时间格式化非常鸡肋,其实这东西,熟能生巧,就是一个字符串而已,没有必要在这些东西上浪费时间,这完全没有必要去争论。我也看到网上确实有很多类似的库,看来大家都还是有目共睹,像PHP一样处理时间最简单。不过我也没有使用,觉得这些多写几个字符影响不了工作效率,等到真的需要改善时,再来处理吧。
我喜欢用Go还有一个更大的好处是,Go本身的SDK就是用Go来写的,每个源代码都能看到,每个Go文件都有对应的测试和样例文件,很多功能直接将样例文件拷贝过来,稍加研究和理解就可以写出符合自己业务需要的代码,比如csv包:
func ExampleWriter_WriteAll() {
records := [][]string{
{"first_name", "last_name", "username"},
{"Rob", "Pike", "rob"},
{"Ken", "Thompson", "ken"},
{"Robert", "Griesemer", "gri"},
}
w := csv.NewWriter(os.Stdout)
w.WriteAll(records) // calls Flush internally
if err := w.Error(); err != nil {
log.Fatalln("error writing csv:", err)
}
// Output:
// first_name,last_name,username
// Rob,Pike,rob
// Ken,Thompson,ken
// Robert,Griesemer,gri
}
官方源码库中文件example_test.go就给出了这个例子,非常直观,容易,而PHP就没有这么便利,首先PHP源代码是C代码,至少我看不懂C代码,只能看手册。但是Go不一样,本身编写的就是Go代码,Go的源代码本身也是Go代码,我觉得这是这门语言非常让人喜欢的一个原因。只要学有余力,完全可以去读Go源代码,做更多的事情。
9. 编译代码
sudo CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go install finance/main
我是在自己的Mac上编译成CentOS系统上的可执行文件,编译完成后直接拷贝到服务器上,通过PHP代码调用运行。还有一些说法是Go编译后的二进制文件有点大,确实我这个小脚本编译后有5M多,但是好像基本维持在这个容量。
10. PHP如何使用
/**
* go脚本导数
* @param $r
* @param $partnerIds
* @param int $fix
* @param string $emails
* @param int $checkLogId
*/
public function actionReturnExt($r, $partnerIds, $fix = 1, $emails = 'guangxunz@jumei.com', $checkLogId = 0)
{
$path = __DIR__ . "/main {$r} {$partnerIds} {$fix} {$emails} {$checkLogId}";
exec($path, $output);
print_r($output);
}
PHP中使用只能按照命令执行方式来使用,exec(),system(),shell_exec()等函数都可以实现。该处因为我简单封装了路由和传参方式,所以参数传递有自己的规则,入口文件变得就相对简单一些:
// 编译Linux系统amd64下可执行文件
// sudo CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go install finance/main
package main
import (
"os"
_ "github.com/go-sql-driver/mysql"
"finance"
)
func main() {
finance.Handle(os.Args[1:])
}
这就是整个go脚本的入口文件,直接接收参数即可执行,使用频率不高,需要时直接调用,故不需要守护进程。路由解析就很简单,其实就是用一种传参方式,让代码知道何时,何处调用正确的函数而已。
11. 执行效果
66329 : ./protected/yiic2014 Balance ReturnExt --r=settlementExport --partnerIds=3244
Array
(
[0] => 统计开始 2017-08-16 11:40:33
[1] => 2、Partner未结算数据 2017-08-16 11:40:33
[2] => 10、在结算单创建审核中产生退货 2017-08-16 11:40:33
[3] => 6、未发货并且无退货的订单 2017-08-16 11:40:33
[4] => 1、partner系统已结算单 2017-08-16 11:40:33
[5] => 7、未结算订单 2017-08-16 11:40:33
[6] => 9、结算过退货 2017-08-16 11:40:33
[7] => 8、结算过暂不结算 2017-08-16 11:40:33
[8] => 4、FSP未结算数据 2017-08-16 11:40:33
[9] => 5、Finance已结算数据 2017-08-16 11:40:33
[10] => 3、FSP已结算数据 2017-08-16 11:40:33
[11] => 11、没有符合任何条件的订单 2017-08-16 11:40:33
[12] => 统计结束 2017-08-16 11:40:35
)
exitStatus:0
66329 : 结束 "./protected/yiic2014 Balance ReturnExt --r=settlementExport --partnerIds=3244"
[begin:2017-08-16 11:40:33 end:2017-08-16 11:40:35] 历时: 02秒
这是使用我们自己的跑数平台执行后的输出结果,而且以为异步的关系,能正确执行的条件下,肯定会比同步的PHP脚本快得多。而我们原来的PHP脚本代码是这样一行一行阻塞下去的
/**
* 导出商家结余信息
*
* @param $partnerId
* @param int $other
* @param int $fix 是否需要检查结算后退货,重复结算等情况
* @param string $email 邮箱账号 多个邮件之间用逗号分隔
* @param int $checkLogId fsp系统新退保业务,可能会有该任务主键ID,来源于脚本 Balance->clean
*/
public function actionExport($partnerId, $other = 1, $fix = 1, $email = '', $checkLogId = 0)
{
$this->partnerId = $partnerId;
$length = count($this->showSettleColumns);
if ($length != count($this->showOrderColumns)) {
exit("导出excel文件头项目数不对应\n");
}
// 启动结算后退货检测
if ($fix) {
$runner = FinanceConsoleCommandRunner::createCommandRunner($this);
$runner->addRun('TPopTransListFixData', 'FixRmaAfterSettle', ['partnerId' => $partnerId, 'checkLogId' => $checkLogId]);
// 因为要邮件周知结算后退货等日志记录,故只能同步等待
$runner->wait();
unset($runner);
}
$logFile = $this->getLogFile();
echo sprintf("统计开始%s\n", date('Y-m-d H:i:s'));
$partnerName = Common::getPartnerName($partnerId, 'PARTNER');
$list[] = $this->showSettleColumns;
// 1、partner系统已结算单
$list[] = array_pad(['Partner已结算数据'], $length, '');
$partnerList = CommonPopDb::getPopDb()->createCommand($this->getPartnerSettleSql())->queryAll();
foreach ($partnerList as $row) {
$row['partner_id'] = $partnerName;
$row['status'] = self::statusToDes($row['status']);
$list[] = array_values($row);
}
unset($partnerList);
echo sprintf("Partner已结算数据%s\n", date('Y-m-d H:i:s'));
// 2、Partner未结算数据
$list[] = array_pad(['Partner未结算数据'], $length, '');
$partnerUnList = CommonPopDb::getPartnerDb()->createCommand($this->getUnPartnerSettleSql())->queryAll();
foreach ($partnerUnList as $row) {
$row['partner_id'] = $partnerName;
$row['status'] = self::statusToDes($row['status']);
$list[] = array_values($row);
}
unset($partnerUnList);
echo sprintf("Partner未结算数据%s\n", date('Y-m-d H:i:s'));
// 3、FSP已结算数据
$list[] = array_pad(['FSP已结算数据'], $length, '');
$fspList = CommonPopDb::getPopDb()->createCommand($this->getFspSettleSql())->queryAll();
// fsp改结算项为4项,需要重新计算部分结算数值
$fspList = $this->handleFsp($fspList);
foreach ($fspList as $row) {
$row['partner_id'] = $partnerName;
$row['status'] = self::statusToDes($row['status']);
$list[] = array_values($row);
}
unset($fspList);
echo sprintf("FSP已结算数据%s\n", date('Y-m-d H:i:s'));
// 4、FSP未结算数据
$list[] = array_pad(['FSP未结算数据'], $length, '');
$fspUnList = CommonPopDb::getFspDb()->createCommand($this->getUnFspSettleSql($this->partnerId))->queryAll();
foreach ($fspUnList as $row) {
$row['partner_id'] = $partnerName;
$row['status'] = self::statusToDes($row['status']);
$list[] = array_values($row);
}
unset($fspUnList);
echo sprintf("FSP未结算数据%s\n", date('Y-m-d H:i:s'));
// 5、Finance已结算数据
$list[] = array_pad(['Finance已结算数据'], $length, '');
$financeList = CommonPopDb::getPopDb()->createCommand($this->getFinanceSettleSql($this->partnerId))->queryAll();
$this->handleFinance($financeList);
foreach ($financeList as $row) {
$row['partner_id'] = $partnerName;
$list[] = array_values($row);
}
unset($financeList);
echo sprintf("Finance已结算数据%s\n", date('Y-m-d H:i:s'));
if ($other == 1) {
// 6、 未发货并且无退货的订单
$list[] = $this->showOrderColumns;
$list[] = array_pad(['未发货并且无退货的订单'], $length, '');
$noDeliveryNoRma = $this->noDeliveryNoRma();
foreach ($noDeliveryNoRma as $row) {
$this->soIdList[] = $row['id'];
unset($row['id']);
$list[] = array_values($row);
}
unset($noDeliveryNoRma);
echo sprintf("未发货并且无退货的订单%s\n", date('Y-m-d H:i:s'));
// 7、未结算订单
$list[] = array_pad(['未结算订单'], $length, '');
$unSettleList = $this->unsettle();
foreach ($unSettleList as $row) {
// 部分预售订单不结算
$isSale = false;
if ($this->isPreSale($row['order_id'])) {
$isSale = true;
$reason = "预售订单[{$row['order_id']}] sku_no[{$row['sku_no']}]不需结算";
} else {
$reason = "订单[{$row['order_id']}] sku_no[{$row['sku_no']}]未结算";
}
$this->surrenderLod([
'check_log_id' => $checkLogId,
'partner_id' => $partnerId,
'name' => $partnerName,
'type' => 5,
'order_id' => $row['order_id'],
'sku_no' => $row['sku_no'],
'need_paid_total' => $row['need_paid_total'],
'reason' => $reason,
'ctime' => date('Y-m-d H:i:s')
]);
if ($isSale) {
continue;
}
$list[] = array_values($row);
}
unset($unSettleList);
echo sprintf("未结算订单%s\n", date('Y-m-d H:i:s'));
// 8、结算过暂不结算
$list[] = array_pad(['结算过暂不结算'], $length, '');
$onHold = $this->onHold();
foreach ($onHold as $row) {
$list[] = array_values($row);
$this->surrenderLod([
'check_log_id' => $checkLogId,
'partner_id' => $partnerId,
'name' => $partnerName,
'type' => 6,
'order_id' => $row['order_id'],
'sku_no' => $row['sku_no'],
'need_paid_total' => $row['need_paid_total'],
'reason' => "订单[{$row['order_id']}] sku_no[{$row['sku_no']}]结算过暂不结算",
'ctime' => date('Y-m-d H:i:s')
]);
}
unset($onHold);
echo sprintf("结算过暂不结算%s\n", date('Y-m-d H:i:s'));
// 9、结算过退货
$list[] = array_pad(['结算过退货'], $length, '');
$afterSettle = $this->afterSettle();
foreach ($afterSettle as $row) {
$list[] = array_values($row);
}
unset($afterSettle);
echo sprintf("结算过退货%s\n", date('Y-m-d H:i:s'));
// 10、在结算单创建审核中产生退货
$list[] = array_pad(['在结算单创建审核中产生退货'], $length, '');
$whilesSettle = $this->whilesSettle();
foreach ($whilesSettle as $row) {
$list[] = array_values($row);
}
unset($whilesSettle);
echo sprintf("在结算单创建审核中产生退货%s\n", date('Y-m-d H:i:s'));
// 11、没有符合任何条件的订单
$list[] = array_pad(['没有符合任何条件的订单'], $length, '');
$no = $this->CheckDiff();
foreach ($no as $row) {
$list[] = array_values($row);
}
unset($no);
echo sprintf("没有符合任何条件的订单%s\n", date('Y-m-d H:i:s'));
}
$partnerName = str_replace(array(' ', '', '\t'), '_', trim($partnerName));
$content = "<p style='color: red;'><b>$partnerName"."({$partnerId})</b>结算后退货、重复结算或其他差异记录日志:</p>\n\n";
$title = "POP_PartnerId_{$partnerId}";
$mailTo = $this->getMailTo();
if ($email) {
$emails = explode(',', $email);
$mailTo = array_merge($mailTo, $emails);
}
// 查看该商家是否有结算后退货的信息
if (file_exists($logFile)) {
$content .= file_get_contents($logFile) . "\n";
unlink($logFile);
}
$content .= "\n<p style='color: blue; '>以下为商家结余附件:</p>";
FMailer::sendPOPExport(
$content,
[Common::exportExcel($list, ['title' => $title, 'biz_item' => $title])],
$mailTo
);
echo sprintf("统计结束%s\n", date('Y-m-d H:i:s'));
}
哈哈,这还是我后来优化过后的代码,之前的代码好几个SQL效率都很慢,当然第一个编写这个脚本的人已经离职不再公司了,那时候还没有这么多的业务逻辑,随便写的脚本都能够很开心的跑出让大家满意的结果来。有时候在公司的时间也很重要,像我就写新功能的时候很少,但是维护现有功能的多。很多东西,如果咱们自己不想改,装作不知道,完全可以一句话把责任抛给别人,网络慢啊,服务器有点卡啊各种理由似乎都很对,毕竟优化是件让人头疼的事情,而且还不一定有结果。做好了,对自己未必有好处,但对公司,对后来的团队都有好处;做不好,只能是劳民伤财,自作自受,所以,优化其实不是真的必须面对时,一般很少有人会轻易去碰它。