通知日志筛选 + 支付推送按订单所在库处理
后台 - 通知日志页新增筛选:处理结果(成功/失败/已忽略)+ 关键词(事件名、订单号、原始报文) 服务端 - 通知日志接口支持 processed / keyword 过滤 - 支付推送改为「先查生产库、再查开发库」,命中的库即后续写操作的库; 原先只查小程序当前数据源,数据源一被切换,真实推送就查不到订单 - 去掉「开发库不能退款」的拦截:开发库的单是微信沙箱里真下单产生的,可以正常退
This commit is contained in:
1 parent
f05a11f0f3
commit
058d677fc1
8 files changed
+161
-49
No files matched your search
@@ -1,7 +1,5 @@
|
|||||||
{
|
{
|
||||||
"usingComponents": {
|
"usingComponents": {},
|
||||||
"van-loading": "@vant/weapp/loading/index"
|
|
||||||
},
|
|
||||||
"onReachBottomDistance":50,
|
"onReachBottomDistance":50,
|
||||||
"enablePullDownRefresh":true
|
"enablePullDownRefresh":true
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
{
|
{
|
||||||
"usingComponents": {
|
"usingComponents": {
|
||||||
"van-dialog": "@vant/weapp/dialog/index",
|
|
||||||
"van-field": "@vant/weapp/field/index",
|
|
||||||
"van-button": "@vant/weapp/button/index",
|
|
||||||
"van-popup": "@vant/weapp/popup/index",
|
"van-popup": "@vant/weapp/popup/index",
|
||||||
"van-image": "@vant/weapp/image/index"
|
"van-image": "@vant/weapp/image/index"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import { formatDateTime } from '@/lib/datetime';
|
import { formatDateTime } from '@/lib/datetime';
|
||||||
import { Button, Card, Empty, Notice } from '@/components/ui';
|
import { Button, Card, Empty, Input, Notice, Select } from '@/components/ui';
|
||||||
|
|
||||||
interface NotifyLogRow {
|
interface NotifyLogRow {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -29,22 +29,35 @@ export default function NotifyLogsPage() {
|
|||||||
const [items, setItems] = useState<NotifyLogRow[]>([]);
|
const [items, setItems] = useState<NotifyLogRow[]>([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
/** '-1' = 全部结果 */
|
||||||
|
const [processedFilter, setProcessedFilter] = useState('-1');
|
||||||
|
const [keyword, setKeyword] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
const load = useCallback((nextPage: number) => {
|
const load = useCallback(
|
||||||
|
(nextPage: number, nextKeyword: string) => {
|
||||||
setError('');
|
setError('');
|
||||||
api
|
api
|
||||||
.listNotifyLogs(nextPage, PAGE_SIZE)
|
.listNotifyLogs({
|
||||||
|
page: nextPage,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
processed: processedFilter === '-1' ? undefined : Number(processedFilter),
|
||||||
|
keyword: nextKeyword,
|
||||||
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setItems(res.items as unknown as NotifyLogRow[]);
|
setItems(res.items as unknown as NotifyLogRow[]);
|
||||||
setTotal(res.total);
|
setTotal(res.total);
|
||||||
setPage(res.page);
|
setPage(res.page);
|
||||||
})
|
})
|
||||||
.catch((err: { message: string }) => setError(err.message));
|
.catch((err: { message: string }) => setError(err.message));
|
||||||
}, []);
|
},
|
||||||
|
[processedFilter],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 筛选一变,load 的引用就变,这里会带着当前关键词回到第 1 页重拉
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load(1);
|
load(1, keyword);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
||||||
@@ -60,7 +73,29 @@ export default function NotifyLogsPage() {
|
|||||||
<Notice message={error} />
|
<Notice message={error} />
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<div className="mb-4 text-sm text-gray-500">共 {total} 条通知</div>
|
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div className="text-sm text-gray-500">共 {total} 条通知</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-32">
|
||||||
|
<Select
|
||||||
|
value={processedFilter}
|
||||||
|
onChange={setProcessedFilter}
|
||||||
|
options={[
|
||||||
|
{ value: -1, label: '全部结果' },
|
||||||
|
{ value: 1, label: '成功' },
|
||||||
|
{ value: 2, label: '失败' },
|
||||||
|
{ value: 0, label: '已忽略' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-60">
|
||||||
|
<Input value={keyword} onChange={setKeyword} placeholder="事件名 / 订单号 / 说明…" />
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" onClick={() => load(1, keyword)}>
|
||||||
|
搜索
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{items.length === 0 ? (
|
{items.length === 0 ? (
|
||||||
<Empty />
|
<Empty />
|
||||||
@@ -110,10 +145,10 @@ export default function NotifyLogsPage() {
|
|||||||
第 {page} / {totalPages} 页
|
第 {page} / {totalPages} 页
|
||||||
</span>
|
</span>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button variant="ghost" disabled={page <= 1} onClick={() => load(page - 1)}>
|
<Button variant="ghost" disabled={page <= 1} onClick={() => load(page - 1, keyword)}>
|
||||||
上一页
|
上一页
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" disabled={page >= totalPages} onClick={() => load(page + 1)}>
|
<Button variant="ghost" disabled={page >= totalPages} onClick={() => load(page + 1, keyword)}>
|
||||||
下一页
|
下一页
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -161,8 +161,9 @@ export const api = {
|
|||||||
listRefundRequests: (page: number, pageSize: number) =>
|
listRefundRequests: (page: number, pageSize: number) =>
|
||||||
request<Paged<Record<string, unknown>>>(`/admin/v1/refund-requests${qs({ page, pageSize })}`),
|
request<Paged<Record<string, unknown>>>(`/admin/v1/refund-requests${qs({ page, pageSize })}`),
|
||||||
|
|
||||||
listNotifyLogs: (page: number, pageSize: number) =>
|
/** 通知日志。processed:0 已忽略 / 1 成功 / 2 失败;keyword 匹配事件名、说明和原始报文。 */
|
||||||
request<Paged<Record<string, unknown>>>(`/admin/v1/notify-logs${qs({ page, pageSize })}`),
|
listNotifyLogs: (params: { page: number; pageSize: number; processed?: number; keyword?: string }) =>
|
||||||
|
request<Paged<Record<string, unknown>>>(`/admin/v1/notify-logs${qs({ ...params })}`),
|
||||||
|
|
||||||
/** 站点级信息:对外域名。后台只读展示,也用来拼封面图地址 */
|
/** 站点级信息:对外域名。后台只读展示,也用来拼封面图地址 */
|
||||||
siteInfo: () =>
|
siteInfo: () =>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"fileNames":["./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es5.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.object.d.ts",".Line truncated
|
{"fileNames":["./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es5.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.object.d.ts",".Line truncated
|
||||||
@@ -217,8 +217,16 @@ export class AdminController {
|
|||||||
@AdminEnvParam() env: DataSourceEnv,
|
@AdminEnvParam() env: DataSourceEnv,
|
||||||
@Query('page') page?: string,
|
@Query('page') page?: string,
|
||||||
@Query('pageSize') pageSize?: string,
|
@Query('pageSize') pageSize?: string,
|
||||||
|
@Query('processed') processed?: string,
|
||||||
|
@Query('keyword') keyword?: string,
|
||||||
) {
|
) {
|
||||||
return this.admin.listNotifyLogs(env, Number(page) || 1, Number(pageSize) || 20);
|
// 空字符串要当成「没传」——Number('') 是 0,会被误当成「只见已忽略」
|
||||||
|
const statusText = processed ?? '';
|
||||||
|
const status = statusText === '' ? undefined : Number(statusText);
|
||||||
|
return this.admin.listNotifyLogs(env, Number(page) || 1, Number(pageSize) || 20, {
|
||||||
|
processed: status !== undefined && Number.isFinite(status) ? status : undefined,
|
||||||
|
keyword: keyword ?? '',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 后台需要的站点级信息(静态资源域名等),不分数据源。 */
|
/** 后台需要的站点级信息(静态资源域名等),不分数据源。 */
|
||||||
|
|||||||
@@ -421,18 +421,39 @@ export class AdminService {
|
|||||||
* 通知日志:微信发来的每一条推送(发货、退款问询、退款成功、投诉、风控…)。
|
* 通知日志:微信发来的每一条推送(发货、退款问询、退款成功、投诉、风控…)。
|
||||||
* 用来回答「这条通知到底来没来、我们怎么处理的」。
|
* 用来回答「这条通知到底来没来、我们怎么处理的」。
|
||||||
*/
|
*/
|
||||||
async listNotifyLogs(env: DataSourceEnv, page = 1, pageSize = 20) {
|
async listNotifyLogs(
|
||||||
|
env: DataSourceEnv,
|
||||||
|
page = 1,
|
||||||
|
pageSize = 20,
|
||||||
|
filters: { processed?: number; keyword?: string } = {},
|
||||||
|
) {
|
||||||
const db = this.prisma.clientFor(env);
|
const db = this.prisma.clientFor(env);
|
||||||
const size = Math.min(Math.max(pageSize, 1), 100);
|
const size = Math.min(Math.max(pageSize, 1), 100);
|
||||||
const current = Math.max(page, 1);
|
const current = Math.max(page, 1);
|
||||||
|
const keyword = (filters.keyword ?? '').trim();
|
||||||
|
|
||||||
|
// 关键词同时搜事件名、说明和原始报文——想找某笔订单的通知,直接输订单号就行
|
||||||
|
const where: Prisma.PaymentNotifyLogWhereInput = {
|
||||||
|
...(filters.processed === undefined ? {} : { processed: filters.processed }),
|
||||||
|
...(keyword
|
||||||
|
? {
|
||||||
|
OR: [
|
||||||
|
{ event: { contains: keyword } },
|
||||||
|
{ err_msg: { contains: keyword } },
|
||||||
|
{ raw_body: { contains: keyword } },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
|
||||||
const [items, total] = await db.$transaction([
|
const [items, total] = await db.$transaction([
|
||||||
db.paymentNotifyLog.findMany({
|
db.paymentNotifyLog.findMany({
|
||||||
|
where,
|
||||||
orderBy: { id: 'desc' },
|
orderBy: { id: 'desc' },
|
||||||
skip: (current - 1) * size,
|
skip: (current - 1) * size,
|
||||||
take: size,
|
take: size,
|
||||||
}),
|
}),
|
||||||
db.paymentNotifyLog.count(),
|
db.paymentNotifyLog.count({ where }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -538,13 +559,14 @@ export class AdminService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发起退款(仅非 iOS 终端)。退款成功由退款推送回写订单状态。 */
|
/**
|
||||||
|
* 发起退款(仅非 iOS 终端)。退款成功由退款推送回写订单状态。
|
||||||
|
*
|
||||||
|
* 开发库的订单也是在微信**沙箱**里真下单产生的(env=1、有平台单号),所以一样能退:
|
||||||
|
* 用沙箱密钥 + 沙箱环境去查单退款,不涉及真钱。
|
||||||
|
* 安全边界靠下面的 env 一致性校验——它挡住「拿现网配置去退沙箱单」这类错配。
|
||||||
|
*/
|
||||||
async refundOrder(env: DataSourceEnv, orderId: number, reason: string, reasonCode: string) {
|
async refundOrder(env: DataSourceEnv, orderId: number, reason: string, reasonCode: string) {
|
||||||
// 开发库的订单在微信侧不存在,发退款请求必然失败,直接拦住
|
|
||||||
if (env === 'develop') {
|
|
||||||
throw AppError.badRequest('当前数据源是开发库,订单在微信侧不存在,无法退款');
|
|
||||||
}
|
|
||||||
|
|
||||||
const db = this.prisma.clientFor(env);
|
const db = this.prisma.clientFor(env);
|
||||||
const order = await db.order.findUnique({ where: { id: orderId }, include: { user: true } });
|
const order = await db.order.findUnique({ where: { id: orderId }, include: { user: true } });
|
||||||
if (!order) throw AppError.notFound('订单不存在');
|
if (!order) throw AppError.notFound('订单不存在');
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import type { PrismaClient } from '@prisma/client';
|
||||||
import { XMLParser } from 'fast-xml-parser';
|
import { XMLParser } from 'fast-xml-parser';
|
||||||
import { APP_CONFIG, type AppConfig } from '../../config/config';
|
import { APP_CONFIG, type AppConfig } from '../../config/config';
|
||||||
import { decryptSecret } from '../../common/crypto';
|
import { decryptSecret } from '../../common/crypto';
|
||||||
@@ -80,6 +81,36 @@ export class PaymentsService {
|
|||||||
private readonly wxpay: WxPayService,
|
private readonly wxpay: WxPayService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信推送里只有单号,没有「这笔订单在哪个库」的信息——订单落在哪个库,
|
||||||
|
* 取决于**下单时**的小程序数据源。所以按单号找订单要两个库都试:
|
||||||
|
* 先当前用的那个,再另一个。
|
||||||
|
*
|
||||||
|
* 只查当前库的后果:数据源一被切换,真实的发货/退款推送就"查不到订单",
|
||||||
|
* 返回非 0 后微信还会重试 15 次,通知日志里刷一屏失败。
|
||||||
|
*/
|
||||||
|
private databases(): PrismaClient[] {
|
||||||
|
// 先生产库:绝大多数订单都在那里,命中就少一次查询。
|
||||||
|
// 开发库只有「下单时小程序刚好切到开发库」才会有单,放后面查。
|
||||||
|
return this.prisma.dev ? [this.prisma.main, this.prisma.dev] : [this.prisma.main];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 两个库里按单号找订单,返回命中的连接和订单。 */
|
||||||
|
private async findOrder<T>(
|
||||||
|
lookup: (db: PrismaClient) => Promise<T | null>,
|
||||||
|
): Promise<{ db: PrismaClient; order: T } | null> {
|
||||||
|
for (const db of this.databases()) {
|
||||||
|
const order = await lookup(db);
|
||||||
|
if (order) return { db, order };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理结果里写上订单所在库,数据源切换时便于排查。 */
|
||||||
|
private dbLabel(db: PrismaClient): string {
|
||||||
|
return db === this.prisma.main ? '生产库' : '开发库';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理发货推送。
|
* 处理发货推送。
|
||||||
*
|
*
|
||||||
@@ -112,11 +143,12 @@ export class PaymentsService {
|
|||||||
notify.WeChatPayInfo?.TransactionId ||
|
notify.WeChatPayInfo?.TransactionId ||
|
||||||
`local:${outTradeNo}`;
|
`local:${outTradeNo}`;
|
||||||
|
|
||||||
const order = await this.prisma.active.order.findUnique({
|
// 推送只带单号,不告诉我们订单在哪个库:两个库都找一遍
|
||||||
where: { out_trade_no: outTradeNo },
|
const hit = await this.findOrder((db) =>
|
||||||
include: { user: true },
|
db.order.findUnique({ where: { out_trade_no: outTradeNo }, include: { user: true } }),
|
||||||
});
|
);
|
||||||
if (!order) throw new Error(`订单不存在:${outTradeNo}`);
|
if (!hit) throw new Error(`两个库都找不到订单:${outTradeNo}`);
|
||||||
|
const { db, order } = hit;
|
||||||
if (order.user.openid !== openid) throw new Error(`推送 openid 与订单不符:${outTradeNo}`);
|
if (order.user.openid !== openid) throw new Error(`推送 openid 与订单不符:${outTradeNo}`);
|
||||||
|
|
||||||
if (order.status === OrderStatus.DELIVERED || order.status === OrderStatus.REFUNDED) {
|
if (order.status === OrderStatus.DELIVERED || order.status === OrderStatus.REFUNDED) {
|
||||||
@@ -124,9 +156,10 @@ export class PaymentsService {
|
|||||||
return this.okXml();
|
return this.okXml();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.grantEntitlement(order.user_id, order.course_id, order.id, order.out_trade_no, wxOrderId);
|
await this.grantEntitlement(db, order.user_id, order.course_id, order.id, order.out_trade_no, wxOrderId);
|
||||||
await this.markNotify(logId, 1, '发货成功');
|
const label = this.dbLabel(db);
|
||||||
this.logger.log(`发货成功 order=${outTradeNo} wx=${wxOrderId}`);
|
await this.markNotify(logId, 1, db === this.prisma.active ? '发货成功' : `发货成功(订单在${label})`);
|
||||||
|
this.logger.log(`发货成功 order=${outTradeNo} wx=${wxOrderId} db=${label}`);
|
||||||
return this.okXml();
|
return this.okXml();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
@@ -157,8 +190,12 @@ export class PaymentsService {
|
|||||||
return this.okXml();
|
return this.okXml();
|
||||||
}
|
}
|
||||||
|
|
||||||
const order = await this.prisma.active.order.findUnique({ where: { out_trade_no: outTradeNo } });
|
// 和发货推送一样:订单可能在另一个库(下单时数据源不同),两个库都找
|
||||||
if (!order) throw new Error(`退款推送找不到订单:${outTradeNo}`);
|
const hit = await this.findOrder((db) =>
|
||||||
|
db.order.findUnique({ where: { out_trade_no: outTradeNo } }),
|
||||||
|
);
|
||||||
|
if (!hit) throw new Error(`两个库都找不到订单:${outTradeNo}`);
|
||||||
|
const { db, order } = hit;
|
||||||
if (order.status === OrderStatus.REFUNDED) {
|
if (order.status === OrderStatus.REFUNDED) {
|
||||||
await this.markNotify(logId, 1, '重复推送,已忽略');
|
await this.markNotify(logId, 1, '重复推送,已忽略');
|
||||||
return this.okXml();
|
return this.okXml();
|
||||||
@@ -169,12 +206,12 @@ export class PaymentsService {
|
|||||||
// 动不到老数据迁移来的 source=2 权益。两个写操作必须同事务。
|
// 动不到老数据迁移来的 source=2 权益。两个写操作必须同事务。
|
||||||
// 已知上限:已经下发出去的播放地址在签名有效期内(默认 30 分钟)仍可播放,
|
// 已知上限:已经下发出去的播放地址在签名有效期内(默认 30 分钟)仍可播放,
|
||||||
// 换密钥或缩短 PLAY_URL_TTL_SECONDS 才能做得更严。
|
// 换密钥或缩短 PLAY_URL_TTL_SECONDS 才能做得更严。
|
||||||
await this.prisma.active.$transaction([
|
await db.$transaction([
|
||||||
this.prisma.active.order.update({
|
db.order.update({
|
||||||
where: { id: order.id },
|
where: { id: order.id },
|
||||||
data: { status: OrderStatus.REFUNDED, refunded_at: new Date() },
|
data: { status: OrderStatus.REFUNDED, refunded_at: new Date() },
|
||||||
}),
|
}),
|
||||||
this.prisma.active.entitlement.deleteMany({
|
db.entitlement.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
user_id: order.user_id,
|
user_id: order.user_id,
|
||||||
course_id: order.course_id,
|
course_id: order.course_id,
|
||||||
@@ -184,8 +221,14 @@ export class PaymentsService {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await this.markNotify(logId, 1, `退款成功并收回权益 ${notify.WxRefundId ?? ''} ${notify.RefundFee ?? ''}分`);
|
const label = this.dbLabel(db);
|
||||||
this.logger.log(`退款已记录 order=${outTradeNo} refund=${notify.WxRefundId ?? '-'}`);
|
const where = db === this.prisma.active ? '' : `(订单在${label})`;
|
||||||
|
await this.markNotify(
|
||||||
|
logId,
|
||||||
|
1,
|
||||||
|
`退款成功并收回权益 ${notify.WxRefundId ?? ''} ${notify.RefundFee ?? ''}分${where}`,
|
||||||
|
);
|
||||||
|
this.logger.log(`退款已记录 order=${outTradeNo} refund=${notify.WxRefundId ?? '-'} db=${label}`);
|
||||||
return this.okXml();
|
return this.okXml();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
@@ -225,13 +268,19 @@ export class PaymentsService {
|
|||||||
const provideStatus = fieldOf(rawBody, 'provide_status');
|
const provideStatus = fieldOf(rawBody, 'provide_status');
|
||||||
const reason = fieldOf(rawBody, 'refund_request_reason');
|
const reason = fieldOf(rawBody, 'refund_request_reason');
|
||||||
|
|
||||||
// pay_order_id 是「支付订单号」,我们自己的 out_trade_no 和平台单号都试一遍
|
// pay_order_id 是「支付订单号」,我们自己的 out_trade_no 和平台单号都试一遍;
|
||||||
const order = payOrderId
|
// 同样两个库都找(数据源切换时订单可能在另一个库)
|
||||||
? await this.prisma.active.order.findFirst({
|
const hit = payOrderId
|
||||||
|
? await this.findOrder((db) =>
|
||||||
|
db.order.findFirst({
|
||||||
where: { OR: [{ out_trade_no: payOrderId }, { wx_order_id: payOrderId }] },
|
where: { OR: [{ out_trade_no: payOrderId }, { wx_order_id: payOrderId }] },
|
||||||
select: { id: true, out_trade_no: true, status: true, refund_consent_at: true },
|
select: { id: true, out_trade_no: true, status: true, refund_consent_at: true },
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
: null;
|
: null;
|
||||||
|
const order = hit?.order ?? null;
|
||||||
|
/** 退款申请记在订单所在的库;没找到订单就记在当前库 */
|
||||||
|
const recordDb = hit?.db ?? this.prisma.active;
|
||||||
|
|
||||||
// 查得到订单就信自己的状态;查不到才退回用推送里的发货状态
|
// 查得到订单就信自己的状态;查不到才退回用推送里的发货状态
|
||||||
const delivered = order ? order.status === OrderStatus.DELIVERED : provideStatus === '1';
|
const delivered = order ? order.status === OrderStatus.DELIVERED : provideStatus === '1';
|
||||||
@@ -255,7 +304,7 @@ export class PaymentsService {
|
|||||||
our_result: resultCode,
|
our_result: resultCode,
|
||||||
our_evidence: evidence.slice(0, 255),
|
our_evidence: evidence.slice(0, 255),
|
||||||
};
|
};
|
||||||
await this.prisma.active.refundRequest.upsert({
|
await recordDb.refundRequest.upsert({
|
||||||
where: { pay_order_id: payOrderId },
|
where: { pay_order_id: payOrderId },
|
||||||
create: { pay_order_id: payOrderId, ...record },
|
create: { pay_order_id: payOrderId, ...record },
|
||||||
update: { ...record, query_count: { increment: 1 } },
|
update: { ...record, query_count: { increment: 1 } },
|
||||||
@@ -328,6 +377,7 @@ export class PaymentsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.grantEntitlement(
|
await this.grantEntitlement(
|
||||||
|
this.prisma.active,
|
||||||
order.user_id,
|
order.user_id,
|
||||||
order.course_id,
|
order.course_id,
|
||||||
order.id,
|
order.id,
|
||||||
@@ -400,6 +450,7 @@ export class PaymentsService {
|
|||||||
|
|
||||||
/** 标记订单已发货并发放权益。两个写操作必须同事务。 */
|
/** 标记订单已发货并发放权益。两个写操作必须同事务。 */
|
||||||
private async grantEntitlement(
|
private async grantEntitlement(
|
||||||
|
db: PrismaClient,
|
||||||
userId: number,
|
userId: number,
|
||||||
courseId: number,
|
courseId: number,
|
||||||
orderId: number,
|
orderId: number,
|
||||||
@@ -407,8 +458,8 @@ export class PaymentsService {
|
|||||||
wxOrderId: string,
|
wxOrderId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
await this.prisma.active.$transaction([
|
await db.$transaction([
|
||||||
this.prisma.active.order.update({
|
db.order.update({
|
||||||
where: { id: orderId },
|
where: { id: orderId },
|
||||||
data: {
|
data: {
|
||||||
status: OrderStatus.DELIVERED,
|
status: OrderStatus.DELIVERED,
|
||||||
@@ -417,7 +468,7 @@ export class PaymentsService {
|
|||||||
delivered_at: now,
|
delivered_at: now,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
this.prisma.active.entitlement.upsert({
|
db.entitlement.upsert({
|
||||||
where: { user_id_course_id: { user_id: userId, course_id: courseId } },
|
where: { user_id_course_id: { user_id: userId, course_id: courseId } },
|
||||||
create: {
|
create: {
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
|
|||||||
Reference in new issue
Block a user