完成了基本调整调整
This commit is contained in:
@@ -6,7 +6,12 @@ const api = {
|
||||
share: '/partners.order/share',
|
||||
qrCode: '/partners.order/qrCode',
|
||||
oilCallback: '/partners.order/oilCallback',
|
||||
useCallback: '/partners.order/useCallback'
|
||||
useCallback: '/partners.order/useCallback',
|
||||
refund: '/partners.order/refund',
|
||||
refundConfirm: '/partners.order/refundConfirm',
|
||||
refundResolve: '/partners.order/refundResolve',
|
||||
refundLog: '/partners.order/refundLog',
|
||||
invoice: '/partners.order/invoice'
|
||||
}
|
||||
|
||||
// 列表记录
|
||||
@@ -65,3 +70,63 @@ export function useCallback (data) {
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 退款(整单全额:企油通 7.17 查凭证 + 7.16 调低额度)
|
||||
* @param {*} data { orderId, reason }
|
||||
*/
|
||||
export function refund (data) {
|
||||
return axios({
|
||||
url: api.refund,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 待人工确认的退款:向企油通取证确认
|
||||
* @param {*} data { orderId }
|
||||
*/
|
||||
export function refundConfirm (data) {
|
||||
return axios({
|
||||
url: api.refundConfirm,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 待人工确认的退款:人工判定
|
||||
* @param {*} data { orderId, result: success|failed, remark }
|
||||
*/
|
||||
export function refundResolve (data) {
|
||||
return axios({
|
||||
url: api.refundResolve,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单退款流水
|
||||
* @param {*} params { orderId }
|
||||
*/
|
||||
export function refundLog (params) {
|
||||
return axios({
|
||||
url: api.refundLog,
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单关联发票(企油通 7.14,按企业+消费月份+司机)
|
||||
* @param {*} params { orderId, refresh }
|
||||
*/
|
||||
export function invoice (params) {
|
||||
return axios({
|
||||
url: api.invoice,
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { axios } from '@/utils/request'
|
||||
|
||||
// api接口列表
|
||||
const api = {
|
||||
list: '/qyt.driver/list',
|
||||
detail: '/qyt.driver/detail',
|
||||
upstream: '/qyt.driver/upstream'
|
||||
}
|
||||
|
||||
// 司机列表
|
||||
export function list (params) {
|
||||
return axios({ url: api.list, method: 'get', params })
|
||||
}
|
||||
|
||||
// 司机详情
|
||||
export function detail (params) {
|
||||
return axios({ url: api.detail, method: 'get', params })
|
||||
}
|
||||
|
||||
// 实时查企油通:type = balance(7.12) / wallet(7.13) / voucher(7.17)
|
||||
export function upstream (params) {
|
||||
return axios({ url: api.upstream, method: 'get', params })
|
||||
}
|
||||
@@ -11,7 +11,8 @@ const api = {
|
||||
setDefault: '/qyt.enterprise/setDefault',
|
||||
apply: '/qyt.enterprise/apply',
|
||||
syncAccount: '/qyt.enterprise/syncAccount',
|
||||
syncList: '/qyt.enterprise/syncList'
|
||||
syncList: '/qyt.enterprise/syncList',
|
||||
balance: '/qyt.enterprise/balance'
|
||||
}
|
||||
|
||||
// 企业列表
|
||||
@@ -63,3 +64,8 @@ export function syncAccount (data) {
|
||||
export function syncList (data) {
|
||||
return axios({ url: api.syncList, method: 'post', data })
|
||||
}
|
||||
|
||||
// 7.11 企业账户余额(仅平安银行通道)
|
||||
export function balance (params) {
|
||||
return axios({ url: api.balance, method: 'get', params })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { axios } from '@/utils/request'
|
||||
|
||||
// api接口列表
|
||||
const api = {
|
||||
list: '/qyt.invoice/list',
|
||||
sync: '/qyt.invoice/sync'
|
||||
}
|
||||
|
||||
// 发票记录
|
||||
export function list (params) {
|
||||
return axios({ url: api.list, method: 'get', params })
|
||||
}
|
||||
|
||||
// 同步发票(企油通 7.14):enterpriseId 不传则批量同步所有启用企业当月全量
|
||||
export function sync (data) {
|
||||
return axios({ url: api.sync, method: 'post', data })
|
||||
}
|
||||
@@ -55,9 +55,11 @@ export default {
|
||||
'appKey', 'callback', 'create_time'
|
||||
])
|
||||
partnersFormData.callback = partnersFormData.callback.toString()
|
||||
console.log(partnersFormData)
|
||||
// 企油通企业:0 表示未绑定(走默认企业),下拉框要给 undefined 才会显示占位提示
|
||||
const enterpriseId = Number(partnersInfo.qyt_enterprise_id || 0)
|
||||
return {
|
||||
...partnersFormData
|
||||
...partnersFormData,
|
||||
qyt_enterprise_id: enterpriseId > 0 ? enterpriseId : undefined
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -570,6 +570,16 @@ export const asyncRouterMap = [
|
||||
component: () => import(/* webpackChunkName: "goods" */ '@/views/partners/order/Index'),
|
||||
meta: { title: '订单列表', keepAlive: false, permission: ['/partners/order/index'] }
|
||||
},
|
||||
{
|
||||
path: '/qyt/driver/index',
|
||||
component: () => import(/* webpackChunkName: "goods" */ '@/views/qyt/driver/Index'),
|
||||
meta: { title: '司机管理', keepAlive: false, permission: ['/qyt/driver/index'] }
|
||||
},
|
||||
{
|
||||
path: '/qyt/invoice/index',
|
||||
component: () => import(/* webpackChunkName: "goods" */ '@/views/qyt/invoice/Index'),
|
||||
meta: { title: '发票管理', keepAlive: false, permission: ['/qyt/invoice/index'] }
|
||||
},
|
||||
{
|
||||
path: '/partners/balance',
|
||||
component: RouteView,
|
||||
|
||||
@@ -152,7 +152,7 @@ export default {
|
||||
onFormSubmit (values) {
|
||||
this.isLoading = true
|
||||
this.isBtnLoading = true
|
||||
PartnersApi.add({ form: values })
|
||||
PartnersApi.add({ form: { ...values, qyt_enterprise_id: values.qyt_enterprise_id || 0 } })
|
||||
.then(result => {
|
||||
// 显示提示信息
|
||||
this.$message.success(result.message, 1.5)
|
||||
|
||||
@@ -37,8 +37,24 @@
|
||||
:data="loadData"
|
||||
:rowSelection="rowSelection"
|
||||
:pageSize="15"
|
||||
:scroll="{ x: 1450 }"
|
||||
:scroll="{ x: 1670 }"
|
||||
>
|
||||
<!-- 企油通企业:下单时实际使用的企业 -->
|
||||
<div slot="qyt" slot-scope="text, item">
|
||||
<template v-if="item.qyt_enterprise_source === 'bind'">
|
||||
<p class="qyt-name">{{ item.qyt_enterprise_name }}</p>
|
||||
<p class="qyt-sub">已绑定 · {{ item.qyt_enterprise_channel }}</p>
|
||||
</template>
|
||||
<template v-else-if="item.qyt_enterprise_source === 'default'">
|
||||
<p class="qyt-name">{{ item.qyt_enterprise_name }}</p>
|
||||
<p class="qyt-sub">未绑定,使用默认企业 · {{ item.qyt_enterprise_channel }}</p>
|
||||
</template>
|
||||
<template v-else-if="item.qyt_enterprise_source === 'invalid'">
|
||||
<p class="qyt-name text-red">绑定失效:{{ item.qyt_bind_name || '企业已删除' }}</p>
|
||||
<p class="qyt-sub">{{ item.qyt_enterprise_name ? '实际使用默认企业 ' + item.qyt_enterprise_name : '且没有默认企业,下单会被拒绝' }}</p>
|
||||
</template>
|
||||
<p v-else class="qyt-name text-red">无可用企业,下单会被拒绝</p>
|
||||
</div>
|
||||
<!-- 商品名称 -->
|
||||
<span slot="address" slot-scope="text">
|
||||
<p class="twoline-hide" style="width: 270px;">{{ text }}</p>
|
||||
@@ -79,6 +95,11 @@ const columns = [
|
||||
width: '150px',
|
||||
dataIndex: 'name'
|
||||
},
|
||||
{
|
||||
title: '企油通企业',
|
||||
width: '220px',
|
||||
scopedSlots: { customRender: 'qyt' }
|
||||
},
|
||||
{
|
||||
title: '合作商地址',
|
||||
width: '200px',
|
||||
@@ -260,4 +281,15 @@ export default {
|
||||
.tab-list {
|
||||
margin-right: 20px;
|
||||
}
|
||||
.qyt-name {
|
||||
margin: 0;
|
||||
}
|
||||
.qyt-sub {
|
||||
margin: 2px 0 0;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
.text-red {
|
||||
color: #f5222d;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -180,7 +180,8 @@ export default {
|
||||
onFormSubmit (values) {
|
||||
this.isLoading = true
|
||||
this.isBtnLoading = true
|
||||
PartnersApi.edit({ partnersId: this.partnersId, form: values })
|
||||
// 清空企业下拉框时值是 undefined,JSON 序列化会丢掉该字段,后端就不会解绑;显式提交 0
|
||||
PartnersApi.edit({ partnersId: this.partnersId, form: { ...values, qyt_enterprise_id: values.qyt_enterprise_id || 0 } })
|
||||
.then(result => {
|
||||
// 显示提示信息
|
||||
this.$message.success(result.message, 1.5)
|
||||
|
||||
@@ -43,6 +43,16 @@
|
||||
>{{ item.name }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="退款状态">
|
||||
<a-select v-decorator="['refund_status', { initialValue: -1 }]">
|
||||
<a-select-option :value="-1">全部</a-select-option>
|
||||
<a-select-option :value="0">未退款</a-select-option>
|
||||
<a-select-option :value="1">退款中</a-select-option>
|
||||
<a-select-option :value="2">已退款</a-select-option>
|
||||
<a-select-option :value="3">退款失败</a-select-option>
|
||||
<a-select-option :value="4">待人工确认</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="合作商">
|
||||
<a-select v-decorator="['partners_id', { initialValue: -1 }]">
|
||||
<a-select-option :value="-1">全部</a-select-option>
|
||||
@@ -91,9 +101,9 @@
|
||||
</div>
|
||||
<div slot="consume" slot-scope="item">
|
||||
<template v-if="item.use_status === 1">
|
||||
<p class="mtb-2 f-13">{{ item.station_name || '-' }}</p>
|
||||
<p class="mtb-2 f-13">{{ item.oil_name }} {{ item.litter }}L</p>
|
||||
<p class="mtb-2 f-13">实付:¥{{ (item.pay_fee / 100).toFixed(2) }}</p>
|
||||
<p class="mtb-2 f-13">已加油:¥{{ item.used_amount }}({{ item.used_num }} 次)</p>
|
||||
<p class="mtb-2 f-13">剩余:¥{{ remainAmount(item) }}</p>
|
||||
<p class="mtb-2 f-13">最近:{{ item.station_name || '-' }} {{ item.oil_name }} {{ item.litter }}L</p>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</div>
|
||||
@@ -115,6 +125,10 @@
|
||||
<a-tag :color="qytColor(item.oil_status_zsy)">{{ qytText(item.oil_status_zsy) }}</a-tag>
|
||||
</p>
|
||||
<p v-if="item.oil_text" class="mtb-2 f-13 order-remark">{{ item.oil_text }}</p>
|
||||
<p v-if="item.refund_status > 0" class="mtb-2">
|
||||
<span class="f-13">退款:</span>
|
||||
<a-tag :color="refundColor(item.refund_status)">{{ refundText(item.refund_status) }}</a-tag>
|
||||
</p>
|
||||
</div>
|
||||
<div class="actions" slot="action" slot-scope="item">
|
||||
<!--有付款权限,订单状态为进行中,已支付,并且未使用-->
|
||||
@@ -123,7 +137,7 @@
|
||||
@click="handleShare(item)"
|
||||
>重新发放</a>
|
||||
<a
|
||||
v-if="(inArray(item.oil_status_zsy, [2])) && item.oil_status === 1 && item.use_status === 0 && item.oilCardType === 'zsy_ticket'"
|
||||
v-if="(inArray(item.oil_status_zsy, [2])) && item.oil_status === 1 && item.refund_status !== 2 && Number(remainAmount(item)) > 0 && item.oilCardType === 'zsy_ticket'"
|
||||
@click="handlePay(item)"
|
||||
>加油链接</a>
|
||||
<a
|
||||
@@ -134,12 +148,16 @@
|
||||
v-if="(inArray(item.oil_status_zsy, [2])) && item.oil_status === 1 && item.use_status === 1 && item.oilCardType === 'zsy_ticket'"
|
||||
@click="handleUse(item)"
|
||||
>重推加油结果</a>
|
||||
<a v-if="canRefund(item)" @click="handleRefund(item)">{{ refundLabel(item) }}</a>
|
||||
<a v-if="item.use_status === 1" @click="handleInvoice(item)">发票</a>
|
||||
</div>
|
||||
</s-table>
|
||||
<ShareForm ref="ShareForm" @handleSubmit="handleRefresh" />
|
||||
<PayForm ref="PayForm" @handleSubmit="handleRefresh" />
|
||||
<OilForm ref="OilForm" @handleSubmit="handleRefresh" />
|
||||
<UseForm ref="UseForm" @handleSubmit="handleRefresh" />
|
||||
<RefundForm ref="RefundForm" @handleSubmit="handleRefresh" />
|
||||
<InvoiceModal ref="InvoiceModal" />
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
@@ -149,7 +167,7 @@ import { STable } from '@/components/Table'
|
||||
import {
|
||||
OilStatusEnum, UseStatusEnum
|
||||
} from '@/common/enum/order'
|
||||
import { OilForm, UseForm, ShareForm, PayForm } from './modules'
|
||||
import { OilForm, UseForm, ShareForm, PayForm, RefundForm, InvoiceModal } from './modules'
|
||||
import { assignment, inArray } from '@/utils/util'
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN'
|
||||
import * as PartnersApi from '@/api/partners'
|
||||
@@ -161,7 +179,9 @@ export default {
|
||||
ShareForm,
|
||||
PayForm,
|
||||
OilForm,
|
||||
UseForm
|
||||
UseForm,
|
||||
RefundForm,
|
||||
InvoiceModal
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -219,7 +239,7 @@ export default {
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: '180px',
|
||||
width: '220px',
|
||||
scopedSlots: { customRender: 'action' }
|
||||
},
|
||||
{
|
||||
@@ -300,6 +320,36 @@ export default {
|
||||
this.$refs.UseForm.show(record)
|
||||
},
|
||||
|
||||
// 退款状态文案 / 颜色
|
||||
refundText (v) {
|
||||
return { 0: '未退款', 1: '退款中', 2: '已退款', 3: '退款失败', 4: '待人工确认' }[v] || '未知'
|
||||
},
|
||||
refundColor (v) {
|
||||
return { 1: 'blue', 2: 'green', 3: 'red', 4: 'orange' }[v] || ''
|
||||
},
|
||||
// 发放成功才可能退款;已经有退款记录的单总是可以打开看流水
|
||||
canRefund (item) {
|
||||
if (item.oilCardType !== 'zsy_ticket' || item.oil_status_zsy !== 2 || item.oil_status !== 1) {
|
||||
return false
|
||||
}
|
||||
return item.refund_status > 0 || Number(this.remainAmount(item)) > 0
|
||||
},
|
||||
// 剩余未加油金额:订单金额 − 已加油 −(已退款时)退款金额
|
||||
remainAmount (item) {
|
||||
const refunded = item.refund_status === 2 ? Number(item.refund_amount || 0) : 0
|
||||
const remain = Number(item.total_price || 0) - Number(item.used_amount || 0) - refunded
|
||||
return Math.max(0, remain).toFixed(2)
|
||||
},
|
||||
refundLabel (item) {
|
||||
return { 2: '退款记录', 4: '确认退款' }[item.refund_status] || '退款'
|
||||
},
|
||||
handleRefund (record) {
|
||||
this.$refs.RefundForm.show(record)
|
||||
},
|
||||
handleInvoice (record) {
|
||||
this.$refs.InvoiceModal.show(record)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取代理商列表
|
||||
*/
|
||||
@@ -325,6 +375,17 @@ export default {
|
||||
.flex-box{
|
||||
display: flex;
|
||||
}
|
||||
// 操作栏:按钮较多会换行,横向纵向都留出间距(覆盖全局 .actions>a 的 8px 右边距)
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 16px;
|
||||
|
||||
& > a {
|
||||
margin-right: 0 !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
.form-total{
|
||||
color: #1890ff;
|
||||
margin-right: 10px;
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<a-modal
|
||||
title="加油发票"
|
||||
:width="620"
|
||||
:visible="visible"
|
||||
:footer="null"
|
||||
@cancel="visible = false"
|
||||
>
|
||||
<a-spin :spinning="isLoading">
|
||||
<a-alert type="info" show-icon class="block">
|
||||
<div slot="message">
|
||||
企油通发票按「企业 + 月份 + 司机」出具,和单笔订单没有一一对应关系。
|
||||
这里展示的是<b>该订单所属企业、该司机在消费当月</b>的全部发票,同一司机当月的其他订单看到的是同一批。
|
||||
</div>
|
||||
</a-alert>
|
||||
|
||||
<p>消费月份:<b>{{ month || '-' }}</b> 司机:{{ record.driver_name || '-' }} {{ record.mobile }}</p>
|
||||
<p v-if="row.sync_time_text" class="sub">最近同步:{{ row.sync_time_text }} {{ row.sync_text }}</p>
|
||||
<p v-else-if="message" class="sub">{{ message }}</p>
|
||||
|
||||
<div v-if="urls.length" class="links">
|
||||
<p v-for="(url, index) in urls" :key="url">
|
||||
<a :href="url" target="_blank" rel="noopener noreferrer">发票 {{ index + 1 }}</a>
|
||||
<span class="url">{{ url }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<a-empty v-else-if="!isLoading" description="企油通暂未出具该月发票" />
|
||||
|
||||
<div class="btns">
|
||||
<a-button :loading="isLoading" @click="load(true)">重新同步</a-button>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as Api from '@/api/partners/order'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
visible: false,
|
||||
isLoading: false,
|
||||
record: {},
|
||||
month: '',
|
||||
message: '',
|
||||
row: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
urls () {
|
||||
return this.row.invoice_urls || []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
show (record) {
|
||||
this.visible = true
|
||||
this.record = record
|
||||
this.month = ''
|
||||
this.message = ''
|
||||
this.row = {}
|
||||
this.load(false)
|
||||
},
|
||||
|
||||
load (refresh) {
|
||||
this.isLoading = true
|
||||
Api.invoice({ orderId: this.record.order_id, refresh: refresh ? 1 : 0 })
|
||||
.then(result => {
|
||||
this.month = result.data.month
|
||||
this.message = result.data.message
|
||||
this.row = result.data.row || {}
|
||||
refresh && this.$message.success(result.message, 1.5)
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.block {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.sub {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
.links p {
|
||||
margin-bottom: 6px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.url {
|
||||
margin-left: 8px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
.btns {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<a-modal
|
||||
title="订单退款"
|
||||
:width="760"
|
||||
:visible="visible"
|
||||
:footer="null"
|
||||
:maskClosable="false"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-spin :spinning="isLoading">
|
||||
<div class="info">
|
||||
<p>油卡编号:{{ record.oilCardNo }}</p>
|
||||
<p>司机:{{ record.driver_name || '-' }} {{ record.mobile }}</p>
|
||||
<p>订单金额:¥{{ record.total_price }} 已加油:¥{{ record.used_amount || '0.00' }}({{ record.used_num || 0 }} 次)</p>
|
||||
<p>
|
||||
退款金额:<b class="amount">¥{{ refundAmount }}</b>
|
||||
<span v-if="status === 2">(实际退款)</span>
|
||||
<span v-else>(剩余未加油金额,固定不可修改)</span>
|
||||
</p>
|
||||
<p>当前状态:<a-tag :color="statusColor">{{ statusText }}</a-tag></p>
|
||||
<p v-if="record.refund_text" class="sub">{{ record.refund_text }}</p>
|
||||
</div>
|
||||
|
||||
<a-alert v-if="(status === 0 || status === 3) && Number(refundAmount) <= 0" type="warning" show-icon class="block" message="订单金额已全部加油,没有可退金额" />
|
||||
|
||||
<!-- 可发起:未退款 / 退款失败,且还有剩余未加油金额 -->
|
||||
<template v-if="(status === 0 || status === 3) && Number(refundAmount) > 0">
|
||||
<a-alert type="info" show-icon class="block">
|
||||
<div slot="message">
|
||||
一张订单可以分多次加油,<b>退款金额固定为剩余未加油金额</b>,已加油的部分不退,每单只能成功退一次。
|
||||
<br>退款会先用企油通 7.17 查询司机支付凭证,再用 7.16 把凭证额度调低退款金额;成功后原路回补合作商余额,并推送油卡变动通知。
|
||||
<br>额度在凭证上按司机累计,若凭证可用余额不足退款金额(被该司机其他订单用掉),会直接拒绝。
|
||||
</div>
|
||||
</a-alert>
|
||||
<a-textarea v-model="reason" :rows="2" :maxLength="100" placeholder="退款原因(选填,会写入企油通调额记录)" />
|
||||
<div class="btns">
|
||||
<a-button type="primary" :loading="isLoading" @click="handleRefund">确认退款</a-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 待人工确认 -->
|
||||
<template v-if="status === 4">
|
||||
<a-alert type="warning" show-icon class="block">
|
||||
<div slot="message">
|
||||
调额请求已发给企油通但没有拿到明确结果。企油通调额接口<b>没有幂等保护</b>,重复发起会被重复扣减,所以这里不允许直接重试。
|
||||
<br>请先点「向企油通取证」:系统会查司机钱包流水(找退款单号)和凭证额度变化自动判定;判定不了再人工核对后提交。
|
||||
</div>
|
||||
</a-alert>
|
||||
<div class="btns">
|
||||
<a-button type="primary" :loading="isLoading" @click="handleConfirm">向企油通取证</a-button>
|
||||
</div>
|
||||
<a-divider orientation="left">人工判定</a-divider>
|
||||
<a-radio-group v-model="resolveResult">
|
||||
<a-radio value="success">企油通已调低额度(退款成功)</a-radio>
|
||||
<a-radio value="failed">企油通未调额(退款失败,可重新发起)</a-radio>
|
||||
</a-radio-group>
|
||||
<a-textarea v-model="resolveRemark" class="mt-10" :rows="2" :maxLength="100" placeholder="判定依据(必填),如:已在企油通后台核对凭证 xxx 总额度由 300 变为 200" />
|
||||
<div class="btns">
|
||||
<a-button :loading="isLoading" @click="handleResolve">提交人工判定</a-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a-divider orientation="left">退款流水</a-divider>
|
||||
<a-table
|
||||
size="small"
|
||||
rowKey="refund_id"
|
||||
:columns="columns"
|
||||
:dataSource="logs"
|
||||
:pagination="false"
|
||||
>
|
||||
<span slot="status" slot-scope="text, row">
|
||||
<a-tag :color="logColor(row.status)">{{ row.status_text }}</a-tag>
|
||||
</span>
|
||||
</a-table>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as Api from '@/api/partners/order'
|
||||
|
||||
const STATUS_TEXT = { 0: '未退款', 1: '退款中', 2: '已退款', 3: '退款失败', 4: '待人工确认' }
|
||||
const STATUS_COLOR = { 1: 'blue', 2: 'green', 3: 'red', 4: 'orange' }
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
visible: false,
|
||||
isLoading: false,
|
||||
record: {},
|
||||
reason: '',
|
||||
resolveResult: 'success',
|
||||
resolveRemark: '',
|
||||
logs: [],
|
||||
columns: [
|
||||
{ title: '退款单号', dataIndex: 'refund_no' },
|
||||
{ title: '状态', scopedSlots: { customRender: 'status' } },
|
||||
{ title: '凭证', dataIndex: 'quota_id' },
|
||||
{ title: '调额前可用', dataIndex: 'available_before' },
|
||||
{ title: '说明', dataIndex: 'message' },
|
||||
{ title: '操作人', dataIndex: 'operator' },
|
||||
{ title: '时间', dataIndex: 'create_time_text' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
status () {
|
||||
return Number(this.record.refund_status || 0)
|
||||
},
|
||||
statusText () {
|
||||
return STATUS_TEXT[this.status] || '-'
|
||||
},
|
||||
statusColor () {
|
||||
return STATUS_COLOR[this.status] || ''
|
||||
},
|
||||
// 已退款显示实际退款金额;否则为剩余未加油金额(后端同口径计算)
|
||||
refundAmount () {
|
||||
if (this.status === 2) {
|
||||
return Number(this.record.refund_amount || 0).toFixed(2)
|
||||
}
|
||||
const remain = Number(this.record.total_price || 0) - Number(this.record.used_amount || 0)
|
||||
return Math.max(0, remain).toFixed(2)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
show (record) {
|
||||
this.visible = true
|
||||
this.record = { ...record }
|
||||
this.reason = ''
|
||||
this.resolveResult = 'success'
|
||||
this.resolveRemark = ''
|
||||
this.loadLogs()
|
||||
},
|
||||
|
||||
handleCancel () {
|
||||
this.visible = false
|
||||
},
|
||||
|
||||
loadLogs () {
|
||||
Api.refundLog({ orderId: this.record.order_id }).then(result => {
|
||||
this.logs = result.data.list
|
||||
// 流水状态码与订单退款状态一致:用最新一条同步弹窗状态,失败 / 转人工后界面跟着切换
|
||||
if (this.logs.length) {
|
||||
this.record = { ...this.record, refund_status: Number(this.logs[0].status), refund_text: this.logs[0].message }
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
logColor (status) {
|
||||
return STATUS_COLOR[Number(status)] || ''
|
||||
},
|
||||
|
||||
// 退款 / 取证 / 判定之后统一刷新:弹窗里的状态 + 父列表
|
||||
afterAction (result, status) {
|
||||
this.$message.success(result.message, 2)
|
||||
if (status !== undefined) {
|
||||
this.record = { ...this.record, refund_status: status, refund_text: result.message }
|
||||
}
|
||||
this.loadLogs()
|
||||
this.$emit('handleSubmit')
|
||||
},
|
||||
|
||||
// 业务失败时后端返回错误,request 拦截器已弹出提示;这里仍需刷新状态(可能已变成失败/待确认)
|
||||
afterError () {
|
||||
this.loadLogs()
|
||||
this.$emit('handleSubmit')
|
||||
},
|
||||
|
||||
handleRefund () {
|
||||
this.$confirm({
|
||||
title: '确认退款 ¥' + this.refundAmount + '?',
|
||||
content: '退还剩余未加油金额:将调低该司机企油通凭证额度,成功后回补合作商余额。',
|
||||
onOk: () => {
|
||||
this.isLoading = true
|
||||
return Api.refund({ orderId: this.record.order_id, reason: this.reason })
|
||||
.then(result => this.afterAction(result, 2))
|
||||
.catch(() => this.afterError())
|
||||
.finally(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
handleConfirm () {
|
||||
this.isLoading = true
|
||||
Api.refundConfirm({ orderId: this.record.order_id })
|
||||
.then(result => this.afterAction(result, result.data.refundStatus))
|
||||
.catch(() => this.afterError())
|
||||
.finally(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
},
|
||||
|
||||
handleResolve () {
|
||||
if (!this.resolveRemark.trim()) {
|
||||
this.$message.warning('请填写判定依据')
|
||||
return
|
||||
}
|
||||
this.isLoading = true
|
||||
Api.refundResolve({ orderId: this.record.order_id, result: this.resolveResult, remark: this.resolveRemark })
|
||||
.then(result => this.afterAction(result, result.data.refundStatus))
|
||||
.catch(() => this.afterError())
|
||||
.finally(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.info p {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.amount {
|
||||
color: #f5222d;
|
||||
}
|
||||
.sub {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
.block {
|
||||
margin: 12px 0;
|
||||
}
|
||||
.btns {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -2,5 +2,7 @@ import OilForm from './OilForm'
|
||||
import UseForm from './UseForm'
|
||||
import ShareForm from './ShareForm'
|
||||
import PayForm from './PayForm'
|
||||
import RefundForm from './RefundForm'
|
||||
import InvoiceModal from './InvoiceModal'
|
||||
|
||||
export { OilForm, UseForm, ShareForm, PayForm }
|
||||
export { OilForm, UseForm, ShareForm, PayForm, RefundForm, InvoiceModal }
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
<template>
|
||||
<a-card :bordered="false">
|
||||
<div class="card-title">{{ $route.meta.title }}</div>
|
||||
|
||||
<a-alert type="info" show-icon class="tips-alert">
|
||||
<div slot="message">
|
||||
司机按身份证号归档。企油通发放到同一司机的额度会<b>在凭证上累计</b>,列表里的累计发放 / 已退款是按本地订单汇总的参考值,
|
||||
<b>真实可用额度以详情里实时查询的企油通数据为准</b>。
|
||||
标红的「信息冲突」会导致上游实名二要素核验失败,建议先让下单平台核对司机信息。
|
||||
</div>
|
||||
</a-alert>
|
||||
|
||||
<a-form layout="inline" :form="searchForm" class="search-form" @submit="handleSearch">
|
||||
<a-form-item label="司机">
|
||||
<a-input v-decorator="['keyword']" placeholder="姓名 / 手机号 / 完整身份证号" allowClear style="width: 240px" />
|
||||
</a-form-item>
|
||||
<a-form-item label="信息冲突">
|
||||
<a-select v-decorator="['conflict', { initialValue: -1 }]" style="width: 110px">
|
||||
<a-select-option :value="-1">全部</a-select-option>
|
||||
<a-select-option :value="1">有冲突</a-select-option>
|
||||
<a-select-option :value="0">正常</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit" icon="search">搜索</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<s-table ref="table" rowKey="driver_id" :columns="columns" :data="loadData">
|
||||
<template slot="driver" slot-scope="item">
|
||||
<p class="mtb-2">
|
||||
<b>{{ item.name || '-' }}</b>
|
||||
<a-tag v-if="item.conflict" color="red" class="ml-6">信息冲突</a-tag>
|
||||
</p>
|
||||
<p class="mtb-2 sub">{{ item.mobile }} {{ item.id_card_mask }}</p>
|
||||
<p v-if="item.conflict" class="mtb-2 text-red">{{ item.conflict_text }}</p>
|
||||
</template>
|
||||
<template slot="orders" slot-scope="item">
|
||||
<p class="mtb-2">共 {{ item.order_num }} 单</p>
|
||||
<p class="mtb-2 sub">已加油 {{ item.used_num }} 发放中 {{ item.pending_num }} 失败 {{ item.fail_num }}</p>
|
||||
</template>
|
||||
<template slot="amount" slot-scope="item">
|
||||
<p class="mtb-2">累计发放 ¥{{ item.grant_amount }}</p>
|
||||
<p class="mtb-2 sub">已加油 ¥{{ item.used_amount }} 已退款 ¥{{ item.refund_amount }} 净额 ¥{{ item.net_amount }}</p>
|
||||
</template>
|
||||
<template slot="action" slot-scope="item">
|
||||
<a @click="openDetail(item)">详情 / 查额度</a>
|
||||
</template>
|
||||
</s-table>
|
||||
|
||||
<a-drawer
|
||||
title="司机详情"
|
||||
:width="960"
|
||||
:visible="drawer"
|
||||
@close="drawer = false"
|
||||
>
|
||||
<a-spin :spinning="detailLoading">
|
||||
<template v-if="detail.driver_id">
|
||||
<p class="head">
|
||||
<b>{{ detail.name }}</b> {{ detail.mobile }} {{ detail.id_card_mask }}
|
||||
<a-tag v-if="detail.conflict" color="red" class="ml-6">信息冲突</a-tag>
|
||||
</p>
|
||||
<a-alert v-if="detail.conflict" type="error" :message="detail.conflict_text" show-icon class="block" />
|
||||
<p class="sub">
|
||||
本地汇总:共 {{ detail.order_num }} 单,累计发放 ¥{{ detail.grant_amount }},已退款 ¥{{ detail.refund_amount }},
|
||||
净额 ¥{{ detail.net_amount }};最近下单 {{ detail.last_order_time_text }}
|
||||
</p>
|
||||
|
||||
<a-divider orientation="left">企油通实时数据</a-divider>
|
||||
<div class="toolbar">
|
||||
<span>企业:</span>
|
||||
<a-select v-model="enterpriseId" style="width: 280px" placeholder="选择企业">
|
||||
<a-select-option v-for="item in enterprises" :key="item.enterprise_id" :value="item.enterprise_id">
|
||||
{{ item.company_name }}({{ item.passageway === 'pingan' ? '平安银行' : '支付宝企业码' }})
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<span v-if="!enterprises.length" class="sub ml-6">该司机还没有关联企业的订单</span>
|
||||
</div>
|
||||
|
||||
<a-tabs v-model="tab" @change="onTabChange">
|
||||
<a-tab-pane key="balance" tab="钱包余额(7.12)">
|
||||
<a-button :loading="upLoading" :disabled="!enterpriseId" @click="query('balance')">查询</a-button>
|
||||
<template v-if="result.balance">
|
||||
<p class="big">
|
||||
钱包发放总额 ¥{{ result.balance.totalGrantAmountYuan || '0.00' }}
|
||||
当前可用 <b class="green">¥{{ result.balance.availableBalanceYuan || '0.00' }}</b>
|
||||
</p>
|
||||
<a-table size="small" rowKey="business_license" :pagination="false" :columns="balanceColumns" :dataSource="result.balance.list || []" />
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="voucher" tab="支付凭证(7.17)">
|
||||
<a-button :loading="upLoading" :disabled="!enterpriseId" @click="query('voucher')">查询</a-button>
|
||||
<p class="sub mt-8">额度按凭证累计;退款时就是把其中「生效中、可用余额最大」的一张调低订单金额。</p>
|
||||
<a-table
|
||||
v-if="result.voucher"
|
||||
size="small"
|
||||
rowKey="quota_id"
|
||||
:pagination="false"
|
||||
:columns="voucherColumns"
|
||||
:dataSource="result.voucher.list || []"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="wallet" tab="钱包流水(7.13)">
|
||||
<a-button :loading="upLoading" :disabled="!enterpriseId" @click="query('wallet')">查询</a-button>
|
||||
<p class="sub mt-8">企油通默认只回溯近 1 个月流水。</p>
|
||||
<a-table
|
||||
v-if="result.wallet"
|
||||
size="small"
|
||||
rowKey="transactionId"
|
||||
:pagination="false"
|
||||
:columns="walletColumns"
|
||||
:dataSource="result.wallet.transactionDetails || []"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="orders" tab="最近订单">
|
||||
<p class="sub">发放失败原因里能看到上游实名二要素核验的结果。</p>
|
||||
<a-table size="small" rowKey="order_id" :pagination="false" :columns="orderColumns" :dataSource="orders">
|
||||
<span slot="zsy" slot-scope="text, row">
|
||||
<a-tag :color="{ 1: 'blue', 2: 'green', 3: 'red' }[row.oil_status_zsy] || ''">
|
||||
{{ { 0: '未提交', 1: '发放中', 2: '发放成功', 3: '发放失败' }[row.oil_status_zsy] }}
|
||||
</a-tag>
|
||||
</span>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</template>
|
||||
</a-spin>
|
||||
</a-drawer>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as Api from '@/api/qyt/driver'
|
||||
import { STable } from '@/components/Table'
|
||||
|
||||
export default {
|
||||
name: 'QytDriverIndex',
|
||||
components: { STable },
|
||||
data () {
|
||||
return {
|
||||
searchForm: this.$form.createForm(this),
|
||||
queryParam: {},
|
||||
columns: [
|
||||
{ title: 'ID', dataIndex: 'driver_id', width: 70 },
|
||||
{ title: '司机', scopedSlots: { customRender: 'driver' } },
|
||||
{ title: '订单', scopedSlots: { customRender: 'orders' } },
|
||||
{ title: '金额(本地口径)', scopedSlots: { customRender: 'amount' } },
|
||||
{ title: '最近下单', dataIndex: 'last_order_time_text', width: 170 },
|
||||
{ title: '操作', width: 120, scopedSlots: { customRender: 'action' } }
|
||||
],
|
||||
loadData: param => {
|
||||
return Api.list({ ...param, ...this.queryParam }).then(result => result.data.list)
|
||||
},
|
||||
|
||||
drawer: false,
|
||||
detailLoading: false,
|
||||
detail: {},
|
||||
enterprises: [],
|
||||
orders: [],
|
||||
enterpriseId: undefined,
|
||||
tab: 'balance',
|
||||
upLoading: false,
|
||||
result: {},
|
||||
|
||||
balanceColumns: [
|
||||
{ title: '企业', dataIndex: 'enterprise_name' },
|
||||
{ title: '凭证总额(元)', dataIndex: 'total_amount_yuan' },
|
||||
{ title: '可用(元)', dataIndex: 'available_amount_yuan' }
|
||||
],
|
||||
voucherColumns: [
|
||||
{ title: '凭证ID', dataIndex: 'quota_id' },
|
||||
{ title: '名称', dataIndex: 'quota_name' },
|
||||
{ title: '总额度', dataIndex: 'total_amount' },
|
||||
{ title: '可用', dataIndex: 'available_amount' },
|
||||
{ title: '已用', dataIndex: 'used_amount' },
|
||||
{ title: '冻结', dataIndex: 'freeze_amount' },
|
||||
{ title: '状态', dataIndex: 'status_text' },
|
||||
{ title: '有效期至', dataIndex: 'effective_end' }
|
||||
],
|
||||
walletColumns: [
|
||||
{ title: '时间', dataIndex: 'transactionTime' },
|
||||
{ title: '类型', dataIndex: 'transactionType' },
|
||||
{ title: '金额(元)', dataIndex: 'amount_yuan' },
|
||||
{ title: '备注', dataIndex: 'remark' },
|
||||
{ title: '流水号', dataIndex: 'transactionId' }
|
||||
],
|
||||
orderColumns: [
|
||||
{ title: '油卡编号', dataIndex: 'oilCardNo' },
|
||||
{ title: '金额', dataIndex: 'total_price' },
|
||||
{ title: '发放', scopedSlots: { customRender: 'zsy' } },
|
||||
{ title: '说明', dataIndex: 'oil_text' },
|
||||
{ title: '退款', dataIndex: 'refund_status_text' },
|
||||
{ title: '下单时间', dataIndex: 'create_time_text' }
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
handleSearch (e) {
|
||||
e.preventDefault()
|
||||
this.searchForm.validateFields((errors, values) => {
|
||||
if (errors) return
|
||||
this.queryParam = { keyword: values.keyword || '', conflict: values.conflict }
|
||||
this.$refs.table.refresh(true)
|
||||
})
|
||||
},
|
||||
|
||||
openDetail (item) {
|
||||
this.drawer = true
|
||||
this.detail = {}
|
||||
this.enterprises = []
|
||||
this.orders = []
|
||||
this.result = {}
|
||||
this.tab = 'balance'
|
||||
this.enterpriseId = undefined
|
||||
this.detailLoading = true
|
||||
Api.detail({ driverId: item.driver_id })
|
||||
.then(result => {
|
||||
this.detail = result.data.detail
|
||||
this.enterprises = result.data.enterprises
|
||||
this.orders = result.data.orders
|
||||
this.enterpriseId = this.enterprises.length ? this.enterprises[0].enterprise_id : undefined
|
||||
})
|
||||
.finally(() => {
|
||||
this.detailLoading = false
|
||||
})
|
||||
},
|
||||
|
||||
onTabChange () {
|
||||
// 切换页签不自动查上游,避免无意义地打企油通;需要时点「查询」
|
||||
},
|
||||
|
||||
query (type) {
|
||||
this.upLoading = true
|
||||
Api.upstream({ driverId: this.detail.driver_id, enterpriseId: this.enterpriseId, type })
|
||||
.then(result => {
|
||||
this.result = { ...this.result, [type]: result.data.data }
|
||||
})
|
||||
.finally(() => {
|
||||
this.upLoading = false
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tips-alert {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.sub {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
.ml-6 {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.mt-8 {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.text-red {
|
||||
color: #f5222d;
|
||||
font-size: 12px;
|
||||
}
|
||||
.green {
|
||||
color: #52c41a;
|
||||
}
|
||||
.head {
|
||||
font-size: 15px;
|
||||
}
|
||||
.block {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.toolbar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.big {
|
||||
margin: 12px 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
</style>
|
||||
@@ -64,18 +64,15 @@
|
||||
<div v-if="item.sync_text" class="sub">{{ item.sync_text }}</div>
|
||||
</template>
|
||||
|
||||
<template slot="action" slot-scope="item">
|
||||
<div slot="action" slot-scope="item" class="op-actions">
|
||||
<a @click="handleUpdate(item)">编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
<a @click="handleApply(item)">提交开户</a>
|
||||
<a-divider type="vertical" />
|
||||
<a @click="handleSyncAccount(item)">同步账户</a>
|
||||
<a-divider type="vertical" />
|
||||
<a v-if="item.passageway === 'pingan'" @click="handleBalance(item)">账户余额</a>
|
||||
<a v-if="!item.is_default" @click="handleSetDefault(item)">设为默认</a>
|
||||
<span v-else class="text-grey">设为默认</span>
|
||||
<a-divider type="vertical" />
|
||||
<a class="text-red" @click="handleDelete(item)">删除</a>
|
||||
</template>
|
||||
</div>
|
||||
</s-table>
|
||||
|
||||
<a-modal
|
||||
@@ -190,7 +187,7 @@ export default {
|
||||
{ title: '企业', scopedSlots: { customRender: 'company' } },
|
||||
{ title: '通道与机构号', scopedSlots: { customRender: 'passageway' } },
|
||||
{ title: '企油通状态', scopedSlots: { customRender: 'status' } },
|
||||
{ title: '操作', width: 260, scopedSlots: { customRender: 'action' } }
|
||||
{ title: '操作', width: 280, scopedSlots: { customRender: 'action' } }
|
||||
],
|
||||
loadData: param => {
|
||||
return Api.list({ ...param, ...this.queryParam }).then(result => result.data.list)
|
||||
@@ -310,6 +307,17 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
// 7.11 企业账户余额(平安银行银行侧实时查询,勿频繁点击)
|
||||
handleBalance (item) {
|
||||
Api.balance({ enterpriseId: item.enterprise_id }).then(result => {
|
||||
this.$info({
|
||||
title: item.company_name + ' · 平安银行账户余额',
|
||||
content: '账面余额 ¥' + result.data.balance + ',可用余额 ¥' + result.data.available_balance +
|
||||
'(可用余额已扣除冻结/止付金额,即当前可发放凭证的资金上限)'
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
handleSyncList () {
|
||||
this.isSyncing = true
|
||||
Api.syncList({})
|
||||
@@ -351,4 +359,15 @@ export default {
|
||||
.text-grey {
|
||||
color: #bbb;
|
||||
}
|
||||
// 操作栏:按钮较多会换行,用 gap 统一横纵间距,不用竖线分隔(换行后竖线会悬在行首行尾)
|
||||
.op-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 16px;
|
||||
|
||||
& > a,
|
||||
& > span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<a-card :bordered="false">
|
||||
<div class="card-title">{{ $route.meta.title }}</div>
|
||||
|
||||
<a-alert type="info" show-icon class="tips-alert">
|
||||
<div slot="message">
|
||||
企油通 7.14 按「企业 + 月份(+ 司机手机号)」出具发票,发票地址与单笔订单没有对应关系。
|
||||
订单列表里点「发票」会自动按该订单的企业、消费月份、司机同步;这里可以按月批量同步或手动补同步。
|
||||
</div>
|
||||
</a-alert>
|
||||
|
||||
<a-form layout="inline" :form="syncForm" class="sync-form" @submit="handleSync">
|
||||
<a-form-item label="企业">
|
||||
<a-select v-decorator="['enterpriseId', { initialValue: 0 }]" style="width: 220px">
|
||||
<a-select-option :value="0">全部启用企业(企业全量)</a-select-option>
|
||||
<a-select-option v-for="item in enterpriseList" :key="item.enterprise_id" :value="item.enterprise_id">
|
||||
{{ item.company_name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="月份">
|
||||
<a-month-picker v-decorator="['month', { rules: [{ required: true, message: '请选择月份' }] }]" placeholder="选择月份" />
|
||||
</a-form-item>
|
||||
<a-form-item label="司机手机号">
|
||||
<a-input v-decorator="['mobile']" placeholder="选填,仅指定企业时有效" allowClear />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit" icon="sync" :loading="isSyncing">同步发票</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<a-divider />
|
||||
|
||||
<a-form layout="inline" :form="searchForm" class="search-form" @submit="handleSearch">
|
||||
<a-form-item label="企业">
|
||||
<a-select v-decorator="['qyt_enterprise_id', { initialValue: 0 }]" style="width: 200px">
|
||||
<a-select-option :value="0">全部</a-select-option>
|
||||
<a-select-option v-for="item in enterpriseList" :key="item.enterprise_id" :value="item.enterprise_id">
|
||||
{{ item.company_name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="月份">
|
||||
<a-month-picker v-decorator="['month']" placeholder="全部" />
|
||||
</a-form-item>
|
||||
<a-form-item label="司机手机号">
|
||||
<a-input v-decorator="['mobile']" placeholder="手机号" allowClear />
|
||||
</a-form-item>
|
||||
<a-form-item label="发票">
|
||||
<a-select v-decorator="['has_invoice', { initialValue: -1 }]" style="width: 110px">
|
||||
<a-select-option :value="-1">全部</a-select-option>
|
||||
<a-select-option :value="1">已出具</a-select-option>
|
||||
<a-select-option :value="0">暂无</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit" icon="search">搜索</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<s-table ref="table" rowKey="invoice_id" :columns="columns" :data="loadData">
|
||||
<template slot="dimension" slot-scope="item">
|
||||
<p class="mtb-2"><b>{{ item.company_name }}</b></p>
|
||||
<p class="mtb-2 sub">{{ item.passageway === 'pingan' ? '平安银行' : '支付宝企业码' }} {{ item.month }} {{ item.mobile_mask }}</p>
|
||||
</template>
|
||||
<template slot="invoices" slot-scope="item">
|
||||
<template v-if="item.invoice_urls.length">
|
||||
<p v-for="(url, index) in item.invoice_urls" :key="url" class="mtb-2">
|
||||
<a :href="url" target="_blank" rel="noopener noreferrer">发票 {{ index + 1 }}</a>
|
||||
</p>
|
||||
</template>
|
||||
<span v-else class="sub">暂无</span>
|
||||
</template>
|
||||
<template slot="sync" slot-scope="item">
|
||||
<a-tag :color="{ 1: 'green', 2: 'red' }[item.sync_status] || ''">{{ { 0: '未同步', 1: '成功', 2: '失败' }[item.sync_status] }}</a-tag>
|
||||
<p class="mtb-2 sub">{{ item.sync_text }}</p>
|
||||
<p class="mtb-2 sub">{{ item.sync_time_text }}</p>
|
||||
</template>
|
||||
<template slot="action" slot-scope="item">
|
||||
<a @click="handleResync(item)">重新同步</a>
|
||||
</template>
|
||||
</s-table>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as Api from '@/api/qyt/invoice'
|
||||
import * as EnterpriseApi from '@/api/qyt/enterprise'
|
||||
import { STable } from '@/components/Table'
|
||||
|
||||
export default {
|
||||
name: 'QytInvoiceIndex',
|
||||
components: { STable },
|
||||
data () {
|
||||
return {
|
||||
syncForm: this.$form.createForm(this),
|
||||
searchForm: this.$form.createForm(this),
|
||||
enterpriseList: [],
|
||||
isSyncing: false,
|
||||
queryParam: {},
|
||||
columns: [
|
||||
{ title: 'ID', dataIndex: 'invoice_id', width: 70 },
|
||||
{ title: '企业 / 通道 / 月份 / 维度', scopedSlots: { customRender: 'dimension' } },
|
||||
{ title: '发票', scopedSlots: { customRender: 'invoices' } },
|
||||
{ title: '加油明细条数', dataIndex: 'order_count', width: 110 },
|
||||
{ title: '同步状态', scopedSlots: { customRender: 'sync' } },
|
||||
{ title: '操作', width: 100, scopedSlots: { customRender: 'action' } }
|
||||
],
|
||||
loadData: param => {
|
||||
return Api.list({ ...param, ...this.queryParam }).then(result => result.data.list)
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
EnterpriseApi.options().then(result => {
|
||||
this.enterpriseList = result.data.list
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
|
||||
handleSync (e) {
|
||||
e.preventDefault()
|
||||
this.syncForm.validateFields((errors, values) => {
|
||||
if (errors) return
|
||||
this.doSync({
|
||||
enterpriseId: values.enterpriseId || 0,
|
||||
month: values.month.format('YYYY-MM'),
|
||||
mobile: values.enterpriseId ? (values.mobile || '') : ''
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
handleResync (item) {
|
||||
this.doSync({ enterpriseId: item.qyt_enterprise_id, month: item.month, mobile: item.mobile })
|
||||
},
|
||||
|
||||
doSync (data) {
|
||||
this.isSyncing = true
|
||||
Api.sync(data)
|
||||
.then(result => {
|
||||
this.$message.success(result.message, 2)
|
||||
this.$refs.table.refresh()
|
||||
})
|
||||
.finally(() => {
|
||||
this.isSyncing = false
|
||||
})
|
||||
},
|
||||
|
||||
handleSearch (e) {
|
||||
e.preventDefault()
|
||||
this.searchForm.validateFields((errors, values) => {
|
||||
if (errors) return
|
||||
this.queryParam = {
|
||||
qyt_enterprise_id: values.qyt_enterprise_id || '',
|
||||
month: values.month ? values.month.format('YYYY-MM') : '',
|
||||
mobile: values.mobile || '',
|
||||
has_invoice: values.has_invoice
|
||||
}
|
||||
this.$refs.table.refresh(true)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tips-alert {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.sub {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -18,7 +18,9 @@ use app\common\model\Partners as PartnersModel;
|
||||
use app\common\crypto\RsaUtil;
|
||||
use app\common\service\PartnersBalance;
|
||||
use app\common\model\qyt\Enterprise as EnterpriseModel;
|
||||
use app\common\service\QytDriver;
|
||||
use app\common\service\QytOil;
|
||||
use app\common\service\QytRefund;
|
||||
use app\store\model\order\QrCode as QrCodeModel;
|
||||
use cores\exception\BaseException;
|
||||
use think\facade\Request;
|
||||
@@ -168,6 +170,14 @@ class OilCard extends Controller
|
||||
if($created){
|
||||
// 回填流水的订单ID(建单前尚不知 order_id)
|
||||
PartnersBalance::bindOrder($balanceLogId, (int)$OrderModel->order_id);
|
||||
// 司机档案(按身份证号归档并标记信息冲突;内部吞异常,不影响建单)
|
||||
QytDriver::touch([
|
||||
'order_id' => $OrderModel->order_id,
|
||||
'id_card' => $idCard,
|
||||
'driver_name' => $driverName,
|
||||
'mobile' => $mobile,
|
||||
'store_id' => $this->storeId,
|
||||
]);
|
||||
// 存入队列处理
|
||||
$queueService = new QueueService();
|
||||
$queueService->push('partners_queue', json_encode(['order_id'=>$OrderModel->order_id],320));
|
||||
@@ -186,6 +196,76 @@ class OilCard extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单退款(整单全额)
|
||||
* 企油通侧:7.17 查司机支付凭证 -> 7.16 按订单金额调低额度;成功后回补合作商余额,并推送油卡变动通知(optType=refund)
|
||||
* @return Json
|
||||
*/
|
||||
public function refund()
|
||||
{
|
||||
$headers = Request::header();
|
||||
$data = json_decode(Request::instance()->getContent(), true);
|
||||
$appKey = $headers['appkey'] ?? '';
|
||||
$requestId = $headers['requestid'] ?? '';
|
||||
$method = $headers['method'] ?? '';
|
||||
if($method != 'syb.oil.oilCard.refund'){
|
||||
return $this->oilRenderError('12', '0005');
|
||||
}
|
||||
try {
|
||||
Db::name('request_log')->insert([
|
||||
'request_id' => $requestId,
|
||||
'app_key' => $appKey,
|
||||
'create_time' => time()
|
||||
]);
|
||||
} catch (\think\Exception $e) {
|
||||
if (strpos($e->getMessage(), 'Duplicate entry') !== false) {
|
||||
return $this->oilRenderError('12', '0012');
|
||||
}
|
||||
return $this->oilRenderError('99', '9999');
|
||||
}
|
||||
$partners = PartnersModel::detail(['appKey' => $appKey]);
|
||||
if(!$partners){
|
||||
return $this->oilRenderError('02','0004');
|
||||
}
|
||||
$crypto = new RsaUtil($partners);
|
||||
$result = $crypto->processResponse(['header'=>$headers,'encryptData'=>$data]);
|
||||
if ($result['code'] !== '00') {
|
||||
return $this->oilRenderError($result['code'], $result['errCode'], '', $result['errMessage'] ?? '');
|
||||
}
|
||||
$body = $result['data'];
|
||||
if($partners['orgNo'] != ($body['orgNo'] ?? '')){
|
||||
return $this->oilRenderError('12','0014');
|
||||
}
|
||||
$oilCardNo = trim((string)($body['oilCardNo'] ?? ''));
|
||||
if($oilCardNo === ''){
|
||||
return $this->oilRenderError('12','0001', '', '缺少必填请求参数 oilCardNo');
|
||||
}
|
||||
$reason = mb_substr(trim((string)($body['reason'] ?? '')), 0, 100);
|
||||
|
||||
// 只能退自己的订单
|
||||
$order = OrderModel::detail(['oilCardNo' => $oilCardNo, 'partners_id' => $partners['partners_id']]);
|
||||
if(!$order){
|
||||
return $this->oilRenderError('01','2036');
|
||||
}
|
||||
|
||||
$res = QytRefund::apply((int)$order['order_id'], 'api', (string)$appKey, $reason);
|
||||
// 已退款(含重复调用的幂等返回)或结果确认中:都按受理成功返回,由 refundStatus 区分
|
||||
// 确认中说明 7.16 已发出、上游可能已调额,不能告诉下游「失败」让它重试
|
||||
if ($res['ok'] || $res['status'] === QytRefund::CONFIRM) {
|
||||
return $this->oilRenderSuccess($res['data'], '00', $res['message']);
|
||||
}
|
||||
switch ($res['reason']) {
|
||||
case QytRefund::REASON_NOT_FOUND:
|
||||
return $this->oilRenderError('01', '2036', '', $res['message']);
|
||||
case QytRefund::REASON_PROCESSING:
|
||||
return $this->oilRenderError('01', '2032', '', $res['message']);
|
||||
case QytRefund::REASON_STATE:
|
||||
return $this->oilRenderError('01', '2048', '', $res['message']);
|
||||
default:
|
||||
return $this->oilRenderError('01', '2049', '', $res['message']);
|
||||
}
|
||||
}
|
||||
|
||||
public static function createOrderNo(): string
|
||||
{
|
||||
// 获取当前时间戳到毫秒(13位)
|
||||
@@ -469,7 +549,11 @@ class OilCard extends Controller
|
||||
return $this->oilRenderError('12','0013', '', $validate->getError());
|
||||
}
|
||||
|
||||
$detail = OrderModel::detail(['oilCardNo' => $data['oilCardNo']]);
|
||||
// 只能查自己的订单(原先不校验归属,一个合作商可以查到别家的单)
|
||||
$detail = OrderModel::detail(['oilCardNo' => $data['oilCardNo'], 'partners_id' => $partners['partners_id']]);
|
||||
if(!$detail){
|
||||
return $this->oilRenderError('01','2036');
|
||||
}
|
||||
// 组装返回格式
|
||||
$result = [
|
||||
'mobile' => $detail['mobile'],
|
||||
@@ -482,6 +566,15 @@ class OilCard extends Controller
|
||||
'oil_time' => $detail['oil_time'],
|
||||
'use_status' => $detail['use_status'],
|
||||
'use_time' => $detail['use_time'],
|
||||
// 一单可多次加油:已加油金额 / 加油次数 / 剩余可加油(即可退)金额
|
||||
'used_amount' => number_format((float)$detail['used_amount'], 2, '.', ''),
|
||||
'used_num' => (int)$detail['used_num'],
|
||||
'remain_amount' => number_format(max(0, (float)$detail['total_price'] - (float)$detail['used_amount']
|
||||
- ((int)$detail['refund_status'] === 2 ? (float)$detail['refund_amount'] : 0)), 2, '.', ''),
|
||||
// 退款:0未退款 1退款中 2已退款 3退款失败 4退款结果确认中
|
||||
'refund_status' => (int)$detail['refund_status'],
|
||||
'refund_amount' => number_format((float)$detail['refund_amount'], 2, '.', ''),
|
||||
'refund_time' => $detail['refund_time'] ? date('Y-m-d H:i:s', (int)$detail['refund_time']) : '',
|
||||
];
|
||||
return $this->oilRenderSuccess($result);
|
||||
} else {
|
||||
|
||||
@@ -108,8 +108,27 @@ class Qyt
|
||||
|
||||
$params['sign'] = $this->md5Sha1RsaSign($params);
|
||||
|
||||
// 文档示例形如 https://paytaskpage.worknoya.vip/#/?appId=..&mobile=..×tamp=..&sign=..#/
|
||||
return $base . '/?' . http_build_query($params) . '#/';
|
||||
// 最终形如 https://paytaskpage.worknoya.vip/#/?appId=..&mobile=..×tamp=..&sign=iwxE****g==
|
||||
//
|
||||
// ⚠️ 两个坑,都是实测踩出来的,改动前务必看完:
|
||||
//
|
||||
// 1) 不能用 http_build_query:它会把 base64 签名里的 + / = 编码成 %2B %2F %3D。
|
||||
// 参数挂在 # 之后的 fragment 里,浏览器不会自动解码,落地页若不做
|
||||
// decodeURIComponent,拿到的签名串就和我们签的不一致 -> 验签失败。
|
||||
// 文档示例里末尾是裸的 ==,对方期望的就是未编码的原始值。
|
||||
// appId / mobile / timestamp / creditcode 都是字母数字,本来也无需编码。
|
||||
// 若对接人日后确认页面会解码,把 h5_sign_urlencode 配成 true 即可切回。
|
||||
//
|
||||
// 2) 末尾【不能】再拼 '#/'。文档示例末尾是有 #/ 的,但实测:带上之后
|
||||
// 桌面浏览器能打开,微信里打开 / 扫码一律提示验签失败 —— 落地页会把
|
||||
// 这个 #/ 一起算进最后一个参数 sign 的值里。去掉后微信正常。
|
||||
// ⚠️ 别照着文档示例把 '#/' 加回来。
|
||||
$encode = !empty($this->conf['h5_sign_urlencode']);
|
||||
$pairs = [];
|
||||
foreach ($params as $k => $v) {
|
||||
$pairs[] = $k . '=' . ($encode ? rawurlencode((string) $v) : (string) $v);
|
||||
}
|
||||
return $base . '/?' . implode('&', $pairs);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | 企油通 · 司机档案
|
||||
// +----------------------------------------------------------------------
|
||||
// | 按身份证号唯一。上游 7.12 / 7.13 / 7.17 都以「手机号 + 身份证号」定位司机,
|
||||
// | 7.7 下发前会做实名二要素核验,所以本地提前把会导致核验失败的数据标出来:
|
||||
// | · 同一身份证号出现不同姓名
|
||||
// | · 同一手机号对应多个身份证号
|
||||
// | · 同一身份证号换了手机号
|
||||
// | 累计发放 / 退款 / 订单数不存冗余,列表查询时从订单实时汇总,避免对不上账。
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types=1);
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use think\facade\Db;
|
||||
|
||||
class QytDriver
|
||||
{
|
||||
/**
|
||||
* 下单成功后更新司机档案。任何异常都吞掉 —— 档案是辅助数据,不能影响建单
|
||||
*/
|
||||
public static function touch(array $order): void
|
||||
{
|
||||
$idCard = strtoupper(trim((string) ($order['id_card'] ?? '')));
|
||||
if ($idCard === '') {
|
||||
return;
|
||||
}
|
||||
$name = trim((string) ($order['driver_name'] ?? ''));
|
||||
$mobile = trim((string) ($order['mobile'] ?? ''));
|
||||
$storeId = (int) ($order['store_id'] ?? 0);
|
||||
$now = time();
|
||||
|
||||
try {
|
||||
$row = Db::name('qyt_driver')->where('store_id', $storeId)->where('id_card', $idCard)->find();
|
||||
if (!$row) {
|
||||
try {
|
||||
Db::name('qyt_driver')->insert([
|
||||
'id_card' => $idCard, 'name' => $name, 'mobile' => $mobile,
|
||||
'last_order_time' => $now, 'store_id' => $storeId, 'create_time' => $now, 'update_time' => $now,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
if (strpos($e->getMessage(), 'Duplicate') === false) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
$row = Db::name('qyt_driver')->where('store_id', $storeId)->where('id_card', $idCard)->find();
|
||||
}
|
||||
|
||||
$notes = self::detectConflicts($row, $name, $mobile);
|
||||
|
||||
// 同一手机号挂在别的身份证号上:两边都标记
|
||||
if ($mobile !== '') {
|
||||
$others = Db::name('qyt_driver')
|
||||
->where('store_id', $storeId)->where('mobile', $mobile)->where('id_card', '<>', $idCard)
|
||||
->column('driver_id');
|
||||
if ($others) {
|
||||
$notes[] = '同一手机号对应多个身份证号';
|
||||
Db::name('qyt_driver')->whereIn('driver_id', $others)
|
||||
->update(['conflict' => 1, 'conflict_text' => '同一手机号对应多个身份证号', 'update_time' => $now]);
|
||||
}
|
||||
}
|
||||
|
||||
$update = [
|
||||
'name' => $name !== '' ? $name : $row['name'],
|
||||
'mobile' => $mobile !== '' ? $mobile : $row['mobile'],
|
||||
'last_order_time' => $now,
|
||||
'update_time' => $now,
|
||||
];
|
||||
if ($notes) {
|
||||
$update['conflict'] = 1;
|
||||
$update['conflict_text'] = mb_substr(implode(';', array_unique($notes)), 0, 250);
|
||||
}
|
||||
Db::name('qyt_driver')->where('driver_id', $row['driver_id'])->update($update);
|
||||
} catch (\Throwable $e) {
|
||||
(new QytOil())->saveLog('司机档案更新失败 order=' . ($order['order_id'] ?? '') . ':' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 纯函数:对比已有档案与本次下单的姓名 / 手机号
|
||||
*/
|
||||
public static function detectConflicts(array $row, string $name, string $mobile): array
|
||||
{
|
||||
$notes = [];
|
||||
$oldName = (string) ($row['name'] ?? '');
|
||||
$oldMobile = (string) ($row['mobile'] ?? '');
|
||||
if ($oldName !== '' && $name !== '' && $oldName !== $name) {
|
||||
$notes[] = "同一身份证号出现不同姓名:{$oldName} / {$name}";
|
||||
}
|
||||
if ($oldMobile !== '' && $mobile !== '' && $oldMobile !== $mobile) {
|
||||
$notes[] = '手机号变更:' . QytOil::maskMobile($oldMobile) . ' → ' . QytOil::maskMobile($mobile);
|
||||
}
|
||||
return $notes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | 企油通 · 企业发票(7.14)
|
||||
// +----------------------------------------------------------------------
|
||||
// | 7.14 的粒度是「企业 + 通道 + 月份(+ 司机手机号)」,返回的 invoice 只是一组地址,
|
||||
// | 和具体订单之间【没有】对应关系。所以:
|
||||
// | · 表按这个维度存(yoshop_qyt_invoice)
|
||||
// | · 订单按「订单企业 + 消费月份 + 司机手机号」去关联 —— 同一司机当月多笔订单看到的是同一批发票
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types=1);
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\model\qyt\Enterprise as EnterpriseModel;
|
||||
use think\facade\Db;
|
||||
|
||||
class QytInvoice
|
||||
{
|
||||
/** 已同步但暂无发票的记录,超过多久再次点开时自动重新同步(秒) */
|
||||
const RESYNC_EMPTY_AFTER = 3600;
|
||||
|
||||
/**
|
||||
* 调 7.14 同步某个维度的发票并入库
|
||||
* @param EnterpriseModel|array $enterprise
|
||||
* @param string $month YYYY-MM
|
||||
* @param string $mobile 司机手机号,空串=企业全量
|
||||
* @return array ['ok'=>bool, 'message'=>string, 'row'=>array]
|
||||
*/
|
||||
public static function sync($enterprise, string $month, string $mobile = ''): array
|
||||
{
|
||||
if (!preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $month)) {
|
||||
return ['ok' => false, 'message' => '月份格式应为 YYYY-MM', 'row' => []];
|
||||
}
|
||||
if ($mobile !== '' && !preg_match('/^1[3-9]\d{9}$/', $mobile)) {
|
||||
return ['ok' => false, 'message' => '司机手机号格式不正确', 'row' => []];
|
||||
}
|
||||
$ent = $enterprise instanceof EnterpriseModel ? $enterprise->toArray() : (array) $enterprise;
|
||||
if (empty($ent['enterprise_id'])) {
|
||||
return ['ok' => false, 'message' => '企业不存在', 'row' => []];
|
||||
}
|
||||
|
||||
$res = QytOil::forEnterprise($ent)->invoice($month, $mobile);
|
||||
$key = [
|
||||
'qyt_enterprise_id' => (int) $ent['enterprise_id'],
|
||||
'passageway' => (string) $ent['passageway'],
|
||||
'month' => $month,
|
||||
'mobile' => $mobile,
|
||||
];
|
||||
$now = time();
|
||||
|
||||
if ($res['code'] !== '00') {
|
||||
$message = '同步失败:' . ($res['message'] ?: '未知错误');
|
||||
self::upsert($key, [
|
||||
'business_license' => (string) $ent['business_license'],
|
||||
'sync_status' => 2,
|
||||
'sync_time' => $now,
|
||||
'sync_text' => mb_substr($message, 0, 250),
|
||||
'store_id' => (int) ($ent['store_id'] ?? 0),
|
||||
]);
|
||||
return ['ok' => false, 'message' => $message, 'row' => self::format(self::find($key))];
|
||||
}
|
||||
|
||||
$urls = self::normalizeUrls($res['data']['invoice'] ?? []);
|
||||
$orders = (array) ($res['data']['order_list'] ?? []);
|
||||
$text = $urls ? '同步成功,共 ' . count($urls) . ' 张发票' : '同步成功,企油通暂未出具该月发票';
|
||||
self::upsert($key, [
|
||||
'business_license' => (string) $ent['business_license'],
|
||||
'invoice_urls' => json_encode($urls, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'invoice_count' => count($urls),
|
||||
'order_count' => count($orders),
|
||||
'raw' => mb_substr((string) json_encode(QytOil::maskDeep((array) $res['data']), JSON_UNESCAPED_UNICODE), 0, 1000000),
|
||||
'sync_status' => 1,
|
||||
'sync_time' => $now,
|
||||
'sync_text' => $text,
|
||||
'store_id' => (int) ($ent['store_id'] ?? 0),
|
||||
]);
|
||||
return ['ok' => true, 'message' => $text, 'row' => self::format(self::find($key))];
|
||||
}
|
||||
|
||||
/**
|
||||
* 取某个订单关联的发票。本地没有、上次同步失败、或「暂无发票」且已超过 1 小时,会自动去同步一次
|
||||
*/
|
||||
public static function forOrder(array $order, bool $refresh = false): array
|
||||
{
|
||||
$enterprise = EnterpriseModel::detail((int) $order['qyt_enterprise_id']);
|
||||
if (!$enterprise) {
|
||||
return ['ok' => false, 'message' => '本单绑定的企油通企业不存在', 'month' => '', 'row' => []];
|
||||
}
|
||||
$month = self::orderMonth($order);
|
||||
$mobile = (string) $order['mobile'];
|
||||
$key = [
|
||||
'qyt_enterprise_id' => (int) $enterprise['enterprise_id'],
|
||||
'passageway' => (string) $enterprise['passageway'],
|
||||
'month' => $month,
|
||||
'mobile' => $mobile,
|
||||
];
|
||||
$row = self::find($key);
|
||||
$stale = !$row
|
||||
|| (int) $row['sync_status'] !== 1
|
||||
|| ((int) $row['invoice_count'] === 0 && time() - (int) $row['sync_time'] > self::RESYNC_EMPTY_AFTER);
|
||||
|
||||
$message = $row ? (string) $row['sync_text'] : '';
|
||||
if ($refresh || $stale) {
|
||||
$r = self::sync($enterprise, $month, $mobile);
|
||||
$message = $r['message'];
|
||||
$row = self::find($key);
|
||||
if (!$r['ok'] && !$row) {
|
||||
return ['ok' => false, 'message' => $message, 'month' => $month, 'row' => []];
|
||||
}
|
||||
}
|
||||
return ['ok' => true, 'message' => $message, 'month' => $month, 'row' => self::format($row)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单的消费月份:优先取加油流水的支付时间,没有再依次退到消费时间 / 发放时间 / 下单时间
|
||||
*/
|
||||
public static function orderMonth(array $order): string
|
||||
{
|
||||
$t = (int) Db::name('qyt_consume')->where('order_id', $order['order_id'])->max('pay_time');
|
||||
foreach (['consume_time', 'oil_time', 'create_time'] as $field) {
|
||||
if ($t > 0) {
|
||||
break;
|
||||
}
|
||||
$t = is_numeric($order[$field] ?? null) ? (int) $order[$field] : (int) strtotime((string) ($order[$field] ?? ''));
|
||||
}
|
||||
return date('Y-m', $t > 0 ? $t : time());
|
||||
}
|
||||
|
||||
/**
|
||||
* 7.14 的 invoice 文档只写了「发票地址集」,没给元素结构。
|
||||
* 兼容两种形态:直接是 URL 字符串;或对象(取常见的 url 字段,取不到就找第一个 http 开头的值)
|
||||
*/
|
||||
public static function normalizeUrls($invoice): array
|
||||
{
|
||||
$out = [];
|
||||
foreach ((array) $invoice as $item) {
|
||||
$url = '';
|
||||
if (is_string($item)) {
|
||||
$url = trim($item);
|
||||
} elseif (is_array($item)) {
|
||||
foreach (['url', 'invoice_url', 'file_url', 'pdf_url', 'download_url', 'path'] as $k) {
|
||||
if (!empty($item[$k]) && is_string($item[$k])) {
|
||||
$url = trim($item[$k]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($url === '') {
|
||||
foreach ($item as $v) {
|
||||
if (is_string($v) && preg_match('#^https?://#i', trim($v))) {
|
||||
$url = trim($v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($url !== '' && preg_match('#^https?://#i', $url)) {
|
||||
$out[] = $url;
|
||||
}
|
||||
}
|
||||
return array_values(array_unique($out));
|
||||
}
|
||||
|
||||
/** 列表 / 详情输出格式 */
|
||||
public static function format($row): array
|
||||
{
|
||||
if (!$row) {
|
||||
return [];
|
||||
}
|
||||
$row = (array) $row;
|
||||
unset($row['raw']);
|
||||
$urls = json_decode((string) ($row['invoice_urls'] ?? ''), true);
|
||||
$row['invoice_urls'] = is_array($urls) ? $urls : [];
|
||||
$row['mobile_mask'] = $row['mobile'] !== '' ? QytOil::maskMobile((string) $row['mobile']) : '企业全量';
|
||||
$row['sync_time_text'] = !empty($row['sync_time']) ? date('Y-m-d H:i:s', (int) $row['sync_time']) : '';
|
||||
return $row;
|
||||
}
|
||||
|
||||
protected static function find(array $key): ?array
|
||||
{
|
||||
$row = Db::name('qyt_invoice')->where($key)->find();
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
protected static function upsert(array $key, array $data): void
|
||||
{
|
||||
$data['update_time'] = time();
|
||||
if (Db::name('qyt_invoice')->where($key)->update($data)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Db::name('qyt_invoice')->insert($key + $data + ['create_time' => time()]);
|
||||
} catch (\Throwable $e) {
|
||||
// 并发下另一请求刚插入:唯一键冲突,改为更新
|
||||
if (strpos($e->getMessage(), 'Duplicate') === false) {
|
||||
throw $e;
|
||||
}
|
||||
Db::name('qyt_invoice')->where($key)->update($data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,13 @@ class QytOil extends BaseService
|
||||
/** @var EnterpriseModel|array|null 当前企业 */
|
||||
protected $enterprise = null;
|
||||
|
||||
/**
|
||||
* @var bool 最近一次请求是否属于「网络层异常」(超时、连不上、响应不是 JSON)。
|
||||
* 这类失败不能说明上游没处理——对 7.16 这种没有幂等键的写接口,必须和
|
||||
* 「上游明确返回失败」区分开,前者转人工确认,后者才可以放心重试。
|
||||
*/
|
||||
protected $transportError = false;
|
||||
|
||||
protected function initialize()
|
||||
{
|
||||
$this->conf = QytConfig::get();
|
||||
@@ -204,6 +211,132 @@ class QytOil extends BaseService
|
||||
return null;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 7.17 获取支付凭证(须真实 RSA 验签)
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* 查询某司机在当前企业下的支付凭证,退款(7.16)要用其中的 quota_id
|
||||
* @return array data.list[]: quota_id / total_amount / available_amount / used_amount / status ...(金额单位:元)
|
||||
*/
|
||||
public function getVoucherList(string $mobile, string $idCard, int $page = 1, int $pageSize = 50): array
|
||||
{
|
||||
if (($err = $this->requireEnterprise()) !== null) {
|
||||
return $err;
|
||||
}
|
||||
return $this->post('/enterprise/api/quota/getVoucherList', [
|
||||
'passageway' => (string) $this->channelCode(),
|
||||
'business_license' => $this->businessLicense(),
|
||||
'mobile' => $mobile,
|
||||
'id_card' => $idCard,
|
||||
'page' => $page,
|
||||
'page_size' => $pageSize,
|
||||
]);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 7.16 司机额度调整(须真实 RSA 验签;⚠️ 无幂等键,重复调用会重复调额)
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* @param int|string $quotaId 7.17 返回的凭证主键
|
||||
* @param string $changeType 02 增加额度 / 03 减少额度
|
||||
* @param float $amount 调整金额(元)
|
||||
* @param string $reason 操作原因(≤200 字);退款时写入退款单号,确认结果时靠它在 7.13 流水里定位
|
||||
* @param string $waybillNo 运单号(可选)
|
||||
*/
|
||||
public function adjustVoucher($quotaId, string $changeType, float $amount, string $reason = '', string $waybillNo = ''): array
|
||||
{
|
||||
if (($err = $this->requireEnterprise()) !== null) {
|
||||
return $err;
|
||||
}
|
||||
$params = [
|
||||
'passageway' => (string) $this->channelCode(),
|
||||
'business_license' => $this->businessLicense(),
|
||||
'quota_id' => (int) $quotaId,
|
||||
'change_type' => $changeType,
|
||||
'amount' => round($amount, 2),
|
||||
'operation_reason' => mb_substr($reason, 0, 200),
|
||||
];
|
||||
$waybillNo !== '' && $params['waybill_no'] = $waybillNo;
|
||||
return $this->post('/enterprise/api/quota/adjustVoucher', $params);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 7.11 企业账户余额(仅平安银行通道支持,金额:分)
|
||||
// ================================================================
|
||||
|
||||
public function distributable(): array
|
||||
{
|
||||
if (($err = $this->requireEnterprise()) !== null) {
|
||||
return $err;
|
||||
}
|
||||
if ($this->channelCode() !== 2) {
|
||||
return $this->fail('企业账户余额查询仅支持平安银行通道,支付宝企业码没有独立资金账户');
|
||||
}
|
||||
return $this->post('/enterprise/api/zhyk/distributable', [
|
||||
'business_license' => $this->businessLicense(),
|
||||
'passageway' => (string) $this->channelCode(),
|
||||
]);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 7.12 司机钱包金额 / 7.13 司机钱包明细(金额:分)
|
||||
// ================================================================
|
||||
|
||||
public function driverBalanceQuery(string $mobile, string $idCard, int $page = 1, int $pageSize = 20): array
|
||||
{
|
||||
if (($err = $this->requireEnterprise()) !== null) {
|
||||
return $err;
|
||||
}
|
||||
return $this->post('/enterprise/api/zhyk/driverBalanceQuery', [
|
||||
'business_license' => $this->businessLicense(),
|
||||
'passageway' => (string) $this->channelCode(),
|
||||
'mobile' => $mobile,
|
||||
'id_card' => $idCard,
|
||||
'page' => $page,
|
||||
'page_size' => $pageSize,
|
||||
]);
|
||||
}
|
||||
|
||||
public function driverWalletQuery(string $mobile, string $idCard, int $page = 1, int $pageSize = 20): array
|
||||
{
|
||||
if (($err = $this->requireEnterprise()) !== null) {
|
||||
return $err;
|
||||
}
|
||||
return $this->post('/enterprise/api/zhyk/driverWalletQuery', [
|
||||
'business_license' => $this->businessLicense(),
|
||||
'passageway' => (string) $this->channelCode(),
|
||||
'mobile' => $mobile,
|
||||
'id_card' => $idCard,
|
||||
'page' => $page,
|
||||
'page_size' => $pageSize,
|
||||
]);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 7.14 企业发票下载(粒度:企业 + 通道 + 月份,可选按司机手机号收窄)
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* @param string $month YYYY-MM
|
||||
* @param string $mobile 司机手机号;传了只返回该司机相关发票与加油明细
|
||||
* @return array data: business_license / invoice[] 发票地址集 / order_list[] 加油明细(分)
|
||||
*/
|
||||
public function invoice(string $month, string $mobile = ''): array
|
||||
{
|
||||
if (($err = $this->requireEnterprise()) !== null) {
|
||||
return $err;
|
||||
}
|
||||
$params = [
|
||||
'business_license' => $this->businessLicense(),
|
||||
'passageway' => (string) $this->channelCode(),
|
||||
'month' => $month,
|
||||
];
|
||||
$mobile !== '' && $params['mobile'] = $mobile;
|
||||
return $this->post('/enterprise/api/zhyk/invoice', $params);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 7.2 企业申请创建
|
||||
// ================================================================
|
||||
@@ -295,12 +428,15 @@ class QytOil extends BaseService
|
||||
['企业' => $this->enterpriseName(), 'url' => $url, 'params' => self::maskDeep($bizParams)],
|
||||
JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$this->transportError = false;
|
||||
$resp = $this->curlPost($url, json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
$this->saveLog("[响应] {$path} " . self::maskText($resp));
|
||||
|
||||
$json = json_decode($resp, true);
|
||||
if (!is_array($json)) {
|
||||
return ['code' => 'ERR', 'data' => [], 'message' => '响应解析失败', 'dup_batch' => false, 'raw' => ['body' => $resp]];
|
||||
// 拿不到可解析的响应,无法判断上游是否已处理
|
||||
return ['code' => 'ERR', 'data' => [], 'message' => '响应解析失败', 'dup_batch' => false,
|
||||
'transport_error' => true, 'raw' => ['body' => mb_substr((string) $resp, 0, 500)]];
|
||||
}
|
||||
|
||||
// 通用响应结构:{ status: ok|error, data, message, serialNo, sign }
|
||||
@@ -323,6 +459,8 @@ class QytOil extends BaseService
|
||||
'data' => $json['data'] ?? [],
|
||||
'message' => $message,
|
||||
'dup_batch' => $dupBatch,
|
||||
// cURL 失败时 curlPost 会伪造一个 {"status":"error"} 的 JSON,靠标记区分
|
||||
'transport_error' => $this->transportError,
|
||||
'raw' => $json,
|
||||
];
|
||||
}
|
||||
@@ -344,9 +482,15 @@ class QytOil extends BaseService
|
||||
if ($resp === false) {
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
$this->transportError = true;
|
||||
return json_encode(['status' => 'error', 'message' => 'cURL: ' . $err], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
// 网关层 5xx:请求可能已到达上游业务层,也按结果未知处理
|
||||
if ($httpCode >= 500) {
|
||||
$this->transportError = true;
|
||||
}
|
||||
return (string) $resp;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,524 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | 企油通 · 订单退款(7.17 获取支付凭证 + 7.16 司机额度调整)
|
||||
// +----------------------------------------------------------------------
|
||||
// | 规则(已与业务确认):退「剩余未加油金额」,每单只退一次
|
||||
// | · 订单必须「发放成功」;一单可以分多次加油(7.10 逐笔累计到 used_amount)
|
||||
// | · 退款金额 = 订单金额 − 已加油金额,固定不可指定;为 0 时不能退
|
||||
// | · 额度在上游是按司机累计到凭证上的,退款只能「把凭证调低 X 元」。
|
||||
// | 还要求凭证可用余额 ≥ 退款金额(额度可能被该司机其他订单用掉),否则直接拒绝。
|
||||
// |
|
||||
// | 状态(yoshop_partners_order.refund_status):
|
||||
// | 0 未退款 -> 1 退款中 -> 2 已退款
|
||||
// | -> 3 退款失败(上游明确拒绝 / 凭证不足,可重新发起)
|
||||
// | -> 4 待人工确认(7.16 已发出但结果未知)
|
||||
// |
|
||||
// | ⚠️ 7.16 没有幂等键,同一请求发两次就调低两次。所以:
|
||||
// | · 一旦走到 7.16,任何「不确定」都进 4,绝不自动重试
|
||||
// | · 4 只能通过 confirm()(查上游证据)或 resolve()(人工判定)离开
|
||||
// | · 退款单号写进 7.16 的 operation_reason,confirm() 优先在 7.13 流水里找它
|
||||
// |
|
||||
// | 并发:RedisLock 串行同一订单;状态跃迁是条件 UPDATE,affected=1 才退合作商余额。
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types=1);
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\lock\RedisLock;
|
||||
use app\common\model\qyt\Enterprise as EnterpriseModel;
|
||||
use think\facade\Db;
|
||||
|
||||
class QytRefund
|
||||
{
|
||||
const NONE = 0;
|
||||
const PROCESSING = 1;
|
||||
const SUCCESS = 2;
|
||||
const FAILED = 3;
|
||||
const CONFIRM = 4;
|
||||
|
||||
const STATUS_TEXT = [
|
||||
self::NONE => '未退款',
|
||||
self::PROCESSING => '退款中',
|
||||
self::SUCCESS => '已退款',
|
||||
self::FAILED => '退款失败',
|
||||
self::CONFIRM => '待人工确认',
|
||||
];
|
||||
|
||||
// 失败分类,开放接口据此映射错误码
|
||||
const REASON_NOT_FOUND = 'not_found';
|
||||
const REASON_STATE = 'state';
|
||||
const REASON_PROCESSING = 'processing';
|
||||
const REASON_UPSTREAM = 'upstream';
|
||||
|
||||
/** 元金额比较容差 */
|
||||
const EPS = 0.005;
|
||||
|
||||
// ================================================================
|
||||
// 发起退款
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* @param string $source admin / api
|
||||
* @param string $operator 后台账号 或 合作商 appKey
|
||||
*/
|
||||
public static function apply(int $orderId, string $source, string $operator, string $reason = ''): array
|
||||
{
|
||||
$lock = new RedisLock('qyt:refund:' . $orderId, 120);
|
||||
if (!$lock->acquireLock()) {
|
||||
return self::result(false, self::PROCESSING, '该订单退款正在处理中,请勿重复提交', self::REASON_PROCESSING);
|
||||
}
|
||||
|
||||
$order = null;
|
||||
$refundId = 0;
|
||||
$refundNo = '';
|
||||
// before_adjust:还没调 7.16,出任何问题都可以安全地判失败
|
||||
// after_adjust :7.16 已发出,出问题只能进人工确认
|
||||
$stage = 'before_adjust';
|
||||
|
||||
try {
|
||||
$order = Db::name('partners_order')->where('order_id', $orderId)->find();
|
||||
if (!$order) {
|
||||
return self::result(false, self::NONE, '订单不存在', self::REASON_NOT_FOUND);
|
||||
}
|
||||
|
||||
$current = (int) $order['refund_status'];
|
||||
if ($current === self::SUCCESS) {
|
||||
// 幂等:已退过直接返回成功
|
||||
return self::result(true, self::SUCCESS, '该订单已退款', '', self::orderData($order));
|
||||
}
|
||||
if ($current === self::PROCESSING || $current === self::CONFIRM) {
|
||||
return self::result(false, $current,
|
||||
$current === self::CONFIRM ? '该订单退款结果待人工确认,请勿重复发起' : '该订单退款正在处理中',
|
||||
self::REASON_PROCESSING, self::orderData($order));
|
||||
}
|
||||
if (($err = self::checkRefundable($order)) !== '') {
|
||||
return self::result(false, $current, $err, self::REASON_STATE, self::orderData($order));
|
||||
}
|
||||
$enterprise = EnterpriseModel::detail((int) $order['qyt_enterprise_id']);
|
||||
if (!$enterprise) {
|
||||
return self::result(false, $current, '本单绑定的企油通企业不存在,无法退款', self::REASON_STATE, self::orderData($order));
|
||||
}
|
||||
|
||||
// 退款金额 = 剩余未加油金额(锁内重读订单后计算)
|
||||
$amount = self::remainAmount($order);
|
||||
|
||||
// 抢占:只有 0/3 能进入退款中
|
||||
$affected = Db::name('partners_order')
|
||||
->where('order_id', $orderId)
|
||||
->whereIn('refund_status', [self::NONE, self::FAILED])
|
||||
->update(['refund_status' => self::PROCESSING, 'refund_text' => '退款中', 'update_time' => time()]);
|
||||
if (!$affected) {
|
||||
return self::result(false, $current, '订单状态已变化,请刷新后重试', self::REASON_PROCESSING, self::orderData($order));
|
||||
}
|
||||
|
||||
$refundNo = 'RF' . $orderId . 'T' . date('YmdHis') . mt_rand(10, 99);
|
||||
$refundId = (int) Db::name('qyt_refund')->insertGetId([
|
||||
'refund_no' => $refundNo,
|
||||
'order_id' => $orderId,
|
||||
'partners_id' => (int) $order['partners_id'],
|
||||
'qyt_enterprise_id' => (int) $order['qyt_enterprise_id'],
|
||||
'source' => $source,
|
||||
'operator' => mb_substr($operator, 0, 64),
|
||||
'reason' => mb_substr($reason, 0, 200),
|
||||
'amount' => $amount,
|
||||
'status' => self::PROCESSING,
|
||||
'store_id' => (int) $order['store_id'],
|
||||
'create_time' => time(),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
|
||||
$qyt = QytOil::forEnterprise($enterprise);
|
||||
|
||||
// 1) 7.17 找凭证(只读接口,失败不影响上游,可以放心判失败)
|
||||
$vres = $qyt->getVoucherList((string) $order['mobile'], (string) $order['id_card'], 1, 50);
|
||||
if ($vres['code'] !== '00') {
|
||||
return self::finishFailed($order, $refundId, $refundNo,
|
||||
'查询支付凭证失败:' . ($vres['message'] ?: '未知错误'), $vres);
|
||||
}
|
||||
$pick = self::pickVoucher((array) ($vres['data']['list'] ?? []), $amount);
|
||||
if (!$pick['voucher']) {
|
||||
return self::finishFailed($order, $refundId, $refundNo, $pick['message'], $vres);
|
||||
}
|
||||
$voucher = $pick['voucher'];
|
||||
Db::name('qyt_refund')->where('refund_id', $refundId)->update([
|
||||
'quota_id' => (string) $voucher['quota_id'],
|
||||
'total_before' => round((float) ($voucher['total_amount'] ?? 0), 2),
|
||||
'available_before' => round((float) ($voucher['available_amount'] ?? 0), 2),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
|
||||
// 2) 7.16 调低额度
|
||||
$stage = 'after_adjust';
|
||||
$ares = $qyt->adjustVoucher(
|
||||
$voucher['quota_id'],
|
||||
'03',
|
||||
$amount,
|
||||
'订单退款 ' . $refundNo . ($reason !== '' ? ':' . $reason : ''),
|
||||
(string) $order['waybill_no']
|
||||
);
|
||||
|
||||
if (!empty($ares['transport_error'])) {
|
||||
return self::finishConfirm($order, $refundId, $refundNo,
|
||||
'退款请求已发出但结果未知(网络超时或响应异常),已转人工确认,切勿重复发起', $ares);
|
||||
}
|
||||
$success = $ares['code'] === '00' && !(isset($ares['data']['success']) && $ares['data']['success'] === false);
|
||||
if (!$success) {
|
||||
$msg = (string) ($ares['data']['message'] ?? '') ?: $ares['message'];
|
||||
return self::finishFailed($order, $refundId, $refundNo, '企油通调额失败:' . ($msg ?: '未知错误'), $ares);
|
||||
}
|
||||
return self::finishSuccess($order, $refundId, $refundNo, $amount, '退款成功', $ares);
|
||||
} catch (\Throwable $e) {
|
||||
(new QytOil())->saveLog("退款异常 order={$orderId} stage={$stage}:" . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
|
||||
if ($order && $refundId) {
|
||||
return $stage === 'after_adjust'
|
||||
? self::finishConfirm($order, $refundId, $refundNo, '退款过程发生异常且 7.16 可能已生效,已转人工确认:' . $e->getMessage(), [])
|
||||
: self::finishFailed($order, $refundId, $refundNo, '退款过程发生异常:' . $e->getMessage(), []);
|
||||
}
|
||||
return self::result(false, self::NONE, '退款处理异常:' . $e->getMessage(), self::REASON_STATE);
|
||||
} finally {
|
||||
$lock->releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 确认「待人工确认」的退款:向上游取证
|
||||
// ================================================================
|
||||
|
||||
public static function confirm(int $orderId): array
|
||||
{
|
||||
$lock = new RedisLock('qyt:refund:' . $orderId, 120);
|
||||
if (!$lock->acquireLock()) {
|
||||
return self::result(false, self::CONFIRM, '该订单正在处理中,请稍后再试', self::REASON_PROCESSING);
|
||||
}
|
||||
try {
|
||||
[$order, $refund, $enterprise, $err] = self::loadConfirmContext($orderId);
|
||||
if ($err !== '') {
|
||||
return self::result(false, (int) ($order['refund_status'] ?? self::NONE), $err, self::REASON_STATE);
|
||||
}
|
||||
$qyt = QytOil::forEnterprise($enterprise);
|
||||
$amount = round((float) $refund['amount'], 2);
|
||||
|
||||
// 证据 1:7.13 钱包流水里带有退款单号的调额记录 —— 最可靠
|
||||
$inWallet = false;
|
||||
$wres = $qyt->driverWalletQuery((string) $order['mobile'], (string) $order['id_card'], 1, 50);
|
||||
if ($wres['code'] === '00') {
|
||||
foreach ((array) ($wres['data']['transactionDetails'] ?? []) as $t) {
|
||||
if (strpos((string) ($t['remark'] ?? ''), (string) $refund['refund_no']) !== false) {
|
||||
$inWallet = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 证据 2:7.17 同一张凭证的总额度 / 可用余额变化
|
||||
$vres = $qyt->getVoucherList((string) $order['mobile'], (string) $order['id_card'], 1, 50);
|
||||
$now = null;
|
||||
if ($vres['code'] === '00') {
|
||||
foreach ((array) ($vres['data']['list'] ?? []) as $v) {
|
||||
if ((string) ($v['quota_id'] ?? '') === (string) $refund['quota_id']) {
|
||||
$now = $v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$inWallet && $vres['code'] !== '00') {
|
||||
return self::result(false, self::CONFIRM, '暂时无法向企油通取证:' . ($vres['message'] ?: '查询失败') . ',请稍后再试', self::REASON_UPSTREAM);
|
||||
}
|
||||
|
||||
$totalNow = $now ? round((float) ($now['total_amount'] ?? 0), 2) : null;
|
||||
$availNow = $now ? round((float) ($now['available_amount'] ?? 0), 2) : null;
|
||||
if ($now) {
|
||||
Db::name('qyt_refund')->where('refund_id', $refund['refund_id'])->update([
|
||||
'total_after' => $totalNow, 'available_after' => $availNow, 'update_time' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
// 退款发起前后 10 分钟起,该司机在本企业是否有其他单在发放 / 发放成功(会干扰额度比对)
|
||||
$grantedSince = Db::name('partners_order')
|
||||
->where('id_card', $order['id_card'])
|
||||
->where('qyt_enterprise_id', $order['qyt_enterprise_id'])
|
||||
->where('order_id', '<>', $order['order_id'])
|
||||
->whereIn('oil_status_zsy', [1, 2])
|
||||
->where('update_time', '>=', (int) $refund['create_time'] - 600)
|
||||
->count() > 0;
|
||||
|
||||
$verdict = self::judgeConfirm(
|
||||
$inWallet,
|
||||
(float) $refund['total_before'], $totalNow,
|
||||
(float) $refund['available_before'], $availNow,
|
||||
$amount,
|
||||
$grantedSince
|
||||
);
|
||||
|
||||
if ($verdict === 'applied') {
|
||||
return self::finishSuccess($order, (int) $refund['refund_id'], (string) $refund['refund_no'], $amount,
|
||||
'人工确认:企油通已调低额度,退款成功', $vres);
|
||||
}
|
||||
if ($verdict === 'not_applied') {
|
||||
return self::finishFailed($order, (int) $refund['refund_id'], (string) $refund['refund_no'],
|
||||
'确认结果:企油通未调低额度,退款未生效,可重新发起', $vres);
|
||||
}
|
||||
return self::result(false, self::CONFIRM,
|
||||
'上游数据无法自动判定(期间可能有新的发放或加油消费),请到企油通后台核对该司机凭证后使用「人工判定」',
|
||||
self::REASON_UPSTREAM, self::orderData($order, (string) $refund['refund_no']));
|
||||
} catch (\Throwable $e) {
|
||||
return self::result(false, self::CONFIRM, '确认过程异常:' . $e->getMessage(), self::REASON_STATE);
|
||||
} finally {
|
||||
$lock->releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 人工判定「待人工确认」的退款(运营到企油通后台核对后操作)
|
||||
*/
|
||||
public static function resolve(int $orderId, bool $success, string $operator, string $remark): array
|
||||
{
|
||||
if (trim($remark) === '') {
|
||||
return self::result(false, self::CONFIRM, '请填写判定依据', self::REASON_STATE);
|
||||
}
|
||||
$lock = new RedisLock('qyt:refund:' . $orderId, 120);
|
||||
if (!$lock->acquireLock()) {
|
||||
return self::result(false, self::CONFIRM, '该订单正在处理中,请稍后再试', self::REASON_PROCESSING);
|
||||
}
|
||||
try {
|
||||
[$order, $refund, , $err] = self::loadConfirmContext($orderId);
|
||||
if ($err !== '') {
|
||||
return self::result(false, (int) ($order['refund_status'] ?? self::NONE), $err, self::REASON_STATE);
|
||||
}
|
||||
$text = "人工判定({$operator}):" . mb_substr($remark, 0, 100);
|
||||
return $success
|
||||
? self::finishSuccess($order, (int) $refund['refund_id'], (string) $refund['refund_no'], (float) $refund['amount'], $text, [])
|
||||
: self::finishFailed($order, (int) $refund['refund_id'], (string) $refund['refund_no'], $text, []);
|
||||
} finally {
|
||||
$lock->releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 纯函数(无副作用,便于单测)
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* 退款前置条件,返回空串表示可以退
|
||||
*/
|
||||
public static function checkRefundable(array $order): string
|
||||
{
|
||||
if ((int) $order['oil_status_zsy'] !== 2 || (int) $order['oil_status'] !== 1) {
|
||||
return '仅「发放成功」的订单可以退款';
|
||||
}
|
||||
if (self::remainAmount($order) <= self::EPS) {
|
||||
return '订单金额已全部加油,没有可退金额';
|
||||
}
|
||||
if (trim((string) $order['id_card']) === '') {
|
||||
return '订单缺少司机身份证号,无法查询支付凭证';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 剩余未加油金额(元)= 订单金额 − 已加油金额,不小于 0。这就是可退金额
|
||||
*/
|
||||
public static function remainAmount(array $order): float
|
||||
{
|
||||
return max(0.0, round((float) ($order['total_price'] ?? 0) - (float) ($order['used_amount'] ?? 0), 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 7.17 凭证列表里挑一张用于调低额度的凭证
|
||||
* 规则:只看生效中的凭证,取可用余额最大的一张;它不够就拒绝(整单全额退,不拆多张凭证)
|
||||
* @return array ['voucher' => array|null, 'message' => string]
|
||||
*/
|
||||
public static function pickVoucher(array $list, float $amount): array
|
||||
{
|
||||
$active = array_values(array_filter($list, function ($v) {
|
||||
return is_array($v) && (string) ($v['status'] ?? '') === 'active';
|
||||
}));
|
||||
if (!$active) {
|
||||
return ['voucher' => null, 'message' => '该司机在本企业下没有生效中的支付凭证,无法退款'];
|
||||
}
|
||||
usort($active, function ($a, $b) {
|
||||
return (float) ($b['available_amount'] ?? 0) <=> (float) ($a['available_amount'] ?? 0);
|
||||
});
|
||||
$top = $active[0];
|
||||
$avail = round((float) ($top['available_amount'] ?? 0), 2);
|
||||
if ($avail + self::EPS < $amount) {
|
||||
return ['voucher' => null, 'message' => sprintf(
|
||||
'司机凭证可用余额不足:可用 %.2f 元,需退 %.2f 元(凭证额度按司机累计,可能已被该司机其他订单加油用掉)', $avail, $amount)];
|
||||
}
|
||||
return ['voucher' => $top, 'message' => ''];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据上游证据判定 7.16 是否已生效
|
||||
* · 7.13 流水里找到退款单号 -> applied
|
||||
* · 凭证总额度恰好减少了退款金额 -> applied
|
||||
* (加油消费只动可用/已用,不动总额度;调额才动总额度)
|
||||
* · 总额度没变 且 可用余额没有下降 -> not_applied
|
||||
* · 其余(凭证找不到、期间有新发放或消费导致对不上) -> unknown,交人工
|
||||
* @return string applied / not_applied / unknown
|
||||
*/
|
||||
public static function judgeConfirm(bool $inWallet, float $totalBefore, ?float $totalNow,
|
||||
float $availBefore, ?float $availNow, float $amount,
|
||||
bool $grantedSince = false): string
|
||||
{
|
||||
if ($inWallet) {
|
||||
return 'applied';
|
||||
}
|
||||
if ($totalNow === null || $availNow === null) {
|
||||
return 'unknown';
|
||||
}
|
||||
// $grantedSince:退款发起前后该司机在本企业有新的发放。此时额度比对不可信——
|
||||
// 「调低 100 + 新发 100」看起来就是没变,若据此判「未生效」,运营重新发起会重复扣减。
|
||||
if (abs(($totalBefore - $totalNow) - $amount) <= self::EPS) {
|
||||
return $grantedSince ? 'unknown' : 'applied';
|
||||
}
|
||||
if (abs($totalNow - $totalBefore) <= self::EPS && $availNow + self::EPS >= $availBefore) {
|
||||
return $grantedSince ? 'unknown' : 'not_applied';
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 内部
|
||||
// ================================================================
|
||||
|
||||
protected static function finishSuccess(array $order, int $refundId, string $refundNo, float $amount, string $text, array $res): array
|
||||
{
|
||||
$amount = round($amount, 2);
|
||||
$won = false;
|
||||
Db::transaction(function () use ($order, $refundId, $amount, $text, $res, &$won) {
|
||||
$won = (bool) Db::name('partners_order')
|
||||
->where('order_id', $order['order_id'])
|
||||
->whereIn('refund_status', [self::PROCESSING, self::CONFIRM])
|
||||
->update([
|
||||
'refund_status' => self::SUCCESS,
|
||||
'refund_amount' => $amount,
|
||||
'refund_time' => time(),
|
||||
'refund_text' => mb_substr($text, 0, 250),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
Db::name('qyt_refund')->where('refund_id', $refundId)->update([
|
||||
'status' => self::SUCCESS,
|
||||
'message' => mb_substr($text, 0, 250),
|
||||
'response' => self::dumpRes($res),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
// 只有状态跃迁胜出者才回补合作商余额,保证只退一次
|
||||
if ($won) {
|
||||
PartnersBalance::refund((int) $order['partners_id'], $amount, (string) $order['partners_order_no'],
|
||||
(int) $order['order_id'], (int) $order['store_id']);
|
||||
}
|
||||
});
|
||||
|
||||
if ($won) {
|
||||
// 退款计算与加油推送之间有时间差:退款期间若又来了加油,已加油 + 已退款可能超过订单金额,告警人工核对
|
||||
$fresh = Db::name('partners_order')->where('order_id', $order['order_id'])->field('total_price,used_amount')->find();
|
||||
if ($fresh && (float) $fresh['used_amount'] + $amount > (float) $fresh['total_price'] + self::EPS) {
|
||||
(new QytOil())->saveLog("【告警】退款后 已加油+已退款 超出订单金额 order={$order['order_id']}"
|
||||
. " total={$fresh['total_price']} used={$fresh['used_amount']} refund={$amount}");
|
||||
}
|
||||
(new QueueService())->push('notify_queue', json_encode([
|
||||
'order_id' => $order['order_id'],
|
||||
'optType' => 'refund',
|
||||
'retry_count' => 0,
|
||||
], 320));
|
||||
(new QytOil())->saveLog("退款成功 order={$order['order_id']} refund_no={$refundNo} amount={$amount},已回补合作商余额");
|
||||
}
|
||||
$order['refund_status'] = self::SUCCESS;
|
||||
$order['refund_amount'] = $amount;
|
||||
return self::result(true, self::SUCCESS, $text, '', self::orderData($order, $refundNo));
|
||||
}
|
||||
|
||||
protected static function finishFailed(array $order, int $refundId, string $refundNo, string $text, array $res): array
|
||||
{
|
||||
Db::name('partners_order')
|
||||
->where('order_id', $order['order_id'])
|
||||
->whereIn('refund_status', [self::PROCESSING, self::CONFIRM])
|
||||
->update(['refund_status' => self::FAILED, 'refund_text' => mb_substr($text, 0, 250), 'update_time' => time()]);
|
||||
Db::name('qyt_refund')->where('refund_id', $refundId)->update([
|
||||
'status' => self::FAILED, 'message' => mb_substr($text, 0, 250), 'response' => self::dumpRes($res), 'update_time' => time(),
|
||||
]);
|
||||
(new QytOil())->saveLog("退款失败 order={$order['order_id']} refund_no={$refundNo}:{$text}");
|
||||
$order['refund_status'] = self::FAILED;
|
||||
return self::result(false, self::FAILED, $text, self::REASON_UPSTREAM, self::orderData($order, $refundNo));
|
||||
}
|
||||
|
||||
protected static function finishConfirm(array $order, int $refundId, string $refundNo, string $text, array $res): array
|
||||
{
|
||||
Db::name('partners_order')
|
||||
->where('order_id', $order['order_id'])
|
||||
->where('refund_status', self::PROCESSING)
|
||||
->update(['refund_status' => self::CONFIRM, 'refund_text' => mb_substr($text, 0, 250), 'update_time' => time()]);
|
||||
Db::name('qyt_refund')->where('refund_id', $refundId)->update([
|
||||
'status' => self::CONFIRM, 'message' => mb_substr($text, 0, 250), 'response' => self::dumpRes($res), 'update_time' => time(),
|
||||
]);
|
||||
(new QytOil())->saveLog("【告警】退款结果未知 order={$order['order_id']} refund_no={$refundNo}:{$text}");
|
||||
$order['refund_status'] = self::CONFIRM;
|
||||
return self::result(false, self::CONFIRM, $text, self::REASON_PROCESSING, self::orderData($order, $refundNo));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array [order, refund, enterprise, errorMessage]
|
||||
*/
|
||||
protected static function loadConfirmContext(int $orderId): array
|
||||
{
|
||||
$order = Db::name('partners_order')->where('order_id', $orderId)->find();
|
||||
if (!$order) {
|
||||
return [[], [], null, '订单不存在'];
|
||||
}
|
||||
if ((int) $order['refund_status'] !== self::CONFIRM) {
|
||||
return [$order, [], null, '只有「待人工确认」的退款需要确认,当前状态:' . (self::STATUS_TEXT[(int) $order['refund_status']] ?? '')];
|
||||
}
|
||||
$refund = Db::name('qyt_refund')->where('order_id', $orderId)->where('status', self::CONFIRM)
|
||||
->order('refund_id', 'desc')->find();
|
||||
if (!$refund) {
|
||||
return [$order, [], null, '未找到待确认的退款流水'];
|
||||
}
|
||||
$enterprise = EnterpriseModel::detail((int) $order['qyt_enterprise_id']);
|
||||
if (!$enterprise) {
|
||||
return [$order, $refund, null, '本单绑定的企油通企业不存在'];
|
||||
}
|
||||
return [$order, $refund, $enterprise, ''];
|
||||
}
|
||||
|
||||
protected static function orderData(array $order, string $refundNo = ''): array
|
||||
{
|
||||
if ($refundNo === '' && !empty($order['order_id'])) {
|
||||
$refundNo = (string) Db::name('qyt_refund')->where('order_id', $order['order_id'])
|
||||
->order('refund_id', 'desc')->value('refund_no');
|
||||
}
|
||||
return [
|
||||
'oilCardNo' => (string) ($order['oilCardNo'] ?? ''),
|
||||
'saleOrderNo' => (string) ($order['partners_order_no'] ?? ''),
|
||||
'refundNo' => $refundNo,
|
||||
// 已退款:实际退款金额;其余:当前剩余未加油金额(即本次会退的金额)
|
||||
'refundAmount' => number_format((int) ($order['refund_status'] ?? 0) === self::SUCCESS
|
||||
? (float) ($order['refund_amount'] ?? 0)
|
||||
: self::remainAmount($order), 2, '.', ''),
|
||||
'usedAmount' => number_format((float) ($order['used_amount'] ?? 0), 2, '.', ''),
|
||||
'refundStatus' => (int) ($order['refund_status'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
protected static function result(bool $ok, int $status, string $message, string $reason = '', array $data = []): array
|
||||
{
|
||||
return [
|
||||
'ok' => $ok,
|
||||
'status' => $status,
|
||||
'status_text' => self::STATUS_TEXT[$status] ?? '',
|
||||
'message' => $message,
|
||||
'reason' => $reason,
|
||||
'data' => $data,
|
||||
];
|
||||
}
|
||||
|
||||
protected static function dumpRes(array $res): string
|
||||
{
|
||||
if (!$res) {
|
||||
return '';
|
||||
}
|
||||
$keep = ['code' => $res['code'] ?? '', 'message' => $res['message'] ?? '',
|
||||
'transport_error' => !empty($res['transport_error']), 'data' => QytOil::maskDeep($res['data'] ?? [])];
|
||||
return mb_substr((string) json_encode($keep, JSON_UNESCAPED_UNICODE), 0, 60000);
|
||||
}
|
||||
}
|
||||
@@ -340,58 +340,91 @@ class CallbackQueue extends Command
|
||||
continue;
|
||||
}
|
||||
$payTime = !empty($c['pay_time']) ? (strtotime($c['pay_time']) ?: time()) : time();
|
||||
// 落消费流水(uk_consume 去重:重复回调直接忽略)
|
||||
// 一单可以分多次加油:每条推送是一次加油,consume_amount = 本次占用运单金额(分)
|
||||
$consumeFen = (int)($c['consume_amount'] ?? 0);
|
||||
$consumeYuan = number_format($consumeFen / 100, 2, '.', '');
|
||||
$orderCode = trim((string)($c['order_code'] ?? ''));
|
||||
// 去重键:文档里 order_code 是选填,缺失时用本笔消费的特征字段做指纹,
|
||||
// 否则同一运单的第二笔加油会被唯一键误判为重复推送而丢掉(口径与迁移脚本回填一致)
|
||||
$consumeKey = $orderCode !== '' ? $orderCode : 'FP' . md5(implode('|', [
|
||||
$c['quota_id'] ?? '', $c['pay_time'] ?? '', $c['consume_amount'] ?? '',
|
||||
$c['pay_fee'] ?? '', $c['station_code'] ?? '', $c['litter'] ?? '',
|
||||
]));
|
||||
$consumeId = 0;
|
||||
try {
|
||||
Db::name('qyt_consume')->insert([
|
||||
'order_id' => $order['order_id'],
|
||||
'waybill_no' => $c['waybill_no'] ?? '',
|
||||
'waybill_no_code' => $code,
|
||||
'order_code' => $c['order_code'] ?? '',
|
||||
'station_code' => $c['station_code'] ?? '',
|
||||
'station_name' => $c['station_name'] ?? '',
|
||||
'oil_name' => $c['oil_name'] ?? '',
|
||||
'litter' => $c['litter'] ?? '',
|
||||
'oil_price' => (int)($c['oil_price'] ?? 0),
|
||||
'actual_oil_price' => (int)($c['actual_oil_price'] ?? 0),
|
||||
'pay_fee' => (int)($c['pay_fee'] ?? 0),
|
||||
'consume_amount' => (int)($c['consume_amount'] ?? 0),
|
||||
'pay_time' => $payTime,
|
||||
'raw' => json_encode($c, 320),
|
||||
'store_id' => $order['store_id'],
|
||||
'create_time' => time(),
|
||||
]);
|
||||
// 流水落库与订单累计放在同一事务:唯一键挡住重推,重推既不重复累计也不重复通知
|
||||
Db::transaction(function () use ($order, $c, $code, $orderCode, $consumeKey, $payTime, $consumeFen, $consumeYuan, &$consumeId) {
|
||||
$consumeId = (int)Db::name('qyt_consume')->insertGetId([
|
||||
'order_id' => $order['order_id'],
|
||||
'waybill_no' => $c['waybill_no'] ?? '',
|
||||
'waybill_no_code' => $code,
|
||||
'order_code' => $orderCode,
|
||||
'consume_key' => $consumeKey,
|
||||
'station_code' => $c['station_code'] ?? '',
|
||||
'station_name' => $c['station_name'] ?? '',
|
||||
'oil_name' => $c['oil_name'] ?? '',
|
||||
'litter' => $c['litter'] ?? '',
|
||||
'oil_price' => (int)($c['oil_price'] ?? 0),
|
||||
'actual_oil_price' => (int)($c['actual_oil_price'] ?? 0),
|
||||
'pay_fee' => (int)($c['pay_fee'] ?? 0),
|
||||
'consume_amount' => $consumeFen,
|
||||
'pay_time' => $payTime,
|
||||
'raw' => json_encode($c, 320),
|
||||
'store_id' => $order['store_id'],
|
||||
'create_time' => time(),
|
||||
]);
|
||||
// 累计已加油金额 / 次数,并记下最近一次加油信息(原子自增,不读后写)
|
||||
Db::name('partners_order')->where('order_id', $order['order_id'])->update([
|
||||
'used_amount' => Db::raw('`used_amount` + ' . $consumeYuan),
|
||||
'used_num' => Db::raw('`used_num` + 1'),
|
||||
'use_text' => '加油',
|
||||
'qyt_order_code' => $orderCode,
|
||||
'station_name' => $c['station_name'] ?? '',
|
||||
'oil_name' => $c['oil_name'] ?? '',
|
||||
'litter' => $c['litter'] ?? '',
|
||||
'pay_fee' => (int)($c['pay_fee'] ?? 0),
|
||||
'consume_time' => $payTime,
|
||||
'update_time' => time(),
|
||||
]);
|
||||
// use_status 语义不变(0 未加油 / 1 有加油记录):只在第一笔加油时置 1 并记首次加油时间
|
||||
Db::name('partners_order')->where('order_id', $order['order_id'])->where('use_status', 0)->update([
|
||||
'use_status' => 1,
|
||||
'use_time' => time(),
|
||||
'shareNo' => $orderCode !== '' ? $orderCode : $order['shareNo'],
|
||||
]);
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
if (strpos($e->getMessage(), 'Duplicate') === false) {
|
||||
throw $e;
|
||||
}
|
||||
$SybOil->saveLogStore('企油通消费回调重复,忽略流水:' . $code . '/' . ($c['order_code'] ?? ''));
|
||||
$SybOil->saveLogStore('企油通消费回调重复,忽略(不重复累计、不重复通知):' . $code . '/' . $consumeKey);
|
||||
continue;
|
||||
}
|
||||
// 条件跃迁 use_status 0 -> 1(要求已发放),仅胜出者通知下单平台
|
||||
$affected = Db::name('partners_order')->where('order_id', $order['order_id'])
|
||||
->where('use_status', 0)
|
||||
->where('oil_status', 1)
|
||||
->update([
|
||||
'use_status' => 1,
|
||||
'use_time' => time(),
|
||||
'use_text' => '加油',
|
||||
'qyt_order_code' => $c['order_code'] ?? '',
|
||||
'shareNo' => $c['order_code'] ?: $order['shareNo'],
|
||||
'station_name' => $c['station_name'] ?? '',
|
||||
'oil_name' => $c['oil_name'] ?? '',
|
||||
'litter' => $c['litter'] ?? '',
|
||||
'pay_fee' => (int)($c['pay_fee'] ?? 0),
|
||||
'consume_time' => $payTime,
|
||||
'update_time' => time(),
|
||||
]);
|
||||
if ($affected) {
|
||||
$queueService = new QueueService();
|
||||
$queueService->push('notify_queue', json_encode([
|
||||
'order_id' => $order['order_id'],
|
||||
'optType' => 'add_oil',
|
||||
'retry_count' => 0,
|
||||
], 320));
|
||||
$SybOil->saveLogStore('企油通消费回调处理成功:' . json_encode($c, 320));
|
||||
|
||||
// 超额告警:累计加油 + 已退款 > 订单金额。上游额度按司机凭证累计,
|
||||
// 归属到本单的消费可能与本地可退金额算出的不一致,需要人工核对
|
||||
$fresh = Db::name('partners_order')->where('order_id', $order['order_id'])
|
||||
->field('total_price,used_amount,refund_amount,oil_status_zsy')->find();
|
||||
if ($fresh && (float)$fresh['used_amount'] + (float)$fresh['refund_amount'] > (float)$fresh['total_price'] + 0.005) {
|
||||
$SybOil->saveLogStore('【告警】订单已加油+已退款超出订单金额 order=' . $order['order_id']
|
||||
. ' total=' . $fresh['total_price'] . ' used=' . $fresh['used_amount'] . ' refund=' . $fresh['refund_amount']);
|
||||
}
|
||||
|
||||
// 每笔加油都通知下单平台(amount = 本次占用金额)
|
||||
$notify = json_encode([
|
||||
'order_id' => $order['order_id'],
|
||||
'optType' => 'add_oil',
|
||||
'consume_id' => $consumeId,
|
||||
'retry_count' => 0,
|
||||
], 320);
|
||||
$queueService = new QueueService();
|
||||
if ((int)($fresh['oil_status_zsy'] ?? 2) === 2) {
|
||||
$queueService->push('notify_queue', $notify);
|
||||
} else {
|
||||
// 7.8 发放结果还没确认就先收到了加油:延后 60 秒再通知,尽量让「分油成功」通知先到下游
|
||||
$queueService->laterPush('notify_queue', $notify, 60);
|
||||
}
|
||||
$SybOil->saveLogStore('企油通消费回调处理成功:' . json_encode($c, 320));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -87,12 +87,31 @@ class NotifyQueue extends Command
|
||||
return false;
|
||||
}
|
||||
$partnersOil = new PartnersOil();
|
||||
$optType = (string)($data['optType'] ?? '');
|
||||
|
||||
// 加油:一单可多次加油,每笔单独通知。消息里带 consume_id 时推这一笔的占用金额和消费单号;
|
||||
// 老消息没有 consume_id(上线前入队的)则沿用旧口径推订单金额
|
||||
$consume = null;
|
||||
if ($optType == 'add_oil' && !empty($data['consume_id'])) {
|
||||
$consume = Db::name('qyt_consume')->where('id', (int)$data['consume_id'])->find();
|
||||
}
|
||||
if ($optType == 'refund') {
|
||||
$amount = $order['refund_amount'];
|
||||
} elseif ($consume) {
|
||||
$amount = number_format((int)$consume['consume_amount'] / 100, 2, '.', '');
|
||||
} else {
|
||||
$amount = $order['total_price'];
|
||||
}
|
||||
|
||||
$data = [
|
||||
'oilCardNo' => $order['oilCardNo'],
|
||||
'amount' => $order['total_price'],
|
||||
'shareNo' => $order['shareNo'],
|
||||
'type' => $data['optType'] == 'share_oil' ? 'add' : 'sub',
|
||||
'optType' => $data['optType'],
|
||||
// 分油:订单金额;加油:本次加油占用金额;退款:退款金额
|
||||
'amount' => $amount,
|
||||
// 订单实付金额(即分油金额),所有场景都带,方便对接方拿 amount 对照参考
|
||||
'orderAmount' => number_format((float)$order['total_price'], 2, '.', ''),
|
||||
'shareNo' => $consume && $consume['order_code'] !== '' ? $consume['order_code'] : $order['shareNo'],
|
||||
'type' => $optType == 'share_oil' ? 'add' : 'sub',
|
||||
'optType' => $optType,
|
||||
];
|
||||
$header['method'] = 'syb.notify.oilCard.change';
|
||||
$res = $partnersOil->sendRequest($partners, $partners['callback'], $data, $header);
|
||||
@@ -103,11 +122,16 @@ class NotifyQueue extends Command
|
||||
'oil_callback' => 1,
|
||||
'oil_callback_time' => time(),
|
||||
]);
|
||||
}else{
|
||||
}elseif($data['optType'] == 'add_oil'){
|
||||
// 退款通知成功不记 use_callback,避免把「加油已回调」标错
|
||||
$order->save([
|
||||
'use_callback' => 1,
|
||||
'use_callback_time' => time(),
|
||||
]);
|
||||
// 逐笔记录这一笔加油已通知
|
||||
if ($consume) {
|
||||
Db::name('qyt_consume')->where('id', $consume['id'])->update(['notify_status' => 1, 'notify_time' => time()]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ declare (strict_types=1);
|
||||
|
||||
namespace app\store\controller;
|
||||
|
||||
use app\common\model\qyt\Enterprise as EnterpriseModel;
|
||||
use app\store\controller\Controller;
|
||||
use app\store\model\Partners as PartnersModel;
|
||||
use app\store\model\partners\Order as OrderModel;
|
||||
@@ -40,15 +41,47 @@ class Partners extends Controller
|
||||
$newList = [];
|
||||
$OrderModel = new OrderModel();
|
||||
$date = date("Y-m-d");
|
||||
// 企油通企业:口径与下单时 Enterprise::resolve 一致(绑定且启用 -> 用绑定的;否则 -> 默认企业)
|
||||
$bindIds = array_filter(array_map('intval', array_column($list['data'], 'qyt_enterprise_id')));
|
||||
$enterprises = $bindIds
|
||||
? array_column(EnterpriseModel::whereIn('enterprise_id', $bindIds)
|
||||
->field('enterprise_id,company_name,passageway,is_enable')->select()->toArray(), null, 'enterprise_id')
|
||||
: [];
|
||||
$default = EnterpriseModel::getDefault();
|
||||
foreach ($list['data'] as $val){
|
||||
$val['day_money'] =$OrderModel->getOrderTotalPrice($date, $date, $val);
|
||||
$val['total_money'] =$OrderModel->getOrderTotalPrice(null, null, $val);
|
||||
$val = array_merge($val, $this->enterpriseInfo((int)$val['qyt_enterprise_id'], $enterprises, $default));
|
||||
$newList[] = $val;
|
||||
}
|
||||
$list['data'] = $newList;
|
||||
return $this->renderSuccess(['list'=>$list,'user'=>$this->store['user']]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合作商实际使用的企油通企业
|
||||
* qyt_enterprise_source:bind 绑定 / default 未绑定走默认 / invalid 绑定的企业已停用或删除(实际走默认)/ none 无可用企业
|
||||
*/
|
||||
private function enterpriseInfo(int $bindId, array $enterprises, $default): array
|
||||
{
|
||||
$bound = $bindId > 0 ? ($enterprises[$bindId] ?? null) : null;
|
||||
if ($bound && (int)$bound['is_enable'] === 1) {
|
||||
$source = 'bind';
|
||||
$use = $bound;
|
||||
} else {
|
||||
$source = $bindId > 0 ? 'invalid' : 'default';
|
||||
$use = $default ? $default->toArray() : null;
|
||||
!$use && $source = $bindId > 0 ? 'invalid' : 'none';
|
||||
}
|
||||
return [
|
||||
'qyt_enterprise_source' => $source,
|
||||
// 绑定失效时也把原绑定企业名带出来,便于排查
|
||||
'qyt_bind_name' => $bound['company_name'] ?? '',
|
||||
'qyt_enterprise_name' => $use['company_name'] ?? '',
|
||||
'qyt_enterprise_channel' => isset($use['passageway']) ? ($use['passageway'] === 'pingan' ? '平安银行' : '支付宝企业码') : '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有商户
|
||||
* @return mixed
|
||||
|
||||
@@ -16,7 +16,9 @@ namespace app\store\controller\partners;
|
||||
use app\common\service\PartnersBalance;
|
||||
use app\common\service\QueueService;
|
||||
use app\common\model\qyt\Enterprise as EnterpriseModel;
|
||||
use app\common\service\QytInvoice;
|
||||
use app\common\service\QytOil;
|
||||
use app\common\service\QytRefund;
|
||||
use app\store\controller\Controller;
|
||||
use app\store\model\order\QrCode as QrCodeModel;
|
||||
use app\store\model\partners\Order as OrderModel;
|
||||
@@ -66,8 +68,12 @@ class Order extends Controller
|
||||
if (!$order) {
|
||||
return $this->renderError('订单不存在');
|
||||
}
|
||||
if ($order['use_status'] != 0) {
|
||||
return $this->renderError('该单已加油核销');
|
||||
// 一单可以分多次加油:没加完的单继续给加油地址
|
||||
if ((int) $order['refund_status'] === 2) {
|
||||
return $this->renderError('该单已退款,不能再加油');
|
||||
}
|
||||
if ((float) $order['used_amount'] + 0.005 >= (float) $order['total_price']) {
|
||||
return $this->renderError('该单金额已全部加油');
|
||||
}
|
||||
if ($order['oil_status'] != 1) {
|
||||
return $this->renderError('该单尚未发放成功,无法获取加油地址');
|
||||
@@ -193,12 +199,120 @@ class Order extends Controller
|
||||
if ($order['use_status'] != 1) {
|
||||
return $this->renderError('该单未加油核销,无可回调内容');
|
||||
}
|
||||
(new QueueService())->push('notify_queue', json_encode([
|
||||
'order_id' => $orderId,
|
||||
'type' => 'sub',
|
||||
'optType' => 'add_oil',
|
||||
'retry_count' => 0,
|
||||
], 320));
|
||||
return $this->renderSuccess('已重新推送加油结果');
|
||||
// 每笔加油单独通知:优先补推还没通知成功的;都通知过了就全部重推一遍(下游按 shareNo 去重)
|
||||
$ids = Db::name('qyt_consume')->where('order_id', $orderId)->where('notify_status', 0)->order('id')->column('id');
|
||||
$all = !$ids;
|
||||
$all && $ids = Db::name('qyt_consume')->where('order_id', $orderId)->order('id')->column('id');
|
||||
$queue = new QueueService();
|
||||
if (!$ids) {
|
||||
// 没有消费流水(改造前的历史单),按旧口径推一次
|
||||
$queue->push('notify_queue', json_encode(['order_id' => $orderId, 'optType' => 'add_oil', 'retry_count' => 0], 320));
|
||||
return $this->renderSuccess('已重新推送加油结果');
|
||||
}
|
||||
foreach ($ids as $consumeId) {
|
||||
$queue->push('notify_queue', json_encode([
|
||||
'order_id' => $orderId,
|
||||
'optType' => 'add_oil',
|
||||
'consume_id' => (int) $consumeId,
|
||||
'retry_count' => 0,
|
||||
], 320));
|
||||
}
|
||||
return $this->renderSuccess(($all ? '已重新推送全部 ' : '已补推未通知的 ') . count($ids) . ' 笔加油结果');
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 退款(整单全额:7.17 查凭证 + 7.16 调低额度)
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* 发起退款
|
||||
*/
|
||||
public function refund(): Json
|
||||
{
|
||||
$orderId = (int) $this->request->param('orderId');
|
||||
$reason = trim((string) $this->request->param('reason', ''));
|
||||
if (!Db::name('partners_order')->where('order_id', $orderId)->count()) {
|
||||
return $this->renderError('订单不存在');
|
||||
}
|
||||
$res = QytRefund::apply($orderId, 'admin', $this->operator(), $reason);
|
||||
return $res['ok'] ? $this->renderSuccess($res['data'], $res['message']) : $this->renderError($res['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认「待人工确认」的退款:向企油通取证(7.13 流水 + 7.17 凭证)
|
||||
*/
|
||||
public function refundConfirm(): Json
|
||||
{
|
||||
$res = QytRefund::confirm((int) $this->request->param('orderId'));
|
||||
return $this->decided($res) ? $this->renderSuccess($res['data'], $res['message']) : $this->renderError($res['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 人工判定「待人工确认」的退款
|
||||
* result = success / failed,remark 必填(判定依据)
|
||||
*/
|
||||
public function refundResolve(): Json
|
||||
{
|
||||
$result = (string) $this->request->param('result');
|
||||
if (!in_array($result, ['success', 'failed'], true)) {
|
||||
return $this->renderError('判定结果不合法');
|
||||
}
|
||||
$res = QytRefund::resolve(
|
||||
(int) $this->request->param('orderId'),
|
||||
$result === 'success',
|
||||
$this->operator(),
|
||||
(string) $this->request->param('remark', '')
|
||||
);
|
||||
return $this->decided($res) ? $this->renderSuccess($res['data'], $res['message']) : $this->renderError($res['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单的退款流水
|
||||
*/
|
||||
public function refundLog(): Json
|
||||
{
|
||||
$list = Db::name('qyt_refund')
|
||||
->where('order_id', (int) $this->request->param('orderId'))
|
||||
->field('refund_id,refund_no,source,operator,reason,amount,quota_id,total_before,available_before,total_after,available_after,status,message,create_time,update_time')
|
||||
->order('refund_id', 'desc')
|
||||
->select()->toArray();
|
||||
foreach ($list as &$row) {
|
||||
$row['status_text'] = QytRefund::STATUS_TEXT[(int) $row['status']] ?? '';
|
||||
$row['create_time_text'] = date('Y-m-d H:i:s', (int) $row['create_time']);
|
||||
}
|
||||
return $this->renderSuccess(compact('list'));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 发票(7.14,按「企业 + 消费月份 + 司机」关联)
|
||||
// ================================================================
|
||||
|
||||
public function invoice(): Json
|
||||
{
|
||||
$order = Db::name('partners_order')->where('order_id', (int) $this->request->param('orderId'))->find();
|
||||
if (!$order) {
|
||||
return $this->renderError('订单不存在');
|
||||
}
|
||||
if ((int) $order['use_status'] !== 1) {
|
||||
return $this->renderError('该订单还没有加油记录,暂无发票');
|
||||
}
|
||||
$res = QytInvoice::forOrder($order, (bool) $this->request->param('refresh', 0));
|
||||
return $res['ok'] ? $this->renderSuccess($res, $res['message']) : $this->renderError($res['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取证 / 人工判定是否得出了结论:退款成功,或明确判定为「未生效」都算请求成功,
|
||||
* 由返回的 refundStatus 区分;状态不符、取证失败、仍无法判定才算失败
|
||||
*/
|
||||
private function decided(array $res): bool
|
||||
{
|
||||
return $res['ok']
|
||||
|| ($res['status'] === QytRefund::FAILED && $res['reason'] === QytRefund::REASON_UPSTREAM);
|
||||
}
|
||||
|
||||
/** 当前后台操作人 */
|
||||
private function operator(): string
|
||||
{
|
||||
return (string) ($this->store['user']['user_name'] ?? 'admin');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | 后台 · 企油通司机管理
|
||||
// +----------------------------------------------------------------------
|
||||
// | list() 司机档案 + 从订单实时汇总的累计发放 / 已退款 / 订单数
|
||||
// | detail() 档案、涉及企业、最近订单(发放失败原因里能看到二要素核验结果)
|
||||
// | upstream() 实时查企油通:balance 7.12 钱包余额 / wallet 7.13 钱包流水 / voucher 7.17 支付凭证
|
||||
// |
|
||||
// | 身份证号全程只在后端使用,返回前端一律打码。
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types=1);
|
||||
|
||||
namespace app\store\controller\qyt;
|
||||
|
||||
use app\common\model\qyt\Enterprise as EnterpriseModel;
|
||||
use app\common\service\QytOil;
|
||||
use app\common\service\QytRefund;
|
||||
use app\store\controller\Controller;
|
||||
use think\facade\Db;
|
||||
use think\response\Json;
|
||||
|
||||
class Driver extends Controller
|
||||
{
|
||||
public function list(): Json
|
||||
{
|
||||
$param = $this->request->param();
|
||||
$keyword = trim((string) ($param['keyword'] ?? ''));
|
||||
|
||||
$query = Db::name('qyt_driver')->where('store_id', $this->storeId);
|
||||
if ($keyword !== '') {
|
||||
if (preg_match('/^\d{17}[\dXx]$/', $keyword)) {
|
||||
$query->where('id_card', strtoupper($keyword));
|
||||
} elseif (preg_match('/^1\d{10}$/', $keyword)) {
|
||||
$query->where('mobile', $keyword);
|
||||
} else {
|
||||
$query->whereLike('name', '%' . $keyword . '%');
|
||||
}
|
||||
}
|
||||
if (isset($param['conflict']) && $param['conflict'] !== '' && (int) $param['conflict'] >= 0) {
|
||||
$query->where('conflict', (int) $param['conflict']);
|
||||
}
|
||||
|
||||
$list = $query->order('last_order_time', 'desc')->paginate(15)->toArray();
|
||||
$stats = $this->stats(array_column($list['data'], 'id_card'));
|
||||
foreach ($list['data'] as &$row) {
|
||||
$row = $this->formatDriver($row, $stats[$row['id_card']] ?? []);
|
||||
}
|
||||
return $this->renderSuccess(compact('list'));
|
||||
}
|
||||
|
||||
public function detail(int $driverId): Json
|
||||
{
|
||||
$driver = $this->findDriver($driverId);
|
||||
if (!$driver) {
|
||||
return $this->renderError('司机不存在');
|
||||
}
|
||||
$stats = $this->stats([$driver['id_card']]);
|
||||
|
||||
// 涉及过的企业(上游查询要按企业查)
|
||||
$entIds = Db::name('partners_order')->where('store_id', $this->storeId)
|
||||
->where('id_card', $driver['id_card'])->where('qyt_enterprise_id', '>', 0)
|
||||
->group('qyt_enterprise_id')->column('qyt_enterprise_id');
|
||||
$enterprises = $entIds
|
||||
? EnterpriseModel::whereIn('enterprise_id', $entIds)
|
||||
->field('enterprise_id,company_name,passageway,status')->select()->toArray()
|
||||
: [];
|
||||
|
||||
$orders = Db::name('partners_order')->where('store_id', $this->storeId)->where('id_card', $driver['id_card'])
|
||||
->field('order_id,oilCardNo,partners_order_no,total_price,driver_name,mobile,oil_status_zsy,oil_text,use_status,refund_status,refund_text,qyt_enterprise_id,create_time')
|
||||
->order('order_id', 'desc')->limit(20)->select()->toArray();
|
||||
foreach ($orders as &$o) {
|
||||
$o['mobile'] = QytOil::maskMobile((string) $o['mobile']);
|
||||
$o['refund_status_text'] = QytRefund::STATUS_TEXT[(int) $o['refund_status']] ?? '';
|
||||
$o['create_time_text'] = date('Y-m-d H:i:s', (int) $o['create_time']);
|
||||
}
|
||||
|
||||
return $this->renderSuccess([
|
||||
'detail' => $this->formatDriver($driver, $stats[$driver['id_card']] ?? []),
|
||||
'enterprises' => $enterprises,
|
||||
'orders' => $orders,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实时查企油通
|
||||
* type: balance 7.12 / wallet 7.13 / voucher 7.17
|
||||
*/
|
||||
public function upstream(int $driverId, int $enterpriseId, string $type = 'balance', int $page = 1): Json
|
||||
{
|
||||
$driver = $this->findDriver($driverId);
|
||||
if (!$driver) {
|
||||
return $this->renderError('司机不存在');
|
||||
}
|
||||
$enterprise = EnterpriseModel::detail($enterpriseId);
|
||||
if (!$enterprise) {
|
||||
return $this->renderError('企业不存在');
|
||||
}
|
||||
$qyt = QytOil::forEnterprise($enterprise);
|
||||
$mobile = (string) $driver['mobile'];
|
||||
$idCard = (string) $driver['id_card'];
|
||||
$page = max(1, $page);
|
||||
|
||||
switch ($type) {
|
||||
case 'balance':
|
||||
$res = $qyt->driverBalanceQuery($mobile, $idCard, $page, 20);
|
||||
break;
|
||||
case 'wallet':
|
||||
$res = $qyt->driverWalletQuery($mobile, $idCard, $page, 20);
|
||||
break;
|
||||
case 'voucher':
|
||||
$res = $qyt->getVoucherList($mobile, $idCard, $page, 20);
|
||||
break;
|
||||
default:
|
||||
return $this->renderError('查询类型不合法');
|
||||
}
|
||||
if ($res['code'] !== '00') {
|
||||
return $this->renderError('企油通返回:' . ($res['message'] ?: '查询失败'));
|
||||
}
|
||||
|
||||
$data = (array) $res['data'];
|
||||
// 7.12 / 7.13 金额是「分」,统一转成元再给前端;7.17 本来就是元
|
||||
if ($type === 'balance' || $type === 'wallet') {
|
||||
foreach (['totalGrantAmount', 'availableBalance'] as $k) {
|
||||
isset($data[$k]) && $data[$k . 'Yuan'] = QytOil::fenToYuan($data[$k]);
|
||||
}
|
||||
foreach (['list', 'transactionDetails'] as $listKey) {
|
||||
foreach ((array) ($data[$listKey] ?? []) as $i => $item) {
|
||||
foreach (['total_amount', 'available_amount', 'amount'] as $k) {
|
||||
if (isset($item[$k]) && is_numeric($item[$k])) {
|
||||
$data[$listKey][$i][$k . '_yuan'] = QytOil::fenToYuan((int) $item[$k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->renderSuccess(['type' => $type, 'data' => QytOil::maskDeep($data)]);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
|
||||
private function findDriver(int $driverId): ?array
|
||||
{
|
||||
$row = Db::name('qyt_driver')->where('store_id', $this->storeId)->where('driver_id', $driverId)->find();
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从订单实时汇总,按身份证号分组
|
||||
*/
|
||||
private function stats(array $idCards): array
|
||||
{
|
||||
if (!$idCards) {
|
||||
return [];
|
||||
}
|
||||
$rows = Db::name('partners_order')
|
||||
->where('store_id', $this->storeId)
|
||||
->whereIn('id_card', $idCards)
|
||||
->fieldRaw('id_card,'
|
||||
. ' COUNT(*) AS order_num,'
|
||||
. ' SUM(IF(oil_status_zsy = 2, total_price, 0)) AS grant_amount,'
|
||||
. ' SUM(IF(refund_status = 2, refund_amount, 0)) AS refund_amount,'
|
||||
. ' SUM(IF(use_status = 1, 1, 0)) AS used_num,'
|
||||
. ' SUM(used_amount) AS used_amount,'
|
||||
. ' SUM(IF(oil_status_zsy = 3, 1, 0)) AS fail_num,'
|
||||
. ' SUM(IF(oil_status_zsy = 1, 1, 0)) AS pending_num')
|
||||
->group('id_card')
|
||||
->select()->toArray();
|
||||
return array_column($rows, null, 'id_card');
|
||||
}
|
||||
|
||||
private function formatDriver(array $row, array $stat): array
|
||||
{
|
||||
$grant = round((float) ($stat['grant_amount'] ?? 0), 2);
|
||||
$refund = round((float) ($stat['refund_amount'] ?? 0), 2);
|
||||
$out = [
|
||||
'driver_id' => (int) $row['driver_id'],
|
||||
'name' => $row['name'],
|
||||
'mobile' => $row['mobile'],
|
||||
'id_card_mask' => QytOil::maskIdCard((string) $row['id_card']),
|
||||
'conflict' => (int) $row['conflict'],
|
||||
'conflict_text' => $row['conflict_text'],
|
||||
'last_order_time_text' => $row['last_order_time'] ? date('Y-m-d H:i:s', (int) $row['last_order_time']) : '',
|
||||
'order_num' => (int) ($stat['order_num'] ?? 0),
|
||||
'used_num' => (int) ($stat['used_num'] ?? 0),
|
||||
'fail_num' => (int) ($stat['fail_num'] ?? 0),
|
||||
'pending_num' => (int) ($stat['pending_num'] ?? 0),
|
||||
'grant_amount' => number_format($grant, 2, '.', ''),
|
||||
'refund_amount' => number_format($refund, 2, '.', ''),
|
||||
'used_amount' => number_format((float) ($stat['used_amount'] ?? 0), 2, '.', ''),
|
||||
// 本地口径的净发放额,仅供参考;真实可用额度以企油通 7.12 为准
|
||||
'net_amount' => number_format($grant - $refund, 2, '.', ''),
|
||||
];
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
@@ -234,6 +234,25 @@ class Enterprise extends Controller
|
||||
return $this->renderSuccess("同步完成:新增 {$added} 家,更新 {$updated} 家");
|
||||
}
|
||||
|
||||
/**
|
||||
* 7.11 企业账户余额(仅平安银行通道;银行侧实时查询,勿高频调用)
|
||||
*/
|
||||
public function balance(int $enterpriseId): Json
|
||||
{
|
||||
$model = EnterpriseModel::detail($enterpriseId);
|
||||
if (!$model) {
|
||||
return $this->renderError('企业不存在');
|
||||
}
|
||||
$res = QytOil::forEnterprise($model)->distributable();
|
||||
if ($res['code'] !== '00') {
|
||||
return $this->renderError('企油通返回:' . ($res['message'] ?: '查询失败'));
|
||||
}
|
||||
return $this->renderSuccess([
|
||||
'balance' => QytOil::fenToYuan($res['data']['balance'] ?? 0),
|
||||
'available_balance' => QytOil::fenToYuan($res['data']['available_balance'] ?? 0),
|
||||
], '查询成功');
|
||||
}
|
||||
|
||||
/** 记录同步结果 */
|
||||
private function saveSync($model, string $text): void
|
||||
{
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | 后台 · 企油通发票管理(7.14)
|
||||
// +----------------------------------------------------------------------
|
||||
// | list() 发票记录(按 企业 + 通道 + 月份 + 司机手机号 维度)
|
||||
// | sync() 同步:指定企业则同步该企业(可带司机手机号);不指定则批量同步所有启用企业的当月全量
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types=1);
|
||||
|
||||
namespace app\store\controller\qyt;
|
||||
|
||||
use app\common\model\qyt\Enterprise as EnterpriseModel;
|
||||
use app\common\service\QytInvoice;
|
||||
use app\store\controller\Controller;
|
||||
use think\facade\Db;
|
||||
use think\response\Json;
|
||||
|
||||
class Invoice extends Controller
|
||||
{
|
||||
public function list(): Json
|
||||
{
|
||||
$param = $this->request->param();
|
||||
$query = Db::name('qyt_invoice');
|
||||
!empty($param['qyt_enterprise_id']) && $query->where('qyt_enterprise_id', (int) $param['qyt_enterprise_id']);
|
||||
!empty($param['month']) && $query->where('month', (string) $param['month']);
|
||||
!empty($param['mobile']) && $query->where('mobile', trim((string) $param['mobile']));
|
||||
if (isset($param['has_invoice']) && $param['has_invoice'] !== '' && (int) $param['has_invoice'] >= 0) {
|
||||
(int) $param['has_invoice'] === 1 ? $query->where('invoice_count', '>', 0) : $query->where('invoice_count', 0);
|
||||
}
|
||||
|
||||
$list = $query->order(['month' => 'desc', 'invoice_id' => 'desc'])->paginate(15)->toArray();
|
||||
|
||||
$names = EnterpriseModel::column('company_name', 'enterprise_id');
|
||||
foreach ($list['data'] as &$row) {
|
||||
$row = QytInvoice::format($row);
|
||||
$row['company_name'] = $names[$row['qyt_enterprise_id']] ?? '(企业已删除)';
|
||||
}
|
||||
return $this->renderSuccess(compact('list'));
|
||||
}
|
||||
|
||||
public function sync(): Json
|
||||
{
|
||||
$month = trim((string) $this->request->param('month'));
|
||||
$mobile = trim((string) $this->request->param('mobile', ''));
|
||||
$enterpriseId = (int) $this->request->param('enterpriseId', 0);
|
||||
|
||||
if ($enterpriseId > 0) {
|
||||
$enterprise = EnterpriseModel::detail($enterpriseId);
|
||||
if (!$enterprise) {
|
||||
return $this->renderError('企业不存在');
|
||||
}
|
||||
$res = QytInvoice::sync($enterprise, $month, $mobile);
|
||||
return $res['ok'] ? $this->renderSuccess($res['row'], $res['message']) : $this->renderError($res['message']);
|
||||
}
|
||||
|
||||
// 批量:所有启用企业的该月全量
|
||||
$ok = $fail = 0;
|
||||
$errors = [];
|
||||
foreach (EnterpriseModel::where('is_enable', 1)->select() as $enterprise) {
|
||||
$res = QytInvoice::sync($enterprise, $month, '');
|
||||
if ($res['ok']) {
|
||||
$ok++;
|
||||
} else {
|
||||
$fail++;
|
||||
$errors[] = $enterprise['company_name'] . ':' . $res['message'];
|
||||
}
|
||||
}
|
||||
$message = "批量同步完成:成功 {$ok} 家,失败 {$fail} 家" . ($errors ? '(' . implode(';', array_slice($errors, 0, 3)) . ')' : '');
|
||||
return $fail && !$ok ? $this->renderError($message) : $this->renderSuccess([], $message);
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,8 @@ class Order extends OrderModel
|
||||
'oil_status' => -1, // 分油(发放)状态
|
||||
'oil_status_zsy' => -1, // 企油通发放态 0未提交 1发放中 2成功 3失败
|
||||
'use_status' => -1, // 加油核销状态
|
||||
'refund_status' => -1, // 退款状态 0未退款 1退款中 2已退款 3退款失败 4待人工确认
|
||||
'id_card' => '', // 司机身份证号(从司机管理跳转过来时使用)
|
||||
'betweenTime' => [] // 起止时间
|
||||
]);
|
||||
// 检索查询条件
|
||||
@@ -100,6 +102,10 @@ class Order extends OrderModel
|
||||
$params['oil_status_zsy'] > -1 && $filter[] = ['oil_status_zsy', '=', (int)$params['oil_status_zsy']];
|
||||
// 加油核销状态
|
||||
$params['use_status'] > -1 && $filter[] = ['use_status', '=', (int)$params['use_status']];
|
||||
// 退款状态
|
||||
$params['refund_status'] > -1 && $filter[] = ['order.refund_status', '=', (int)$params['refund_status']];
|
||||
// 司机身份证号(精确匹配)
|
||||
!empty($params['id_card']) && $filter[] = ['order.id_card', '=', strtoupper(trim((string)$params['id_card']))];
|
||||
// 起止时间
|
||||
if (!empty($params['betweenTime'])) {
|
||||
$times = between_time($params['betweenTime']);
|
||||
|
||||
@@ -66,6 +66,11 @@ return [
|
||||
// 7.15 是否把 creditcode(营业执照号)带入链接并入签(true 则本次登录固定到该企业)
|
||||
'h5_with_creditcode' => true,
|
||||
|
||||
// 7.15 是否对链接参数做 URL 编码。
|
||||
// 默认 false:文档示例里 sign 末尾是裸的 ==,说明落地页拿的是未编码的原始 base64。
|
||||
// 若对接人确认对方页面会先 decodeURIComponent,改成 true。
|
||||
'h5_sign_urlencode' => (bool) env('qyt.h5_sign_urlencode', false),
|
||||
|
||||
|
||||
// ============ 杂项 ============
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | 7.15 免登链接 · 编码方式对照工具(仅联调用,CLI)
|
||||
// +----------------------------------------------------------------------
|
||||
// | 背景:同一条链接在桌面浏览器能打开,微信里打开 / 扫码却提示验签失败。
|
||||
// | base64 签名里的 + / = 在不同 URL 解析实现下会被区别对待,
|
||||
// | 本工具用同一个签名生成多种编码变体,逐个在微信里试,定位是哪一种。
|
||||
// |
|
||||
// | 用法(在站点根目录执行):
|
||||
// | php deploy/h5_debug.php 13800000000
|
||||
// | php deploy/h5_debug.php 13800000000 2 # 指定企业ID,不传用默认企业
|
||||
// |
|
||||
// | 测法:把打印出来的每条链接发到微信「文件传输助手」,逐条点开,
|
||||
// | 记下哪几条能进、哪几条报验签失败,再据此定方案。
|
||||
// |
|
||||
// | ⚠️ 会生成真实可登录的免登链接,联调结束请删除本文件。
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types=1);
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(403);
|
||||
exit('仅限命令行执行');
|
||||
}
|
||||
|
||||
$root = dirname(__DIR__);
|
||||
require $root . '/vendor/autoload.php';
|
||||
|
||||
$app = new \think\App($root . '/');
|
||||
$app->initialize();
|
||||
|
||||
use app\common\model\qyt\Enterprise as EnterpriseModel;
|
||||
use app\common\service\QytConfig;
|
||||
|
||||
$mobile = $argv[1] ?? '';
|
||||
$enterpriseId = (int) ($argv[2] ?? 0);
|
||||
|
||||
if (!preg_match('/^1[3-9]\d{9}$/', $mobile)) {
|
||||
exit("用法:php deploy/h5_debug.php <司机手机号> [企业ID]\n");
|
||||
}
|
||||
|
||||
$enterprise = EnterpriseModel::resolve($enterpriseId);
|
||||
if (!$enterprise) {
|
||||
exit("未找到可用企业,请先在后台「设置 → 企业管理」建企业并设为默认\n");
|
||||
}
|
||||
|
||||
$conf = QytConfig::forEnterprise($enterprise);
|
||||
$base = rtrim((string) $conf['h5_base_url'], '/');
|
||||
$appId = (string) ($conf['h5_app_id'] ?: $conf['app_id']);
|
||||
|
||||
// ---------- 按 7.15 规则签名(ksort -> http_build_query -> urldecode -> 大写MD5 -> SHA1withRSA -> base64) ----------
|
||||
$params = [
|
||||
'appId' => $appId,
|
||||
'mobile' => $mobile,
|
||||
'timestamp' => date('YmdHis'),
|
||||
];
|
||||
!empty($conf['h5_with_creditcode']) && $params['creditcode'] = (string) $conf['business_license'];
|
||||
|
||||
$forSign = $params;
|
||||
ksort($forSign);
|
||||
$raw = urldecode(http_build_query($forSign));
|
||||
$md5 = strtoupper(md5($raw));
|
||||
|
||||
$pem = trim((string) $conf['private_key']);
|
||||
if (strpos($pem, '-----BEGIN') === false) {
|
||||
$pem = "-----BEGIN PRIVATE KEY-----\n" . chunk_split($pem, 64, "\n") . "-----END PRIVATE KEY-----\n";
|
||||
}
|
||||
$pkey = openssl_pkey_get_private($pem);
|
||||
if ($pkey === false) {
|
||||
exit("私钥无效:" . openssl_error_string() . "\n");
|
||||
}
|
||||
$sig = '';
|
||||
openssl_sign($md5, $sig, $pkey, OPENSSL_ALGO_SHA1);
|
||||
$sign = base64_encode($sig);
|
||||
|
||||
// ---------- 输出 ----------
|
||||
$build = function (array $p, string $sign, string $tail) {
|
||||
$pairs = [];
|
||||
foreach ($p as $k => $v) {
|
||||
$pairs[] = $k . '=' . $v;
|
||||
}
|
||||
$pairs[] = 'sign=' . $sign;
|
||||
return '/?' . implode('&', $pairs) . $tail;
|
||||
};
|
||||
|
||||
echo "企业:{$enterprise['company_name']}({$enterprise['business_license']},状态 {$enterprise['status']})\n";
|
||||
echo "通道:{$conf['passageway_name']} appId:{$appId}\n";
|
||||
echo "待签串:{$raw}\n";
|
||||
echo "大写MD5:{$md5}\n";
|
||||
echo "签名含 + 的个数:" . substr_count($sign, '+') . ",含 / 的个数:" . substr_count($sign, '/') . "\n";
|
||||
echo str_repeat('=', 70) . "\n\n";
|
||||
|
||||
$variants = [
|
||||
'A. 原样(当前线上行为)' => [$sign, '#/'],
|
||||
'B. 只把 + 编码为 %2B' => [str_replace('+', '%2B', $sign), '#/'],
|
||||
'C. 把 + 和 / 都编码' => [str_replace(['+', '/'], ['%2B', '%2F'], $sign), '#/'],
|
||||
'D. 全部编码(改之前的老行为)' => [rawurlencode($sign), '#/'],
|
||||
'E. 原样,但去掉末尾的 #/' => [$sign, ''],
|
||||
];
|
||||
|
||||
$i = 1;
|
||||
foreach ($variants as $label => [$s, $tail]) {
|
||||
echo "【{$i}】{$label}\n";
|
||||
echo $base . $build($params, $s, $tail) . "\n\n";
|
||||
$i++;
|
||||
}
|
||||
|
||||
echo str_repeat('-', 70) . "\n";
|
||||
echo "把上面 5 条分别发到微信里点开,记录结果:\n";
|
||||
echo " 只有 A 失败、B 成功 -> 问题是 +,对方页面会解码,我们编码 + 即可\n";
|
||||
echo " A、B 都失败、C 成功 -> + 和 / 都要编码\n";
|
||||
echo " A 成功、其余失败 -> 不是编码问题,重点查 E(末尾 #/)与微信追加参数\n";
|
||||
echo " 只有 E 成功 -> 末尾的 #/ 被算进了 sign\n";
|
||||
echo " 全都失败 -> 不是 URL 层面的问题,查公钥是否配对、企业是否已审核通过\n";
|
||||
@@ -318,16 +318,18 @@ function defaultBody($act, $orgNo)
|
||||
if ($act === 'share') {
|
||||
$body = [
|
||||
'orgNo' => $orgNo,
|
||||
'amount' => isset($_GET['amount']) ? $_GET['amount'] : '100.00',
|
||||
'amount' => isset($_GET['amount']) ? trim($_GET['amount']) : '100.00',
|
||||
'mobile' => $mobile,
|
||||
'name' => isset($_GET['name']) ? $_GET['name'] : '张三',
|
||||
'saleOrderNo' => $saleOrderNo,
|
||||
'name' => isset($_GET['name']) ? trim($_GET['name']) : '张三',
|
||||
// 销售单号留空则自动生成(同一单号重复提交会走幂等返回,便于测重复下单)
|
||||
'saleOrderNo' => isset($_GET['saleOrderNo']) && trim($_GET['saleOrderNo']) !== '' ? trim($_GET['saleOrderNo']) : $saleOrderNo,
|
||||
'oilCardType' => 'zsy_ticket',
|
||||
];
|
||||
// 司机身份证号:两个通道都必传。
|
||||
// 加 &noIdCard=1 可故意不传,用来验证实名通道下的 0001 拒单是否生效。
|
||||
if (empty($_GET['noIdCard'])) {
|
||||
$body['idCard'] = isset($_GET['idCard']) ? $_GET['idCard'] : '450103199001011238';
|
||||
// 页面上身份证号留空(或带 &noIdCard=1)即不传,用来验证 0001 拒单是否生效。
|
||||
$idCard = isset($_GET['idCard']) ? trim($_GET['idCard']) : '450103199001011238';
|
||||
if ($idCard !== '' && empty($_GET['noIdCard'])) {
|
||||
$body['idCard'] = $idCard;
|
||||
}
|
||||
return [
|
||||
'method' => 'syb.oil.oilCard.share',
|
||||
@@ -349,6 +351,17 @@ function defaultBody($act, $orgNo)
|
||||
'body' => ['orgNo' => $orgNo, 'oilCardNo' => $oilCardNo],
|
||||
];
|
||||
}
|
||||
if ($act === 'refund') {
|
||||
return [
|
||||
'method' => 'syb.oil.oilCard.refund',
|
||||
'path' => '/oilCard/refund',
|
||||
'body' => [
|
||||
'orgNo' => $orgNo,
|
||||
'oilCardNo' => $oilCardNo,
|
||||
'reason' => isset($_GET['reason']) ? $_GET['reason'] : '联调测试退款',
|
||||
],
|
||||
];
|
||||
}
|
||||
if ($act === 'sharelist') {
|
||||
return [
|
||||
'method' => 'syb.oil.oilCard.shareList',
|
||||
@@ -383,7 +396,7 @@ try {
|
||||
$platformPub = platformPublicKey($ROOT);
|
||||
$qconf = qytConf($ROOT, $partner);
|
||||
|
||||
if (in_array($act, ['share', 'qrcode', 'status', 'sharelist'], true)) {
|
||||
if (in_array($act, ['share', 'qrcode', 'status', 'sharelist', 'refund'], true)) {
|
||||
$def = defaultBody($act, $partner['orgNo']);
|
||||
$env = buildEnvelope($partner, $platformPub, $def['method'], $def['body']);
|
||||
$url = apiUrl($def['path']);
|
||||
@@ -416,7 +429,7 @@ if (PHP_SAPI === 'cli') {
|
||||
";
|
||||
}
|
||||
if (!$result) {
|
||||
echo "用法:php qyt_test.php [share|qrcode|status|sharelist]\n";
|
||||
echo "用法:php qyt_test.php [share|qrcode|status|sharelist|refund](qrcode/status/refund 需带 oilCardNo,CLI 下请用浏览器方式)\n";
|
||||
exit;
|
||||
}
|
||||
echo '接口:' . $result['def']['method'] . "\n";
|
||||
@@ -459,6 +472,11 @@ $curOil = isset($_GET['oilCardNo']) ? $_GET['oilCardNo'] : '';
|
||||
.ok{color:#52c41a;font-weight:600}.err{color:#ff4d4f;font-weight:600}
|
||||
.tip{background:#fffbe6;border:1px solid #ffe58f;padding:10px 14px;border-radius:6px;font-size:13px;margin-bottom:16px}
|
||||
.lbl{margin:12px 0 6px;color:#555}
|
||||
.share-form{margin-bottom:14px;padding-bottom:14px;border-bottom:1px dashed #eee}
|
||||
.share-form .fields{display:flex;flex-wrap:wrap;gap:10px 16px;margin-bottom:10px}
|
||||
.share-form label{display:flex;flex-direction:column;font-size:12px;color:#666}
|
||||
.share-form input{margin-top:4px;width:180px;padding:6px 8px;border:1px solid #d9d9d9;border-radius:4px;font-size:13px}
|
||||
.share-form button{padding:7px 16px;background:#52c41a;color:#fff;border:0;border-radius:4px;font-size:13px;cursor:pointer;margin-right:10px}
|
||||
</style>
|
||||
</head>
|
||||
<body><div class="wrap">
|
||||
@@ -526,17 +544,36 @@ $curOil = isset($_GET['oilCardNo']) ? $_GET['oilCardNo'] : '';
|
||||
|
||||
<div class="card">
|
||||
<h2>发起测试</h2>
|
||||
<?php
|
||||
// 表单回填:提交后保留上次填写的值;首次打开用默认值
|
||||
$isShare = $act === 'share';
|
||||
$fv = function ($key, $default) use ($isShare) {
|
||||
return htmlspecialchars($isShare && isset($_GET[$key]) ? (string)$_GET[$key] : $default);
|
||||
};
|
||||
?>
|
||||
<form method="get" class="share-form">
|
||||
<input type="hidden" name="act" value="share">
|
||||
<?php if (!empty($_GET['api'])): ?>
|
||||
<input type="hidden" name="api" value="<?php echo htmlspecialchars($_GET['api']); ?>">
|
||||
<?php endif; ?>
|
||||
<div class="fields">
|
||||
<label>分油金额(元)<input name="amount" value="<?php echo $fv('amount', '100.00'); ?>" placeholder="最多两位小数"></label>
|
||||
<label>司机手机号<input name="mobile" value="<?php echo $fv('mobile', '13800000001'); ?>" maxlength="11"></label>
|
||||
<label>司机姓名<input name="name" value="<?php echo $fv('name', '张三'); ?>"></label>
|
||||
<label>身份证号<input name="idCard" value="<?php echo $fv('idCard', '450103199001011238'); ?>" maxlength="18" placeholder="留空即不传"></label>
|
||||
<label>销售单号<input name="saleOrderNo" value="<?php echo $fv('saleOrderNo', ''); ?>" placeholder="留空自动生成"></label>
|
||||
</div>
|
||||
<button type="submit">① 下单分油 (share)</button>
|
||||
<span class="sub">身份证号留空即不传,应返回 <b>0001</b> 拒单;销售单号填已用过的可测幂等返回</span>
|
||||
</form>
|
||||
<div class="btns">
|
||||
<a class="g" href="?act=share">① 下单分油 (share)</a>
|
||||
<a class="o" href="?act=qrcode&oilCardNo=<?php echo urlencode($curOil); ?>">② 获取加油链接 (qrCode)</a>
|
||||
<a class="p" href="?act=status&oilCardNo=<?php echo urlencode($curOil); ?>">③ 订单状态 (orderStatus)</a>
|
||||
<a href="?act=sharelist">④ 分油记录 (shareList)</a>
|
||||
<a class="r" href="?act=share&noIdCard=1">⑤ 下单但不传身份证号</a>
|
||||
<a href="?act=sharelist&mobile=<?php echo urlencode($isShare && isset($_GET['mobile']) ? $_GET['mobile'] : '13800000001'); ?>">④ 分油记录 (shareList)</a>
|
||||
<a class="r" href="?act=refund&oilCardNo=<?php echo urlencode($curOil); ?>">⑥ 退款 (refund)</a>
|
||||
</div>
|
||||
<div class="sub" style="margin:6px 0 0">
|
||||
②③ 需要 oilCardNo:先点①拿到后会自动带上。<br>
|
||||
① 可自定义参数:<code>?act=share&amount=50.00&mobile=13800000002&name=李四&idCard=110101199003071233</code><br>
|
||||
⑤ 用来验证实名校验:两个通道都要求实名,应当返回 <b>0001</b> 拒单
|
||||
②③⑥ 需要 oilCardNo:先下单拿到后,在下方响应区点对应按钮会自动带上。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -569,6 +606,7 @@ $curOil = isset($_GET['oilCardNo']) ? $_GET['oilCardNo'] : '';
|
||||
<div class="btns" style="margin-top:14px">
|
||||
<a class="o" href="?act=qrcode&oilCardNo=<?php echo urlencode($newOil); ?>">用 <?php echo htmlspecialchars($newOil); ?> 取加油链接</a>
|
||||
<a class="p" href="?act=status&oilCardNo=<?php echo urlencode($newOil); ?>">查该单状态</a>
|
||||
<a class="r" href="?act=refund&oilCardNo=<?php echo urlencode($newOil); ?>">退款该单(需发放成功且未加油)</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
||||
[data-v-f551b524] .ant-form-item-control{padding-left:10px}[data-v-f551b524] .ant-form-item-control .ant-form-item-control{padding-left:0}.ant-divider[data-v-f551b524]{margin-top:60px!important}.ant-input-number[data-v-f551b524]{width:160px}.input-text_right[data-v-f551b524]{margin-left:10px}.radio-item[data-v-f551b524]{margin-bottom:16px}.tips-alert[data-v-e3266188]{margin-bottom:20px}.search-form[data-v-e3266188]{display:inline-block;margin-left:16px}.sub[data-v-e3266188]{color:#999;font-size:12px;line-height:1.7}.ml-6[data-v-e3266188]{margin-left:6px}.text-red[data-v-e3266188]{color:#ff4d4f}.text-grey[data-v-e3266188]{color:#bbb}.op-actions[data-v-e3266188]{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;gap:8px 16px}.op-actions>a[data-v-e3266188],.op-actions>span[data-v-e3266188]{white-space:nowrap}[data-v-7e59f1c6] .ant-form-item-control{padding-left:10px}[data-v-7e59f1c6] .ant-form-item-control .ant-form-item-control{padding-left:0}.tips-alert[data-v-7e59f1c6]{margin-bottom:28px}.input-short[data-v-7e59f1c6]{width:160px}.input-code[data-v-7e59f1c6]{width:30%}.input-inst[data-v-7e59f1c6]{width:70%}[data-v-01d8132a] .ant-form-item-control{padding-left:10px}[data-v-01d8132a] .ant-form-item-control .ant-form-item-control{padding-left:0}.ant-divider[data-v-01d8132a]{margin-top:60px!important}.ant-input-number[data-v-01d8132a]{width:160px}.input-text_right[data-v-01d8132a]{margin-left:10px}.radio-item[data-v-01d8132a]{margin-bottom:16px}[data-v-dd6519b2] .ant-form-item-control{padding-left:10px}[data-v-dd6519b2] .ant-form-item-control .ant-form-item-control{padding-left:0}.ant-form-item[data-v-12cd9802]{margin-bottom:10px}[data-v-12cd9802] .ant-form-item-control{padding-left:10px}[data-v-12cd9802] .ant-form-item-control .ant-form-item-control{padding-left:0}.ant-divider[data-v-12cd9802]{margin-top:50px!important}
|
||||
@@ -1 +0,0 @@
|
||||
[data-v-f551b524] .ant-form-item-control{padding-left:10px}[data-v-f551b524] .ant-form-item-control .ant-form-item-control{padding-left:0}.ant-divider[data-v-f551b524]{margin-top:60px!important}.ant-input-number[data-v-f551b524]{width:160px}.input-text_right[data-v-f551b524]{margin-left:10px}.radio-item[data-v-f551b524]{margin-bottom:16px}.tips-alert[data-v-4e165f10]{margin-bottom:20px}.search-form[data-v-4e165f10]{display:inline-block;margin-left:16px}.sub[data-v-4e165f10]{color:#999;font-size:12px;line-height:1.7}.ml-6[data-v-4e165f10]{margin-left:6px}.text-red[data-v-4e165f10]{color:#ff4d4f}.text-grey[data-v-4e165f10]{color:#bbb}[data-v-7e59f1c6] .ant-form-item-control{padding-left:10px}[data-v-7e59f1c6] .ant-form-item-control .ant-form-item-control{padding-left:0}.tips-alert[data-v-7e59f1c6]{margin-bottom:28px}.input-short[data-v-7e59f1c6]{width:160px}.input-code[data-v-7e59f1c6]{width:30%}.input-inst[data-v-7e59f1c6]{width:70%}[data-v-01d8132a] .ant-form-item-control{padding-left:10px}[data-v-01d8132a] .ant-form-item-control .ant-form-item-control{padding-left:0}.ant-divider[data-v-01d8132a]{margin-top:60px!important}.ant-input-number[data-v-01d8132a]{width:160px}.input-text_right[data-v-01d8132a]{margin-left:10px}.radio-item[data-v-01d8132a]{margin-bottom:16px}[data-v-dd6519b2] .ant-form-item-control{padding-left:10px}[data-v-dd6519b2] .ant-form-item-control .ant-form-item-control{padding-left:0}.ant-form-item[data-v-12cd9802]{margin-bottom:10px}[data-v-12cd9802] .ant-form-item-control{padding-left:10px}[data-v-12cd9802] .ant-form-item-control .ant-form-item-control{padding-left:0}.ant-divider[data-v-12cd9802]{margin-top:50px!important}
|
||||
@@ -1 +1 @@
|
||||
<!DOCTYPE html><html lang="zh-cmn-Hans"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>商家管理中心</title><style>#loading-mask{position:fixed;left:0;top:0;height:100%;width:100%;background:#fff;user-select:none;z-index:9999;overflow:hidden}.loading-wrapper{position:absolute;top:50%;left:50%;transform:translate(-50%,-100%)}.loading-dot{animation:antRotate 1.2s infinite linear;transform:rotate(45deg);position:relative;display:inline-block;font-size:64px;width:64px;height:64px;box-sizing:border-box}.loading-dot i{width:22px;height:22px;position:absolute;display:block;background-color:#1890ff;border-radius:100%;transform:scale(.75);transform-origin:50% 50%;opacity:.3;animation:antSpinMove 1s infinite linear alternate}.loading-dot i:nth-child(1){top:0;left:0}.loading-dot i:nth-child(2){top:0;right:0;-webkit-animation-delay:.4s;animation-delay:.4s}.loading-dot i:nth-child(3){right:0;bottom:0;-webkit-animation-delay:.8s;animation-delay:.8s}.loading-dot i:nth-child(4){bottom:0;left:0;-webkit-animation-delay:1.2s;animation-delay:1.2s}@keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@-webkit-keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@keyframes antSpinMove{to{opacity:1}}@-webkit-keyframes antSpinMove{to{opacity:1}}</style><link href="css/card.83fb7f11.css" rel="prefetch"><link href="css/card~content~goods~manage~market~order~page~setting~store~user.bd86e290.css" rel="prefetch"><link href="css/card~content~goods~manage~market~page~store~user.06d71c34.css" rel="prefetch"><link href="css/client.6fbe4ba8.css" rel="prefetch"><link href="css/content.c53a1699.css" rel="prefetch"><link href="css/exception.37940980.css" rel="prefetch"><link href="css/goods.50f80ed8.css" rel="prefetch"><link href="css/index.ff363719.css" rel="prefetch"><link href="css/market.cc3678af.css" rel="prefetch"><link href="css/order.6124dbc9.css" rel="prefetch"><link href="css/page.2d2d2056.css" rel="prefetch"><link href="css/passport.68f7ba8b.css" rel="prefetch"><link href="css/setting.a8d19305.css" rel="prefetch"><link href="css/store.3d002fc4.css" rel="prefetch"><link href="css/user.21b3449e.css" rel="prefetch"><link href="js/card.c3d0e7b1.js" rel="prefetch"><link href="js/card~content~goods~manage~market~order~page~setting~store~user.193a5813.js" rel="prefetch"><link href="js/card~content~goods~manage~market~page~store~user.a13c007a.js" rel="prefetch"><link href="js/client.58c03cfb.js" rel="prefetch"><link href="js/content.fd60d4aa.js" rel="prefetch"><link href="js/exception.d3b6b1e8.js" rel="prefetch"><link href="js/goods.e491ade3.js" rel="prefetch"><link href="js/goods~order.d9a20aa1.js" rel="prefetch"><link href="js/index.874a9e0d.js" rel="prefetch"><link href="js/lang-zh-CN.303b0ef5.js" rel="prefetch"><link href="js/manage.3485dc4a.js" rel="prefetch"><link href="js/market.db25da7c.js" rel="prefetch"><link href="js/order.bc3eac19.js" rel="prefetch"><link href="js/page.24dbd519.js" rel="prefetch"><link href="js/passport.4036de85.js" rel="prefetch"><link href="js/setting.c3c97868.js" rel="prefetch"><link href="js/store.e6d3d371.js" rel="prefetch"><link href="js/user.dde70d3c.js" rel="prefetch"><link href="css/app.77281994.css" rel="preload" as="style"><link href="css/chunk-vendors.eb75505e.css" rel="preload" as="style"><link href="js/app.b2cf6a22.js" rel="preload" as="script"><link href="js/chunk-vendors.d8af00dc.js" rel="preload" as="script"><link href="css/chunk-vendors.eb75505e.css" rel="stylesheet"><link href="css/app.77281994.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but vue-antd-pro doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"><div id="loading-mask"><div class="loading-wrapper"><span class="loading-dot loading-dot-spin"><i></i><i></i><i></i><i></i></span></div></div></div><script src="config.js"></script><script src="js/chunk-vendors.d8af00dc.js"></script><script src="js/app.b2cf6a22.js"></script></body></html>
|
||||
<!DOCTYPE html><html lang="zh-cmn-Hans"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>商家管理中心</title><style>#loading-mask{position:fixed;left:0;top:0;height:100%;width:100%;background:#fff;user-select:none;z-index:9999;overflow:hidden}.loading-wrapper{position:absolute;top:50%;left:50%;transform:translate(-50%,-100%)}.loading-dot{animation:antRotate 1.2s infinite linear;transform:rotate(45deg);position:relative;display:inline-block;font-size:64px;width:64px;height:64px;box-sizing:border-box}.loading-dot i{width:22px;height:22px;position:absolute;display:block;background-color:#1890ff;border-radius:100%;transform:scale(.75);transform-origin:50% 50%;opacity:.3;animation:antSpinMove 1s infinite linear alternate}.loading-dot i:nth-child(1){top:0;left:0}.loading-dot i:nth-child(2){top:0;right:0;-webkit-animation-delay:.4s;animation-delay:.4s}.loading-dot i:nth-child(3){right:0;bottom:0;-webkit-animation-delay:.8s;animation-delay:.8s}.loading-dot i:nth-child(4){bottom:0;left:0;-webkit-animation-delay:1.2s;animation-delay:1.2s}@keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@-webkit-keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@keyframes antSpinMove{to{opacity:1}}@-webkit-keyframes antSpinMove{to{opacity:1}}</style><link href="css/card.83fb7f11.css" rel="prefetch"><link href="css/card~content~goods~manage~market~order~page~setting~store~user.bd86e290.css" rel="prefetch"><link href="css/card~content~goods~manage~market~page~store~user.06d71c34.css" rel="prefetch"><link href="css/client.6fbe4ba8.css" rel="prefetch"><link href="css/content.c53a1699.css" rel="prefetch"><link href="css/exception.37940980.css" rel="prefetch"><link href="css/goods.5e999d2c.css" rel="prefetch"><link href="css/index.ff363719.css" rel="prefetch"><link href="css/market.cc3678af.css" rel="prefetch"><link href="css/order.6124dbc9.css" rel="prefetch"><link href="css/page.2d2d2056.css" rel="prefetch"><link href="css/passport.68f7ba8b.css" rel="prefetch"><link href="css/setting.37110db2.css" rel="prefetch"><link href="css/store.3d002fc4.css" rel="prefetch"><link href="css/user.21b3449e.css" rel="prefetch"><link href="js/card.c3d0e7b1.js" rel="prefetch"><link href="js/card~content~goods~manage~market~order~page~setting~store~user.193a5813.js" rel="prefetch"><link href="js/card~content~goods~manage~market~page~store~user.a13c007a.js" rel="prefetch"><link href="js/client.58c03cfb.js" rel="prefetch"><link href="js/content.fd60d4aa.js" rel="prefetch"><link href="js/exception.d3b6b1e8.js" rel="prefetch"><link href="js/goods.19f5e8e7.js" rel="prefetch"><link href="js/goods~order.d9a20aa1.js" rel="prefetch"><link href="js/index.874a9e0d.js" rel="prefetch"><link href="js/lang-zh-CN.303b0ef5.js" rel="prefetch"><link href="js/manage.3485dc4a.js" rel="prefetch"><link href="js/market.db25da7c.js" rel="prefetch"><link href="js/order.bc3eac19.js" rel="prefetch"><link href="js/page.24dbd519.js" rel="prefetch"><link href="js/passport.4036de85.js" rel="prefetch"><link href="js/setting.02914954.js" rel="prefetch"><link href="js/store.e6d3d371.js" rel="prefetch"><link href="js/user.dde70d3c.js" rel="prefetch"><link href="css/app.77281994.css" rel="preload" as="style"><link href="css/chunk-vendors.43940245.css" rel="preload" as="style"><link href="js/app.76ad6bca.js" rel="preload" as="script"><link href="js/chunk-vendors.d8af00dc.js" rel="preload" as="script"><link href="css/chunk-vendors.43940245.css" rel="stylesheet"><link href="css/app.77281994.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but vue-antd-pro doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"><div id="loading-mask"><div class="loading-wrapper"><span class="loading-dot loading-dot-spin"><i></i><i></i><i></i><i></i></span></div></div></div><script src="config.js"></script><script src="js/chunk-vendors.d8af00dc.js"></script><script src="js/app.76ad6bca.js"></script></body></html>
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1,84 @@
|
||||
-- ============================================================
|
||||
-- 企油通对接 · 一单多次加油(7.10 consume_amount 累计)
|
||||
-- 生成:2026-09-15
|
||||
-- ------------------------------------------------------------
|
||||
-- 7.10 每条推送是一次加油,consume_amount = 本次占用运单金额(分)。
|
||||
-- 一张订单可以分多次加油,因此:
|
||||
-- · 订单累计已加油金额 used_amount / 次数 used_num
|
||||
-- · 退款金额 = 订单金额 − 已加油金额(剩余未加油金额),不能超过它
|
||||
-- · 每笔加油都通知下单平台(amount = 本次占用金额),逐笔记录是否已通知
|
||||
--
|
||||
-- 执行顺序:... -> qyt_migration_refund_invoice_driver.sql -> 本文件
|
||||
-- ============================================================
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 1. 订单:已加油金额 / 次数
|
||||
-- use_status 语义不变:0 未加油 / 1 有加油记录(可能只加了一部分)
|
||||
-- ------------------------------------------------------------
|
||||
ALTER TABLE `yoshop_partners_order`
|
||||
ADD COLUMN `used_amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '已加油占用金额(元),7.10 consume_amount 累计' AFTER `use_status`,
|
||||
ADD COLUMN `used_num` int(11) NOT NULL DEFAULT 0 COMMENT '加油次数' AFTER `used_amount`;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 2. 消费流水:去重键 + 逐笔通知状态
|
||||
-- 原唯一键 (waybill_no_code, order_code) 有缺陷:文档里 order_code 是选填,
|
||||
-- 不传时同一运单的第二笔加油会被当成重复推送丢掉,已加油金额就少记了。
|
||||
-- 改为 (waybill_no_code, consume_key):有 order_code 用它,没有就用本笔消费特征字段的指纹。
|
||||
-- ------------------------------------------------------------
|
||||
ALTER TABLE `yoshop_qyt_consume`
|
||||
ADD COLUMN `consume_key` varchar(64) NOT NULL DEFAULT '' COMMENT '去重键:order_code,缺失时为消费特征指纹(FP+md5)' AFTER `order_code`,
|
||||
ADD COLUMN `notify_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已通知下单平台 0否 1是' AFTER `raw`,
|
||||
ADD COLUMN `notify_time` int(11) NOT NULL DEFAULT 0 COMMENT '通知成功时间' AFTER `notify_status`,
|
||||
ADD INDEX `idx_order_notify` (`order_id`, `notify_status`);
|
||||
|
||||
-- 2.1 回填去重键。指纹口径与 CallbackQueue 一致:
|
||||
-- md5(quota_id|pay_time|consume_amount|pay_fee|station_code|litter),取值来自回调原文 raw
|
||||
UPDATE `yoshop_qyt_consume`
|
||||
SET `consume_key` = `order_code`
|
||||
WHERE `consume_key` = '' AND `order_code` <> '';
|
||||
|
||||
UPDATE `yoshop_qyt_consume`
|
||||
SET `consume_key` = CONCAT('FP', MD5(CONCAT_WS('|',
|
||||
IFNULL(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(`raw`, '$.quota_id')), 'null'), ''),
|
||||
IFNULL(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(`raw`, '$.pay_time')), 'null'), ''),
|
||||
IFNULL(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(`raw`, '$.consume_amount')), 'null'), ''),
|
||||
IFNULL(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(`raw`, '$.pay_fee')), 'null'), ''),
|
||||
IFNULL(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(`raw`, '$.station_code')), 'null'), ''),
|
||||
IFNULL(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(`raw`, '$.litter')), 'null'), '')
|
||||
)))
|
||||
WHERE `consume_key` = '' AND `raw` IS NOT NULL AND JSON_VALID(`raw`);
|
||||
|
||||
-- 2.2 兜底:原文不可解析的历史行用主键,保证唯一键能建起来
|
||||
UPDATE `yoshop_qyt_consume` SET `consume_key` = CONCAT('ID', `id`) WHERE `consume_key` = '';
|
||||
|
||||
-- 2.3 更换唯一键
|
||||
-- ⚠️ 执行前可先检查是否有冲突:
|
||||
-- SELECT waybill_no_code, consume_key, COUNT(*) c FROM yoshop_qyt_consume GROUP BY waybill_no_code, consume_key HAVING c > 1;
|
||||
ALTER TABLE `yoshop_qyt_consume`
|
||||
DROP INDEX `uk_consume`,
|
||||
ADD UNIQUE KEY `uk_consume` (`waybill_no_code`, `consume_key`);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 3. 回填订单已加油金额 / 次数
|
||||
-- ------------------------------------------------------------
|
||||
UPDATE `yoshop_partners_order` o
|
||||
JOIN (
|
||||
SELECT `order_id`, SUM(`consume_amount`) AS fen, COUNT(*) AS n
|
||||
FROM `yoshop_qyt_consume` GROUP BY `order_id`
|
||||
) c ON c.`order_id` = o.`order_id`
|
||||
SET o.`used_amount` = ROUND(c.fen / 100, 2),
|
||||
o.`used_num` = c.n;
|
||||
|
||||
-- 3.1 存量:订单已回调过加油结果的,其消费流水视为已通知,避免上线后被重推
|
||||
UPDATE `yoshop_qyt_consume` c
|
||||
JOIN `yoshop_partners_order` o ON o.`order_id` = c.`order_id`
|
||||
SET c.`notify_status` = 1,
|
||||
c.`notify_time` = o.`use_callback_time`
|
||||
WHERE o.`use_callback` = 1;
|
||||
|
||||
-- ============================================================
|
||||
-- 回滚:
|
||||
-- ALTER TABLE `yoshop_qyt_consume` DROP INDEX `uk_consume`, ADD UNIQUE KEY `uk_consume` (`waybill_no_code`, `order_code`);
|
||||
-- ALTER TABLE `yoshop_qyt_consume` DROP COLUMN `consume_key`, DROP COLUMN `notify_status`, DROP COLUMN `notify_time`, DROP INDEX `idx_order_notify`;
|
||||
-- ALTER TABLE `yoshop_partners_order` DROP COLUMN `used_amount`, DROP COLUMN `used_num`;
|
||||
-- ============================================================
|
||||
@@ -0,0 +1,162 @@
|
||||
-- ============================================================
|
||||
-- 企油通对接 · 退款 / 发票 / 司机管理
|
||||
-- 生成:2026-09-14
|
||||
-- ------------------------------------------------------------
|
||||
-- 执行顺序:qyt_migration.sql -> _channel -> _enterprise -> _waybill_status -> 本文件
|
||||
-- 本地库执行前请先确认上面四个已执行。
|
||||
-- ============================================================
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 1. 订单退款状态
|
||||
-- refund_status:0 未退款 / 1 退款中 / 2 已退款 / 3 退款失败(可重新发起) / 4 待人工确认
|
||||
-- 4 的含义:7.16 请求已发出但结果未知(超时/响应异常)。7.16 没有幂等键,
|
||||
-- 这种情况绝不能自动重试,必须先确认上游到底扣没扣,否则可能重复调低额度。
|
||||
-- ------------------------------------------------------------
|
||||
ALTER TABLE `yoshop_partners_order`
|
||||
ADD COLUMN `refund_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '退款状态 0未退款 1退款中 2已退款 3退款失败 4待人工确认' AFTER `qyt_retry_num`,
|
||||
ADD COLUMN `refund_amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '已退款金额(元)' AFTER `refund_status`,
|
||||
ADD COLUMN `refund_time` int(11) NOT NULL DEFAULT 0 COMMENT '退款成功时间' AFTER `refund_amount`,
|
||||
ADD COLUMN `refund_text` varchar(255) NOT NULL DEFAULT '' COMMENT '退款结果说明' AFTER `refund_time`,
|
||||
ADD INDEX `idx_refund_status` (`refund_status`),
|
||||
ADD INDEX `idx_id_card` (`id_card`);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 2. 退款流水(每次发起一行,失败后重新发起会新增一行)
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE `yoshop_qyt_refund` (
|
||||
`refund_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`refund_no` varchar(64) NOT NULL DEFAULT '' COMMENT '退款单号(同时写入7.16 operation_reason,确认时据此在7.13流水里找)',
|
||||
`order_id` int(11) NOT NULL DEFAULT 0 COMMENT 'yoshop_partners_order.order_id',
|
||||
`partners_id` int(11) NOT NULL DEFAULT 0 COMMENT '合作商ID',
|
||||
`qyt_enterprise_id` int(11) NOT NULL DEFAULT 0 COMMENT '企油通企业ID',
|
||||
`source` varchar(10) NOT NULL DEFAULT '' COMMENT '发起来源 admin后台 / api下单平台',
|
||||
`operator` varchar(64) NOT NULL DEFAULT '' COMMENT '操作人(后台账号 / 合作商appKey)',
|
||||
`reason` varchar(200) NOT NULL DEFAULT '' COMMENT '退款原因',
|
||||
`amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '退款金额(元)',
|
||||
`quota_id` varchar(32) NOT NULL DEFAULT '' COMMENT '被调低的凭证ID(7.17 quota_id)',
|
||||
`total_before` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '调额前凭证总额度(元)',
|
||||
`available_before` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '调额前凭证可用余额(元)',
|
||||
`total_after` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '确认时凭证总额度(元)',
|
||||
`available_after` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '确认时凭证可用余额(元)',
|
||||
`status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '1处理中 2成功 3失败 4待人工确认',
|
||||
`message` varchar(255) NOT NULL DEFAULT '' COMMENT '结果说明',
|
||||
`response` text COMMENT '上游响应(已脱敏)',
|
||||
`store_id` int(11) NOT NULL DEFAULT 0 COMMENT '商城ID',
|
||||
`create_time` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`update_time` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
PRIMARY KEY (`refund_id`),
|
||||
UNIQUE KEY `uk_refund_no` (`refund_no`),
|
||||
KEY `idx_order` (`order_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='企油通退款流水(7.17查凭证+7.16调低额度)';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 3. 企业发票(7.14)
|
||||
-- 7.14 的粒度是「企业 + 通道 + 月份(+ 司机手机号)」,返回的发票地址与订单
|
||||
-- 之间没有对应关系,因此按这个维度存;订单按「企业 + 消费月份 + 司机」关联展示。
|
||||
-- mobile 为空串表示该企业该月全量。
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE `yoshop_qyt_invoice` (
|
||||
`invoice_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`qyt_enterprise_id` int(11) NOT NULL DEFAULT 0 COMMENT '企油通企业ID',
|
||||
`business_license` varchar(64) NOT NULL DEFAULT '' COMMENT '统一社会信用代码',
|
||||
`passageway` varchar(20) NOT NULL DEFAULT '' COMMENT '通道 alipay/pingan',
|
||||
`month` char(7) NOT NULL DEFAULT '' COMMENT '月份 YYYY-MM',
|
||||
`mobile` varchar(20) NOT NULL DEFAULT '' COMMENT '司机手机号,空=企业全量',
|
||||
`invoice_urls` text COMMENT '发票地址(JSON数组)',
|
||||
`invoice_count` int(11) NOT NULL DEFAULT 0 COMMENT '发票数量',
|
||||
`order_count` int(11) NOT NULL DEFAULT 0 COMMENT '该维度加油明细条数',
|
||||
`raw` mediumtext COMMENT '7.14 原始返回(已脱敏)',
|
||||
`sync_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '同步状态 0未同步 1成功 2失败',
|
||||
`sync_time` int(11) NOT NULL DEFAULT 0 COMMENT '最近同步时间',
|
||||
`sync_text` varchar(255) NOT NULL DEFAULT '' COMMENT '最近同步说明',
|
||||
`store_id` int(11) NOT NULL DEFAULT 0 COMMENT '商城ID',
|
||||
`create_time` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`update_time` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
PRIMARY KEY (`invoice_id`),
|
||||
UNIQUE KEY `uk_invoice` (`qyt_enterprise_id`, `passageway`, `month`, `mobile`),
|
||||
KEY `idx_month` (`month`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='企油通企业发票(7.14)';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 4. 司机档案
|
||||
-- 按身份证号唯一。累计发放/退款/订单数不存冗余字段,查询时从订单实时汇总,避免对不上账。
|
||||
-- conflict 用来提前发现会导致上游二要素核验失败的数据:
|
||||
-- 同一身份证号出现不同姓名 / 同一手机号对应多个身份证号 / 手机号变更
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE `yoshop_qyt_driver` (
|
||||
`driver_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`id_card` varchar(32) NOT NULL DEFAULT '' COMMENT '身份证号',
|
||||
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '姓名(最近一次下单)',
|
||||
`mobile` varchar(20) NOT NULL DEFAULT '' COMMENT '手机号(最近一次下单)',
|
||||
`conflict` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否存在信息冲突',
|
||||
`conflict_text` varchar(255) NOT NULL DEFAULT '' COMMENT '冲突说明',
|
||||
`last_order_time` int(11) NOT NULL DEFAULT 0 COMMENT '最近下单时间',
|
||||
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
|
||||
`store_id` int(11) NOT NULL DEFAULT 0 COMMENT '商城ID',
|
||||
`create_time` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`update_time` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
PRIMARY KEY (`driver_id`),
|
||||
UNIQUE KEY `uk_id_card` (`store_id`, `id_card`),
|
||||
KEY `idx_mobile` (`mobile`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='企油通司机档案';
|
||||
|
||||
-- 4.1 用存量订单回填司机档案(姓名/手机号取最近一单)
|
||||
INSERT IGNORE INTO `yoshop_qyt_driver` (`id_card`, `name`, `mobile`, `conflict`, `conflict_text`, `last_order_time`, `store_id`, `create_time`, `update_time`)
|
||||
SELECT
|
||||
UPPER(o.`id_card`),
|
||||
SUBSTRING_INDEX(GROUP_CONCAT(o.`driver_name` ORDER BY o.`create_time` DESC SEPARATOR '\n'), '\n', 1),
|
||||
SUBSTRING_INDEX(GROUP_CONCAT(o.`mobile` ORDER BY o.`create_time` DESC SEPARATOR '\n'), '\n', 1),
|
||||
IF(COUNT(DISTINCT o.`driver_name`) > 1, 1, 0),
|
||||
IF(COUNT(DISTINCT o.`driver_name`) > 1, '同一身份证号出现不同姓名(存量订单回填)', ''),
|
||||
MAX(o.`create_time`),
|
||||
o.`store_id`,
|
||||
UNIX_TIMESTAMP(),
|
||||
UNIX_TIMESTAMP()
|
||||
FROM `yoshop_partners_order` o
|
||||
WHERE o.`id_card` <> ''
|
||||
GROUP BY o.`store_id`, UPPER(o.`id_card`);
|
||||
|
||||
-- 4.2 标记「同一手机号对应多个身份证号」
|
||||
UPDATE `yoshop_qyt_driver` d
|
||||
JOIN (
|
||||
SELECT `store_id`, `mobile` FROM `yoshop_qyt_driver`
|
||||
WHERE `mobile` <> '' GROUP BY `store_id`, `mobile` HAVING COUNT(*) > 1
|
||||
) m ON m.`store_id` = d.`store_id` AND m.`mobile` = d.`mobile`
|
||||
SET d.`conflict` = 1,
|
||||
d.`conflict_text` = IF(d.`conflict_text` = '', '同一手机号对应多个身份证号', CONCAT(d.`conflict_text`, ';同一手机号对应多个身份证号'));
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 5. 后台菜单(挂在「合作商管理」下;幂等,可重复执行)
|
||||
-- 注意:部分环境的菜单表里根本没有「合作商管理」(/partners) 这条父级
|
||||
-- (超管拥有全部路由,不依赖菜单表,所以之前一直没暴露),先补父级再挂子菜单,
|
||||
-- 否则子查询取不到 menu_id,会报 #1048 Column 'parent_id' cannot be null。
|
||||
-- ------------------------------------------------------------
|
||||
INSERT INTO `yoshop_store_menu` (`module`, `name`, `path`, `action_mark`, `parent_id`, `sort`, `create_time`, `update_time`)
|
||||
SELECT 10, '合作商管理', '/partners', '', 0, 100, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM (SELECT 1 FROM `yoshop_store_menu` WHERE `path` = '/partners' LIMIT 1) x);
|
||||
|
||||
INSERT INTO `yoshop_store_menu` (`module`, `name`, `path`, `action_mark`, `parent_id`, `sort`, `create_time`, `update_time`)
|
||||
SELECT 10, '司机管理', '/qyt/driver/index', '',
|
||||
(SELECT `menu_id` FROM (SELECT `menu_id` FROM `yoshop_store_menu` WHERE `path` = '/partners' LIMIT 1) t),
|
||||
100, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM (SELECT 1 FROM `yoshop_store_menu` WHERE `path` = '/qyt/driver/index' LIMIT 1) x);
|
||||
|
||||
INSERT INTO `yoshop_store_menu` (`module`, `name`, `path`, `action_mark`, `parent_id`, `sort`, `create_time`, `update_time`)
|
||||
SELECT 10, '发票管理', '/qyt/invoice/index', '',
|
||||
(SELECT `menu_id` FROM (SELECT `menu_id` FROM `yoshop_store_menu` WHERE `path` = '/partners' LIMIT 1) t),
|
||||
100, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM (SELECT 1 FROM `yoshop_store_menu` WHERE `path` = '/qyt/invoice/index' LIMIT 1) x);
|
||||
|
||||
-- ============================================================
|
||||
-- 回滚:
|
||||
-- ALTER TABLE `yoshop_partners_order`
|
||||
-- DROP COLUMN `refund_status`, DROP COLUMN `refund_amount`, DROP COLUMN `refund_time`, DROP COLUMN `refund_text`,
|
||||
-- DROP INDEX `idx_refund_status`, DROP INDEX `idx_id_card`;
|
||||
-- DROP TABLE `yoshop_qyt_refund`;
|
||||
-- DROP TABLE `yoshop_qyt_invoice`;
|
||||
-- DROP TABLE `yoshop_qyt_driver`;
|
||||
-- DELETE FROM `yoshop_store_menu` WHERE `path` IN ('/qyt/driver/index', '/qyt/invoice/index');
|
||||
-- ============================================================
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user