Merge branch 'yongpxu-dev' into yongpxu-dev4
1
.gitignore
vendored
@@ -9,3 +9,4 @@ Store_vue/.specstory/
|
|||||||
Store_vue/unpackage/
|
Store_vue/unpackage/
|
||||||
Store_vue/.vscode/
|
Store_vue/.vscode/
|
||||||
SuperAdmin/.specstory/
|
SuperAdmin/.specstory/
|
||||||
|
Cunkebao/dist
|
||||||
|
|||||||
1
Cunkebao/.env.local
Normal file
@@ -0,0 +1 @@
|
|||||||
|
NEXT_PUBLIC_API_BASE_URL= http://yishi.com
|
||||||
50
Cunkebao/dist/.vite/manifest.json
vendored
@@ -1,50 +0,0 @@
|
|||||||
{
|
|
||||||
"_charts-B449e2xS.js": {
|
|
||||||
"file": "assets/charts-B449e2xS.js",
|
|
||||||
"name": "charts",
|
|
||||||
"imports": [
|
|
||||||
"_ui-DDu9FCjt.js",
|
|
||||||
"_vendor-0WYR1k4q.js"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"_ui-D0C0OGrH.css": {
|
|
||||||
"file": "assets/ui-D0C0OGrH.css",
|
|
||||||
"src": "_ui-D0C0OGrH.css"
|
|
||||||
},
|
|
||||||
"_ui-DDu9FCjt.js": {
|
|
||||||
"file": "assets/ui-DDu9FCjt.js",
|
|
||||||
"name": "ui",
|
|
||||||
"imports": [
|
|
||||||
"_vendor-0WYR1k4q.js"
|
|
||||||
],
|
|
||||||
"css": [
|
|
||||||
"assets/ui-D0C0OGrH.css"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"_utils-DC06x9DY.js": {
|
|
||||||
"file": "assets/utils-DC06x9DY.js",
|
|
||||||
"name": "utils",
|
|
||||||
"imports": [
|
|
||||||
"_vendor-0WYR1k4q.js"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"_vendor-0WYR1k4q.js": {
|
|
||||||
"file": "assets/vendor-0WYR1k4q.js",
|
|
||||||
"name": "vendor"
|
|
||||||
},
|
|
||||||
"index.html": {
|
|
||||||
"file": "assets/index-DzNmnMYg.js",
|
|
||||||
"name": "index",
|
|
||||||
"src": "index.html",
|
|
||||||
"isEntry": true,
|
|
||||||
"imports": [
|
|
||||||
"_vendor-0WYR1k4q.js",
|
|
||||||
"_ui-DDu9FCjt.js",
|
|
||||||
"_utils-DC06x9DY.js",
|
|
||||||
"_charts-B449e2xS.js"
|
|
||||||
],
|
|
||||||
"css": [
|
|
||||||
"assets/index-QrS4Cvyc.css"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
Cunkebao/dist/assets/ui-D0C0OGrH.css
vendored
25
Cunkebao/dist/index.html
vendored
@@ -1,25 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>存客宝</title>
|
|
||||||
<style>
|
|
||||||
html {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<!-- 引入 uni-app web-view SDK(必须) -->
|
|
||||||
<script type="text/javascript" src="/websdk.js"></script>
|
|
||||||
<script type="module" crossorigin src="/assets/index-DzNmnMYg.js"></script>
|
|
||||||
<link rel="modulepreload" crossorigin href="/assets/vendor-0WYR1k4q.js">
|
|
||||||
<link rel="modulepreload" crossorigin href="/assets/ui-DDu9FCjt.js">
|
|
||||||
<link rel="modulepreload" crossorigin href="/assets/utils-DC06x9DY.js">
|
|
||||||
<link rel="modulepreload" crossorigin href="/assets/charts-B449e2xS.js">
|
|
||||||
<link rel="stylesheet" crossorigin href="/assets/ui-D0C0OGrH.css">
|
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-QrS4Cvyc.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
BIN
Cunkebao/dist/logo.png
vendored
|
Before Width: | Height: | Size: 488 KiB |
30
Cunkebao/dist/manifest.json
vendored
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Cunkebao",
|
|
||||||
"short_name": "Cunkebao",
|
|
||||||
"description": "Cunkebao Mobile App",
|
|
||||||
"theme_color": "#ffffff",
|
|
||||||
"background_color": "#ffffff",
|
|
||||||
"display": "standalone",
|
|
||||||
"orientation": "portrait",
|
|
||||||
"scope": "/",
|
|
||||||
"start_url": "/",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "favicon.ico",
|
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
|
||||||
"type": "image/x-icon"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "any maskable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "any maskable"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
308
Cunkebao/dist/websdk.js
vendored
@@ -1,308 +0,0 @@
|
|||||||
!(function (e, n) {
|
|
||||||
"object" == typeof exports && "undefined" != typeof module
|
|
||||||
? (module.exports = n())
|
|
||||||
: "function" == typeof define && define.amd
|
|
||||||
? define(n)
|
|
||||||
: ((e = e || self).uni = n());
|
|
||||||
})(this, function () {
|
|
||||||
"use strict";
|
|
||||||
try {
|
|
||||||
var e = {};
|
|
||||||
(Object.defineProperty(e, "passive", {
|
|
||||||
get: function () {
|
|
||||||
!0;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
window.addEventListener("test-passive", null, e));
|
|
||||||
} catch (e) {}
|
|
||||||
var n = Object.prototype.hasOwnProperty;
|
|
||||||
function i(e, i) {
|
|
||||||
return n.call(e, i);
|
|
||||||
}
|
|
||||||
var t = [];
|
|
||||||
function o() {
|
|
||||||
return window.__dcloud_weex_postMessage || window.__dcloud_weex_;
|
|
||||||
}
|
|
||||||
function a() {
|
|
||||||
return window.__uniapp_x_postMessage || window.__uniapp_x_;
|
|
||||||
}
|
|
||||||
var r = function (e, n) {
|
|
||||||
var i = { options: { timestamp: +new Date() }, name: e, arg: n };
|
|
||||||
if (a()) {
|
|
||||||
if ("postMessage" === e) {
|
|
||||||
var r = { data: n };
|
|
||||||
return window.__uniapp_x_postMessage
|
|
||||||
? window.__uniapp_x_postMessage(r)
|
|
||||||
: window.__uniapp_x_.postMessage(JSON.stringify(r));
|
|
||||||
}
|
|
||||||
var d = {
|
|
||||||
type: "WEB_INVOKE_APPSERVICE",
|
|
||||||
args: { data: i, webviewIds: t },
|
|
||||||
};
|
|
||||||
window.__uniapp_x_postMessage
|
|
||||||
? window.__uniapp_x_postMessageToService(d)
|
|
||||||
: window.__uniapp_x_.postMessageToService(JSON.stringify(d));
|
|
||||||
} else if (o()) {
|
|
||||||
if ("postMessage" === e) {
|
|
||||||
var s = { data: [n] };
|
|
||||||
return window.__dcloud_weex_postMessage
|
|
||||||
? window.__dcloud_weex_postMessage(s)
|
|
||||||
: window.__dcloud_weex_.postMessage(JSON.stringify(s));
|
|
||||||
}
|
|
||||||
var w = {
|
|
||||||
type: "WEB_INVOKE_APPSERVICE",
|
|
||||||
args: { data: i, webviewIds: t },
|
|
||||||
};
|
|
||||||
window.__dcloud_weex_postMessage
|
|
||||||
? window.__dcloud_weex_postMessageToService(w)
|
|
||||||
: window.__dcloud_weex_.postMessageToService(JSON.stringify(w));
|
|
||||||
} else {
|
|
||||||
if (!window.plus)
|
|
||||||
return window.parent.postMessage(
|
|
||||||
{ type: "WEB_INVOKE_APPSERVICE", data: i, pageId: "" },
|
|
||||||
"*",
|
|
||||||
);
|
|
||||||
if (0 === t.length) {
|
|
||||||
var u = plus.webview.currentWebview();
|
|
||||||
if (!u) throw new Error("plus.webview.currentWebview() is undefined");
|
|
||||||
var g = u.parent(),
|
|
||||||
v = "";
|
|
||||||
((v = g ? g.id : u.id), t.push(v));
|
|
||||||
}
|
|
||||||
if (plus.webview.getWebviewById("__uniapp__service"))
|
|
||||||
plus.webview.postMessageToUniNView(
|
|
||||||
{ type: "WEB_INVOKE_APPSERVICE", args: { data: i, webviewIds: t } },
|
|
||||||
"__uniapp__service",
|
|
||||||
);
|
|
||||||
else {
|
|
||||||
var c = JSON.stringify(i);
|
|
||||||
plus.webview
|
|
||||||
.getLaunchWebview()
|
|
||||||
.evalJS(
|
|
||||||
'UniPlusBridge.subscribeHandler("'
|
|
||||||
.concat("WEB_INVOKE_APPSERVICE", '",')
|
|
||||||
.concat(c, ",")
|
|
||||||
.concat(JSON.stringify(t), ");"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
d = {
|
|
||||||
navigateTo: function () {
|
|
||||||
var e =
|
|
||||||
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
|
|
||||||
n = e.url;
|
|
||||||
r("navigateTo", { url: encodeURI(n) });
|
|
||||||
},
|
|
||||||
navigateBack: function () {
|
|
||||||
var e =
|
|
||||||
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
|
|
||||||
n = e.delta;
|
|
||||||
r("navigateBack", { delta: parseInt(n) || 1 });
|
|
||||||
},
|
|
||||||
switchTab: function () {
|
|
||||||
var e =
|
|
||||||
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
|
|
||||||
n = e.url;
|
|
||||||
r("switchTab", { url: encodeURI(n) });
|
|
||||||
},
|
|
||||||
reLaunch: function () {
|
|
||||||
var e =
|
|
||||||
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
|
|
||||||
n = e.url;
|
|
||||||
r("reLaunch", { url: encodeURI(n) });
|
|
||||||
},
|
|
||||||
redirectTo: function () {
|
|
||||||
var e =
|
|
||||||
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
|
|
||||||
n = e.url;
|
|
||||||
r("redirectTo", { url: encodeURI(n) });
|
|
||||||
},
|
|
||||||
getEnv: function (e) {
|
|
||||||
a()
|
|
||||||
? e({ uvue: !0 })
|
|
||||||
: o()
|
|
||||||
? e({ nvue: !0 })
|
|
||||||
: window.plus
|
|
||||||
? e({ plus: !0 })
|
|
||||||
: e({ h5: !0 });
|
|
||||||
},
|
|
||||||
postMessage: function () {
|
|
||||||
var e =
|
|
||||||
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {};
|
|
||||||
r("postMessage", e.data || {});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
s = /uni-app/i.test(navigator.userAgent),
|
|
||||||
w = /Html5Plus/i.test(navigator.userAgent),
|
|
||||||
u = /complete|loaded|interactive/;
|
|
||||||
var g =
|
|
||||||
window.my &&
|
|
||||||
navigator.userAgent.indexOf(
|
|
||||||
["t", "n", "e", "i", "l", "C", "y", "a", "p", "i", "l", "A"]
|
|
||||||
.reverse()
|
|
||||||
.join(""),
|
|
||||||
) > -1;
|
|
||||||
var v =
|
|
||||||
window.swan && window.swan.webView && /swan/i.test(navigator.userAgent);
|
|
||||||
var c =
|
|
||||||
window.qq &&
|
|
||||||
window.qq.miniProgram &&
|
|
||||||
/QQ/i.test(navigator.userAgent) &&
|
|
||||||
/miniProgram/i.test(navigator.userAgent);
|
|
||||||
var p =
|
|
||||||
window.tt &&
|
|
||||||
window.tt.miniProgram &&
|
|
||||||
/toutiaomicroapp/i.test(navigator.userAgent);
|
|
||||||
var _ =
|
|
||||||
window.wx &&
|
|
||||||
window.wx.miniProgram &&
|
|
||||||
/micromessenger/i.test(navigator.userAgent) &&
|
|
||||||
/miniProgram/i.test(navigator.userAgent);
|
|
||||||
var m = window.qa && /quickapp/i.test(navigator.userAgent);
|
|
||||||
var f =
|
|
||||||
window.ks &&
|
|
||||||
window.ks.miniProgram &&
|
|
||||||
/micromessenger/i.test(navigator.userAgent) &&
|
|
||||||
/miniProgram/i.test(navigator.userAgent);
|
|
||||||
var l =
|
|
||||||
window.tt &&
|
|
||||||
window.tt.miniProgram &&
|
|
||||||
/Lark|Feishu/i.test(navigator.userAgent);
|
|
||||||
var E =
|
|
||||||
window.jd && window.jd.miniProgram && /jdmp/i.test(navigator.userAgent);
|
|
||||||
var x =
|
|
||||||
window.xhs &&
|
|
||||||
window.xhs.miniProgram &&
|
|
||||||
/xhsminiapp/i.test(navigator.userAgent);
|
|
||||||
for (
|
|
||||||
var S,
|
|
||||||
h = function () {
|
|
||||||
((window.UniAppJSBridge = !0),
|
|
||||||
document.dispatchEvent(
|
|
||||||
new CustomEvent("UniAppJSBridgeReady", {
|
|
||||||
bubbles: !0,
|
|
||||||
cancelable: !0,
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
},
|
|
||||||
y = [
|
|
||||||
function (e) {
|
|
||||||
if (s || w)
|
|
||||||
return (
|
|
||||||
window.__uniapp_x_postMessage ||
|
|
||||||
window.__uniapp_x_ ||
|
|
||||||
window.__dcloud_weex_postMessage ||
|
|
||||||
window.__dcloud_weex_
|
|
||||||
? document.addEventListener("DOMContentLoaded", e)
|
|
||||||
: window.plus && u.test(document.readyState)
|
|
||||||
? setTimeout(e, 0)
|
|
||||||
: document.addEventListener("plusready", e),
|
|
||||||
d
|
|
||||||
);
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
if (_)
|
|
||||||
return (
|
|
||||||
window.WeixinJSBridge && window.WeixinJSBridge.invoke
|
|
||||||
? setTimeout(e, 0)
|
|
||||||
: document.addEventListener("WeixinJSBridgeReady", e),
|
|
||||||
window.wx.miniProgram
|
|
||||||
);
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
if (c)
|
|
||||||
return (
|
|
||||||
window.QQJSBridge && window.QQJSBridge.invoke
|
|
||||||
? setTimeout(e, 0)
|
|
||||||
: document.addEventListener("QQJSBridgeReady", e),
|
|
||||||
window.qq.miniProgram
|
|
||||||
);
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
if (g) {
|
|
||||||
document.addEventListener("DOMContentLoaded", e);
|
|
||||||
var n = window.my;
|
|
||||||
return {
|
|
||||||
navigateTo: n.navigateTo,
|
|
||||||
navigateBack: n.navigateBack,
|
|
||||||
switchTab: n.switchTab,
|
|
||||||
reLaunch: n.reLaunch,
|
|
||||||
redirectTo: n.redirectTo,
|
|
||||||
postMessage: n.postMessage,
|
|
||||||
getEnv: n.getEnv,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
if (v)
|
|
||||||
return (
|
|
||||||
document.addEventListener("DOMContentLoaded", e),
|
|
||||||
window.swan.webView
|
|
||||||
);
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
if (p)
|
|
||||||
return (
|
|
||||||
document.addEventListener("DOMContentLoaded", e),
|
|
||||||
window.tt.miniProgram
|
|
||||||
);
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
if (m) {
|
|
||||||
window.QaJSBridge && window.QaJSBridge.invoke
|
|
||||||
? setTimeout(e, 0)
|
|
||||||
: document.addEventListener("QaJSBridgeReady", e);
|
|
||||||
var n = window.qa;
|
|
||||||
return {
|
|
||||||
navigateTo: n.navigateTo,
|
|
||||||
navigateBack: n.navigateBack,
|
|
||||||
switchTab: n.switchTab,
|
|
||||||
reLaunch: n.reLaunch,
|
|
||||||
redirectTo: n.redirectTo,
|
|
||||||
postMessage: n.postMessage,
|
|
||||||
getEnv: n.getEnv,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
if (f)
|
|
||||||
return (
|
|
||||||
window.WeixinJSBridge && window.WeixinJSBridge.invoke
|
|
||||||
? setTimeout(e, 0)
|
|
||||||
: document.addEventListener("WeixinJSBridgeReady", e),
|
|
||||||
window.ks.miniProgram
|
|
||||||
);
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
if (l)
|
|
||||||
return (
|
|
||||||
document.addEventListener("DOMContentLoaded", e),
|
|
||||||
window.tt.miniProgram
|
|
||||||
);
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
if (E)
|
|
||||||
return (
|
|
||||||
window.JDJSBridgeReady && window.JDJSBridgeReady.invoke
|
|
||||||
? setTimeout(e, 0)
|
|
||||||
: document.addEventListener("JDJSBridgeReady", e),
|
|
||||||
window.jd.miniProgram
|
|
||||||
);
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
if (x) return window.xhs.miniProgram;
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
return (document.addEventListener("DOMContentLoaded", e), d);
|
|
||||||
},
|
|
||||||
],
|
|
||||||
M = 0;
|
|
||||||
M < y.length && !(S = y[M](h));
|
|
||||||
M++
|
|
||||||
);
|
|
||||||
S || (S = {});
|
|
||||||
var P = "undefined" != typeof uni ? uni : {};
|
|
||||||
if (!P.navigateTo) for (var b in S) i(S, b) && (P[b] = S[b]);
|
|
||||||
return ((P.webView = S), P);
|
|
||||||
});
|
|
||||||
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@@ -161,273 +161,313 @@ const EMOJI_DATA: Record<EmojiName, EmojiInfo> = {
|
|||||||
微笑: {
|
微笑: {
|
||||||
name: "微笑",
|
name: "微笑",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/微笑.png",
|
path: "/assets/face/smile.png",
|
||||||
},
|
},
|
||||||
撇嘴: {
|
撇嘴: {
|
||||||
name: "撇嘴",
|
name: "撇嘴",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/撇嘴.png",
|
path: "/assets/face/pout.png",
|
||||||
|
},
|
||||||
|
色: {
|
||||||
|
name: "色",
|
||||||
|
category: EmojiCategory.FACE,
|
||||||
|
path: "/assets/face/lustful.png",
|
||||||
},
|
},
|
||||||
色: { name: "色", category: EmojiCategory.FACE, path: "/assets/face/色.png" },
|
|
||||||
发呆: {
|
发呆: {
|
||||||
name: "发呆",
|
name: "发呆",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/发呆.png",
|
path: "/assets/face/daze.png",
|
||||||
},
|
},
|
||||||
得意: {
|
得意: {
|
||||||
name: "得意",
|
name: "得意",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/得意.png",
|
path: "/assets/face/smug.png",
|
||||||
},
|
},
|
||||||
流泪: {
|
流泪: {
|
||||||
name: "流泪",
|
name: "流泪",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/流泪.png",
|
path: "/assets/face/crying.png",
|
||||||
},
|
},
|
||||||
害羞: {
|
害羞: {
|
||||||
name: "害羞",
|
name: "害羞",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/害羞.png",
|
path: "/assets/face/shy.png",
|
||||||
},
|
},
|
||||||
闭嘴: {
|
闭嘴: {
|
||||||
name: "闭嘴",
|
name: "闭嘴",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/闭嘴.png",
|
path: "/assets/face/shut-up.png",
|
||||||
|
},
|
||||||
|
睡: {
|
||||||
|
name: "睡",
|
||||||
|
category: EmojiCategory.FACE,
|
||||||
|
path: "/assets/face/sleep.png",
|
||||||
},
|
},
|
||||||
睡: { name: "睡", category: EmojiCategory.FACE, path: "/assets/face/睡.png" },
|
|
||||||
大哭: {
|
大哭: {
|
||||||
name: "大哭",
|
name: "大哭",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/大哭.png",
|
path: "/assets/face/wail.png",
|
||||||
},
|
},
|
||||||
尴尬: {
|
尴尬: {
|
||||||
name: "尴尬",
|
name: "尴尬",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/尴尬.png",
|
path: "/assets/face/awkward.png",
|
||||||
},
|
},
|
||||||
发怒: {
|
发怒: {
|
||||||
name: "发怒",
|
name: "发怒",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/发怒.png",
|
path: "/assets/face/angry.png",
|
||||||
},
|
},
|
||||||
调皮: {
|
调皮: {
|
||||||
name: "调皮",
|
name: "调皮",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/调皮.png",
|
path: "/assets/face/naughty.png",
|
||||||
},
|
},
|
||||||
呲牙: {
|
呲牙: {
|
||||||
name: "呲牙",
|
name: "呲牙",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/呲牙.png",
|
path: "/assets/face/grin.png",
|
||||||
},
|
},
|
||||||
惊讶: {
|
惊讶: {
|
||||||
name: "惊讶",
|
name: "惊讶",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/惊讶.png",
|
path: "/assets/face/surprised.png",
|
||||||
},
|
},
|
||||||
难过: {
|
难过: {
|
||||||
name: "难过",
|
name: "难过",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/难过.png",
|
path: "/assets/face/sad.png",
|
||||||
|
},
|
||||||
|
囧: {
|
||||||
|
name: "囧",
|
||||||
|
category: EmojiCategory.FACE,
|
||||||
|
path: "/assets/face/embarrassed.png",
|
||||||
},
|
},
|
||||||
囧: { name: "囧", category: EmojiCategory.FACE, path: "/assets/face/囧.png" },
|
|
||||||
抓狂: {
|
抓狂: {
|
||||||
name: "抓狂",
|
name: "抓狂",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/抓狂.png",
|
path: "/assets/face/crazy.png",
|
||||||
|
},
|
||||||
|
吐: {
|
||||||
|
name: "吐",
|
||||||
|
category: EmojiCategory.FACE,
|
||||||
|
path: "/assets/face/vomit.png",
|
||||||
},
|
},
|
||||||
吐: { name: "吐", category: EmojiCategory.FACE, path: "/assets/face/吐.png" },
|
|
||||||
偷笑: {
|
偷笑: {
|
||||||
name: "偷笑",
|
name: "偷笑",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/偷笑.png",
|
path: "/assets/face/snicker.png",
|
||||||
},
|
},
|
||||||
愉快: {
|
愉快: {
|
||||||
name: "愉快",
|
name: "愉快",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/愉快.png",
|
path: "/assets/face/happy.png",
|
||||||
},
|
},
|
||||||
白眼: {
|
白眼: {
|
||||||
name: "白眼",
|
name: "白眼",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/白眼.png",
|
path: "/assets/face/roll-eyes.png",
|
||||||
},
|
},
|
||||||
傲慢: {
|
傲慢: {
|
||||||
name: "傲慢",
|
name: "傲慢",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/傲慢.png",
|
path: "/assets/face/arrogant.png",
|
||||||
|
},
|
||||||
|
困: {
|
||||||
|
name: "困",
|
||||||
|
category: EmojiCategory.FACE,
|
||||||
|
path: "/assets/face/sleepy.png",
|
||||||
},
|
},
|
||||||
困: { name: "困", category: EmojiCategory.FACE, path: "/assets/face/困.png" },
|
|
||||||
惊恐: {
|
惊恐: {
|
||||||
name: "惊恐",
|
name: "惊恐",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/惊恐.png",
|
path: "/assets/face/panic.png",
|
||||||
},
|
},
|
||||||
憨笑: {
|
憨笑: {
|
||||||
name: "憨笑",
|
name: "憨笑",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/憨笑.png",
|
path: "/assets/face/silly-smile.png",
|
||||||
},
|
},
|
||||||
悠闲: {
|
悠闲: {
|
||||||
name: "悠闲",
|
name: "悠闲",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/悠闲.png",
|
path: "/assets/face/leisurely.png",
|
||||||
},
|
},
|
||||||
咒骂: {
|
咒骂: {
|
||||||
name: "咒骂",
|
name: "咒骂",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/咒骂.png",
|
path: "/assets/face/curse.png",
|
||||||
},
|
},
|
||||||
疑问: {
|
疑问: {
|
||||||
name: "疑问",
|
name: "疑问",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/疑问.png",
|
path: "/assets/face/question.png",
|
||||||
|
},
|
||||||
|
嘘: {
|
||||||
|
name: "嘘",
|
||||||
|
category: EmojiCategory.FACE,
|
||||||
|
path: "/assets/face/shush.png",
|
||||||
|
},
|
||||||
|
晕: {
|
||||||
|
name: "晕",
|
||||||
|
category: EmojiCategory.FACE,
|
||||||
|
path: "/assets/face/dizzy.png",
|
||||||
|
},
|
||||||
|
衰: {
|
||||||
|
name: "衰",
|
||||||
|
category: EmojiCategory.FACE,
|
||||||
|
path: "/assets/face/unlucky.png",
|
||||||
},
|
},
|
||||||
嘘: { name: "嘘", category: EmojiCategory.FACE, path: "/assets/face/嘘.png" },
|
|
||||||
晕: { name: "晕", category: EmojiCategory.FACE, path: "/assets/face/晕.png" },
|
|
||||||
衰: { name: "衰", category: EmojiCategory.FACE, path: "/assets/face/衰.png" },
|
|
||||||
骷髅: {
|
骷髅: {
|
||||||
name: "骷髅",
|
name: "骷髅",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/骷髅.png",
|
path: "/assets/face/skull.png",
|
||||||
},
|
},
|
||||||
敲打: {
|
敲打: {
|
||||||
name: "敲打",
|
name: "敲打",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/敲打.png",
|
path: "/assets/face/knock.png",
|
||||||
},
|
},
|
||||||
再见: {
|
再见: {
|
||||||
name: "再见",
|
name: "再见",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/再见.png",
|
path: "/assets/face/goodbye.png",
|
||||||
},
|
},
|
||||||
擦汗: {
|
擦汗: {
|
||||||
name: "擦汗",
|
name: "擦汗",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/擦汗.png",
|
path: "/assets/face/wipe-sweat.png",
|
||||||
},
|
},
|
||||||
抠鼻: {
|
抠鼻: {
|
||||||
name: "抠鼻",
|
name: "抠鼻",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/抠鼻.png",
|
path: "/assets/face/pick-nose.png",
|
||||||
},
|
},
|
||||||
鼓掌: {
|
鼓掌: {
|
||||||
name: "鼓掌",
|
name: "鼓掌",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/鼓掌.png",
|
path: "/assets/face/clap.png",
|
||||||
},
|
},
|
||||||
坏笑: {
|
坏笑: {
|
||||||
name: "坏笑",
|
name: "坏笑",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/坏笑.png",
|
path: "/assets/face/evil-smile.png",
|
||||||
},
|
},
|
||||||
右哼哼: {
|
右哼哼: {
|
||||||
name: "右哼哼",
|
name: "右哼哼",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/右哼哼.png",
|
path: "/assets/face/right-hum.png",
|
||||||
},
|
},
|
||||||
鄙视: {
|
鄙视: {
|
||||||
name: "鄙视",
|
name: "鄙视",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/鄙视.png",
|
path: "/assets/face/despise.png",
|
||||||
},
|
},
|
||||||
委屈: {
|
委屈: {
|
||||||
name: "委屈",
|
name: "委屈",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/委屈.png",
|
path: "/assets/face/wronged.png",
|
||||||
},
|
},
|
||||||
快哭了: {
|
快哭了: {
|
||||||
name: "快哭了",
|
name: "快哭了",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/快哭了.png",
|
path: "/assets/face/about-to-cry.png",
|
||||||
},
|
},
|
||||||
阴险: {
|
阴险: {
|
||||||
name: "阴险",
|
name: "阴险",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/阴险.png",
|
path: "/assets/face/sinister.png",
|
||||||
},
|
},
|
||||||
亲亲: {
|
亲亲: {
|
||||||
name: "亲亲",
|
name: "亲亲",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/亲亲.png",
|
path: "/assets/face/kiss.png",
|
||||||
},
|
},
|
||||||
可怜: {
|
可怜: {
|
||||||
name: "可怜",
|
name: "可怜",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/可怜.png",
|
path: "/assets/face/pitiful.png",
|
||||||
},
|
},
|
||||||
笑脸: {
|
笑脸: {
|
||||||
name: "笑脸",
|
name: "笑脸",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/笑脸.png",
|
path: "/assets/face/smiley.png",
|
||||||
},
|
},
|
||||||
生病: {
|
生病: {
|
||||||
name: "生病",
|
name: "生病",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/生病.png",
|
path: "/assets/face/sick.png",
|
||||||
},
|
},
|
||||||
脸红: {
|
脸红: {
|
||||||
name: "脸红",
|
name: "脸红",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/脸红.png",
|
path: "/assets/face/blush.png",
|
||||||
},
|
},
|
||||||
破涕为笑: {
|
破涕为笑: {
|
||||||
name: "破涕为笑",
|
name: "破涕为笑",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/破涕为笑.png",
|
path: "/assets/face/tears-to-smile.png",
|
||||||
},
|
},
|
||||||
恐惧: {
|
恐惧: {
|
||||||
name: "恐惧",
|
name: "恐惧",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/恐惧.png",
|
path: "/assets/face/fear.png",
|
||||||
},
|
},
|
||||||
失望: {
|
失望: {
|
||||||
name: "失望",
|
name: "失望",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/失望.png",
|
path: "/assets/face/disappointed.png",
|
||||||
},
|
},
|
||||||
无语: {
|
无语: {
|
||||||
name: "无语",
|
name: "无语",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/无语.png",
|
path: "/assets/face/speechless.png",
|
||||||
},
|
},
|
||||||
嘿哈: {
|
嘿哈: {
|
||||||
name: "嘿哈",
|
name: "嘿哈",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/嘿哈.png",
|
path: "/assets/face/hey-ha.png",
|
||||||
},
|
},
|
||||||
捂脸: {
|
捂脸: {
|
||||||
name: "捂脸",
|
name: "捂脸",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/捂脸.png",
|
path: "/assets/face/facepalm.png",
|
||||||
},
|
},
|
||||||
机智: {
|
机智: {
|
||||||
name: "机智",
|
name: "机智",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/机智.png",
|
path: "/assets/face/smart.png",
|
||||||
},
|
},
|
||||||
皱眉: {
|
皱眉: {
|
||||||
name: "皱眉",
|
name: "皱眉",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/皱眉.png",
|
path: "/assets/face/frown.png",
|
||||||
|
},
|
||||||
|
耶: {
|
||||||
|
name: "耶",
|
||||||
|
category: EmojiCategory.FACE,
|
||||||
|
path: "/assets/face/yeah.png",
|
||||||
},
|
},
|
||||||
耶: { name: "耶", category: EmojiCategory.FACE, path: "/assets/face/耶.png" },
|
|
||||||
吃瓜: {
|
吃瓜: {
|
||||||
name: "吃瓜",
|
name: "吃瓜",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/吃瓜.png",
|
path: "/assets/face/eat-melon.png",
|
||||||
},
|
},
|
||||||
加油: {
|
加油: {
|
||||||
name: "加油",
|
name: "加油",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/加油.png",
|
path: "/assets/face/cheer-up.png",
|
||||||
},
|
},
|
||||||
|
|
||||||
汗: { name: "汗", category: EmojiCategory.FACE, path: "/assets/face/汗.png" },
|
汗: {
|
||||||
|
name: "汗",
|
||||||
|
category: EmojiCategory.FACE,
|
||||||
|
path: "/assets/face/sweat.png",
|
||||||
|
},
|
||||||
天啊: {
|
天啊: {
|
||||||
name: "天啊",
|
name: "天啊",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/天啊.png",
|
path: "/assets/face/oh-my.png",
|
||||||
},
|
},
|
||||||
Emm: {
|
Emm: {
|
||||||
name: "Emm",
|
name: "Emm",
|
||||||
@@ -437,28 +477,32 @@ const EMOJI_DATA: Record<EmojiName, EmojiInfo> = {
|
|||||||
社会社会: {
|
社会社会: {
|
||||||
name: "社会社会",
|
name: "社会社会",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/社会社会.png",
|
path: "/assets/face/social.png",
|
||||||
},
|
},
|
||||||
旺柴: {
|
旺柴: {
|
||||||
name: "旺柴",
|
name: "旺柴",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/旺柴.png",
|
path: "/assets/face/doge.png",
|
||||||
},
|
},
|
||||||
好的: {
|
好的: {
|
||||||
name: "好的",
|
name: "好的",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/好的.png",
|
path: "/assets/face/good.png",
|
||||||
},
|
},
|
||||||
打脸: {
|
打脸: {
|
||||||
name: "打脸",
|
name: "打脸",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/打脸.png",
|
path: "/assets/face/slap-face.png",
|
||||||
|
},
|
||||||
|
哇: {
|
||||||
|
name: "哇",
|
||||||
|
category: EmojiCategory.FACE,
|
||||||
|
path: "/assets/face/wow.png",
|
||||||
},
|
},
|
||||||
哇: { name: "哇", category: EmojiCategory.FACE, path: "/assets/face/哇.png" },
|
|
||||||
翻白眼: {
|
翻白眼: {
|
||||||
name: "翻白眼",
|
name: "翻白眼",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/翻白眼.png",
|
path: "/assets/face/eye-roll.png",
|
||||||
},
|
},
|
||||||
"666": {
|
"666": {
|
||||||
name: "666",
|
name: "666",
|
||||||
@@ -468,54 +512,54 @@ const EMOJI_DATA: Record<EmojiName, EmojiInfo> = {
|
|||||||
让我看看: {
|
让我看看: {
|
||||||
name: "让我看看",
|
name: "让我看看",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/让我看看.png",
|
path: "/assets/face/let-me-see.png",
|
||||||
},
|
},
|
||||||
叹气: {
|
叹气: {
|
||||||
name: "叹气",
|
name: "叹气",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/叹气.png",
|
path: "/assets/face/sigh.png",
|
||||||
},
|
},
|
||||||
苦涩: {
|
苦涩: {
|
||||||
name: "苦涩",
|
name: "苦涩",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/苦涩.png",
|
path: "/assets/face/bitter.png",
|
||||||
},
|
},
|
||||||
裂开: {
|
裂开: {
|
||||||
name: "裂开",
|
name: "裂开",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/裂开.png",
|
path: "/assets/face/crack.png",
|
||||||
},
|
},
|
||||||
奸笑: {
|
奸笑: {
|
||||||
name: "奸笑",
|
name: "奸笑",
|
||||||
category: EmojiCategory.FACE,
|
category: EmojiCategory.FACE,
|
||||||
path: "/assets/face/奸笑.png",
|
path: "/assets/face/sly-smile.png",
|
||||||
},
|
},
|
||||||
|
|
||||||
// 手势表情
|
// 手势表情
|
||||||
握手: {
|
握手: {
|
||||||
name: "握手",
|
name: "握手",
|
||||||
category: EmojiCategory.GESTURE,
|
category: EmojiCategory.GESTURE,
|
||||||
path: "/assets/gesture/握手.png",
|
path: "/assets/gesture/handshake.png",
|
||||||
},
|
},
|
||||||
胜利: {
|
胜利: {
|
||||||
name: "胜利",
|
name: "胜利",
|
||||||
category: EmojiCategory.GESTURE,
|
category: EmojiCategory.GESTURE,
|
||||||
path: "/assets/gesture/胜利.png",
|
path: "/assets/gesture/victory.png",
|
||||||
},
|
},
|
||||||
抱拳: {
|
抱拳: {
|
||||||
name: "抱拳",
|
name: "抱拳",
|
||||||
category: EmojiCategory.GESTURE,
|
category: EmojiCategory.GESTURE,
|
||||||
path: "/assets/gesture/抱拳.png",
|
path: "/assets/gesture/fist-salute.png",
|
||||||
},
|
},
|
||||||
勾引: {
|
勾引: {
|
||||||
name: "勾引",
|
name: "勾引",
|
||||||
category: EmojiCategory.GESTURE,
|
category: EmojiCategory.GESTURE,
|
||||||
path: "/assets/gesture/勾引.png",
|
path: "/assets/gesture/beckon.png",
|
||||||
},
|
},
|
||||||
拳头: {
|
拳头: {
|
||||||
name: "拳头",
|
name: "拳头",
|
||||||
category: EmojiCategory.GESTURE,
|
category: EmojiCategory.GESTURE,
|
||||||
path: "/assets/gesture/拳头.png",
|
path: "/assets/gesture/fist.png",
|
||||||
},
|
},
|
||||||
OK: {
|
OK: {
|
||||||
name: "OK",
|
name: "OK",
|
||||||
@@ -525,148 +569,148 @@ const EMOJI_DATA: Record<EmojiName, EmojiInfo> = {
|
|||||||
合十: {
|
合十: {
|
||||||
name: "合十",
|
name: "合十",
|
||||||
category: EmojiCategory.GESTURE,
|
category: EmojiCategory.GESTURE,
|
||||||
path: "/assets/gesture/合十.png",
|
path: "/assets/gesture/pray.png",
|
||||||
},
|
},
|
||||||
强: {
|
强: {
|
||||||
name: "强",
|
name: "强",
|
||||||
category: EmojiCategory.GESTURE,
|
category: EmojiCategory.GESTURE,
|
||||||
path: "/assets/gesture/强.png",
|
path: "/assets/gesture/strong.png",
|
||||||
},
|
},
|
||||||
拥抱: {
|
拥抱: {
|
||||||
name: "拥抱",
|
name: "拥抱",
|
||||||
category: EmojiCategory.GESTURE,
|
category: EmojiCategory.GESTURE,
|
||||||
path: "/assets/gesture/拥抱.png",
|
path: "/assets/gesture/hug.png",
|
||||||
},
|
},
|
||||||
弱: {
|
弱: {
|
||||||
name: "弱",
|
name: "弱",
|
||||||
category: EmojiCategory.GESTURE,
|
category: EmojiCategory.GESTURE,
|
||||||
path: "/assets/gesture/弱.png",
|
path: "/assets/gesture/weak.png",
|
||||||
},
|
},
|
||||||
|
|
||||||
// 动物表情
|
// 动物表情
|
||||||
猪头: {
|
猪头: {
|
||||||
name: "猪头",
|
name: "猪头",
|
||||||
category: EmojiCategory.ANIMAL,
|
category: EmojiCategory.ANIMAL,
|
||||||
path: "/assets/animal/猪头.png",
|
path: "/assets/animal/pig.png",
|
||||||
},
|
},
|
||||||
跳跳: {
|
跳跳: {
|
||||||
name: "跳跳",
|
name: "跳跳",
|
||||||
category: EmojiCategory.ANIMAL,
|
category: EmojiCategory.ANIMAL,
|
||||||
path: "/assets/animal/跳跳.png",
|
path: "/assets/animal/jump.png",
|
||||||
},
|
},
|
||||||
发抖: {
|
发抖: {
|
||||||
name: "发抖",
|
name: "发抖",
|
||||||
category: EmojiCategory.ANIMAL,
|
category: EmojiCategory.ANIMAL,
|
||||||
path: "/assets/animal/发抖.png",
|
path: "/assets/animal/tremble.png",
|
||||||
},
|
},
|
||||||
转圈: {
|
转圈: {
|
||||||
name: "转圈",
|
name: "转圈",
|
||||||
category: EmojiCategory.ANIMAL,
|
category: EmojiCategory.ANIMAL,
|
||||||
path: "/assets/animal/转圈.png",
|
path: "/assets/animal/circle.png",
|
||||||
},
|
},
|
||||||
|
|
||||||
// 祝福表情
|
// 祝福表情
|
||||||
庆祝: {
|
庆祝: {
|
||||||
name: "庆祝",
|
name: "庆祝",
|
||||||
category: EmojiCategory.BLESSING,
|
category: EmojiCategory.BLESSING,
|
||||||
path: "/assets/blessing/庆祝.png",
|
path: "/assets/blessing/celebrate.png",
|
||||||
},
|
},
|
||||||
礼物: {
|
礼物: {
|
||||||
name: "礼物",
|
name: "礼物",
|
||||||
category: EmojiCategory.BLESSING,
|
category: EmojiCategory.BLESSING,
|
||||||
path: "/assets/blessing/礼物.png",
|
path: "/assets/blessing/gift.png",
|
||||||
},
|
},
|
||||||
红包: {
|
红包: {
|
||||||
name: "红包",
|
name: "红包",
|
||||||
category: EmojiCategory.BLESSING,
|
category: EmojiCategory.BLESSING,
|
||||||
path: "/assets/blessing/红包.png",
|
path: "/assets/blessing/red-envelope.png",
|
||||||
},
|
},
|
||||||
發: {
|
發: {
|
||||||
name: "發",
|
name: "發",
|
||||||
category: EmojiCategory.BLESSING,
|
category: EmojiCategory.BLESSING,
|
||||||
path: "/assets/blessing/發.png",
|
path: "/assets/blessing/get-rich.png",
|
||||||
},
|
},
|
||||||
福: {
|
福: {
|
||||||
name: "福",
|
name: "福",
|
||||||
category: EmojiCategory.BLESSING,
|
category: EmojiCategory.BLESSING,
|
||||||
path: "/assets/blessing/福.png",
|
path: "/assets/blessing/fortune.png",
|
||||||
},
|
},
|
||||||
烟花: {
|
烟花: {
|
||||||
name: "烟花",
|
name: "烟花",
|
||||||
category: EmojiCategory.BLESSING,
|
category: EmojiCategory.BLESSING,
|
||||||
path: "/assets/blessing/烟花.png",
|
path: "/assets/blessing/fireworks.png",
|
||||||
},
|
},
|
||||||
爆竹: {
|
爆竹: {
|
||||||
name: "爆竹",
|
name: "爆竹",
|
||||||
category: EmojiCategory.BLESSING,
|
category: EmojiCategory.BLESSING,
|
||||||
path: "/assets/blessing/爆竹.png",
|
path: "/assets/blessing/firecrackers.png",
|
||||||
},
|
},
|
||||||
|
|
||||||
// 其他表情
|
// 其他表情
|
||||||
嘴唇: {
|
嘴唇: {
|
||||||
name: "嘴唇",
|
name: "嘴唇",
|
||||||
category: EmojiCategory.OTHER,
|
category: EmojiCategory.OTHER,
|
||||||
path: "/assets/other/嘴唇.png",
|
path: "/assets/other/lips.png",
|
||||||
},
|
},
|
||||||
爱心: {
|
爱心: {
|
||||||
name: "爱心",
|
name: "爱心",
|
||||||
category: EmojiCategory.OTHER,
|
category: EmojiCategory.OTHER,
|
||||||
path: "/assets/other/爱心.png",
|
path: "/assets/other/heart.png",
|
||||||
},
|
},
|
||||||
心碎: {
|
心碎: {
|
||||||
name: "心碎",
|
name: "心碎",
|
||||||
category: EmojiCategory.OTHER,
|
category: EmojiCategory.OTHER,
|
||||||
path: "/assets/other/心碎.png",
|
path: "/assets/other/broken-heart.png",
|
||||||
},
|
},
|
||||||
啤酒: {
|
啤酒: {
|
||||||
name: "啤酒",
|
name: "啤酒",
|
||||||
category: EmojiCategory.OTHER,
|
category: EmojiCategory.OTHER,
|
||||||
path: "/assets/other/啤酒.png",
|
path: "/assets/other/beer.png",
|
||||||
},
|
},
|
||||||
咖啡: {
|
咖啡: {
|
||||||
name: "咖啡",
|
name: "咖啡",
|
||||||
category: EmojiCategory.OTHER,
|
category: EmojiCategory.OTHER,
|
||||||
path: "/assets/other/咖啡.png",
|
path: "/assets/other/coffee.png",
|
||||||
},
|
},
|
||||||
蛋糕: {
|
蛋糕: {
|
||||||
name: "蛋糕",
|
name: "蛋糕",
|
||||||
category: EmojiCategory.OTHER,
|
category: EmojiCategory.OTHER,
|
||||||
path: "/assets/other/蛋糕.png",
|
path: "/assets/other/cake.png",
|
||||||
},
|
},
|
||||||
凋谢: {
|
凋谢: {
|
||||||
name: "凋谢",
|
name: "凋谢",
|
||||||
category: EmojiCategory.OTHER,
|
category: EmojiCategory.OTHER,
|
||||||
path: "/assets/other/凋谢.png",
|
path: "/assets/other/wither.png",
|
||||||
},
|
},
|
||||||
菜刀: {
|
菜刀: {
|
||||||
name: "菜刀",
|
name: "菜刀",
|
||||||
category: EmojiCategory.OTHER,
|
category: EmojiCategory.OTHER,
|
||||||
path: "/assets/other/菜刀.png",
|
path: "/assets/other/knife.png",
|
||||||
},
|
},
|
||||||
炸弹: {
|
炸弹: {
|
||||||
name: "炸弹",
|
name: "炸弹",
|
||||||
category: EmojiCategory.OTHER,
|
category: EmojiCategory.OTHER,
|
||||||
path: "/assets/other/炸弹.png",
|
path: "/assets/other/bomb.png",
|
||||||
},
|
},
|
||||||
便便: {
|
便便: {
|
||||||
name: "便便",
|
name: "便便",
|
||||||
category: EmojiCategory.OTHER,
|
category: EmojiCategory.OTHER,
|
||||||
path: "/assets/other/便便.png",
|
path: "/assets/other/poop.png",
|
||||||
},
|
},
|
||||||
太阳: {
|
太阳: {
|
||||||
name: "太阳",
|
name: "太阳",
|
||||||
category: EmojiCategory.OTHER,
|
category: EmojiCategory.OTHER,
|
||||||
path: "/assets/other/太阳.png",
|
path: "/assets/other/sun.png",
|
||||||
},
|
},
|
||||||
月亮: {
|
月亮: {
|
||||||
name: "月亮",
|
name: "月亮",
|
||||||
category: EmojiCategory.OTHER,
|
category: EmojiCategory.OTHER,
|
||||||
path: "/assets/other/月亮.png",
|
path: "/assets/other/moon.png",
|
||||||
},
|
},
|
||||||
玫瑰: {
|
玫瑰: {
|
||||||
name: "玫瑰",
|
name: "玫瑰",
|
||||||
category: EmojiCategory.OTHER,
|
category: EmojiCategory.OTHER,
|
||||||
path: "/assets/other/玫瑰.png",
|
path: "/assets/other/rose.png",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -784,12 +784,6 @@ const TrafficPoolDetail: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 添加新标签按钮 */}
|
|
||||||
<Button block color="primary" className={styles.addTagBtn}>
|
|
||||||
<TagOutlined />
|
|
||||||
添加新标签
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,57 +1,94 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Popup, Selector } from "antd-mobile";
|
import { Popup, Selector, Button } from "antd-mobile";
|
||||||
|
import { fetchPackageOptions } from "./api";
|
||||||
import type { PackageOption } from "./data";
|
import type { PackageOption } from "./data";
|
||||||
|
|
||||||
interface BatchAddModalProps {
|
interface BatchAddModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
packageOptions: PackageOption[];
|
|
||||||
batchTarget: string;
|
|
||||||
setBatchTarget: (v: string) => void;
|
|
||||||
selectedCount: number;
|
selectedCount: number;
|
||||||
onConfirm: () => void;
|
onConfirm: (data: {
|
||||||
|
packageOptions: PackageOption[];
|
||||||
|
selectedPackageId: string;
|
||||||
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BatchAddModal: React.FC<BatchAddModalProps> = ({
|
const BatchAddModal: React.FC<BatchAddModalProps> = ({
|
||||||
visible,
|
visible,
|
||||||
onClose,
|
onClose,
|
||||||
packageOptions = [],
|
|
||||||
batchTarget,
|
|
||||||
setBatchTarget,
|
|
||||||
selectedCount,
|
selectedCount,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
}) => (
|
}) => {
|
||||||
// <Modal visible={visible} title="批量加入分组" onConfirm={onConfirm}>
|
const [packageOptions, setPackageOptions] = useState<PackageOption[]>([]);
|
||||||
// <div style={{ marginBottom: 12 }}>
|
const [selectedPackageId, setSelectedPackageId] = useState<string>("");
|
||||||
// <div>选择目标分组</div>
|
const [loading, setLoading] = useState(false);
|
||||||
// <Selector
|
|
||||||
// options={packageOptions.map(p => ({ label: p.name, value: p.id }))}
|
// 获取分组选项
|
||||||
// value={[batchTarget]}
|
useEffect(() => {
|
||||||
// onChange={v => setBatchTarget(v[0])}
|
if (visible) {
|
||||||
// />
|
setLoading(true);
|
||||||
// </div>
|
fetchPackageOptions()
|
||||||
// <div style={{ color: "#888", fontSize: 13 }}>
|
.then(res => {
|
||||||
// 将选中的{selectedCount}个用户加入所选分组
|
setPackageOptions(res.list || []);
|
||||||
// </div>
|
})
|
||||||
// </Modal>
|
.catch(error => {
|
||||||
<Popup
|
console.error("获取分组选项失败:", error);
|
||||||
visible={visible}
|
})
|
||||||
onMaskClick={() => onClose()}
|
.finally(() => {
|
||||||
position="bottom"
|
setLoading(false);
|
||||||
bodyStyle={{ height: "80vh" }}
|
});
|
||||||
>
|
}
|
||||||
<div style={{ marginBottom: 12 }}>
|
}, [visible]);
|
||||||
<div>选择目标分组</div>
|
|
||||||
<Selector
|
const handleSubmit = () => {
|
||||||
options={packageOptions.map(p => ({ label: p.name, value: p.id }))}
|
if (!selectedPackageId) {
|
||||||
value={[batchTarget]}
|
// 可以添加提示
|
||||||
onChange={v => setBatchTarget(v[0])}
|
return;
|
||||||
/>
|
}
|
||||||
</div>
|
onConfirm({
|
||||||
<div style={{ color: "#888", fontSize: 13 }}>
|
packageOptions,
|
||||||
将选中的{selectedCount}个用户加入所选分组
|
selectedPackageId,
|
||||||
</div>
|
});
|
||||||
</Popup>
|
};
|
||||||
);
|
return (
|
||||||
|
<Popup
|
||||||
|
visible={visible}
|
||||||
|
onMaskClick={() => onClose()}
|
||||||
|
position="bottom"
|
||||||
|
bodyStyle={{ height: "80vh" }}
|
||||||
|
>
|
||||||
|
<div style={{ marginBottom: 12, padding: 10 }}>
|
||||||
|
<div style={{ marginBottom: 12 }}>选择目标分组</div>
|
||||||
|
{loading ? (
|
||||||
|
<div style={{ textAlign: "center", padding: 20 }}>加载中...</div>
|
||||||
|
) : (
|
||||||
|
<Selector
|
||||||
|
options={packageOptions.map(p => ({ label: p.name, value: p.id }))}
|
||||||
|
value={[selectedPackageId]}
|
||||||
|
onChange={v => setSelectedPackageId(v[0])}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: "#888",
|
||||||
|
fontSize: 12,
|
||||||
|
paddingTop: 15,
|
||||||
|
marginBottom: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
将选中的{selectedCount}个用户加入所选分组
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
color="primary"
|
||||||
|
block
|
||||||
|
disabled={!selectedPackageId || loading}
|
||||||
|
>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Popup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default BatchAddModal;
|
export default BatchAddModal;
|
||||||
|
|||||||
@@ -1,24 +1,77 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect, useMemo } from "react";
|
||||||
import { Card, Button } from "antd-mobile";
|
import { Card, Button } from "antd-mobile";
|
||||||
|
import { fetchTrafficPoolList } from "./api";
|
||||||
|
import type { TrafficPoolUser } from "./data";
|
||||||
|
|
||||||
interface DataAnalysisPanelProps {
|
interface DataAnalysisPanelProps {
|
||||||
stats: {
|
showStats: boolean;
|
||||||
|
setShowStats: (v: boolean) => void;
|
||||||
|
onConfirm: (stats: {
|
||||||
total: number;
|
total: number;
|
||||||
highValue: number;
|
highValue: number;
|
||||||
added: number;
|
added: number;
|
||||||
pending: number;
|
pending: number;
|
||||||
failed: number;
|
failed: number;
|
||||||
addSuccessRate: number;
|
addSuccessRate: number;
|
||||||
};
|
}) => void;
|
||||||
showStats: boolean;
|
|
||||||
setShowStats: (v: boolean) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DataAnalysisPanel: React.FC<DataAnalysisPanelProps> = ({
|
const DataAnalysisPanel: React.FC<DataAnalysisPanelProps> = ({
|
||||||
stats,
|
|
||||||
showStats,
|
showStats,
|
||||||
setShowStats,
|
setShowStats,
|
||||||
|
onConfirm,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [list, setList] = useState<TrafficPoolUser[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// 计算统计数据
|
||||||
|
const stats = useMemo(() => {
|
||||||
|
const total = list.length;
|
||||||
|
const highValue = list.filter(
|
||||||
|
u => u.tags && u.tags.includes("高价值客户池"),
|
||||||
|
).length;
|
||||||
|
const added = list.filter(u => u.status === 1).length;
|
||||||
|
const pending = list.filter(u => u.status === 0).length;
|
||||||
|
const failed = list.filter(u => u.status === -1).length;
|
||||||
|
const addSuccessRate = total ? Math.round((added / total) * 100) : 0;
|
||||||
|
return { total, highValue, added, pending, failed, addSuccessRate };
|
||||||
|
}, [list]);
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (showStats) {
|
||||||
|
setLoading(true);
|
||||||
|
fetchTrafficPoolList({ page: 1, pageSize: 1000 }) // 获取所有数据进行统计
|
||||||
|
.then(res => {
|
||||||
|
setList(res.list || []);
|
||||||
|
// 通过 onConfirm 抛出统计数据
|
||||||
|
const total = res.list?.length || 0;
|
||||||
|
const highValue =
|
||||||
|
res.list?.filter(u => u.tags && u.tags.includes("高价值客户池"))
|
||||||
|
.length || 0;
|
||||||
|
const added = res.list?.filter(u => u.status === 1).length || 0;
|
||||||
|
const pending = res.list?.filter(u => u.status === 0).length || 0;
|
||||||
|
const failed = res.list?.filter(u => u.status === -1).length || 0;
|
||||||
|
const addSuccessRate = total ? Math.round((added / total) * 100) : 0;
|
||||||
|
|
||||||
|
onConfirm({
|
||||||
|
total,
|
||||||
|
highValue,
|
||||||
|
added,
|
||||||
|
pending,
|
||||||
|
failed,
|
||||||
|
addSuccessRate,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("获取统计数据失败:", error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [showStats, onConfirm]);
|
||||||
|
|
||||||
if (!showStats) return null;
|
if (!showStats) return null;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -30,46 +83,54 @@ const DataAnalysisPanel: React.FC<DataAnalysisPanelProps> = ({
|
|||||||
boxShadow: "0 2px 8px rgba(0,0,0,0.04)",
|
boxShadow: "0 2px 8px rgba(0,0,0,0.04)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", gap: 16, marginBottom: 12 }}>
|
{loading ? (
|
||||||
<Card style={{ flex: 1 }}>
|
<div style={{ textAlign: "center", padding: 20 }}>
|
||||||
<div style={{ fontSize: 18, fontWeight: 600, color: "#1677ff" }}>
|
加载统计数据中...
|
||||||
{stats.total}
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div style={{ display: "flex", gap: 16, marginBottom: 12 }}>
|
||||||
|
<Card style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontSize: 18, fontWeight: 600, color: "#1677ff" }}>
|
||||||
|
{stats.total}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 13, color: "#888" }}>总用户数</div>
|
||||||
|
</Card>
|
||||||
|
<Card style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontSize: 18, fontWeight: 600, color: "#eb2f96" }}>
|
||||||
|
{stats.highValue}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 13, color: "#888" }}>高价值用户</div>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 13, color: "#888" }}>总用户数</div>
|
<div style={{ display: "flex", gap: 16 }}>
|
||||||
</Card>
|
<Card style={{ flex: 1 }}>
|
||||||
<Card style={{ flex: 1 }}>
|
<div style={{ fontSize: 18, fontWeight: 600, color: "#52c41a" }}>
|
||||||
<div style={{ fontSize: 18, fontWeight: 600, color: "#eb2f96" }}>
|
{stats.addSuccessRate}%
|
||||||
{stats.highValue}
|
</div>
|
||||||
|
<div style={{ fontSize: 13, color: "#888" }}>添加成功率</div>
|
||||||
|
</Card>
|
||||||
|
<Card style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontSize: 18, fontWeight: 600, color: "#faad14" }}>
|
||||||
|
{stats.added}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 13, color: "#888" }}>已添加</div>
|
||||||
|
</Card>
|
||||||
|
<Card style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontSize: 18, fontWeight: 600, color: "#bfbfbf" }}>
|
||||||
|
{stats.pending}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 13, color: "#888" }}>待添加</div>
|
||||||
|
</Card>
|
||||||
|
<Card style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontSize: 18, fontWeight: 600, color: "#ff4d4f" }}>
|
||||||
|
{stats.failed}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 13, color: "#888" }}>添加失败</div>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 13, color: "#888" }}>高价值用户</div>
|
</>
|
||||||
</Card>
|
)}
|
||||||
</div>
|
|
||||||
<div style={{ display: "flex", gap: 16 }}>
|
|
||||||
<Card style={{ flex: 1 }}>
|
|
||||||
<div style={{ fontSize: 18, fontWeight: 600, color: "#52c41a" }}>
|
|
||||||
{stats.addSuccessRate}%
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 13, color: "#888" }}>添加成功率</div>
|
|
||||||
</Card>
|
|
||||||
<Card style={{ flex: 1 }}>
|
|
||||||
<div style={{ fontSize: 18, fontWeight: 600, color: "#faad14" }}>
|
|
||||||
{stats.added}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 13, color: "#888" }}>已添加</div>
|
|
||||||
</Card>
|
|
||||||
<Card style={{ flex: 1 }}>
|
|
||||||
<div style={{ fontSize: 18, fontWeight: 600, color: "#bfbfbf" }}>
|
|
||||||
{stats.pending}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 13, color: "#888" }}>待添加</div>
|
|
||||||
</Card>
|
|
||||||
<Card style={{ flex: 1 }}>
|
|
||||||
<div style={{ fontSize: 18, fontWeight: 600, color: "#ff4d4f" }}>
|
|
||||||
{stats.failed}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 13, color: "#888" }}>添加失败</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
style={{ marginTop: 12 }}
|
style={{ marginTop: 12 }}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
|
|||||||
import { Popup } from "antd-mobile";
|
import { Popup } from "antd-mobile";
|
||||||
import { Select, Button } from "antd";
|
import { Select, Button } from "antd";
|
||||||
import DeviceSelection from "@/components/DeviceSelection";
|
import DeviceSelection from "@/components/DeviceSelection";
|
||||||
import type { UserStatus, ScenarioOption } from "./data";
|
import type { ScenarioOption } from "./data";
|
||||||
import { fetchScenarioOptions, fetchPackageOptions } from "./api";
|
import { fetchScenarioOptions, fetchPackageOptions } from "./api";
|
||||||
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
||||||
|
|
||||||
@@ -10,13 +10,21 @@ interface FilterModalProps {
|
|||||||
visible: boolean;
|
visible: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onConfirm: (filters: {
|
onConfirm: (filters: {
|
||||||
deviceIds: string[];
|
selectedDevices: DeviceSelectionItem[]; // 更新为 deviceld
|
||||||
packageId: string;
|
packageld: number; // 更新为 packageld
|
||||||
scenarioId: string;
|
sceneId: number; // 更新为 sceneId
|
||||||
userValue: number;
|
userValue: number;
|
||||||
userStatus: number;
|
addStatus: number; // 更新为 addStatus
|
||||||
}) => void;
|
}) => void;
|
||||||
scenarioOptions: ScenarioOption[];
|
scenarioOptions: ScenarioOption[];
|
||||||
|
// 初始筛选值
|
||||||
|
initialFilters?: {
|
||||||
|
selectedDevices: DeviceSelectionItem[];
|
||||||
|
packageId: number;
|
||||||
|
scenarioId: number;
|
||||||
|
userValue: number;
|
||||||
|
userStatus: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const valueLevelOptions = [
|
const valueLevelOptions = [
|
||||||
@@ -37,17 +45,35 @@ const FilterModal: React.FC<FilterModalProps> = ({
|
|||||||
visible,
|
visible,
|
||||||
onClose,
|
onClose,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
|
initialFilters,
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedDevices, setSelectedDevices] = useState<DeviceSelectionItem[]>(
|
const [selectedDevices, setSelectedDevices] = useState<DeviceSelectionItem[]>(
|
||||||
[],
|
initialFilters?.selectedDevices || [],
|
||||||
|
);
|
||||||
|
const [packageId, setPackageId] = useState<number>(initialFilters?.packageId);
|
||||||
|
const [scenarioId, setScenarioId] = useState<number>(
|
||||||
|
initialFilters?.scenarioId,
|
||||||
|
);
|
||||||
|
const [userValue, setUserValue] = useState<number>(
|
||||||
|
initialFilters?.userValue || 0,
|
||||||
|
);
|
||||||
|
const [userStatus, setUserStatus] = useState<number>(
|
||||||
|
initialFilters?.userStatus || 0,
|
||||||
);
|
);
|
||||||
const [packageId, setPackageId] = useState<string>("");
|
|
||||||
const [scenarioId, setScenarioId] = useState<string>("");
|
|
||||||
const [userValue, setUserValue] = useState<number>(0);
|
|
||||||
const [userStatus, setUserStatus] = useState<number>(0);
|
|
||||||
const [scenarioOptions, setScenarioOptions] = useState<any[]>([]);
|
const [scenarioOptions, setScenarioOptions] = useState<any[]>([]);
|
||||||
const [packageOptions, setPackageOptions] = useState<any[]>([]);
|
const [packageOptions, setPackageOptions] = useState<any[]>([]);
|
||||||
|
|
||||||
|
// 同步初始值变化
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialFilters) {
|
||||||
|
setSelectedDevices(initialFilters.selectedDevices || []);
|
||||||
|
setPackageId(initialFilters.packageId || 0);
|
||||||
|
setScenarioId(initialFilters.scenarioId || 0);
|
||||||
|
setUserValue(initialFilters.userValue || 0);
|
||||||
|
setUserStatus(initialFilters.userStatus || 0);
|
||||||
|
}
|
||||||
|
}, [initialFilters]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
fetchScenarioOptions()
|
fetchScenarioOptions()
|
||||||
@@ -72,11 +98,11 @@ const FilterModal: React.FC<FilterModalProps> = ({
|
|||||||
|
|
||||||
const handleApply = () => {
|
const handleApply = () => {
|
||||||
const params = {
|
const params = {
|
||||||
deviceIds: selectedDevices.map(d => d.id.toString()),
|
selectedDevices: selectedDevices, // 更新为 deviceld
|
||||||
packageId,
|
packageld: packageId, // 更新为 packageld
|
||||||
scenarioId,
|
sceneId: scenarioId, // 更新为 sceneId
|
||||||
userValue,
|
userValue,
|
||||||
userStatus,
|
addStatus: userStatus, // 更新为 addStatus
|
||||||
};
|
};
|
||||||
console.log(params);
|
console.log(params);
|
||||||
|
|
||||||
@@ -86,8 +112,8 @@ const FilterModal: React.FC<FilterModalProps> = ({
|
|||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
setSelectedDevices([]);
|
setSelectedDevices([]);
|
||||||
setPackageId("");
|
setPackageId(0);
|
||||||
setScenarioId("");
|
setScenarioId(0);
|
||||||
setUserValue(0);
|
setUserValue(0);
|
||||||
setUserStatus(0);
|
setUserStatus(0);
|
||||||
};
|
};
|
||||||
@@ -119,7 +145,7 @@ const FilterModal: React.FC<FilterModalProps> = ({
|
|||||||
value={packageId}
|
value={packageId}
|
||||||
onChange={setPackageId}
|
onChange={setPackageId}
|
||||||
options={[
|
options={[
|
||||||
{ label: "全部流量池", value: "" },
|
{ label: "全部流量池", value: 0 },
|
||||||
...packageOptions.map(p => ({ label: p.name, value: p.id })),
|
...packageOptions.map(p => ({ label: p.name, value: p.id })),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -131,7 +157,7 @@ const FilterModal: React.FC<FilterModalProps> = ({
|
|||||||
value={scenarioId}
|
value={scenarioId}
|
||||||
onChange={setScenarioId}
|
onChange={setScenarioId}
|
||||||
options={[
|
options={[
|
||||||
{ label: "全部场景", value: "" },
|
{ label: "全部场景", value: 0 },
|
||||||
...scenarioOptions.map(s => ({ label: s.name, value: s.id })),
|
...scenarioOptions.map(s => ({ label: s.name, value: s.id })),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -16,3 +16,19 @@ export async function fetchScenarioOptions() {
|
|||||||
export async function fetchPackageOptions() {
|
export async function fetchPackageOptions() {
|
||||||
return request("/v1/traffic/pool/getPackage", {}, "GET");
|
return request("/v1/traffic/pool/getPackage", {}, "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function addPackage(params: {
|
||||||
|
type: string; // 类型 1搜索 2选择用户 3文件上传
|
||||||
|
addPackageId?: number;
|
||||||
|
addStatus?: number;
|
||||||
|
deviceId?: string;
|
||||||
|
keyword?: string;
|
||||||
|
packageId?: number;
|
||||||
|
packageName?: number; // 添加的流量池名称
|
||||||
|
tableFile?: number;
|
||||||
|
taskId?: number; // 任务id j及场景获客id
|
||||||
|
userIds?: number[];
|
||||||
|
userValue?: number;
|
||||||
|
}) {
|
||||||
|
return request("/v1/traffic/pool/addPackage", params, "POST");
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,182 +0,0 @@
|
|||||||
import { useState, useEffect, useMemo } from "react";
|
|
||||||
import {
|
|
||||||
fetchTrafficPoolList,
|
|
||||||
fetchPackageOptions,
|
|
||||||
fetchScenarioOptions,
|
|
||||||
} from "./api";
|
|
||||||
import type { TrafficPoolUser, PackageOption, ScenarioOption } from "./data";
|
|
||||||
import { Toast } from "antd-mobile";
|
|
||||||
|
|
||||||
export function useTrafficPoolListLogic() {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [list, setList] = useState<TrafficPoolUser[]>([]);
|
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [pageSize] = useState(10);
|
|
||||||
const [total, setTotal] = useState(0);
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
|
|
||||||
// 筛选相关
|
|
||||||
const [showFilter, setShowFilter] = useState(false);
|
|
||||||
const [packageOptions, setPackageOptions] = useState<PackageOption[]>([]);
|
|
||||||
const [scenarioOptions, setScenarioOptions] = useState<ScenarioOption[]>([]);
|
|
||||||
const [selectedDevices, setSelectedDevices] = useState<any[]>([]);
|
|
||||||
const [packageId, setPackageId] = useState<number>(0);
|
|
||||||
const [scenarioId, setScenarioId] = useState<number>(0);
|
|
||||||
const [userValue, setUserValue] = useState<number>(0);
|
|
||||||
const [userStatus, setUserStatus] = useState<number>(0);
|
|
||||||
|
|
||||||
// 批量相关
|
|
||||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
|
||||||
const [batchModal, setBatchModal] = useState(false);
|
|
||||||
const [batchTarget, setBatchTarget] = useState<string>("");
|
|
||||||
|
|
||||||
// 数据分析
|
|
||||||
const [showStats, setShowStats] = useState(false);
|
|
||||||
const stats = useMemo(() => {
|
|
||||||
const total = list.length;
|
|
||||||
const highValue = list.filter(
|
|
||||||
u => u.tags && u.tags.includes("高价值客户池"),
|
|
||||||
).length;
|
|
||||||
const added = list.filter(u => u.status === 1).length;
|
|
||||||
const pending = list.filter(u => u.status === 0).length;
|
|
||||||
const failed = list.filter(u => u.status === -1).length;
|
|
||||||
const addSuccessRate = total ? Math.round((added / total) * 100) : 0;
|
|
||||||
return { total, highValue, added, pending, failed, addSuccessRate };
|
|
||||||
}, [list]);
|
|
||||||
|
|
||||||
// 获取列表
|
|
||||||
const getList = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const params: any = {
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
keyword: search,
|
|
||||||
packageId,
|
|
||||||
taskId: scenarioId,
|
|
||||||
userValue,
|
|
||||||
addStatus: userStatus,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加筛选参数
|
|
||||||
if (selectedDevices.length > 0) {
|
|
||||||
params.deviceId = selectedDevices.map(d => d.id).join(",");
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetchTrafficPoolList(params);
|
|
||||||
setList(res.list || []);
|
|
||||||
setTotal(res.total || 0);
|
|
||||||
} catch (error) {
|
|
||||||
// 忽略请求过于频繁的错误,避免页面崩溃
|
|
||||||
if (error !== "请求过于频繁,请稍后再试") {
|
|
||||||
console.error("获取列表失败:", error);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取筛选项
|
|
||||||
useEffect(() => {
|
|
||||||
fetchPackageOptions().then(res => {
|
|
||||||
setPackageOptions(res.list || []);
|
|
||||||
});
|
|
||||||
fetchScenarioOptions().then(res => {
|
|
||||||
setScenarioOptions(res.list || []);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 筛选条件变化时刷新列表
|
|
||||||
useEffect(() => {
|
|
||||||
getList();
|
|
||||||
// eslint-disable-next-line
|
|
||||||
}, [
|
|
||||||
page,
|
|
||||||
search,
|
|
||||||
selectedDevices,
|
|
||||||
packageId,
|
|
||||||
scenarioId,
|
|
||||||
userValue,
|
|
||||||
userStatus,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 全选/反选
|
|
||||||
const handleSelectAll = (checked: boolean) => {
|
|
||||||
if (checked) {
|
|
||||||
setSelectedIds(list.map(item => item.id));
|
|
||||||
} else {
|
|
||||||
setSelectedIds([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// 单选
|
|
||||||
const handleSelect = (id: number, checked: boolean) => {
|
|
||||||
setSelectedIds(prev =>
|
|
||||||
checked ? [...prev, id] : prev.filter(i => i !== id),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 批量加入分组/流量池
|
|
||||||
const handleBatchAdd = () => {
|
|
||||||
if (!batchTarget) {
|
|
||||||
Toast.show({ content: "请选择目标分组", position: "top" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// TODO: 调用后端批量接口,这里仅模拟
|
|
||||||
Toast.show({
|
|
||||||
content: `已将${selectedIds.length}个用户加入${packageOptions.find(p => p.id === batchTarget)?.name || ""}`,
|
|
||||||
position: "top",
|
|
||||||
});
|
|
||||||
setBatchModal(false);
|
|
||||||
setSelectedIds([]);
|
|
||||||
setBatchTarget("");
|
|
||||||
// 可刷新列表
|
|
||||||
};
|
|
||||||
|
|
||||||
// 筛选重置
|
|
||||||
const resetFilter = () => {
|
|
||||||
setSelectedDevices([]);
|
|
||||||
setPackageId(0);
|
|
||||||
setScenarioId(0);
|
|
||||||
setUserValue(0);
|
|
||||||
setUserStatus(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
loading,
|
|
||||||
list,
|
|
||||||
page,
|
|
||||||
setPage,
|
|
||||||
pageSize,
|
|
||||||
total,
|
|
||||||
search,
|
|
||||||
setSearch,
|
|
||||||
showFilter,
|
|
||||||
setShowFilter,
|
|
||||||
packageOptions,
|
|
||||||
scenarioOptions,
|
|
||||||
selectedDevices,
|
|
||||||
setSelectedDevices,
|
|
||||||
packageId,
|
|
||||||
setPackageId,
|
|
||||||
scenarioId,
|
|
||||||
setScenarioId,
|
|
||||||
userValue,
|
|
||||||
setUserValue,
|
|
||||||
userStatus,
|
|
||||||
setUserStatus,
|
|
||||||
selectedIds,
|
|
||||||
setSelectedIds,
|
|
||||||
handleSelectAll,
|
|
||||||
handleSelect,
|
|
||||||
batchModal,
|
|
||||||
setBatchModal,
|
|
||||||
batchTarget,
|
|
||||||
setBatchTarget,
|
|
||||||
handleBatchAdd,
|
|
||||||
showStats,
|
|
||||||
setShowStats,
|
|
||||||
stats,
|
|
||||||
getList,
|
|
||||||
resetFilter,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -5,51 +5,155 @@ import {
|
|||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
BarChartOutlined,
|
BarChartOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
|
import { Toast } from "antd-mobile";
|
||||||
import { Input, Button, Checkbox, Pagination } from "antd";
|
import { Input, Button, Checkbox, Pagination } from "antd";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
import { Empty, Avatar } from "antd-mobile";
|
import { Empty, Avatar } from "antd-mobile";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import NavCommon from "@/components/NavCommon";
|
import NavCommon from "@/components/NavCommon";
|
||||||
import { useTrafficPoolListLogic } from "./dataAnyx";
|
import { fetchTrafficPoolList, fetchScenarioOptions, addPackage } from "./api";
|
||||||
|
import type { TrafficPoolUser, ScenarioOption } from "./data";
|
||||||
import DataAnalysisPanel from "./DataAnalysisPanel";
|
import DataAnalysisPanel from "./DataAnalysisPanel";
|
||||||
import FilterModal from "./FilterModal";
|
import FilterModal from "./FilterModal";
|
||||||
import BatchAddModal from "./BatchAddModal";
|
import BatchAddModal from "./BatchAddModal";
|
||||||
|
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
||||||
const defaultAvatar =
|
const defaultAvatar =
|
||||||
"https://cdn.jsdelivr.net/gh/maokaka/static/avatar-default.png";
|
"https://cdn.jsdelivr.net/gh/maokaka/static/avatar-default.png";
|
||||||
|
|
||||||
const TrafficPoolList: React.FC = () => {
|
const TrafficPoolList: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const {
|
|
||||||
loading,
|
// 基础状态
|
||||||
list,
|
const [loading, setLoading] = useState(false);
|
||||||
page,
|
const [list, setList] = useState<TrafficPoolUser[]>([]);
|
||||||
setPage,
|
const [page, setPage] = useState(1);
|
||||||
total,
|
const [pageSize] = useState(10);
|
||||||
search,
|
const [total, setTotal] = useState(0);
|
||||||
setSearch,
|
const [search, setSearch] = useState("");
|
||||||
showFilter,
|
|
||||||
setShowFilter,
|
// 筛选相关
|
||||||
packageOptions,
|
const [showFilter, setShowFilter] = useState(false);
|
||||||
scenarioOptions,
|
const [scenarioOptions, setScenarioOptions] = useState<ScenarioOption[]>([]);
|
||||||
setSelectedDevices,
|
|
||||||
setPackageId,
|
// 公共筛选条件状态
|
||||||
setScenarioId,
|
const [filterParams, setFilterParams] = useState({
|
||||||
setUserValue,
|
selectedDevices: [] as DeviceSelectionItem[],
|
||||||
setUserStatus,
|
packageId: 0,
|
||||||
selectedIds,
|
scenarioId: 0,
|
||||||
handleSelectAll,
|
userValue: 0,
|
||||||
handleSelect,
|
userStatus: 0,
|
||||||
batchModal,
|
});
|
||||||
setBatchModal,
|
|
||||||
batchTarget,
|
// 批量相关
|
||||||
setBatchTarget,
|
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||||
handleBatchAdd,
|
const [batchModal, setBatchModal] = useState(false);
|
||||||
showStats,
|
|
||||||
setShowStats,
|
// 数据分析
|
||||||
stats,
|
const [showStats, setShowStats] = useState(false);
|
||||||
getList,
|
|
||||||
} = useTrafficPoolListLogic();
|
// 获取列表
|
||||||
|
const getList = async (customParams?: any) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const params: any = {
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
keyword: search,
|
||||||
|
packageld: filterParams.packageId,
|
||||||
|
sceneId: filterParams.scenarioId,
|
||||||
|
userValue: filterParams.userValue,
|
||||||
|
addStatus: filterParams.userStatus,
|
||||||
|
deviceld: filterParams.selectedDevices.map(d => d.id).join(),
|
||||||
|
...customParams, // 允许传入自定义参数覆盖
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetchTrafficPoolList(params);
|
||||||
|
setList(res.list || []);
|
||||||
|
setTotal(res.total || 0);
|
||||||
|
} catch (error) {
|
||||||
|
// 忽略请求过于频繁的错误,避免页面崩溃
|
||||||
|
if (error !== "请求过于频繁,请稍后再试") {
|
||||||
|
console.error("获取列表失败:", error);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取筛选项
|
||||||
|
useEffect(() => {
|
||||||
|
fetchScenarioOptions().then(res => {
|
||||||
|
setScenarioOptions(res.list || []);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 全选/反选
|
||||||
|
const handleSelectAll = (checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
setSelectedIds(list.map(item => item.id));
|
||||||
|
} else {
|
||||||
|
setSelectedIds([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 单选
|
||||||
|
const handleSelect = (id: number, checked: boolean) => {
|
||||||
|
setSelectedIds(prev =>
|
||||||
|
checked ? [...prev, id] : prev.filter(i => i !== id),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 批量加入分组/流量池
|
||||||
|
const handleBatchAdd = async options => {
|
||||||
|
try {
|
||||||
|
// 构建请求参数
|
||||||
|
const params = {
|
||||||
|
type: "2", // 2选择用户
|
||||||
|
addPackageId: options.selectedPackageId, // 目标分组ID
|
||||||
|
userIds: selectedIds.map(id => id), // 选中的用户ID数组
|
||||||
|
// 如果有当前筛选条件,也可以传递
|
||||||
|
...(filterParams.packageId && {
|
||||||
|
packageId: filterParams.packageId,
|
||||||
|
}),
|
||||||
|
...(filterParams.scenarioId && {
|
||||||
|
taskId: filterParams.scenarioId,
|
||||||
|
}),
|
||||||
|
...(filterParams.userValue && {
|
||||||
|
userValue: filterParams.userValue,
|
||||||
|
}),
|
||||||
|
...(filterParams.userStatus && {
|
||||||
|
addStatus: filterParams.userStatus,
|
||||||
|
}),
|
||||||
|
...(filterParams.selectedDevices.length > 0 && {
|
||||||
|
deviceId: filterParams.selectedDevices.map(d => d.id).join(","),
|
||||||
|
}),
|
||||||
|
...(search && { keyword: search }),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("批量加入请求参数:", params);
|
||||||
|
|
||||||
|
// 调用接口
|
||||||
|
const result = await addPackage(params);
|
||||||
|
console.log("批量加入结果:", result);
|
||||||
|
|
||||||
|
// 成功后刷新列表
|
||||||
|
getList();
|
||||||
|
|
||||||
|
// 关闭弹窗并清空选择
|
||||||
|
setBatchModal(false);
|
||||||
|
setSelectedIds([]);
|
||||||
|
|
||||||
|
// 可以添加成功提示
|
||||||
|
Toast.show({
|
||||||
|
content: `成功将用户加入分组`,
|
||||||
|
position: "top",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("批量加入失败:", error);
|
||||||
|
// 可以添加错误提示
|
||||||
|
Toast.show({ content: "批量加入失败,请重试", position: "top" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 搜索防抖处理
|
// 搜索防抖处理
|
||||||
const [searchInput, setSearchInput] = useState(search);
|
const [searchInput, setSearchInput] = useState(search);
|
||||||
@@ -57,16 +161,25 @@ const TrafficPoolList: React.FC = () => {
|
|||||||
const debouncedSearch = useCallback(() => {
|
const debouncedSearch = useCallback(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setSearch(searchInput);
|
setSearch(searchInput);
|
||||||
|
// 搜索时重置到第一页并请求列表
|
||||||
|
setPage(1);
|
||||||
|
getList({ keyword: searchInput, page: 1 });
|
||||||
}, 500); // 500ms 防抖延迟
|
}, 500); // 500ms 防抖延迟
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [searchInput, setSearch]);
|
}, [searchInput]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cleanup = debouncedSearch();
|
const cleanup = debouncedSearch();
|
||||||
return cleanup;
|
return cleanup;
|
||||||
}, [debouncedSearch]);
|
}, [debouncedSearch]);
|
||||||
|
|
||||||
|
const handSearch = (value: string) => {
|
||||||
|
setSearchInput(value);
|
||||||
|
setSelectedIds([]);
|
||||||
|
debouncedSearch();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@@ -89,14 +202,14 @@ const TrafficPoolList: React.FC = () => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="搜索计划名称"
|
placeholder="搜索计划名称"
|
||||||
value={searchInput}
|
value={searchInput}
|
||||||
onChange={e => setSearchInput(e.target.value)}
|
onChange={e => handSearch(e.target.value)}
|
||||||
prefix={<SearchOutlined />}
|
prefix={<SearchOutlined />}
|
||||||
allowClear
|
allowClear
|
||||||
size="large"
|
size="large"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={getList}
|
onClick={() => getList()}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
size="large"
|
size="large"
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined />}
|
||||||
@@ -104,9 +217,12 @@ const TrafficPoolList: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
{/* 数据分析面板 */}
|
{/* 数据分析面板 */}
|
||||||
<DataAnalysisPanel
|
<DataAnalysisPanel
|
||||||
stats={stats}
|
|
||||||
showStats={showStats}
|
showStats={showStats}
|
||||||
setShowStats={setShowStats}
|
setShowStats={setShowStats}
|
||||||
|
onConfirm={statsData => {
|
||||||
|
// 可以在这里处理统计数据,比如更新本地状态或发送到父组件
|
||||||
|
console.log("收到统计数据:", statsData);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 批量操作栏 */}
|
{/* 批量操作栏 */}
|
||||||
@@ -114,7 +230,7 @@ const TrafficPoolList: React.FC = () => {
|
|||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
padding: "8px 12px",
|
padding: "8px 12px 8px 26px",
|
||||||
background: "#fff",
|
background: "#fff",
|
||||||
borderBottom: "1px solid #f0f0f0",
|
borderBottom: "1px solid #f0f0f0",
|
||||||
}}
|
}}
|
||||||
@@ -140,6 +256,18 @@ const TrafficPoolList: React.FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{searchInput.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
style={{ marginLeft: 16 }}
|
||||||
|
onClick={() => setBatchModal(true)}
|
||||||
|
>
|
||||||
|
导入当前搜索结果
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
@@ -158,7 +286,10 @@ const TrafficPoolList: React.FC = () => {
|
|||||||
pageSize={20}
|
pageSize={20}
|
||||||
total={total}
|
total={total}
|
||||||
showSizeChanger={false}
|
showSizeChanger={false}
|
||||||
onChange={setPage}
|
onChange={newPage => {
|
||||||
|
setPage(newPage);
|
||||||
|
getList({ page: newPage });
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -167,35 +298,40 @@ const TrafficPoolList: React.FC = () => {
|
|||||||
<BatchAddModal
|
<BatchAddModal
|
||||||
visible={batchModal}
|
visible={batchModal}
|
||||||
onClose={() => setBatchModal(false)}
|
onClose={() => setBatchModal(false)}
|
||||||
packageOptions={packageOptions}
|
|
||||||
batchTarget={batchTarget}
|
|
||||||
setBatchTarget={setBatchTarget}
|
|
||||||
selectedCount={selectedIds.length}
|
selectedCount={selectedIds.length}
|
||||||
onConfirm={handleBatchAdd}
|
onConfirm={data => {
|
||||||
|
// 处理批量加入逻辑
|
||||||
|
handleBatchAdd(data);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{/* 筛选弹窗 */}
|
{/* 筛选弹窗 */}
|
||||||
<FilterModal
|
<FilterModal
|
||||||
visible={showFilter}
|
visible={showFilter}
|
||||||
onClose={() => setShowFilter(false)}
|
onClose={() => setShowFilter(false)}
|
||||||
onConfirm={filters => {
|
onConfirm={filters => {
|
||||||
// 更新筛选条件
|
// 更新公共筛选条件状态
|
||||||
setSelectedDevices(
|
const newFilterParams = {
|
||||||
filters.deviceIds.map(id => ({
|
selectedDevices: filters.selectedDevices,
|
||||||
id: parseInt(id),
|
packageId: filters.packageld,
|
||||||
memo: "",
|
scenarioId: filters.sceneId,
|
||||||
imei: "",
|
userValue: filters.userValue,
|
||||||
wechatId: "",
|
userStatus: filters.addStatus,
|
||||||
status: "offline" as const,
|
};
|
||||||
})),
|
|
||||||
);
|
setFilterParams(newFilterParams);
|
||||||
setPackageId(filters.packageId ? parseInt(filters.packageId) : 0);
|
// 重置到第一页并请求列表
|
||||||
setScenarioId(filters.scenarioId ? parseInt(filters.scenarioId) : 0);
|
setPage(1);
|
||||||
setUserValue(filters.userValue);
|
getList({
|
||||||
setUserStatus(filters.userStatus);
|
page: 1,
|
||||||
// 重新获取列表
|
packageld: newFilterParams.packageId,
|
||||||
getList();
|
sceneId: newFilterParams.scenarioId,
|
||||||
|
userValue: newFilterParams.userValue,
|
||||||
|
addStatus: newFilterParams.userStatus,
|
||||||
|
deviceld: newFilterParams.selectedDevices.map(d => d.id).join(),
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
scenarioOptions={scenarioOptions}
|
scenarioOptions={scenarioOptions}
|
||||||
|
initialFilters={filterParams}
|
||||||
/>
|
/>
|
||||||
<div className={styles.listWrap}>
|
<div className={styles.listWrap}>
|
||||||
{list.length === 0 && !loading ? (
|
{list.length === 0 && !loading ? (
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export interface Allocation {
|
|||||||
/** 设备id */
|
/** 设备id */
|
||||||
deviceGroups: number[];
|
deviceGroups: number[];
|
||||||
/** 流量池 */
|
/** 流量池 */
|
||||||
pools?: JSON | null;
|
poolGroups?: number[];
|
||||||
|
|
||||||
/** 分配数量 */
|
/** 分配数量 */
|
||||||
num?: number | null;
|
num?: number | null;
|
||||||
@@ -72,7 +72,7 @@ export interface ContactImportTaskConfig {
|
|||||||
id: number;
|
id: number;
|
||||||
workbenchId: number;
|
workbenchId: number;
|
||||||
devices: number[];
|
devices: number[];
|
||||||
pools: number[];
|
poolGroups: number[];
|
||||||
num: number;
|
num: number;
|
||||||
clearContact: number;
|
clearContact: number;
|
||||||
remarkType: number;
|
remarkType: number;
|
||||||
@@ -114,7 +114,7 @@ export interface CreateContactImportTaskData {
|
|||||||
type: number;
|
type: number;
|
||||||
config: {
|
config: {
|
||||||
devices: number[];
|
devices: number[];
|
||||||
pools: number[];
|
poolGroups: number[];
|
||||||
num: number;
|
num: number;
|
||||||
clearContact: number;
|
clearContact: number;
|
||||||
remarkType: number;
|
remarkType: number;
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { PlusOutlined, MinusOutlined } from "@ant-design/icons";
|
import { PlusOutlined, MinusOutlined } from "@ant-design/icons";
|
||||||
import { Button, Input, message, TimePicker, Select, Switch } from "antd";
|
import { Button, Input, message, TimePicker, Select, Switch } from "antd";
|
||||||
import NavCommon from "@/components/NavCommon";
|
import NavCommon from "@/components/NavCommon";
|
||||||
import Layout from "@/components/Layout/Layout";
|
import Layout from "@/components/Layout/Layout";
|
||||||
import DeviceSelection from "@/components/DeviceSelection";
|
import DeviceSelection from "@/components/DeviceSelection";
|
||||||
|
import PoolSelection from "@/components/PoolSelection";
|
||||||
import {
|
import {
|
||||||
createContactImportTask,
|
createContactImportTask,
|
||||||
updateContactImportTask,
|
updateContactImportTask,
|
||||||
fetchContactImportTaskDetail,
|
fetchContactImportTaskDetail,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
import { Allocation } from "./data";
|
import { Allocation } from "./data";
|
||||||
|
import { PoolSelectionItem } from "@/components/PoolSelection/data";
|
||||||
|
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
||||||
import style from "./index.module.scss";
|
import style from "./index.module.scss";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
@@ -27,7 +29,7 @@ const ContactImportForm: React.FC = () => {
|
|||||||
type: 6, // 任务类型,固定为6
|
type: 6, // 任务类型,固定为6
|
||||||
workbenchId: 1, // 默认工作台ID
|
workbenchId: 1, // 默认工作台ID
|
||||||
deviceGroups: [] as number[],
|
deviceGroups: [] as number[],
|
||||||
pools: [] as any[],
|
poolGroups: [] as number[],
|
||||||
num: 50,
|
num: 50,
|
||||||
clearContact: 0,
|
clearContact: 0,
|
||||||
remarkType: 0,
|
remarkType: 0,
|
||||||
@@ -35,7 +37,8 @@ const ContactImportForm: React.FC = () => {
|
|||||||
startTime: dayjs("09:00", "HH:mm"),
|
startTime: dayjs("09:00", "HH:mm"),
|
||||||
endTime: dayjs("21:00", "HH:mm"),
|
endTime: dayjs("21:00", "HH:mm"),
|
||||||
// 保留原有字段用于UI显示
|
// 保留原有字段用于UI显示
|
||||||
deviceGroupsOptions: [] as any[],
|
deviceGroupsOptions: [] as DeviceSelectionItem[],
|
||||||
|
poolGroupsOptions: [] as PoolSelectionItem[], // 流量池选择项
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理设备选择
|
// 处理设备选择
|
||||||
@@ -47,8 +50,17 @@ const ContactImportForm: React.FC = () => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理流量池选择
|
||||||
|
const handlePoolSelect = (selectedpoolGroups: PoolSelectionItem[]) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
poolGroupsOptions: selectedpoolGroups,
|
||||||
|
poolGroups: selectedpoolGroups.map(pool => Number(pool.id)), // 提取流量池信息存储到pools数组
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
// 获取任务详情(编辑模式)
|
// 获取任务详情(编辑模式)
|
||||||
const loadTaskDetail = async () => {
|
const loadTaskDetail = useCallback(async () => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -59,6 +71,7 @@ const ContactImportForm: React.FC = () => {
|
|||||||
|
|
||||||
// 构造设备选择组件需要的数据格式
|
// 构造设备选择组件需要的数据格式
|
||||||
const deviceGroupsOptions = config.deviceGroupsOptions || [];
|
const deviceGroupsOptions = config.deviceGroupsOptions || [];
|
||||||
|
const poolGroupsOptions = config.poolGroupsOptions || [];
|
||||||
|
|
||||||
setFormData({
|
setFormData({
|
||||||
name: data.name || "",
|
name: data.name || "",
|
||||||
@@ -67,7 +80,7 @@ const ContactImportForm: React.FC = () => {
|
|||||||
workbenchId: config.workbenchId || 1,
|
workbenchId: config.workbenchId || 1,
|
||||||
deviceGroups:
|
deviceGroups:
|
||||||
deviceGroupsOptions.map((device: any) => device.id) || [],
|
deviceGroupsOptions.map((device: any) => device.id) || [],
|
||||||
pools: config.pools ? JSON.parse(JSON.stringify(config.pools)) : [],
|
poolGroups: config.poolGroups || [],
|
||||||
num: config.num || 50,
|
num: config.num || 50,
|
||||||
clearContact: config.clearContact || 0,
|
clearContact: config.clearContact || 0,
|
||||||
remarkType: config.remarkType || 0,
|
remarkType: config.remarkType || 0,
|
||||||
@@ -75,6 +88,7 @@ const ContactImportForm: React.FC = () => {
|
|||||||
startTime: config.startTime ? dayjs(config.startTime, "HH:mm") : null,
|
startTime: config.startTime ? dayjs(config.startTime, "HH:mm") : null,
|
||||||
endTime: config.endTime ? dayjs(config.endTime, "HH:mm") : null,
|
endTime: config.endTime ? dayjs(config.endTime, "HH:mm") : null,
|
||||||
deviceGroupsOptions,
|
deviceGroupsOptions,
|
||||||
|
poolGroupsOptions,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -84,7 +98,7 @@ const ContactImportForm: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [id, navigate]);
|
||||||
|
|
||||||
// 更新表单数据
|
// 更新表单数据
|
||||||
const handleUpdateFormData = (data: Partial<typeof formData>) => {
|
const handleUpdateFormData = (data: Partial<typeof formData>) => {
|
||||||
@@ -125,7 +139,7 @@ const ContactImportForm: React.FC = () => {
|
|||||||
type: formData.type,
|
type: formData.type,
|
||||||
workbenchId: formData.workbenchId,
|
workbenchId: formData.workbenchId,
|
||||||
deviceGroups: formData.deviceGroups,
|
deviceGroups: formData.deviceGroups,
|
||||||
pools: JSON.parse(JSON.stringify(formData.pools)),
|
poolGroups: formData.poolGroups,
|
||||||
num: formData.num,
|
num: formData.num,
|
||||||
clearContact: formData.clearContact,
|
clearContact: formData.clearContact,
|
||||||
remarkType: formData.remarkType,
|
remarkType: formData.remarkType,
|
||||||
@@ -161,7 +175,7 @@ const ContactImportForm: React.FC = () => {
|
|||||||
type: 6,
|
type: 6,
|
||||||
workbenchId: 1,
|
workbenchId: 1,
|
||||||
deviceGroups: [],
|
deviceGroups: [],
|
||||||
pools: [],
|
poolGroups: [],
|
||||||
num: 50,
|
num: 50,
|
||||||
clearContact: 0,
|
clearContact: 0,
|
||||||
remarkType: 0,
|
remarkType: 0,
|
||||||
@@ -169,6 +183,7 @@ const ContactImportForm: React.FC = () => {
|
|||||||
startTime: dayjs("09:00", "HH:mm"),
|
startTime: dayjs("09:00", "HH:mm"),
|
||||||
endTime: dayjs("21:00", "HH:mm"),
|
endTime: dayjs("21:00", "HH:mm"),
|
||||||
deviceGroupsOptions: [],
|
deviceGroupsOptions: [],
|
||||||
|
poolGroupsOptions: [],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -176,7 +191,7 @@ const ContactImportForm: React.FC = () => {
|
|||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
loadTaskDetail();
|
loadTaskDetail();
|
||||||
}
|
}
|
||||||
}, [id, isEdit]);
|
}, [id, isEdit, loadTaskDetail]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
@@ -227,6 +242,17 @@ const ContactImportForm: React.FC = () => {
|
|||||||
<div className={style.counterTip}>选择要分配联系人的设备</div>
|
<div className={style.counterTip}>选择要分配联系人的设备</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={style.formItem}>
|
||||||
|
<div className={style.formLabel}>流量池选择</div>
|
||||||
|
<PoolSelection
|
||||||
|
selectedOptions={formData.poolGroupsOptions}
|
||||||
|
onSelect={handlePoolSelect}
|
||||||
|
placeholder="请选择流量池"
|
||||||
|
className={style.poolSelection}
|
||||||
|
/>
|
||||||
|
<div className={style.counterTip}>选择要导入的流量池</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={style.formItem}>
|
<div className={style.formItem}>
|
||||||
<div className={style.formLabel}>分配数量</div>
|
<div className={style.formLabel}>分配数量</div>
|
||||||
<div className={style.stepperContainer}>
|
<div className={style.stepperContainer}>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export interface ContactImportTaskConfig {
|
|||||||
id: number;
|
id: number;
|
||||||
workbenchId: number;
|
workbenchId: number;
|
||||||
devices: number[];
|
devices: number[];
|
||||||
pools: number[];
|
poolGroups: number[];
|
||||||
num: number;
|
num: number;
|
||||||
clearContact: number;
|
clearContact: number;
|
||||||
remarkType: number;
|
remarkType: number;
|
||||||
@@ -77,7 +77,7 @@ export interface CreateContactImportTaskData {
|
|||||||
type: number;
|
type: number;
|
||||||
config: {
|
config: {
|
||||||
devices: number[];
|
devices: number[];
|
||||||
pools: number[];
|
poolGroups: number[];
|
||||||
num: number;
|
num: number;
|
||||||
clearContact: number;
|
clearContact: number;
|
||||||
remarkType: number;
|
remarkType: number;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export interface TrafficDistributionConfig {
|
|||||||
accountGroupsOptions: any[];
|
accountGroupsOptions: any[];
|
||||||
deviceGroups: any[];
|
deviceGroups: any[];
|
||||||
deviceGroupsOptions: any[];
|
deviceGroupsOptions: any[];
|
||||||
pools: any[];
|
poolGroups: any[];
|
||||||
exp: number;
|
exp: number;
|
||||||
createTime: string;
|
createTime: string;
|
||||||
updateTime: string;
|
updateTime: string;
|
||||||
@@ -58,7 +58,7 @@ export interface TrafficDistributionFormData {
|
|||||||
deviceGroupsOptions: any[];
|
deviceGroupsOptions: any[];
|
||||||
accountGroups: any[];
|
accountGroups: any[];
|
||||||
accountGroupsOptions: any[];
|
accountGroupsOptions: any[];
|
||||||
pools: any[];
|
poolGroups: any[];
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import {
|
import { Form, Input, Button, Radio, Slider, TimePicker, message } from "antd";
|
||||||
Form,
|
|
||||||
Input,
|
|
||||||
Button,
|
|
||||||
Radio,
|
|
||||||
Slider,
|
|
||||||
TimePicker,
|
|
||||||
message,
|
|
||||||
Checkbox,
|
|
||||||
} from "antd";
|
|
||||||
import { SearchOutlined } from "@ant-design/icons";
|
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import style from "./index.module.scss";
|
import style from "./index.module.scss";
|
||||||
import StepIndicator from "@/components/StepIndicator";
|
import StepIndicator from "@/components/StepIndicator";
|
||||||
@@ -19,38 +9,16 @@ import AccountSelection from "@/components/AccountSelection";
|
|||||||
import { AccountItem } from "@/components/AccountSelection/data";
|
import { AccountItem } from "@/components/AccountSelection/data";
|
||||||
import DeviceSelection from "@/components/DeviceSelection";
|
import DeviceSelection from "@/components/DeviceSelection";
|
||||||
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
||||||
|
import PoolSelection from "@/components/PoolSelection";
|
||||||
|
import { PoolSelectionItem } from "@/components/PoolSelection/data";
|
||||||
import {
|
import {
|
||||||
getTrafficDistributionDetail,
|
getTrafficDistributionDetail,
|
||||||
updateTrafficDistribution,
|
updateTrafficDistribution,
|
||||||
createTrafficDistribution,
|
createTrafficDistribution,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
import type {
|
import type { TrafficDistributionFormData } from "./data";
|
||||||
TrafficDistributionDetail,
|
|
||||||
TrafficDistributionFormData,
|
|
||||||
} from "./data";
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
const scenarioList = [
|
|
||||||
{ label: "海报获客", value: "poster" },
|
|
||||||
{ label: "电话获客", value: "phone" },
|
|
||||||
{ label: "抖音获客", value: "douyin" },
|
|
||||||
{ label: "小红书获客", value: "xiaohongshu" },
|
|
||||||
{ label: "微信群获客", value: "weixinqun" },
|
|
||||||
{ label: "API获客", value: "api" },
|
|
||||||
{ label: "订单获客", value: "order" },
|
|
||||||
{ label: "付款码获客", value: "payment" },
|
|
||||||
];
|
|
||||||
const poolList = [
|
|
||||||
{
|
|
||||||
id: "pool-1",
|
|
||||||
name: "高价值客户池",
|
|
||||||
userCount: 156,
|
|
||||||
tags: ["高价值", "优先添加"],
|
|
||||||
},
|
|
||||||
{ id: "pool-2", name: "潜在客户池", userCount: 289, tags: ["潜在客户"] },
|
|
||||||
{ id: "pool-3", name: "新用户池", userCount: 432, tags: ["新用户"] },
|
|
||||||
];
|
|
||||||
|
|
||||||
const stepList = [
|
const stepList = [
|
||||||
{ id: 1, title: "基本信息", subtitle: "基本信息" },
|
{ id: 1, title: "基本信息", subtitle: "基本信息" },
|
||||||
{ id: 2, title: "目标设置", subtitle: "目标设置" },
|
{ id: 2, title: "目标设置", subtitle: "目标设置" },
|
||||||
@@ -73,22 +41,20 @@ const TrafficDistributionForm: React.FC = () => {
|
|||||||
const [deviceGroupsOptions, setDeviceGroupsOptions] = useState<any[]>([]);
|
const [deviceGroupsOptions, setDeviceGroupsOptions] = useState<any[]>([]);
|
||||||
const [accountGroups, setAccountGroups] = useState<any[]>([]);
|
const [accountGroups, setAccountGroups] = useState<any[]>([]);
|
||||||
const [accountGroupsOptions, setAccountGroupsOptions] = useState<any[]>([]);
|
const [accountGroupsOptions, setAccountGroupsOptions] = useState<any[]>([]);
|
||||||
const [distributeType, setDistributeType] = useState(1);
|
// 使用 Form 管理字段,配合 useWatch 读取值
|
||||||
const [maxPerDay, setMaxPerDay] = useState(50);
|
|
||||||
const [timeType, setTimeType] = useState(1);
|
|
||||||
const [timeRange, setTimeRange] = useState<any>(null);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [detailLoading, setDetailLoading] = useState(false);
|
const [detailLoading, setDetailLoading] = useState(false);
|
||||||
|
|
||||||
const [selectedPools, setSelectedPools] = useState<string[]>([]);
|
const [poolGroupsOptions, setPoolGroupsOptions] = useState<
|
||||||
const [poolSearch, setPoolSearch] = useState("");
|
PoolSelectionItem[]
|
||||||
const [targetSelectionTab, setTargetSelectionTab] = useState<
|
>([]);
|
||||||
"device" | "account"
|
|
||||||
>("device");
|
|
||||||
|
|
||||||
// 编辑时的详情数据
|
// 编辑时的详情数据(不需要保存整份数据,仅回填表单与本地状态)
|
||||||
const [detailData, setDetailData] =
|
|
||||||
useState<TrafficDistributionDetail | null>(null);
|
// 监听表单字段变化(antd v5 推荐)
|
||||||
|
const maxPerDay = Form.useWatch("maxPerDay", form);
|
||||||
|
const timeType = Form.useWatch("timeType", form);
|
||||||
|
// const timeRange = Form.useWatch("timeRange", form);
|
||||||
|
|
||||||
// 生成默认名称
|
// 生成默认名称
|
||||||
const generateDefaultName = () => {
|
const generateDefaultName = () => {
|
||||||
@@ -98,20 +64,12 @@ const TrafficDistributionForm: React.FC = () => {
|
|||||||
return `流量分发 ${dateStr} ${timeStr}`;
|
return `流量分发 ${dateStr} ${timeStr}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取详情数据
|
const fetchDetail = useCallback(async () => {
|
||||||
useEffect(() => {
|
|
||||||
if (isEdit && id) {
|
|
||||||
fetchDetail();
|
|
||||||
}
|
|
||||||
}, [isEdit, id]);
|
|
||||||
|
|
||||||
const fetchDetail = async () => {
|
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
setDetailLoading(true);
|
setDetailLoading(true);
|
||||||
try {
|
try {
|
||||||
const detail = await getTrafficDistributionDetail(id);
|
const detail = await getTrafficDistributionDetail(id);
|
||||||
setDetailData(detail);
|
|
||||||
|
|
||||||
// 回填表单数据
|
// 回填表单数据
|
||||||
const config = detail.config;
|
const config = detail.config;
|
||||||
@@ -122,11 +80,6 @@ const TrafficDistributionForm: React.FC = () => {
|
|||||||
timeType: config.timeType,
|
timeType: config.timeType,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 设置状态
|
|
||||||
setDistributeType(config.distributeType);
|
|
||||||
setMaxPerDay(config.maxPerDay);
|
|
||||||
setTimeType(config.timeType);
|
|
||||||
|
|
||||||
// 设置账号组数据
|
// 设置账号组数据
|
||||||
setAccountGroups(config.accountGroups || []);
|
setAccountGroups(config.accountGroups || []);
|
||||||
setAccountGroupsOptions(config.accountGroupsOptions || []);
|
setAccountGroupsOptions(config.accountGroupsOptions || []);
|
||||||
@@ -136,6 +89,8 @@ const TrafficDistributionForm: React.FC = () => {
|
|||||||
setDeviceGroups(config.deviceGroups || []);
|
setDeviceGroups(config.deviceGroups || []);
|
||||||
setDeviceGroupsOptions(config.deviceGroupsOptions || []);
|
setDeviceGroupsOptions(config.deviceGroupsOptions || []);
|
||||||
setSelectedDevices(config.deviceGroupsOptions || []);
|
setSelectedDevices(config.deviceGroupsOptions || []);
|
||||||
|
//设置流量池
|
||||||
|
setPoolGroupsOptions(config.poolGroupsOptions || []);
|
||||||
|
|
||||||
// 设置时间范围 - 使用dayjs格式
|
// 设置时间范围 - 使用dayjs格式
|
||||||
if (config.timeType === 2 && config.startTime && config.endTime) {
|
if (config.timeType === 2 && config.startTime && config.endTime) {
|
||||||
@@ -147,22 +102,37 @@ const TrafficDistributionForm: React.FC = () => {
|
|||||||
// 使用dayjs创建时间对象
|
// 使用dayjs创建时间对象
|
||||||
const startTime = dayjs().hour(startHour).minute(startMinute).second(0);
|
const startTime = dayjs().hour(startHour).minute(startMinute).second(0);
|
||||||
const endTime = dayjs().hour(endHour).minute(endMinute).second(0);
|
const endTime = dayjs().hour(endHour).minute(endMinute).second(0);
|
||||||
setTimeRange([startTime, endTime]);
|
form.setFieldsValue({ timeRange: [startTime, endTime] });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置流量池
|
// 设置流量池 - 交由 PoolSelection 控件受控
|
||||||
setSelectedPools(config.pools.map((pool: any) => pool.id || pool));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取详情失败:", error);
|
console.error("获取详情失败:", error);
|
||||||
message.error("获取详情失败");
|
message.error("获取详情失败");
|
||||||
} finally {
|
} finally {
|
||||||
setDetailLoading(false);
|
setDetailLoading(false);
|
||||||
}
|
}
|
||||||
|
}, [id, form]);
|
||||||
|
|
||||||
|
// 获取详情数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEdit && id) {
|
||||||
|
fetchDetail();
|
||||||
|
}
|
||||||
|
}, [isEdit, id, fetchDetail]);
|
||||||
|
const handleFinish = async () => {
|
||||||
|
form.submit();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFinish = async (values?: any) => {
|
const handleFinish2 = async (values?: any) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
// 校验流量池至少选择一个
|
||||||
|
if (!poolGroupsOptions || poolGroupsOptions.length === 0) {
|
||||||
|
message.error("请至少选择一个流量池");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// 如果没有传递values参数,从表单中获取
|
// 如果没有传递values参数,从表单中获取
|
||||||
const formValues = values || form.getFieldsValue();
|
const formValues = values || form.getFieldsValue();
|
||||||
|
|
||||||
@@ -173,18 +143,22 @@ const TrafficDistributionForm: React.FC = () => {
|
|||||||
source: "",
|
source: "",
|
||||||
sourceIcon: "",
|
sourceIcon: "",
|
||||||
description: "",
|
description: "",
|
||||||
distributeType: distributeType,
|
distributeType: formValues.distributeType,
|
||||||
maxPerDay: maxPerDay,
|
maxPerDay: formValues.maxPerDay,
|
||||||
timeType: timeType,
|
timeType: formValues.timeType,
|
||||||
startTime:
|
startTime:
|
||||||
timeType === 2 && timeRange?.[0] ? timeRange[0].format("HH:mm") : "",
|
formValues.timeType === 2 && formValues.timeRange?.[0]
|
||||||
|
? formValues.timeRange[0].format("HH:mm")
|
||||||
|
: "",
|
||||||
endTime:
|
endTime:
|
||||||
timeType === 2 && timeRange?.[1] ? timeRange[1].format("HH:mm") : "",
|
formValues.timeType === 2 && formValues.timeRange?.[1]
|
||||||
deviceGroups: deviceGroups,
|
? formValues.timeRange[1].format("HH:mm")
|
||||||
|
: "",
|
||||||
|
deviceGroups: deviceGroupsOptions.map(v => v.id),
|
||||||
deviceGroupsOptions: deviceGroupsOptions,
|
deviceGroupsOptions: deviceGroupsOptions,
|
||||||
accountGroups: accountGroups,
|
accountGroups: accountGroupsOptions.map(v => v.id),
|
||||||
accountGroupsOptions: accountGroupsOptions,
|
accountGroupsOptions: accountGroupsOptions,
|
||||||
pools: selectedPools,
|
poolGroups: poolGroupsOptions.map(v => v.id),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -216,15 +190,26 @@ const TrafficDistributionForm: React.FC = () => {
|
|||||||
.catch(() => {
|
.catch(() => {
|
||||||
// 验证失败,不进行下一步
|
// 验证失败,不进行下一步
|
||||||
});
|
});
|
||||||
|
} else if (current === 1) {
|
||||||
|
// 第二步:目标设置至少需要选择设备或客服之一
|
||||||
|
const hasDevice =
|
||||||
|
Array.isArray(selectedDevices) && selectedDevices.length > 0;
|
||||||
|
if (!hasDevice) {
|
||||||
|
message.error("请至少选择一个设备");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCurrent(cur => cur + 1);
|
||||||
} else {
|
} else {
|
||||||
setCurrent(cur => cur + 1);
|
setCurrent(cur => cur + 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const prev = () => setCurrent(cur => cur - 1);
|
const prev = () => setCurrent(cur => cur - 1);
|
||||||
|
|
||||||
// 过滤流量池
|
const handPoolAction = params => {
|
||||||
const filteredPools = poolList.filter(pool => pool.name.includes(poolSearch));
|
setPoolGroupsOptions(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 移除未使用的输入同步函数
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
header={
|
header={
|
||||||
@@ -262,204 +247,136 @@ const TrafficDistributionForm: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<div className={style.formPage}>
|
<div className={style.formPage}>
|
||||||
<div className={style.formBody}>
|
<div className={style.formBody}>
|
||||||
{current === 0 && (
|
<Form
|
||||||
<Form
|
form={form}
|
||||||
form={form}
|
layout="vertical"
|
||||||
layout="vertical"
|
initialValues={{
|
||||||
initialValues={{
|
name: isEdit ? "" : generateDefaultName(),
|
||||||
name: isEdit ? "" : generateDefaultName(),
|
distributeType: 1,
|
||||||
distributeType: 1,
|
maxPerDay: 50,
|
||||||
maxPerDay: 50,
|
timeType: 1,
|
||||||
timeType: 1,
|
}}
|
||||||
}}
|
disabled={detailLoading}
|
||||||
disabled={detailLoading}
|
onFinish={handleFinish2}
|
||||||
|
style={{ display: current === 0 ? "block" : "none" }}
|
||||||
|
>
|
||||||
|
<div className={style.sectionTitle}>基本信息</div>
|
||||||
|
<Form.Item
|
||||||
|
label="计划名称"
|
||||||
|
name="name"
|
||||||
|
rules={[{ required: true, message: "请输入计划名称" }]}
|
||||||
>
|
>
|
||||||
<div className={style.sectionTitle}>基本信息</div>
|
<Input placeholder="流量分发 20250724 1700" maxLength={30} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="分配方式" name="distributeType" required>
|
||||||
|
<Radio.Group className={style.radioGroup}>
|
||||||
|
<Radio value={1}>
|
||||||
|
均分配
|
||||||
|
<span className={style.radioDesc}>
|
||||||
|
(流量将均分分配给所有客服)
|
||||||
|
</span>
|
||||||
|
</Radio>
|
||||||
|
<Radio value={2}>
|
||||||
|
优先级分配
|
||||||
|
<span className={style.radioDesc}>
|
||||||
|
(按客服优先级顺序分配)
|
||||||
|
</span>
|
||||||
|
</Radio>
|
||||||
|
<Radio value={3}>
|
||||||
|
比例分配
|
||||||
|
<span className={style.radioDesc}>(按设置比例分配流量)</span>
|
||||||
|
</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="分配限制" required>
|
||||||
|
<div className={style.sliderLabelWrap}>
|
||||||
|
<span>每日最大分配量</span>
|
||||||
|
<span className={style.sliderValue}>
|
||||||
|
{maxPerDay || 0} 人/天
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="计划名称"
|
name="maxPerDay"
|
||||||
name="name"
|
noStyle
|
||||||
rules={[{ required: true, message: "请输入计划名称" }]}
|
rules={[{ required: true, message: "请设置每日最大分配量" }]}
|
||||||
>
|
>
|
||||||
<Input placeholder="流量分发 20250724 1700" maxLength={30} />
|
<Slider min={1} max={100} className={style.slider} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="分配方式" name="distributeType" required>
|
<div className={style.sliderDesc}>限制每天最多分配的流量数量</div>
|
||||||
<Radio.Group
|
</Form.Item>
|
||||||
value={distributeType}
|
<Form.Item label="时间限制" name="timeType" required>
|
||||||
onChange={e => setDistributeType(e.target.value)}
|
<Radio.Group className={style.radioGroup}>
|
||||||
className={style.radioGroup}
|
<Radio value={1}>全天分配</Radio>
|
||||||
>
|
<Radio value={2}>自定义时间段</Radio>
|
||||||
<Radio value={1}>
|
</Radio.Group>
|
||||||
均分配
|
</Form.Item>
|
||||||
<span className={style.radioDesc}>
|
{timeType === 2 && (
|
||||||
(流量将均分分配给所有客服)
|
<Form.Item
|
||||||
</span>
|
label=""
|
||||||
</Radio>
|
name="timeRange"
|
||||||
<Radio value={2}>
|
required
|
||||||
优先级分配
|
dependencies={["timeType"]}
|
||||||
<span className={style.radioDesc}>
|
rules={[
|
||||||
(按客服优先级顺序分配)
|
({ getFieldValue }) => ({
|
||||||
</span>
|
validator(_, value) {
|
||||||
</Radio>
|
if (getFieldValue("timeType") === 1) {
|
||||||
<Radio value={3}>
|
return Promise.resolve();
|
||||||
比例分配
|
}
|
||||||
<span className={style.radioDesc}>
|
if (value && value.length === 2) {
|
||||||
(按设置比例分配流量)
|
return Promise.resolve();
|
||||||
</span>
|
}
|
||||||
</Radio>
|
return Promise.reject(new Error("请选择开始和结束时间"));
|
||||||
</Radio.Group>
|
},
|
||||||
</Form.Item>
|
}),
|
||||||
<Form.Item label="分配限制" required>
|
]}
|
||||||
<div className={style.sliderLabelWrap}>
|
>
|
||||||
<span>每日最大分配量</span>
|
<TimePicker.RangePicker
|
||||||
<span className={style.sliderValue}>{maxPerDay} 人/天</span>
|
format="HH:mm"
|
||||||
</div>
|
style={{ width: "100%" }}
|
||||||
<Slider
|
|
||||||
min={1}
|
|
||||||
max={100}
|
|
||||||
value={maxPerDay}
|
|
||||||
onChange={setMaxPerDay}
|
|
||||||
className={style.slider}
|
|
||||||
/>
|
/>
|
||||||
<div className={style.sliderDesc}>
|
|
||||||
限制每天最多分配的流量数量
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="时间限制" name="timeType" required>
|
)}
|
||||||
<Radio.Group
|
|
||||||
value={timeType}
|
<div className={style.sectionTitle}>客服选择</div>
|
||||||
onChange={e => setTimeType(e.target.value)}
|
<div className={style.formBlock}>
|
||||||
className={style.radioGroup}
|
<AccountSelection
|
||||||
>
|
selectedOptions={accountGroupsOptions}
|
||||||
<Radio value={1}>全天分配</Radio>
|
onSelect={accounts => {
|
||||||
<Radio value={2}>自定义时间段</Radio>
|
setAccountGroupsOptions(accounts);
|
||||||
</Radio.Group>
|
}}
|
||||||
</Form.Item>
|
placeholder="请选择客服"
|
||||||
{timeType === 2 && (
|
showSelectedList={true}
|
||||||
<Form.Item label="" required>
|
selectedListMaxHeight={300}
|
||||||
<div className={style.timeRangeWrap}>
|
accountGroups={accountGroups}
|
||||||
<div>
|
/>
|
||||||
<span>开始时间</span>
|
</div>
|
||||||
<TimePicker
|
</Form>
|
||||||
format="HH:mm"
|
|
||||||
style={{ width: 120 }}
|
|
||||||
value={timeRange?.[0]}
|
|
||||||
onChange={v => setTimeRange([v, timeRange?.[1]])}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>结束时间</span>
|
|
||||||
<TimePicker
|
|
||||||
format="HH:mm"
|
|
||||||
style={{ width: 120 }}
|
|
||||||
value={timeRange?.[1]}
|
|
||||||
onChange={v => setTimeRange([timeRange?.[0], v])}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
{current === 1 && (
|
{current === 1 && (
|
||||||
<div>
|
<div>
|
||||||
<div className={style.sectionTitle}>目标设置</div>
|
<div className={style.sectionTitle}>目标设置</div>
|
||||||
|
<div className={style.formBlock}>
|
||||||
{/* Tab 切换 */}
|
<DeviceSelection
|
||||||
<div className={style.tabWrapper}>
|
selectedOptions={deviceGroupsOptions}
|
||||||
<div className={style.tabList}>
|
onSelect={devices => {
|
||||||
<div
|
setSelectedDevices(devices);
|
||||||
className={`${style.tabItem} ${
|
setDeviceGroupsOptions(devices);
|
||||||
targetSelectionTab === "device" ? style.tabActive : ""
|
}}
|
||||||
}`}
|
placeholder="请选择设备"
|
||||||
onClick={() => setTargetSelectionTab("device")}
|
showSelectedList={true}
|
||||||
>
|
selectedListMaxHeight={300}
|
||||||
设备选择
|
deviceGroups={deviceGroups}
|
||||||
</div>
|
/>
|
||||||
<div
|
|
||||||
className={`${style.tabItem} ${
|
|
||||||
targetSelectionTab === "account" ? style.tabActive : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => setTargetSelectionTab("account")}
|
|
||||||
>
|
|
||||||
客服选择
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tab 内容 */}
|
|
||||||
<div className={style.tabContent}>
|
|
||||||
{targetSelectionTab === "device" && (
|
|
||||||
<div className={style.formBlock}>
|
|
||||||
<DeviceSelection
|
|
||||||
selectedOptions={selectedDevices}
|
|
||||||
onSelect={devices => {
|
|
||||||
setSelectedDevices(devices);
|
|
||||||
setDeviceGroupsOptions(devices);
|
|
||||||
}}
|
|
||||||
placeholder="请选择设备"
|
|
||||||
showSelectedList={true}
|
|
||||||
selectedListMaxHeight={300}
|
|
||||||
deviceGroups={deviceGroups}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{targetSelectionTab === "account" && (
|
|
||||||
<div className={style.formBlock}>
|
|
||||||
<AccountSelection
|
|
||||||
selectedOptions={selectedAccounts}
|
|
||||||
onSelect={accounts => {
|
|
||||||
setSelectedAccounts(accounts);
|
|
||||||
setAccountGroupsOptions(accounts);
|
|
||||||
}}
|
|
||||||
placeholder="请选择客服"
|
|
||||||
showSelectedList={true}
|
|
||||||
selectedListMaxHeight={300}
|
|
||||||
accountGroups={accountGroups}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{current === 2 && (
|
{current === 2 && (
|
||||||
<div>
|
<PoolSelection
|
||||||
<div className={style.sectionTitle}>流量池选择</div>
|
selectedOptions={poolGroupsOptions}
|
||||||
<div className={style.formBlock}>
|
onSelect={handPoolAction}
|
||||||
<Input
|
placeholder="请选择流量池"
|
||||||
placeholder="搜索流量池"
|
showSelectedList={true}
|
||||||
value={poolSearch}
|
selectedListMaxHeight={300}
|
||||||
onChange={e => setPoolSearch(e.target.value)}
|
/>
|
||||||
style={{ marginBottom: 12 }}
|
|
||||||
/>
|
|
||||||
<div className={style.poolListWrap}>
|
|
||||||
{filteredPools.map(pool => (
|
|
||||||
<label key={pool.id} className={style.poolItem}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={selectedPools.includes(pool.id)}
|
|
||||||
onChange={e => {
|
|
||||||
setSelectedPools(val =>
|
|
||||||
e.target.checked
|
|
||||||
? [...val, pool.id]
|
|
||||||
: val.filter(v => v !== pool.id),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className={style.poolName}>{pool.name}</span>
|
|
||||||
<span className={style.poolTags}>
|
|
||||||
{pool.tags.join("/")}
|
|
||||||
</span>
|
|
||||||
<span className={style.poolCount}>
|
|
||||||
{pool.userCount}人
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className={style.poolSelectedCount}>
|
|
||||||
已选流量池:<span>{selectedPools.length}</span> 个
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function toggleDistributionRuleStatus(
|
|||||||
|
|
||||||
// 删除计划
|
// 删除计划
|
||||||
export function deleteDistributionRule(id: number): Promise<any> {
|
export function deleteDistributionRule(id: number): Promise<any> {
|
||||||
return request("/v1/workbench/delete", { id }, "POST");
|
return request("/v1/workbench/delete", { id }, "DELETE");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取流量分发规则详情
|
// 获取流量分发规则详情
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export interface DistributionRule {
|
|||||||
endTime: string;
|
endTime: string;
|
||||||
account: (string | number)[];
|
account: (string | number)[];
|
||||||
devices: string[];
|
devices: string[];
|
||||||
pools: string[];
|
poolGroups: string[];
|
||||||
exp: number;
|
exp: number;
|
||||||
createTime: string;
|
createTime: string;
|
||||||
updateTime: string;
|
updateTime: string;
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ const TrafficDistributionList: React.FC = () => {
|
|||||||
onClick={() => showPoolList(item)}
|
onClick={() => showPoolList(item)}
|
||||||
>
|
>
|
||||||
<div style={{ fontSize: 18, fontWeight: 600 }}>
|
<div style={{ fontSize: 18, fontWeight: 600 }}>
|
||||||
{item.config?.pools?.length || 0}
|
{item.config?.poolGroups?.length || 0}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 13, color: "#888" }}>流量池</div>
|
<div style={{ fontSize: 13, color: "#888" }}>流量池</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,32 +28,25 @@ class DouBaoAI
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function text()
|
|
||||||
|
public function text($params = [])
|
||||||
{
|
{
|
||||||
$this->__init();
|
$this->__init();
|
||||||
|
if (empty($params)){
|
||||||
$content = input('content','');
|
|
||||||
if (empty($content)){
|
|
||||||
return json_encode(['code' => 500, 'msg' => '提示词缺失']);
|
return json_encode(['code' => 500, 'msg' => '提示词缺失']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送请求
|
|
||||||
$params = [
|
|
||||||
'model' => 'doubao-1-5-pro-32k-250115',
|
|
||||||
'messages' => [
|
|
||||||
['role' => 'system', 'content' => '你是人工智能助手.'],
|
|
||||||
['role' => 'user', 'content' => $content],
|
|
||||||
],
|
|
||||||
/*'extra_headers' => [
|
|
||||||
'x-is-encrypted' => true
|
|
||||||
],
|
|
||||||
'temperature' => 1,
|
|
||||||
'top_p' => 0.7,
|
|
||||||
'max_tokens' => 4096,
|
|
||||||
'frequency_penalty' => 0,*/
|
|
||||||
];
|
|
||||||
$result = requestCurl($this->apiUrl, $params, 'POST', $this->headers, 'json');
|
$result = requestCurl($this->apiUrl, $params, 'POST', $this->headers, 'json');
|
||||||
$result = json_decode($result, true);
|
$result = json_decode($result, true);
|
||||||
return successJson($result);
|
if(isset($result['error'])){
|
||||||
|
$error = $result['error'];
|
||||||
|
return json_encode(['code' => 500, 'msg' => $error['message']]);
|
||||||
|
}else{
|
||||||
|
$content = $result['choices'][0]['message']['content'];
|
||||||
|
$token = intval($result['usage']['total_tokens']) * 20;
|
||||||
|
return json_encode(['code' => 200, 'msg' => '成功','data' => ['token' => $token,'content' => $content]]);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,7 @@ Route::group('v1/', function () {
|
|||||||
//群相关
|
//群相关
|
||||||
Route::group('wechatChatroom/', function () {
|
Route::group('wechatChatroom/', function () {
|
||||||
Route::get('list', 'app\chukebao\controller\WechatChatroomController@getList'); // 获取好友列表
|
Route::get('list', 'app\chukebao\controller\WechatChatroomController@getList'); // 获取好友列表
|
||||||
|
Route::post('aiAnnouncement', 'app\chukebao\controller\WechatChatroomController@aiAnnouncement'); // AI群公告
|
||||||
});
|
});
|
||||||
|
|
||||||
//客服相关
|
//客服相关
|
||||||
@@ -26,8 +27,62 @@ Route::group('v1/', function () {
|
|||||||
//客服相关
|
//客服相关
|
||||||
Route::group('message/', function () {
|
Route::group('message/', function () {
|
||||||
Route::get('list', 'app\chukebao\controller\MessageController@getList'); // 获取好友列表
|
Route::get('list', 'app\chukebao\controller\MessageController@getList'); // 获取好友列表
|
||||||
|
Route::get('readMessage', 'app\chukebao\controller\MessageController@readMessage'); // 读取消息
|
||||||
|
Route::get('details', 'app\chukebao\controller\MessageController@details'); // 消息详情
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//AI相关
|
||||||
|
Route::group('ai/', function () {
|
||||||
|
//问答
|
||||||
|
Route::group('questions/', function () {
|
||||||
|
Route::get('list', 'app\chukebao\controller\QuestionsController@getList'); // 问答列表
|
||||||
|
Route::post('add', 'app\chukebao\controller\QuestionsController@create'); // 问答添加
|
||||||
|
Route::post('update', 'app\chukebao\controller\QuestionsController@update'); // 问答更新
|
||||||
|
Route::get('delete', 'app\chukebao\controller\QuestionsController@delete'); // 问答删除
|
||||||
|
Route::get('detail', 'app\chukebao\controller\QuestionsController@detail'); // 问答详情
|
||||||
|
});
|
||||||
|
|
||||||
|
//全局配置
|
||||||
|
Route::group('settings/', function () {
|
||||||
|
Route::get('get', 'app\chukebao\controller\AiSettingsController@getSetting');
|
||||||
|
Route::post('set', 'app\chukebao\controller\AiSettingsController@setSetting');
|
||||||
|
});
|
||||||
|
|
||||||
|
//好友配置
|
||||||
|
Route::group('friend/', function () {
|
||||||
|
Route::post('set', 'app\chukebao\controller\AiSettingsController@setFriend');
|
||||||
|
Route::get('get', 'app\chukebao\controller\AiSettingsController@getFriend');
|
||||||
|
Route::post('setAll', 'app\chukebao\controller\AiSettingsController@setAllFriend');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
//ai对话
|
||||||
|
Route::get('getUserTokens', 'app\chukebao\controller\AiSettingsController@getUserTokens');
|
||||||
|
Route::post('chat', 'app\chukebao\controller\AiChatController@index');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
//代办事项
|
||||||
|
Route::group('todo/', function () {
|
||||||
|
Route::get('list', 'app\chukebao\controller\ToDoController@getList');
|
||||||
|
Route::post('add', 'app\chukebao\controller\ToDoController@create');
|
||||||
|
Route::get('process', 'app\chukebao\controller\ToDoController@process');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
//跟进提醒
|
||||||
|
Route::group('followUp/', function () {
|
||||||
|
Route::get('list', 'app\chukebao\controller\FollowUpController@getList');
|
||||||
|
Route::post('add', 'app\chukebao\controller\FollowUpController@create');
|
||||||
|
Route::get('process', 'app\chukebao\controller\FollowUpController@process');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
//算力相关
|
||||||
|
Route::group('tokensRecord/', function () {
|
||||||
|
Route::get('list', 'app\chukebao\controller\TokensRecordController@getList');
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
106
Server/application/chukebao/controller/AiChatController.php
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\chukebao\controller;
|
||||||
|
|
||||||
|
use app\ai\controller\DouBaoAI;
|
||||||
|
use app\chukebao\controller\TokensRecordController as tokensRecord;
|
||||||
|
use library\ResponseHelper;
|
||||||
|
use think\Db;
|
||||||
|
|
||||||
|
class AiChatController extends BaseController
|
||||||
|
{
|
||||||
|
public function index(){
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
$friendId = $this->request->param('friendId', '');
|
||||||
|
$wechatAccountId = $this->request->param('wechatAccountId', '');
|
||||||
|
$content = $this->request->param('content', '');
|
||||||
|
|
||||||
|
if (empty($wechatAccountId) || empty($friendId)){
|
||||||
|
return ResponseHelper::error('参数缺失');
|
||||||
|
}
|
||||||
|
|
||||||
|
$tokens = Db::name('users')
|
||||||
|
->where('id', $userId)
|
||||||
|
->where('companyId', $companyId)
|
||||||
|
->value('tokens');
|
||||||
|
if ($tokens <= 0){
|
||||||
|
return ResponseHelper::error('用户Tokens余额不足');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//读取AI配置
|
||||||
|
$setting = Db::name('ai_settings')->where(['companyId' => $companyId,'userId' => $userId])->find();
|
||||||
|
if(empty($setting)){
|
||||||
|
return ResponseHelper::error('未找到配置信息,请先配置AI策略');
|
||||||
|
}
|
||||||
|
$config = json_decode($setting['config'],true);
|
||||||
|
$modelSetting = $config['modelSetting'];
|
||||||
|
$round = isset($config['round']) ? $config['round'] : 10;
|
||||||
|
|
||||||
|
|
||||||
|
// 导出聊天
|
||||||
|
$messages = Db::table('s2_wechat_message')
|
||||||
|
->where('wechatFriendId', $friendId)
|
||||||
|
->order('wechatTime desc')
|
||||||
|
->field('id,content,msgType,isSend,wechatTime')
|
||||||
|
->limit($round)
|
||||||
|
->select();
|
||||||
|
|
||||||
|
usort($messages, function($a, $b) {
|
||||||
|
return $a['wechatTime'] <=> $b['wechatTime'];
|
||||||
|
});
|
||||||
|
|
||||||
|
//处理聊天数据
|
||||||
|
$msg = [];
|
||||||
|
foreach ($messages as $val){
|
||||||
|
if (empty($val['content'])){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!empty($val['isSend'])){
|
||||||
|
$msg[] = '客服:' . $val['content'];
|
||||||
|
}else{
|
||||||
|
$msg[] = '用户:' . $val['content'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$content = implode("\n", $msg);
|
||||||
|
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
'model' => 'doubao-1-5-pro-32k-250115',
|
||||||
|
'messages' => [
|
||||||
|
// ['role' => 'system', 'content' => '请完成跟客户的对话'],
|
||||||
|
['role' => 'system', 'content' => '角色设定:' . $modelSetting['role']],
|
||||||
|
['role' => 'system', 'content' => '公司背景:' . $modelSetting['businessBackground']],
|
||||||
|
['role' => 'system', 'content' => '对话风格:' . $modelSetting['dialogueStyle']],
|
||||||
|
['role' => 'user', 'content' => $content],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
//AI处理
|
||||||
|
$ai = new DouBaoAI();
|
||||||
|
$res = $ai->text($params);
|
||||||
|
$res = json_decode($res,true);
|
||||||
|
|
||||||
|
if ($res['code'] == 200) {
|
||||||
|
//扣除Tokens
|
||||||
|
$tokensRecord = new tokensRecord();
|
||||||
|
$nickname = Db::table('s2_wechat_friend')->where(['id' => $friendId])->value('nickname');
|
||||||
|
$remarks = !empty($nickname) ? '与好友【'.$nickname.'】聊天' : '与好友聊天';
|
||||||
|
$data = [
|
||||||
|
'tokens' => $res['data']['token'],
|
||||||
|
'type' => 0,
|
||||||
|
'form' => 1,
|
||||||
|
'wechatAccountId' => $wechatAccountId,
|
||||||
|
'friendIdOrGroupId' => $friendId,
|
||||||
|
'remarks' => $remarks,
|
||||||
|
];
|
||||||
|
$tokensRecord->consumeTokens($data);
|
||||||
|
return ResponseHelper::success($res['data']['content']);
|
||||||
|
}else{
|
||||||
|
return ResponseHelper::error($res['msg']);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
270
Server/application/chukebao/controller/AiSettingsController.php
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\chukebao\controller;
|
||||||
|
|
||||||
|
use app\chukebao\model\AiFriendSettings;
|
||||||
|
use app\chukebao\model\Questions;
|
||||||
|
use library\ResponseHelper;
|
||||||
|
use think\Db;
|
||||||
|
|
||||||
|
class AiSettingsController extends BaseController
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
const SETTING_DEFAULT = [
|
||||||
|
'audioSetting' => false,
|
||||||
|
'round' => 10,
|
||||||
|
'aiStopSetting' => [
|
||||||
|
'status' => true,
|
||||||
|
'key' => ['好', '不错', '好的', '下次', '可以']
|
||||||
|
],
|
||||||
|
'fileSetting' => [
|
||||||
|
'type' => 1,
|
||||||
|
'content' => ''
|
||||||
|
],
|
||||||
|
'modelSetting' => [
|
||||||
|
'model' => 'GPT-4',
|
||||||
|
'role' => '你是一名销售的AI助理,同时也是一个工智能技术专家,你的名字叫小灵,你是单身女性,出生于2003年10月10日,喜欢听音乐和看电影有着丰富的人生阅历,前成熟大方,分享用幽默风趣的语言和客户交流,顾客问起你的感情,回复内容中不要使用号,特别注意不要跟客户问题,不要更多选择发送的信息。',
|
||||||
|
'businessBackground' => '灵销智能公司开发了多款AI营销智能技术产品,以提升销售GPT AI大模型为核心,接入打造的销售/营销/客服等AI智能应用,为企业AI办公,AI助理,AI销售,AI营销,AI直播等大AI应用产品。',
|
||||||
|
'dialogueStyle' => '客户:你们的AI解决方案具体是怎么收费的?销售:嗯,朋友,我们的AI解决方案是根据项目需求来定的,这样吧,你能跟我说说你们的具体情况吗,不过这样一分钱,您看怎么样?我们可以给您做个详细的方案对比。',
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
const TYPE_DATA = ['audioSetting', 'round', 'aiStopSetting', 'fileSetting', 'modelSetting'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取配置信息
|
||||||
|
* @return \think\response\Json
|
||||||
|
* @throws \think\db\exception\DataNotFoundException
|
||||||
|
* @throws \think\db\exception\ModelNotFoundException
|
||||||
|
* @throws \think\exception\DbException
|
||||||
|
*/
|
||||||
|
public function getSetting()
|
||||||
|
{
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
|
||||||
|
$data = Db::name('ai_settings')->where(['userId' => $userId, 'companyId' => $companyId])->find();
|
||||||
|
if (empty($data)) {
|
||||||
|
$setting = self::SETTING_DEFAULT;
|
||||||
|
$data = [
|
||||||
|
'companyId' => $companyId,
|
||||||
|
'userId' => $userId,
|
||||||
|
'config' => json_encode($setting, 256),
|
||||||
|
'createTime' => time(),
|
||||||
|
'updateTime' => time()
|
||||||
|
];
|
||||||
|
Db::name('ai_settings')->insert($data);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$setting = json_decode($data['config'], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseHelper::success($setting, '获取成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置
|
||||||
|
* @return \think\response\Json
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function setSetting()
|
||||||
|
{
|
||||||
|
$key = $this->request->param('key', '');
|
||||||
|
$value = $this->request->param('value', '');
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
|
||||||
|
if (empty($key) || empty($value)) {
|
||||||
|
return ResponseHelper::error('参数缺失');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!in_array($key, self::TYPE_DATA)) {
|
||||||
|
return ResponseHelper::error('该类型不在配置项');
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
$data = Db::name('ai_settings')->where(['userId' => $userId, 'companyId' => $companyId])->find();
|
||||||
|
if (empty($data)) {
|
||||||
|
$setting = self::SETTING_DEFAULT;
|
||||||
|
} else {
|
||||||
|
$setting = json_decode($data['config'], true);
|
||||||
|
}
|
||||||
|
$setting[$key] = $value;
|
||||||
|
$setting = json_encode($setting, 256);
|
||||||
|
Db::name('ai_settings')->where(['id' => $data['id']])->update(['config' => $setting, 'updateTime' => time()]);
|
||||||
|
Db::commit();
|
||||||
|
return ResponseHelper::success(' ', '配置成功');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Db::rollback();
|
||||||
|
return ResponseHelper::error('配置失败:' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getUserTokens()
|
||||||
|
{
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
$tokens = Db::name('users')
|
||||||
|
->where('id', $userId)
|
||||||
|
->where('companyId', $companyId)
|
||||||
|
->value('tokens');
|
||||||
|
|
||||||
|
return ResponseHelper::success($tokens, '获取成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public function getFriend()
|
||||||
|
{
|
||||||
|
$friendId = $this->request->param('friendId', '');
|
||||||
|
$wechatAccountId = $this->request->param('wechatAccountId', '');
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
$aiType = AiFriendSettings::where(['userId' => $userId, 'companyId' => $companyId,'friendId' => $friendId,'wechatAccountId' => $wechatAccountId])->value('type');
|
||||||
|
if (empty($aiType)) {
|
||||||
|
$aiType = 0;
|
||||||
|
}
|
||||||
|
return ResponseHelper::success($aiType, '获取成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public function setFriend()
|
||||||
|
{
|
||||||
|
$friendId = $this->request->param('friendId', '');
|
||||||
|
$wechatAccountId = $this->request->param('wechatAccountId', '');
|
||||||
|
$type = $this->request->param('type', 0);
|
||||||
|
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
|
||||||
|
if (empty($friendId) || empty($wechatAccountId)) {
|
||||||
|
return ResponseHelper::error('参数缺失');
|
||||||
|
}
|
||||||
|
$friend = Db::table('s2_wechat_friend')->where(['id' => $friendId,'wechatAccountId' => $wechatAccountId])->find();
|
||||||
|
|
||||||
|
if (empty($friend)) {
|
||||||
|
return ResponseHelper::error('该好友不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$aiFriendSettings = AiFriendSettings::where(['userId' => $userId, 'companyId' => $companyId,'friendId' => $friendId,'wechatAccountId' => $wechatAccountId])->find();
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
if (empty($aiFriendSettings)) {
|
||||||
|
$aiFriendSettings = new AiFriendSettings();
|
||||||
|
$aiFriendSettings->companyId = $companyId;
|
||||||
|
$aiFriendSettings->userId = $userId;
|
||||||
|
$aiFriendSettings->type = $type;
|
||||||
|
$aiFriendSettings->wechatAccountId = $wechatAccountId;
|
||||||
|
$aiFriendSettings->friendId = $friendId;
|
||||||
|
$aiFriendSettings->createTime = time();
|
||||||
|
$aiFriendSettings->updateTime = time();
|
||||||
|
}else{
|
||||||
|
$aiFriendSettings->type = $type;
|
||||||
|
$aiFriendSettings->updateTime = time();
|
||||||
|
}
|
||||||
|
$aiFriendSettings->save();
|
||||||
|
Db::commit();
|
||||||
|
return ResponseHelper::success(' ', '配置成功');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Db::rollback();
|
||||||
|
return ResponseHelper::error('配置失败:' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public function setAllFriend()
|
||||||
|
{
|
||||||
|
$packageId = $this->request->param('packageId', []);
|
||||||
|
$type = $this->request->param('type', 0);
|
||||||
|
$isUpdata = $this->request->param('isUpdata', 0);
|
||||||
|
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
|
||||||
|
if (empty($packageId)) {
|
||||||
|
return ResponseHelper::error('参数缺失');
|
||||||
|
}
|
||||||
|
//列出所有好友
|
||||||
|
$row = Db::name('traffic_source_package_item')->alias('a')
|
||||||
|
->join('wechat_friendship f','a.identifier = f.wechatId and f.companyId = '.$companyId)
|
||||||
|
->join(['s2_wechat_account' => 'wa'],'f.ownerWechatId = wa.wechatId')
|
||||||
|
->whereIn('a.packageId' , $packageId)
|
||||||
|
->field('f.id as friendId,wa.id as wechatAccountId')
|
||||||
|
->group('f.id')
|
||||||
|
->select();
|
||||||
|
|
||||||
|
if (empty($row)) {
|
||||||
|
return ResponseHelper::error('`好友不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 1000条为一组进行批量处理
|
||||||
|
$batchSize = 1000;
|
||||||
|
$totalRows = count($row);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $totalRows; $i += $batchSize) {
|
||||||
|
$batchRows = array_slice($row, $i, $batchSize);
|
||||||
|
if (!empty($batchRows)) {
|
||||||
|
// 1. 提取当前批次的phone
|
||||||
|
$friendIds = array_column($batchRows, 'friendId');
|
||||||
|
// 2. 批量查询已存在的phone
|
||||||
|
$existingPhones = [];
|
||||||
|
if (!empty($friendIds)) {
|
||||||
|
//强制更新
|
||||||
|
if(!empty($isUpdata)){
|
||||||
|
Db::name('ai_friend_settings')->whereIn('friendId',$friendIds)->update(['type' => $type,'updateTime' => time()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing = Db::name('ai_friend_settings')
|
||||||
|
->where('companyId', $companyId)
|
||||||
|
->where('friendId', 'in', $friendIds)
|
||||||
|
->field('friendId')
|
||||||
|
->select();
|
||||||
|
$existingPhones = array_column($existing, 'friendId');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 过滤出新数据,批量插入
|
||||||
|
$newData = [];
|
||||||
|
foreach ($batchRows as $row) {
|
||||||
|
if (!empty($friendIds) && !in_array($row['friendId'], $existingPhones)) {
|
||||||
|
$newData[] = [
|
||||||
|
'companyId' => $companyId,
|
||||||
|
'userId' => $userId,
|
||||||
|
'type' => $type,
|
||||||
|
'wechatAccountId' => $row['wechatAccountId'],
|
||||||
|
'friendId' => $row['friendId'],
|
||||||
|
'createTime' => time(),
|
||||||
|
'updateTime' => time(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 4. 批量插入新数据
|
||||||
|
if (!empty($newData)) {
|
||||||
|
Db::name('ai_friend_settings')->insertAll($newData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return ResponseHelper::success(' ', '配置成功');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ResponseHelper::error('配置失败:' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
143
Server/application/chukebao/controller/FollowUpController.php
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\chukebao\controller;
|
||||||
|
|
||||||
|
use app\chukebao\model\FollowUp;
|
||||||
|
use library\ResponseHelper;
|
||||||
|
use think\Db;
|
||||||
|
|
||||||
|
class FollowUpController extends BaseController
|
||||||
|
{
|
||||||
|
|
||||||
|
public function getList(){
|
||||||
|
$page = $this->request->param('page', 1);
|
||||||
|
$limit = $this->request->param('limit', 10);
|
||||||
|
$keyword = $this->request->param('keyword', '');
|
||||||
|
$isRemind = $this->request->param('isRemind', '');
|
||||||
|
$isProcess = $this->request->param('isProcess', '');
|
||||||
|
$type = $this->request->param('type', '');
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
|
||||||
|
|
||||||
|
$where = [
|
||||||
|
['companyId','=',$companyId],
|
||||||
|
['userId' ,'=', $userId]
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($isRemind != '') {
|
||||||
|
$where[] = ['isRemind','=',$isRemind];
|
||||||
|
}
|
||||||
|
if ($type != '') {
|
||||||
|
$where[] = ['type','=',$type];
|
||||||
|
}
|
||||||
|
if ($isProcess != '') {
|
||||||
|
$where[] = ['isProcess','=',$isProcess];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!empty($keyword)){
|
||||||
|
$where[] = ['title|description','like','%'.$keyword.'%'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = FollowUp::where($where);
|
||||||
|
|
||||||
|
$list = $query->where($where)->page($page,$limit)->order('id desc')->select();
|
||||||
|
$total = $query->count();
|
||||||
|
|
||||||
|
foreach ($list as &$item) {
|
||||||
|
$nickname = Db::table('s2_wechat_friend')->where(['id' => $item['friendId']])->value('nickname');
|
||||||
|
$item['nickname'] = !empty($nickname) ? $nickname : '-';
|
||||||
|
$item['reminderTime'] = date('Y-m-d H:i:s',$item['reminderTime']);
|
||||||
|
}
|
||||||
|
unset($item);
|
||||||
|
|
||||||
|
return ResponseHelper::success(['list'=>$list,'total'=>$total]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加
|
||||||
|
* @return \think\response\Json
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function create(){
|
||||||
|
$type = $this->request->param('type', 0);
|
||||||
|
$title = $this->request->param('title', '');
|
||||||
|
$reminderTime = $this->request->param('reminderTime', '');
|
||||||
|
$description = $this->request->param('description', '');
|
||||||
|
$friendId = $this->request->param('friendId', '');
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
|
||||||
|
if (empty($title) || empty($reminderTime) || empty($description) || empty($friendId)){
|
||||||
|
return ResponseHelper::error('参数缺失');
|
||||||
|
}
|
||||||
|
$friend = Db::name('wechat_friendship')->where(['id' => $friendId,'companyId' => $companyId])->find();
|
||||||
|
if (empty($friend)) {
|
||||||
|
return ResponseHelper::error('好友不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
$FollowUp = new FollowUp();
|
||||||
|
$FollowUp->type = $type;
|
||||||
|
$FollowUp->title = $title;
|
||||||
|
$FollowUp->friendId = $friendId;
|
||||||
|
$FollowUp->reminderTime = !empty($reminderTime) ? strtotime($reminderTime) : time();
|
||||||
|
$FollowUp->description = $description;
|
||||||
|
$FollowUp->userId = $userId;
|
||||||
|
$FollowUp->companyId = $companyId;
|
||||||
|
$FollowUp->updateTime = time();
|
||||||
|
$FollowUp->createTime = time();
|
||||||
|
$FollowUp->save();
|
||||||
|
Db::commit();
|
||||||
|
return ResponseHelper::success(' ','创建成功');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Db::rollback();
|
||||||
|
return ResponseHelper::error('创建失败:'.$e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理代办事项
|
||||||
|
* @return \think\response\Json
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function process(){
|
||||||
|
$ids = $this->request->param('ids','');
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
|
||||||
|
if (empty($ids)){
|
||||||
|
return ResponseHelper::error('参数缺失');
|
||||||
|
}
|
||||||
|
$ids = explode(',',$ids);
|
||||||
|
|
||||||
|
if (!is_array($ids)){
|
||||||
|
return ResponseHelper::error('格式错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
$FollowUpIds = FollowUp::where(['userId' => $userId,'companyId' => $companyId,'isProcess' => 0])->whereIn('id',$ids)->column('id');
|
||||||
|
if (empty($FollowUpIds)){
|
||||||
|
return ResponseHelper::error('代办事项不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
FollowUp::whereIn('id',$FollowUpIds)->update(['isProcess' => 1,'isRemind' => 1,'updateTime' => time()]);
|
||||||
|
Db::commit();
|
||||||
|
return ResponseHelper::success(' ','已处理');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Db::rollback();
|
||||||
|
return ResponseHelper::error('处理失败:'.$e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -45,7 +45,9 @@ class LoginController extends Controller
|
|||||||
->where(function ($query) use ($username) {
|
->where(function ($query) use ($username) {
|
||||||
$query->where('account', $username)->whereOr('phone', $username);
|
$query->where('account', $username)->whereOr('phone', $username);
|
||||||
})
|
})
|
||||||
->where('passwordMd5', md5($password))
|
->where(function ($query2) use ($password) {
|
||||||
|
$query2->where('passwordMd5', md5($password))->whereOr('passwordLocal', localEncrypt($password));
|
||||||
|
})
|
||||||
->find();
|
->find();
|
||||||
}else{
|
}else{
|
||||||
$user = $payload;
|
$user = $payload;
|
||||||
@@ -84,9 +86,41 @@ class LoginController extends Controller
|
|||||||
$kefuData['self'] = $self;
|
$kefuData['self'] = $self;
|
||||||
Db::name('users')->where('id', $user['id'])->update(['passwordLocal' => localEncrypt($params['password']),'updateTime' => time()]);
|
Db::name('users')->where('id', $user['id'])->update(['passwordLocal' => localEncrypt($params['password']),'updateTime' => time()]);
|
||||||
}else{
|
}else{
|
||||||
return ResponseHelper::error($result['error_description']);
|
$kefuData = [
|
||||||
|
'token' => [
|
||||||
|
"access_token"=> "27gINKZqGux6V4j9QLawOcTKlWXg-j4zxQjKvScvDTq-YlLcwIrDP2AFaNZKnOo9zLzepOBC8qrdXh4z9GxxkwE9TKGRQI1FjITRlMZzrim13IbSEbJUoywGs_BhDmIZnnPhfjqxDB1vjZgVtT2Kp4bxbUCV3i2uO_FTv_DT2G7NUFFLjq8oIuUrd_c1YXeYkH8m8Fw1AM4yPZJZyfdaHSSMOpJ2Bk2LAghnB6OaZCYWNFQcwWARsmh1BSAANUOAoadjkztZC7Fme-GGOm2sLo0WL6Mf26NfeLmnkluewTiPMyacD7RYclAR2LZ_8Mhwr3pwRg",
|
||||||
|
"token_type"=> "bearer",
|
||||||
|
"expires_in"=> 195519999,
|
||||||
|
"refresh_token"=> "a9545daa-d1c4-4c87-8c4c-b713631d4f0d"
|
||||||
|
],
|
||||||
|
'self' => [
|
||||||
|
'account' => [
|
||||||
|
"id"=> 5538,
|
||||||
|
"realName"=> "测试",
|
||||||
|
"nickname"=> "",
|
||||||
|
"memo"=> "",
|
||||||
|
"avatar"=> "",
|
||||||
|
"userName"=> "wz_02",
|
||||||
|
"secret"=> "8f6f743395ad4198b6a4c0e6ca0e452f",
|
||||||
|
"accountType"=> 10,
|
||||||
|
"departmentId"=> 2130,
|
||||||
|
"useGoogleSecretKey"=> false,
|
||||||
|
"hasVerifyGoogleSecret"=> true
|
||||||
|
],
|
||||||
|
'tenant' => [
|
||||||
|
"id" => 242,
|
||||||
|
"name"=> "泉州市卡若网络技术有限公司",
|
||||||
|
"guid"=> "5E2C38F5A275450D935F3ECEC076124E",
|
||||||
|
"thirdParty"=> null,
|
||||||
|
"tenantType"=> 0,
|
||||||
|
"deployName"=> "deploy-s2"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
//return ResponseHelper::error($result['error_description']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
unset($user['passwordMd5'],$user['deleteTime']);
|
unset($user['passwordMd5'],$user['deleteTime']);
|
||||||
$userData['member'] = $user;
|
$userData['member'] = $user;
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ class MessageController extends BaseController
|
|||||||
->field('id,nickname,avatar')
|
->field('id,nickname,avatar')
|
||||||
->find();
|
->find();
|
||||||
$v['msgInfo'] = $friend;
|
$v['msgInfo'] = $friend;
|
||||||
|
$v['unreadCount'] = Db::table('s2_wechat_message')
|
||||||
|
->where(['wechatFriendId' => $v['wechatFriendId'],'isRead' => 0])
|
||||||
|
->count();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($v['wechatChatroomId'])){
|
if (!empty($v['wechatChatroomId'])){
|
||||||
@@ -61,12 +64,87 @@ class MessageController extends BaseController
|
|||||||
->field('id,nickname,chatroomAvatar as avatar')
|
->field('id,nickname,chatroomAvatar as avatar')
|
||||||
->find();
|
->find();
|
||||||
$v['msgInfo'] = $chatroom;
|
$v['msgInfo'] = $chatroom;
|
||||||
|
$v['unreadCount'] = Db::table('s2_wechat_message')
|
||||||
|
->where(['wechatChatroomId' => $v['wechatChatroomId'],'isRead' => 0])
|
||||||
|
->count();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
unset($v);
|
unset($v);
|
||||||
|
|
||||||
return ResponseHelper::success($list);
|
return ResponseHelper::success($list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function readMessage(){
|
||||||
|
$wechatFriendId = $this->request->param('wechatFriendId', '');
|
||||||
|
$wechatChatroomId = $this->request->param('wechatChatroomId', '');
|
||||||
|
$accountId = $this->getUserInfo('s2_accountId');
|
||||||
|
if (empty($accountId)){
|
||||||
|
return ResponseHelper::error('请先登录');
|
||||||
|
}
|
||||||
|
if (empty($wechatChatroomId) && empty($wechatFriendId)){
|
||||||
|
return ResponseHelper::error('参数缺失');
|
||||||
|
}
|
||||||
|
|
||||||
|
$where = [];
|
||||||
|
if (!empty($wechatChatroomId)){
|
||||||
|
$where[] = ['wechatChatroomId','=',$wechatChatroomId];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($wechatFriendId)){
|
||||||
|
$where[] = ['wechatFriendId','=',$wechatFriendId];
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::table('s2_wechat_message')->where($where)->update(['isRead' => 1]);
|
||||||
|
return ResponseHelper::success([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public function details(){
|
||||||
|
$wechatFriendId = $this->request->param('wechatFriendId', '');
|
||||||
|
$wechatChatroomId = $this->request->param('wechatChatroomId', '');
|
||||||
|
$wechatAccountId = $this->request->param('wechatAccountId', '');
|
||||||
|
$page = $this->request->param('page', 1);
|
||||||
|
$limit = $this->request->param('limit', 10);
|
||||||
|
$from = $this->request->param('From', '');
|
||||||
|
$to = $this->request->param('To', '');
|
||||||
|
$olderData = $this->request->param('olderData', false);
|
||||||
|
$accountId = $this->getUserInfo('s2_accountId');
|
||||||
|
if (empty($accountId)){
|
||||||
|
return ResponseHelper::error('请先登录');
|
||||||
|
}
|
||||||
|
if (empty($wechatChatroomId) && empty($wechatFriendId)){
|
||||||
|
return ResponseHelper::error('参数缺失');
|
||||||
|
}
|
||||||
|
|
||||||
|
$where = [];
|
||||||
|
if (!empty($wechatChatroomId)){
|
||||||
|
$where[] = ['wechatChatroomId','=',$wechatChatroomId];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($wechatFriendId)){
|
||||||
|
$where[] = ['wechatFriendId','=',$wechatFriendId];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($From) && !empty($To)){
|
||||||
|
$where[] = ['wechatTime','between',[$from,$to]];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$list = Db::table('s2_wechat_message')->where($where)->page($page,$limit)->order('id DESC')->select();
|
||||||
|
$total = Db::table('s2_wechat_message')->where($where)->count();
|
||||||
|
|
||||||
|
|
||||||
|
foreach ($list as $k=>&$v){
|
||||||
|
$v['wechatTime'] = !empty($v['wechatTime']) ? date('Y-m-d H:i:s',$v['wechatTime']) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return ResponseHelper::success(['total'=>$total,'list'=>$list]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
232
Server/application/chukebao/controller/QuestionsController.php
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\chukebao\controller;
|
||||||
|
|
||||||
|
use app\chukebao\model\Questions;
|
||||||
|
use library\ResponseHelper;
|
||||||
|
use think\Db;
|
||||||
|
|
||||||
|
class QuestionsController extends BaseController
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表
|
||||||
|
* @return \think\response\Json
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function getList(){
|
||||||
|
$page = $this->request->param('page', 1);
|
||||||
|
$limit = $this->request->param('limit', 10);
|
||||||
|
$keyword = $this->request->param('keyword', '');
|
||||||
|
$accountId = $this->getUserInfo('s2_accountId');
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
|
||||||
|
if (empty($accountId)){
|
||||||
|
return ResponseHelper::error('请先登录');
|
||||||
|
}
|
||||||
|
$query = Questions::where(['userId' => $userId,'companyId' => $companyId,'isDel' => 0])
|
||||||
|
->order('id desc');
|
||||||
|
if (!empty($keyword)){
|
||||||
|
$query->where('questions|answers', 'like', '%'.$keyword.'%');
|
||||||
|
}
|
||||||
|
$list = $query->page($page, $limit)->select()->toArray();
|
||||||
|
$total = $query->count();
|
||||||
|
|
||||||
|
foreach ($list as $k => &$v){
|
||||||
|
$user = Db::name('users')->where(['id' => $v['userId']])->field('username,account')->find();
|
||||||
|
if (!empty($user)){
|
||||||
|
$v['userName'] = !empty($user['username']) ? $user['username'] : $user['account'];
|
||||||
|
}else{
|
||||||
|
$v['userName'] = '';
|
||||||
|
}
|
||||||
|
$v['answers'] = json_decode($v['answers'],true);
|
||||||
|
}
|
||||||
|
unset($v);
|
||||||
|
return ResponseHelper::success(['list'=>$list,'total'=>$total]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增
|
||||||
|
* @return \think\response\Json
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function create(){
|
||||||
|
|
||||||
|
$type = $this->request->param('type', 0);
|
||||||
|
$questions = $this->request->param('questions', '');
|
||||||
|
$answers = $this->request->param('answers', []);
|
||||||
|
$status = $this->request->param('status', 0);
|
||||||
|
$accountId = $this->getUserInfo('s2_accountId');
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
|
||||||
|
if (empty($accountId)){
|
||||||
|
return ResponseHelper::error('请先登录');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($questions) || empty($answers)){
|
||||||
|
return ResponseHelper::error('问题和答案不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
$questionsModel = new Questions();
|
||||||
|
$questionsModel->type = $type;
|
||||||
|
$questionsModel->questions = $questions;
|
||||||
|
$questionsModel->answers = !empty($answers) ? json_encode($answers,256) : json_encode([],256);
|
||||||
|
$questionsModel->status = $status;
|
||||||
|
$questionsModel->accountId = $accountId;
|
||||||
|
$questionsModel->userId = $userId;
|
||||||
|
$questionsModel->companyId = $companyId;
|
||||||
|
$questionsModel->createTime = time();
|
||||||
|
$questionsModel->updateTime = time();
|
||||||
|
$questionsModel->save();
|
||||||
|
Db::commit();
|
||||||
|
return ResponseHelper::success(' ','创建成功');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Db::rollback();
|
||||||
|
return ResponseHelper::error('创建失败:'.$e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新
|
||||||
|
* @return \think\response\Json
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function update(){
|
||||||
|
|
||||||
|
$id = $this->request->param('id', 0);
|
||||||
|
$accountId = $this->getUserInfo('s2_accountId');
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
$type = $this->request->param('type', 0);
|
||||||
|
$questions = $this->request->param('questions', '');
|
||||||
|
$answers = $this->request->param('answers', []);
|
||||||
|
$status = $this->request->param('status', 0);
|
||||||
|
$accountId = $this->getUserInfo('s2_accountId');
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
|
||||||
|
if (empty($accountId)){
|
||||||
|
return ResponseHelper::error('请先登录');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($id)){
|
||||||
|
return ResponseHelper::error('参数缺失');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($questions) || empty($answers)){
|
||||||
|
return ResponseHelper::error('问题和答案不能为空');
|
||||||
|
}
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
$questionsData = Questions::where(['id' => $id,'userId' => $userId,'companyId' => $companyId,'isDel' => 0])->find();
|
||||||
|
$questionsData->type = $type;
|
||||||
|
$questionsData->questions = $questions;
|
||||||
|
$questionsData->answers = !empty($answers) ? json_encode($answers,256) : json_encode([],256);
|
||||||
|
$questionsData->status = $status;
|
||||||
|
$questionsData->accountId = $accountId;
|
||||||
|
$questionsData->userId = $userId;
|
||||||
|
$questionsData->companyId = $companyId;
|
||||||
|
$questionsData->updateTime = time();
|
||||||
|
$questionsData->save();
|
||||||
|
Db::commit();
|
||||||
|
return ResponseHelper::success(' ','更新成功');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Db::rollback();
|
||||||
|
return ResponseHelper::error('更新失败:'.$e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除
|
||||||
|
* @return \think\response\Json
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function delete(){
|
||||||
|
|
||||||
|
$id = $this->request->param('id', 0);
|
||||||
|
$accountId = $this->getUserInfo('s2_accountId');
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
|
||||||
|
if (empty($accountId)){
|
||||||
|
return ResponseHelper::error('请先登录');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($id)){
|
||||||
|
return ResponseHelper::error('参数缺失');
|
||||||
|
}
|
||||||
|
$questions = Questions::where(['id' => $id,'userId' => $userId,'companyId' => $companyId,'isDel' => 0])->find();
|
||||||
|
|
||||||
|
if (empty($questions)){
|
||||||
|
return ResponseHelper::error('该问题不存在或者已删除');
|
||||||
|
}
|
||||||
|
$res = Questions::where(['id' => $id])->update(['isDel' => 1,'deleteTime' => time()]);
|
||||||
|
|
||||||
|
if (!empty($res)){
|
||||||
|
return ResponseHelper::success('','已删除');
|
||||||
|
}else{
|
||||||
|
return ResponseHelper::error('删除失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 详情
|
||||||
|
* @return \think\response\Json
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function detail(){
|
||||||
|
|
||||||
|
$id = $this->request->param('id', 0);
|
||||||
|
$accountId = $this->getUserInfo('s2_accountId');
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
|
||||||
|
if (empty($accountId)){
|
||||||
|
return ResponseHelper::error('请先登录');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($id)){
|
||||||
|
return ResponseHelper::error('参数缺失');
|
||||||
|
}
|
||||||
|
$questions = Questions::where(['id' => $id,'userId' => $userId,'companyId' => $companyId,'isDel' => 0])->find();
|
||||||
|
|
||||||
|
if (empty($questions)){
|
||||||
|
return ResponseHelper::error('该问题不存在或者已删除');
|
||||||
|
}
|
||||||
|
|
||||||
|
$questions['answers'] = json_decode($questions['answers'],true);
|
||||||
|
$user = Db::name('users')->where(['id' => $questions['userId']])->field('username,account')->find();
|
||||||
|
if (!empty($user)){
|
||||||
|
$questions['userName'] = !empty($user['username']) ? $user['username'] : $user['account'];
|
||||||
|
}else{
|
||||||
|
$questions['userName'] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
unset(
|
||||||
|
$questions['isDel'],
|
||||||
|
$questions['deleteTime'],
|
||||||
|
$questions['createTime'],
|
||||||
|
$questions['updateTime']
|
||||||
|
);
|
||||||
|
|
||||||
|
return ResponseHelper::success($questions,'获取成功');
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
143
Server/application/chukebao/controller/ToDoController.php
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\chukebao\controller;
|
||||||
|
|
||||||
|
use app\chukebao\model\ToDo;
|
||||||
|
use library\ResponseHelper;
|
||||||
|
use think\Db;
|
||||||
|
|
||||||
|
class ToDoController extends BaseController
|
||||||
|
{
|
||||||
|
|
||||||
|
public function getList(){
|
||||||
|
$page = $this->request->param('page', 1);
|
||||||
|
$limit = $this->request->param('limit', 10);
|
||||||
|
$keyword = $this->request->param('keyword', '');
|
||||||
|
$isRemind = $this->request->param('isRemind', '');
|
||||||
|
$isProcess = $this->request->param('isProcess', '');
|
||||||
|
$level = $this->request->param('level', '');
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
|
||||||
|
|
||||||
|
$where = [
|
||||||
|
['companyId','=',$companyId],
|
||||||
|
['userId' ,'=', $userId]
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($isRemind != '') {
|
||||||
|
$where[] = ['isRemind','=',$isRemind];
|
||||||
|
}
|
||||||
|
if ($level != '') {
|
||||||
|
$where[] = ['level','=',$level];
|
||||||
|
}
|
||||||
|
if ($isProcess != '') {
|
||||||
|
$where[] = ['isProcess','=',$isProcess];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!empty($keyword)){
|
||||||
|
$where[] = ['title|description','like','%'.$keyword.'%'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = ToDo::where($where);
|
||||||
|
|
||||||
|
$list = $query->where($where)->page($page,$limit)->order('id desc')->select();
|
||||||
|
$total = $query->count();
|
||||||
|
|
||||||
|
foreach ($list as &$item) {
|
||||||
|
$nickname = Db::table('s2_wechat_friend')->where(['id' => $item['friendId']])->value('nickname');
|
||||||
|
$item['nickname'] = !empty($nickname) ? $nickname : '-';
|
||||||
|
$item['reminderTime'] = date('Y-m-d H:i:s',$item['reminderTime']);
|
||||||
|
}
|
||||||
|
unset($item);
|
||||||
|
|
||||||
|
return ResponseHelper::success(['list'=>$list,'total'=>$total]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加
|
||||||
|
* @return \think\response\Json
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function create(){
|
||||||
|
$level = $this->request->param('level', 0);
|
||||||
|
$title = $this->request->param('title', '');
|
||||||
|
$reminderTime = $this->request->param('reminderTime', '');
|
||||||
|
$description = $this->request->param('description', '');
|
||||||
|
$friendId = $this->request->param('friendId', '');
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
|
||||||
|
if (empty($title) || empty($reminderTime) || empty($description) || empty($friendId)){
|
||||||
|
return ResponseHelper::error('参数缺失');
|
||||||
|
}
|
||||||
|
$friend = Db::name('wechat_friendship')->where(['id' => $friendId,'companyId' => $companyId])->find();
|
||||||
|
if (empty($friend)) {
|
||||||
|
return ResponseHelper::error('好友不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
$todo = new ToDo();
|
||||||
|
$todo->level = $level;
|
||||||
|
$todo->title = $title;
|
||||||
|
$todo->friendId = $friendId;
|
||||||
|
$todo->reminderTime = !empty($reminderTime) ? strtotime($reminderTime) : time();
|
||||||
|
$todo->description = $description;
|
||||||
|
$todo->userId = $userId;
|
||||||
|
$todo->companyId = $companyId;
|
||||||
|
$todo->updateTime = time();
|
||||||
|
$todo->createTime = time();
|
||||||
|
$todo->save();
|
||||||
|
Db::commit();
|
||||||
|
return ResponseHelper::success(' ','创建成功');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Db::rollback();
|
||||||
|
return ResponseHelper::error('创建失败:'.$e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理代办事项
|
||||||
|
* @return \think\response\Json
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function process(){
|
||||||
|
$ids = $this->request->param('ids','');
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
|
||||||
|
if (empty($ids)){
|
||||||
|
return ResponseHelper::error('参数缺失');
|
||||||
|
}
|
||||||
|
$ids = explode(',',$ids);
|
||||||
|
|
||||||
|
if (!is_array($ids)){
|
||||||
|
return ResponseHelper::error('格式错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
$todoIds = ToDo::where(['userId' => $userId,'companyId' => $companyId,'isProcess' => 0])->whereIn('id',$ids)->column('id');
|
||||||
|
if (empty($todoIds)){
|
||||||
|
return ResponseHelper::error('代办事项不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
ToDo::whereIn('id',$todoIds)->update(['isProcess' => 1,'isRemind' => 1,'updateTime' => time()]);
|
||||||
|
Db::commit();
|
||||||
|
return ResponseHelper::success(' ','已处理');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Db::rollback();
|
||||||
|
return ResponseHelper::error('处理失败:'.$e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\chukebao\controller;
|
||||||
|
|
||||||
|
use app\chukebao\model\TokensRecord;
|
||||||
|
use library\ResponseHelper;
|
||||||
|
use think\Db;
|
||||||
|
|
||||||
|
class TokensRecordController extends BaseController
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
public function getList(){
|
||||||
|
$page = $this->request->param('page', 1);
|
||||||
|
$limit = $this->request->param('limit', 10);
|
||||||
|
$type = $this->request->param('type', '');
|
||||||
|
$form = $this->request->param('form', '');
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
|
||||||
|
|
||||||
|
$where = [
|
||||||
|
['companyId','=',$companyId],
|
||||||
|
['userId' ,'=', $userId]
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($type != '') {
|
||||||
|
$where[] = ['type','=',$type];
|
||||||
|
}
|
||||||
|
if ($form != '') {
|
||||||
|
$where[] = ['form','=',$form];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$query = TokensRecord::where($where);
|
||||||
|
|
||||||
|
$list = $query->where($where)->page($page,$limit)->order('id desc')->select();
|
||||||
|
$total = $query->count();
|
||||||
|
|
||||||
|
foreach ($list as &$item) {
|
||||||
|
if (in_array($item['type'],[1])){
|
||||||
|
$nickname = Db::table('s2_wechat_friend')->where(['id' => $item['friendIdOrGroupId']])->value('nickname');
|
||||||
|
$item['nickname'] = !empty($nickname) ? $nickname : '-';
|
||||||
|
}
|
||||||
|
if (in_array($item['type'],[2,3])){
|
||||||
|
$nickname = Db::table('s2_wechat_chatroom')->where(['id' => $item['friendIdOrGroupId']])->value('nickname');
|
||||||
|
$item['nickname'] = !empty($nickname) ? $nickname : '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($item);
|
||||||
|
|
||||||
|
return ResponseHelper::success(['list'=>$list,'total'=>$total]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function consumeTokens($data = [])
|
||||||
|
{
|
||||||
|
if (empty($data)){
|
||||||
|
return ResponseHelper::error('数据缺失');
|
||||||
|
}
|
||||||
|
|
||||||
|
$tokens = isset($data['tokens']) ? intval($data['tokens']) : 0;
|
||||||
|
$type = isset($data['type']) ? intval($data['type']) : 0;
|
||||||
|
$form = isset($data['form']) ? intval($data['form']) : 0;
|
||||||
|
$wechatAccountId = isset($data['wechatAccountId']) ? intval($data['wechatAccountId']) : 0;
|
||||||
|
$friendIdOrGroupId = isset($data['friendIdOrGroupId']) ? intval($data['friendIdOrGroupId']) : 0;
|
||||||
|
$remarks = isset($data['remarks']) ? $data['remarks'] : '';
|
||||||
|
$companyId = isset($data['companyId']) ? intval($data['companyId']) : $this->getUserInfo('companyId');
|
||||||
|
$userId = isset($data['userId']) ? intval($data['userId']) : $this->getUserInfo('id');
|
||||||
|
|
||||||
|
// 验证必要参数
|
||||||
|
if ($tokens <= 0) {
|
||||||
|
return ResponseHelper::error('tokens数量必须大于0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($type, [0, 1])) {
|
||||||
|
return ResponseHelper::error('类型参数错误,0为减少,1为增加');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($form, [0, 1, 2, 3, 4, 5])) {
|
||||||
|
return ResponseHelper::error('来源参数错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重试机制,最多重试3次
|
||||||
|
$maxRetries = 3;
|
||||||
|
$retryCount = 0;
|
||||||
|
while ($retryCount < $maxRetries) {
|
||||||
|
try {
|
||||||
|
return $this->doConsumeTokens($userId, $companyId, $tokens, $type, $form, $wechatAccountId, $friendIdOrGroupId, $remarks);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$retryCount++;
|
||||||
|
if ($retryCount >= $maxRetries) {
|
||||||
|
return ResponseHelper::error('操作失败,请稍后重试:' . $e->getMessage());
|
||||||
|
}
|
||||||
|
// 短暂延迟后重试
|
||||||
|
usleep(100000); // 100ms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行tokens消费的核心方法
|
||||||
|
*/
|
||||||
|
private function doConsumeTokens($userId, $companyId, $tokens, $type, $form, $wechatAccountId, $friendIdOrGroupId, $remarks)
|
||||||
|
{
|
||||||
|
// 开启数据库事务
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
// 使用悲观锁获取用户当前tokens余额,确保并发安全
|
||||||
|
$userInfo = Db::name('users')
|
||||||
|
->where('id', $userId)
|
||||||
|
->where('companyId', $companyId)
|
||||||
|
->lock(true) // 悲观锁,防止并发问题
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if (!$userInfo) {
|
||||||
|
throw new \Exception('用户不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentTokens = intval($userInfo['tokens']);
|
||||||
|
|
||||||
|
// 计算新的余额
|
||||||
|
$newBalance = $type == 1 ? ($currentTokens + $tokens) : ($currentTokens - $tokens);
|
||||||
|
|
||||||
|
// 使用原子更新操作,基于当前值进行更新,防止并发覆盖
|
||||||
|
$updateResult = Db::name('users')
|
||||||
|
->where('id', $userId)
|
||||||
|
->where('companyId', $companyId)
|
||||||
|
->where('tokens', $currentTokens) // 确保基于当前值更新
|
||||||
|
->update([
|
||||||
|
'tokens' => $newBalance,
|
||||||
|
'updateTime' => time()
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$updateResult) {
|
||||||
|
// 如果更新失败,说明tokens值已被其他事务修改,需要重新获取
|
||||||
|
throw new \Exception('tokens余额已被其他操作修改,请重试');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录tokens变动
|
||||||
|
$recordData = [
|
||||||
|
'companyId' => $companyId,
|
||||||
|
'userId' => $userId,
|
||||||
|
'wechatAccountId' => $wechatAccountId,
|
||||||
|
'friendIdOrGroupId' => $friendIdOrGroupId,
|
||||||
|
'form' => $form,
|
||||||
|
'type' => $type,
|
||||||
|
'tokens' => $tokens,
|
||||||
|
'balanceTokens' => $newBalance,
|
||||||
|
'remarks' => $remarks,
|
||||||
|
'createTime' => time()
|
||||||
|
];
|
||||||
|
|
||||||
|
$recordId = Db::name('tokens_record')->insertGetId($recordData);
|
||||||
|
|
||||||
|
if (!$recordId) {
|
||||||
|
throw new \Exception('记录tokens变动失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交事务
|
||||||
|
Db::commit();
|
||||||
|
|
||||||
|
return ResponseHelper::success([
|
||||||
|
'recordId' => $recordId,
|
||||||
|
'oldBalance' => $currentTokens,
|
||||||
|
'newBalance' => $newBalance,
|
||||||
|
'changeAmount' => $type == 1 ? $tokens : -$tokens
|
||||||
|
], 'tokens变动记录成功');
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// 回滚事务
|
||||||
|
Db::rollback();
|
||||||
|
throw $e; // 重新抛出异常,让重试机制处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace app\chukebao\controller;
|
namespace app\chukebao\controller;
|
||||||
|
|
||||||
|
use app\ai\controller\DouBaoAI;
|
||||||
|
use app\chukebao\controller\TokensRecordController as tokensRecord;
|
||||||
use library\ResponseHelper;
|
use library\ResponseHelper;
|
||||||
use think\Db;
|
use think\Db;
|
||||||
|
|
||||||
@@ -22,12 +24,121 @@ class WechatChatroomController extends BaseController
|
|||||||
$total = $query->count();
|
$total = $query->count();
|
||||||
|
|
||||||
|
|
||||||
foreach ($list as $k=>&$v){
|
// 提取所有聊天室ID,用于批量查询
|
||||||
$v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s',$v['createTime']) : '';
|
$chatroomIds = array_column($list, 'id');
|
||||||
$v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s',$v['updateTime']) : '';
|
|
||||||
|
|
||||||
|
// 一次性查询所有聊天室的未读消息数量
|
||||||
|
$unreadCounts = [];
|
||||||
|
if (!empty($chatroomIds)) {
|
||||||
|
$unreadResults = Db::table('s2_wechat_message')
|
||||||
|
->field('wechatChatroomId, COUNT(*) as count')
|
||||||
|
->where('wechatChatroomId', 'in', $chatroomIds)
|
||||||
|
->where('isRead', 0)
|
||||||
|
->group('wechatChatroomId')
|
||||||
|
->select();
|
||||||
|
|
||||||
|
foreach ($unreadResults as $result) {
|
||||||
|
$unreadCounts[$result['wechatChatroomId']] = $result['count'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 一次性查询所有聊天室的最新消息
|
||||||
|
$latestMessages = [];
|
||||||
|
if (!empty($chatroomIds)) {
|
||||||
|
// 使用子查询获取每个聊天室的最新消息ID
|
||||||
|
$subQuery = Db::table('s2_wechat_message')
|
||||||
|
->field('MAX(id) as max_id, wechatChatroomId')
|
||||||
|
->where('wechatChatroomId', 'in', $chatroomIds)
|
||||||
|
->group('wechatChatroomId')
|
||||||
|
->buildSql();
|
||||||
|
|
||||||
|
// 查询最新消息的详细信息
|
||||||
|
$messageResults = Db::table('s2_wechat_message')
|
||||||
|
->alias('m')
|
||||||
|
->join([$subQuery => 'sub'], 'm.id = sub.max_id')
|
||||||
|
->field('m.*, sub.wechatChatroomId')
|
||||||
|
->select();
|
||||||
|
|
||||||
|
foreach ($messageResults as $message) {
|
||||||
|
$latestMessages[$message['wechatChatroomId']] = $message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理每个聊天室的数据
|
||||||
|
foreach ($list as $k => &$v) {
|
||||||
|
$v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : '';
|
||||||
|
$v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s', $v['updateTime']) : '';
|
||||||
|
|
||||||
|
$config = [
|
||||||
|
'unreadCount' => isset($unreadCounts[$v['id']]) ? $unreadCounts[$v['id']] : 0,
|
||||||
|
'chat' => isset($latestMessages[$v['id']]),
|
||||||
|
'msgTime' => isset($latestMessages[$v['id']]) ? $latestMessages[$v['id']]['wechatTime'] : 0
|
||||||
|
];
|
||||||
|
$v['config'] = $config;
|
||||||
}
|
}
|
||||||
unset($v);
|
unset($v);
|
||||||
|
|
||||||
return ResponseHelper::success(['list'=>$list,'total'=>$total]);
|
return ResponseHelper::success(['list'=>$list,'total'=>$total]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public function aiAnnouncement()
|
||||||
|
{
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
$wechatAccountId = $this->request->param('wechatAccountId', '');
|
||||||
|
$groupId = $this->request->param('groupId', '');
|
||||||
|
$content = $this->request->param('content', '');
|
||||||
|
|
||||||
|
if (empty($groupId) || empty($content)|| empty($wechatAccountId)){
|
||||||
|
return ResponseHelper::error('参数缺失');
|
||||||
|
}
|
||||||
|
|
||||||
|
$tokens = Db::name('users')
|
||||||
|
->where('id', $userId)
|
||||||
|
->where('companyId', $companyId)
|
||||||
|
->value('tokens');
|
||||||
|
if ($tokens <= 0){
|
||||||
|
return ResponseHelper::error('用户Tokens余额不足');
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
'model' => 'doubao-1-5-pro-32k-250115',
|
||||||
|
'messages' => [
|
||||||
|
['role' => 'system', 'content' => '你现在是存客宝的AI助理,你精通中国大陆的法律'],
|
||||||
|
['role' => 'user', 'content' => $content],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
//AI处理
|
||||||
|
$ai = new DouBaoAI();
|
||||||
|
$res = $ai->text($params);
|
||||||
|
$res = json_decode($res,true);
|
||||||
|
|
||||||
|
|
||||||
|
if ($res['code'] == 200) {
|
||||||
|
//扣除Tokens
|
||||||
|
$tokensRecord = new tokensRecord();
|
||||||
|
$nickname = Db::table('s2_wechat_chatroom')->where(['id' => $groupId])->value('nickname');
|
||||||
|
$remarks = !empty($nickname) ? '生成【'.$nickname.'】群公告' : '生成群公告';
|
||||||
|
$data = [
|
||||||
|
'tokens' => $res['data']['token'],
|
||||||
|
'type' => 0,
|
||||||
|
'form' => 3,
|
||||||
|
'wechatAccountId' => $wechatAccountId,
|
||||||
|
'friendIdOrGroupId' => $groupId,
|
||||||
|
'remarks' => $remarks,
|
||||||
|
];
|
||||||
|
$tokensRecord->consumeTokens($data);
|
||||||
|
return ResponseHelper::success($res['data']['content']);
|
||||||
|
}else{
|
||||||
|
return ResponseHelper::error($res['msg']);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
exit_data($res);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -22,10 +22,71 @@ class WechatFriendController extends BaseController
|
|||||||
$total = $query->count();
|
$total = $query->count();
|
||||||
|
|
||||||
|
|
||||||
foreach ($list as $k=>&$v){
|
// 提取所有好友ID
|
||||||
$v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s',$v['createTime']) : '';
|
$friendIds = array_column($list, 'id');
|
||||||
$v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s',$v['updateTime']) : '';
|
|
||||||
$v['passTime'] = !empty($v['passTime']) ? date('Y-m-d H:i:s',$v['passTime']) : '';
|
// 一次性查询所有好友的未读消息数量
|
||||||
|
$unreadCounts = [];
|
||||||
|
if (!empty($friendIds)) {
|
||||||
|
$unreadResults = Db::table('s2_wechat_message')
|
||||||
|
->field('wechatFriendId, COUNT(*) as count')
|
||||||
|
->where('wechatFriendId', 'in', $friendIds)
|
||||||
|
->where('isRead', 0)
|
||||||
|
->group('wechatFriendId')
|
||||||
|
->select();
|
||||||
|
|
||||||
|
foreach ($unreadResults as $result) {
|
||||||
|
$unreadCounts[$result['wechatFriendId']] = $result['count'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 一次性查询所有好友的最新消息
|
||||||
|
$latestMessages = [];
|
||||||
|
if (!empty($friendIds)) {
|
||||||
|
// 使用子查询获取每个好友的最新消息ID
|
||||||
|
$subQuery = Db::table('s2_wechat_message')
|
||||||
|
->field('MAX(id) as max_id, wechatFriendId')
|
||||||
|
->where('wechatFriendId', 'in', $friendIds)
|
||||||
|
->group('wechatFriendId')
|
||||||
|
->buildSql();
|
||||||
|
|
||||||
|
// 查询最新消息的详细信息
|
||||||
|
$messageResults = Db::table('s2_wechat_message')
|
||||||
|
->alias('m')
|
||||||
|
->join([$subQuery => 'sub'], 'm.id = sub.max_id')
|
||||||
|
->field('m.*, sub.wechatFriendId')
|
||||||
|
->select();
|
||||||
|
|
||||||
|
foreach ($messageResults as $message) {
|
||||||
|
$latestMessages[$message['wechatFriendId']] = $message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$aiTypeData = [];
|
||||||
|
if (!empty($friendIds)) {
|
||||||
|
$aiTypeData = Db::name('ai_friend_settings')
|
||||||
|
->where('friendId', 'in', $friendIds)
|
||||||
|
->column('friendId,type');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 处理每个好友的数据
|
||||||
|
foreach ($list as $k => &$v) {
|
||||||
|
$v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : '';
|
||||||
|
$v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s', $v['updateTime']) : '';
|
||||||
|
$v['passTime'] = !empty($v['passTime']) ? date('Y-m-d H:i:s', $v['passTime']) : '';
|
||||||
|
|
||||||
|
|
||||||
|
$config = [
|
||||||
|
'unreadCount' => isset($unreadCounts[$v['id']]) ? $unreadCounts[$v['id']] : 0,
|
||||||
|
'chat' => isset($latestMessages[$v['id']]),
|
||||||
|
'msgTime' => isset($latestMessages[$v['id']]) ? $latestMessages[$v['id']]['wechatTime'] : 0
|
||||||
|
];
|
||||||
|
|
||||||
|
// 将消息配置添加到好友数据中
|
||||||
|
$v['config'] = $config;
|
||||||
|
$v['aiType'] = isset($aiTypeData[$v['id']]) ? $aiTypeData[$v['id']] : 0;
|
||||||
}
|
}
|
||||||
unset($v);
|
unset($v);
|
||||||
|
|
||||||
|
|||||||
17
Server/application/chukebao/model/AiFriendSettings.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\chukebao\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
class AiFriendSettings extends Model
|
||||||
|
{
|
||||||
|
protected $pk = 'id';
|
||||||
|
protected $name = 'ai_friend_settings';
|
||||||
|
|
||||||
|
// 自动写入时间戳
|
||||||
|
protected $autoWriteTimestamp = true;
|
||||||
|
protected $createTime = 'createTime';
|
||||||
|
protected $updateTime = 'updateTime';
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
17
Server/application/chukebao/model/FollowUp.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\chukebao\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
class FollowUp extends Model
|
||||||
|
{
|
||||||
|
protected $pk = 'id';
|
||||||
|
protected $name = 'follow_up';
|
||||||
|
|
||||||
|
// 自动写入时间戳
|
||||||
|
protected $autoWriteTimestamp = true;
|
||||||
|
protected $createTime = 'createTime';
|
||||||
|
protected $updateTime = 'updateTime';
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
17
Server/application/chukebao/model/Questions.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\chukebao\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
class Questions extends Model
|
||||||
|
{
|
||||||
|
protected $pk = 'id';
|
||||||
|
protected $name = 'ai_questions';
|
||||||
|
|
||||||
|
// 自动写入时间戳
|
||||||
|
protected $autoWriteTimestamp = true;
|
||||||
|
protected $createTime = 'createTime';
|
||||||
|
protected $updateTime = 'updateTime';
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
17
Server/application/chukebao/model/ToDo.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\chukebao\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
class ToDo extends Model
|
||||||
|
{
|
||||||
|
protected $pk = 'id';
|
||||||
|
protected $name = 'to_do';
|
||||||
|
|
||||||
|
// 自动写入时间戳
|
||||||
|
protected $autoWriteTimestamp = true;
|
||||||
|
protected $createTime = 'createTime';
|
||||||
|
protected $updateTime = 'updateTime';
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
16
Server/application/chukebao/model/TokensRecord.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\chukebao\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
class TokensRecord extends Model
|
||||||
|
{
|
||||||
|
protected $pk = 'id';
|
||||||
|
protected $name = 'tokens_record';
|
||||||
|
|
||||||
|
// 自动写入时间戳
|
||||||
|
protected $autoWriteTimestamp = true;
|
||||||
|
protected $createTime = 'createTime';
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
100
Server/application/command/CleanExpiredGroupMessages.php
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
namespace app\command;
|
||||||
|
|
||||||
|
use think\console\Command;
|
||||||
|
use think\console\Input;
|
||||||
|
use think\console\Output;
|
||||||
|
use think\console\input\Option;
|
||||||
|
use think\Db;
|
||||||
|
|
||||||
|
class CleanExpiredGroupMessages extends Command
|
||||||
|
{
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$this->setName('clean:expired_group_messages')
|
||||||
|
->setDescription('Clean expired group messages from the database')
|
||||||
|
->addOption('days', 'd', Option::VALUE_OPTIONAL, 'Number of days to keep messages (default: 90)', 90)
|
||||||
|
->addOption('dry-run', null, Option::VALUE_NONE, 'Perform a dry run without deleting any data')
|
||||||
|
->addOption('batch-size', 'b', Option::VALUE_OPTIONAL, 'Batch size for deletion (default: 1000)', 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(Input $input, Output $output)
|
||||||
|
{
|
||||||
|
$days = (int)$input->getOption('days');
|
||||||
|
$dryRun = $input->getOption('dry-run');
|
||||||
|
$batchSize = (int)$input->getOption('batch-size');
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$output->writeln("<info>Running in dry-run mode. No data will be deleted.</info>");
|
||||||
|
}
|
||||||
|
|
||||||
|
$cutoffDate = date('Y-m-d H:i:s', strtotime("-{$days} days"));
|
||||||
|
$output->writeln("<info>Cleaning group messages older than {$cutoffDate} (keeping last {$days} days)</info>");
|
||||||
|
|
||||||
|
// 清理微信群组消息
|
||||||
|
$this->cleanWechatGroupMessages($cutoffDate, $dryRun, $batchSize, $output);
|
||||||
|
|
||||||
|
$output->writeln("<info>Group message cleanup completed successfully.</info>");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function cleanWechatGroupMessages($cutoffDate, $dryRun, $batchSize, Output $output)
|
||||||
|
{
|
||||||
|
$output->writeln("\nCleaning s2_wechat_group_message table...");
|
||||||
|
|
||||||
|
// 获取符合条件的消息总数
|
||||||
|
$totalCount = Db::table('s2_wechat_group_message')
|
||||||
|
->where('createTime', '<', $cutoffDate)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if ($totalCount === 0) {
|
||||||
|
$output->writeln(" <comment>No expired group messages found.</comment>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$output->writeln(" Found {$totalCount} group messages to clean up.");
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$output->writeln(" <comment>Dry run mode: would delete {$totalCount} group messages.</comment>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算需要执行的批次数
|
||||||
|
$batches = ceil($totalCount / $batchSize);
|
||||||
|
$deletedCount = 0;
|
||||||
|
|
||||||
|
$output->writeln(" Deleting in {$batches} batches of {$batchSize} records...");
|
||||||
|
|
||||||
|
// 分批删除数据
|
||||||
|
for ($i = 0; $i < $batches; $i++) {
|
||||||
|
// 获取一批要删除的ID
|
||||||
|
$ids = Db::table('s2_wechat_group_message')
|
||||||
|
->where('createTime', '<', $cutoffDate)
|
||||||
|
->limit($batchSize)
|
||||||
|
->column('id');
|
||||||
|
|
||||||
|
if (empty($ids)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除这批数据
|
||||||
|
$count = Db::table('s2_wechat_group_message')
|
||||||
|
->whereIn('id', $ids)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
$deletedCount += $count;
|
||||||
|
$progress = round(($deletedCount / $totalCount) * 100, 2);
|
||||||
|
$output->write(" Progress: {$progress}% ({$deletedCount}/{$totalCount})\r");
|
||||||
|
|
||||||
|
// 短暂暂停,减轻数据库负担
|
||||||
|
usleep(500000); // 暂停0.5秒
|
||||||
|
}
|
||||||
|
|
||||||
|
$output->writeln("");
|
||||||
|
$output->writeln(" <info>Successfully deleted {$deletedCount} expired group messages.</info>");
|
||||||
|
|
||||||
|
// 优化表
|
||||||
|
$output->writeln(" Optimizing table...");
|
||||||
|
Db::execute("OPTIMIZE TABLE s2_wechat_group_message");
|
||||||
|
$output->writeln(" <info>Table optimization completed.</info>");
|
||||||
|
}
|
||||||
|
}
|
||||||
100
Server/application/command/CleanExpiredMessages.php
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
namespace app\command;
|
||||||
|
|
||||||
|
use think\console\Command;
|
||||||
|
use think\console\Input;
|
||||||
|
use think\console\Output;
|
||||||
|
use think\console\input\Option;
|
||||||
|
use think\Db;
|
||||||
|
|
||||||
|
class CleanExpiredMessages extends Command
|
||||||
|
{
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$this->setName('clean:expired_messages')
|
||||||
|
->setDescription('Clean expired messages from the database')
|
||||||
|
->addOption('days', 'd', Option::VALUE_OPTIONAL, 'Number of days to keep messages (default: 90)', 90)
|
||||||
|
->addOption('dry-run', null, Option::VALUE_NONE, 'Perform a dry run without deleting any data')
|
||||||
|
->addOption('batch-size', 'b', Option::VALUE_OPTIONAL, 'Batch size for deletion (default: 1000)', 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(Input $input, Output $output)
|
||||||
|
{
|
||||||
|
$days = (int)$input->getOption('days');
|
||||||
|
$dryRun = $input->getOption('dry-run');
|
||||||
|
$batchSize = (int)$input->getOption('batch-size');
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$output->writeln("<info>Running in dry-run mode. No data will be deleted.</info>");
|
||||||
|
}
|
||||||
|
|
||||||
|
$cutoffDate = date('Y-m-d H:i:s', strtotime("-{$days} days"));
|
||||||
|
$output->writeln("<info>Cleaning messages older than {$cutoffDate} (keeping last {$days} days)</info>");
|
||||||
|
|
||||||
|
// 清理微信消息
|
||||||
|
$this->cleanWechatMessages($cutoffDate, $dryRun, $batchSize, $output);
|
||||||
|
|
||||||
|
$output->writeln("<info>Message cleanup completed successfully.</info>");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function cleanWechatMessages($cutoffDate, $dryRun, $batchSize, Output $output)
|
||||||
|
{
|
||||||
|
$output->writeln("\nCleaning s2_wechat_message table...");
|
||||||
|
|
||||||
|
// 获取符合条件的消息总数
|
||||||
|
$totalCount = Db::table('s2_wechat_message')
|
||||||
|
->where('createTime', '<', $cutoffDate)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if ($totalCount === 0) {
|
||||||
|
$output->writeln(" <comment>No expired messages found.</comment>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$output->writeln(" Found {$totalCount} messages to clean up.");
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$output->writeln(" <comment>Dry run mode: would delete {$totalCount} messages.</comment>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算需要执行的批次数
|
||||||
|
$batches = ceil($totalCount / $batchSize);
|
||||||
|
$deletedCount = 0;
|
||||||
|
|
||||||
|
$output->writeln(" Deleting in {$batches} batches of {$batchSize} records...");
|
||||||
|
|
||||||
|
// 分批删除数据
|
||||||
|
for ($i = 0; $i < $batches; $i++) {
|
||||||
|
// 获取一批要删除的ID
|
||||||
|
$ids = Db::table('s2_wechat_message')
|
||||||
|
->where('createTime', '<', $cutoffDate)
|
||||||
|
->limit($batchSize)
|
||||||
|
->column('id');
|
||||||
|
|
||||||
|
if (empty($ids)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除这批数据
|
||||||
|
$count = Db::table('s2_wechat_message')
|
||||||
|
->whereIn('id', $ids)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
$deletedCount += $count;
|
||||||
|
$progress = round(($deletedCount / $totalCount) * 100, 2);
|
||||||
|
$output->write(" Progress: {$progress}% ({$deletedCount}/{$totalCount})\r");
|
||||||
|
|
||||||
|
// 短暂暂停,减轻数据库负担
|
||||||
|
usleep(500000); // 暂停0.5秒
|
||||||
|
}
|
||||||
|
|
||||||
|
$output->writeln("");
|
||||||
|
$output->writeln(" <info>Successfully deleted {$deletedCount} expired messages.</info>");
|
||||||
|
|
||||||
|
// 优化表
|
||||||
|
$output->writeln(" Optimizing table...");
|
||||||
|
Db::execute("OPTIMIZE TABLE s2_wechat_message");
|
||||||
|
$output->writeln(" <info>Table optimization completed.</info>");
|
||||||
|
}
|
||||||
|
}
|
||||||
112
Server/application/command/OptimizeMessageIndexes.php
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<?php
|
||||||
|
namespace app\command;
|
||||||
|
|
||||||
|
use think\console\Command;
|
||||||
|
use think\console\Input;
|
||||||
|
use think\console\Output;
|
||||||
|
use think\Db;
|
||||||
|
|
||||||
|
class OptimizeMessageIndexes extends Command
|
||||||
|
{
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$this->setName('optimize:message_indexes')
|
||||||
|
->setDescription('Optimize database indexes for message-related tables');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(Input $input, Output $output)
|
||||||
|
{
|
||||||
|
$output->writeln("Starting index optimization for message-related tables...");
|
||||||
|
|
||||||
|
// 优化 s2_wechat_message 表索引
|
||||||
|
$this->optimizeWechatMessageIndexes($output);
|
||||||
|
|
||||||
|
// 优化 s2_wechat_chatroom 表索引
|
||||||
|
$this->optimizeWechatChatroomIndexes($output);
|
||||||
|
|
||||||
|
// 优化 s2_wechat_friend 表索引
|
||||||
|
$this->optimizeWechatFriendIndexes($output);
|
||||||
|
|
||||||
|
$output->writeln("Index optimization completed successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function optimizeWechatMessageIndexes(Output $output)
|
||||||
|
{
|
||||||
|
$output->writeln("Optimizing s2_wechat_message table indexes...");
|
||||||
|
|
||||||
|
// 检查并添加 wechatChatroomId 索引
|
||||||
|
$this->addIndexIfNotExists('s2_wechat_message', 'idx_chatroom_id', 'wechatChatroomId', $output);
|
||||||
|
|
||||||
|
// 检查并添加 wechatFriendId 索引
|
||||||
|
$this->addIndexIfNotExists('s2_wechat_message', 'idx_friend_id', 'wechatFriendId', $output);
|
||||||
|
|
||||||
|
// 检查并添加 isRead 索引
|
||||||
|
$this->addIndexIfNotExists('s2_wechat_message', 'idx_is_read', 'isRead', $output);
|
||||||
|
|
||||||
|
// 检查并添加 type 索引
|
||||||
|
$this->addIndexIfNotExists('s2_wechat_message', 'idx_type', 'type', $output);
|
||||||
|
|
||||||
|
// 检查并添加 createTime 索引
|
||||||
|
$this->addIndexIfNotExists('s2_wechat_message', 'idx_create_time', 'createTime', $output);
|
||||||
|
|
||||||
|
// 检查并添加组合索引 (wechatChatroomId, isRead)
|
||||||
|
$this->addIndexIfNotExists('s2_wechat_message', 'idx_chatroom_read', 'wechatChatroomId,isRead', $output);
|
||||||
|
|
||||||
|
// 检查并添加组合索引 (wechatFriendId, isRead)
|
||||||
|
$this->addIndexIfNotExists('s2_wechat_message', 'idx_friend_read', 'wechatFriendId,isRead', $output);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function optimizeWechatChatroomIndexes(Output $output)
|
||||||
|
{
|
||||||
|
$output->writeln("Optimizing s2_wechat_chatroom table indexes...");
|
||||||
|
|
||||||
|
// 检查并添加 accountId 索引
|
||||||
|
$this->addIndexIfNotExists('s2_wechat_chatroom', 'idx_account_id', 'accountId', $output);
|
||||||
|
|
||||||
|
// 检查并添加 isDeleted 索引
|
||||||
|
$this->addIndexIfNotExists('s2_wechat_chatroom', 'idx_is_deleted', 'isDeleted', $output);
|
||||||
|
|
||||||
|
// 检查并添加组合索引 (accountId, isDeleted)
|
||||||
|
$this->addIndexIfNotExists('s2_wechat_chatroom', 'idx_account_deleted', 'accountId,isDeleted', $output);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function optimizeWechatFriendIndexes(Output $output)
|
||||||
|
{
|
||||||
|
$output->writeln("Optimizing s2_wechat_friend table indexes...");
|
||||||
|
|
||||||
|
// 检查并添加 accountId 索引
|
||||||
|
$this->addIndexIfNotExists('s2_wechat_friend', 'idx_account_id', 'accountId', $output);
|
||||||
|
|
||||||
|
// 检查并添加 isDeleted 索引
|
||||||
|
$this->addIndexIfNotExists('s2_wechat_friend', 'idx_is_deleted', 'isDeleted', $output);
|
||||||
|
|
||||||
|
// 检查并添加组合索引 (accountId, isDeleted)
|
||||||
|
$this->addIndexIfNotExists('s2_wechat_friend', 'idx_account_deleted', 'accountId,isDeleted', $output);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function addIndexIfNotExists($table, $indexName, $columns, Output $output)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// 检查索引是否已存在
|
||||||
|
$indexExists = false;
|
||||||
|
$indexes = Db::query("SHOW INDEX FROM {$table}");
|
||||||
|
|
||||||
|
foreach ($indexes as $index) {
|
||||||
|
if ($index['Key_name'] === $indexName) {
|
||||||
|
$indexExists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$indexExists) {
|
||||||
|
// 添加索引
|
||||||
|
Db::execute("ALTER TABLE {$table} ADD INDEX {$indexName} ({$columns})");
|
||||||
|
$output->writeln(" - Added index {$indexName} on {$table}({$columns})");
|
||||||
|
} else {
|
||||||
|
$output->writeln(" - Index {$indexName} already exists on {$table}");
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$output->writeln(" - Error adding index {$indexName} to {$table}: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
121
Server/application/command/ScheduleMessageMaintenance.php
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
namespace app\command;
|
||||||
|
|
||||||
|
use think\console\Command;
|
||||||
|
use think\console\Input;
|
||||||
|
use think\console\Output;
|
||||||
|
use think\console\input\Option;
|
||||||
|
|
||||||
|
class ScheduleMessageMaintenance extends Command
|
||||||
|
{
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$this->setName('schedule:message_maintenance')
|
||||||
|
->setDescription('Schedule and run message maintenance tasks')
|
||||||
|
->addOption('optimize-indexes', null, Option::VALUE_NONE, 'Run index optimization')
|
||||||
|
->addOption('clean-messages', null, Option::VALUE_NONE, 'Clean expired messages')
|
||||||
|
->addOption('days', 'd', Option::VALUE_OPTIONAL, 'Number of days to keep messages (default: 90)', 90)
|
||||||
|
->addOption('batch-size', 'b', Option::VALUE_OPTIONAL, 'Batch size for deletion (default: 1000)', 1000)
|
||||||
|
->addOption('dry-run', null, Option::VALUE_NONE, 'Perform a dry run without deleting any data');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(Input $input, Output $output)
|
||||||
|
{
|
||||||
|
$optimizeIndexes = $input->getOption('optimize-indexes');
|
||||||
|
$cleanMessages = $input->getOption('clean-messages');
|
||||||
|
$days = (int)$input->getOption('days');
|
||||||
|
$batchSize = (int)$input->getOption('batch-size');
|
||||||
|
$dryRun = $input->getOption('dry-run');
|
||||||
|
|
||||||
|
// 如果没有指定任何选项,则运行所有维护任务
|
||||||
|
if (!$optimizeIndexes && !$cleanMessages) {
|
||||||
|
$optimizeIndexes = true;
|
||||||
|
$cleanMessages = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$output->writeln("<info>Starting scheduled message maintenance tasks...</info>");
|
||||||
|
$startTime = microtime(true);
|
||||||
|
|
||||||
|
// 运行索引优化
|
||||||
|
if ($optimizeIndexes) {
|
||||||
|
$this->runCommand($output, 'optimize:message_indexes');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理过期消息
|
||||||
|
if ($cleanMessages) {
|
||||||
|
$options = [];
|
||||||
|
|
||||||
|
if ($days !== 90) {
|
||||||
|
$options[] = "--days={$days}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($batchSize !== 1000) {
|
||||||
|
$options[] = "--batch-size={$batchSize}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$options[] = "--dry-run";
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->runCommand($output, 'clean:expired_messages', $options);
|
||||||
|
$this->runCommand($output, 'clean:expired_group_messages', $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
$endTime = microtime(true);
|
||||||
|
$executionTime = round($endTime - $startTime, 2);
|
||||||
|
$output->writeln("<info>All maintenance tasks completed in {$executionTime} seconds.</info>");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function runCommand(Output $output, $command, array $options = [])
|
||||||
|
{
|
||||||
|
$output->writeln("\n<comment>Running command: {$command}</comment>");
|
||||||
|
|
||||||
|
$optionsStr = implode(' ', $options);
|
||||||
|
$fullCommand = "php think {$command} {$optionsStr}";
|
||||||
|
|
||||||
|
$output->writeln("Executing: {$fullCommand}");
|
||||||
|
$output->writeln("\n<info>Command output:</info>");
|
||||||
|
|
||||||
|
// 执行命令并实时输出结果
|
||||||
|
$descriptorSpec = [
|
||||||
|
0 => ["pipe", "r"], // stdin
|
||||||
|
1 => ["pipe", "w"], // stdout
|
||||||
|
2 => ["pipe", "w"] // stderr
|
||||||
|
];
|
||||||
|
|
||||||
|
$process = proc_open($fullCommand, $descriptorSpec, $pipes);
|
||||||
|
|
||||||
|
if (is_resource($process)) {
|
||||||
|
// 关闭标准输入
|
||||||
|
fclose($pipes[0]);
|
||||||
|
|
||||||
|
// 读取标准输出
|
||||||
|
while (!feof($pipes[1])) {
|
||||||
|
$line = fgets($pipes[1]);
|
||||||
|
if ($line !== false) {
|
||||||
|
$output->write($line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($pipes[1]);
|
||||||
|
|
||||||
|
// 读取标准错误
|
||||||
|
$errorOutput = stream_get_contents($pipes[2]);
|
||||||
|
fclose($pipes[2]);
|
||||||
|
|
||||||
|
// 获取命令执行结果
|
||||||
|
$exitCode = proc_close($process);
|
||||||
|
|
||||||
|
if ($exitCode !== 0) {
|
||||||
|
$output->writeln("\n<error>Command failed with exit code {$exitCode}</error>");
|
||||||
|
if (!empty($errorOutput)) {
|
||||||
|
$output->writeln("<error>Error output:</error>");
|
||||||
|
$output->writeln($errorOutput);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$output->writeln("\n<info>Command completed successfully.</info>");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$output->writeln("<error>Failed to execute command.</error>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ Route::group('v1/auth', function () {
|
|||||||
Route::post('login', 'app\common\controller\PasswordLoginController@index'); // 账号密码登录
|
Route::post('login', 'app\common\controller\PasswordLoginController@index'); // 账号密码登录
|
||||||
Route::post('mobile-login', 'app\common\controller\Auth@mobileLogin'); // 手机号验证码登录
|
Route::post('mobile-login', 'app\common\controller\Auth@mobileLogin'); // 手机号验证码登录
|
||||||
Route::post('code', 'app\common\controller\Auth@SendCodeController'); // 发送验证码
|
Route::post('code', 'app\common\controller\Auth@SendCodeController'); // 发送验证码
|
||||||
|
|
||||||
// 需要JWT认证的接口
|
// 需要JWT认证的接口
|
||||||
Route::get('info', 'app\common\controller\Auth@info')->middleware(['jwt']); // 获取用户信息
|
Route::get('info', 'app\common\controller\Auth@info')->middleware(['jwt']); // 获取用户信息
|
||||||
Route::post('refresh', 'app\common\controller\Auth@refresh')->middleware(['jwt']); // 刷新令牌
|
Route::post('refresh', 'app\common\controller\Auth@refresh')->middleware(['jwt']); // 刷新令牌
|
||||||
@@ -22,4 +21,13 @@ Route::group('v1/', function () {
|
|||||||
})->middleware(['jwt']);
|
})->middleware(['jwt']);
|
||||||
|
|
||||||
|
|
||||||
Route::get('app/update', 'app\common\controller\Api@uploadApp');
|
|
||||||
|
Route::group('v1/pay', function () {
|
||||||
|
Route::post('', 'app\cunkebao\controller\Pay@createOrder')->middleware(['jwt']);
|
||||||
|
Route::any('notify', 'app\common\controller\PaymentService@notify');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Route::get('app/update', 'app\common\controller\PaymentService@createOrder');
|
||||||
366
Server/application/common/controller/PaymentService.php
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common\controller;
|
||||||
|
|
||||||
|
use think\Db;
|
||||||
|
use app\common\util\PaymentUtil;
|
||||||
|
use think\facade\Config;
|
||||||
|
use think\facade\Env;
|
||||||
|
use think\facade\Log;
|
||||||
|
use think\facade\Request;
|
||||||
|
use app\common\model\Order;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付服务(内部调用)
|
||||||
|
*/
|
||||||
|
class PaymentService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 下单
|
||||||
|
*
|
||||||
|
* @param array $order
|
||||||
|
* - out_trade_no: string 商户订单号(必填)
|
||||||
|
* - total_fee: int 金额(分,必填)
|
||||||
|
* - body: string 商品描述(必填)
|
||||||
|
* - notify_url: string 异步通知地址(可覆盖配置)
|
||||||
|
* - attach: string 附加数据(可选)
|
||||||
|
* - time_expire: string 订单失效时间(可选)
|
||||||
|
* - client_ip: string 终端IP(可选)
|
||||||
|
* - sign_type: string MD5/RSA_1_256/RSA_1_1(可选,默认MD5)
|
||||||
|
* - pay_type: string 支付场景,如 JSAPI/APP/H5(可选)
|
||||||
|
* @return array
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function createOrder(array $order)
|
||||||
|
{
|
||||||
|
$params = [
|
||||||
|
'service' => 'unified.trade.native',
|
||||||
|
'sign_type' => PaymentUtil::SIGN_TYPE_MD5,
|
||||||
|
'mch_id' => Env::get('payment.mchId'),
|
||||||
|
'out_trade_no' => $order['orderNo'],
|
||||||
|
'body' => $order['goodsName'] ?? '',
|
||||||
|
'total_fee' => $order['money'] ?? 0,
|
||||||
|
'mch_create_ip' => Request::ip(),
|
||||||
|
'notify_url' => Env::get('payment.notify_url', '127.0.0.1'),
|
||||||
|
'nonce_str' => PaymentUtil::generateNonceStr(),
|
||||||
|
];
|
||||||
|
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
// 过滤空值签名
|
||||||
|
$secret = Env::get('payment.key');
|
||||||
|
$params['sign_type'] = 'MD5';
|
||||||
|
$params['sign'] = PaymentUtil::generateSign($params, $secret, 'MD5');
|
||||||
|
|
||||||
|
$url = Env::get('payment.url');
|
||||||
|
if (empty($url)) {
|
||||||
|
throw new \Exception('支付网关地址未配置');
|
||||||
|
}
|
||||||
|
|
||||||
|
//创建订单
|
||||||
|
Order::create([
|
||||||
|
'mchId' => $params['mch_id'],
|
||||||
|
'companyId' => $order['companyId'],
|
||||||
|
'userId' => $order['userId'],
|
||||||
|
'orderType' => $order['orderType'] ?? 1,
|
||||||
|
'status' => 0,
|
||||||
|
'goodsId' => $order['goodsId'],
|
||||||
|
'goodsName' => $order['goodsName'],
|
||||||
|
'money' => $order['money'],
|
||||||
|
'orderNo' => $order['orderNo'],
|
||||||
|
'ip' => Request::ip(),
|
||||||
|
'nonceStr' => $params['nonce_str'],
|
||||||
|
'createTime' => time(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
// XML POST 请求
|
||||||
|
$xmlBody = $this->arrayToXml($params);
|
||||||
|
$response = $this->postXml($url, $xmlBody);
|
||||||
|
$parsed = $this->parseXmlOrRaw($response);
|
||||||
|
|
||||||
|
if ($parsed['status'] == 0 && $parsed['result_code'] == 0) {
|
||||||
|
Db::commit();
|
||||||
|
return json(['code' => 200, 'msg' => '订单创建成功', 'data' => $parsed['code_url']]);
|
||||||
|
} else {
|
||||||
|
Db::rollback();
|
||||||
|
return json(['code' => 500, 'msg' => '订单创建失败:' . $parsed['err_msg']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Db::rollback();
|
||||||
|
return json(['code' => 500, 'msg' => '订单创建失败:' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST 请求(x-www-form-urlencoded)
|
||||||
|
*/
|
||||||
|
protected function httpPost(string $url, array $params, array $headers = [])
|
||||||
|
{
|
||||||
|
if (!function_exists('requestCurl')) {
|
||||||
|
throw new \RuntimeException('requestCurl 未定义');
|
||||||
|
}
|
||||||
|
return requestCurl($url, $params, 'POST', $headers, 'dataBuild');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析响应
|
||||||
|
*/
|
||||||
|
protected function parseResponse($response)
|
||||||
|
{
|
||||||
|
if ($response === '' || $response === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$decoded = json_decode($response, true);
|
||||||
|
if (json_last_error() === JSON_ERROR_NONE) {
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
if (strpos($response, '=') !== false && strpos($response, '&') !== false) {
|
||||||
|
$arr = [];
|
||||||
|
foreach (explode('&', $response) as $pair) {
|
||||||
|
if ($pair === '') continue;
|
||||||
|
$kv = explode('=', $pair, 2);
|
||||||
|
$arr[$kv[0]] = $kv[1] ?? '';
|
||||||
|
}
|
||||||
|
return $arr;
|
||||||
|
}
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 以 XML 方式 POST(text/xml)
|
||||||
|
*/
|
||||||
|
protected function postXml(string $url, string $xml)
|
||||||
|
{
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $url);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, 1);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Content-Type: text/xml; charset=UTF-8'
|
||||||
|
]);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||||
|
$res = curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数组转 XML(按 ASCII 升序,字符串走 CDATA)
|
||||||
|
*/
|
||||||
|
protected function arrayToXml(array $data): string
|
||||||
|
{
|
||||||
|
// 过滤空值
|
||||||
|
$filtered = [];
|
||||||
|
foreach ($data as $k => $v) {
|
||||||
|
if ($v === '' || $v === null) continue;
|
||||||
|
$filtered[$k] = $v;
|
||||||
|
}
|
||||||
|
ksort($filtered, SORT_STRING);
|
||||||
|
|
||||||
|
$xml = '<xml>';
|
||||||
|
foreach ($filtered as $key => $value) {
|
||||||
|
if (is_numeric($value)) {
|
||||||
|
$xml .= "<{$key}>{$value}</{$key}>";
|
||||||
|
} else {
|
||||||
|
$xml .= "<{$key}><![CDATA[{$value}]]></{$key}>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$xml .= '</xml>';
|
||||||
|
return $xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 XML 响应
|
||||||
|
*/
|
||||||
|
protected function parseXmlOrRaw($response)
|
||||||
|
{
|
||||||
|
if (!is_string($response) || $response === '') {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
libxml_use_internal_errors(true);
|
||||||
|
$xml = simplexml_load_string($response, 'SimpleXMLElement', LIBXML_NOCDATA);
|
||||||
|
if ($xml !== false) {
|
||||||
|
$json = json_encode($xml, JSON_UNESCAPED_UNICODE);
|
||||||
|
return json_decode($json, true);
|
||||||
|
}
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付结果异步通知
|
||||||
|
* - 威富通回调为 XML;需校验签名与业务字段并更新订单
|
||||||
|
* - 回应:成功回"success",失败回"fail"
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function notify()
|
||||||
|
{
|
||||||
|
$rawBody = file_get_contents('php://input');
|
||||||
|
$payload = $this->parseXmlOrRaw($rawBody);
|
||||||
|
if (!is_array($payload) || empty($payload)) {
|
||||||
|
return json_encode(['code' => 500, 'msg' => 'XML解析错误']);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if ($payload['status'] != 0 || $payload['result_code'] != 0) {
|
||||||
|
$errMsg = (isset($payload['err_msg']) ? $payload['err_msg'] : isset($payload['err_msg'])) ? $payload['err_msg'] : '未知错误';
|
||||||
|
return json_encode(['code' => 500, 'msg' => $errMsg]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 业务处理:更新订单
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
$outTradeNo = $payload['out_trade_no'];
|
||||||
|
$pay_result = $payload['pay_result'];
|
||||||
|
$time_end = $payload['time_end'];
|
||||||
|
$order = Order::where('orderNo', $outTradeNo)->find();
|
||||||
|
if (!$order) {
|
||||||
|
Db::rollback();
|
||||||
|
return json_encode(['code' => 500, 'msg' => '该订单不存在']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pay_result != 0) {
|
||||||
|
$order->payInfo = $payload['pay_info'];
|
||||||
|
$order->payType = $payload['trade_type'] == 'pay.wechat.jspay' ? 1 : 2;
|
||||||
|
$order->status = 3;
|
||||||
|
$order->save();
|
||||||
|
Db::commit();
|
||||||
|
return json_encode(['code' => 500, 'msg' => $payload['pay_info']]);
|
||||||
|
}
|
||||||
|
$order->status = 1;
|
||||||
|
$order->payTime = $this->parsePayTime($time_end);
|
||||||
|
$order->save();
|
||||||
|
Db::commit();
|
||||||
|
return json_encode(['code' => 200, 'msg' => '付款成功']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Db::rollback();
|
||||||
|
return json_encode(['code' => 500, 'msg' => '付款失败' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析威富通时间(yyyyMMddHHmmss)为时间戳
|
||||||
|
*/
|
||||||
|
protected function parsePayTime(string $timeEnd)
|
||||||
|
{
|
||||||
|
if ($timeEnd === '') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// 期望格式:20250102153045
|
||||||
|
if (preg_match('/^\\d{14}$/', $timeEnd) !== 1) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
$dt = \DateTime::createFromFormat('YmdHis', $timeEnd, new \DateTimeZone('Asia/Shanghai'));
|
||||||
|
return $dt ? $dt->getTimestamp() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询订单(威富通 unified.trade.query)
|
||||||
|
* - 入参:商户订单号或平台交易号
|
||||||
|
* - 出参:统一 JSON 格式,包含交易状态与关键信息
|
||||||
|
* @param array $query
|
||||||
|
* - out_trade_no: string 商户订单号(与 transaction_id 二选一)
|
||||||
|
* - transaction_id: string 平台交易号(与 out_trade_no 二选一)
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function queryOrder(array $query)
|
||||||
|
{
|
||||||
|
$outTradeNo = $query['out_trade_no'] ?? ($query['orderNo'] ?? '');
|
||||||
|
$transactionId = $query['transaction_id'] ?? '';
|
||||||
|
|
||||||
|
if ($outTradeNo === '' && $transactionId === '') {
|
||||||
|
return json(['code' => 422, 'msg' => '缺少查询参数:out_trade_no 或 transaction_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
'service' => 'unified.trade.query',
|
||||||
|
'mch_id' => Env::get('payment.mchId'),
|
||||||
|
'out_trade_no' => $outTradeNo ?: null,
|
||||||
|
'nonce_str' => PaymentUtil::generateNonceStr(),
|
||||||
|
'sign_type' => 'MD5',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 过滤空值后签名
|
||||||
|
$secret = Env::get('payment.key');
|
||||||
|
if (empty($secret)) {
|
||||||
|
return json(['code' => 500, 'msg' => '支付密钥未配置']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$filtered = [];
|
||||||
|
foreach ($params as $k => $v) {
|
||||||
|
if ($v === '' || $v === null) continue;
|
||||||
|
$filtered[$k] = $v;
|
||||||
|
}
|
||||||
|
$filtered['sign'] = PaymentUtil::generateSign($filtered, $secret, $filtered['sign_type']);
|
||||||
|
|
||||||
|
$url = Env::get('payment.url');
|
||||||
|
if (empty($url)) {
|
||||||
|
return json(['code' => 500, 'msg' => '支付网关地址未配置']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求网关
|
||||||
|
$xmlBody = $this->arrayToXml($filtered);
|
||||||
|
$response = $this->postXml($url, $xmlBody);
|
||||||
|
$parsed = $this->parseXmlOrRaw($response);
|
||||||
|
|
||||||
|
if (!is_array($parsed)) {
|
||||||
|
return json(['code' => 500, 'msg' => '响应解析失败', 'data' => $response]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($parsed['status'] ?? '') !== '0') {
|
||||||
|
return json(['code' => 500, 'msg' => '通信失败:' . ($parsed['message'] ?? 'unknown')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($parsed['result_code'] ?? '') !== '0') {
|
||||||
|
return json(['code' => 200, 'msg' => '业务失败', 'data' => [
|
||||||
|
'err_code' => $parsed['err_code'] ?? '',
|
||||||
|
'err_msg' => $parsed['err_msg'] ?? '',
|
||||||
|
]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tradeState = $parsed['trade_state'] ?? '';
|
||||||
|
$resp = [
|
||||||
|
'trade_state' => $tradeState,
|
||||||
|
'trade_state_desc' => $parsed['trade_state_desc'] ?? '',
|
||||||
|
'transaction_id' => $parsed['transaction_id'] ?? '',
|
||||||
|
'out_trade_no' => $parsed['out_trade_no'] ?? $outTradeNo,
|
||||||
|
'total_fee' => isset($parsed['total_fee']) ? (int)$parsed['total_fee'] : null,
|
||||||
|
'time_end' => $parsed['time_end'] ?? '',
|
||||||
|
'buyer_logon_id' => $parsed['buyer_logon_id'] ?? '',
|
||||||
|
'bank_type' => $parsed['bank_type'] ?? '',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 若已支付,同步本地订单
|
||||||
|
if ($tradeState === 'SUCCESS' && ($resp['out_trade_no'] ?? '') !== '') {
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
/** @var Order|null $order */
|
||||||
|
$order = Order::where('orderNo', $resp['out_trade_no'])->lock(true)->find();
|
||||||
|
if ($order) {
|
||||||
|
$paidAt = $this->parsePayTime($resp['time_end'] ?? '') ?: time();
|
||||||
|
if ((int)$order['status'] !== 1) {
|
||||||
|
$order->save([
|
||||||
|
'status' => 1,
|
||||||
|
'transactionId' => $resp['transaction_id'] ?? '',
|
||||||
|
'payTime' => $paidAt,
|
||||||
|
'updateTime' => time(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Db::commit();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Db::rollback();
|
||||||
|
Log::error('[SwiftPass][query] update order exception: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json(['code' => 200, 'msg' => '查询成功', 'data' => $resp]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
14
Server/application/common/model/Order.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
|
||||||
|
class Order extends Model
|
||||||
|
{
|
||||||
|
|
||||||
|
// 设置数据表名
|
||||||
|
protected $name = 'order';
|
||||||
|
|
||||||
|
}
|
||||||
255
Server/application/common/util/PaymentUtil.php
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common\util;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付工具类
|
||||||
|
* 用于处理第三方支付相关功能
|
||||||
|
* 仅限内部调用
|
||||||
|
*/
|
||||||
|
class PaymentUtil
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 签名算法类型
|
||||||
|
*/
|
||||||
|
const SIGN_TYPE_MD5 = 'MD5';
|
||||||
|
const SIGN_TYPE_RSA_1_256 = 'RSA_1_256';
|
||||||
|
const SIGN_TYPE_RSA_1_1 = 'RSA_1_1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成支付签名
|
||||||
|
*
|
||||||
|
* @param array $params 待签名参数
|
||||||
|
* @param string $secretKey 签名密钥
|
||||||
|
* @param string $signType 签名类型 MD5/RSA_1_256/RSA_1_1
|
||||||
|
* @return string 签名结果
|
||||||
|
*/
|
||||||
|
public static function generateSign(array $params, string $secretKey, string $signType = self::SIGN_TYPE_MD5): string
|
||||||
|
{
|
||||||
|
// 1. 移除sign字段
|
||||||
|
unset($params['sign']);
|
||||||
|
|
||||||
|
// 2. 过滤空值
|
||||||
|
$params = array_filter($params, function($value) {
|
||||||
|
return $value !== '' && $value !== null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 按字段名ASCII码从小到大排序
|
||||||
|
ksort($params);
|
||||||
|
|
||||||
|
// 4. 拼接成QueryString格式
|
||||||
|
$queryString = self::buildQueryString($params);
|
||||||
|
|
||||||
|
// 5. 根据签名类型生成签名
|
||||||
|
switch (strtoupper($signType)) {
|
||||||
|
case self::SIGN_TYPE_MD5:
|
||||||
|
return self::generateMd5Sign($queryString, $secretKey);
|
||||||
|
case self::SIGN_TYPE_RSA_1_256:
|
||||||
|
return self::generateRsa256Sign($queryString, $secretKey);
|
||||||
|
case self::SIGN_TYPE_RSA_1_1:
|
||||||
|
return self::generateRsa1Sign($queryString, $secretKey);
|
||||||
|
default:
|
||||||
|
throw new \InvalidArgumentException('不支持的签名类型: ' . $signType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证支付签名
|
||||||
|
*
|
||||||
|
* @param array $params 待验证参数(包含sign字段)
|
||||||
|
* @param string $secretKey 签名密钥
|
||||||
|
* @param string $signType 签名类型
|
||||||
|
* @return bool 验证结果
|
||||||
|
*/
|
||||||
|
public static function verifySign(array $params, string $secretKey, string $signType = self::SIGN_TYPE_MD5): bool
|
||||||
|
{
|
||||||
|
if (!isset($params['sign'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$receivedSign = $params['sign'];
|
||||||
|
$generatedSign = self::generateSign($params, $secretKey, $signType);
|
||||||
|
|
||||||
|
return $receivedSign === $generatedSign;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建QueryString
|
||||||
|
*
|
||||||
|
* @param array $params 参数数组
|
||||||
|
* @return string QueryString
|
||||||
|
*/
|
||||||
|
private static function buildQueryString(array $params): string
|
||||||
|
{
|
||||||
|
$pairs = [];
|
||||||
|
foreach ($params as $key => $value) {
|
||||||
|
$pairs[] = $key . '=' . $value;
|
||||||
|
}
|
||||||
|
return implode('&', $pairs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成MD5签名
|
||||||
|
*
|
||||||
|
* @param string $queryString 待签名字符串
|
||||||
|
* @param string $secretKey 密钥
|
||||||
|
* @return string MD5签名
|
||||||
|
*/
|
||||||
|
private static function generateMd5Sign(string $queryString, string $secretKey): string
|
||||||
|
{
|
||||||
|
$signString = $queryString . '&key=' . $secretKey;
|
||||||
|
return strtoupper(md5($signString));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成RSA256签名
|
||||||
|
*
|
||||||
|
* @param string $queryString 待签名字符串
|
||||||
|
* @param string $privateKey 私钥
|
||||||
|
* @return string RSA256签名
|
||||||
|
*/
|
||||||
|
private static function generateRsa256Sign(string $queryString, string $privateKey): string
|
||||||
|
{
|
||||||
|
$privateKey = self::formatPrivateKey($privateKey);
|
||||||
|
$key = openssl_pkey_get_private($privateKey);
|
||||||
|
if (!$key) {
|
||||||
|
throw new \Exception('RSA私钥格式错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
$signature = '';
|
||||||
|
$result = openssl_sign($queryString, $signature, $key, OPENSSL_ALGO_SHA256);
|
||||||
|
openssl_pkey_free($key);
|
||||||
|
|
||||||
|
if (!$result) {
|
||||||
|
throw new \Exception('RSA256签名失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64_encode($signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成RSA1签名
|
||||||
|
*
|
||||||
|
* @param string $queryString 待签名字符串
|
||||||
|
* @param string $privateKey 私钥
|
||||||
|
* @return string RSA1签名
|
||||||
|
*/
|
||||||
|
private static function generateRsa1Sign(string $queryString, string $privateKey): string
|
||||||
|
{
|
||||||
|
$privateKey = self::formatPrivateKey($privateKey);
|
||||||
|
$key = openssl_pkey_get_private($privateKey);
|
||||||
|
if (!$key) {
|
||||||
|
throw new \Exception('RSA私钥格式错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
$signature = '';
|
||||||
|
$result = openssl_sign($queryString, $signature, $key, OPENSSL_ALGO_SHA1);
|
||||||
|
openssl_pkey_free($key);
|
||||||
|
|
||||||
|
if (!$result) {
|
||||||
|
throw new \Exception('RSA1签名失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64_encode($signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化私钥
|
||||||
|
*
|
||||||
|
* @param string $privateKey 原始私钥
|
||||||
|
* @return string 格式化后的私钥
|
||||||
|
*/
|
||||||
|
private static function formatPrivateKey(string $privateKey): string
|
||||||
|
{
|
||||||
|
$privateKey = str_replace(['-----BEGIN PRIVATE KEY-----', '-----END PRIVATE KEY-----', "\n", "\r"], '', $privateKey);
|
||||||
|
$privateKey = chunk_split($privateKey, 64, "\n");
|
||||||
|
return "-----BEGIN PRIVATE KEY-----\n" . $privateKey . "-----END PRIVATE KEY-----";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化公钥
|
||||||
|
*
|
||||||
|
* @param string $publicKey 原始公钥
|
||||||
|
* @return string 格式化后的公钥
|
||||||
|
*/
|
||||||
|
private static function formatPublicKey(string $publicKey): string
|
||||||
|
{
|
||||||
|
$publicKey = str_replace(['-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----', "\n", "\r"], '', $publicKey);
|
||||||
|
$publicKey = chunk_split($publicKey, 64, "\n");
|
||||||
|
return "-----BEGIN PUBLIC KEY-----\n" . $publicKey . "-----END PUBLIC KEY-----";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证RSA签名
|
||||||
|
*
|
||||||
|
* @param string $queryString 原始字符串
|
||||||
|
* @param string $signature 签名
|
||||||
|
* @param string $publicKey 公钥
|
||||||
|
* @param string $signType 签名类型
|
||||||
|
* @return bool 验证结果
|
||||||
|
*/
|
||||||
|
public static function verifyRsaSign(string $queryString, string $signature, string $publicKey, string $signType = self::SIGN_TYPE_RSA_1_256): bool
|
||||||
|
{
|
||||||
|
$publicKey = self::formatPublicKey($publicKey);
|
||||||
|
$key = openssl_pkey_get_public($publicKey);
|
||||||
|
if (!$key) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$algorithm = $signType === self::SIGN_TYPE_RSA_1_1 ? OPENSSL_ALGO_SHA1 : OPENSSL_ALGO_SHA256;
|
||||||
|
$result = openssl_verify($queryString, base64_decode($signature), $key, $algorithm);
|
||||||
|
openssl_pkey_free($key);
|
||||||
|
|
||||||
|
return $result === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机字符串
|
||||||
|
*
|
||||||
|
* @param int $length 长度
|
||||||
|
* @return string 随机字符串
|
||||||
|
*/
|
||||||
|
public static function generateNonceStr(int $length = 32): string
|
||||||
|
{
|
||||||
|
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
$str = '';
|
||||||
|
for ($i = 0; $i < $length; $i++) {
|
||||||
|
$str .= $chars[mt_rand(0, strlen($chars) - 1)];
|
||||||
|
}
|
||||||
|
return $str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成时间戳
|
||||||
|
*
|
||||||
|
* @return int 时间戳
|
||||||
|
*/
|
||||||
|
public static function generateTimestamp(): int
|
||||||
|
{
|
||||||
|
return time();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化金额(分转元)
|
||||||
|
*
|
||||||
|
* @param int $amount 金额(分)
|
||||||
|
* @return string 格式化后的金额(元)
|
||||||
|
*/
|
||||||
|
public static function formatAmount(int $amount): string
|
||||||
|
{
|
||||||
|
return number_format($amount / 100, 2, '.', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析金额(元转分)
|
||||||
|
*
|
||||||
|
* @param string $amount 金额(元)
|
||||||
|
* @return int 金额(分)
|
||||||
|
*/
|
||||||
|
public static function parseAmount(string $amount): int
|
||||||
|
{
|
||||||
|
return (int) round(floatval($amount) * 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
135
Server/application/common/util/Signer.php
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common\util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 第三方支付签名工具(仅内部调用)
|
||||||
|
* 规则:
|
||||||
|
* 1. 除 sign 外的所有非空参数,按字段名 ASCII 升序,使用 QueryString 形式拼接(key1=value1&key2=value2)
|
||||||
|
* 2. 参与签名的字段名与值均为原始值,不做 URL Encode
|
||||||
|
* 3. 支持算法:MD5(默认)/ RSA_1_256 / RSA_1_1
|
||||||
|
*/
|
||||||
|
class Signer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 生成签名
|
||||||
|
*
|
||||||
|
* @param array $params 参与签名的参数(会自动剔除 sign 及空值)
|
||||||
|
* @param string $algorithm 签名算法:md5 | RSA_1_256 | RSA_1_1
|
||||||
|
* @param array $options 额外选项:
|
||||||
|
* - secret: string MD5 签名时可选的密钥,若提供则会在原串末尾以 &key=SECRET 追加
|
||||||
|
* - private_key: string RSA 签名所需私钥(PEM 字符串,支持带头尾)
|
||||||
|
* - passphrase: string 可选,RSA 私钥口令
|
||||||
|
* @return string 返回签名串(MD5 为32位小写;RSA为base64编码)
|
||||||
|
* @throws \InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public static function sign(array $params, $algorithm = 'md5', array $options = [])
|
||||||
|
{
|
||||||
|
$signString = self::buildSignString($params);
|
||||||
|
|
||||||
|
$algo = strtolower($algorithm);
|
||||||
|
switch ($algo) {
|
||||||
|
case 'md5':
|
||||||
|
return self::signMd5($signString, isset($options['secret']) ? (string)$options['secret'] : null);
|
||||||
|
case 'rsa_1_256':
|
||||||
|
return self::signRsa($signString, $options, 'sha256');
|
||||||
|
case 'rsa_1_1':
|
||||||
|
return self::signRsa($signString, $options, 'sha1');
|
||||||
|
default:
|
||||||
|
throw new \InvalidArgumentException('Unsupported algorithm: ' . $algorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建签名原始串
|
||||||
|
* - 剔除 sign 字段
|
||||||
|
* - 过滤空值(null、'')
|
||||||
|
* - 按键名 ASCII 升序
|
||||||
|
* - 使用原始值拼接为 key1=value1&key2=value2
|
||||||
|
*
|
||||||
|
* @param array $params
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function buildSignString(array $params)
|
||||||
|
{
|
||||||
|
$filtered = [];
|
||||||
|
foreach ($params as $key => $value) {
|
||||||
|
if ($key === 'sign') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($value === '' || $value === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$filtered[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
ksort($filtered, SORT_STRING);
|
||||||
|
|
||||||
|
$pairs = [];
|
||||||
|
foreach ($filtered as $key => $value) {
|
||||||
|
// 原始值拼接,不做 urlencode
|
||||||
|
$pairs[] = $key . '=' . (is_bool($value) ? ($value ? '1' : '0') : (string)$value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode('&', $pairs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MD5 签名
|
||||||
|
* - 若提供 secret,则原串末尾追加 &key=SECRET
|
||||||
|
* - 返回 32 位小写
|
||||||
|
*
|
||||||
|
* @param string $signString
|
||||||
|
* @param string|null $secret
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected static function signMd5($signString, $secret = null)
|
||||||
|
{
|
||||||
|
if ($secret !== null && $secret !== '') {
|
||||||
|
$signString .= '&key=' . $secret;
|
||||||
|
}
|
||||||
|
return strtolower(md5($signString));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RSA 签名
|
||||||
|
*
|
||||||
|
* @param string $signString
|
||||||
|
* @param array $options 必填:private_key,可选:passphrase
|
||||||
|
* @param string $hashAlgo sha256|sha1
|
||||||
|
* @return string base64 签名
|
||||||
|
* @throws \InvalidArgumentException
|
||||||
|
*/
|
||||||
|
protected static function signRsa($signString, array $options, $hashAlgo = 'sha256')
|
||||||
|
{
|
||||||
|
if (empty($options['private_key'])) {
|
||||||
|
throw new \InvalidArgumentException('RSA signing requires private_key.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$privateKey = $options['private_key'];
|
||||||
|
$passphrase = isset($options['passphrase']) ? (string)$options['passphrase'] : '';
|
||||||
|
|
||||||
|
// 兼容无头尾私钥,自动包裹为 PEM
|
||||||
|
if (strpos($privateKey, 'BEGIN') === false) {
|
||||||
|
$privateKey = "-----BEGIN PRIVATE KEY-----\n" . trim(chunk_split(str_replace(["\r", "\n"], '', $privateKey), 64, "\n")) . "\n-----END PRIVATE KEY-----";
|
||||||
|
}
|
||||||
|
|
||||||
|
$pkeyId = openssl_pkey_get_private($privateKey, $passphrase);
|
||||||
|
if ($pkeyId === false) {
|
||||||
|
throw new \InvalidArgumentException('Invalid RSA private key or passphrase.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$signature = '';
|
||||||
|
$algoConst = $hashAlgo === 'sha1' ? OPENSSL_ALGO_SHA1 : OPENSSL_ALGO_SHA256;
|
||||||
|
$ok = openssl_sign($signString, $signature, $pkeyId, $algoConst);
|
||||||
|
openssl_free_key($pkeyId);
|
||||||
|
|
||||||
|
if (!$ok) {
|
||||||
|
throw new \InvalidArgumentException('OpenSSL sign failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64_encode($signature);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
27
Server/application/cunkebao/controller/Pay.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\cunkebao\controller;
|
||||||
|
|
||||||
|
|
||||||
|
use app\common\controller\PaymentService;
|
||||||
|
|
||||||
|
class Pay
|
||||||
|
{
|
||||||
|
|
||||||
|
public function createOrder()
|
||||||
|
{
|
||||||
|
$order = [
|
||||||
|
'companyId' => 111,
|
||||||
|
'userId' => 111,
|
||||||
|
'orderNo' => date('YmdHis') . rand(100000, 999999),
|
||||||
|
'goodsId' => 34,
|
||||||
|
'goodsName' => '测试测试',
|
||||||
|
'orderType' => 1,
|
||||||
|
'money' => 1
|
||||||
|
];
|
||||||
|
|
||||||
|
$paymentService = new PaymentService();
|
||||||
|
$res = $paymentService->createOrder($order);
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -149,7 +149,7 @@ class WorkbenchController extends Controller
|
|||||||
$config->startTime = $param['startTime'];
|
$config->startTime = $param['startTime'];
|
||||||
$config->endTime = $param['endTime'];
|
$config->endTime = $param['endTime'];
|
||||||
$config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE);
|
$config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE);
|
||||||
$config->pools = json_encode($param['pools'], JSON_UNESCAPED_UNICODE);
|
$config->pools = json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE);
|
||||||
$config->account = json_encode($param['accountGroups'], JSON_UNESCAPED_UNICODE);
|
$config->account = json_encode($param['accountGroups'], JSON_UNESCAPED_UNICODE);
|
||||||
$config->createTime = time();
|
$config->createTime = time();
|
||||||
$config->updateTime = time();
|
$config->updateTime = time();
|
||||||
@@ -159,7 +159,7 @@ class WorkbenchController extends Controller
|
|||||||
$config = new WorkbenchImportContact;
|
$config = new WorkbenchImportContact;
|
||||||
$config->workbenchId = $workbench->id;
|
$config->workbenchId = $workbench->id;
|
||||||
$config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE);
|
$config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE);
|
||||||
$config->pools = json_encode($param['pools'], JSON_UNESCAPED_UNICODE);
|
$config->pools = json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE);
|
||||||
$config->num = $param['num'];
|
$config->num = $param['num'];
|
||||||
$config->clearContact = $param['clearContact'];
|
$config->clearContact = $param['clearContact'];
|
||||||
$config->remark = $param['remark'];
|
$config->remark = $param['remark'];
|
||||||
@@ -314,13 +314,13 @@ class WorkbenchController extends Controller
|
|||||||
if (!empty($item->trafficConfig)) {
|
if (!empty($item->trafficConfig)) {
|
||||||
$item->config = $item->trafficConfig;
|
$item->config = $item->trafficConfig;
|
||||||
$item->config->devices = json_decode($item->config->devices, true);
|
$item->config->devices = json_decode($item->config->devices, true);
|
||||||
$item->config->pools = json_decode($item->config->pools, true);
|
$item->config->poolGroups = json_decode($item->config->pools, true);
|
||||||
$item->config->account = json_decode($item->config->account, true);
|
$item->config->account = json_decode($item->config->account, true);
|
||||||
$config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $item->id])->order('id DESC')->find();
|
$config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $item->id])->order('id DESC')->find();
|
||||||
$item->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i', $config_item['createTime']) : '--';
|
$item->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i', $config_item['createTime']) : '--';
|
||||||
|
|
||||||
//统计
|
//统计
|
||||||
$labels = $item->config->pools;
|
$labels = $item->config->poolGroups;
|
||||||
$totalUsers = Db::table('s2_wechat_friend')->alias('wf')
|
$totalUsers = Db::table('s2_wechat_friend')->alias('wf')
|
||||||
->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left')
|
->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left')
|
||||||
->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
|
->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
|
||||||
@@ -352,7 +352,7 @@ class WorkbenchController extends Controller
|
|||||||
'dailyAverage' => intval($dailyAverage),
|
'dailyAverage' => intval($dailyAverage),
|
||||||
'totalAccounts' => $totalAccounts,
|
'totalAccounts' => $totalAccounts,
|
||||||
'deviceCount' => count($item->config->devices),
|
'deviceCount' => count($item->config->devices),
|
||||||
'poolCount' => !empty($item->config->pools) ? count($item->config->pools) : 'ALL',
|
'poolCount' => !empty($item->config->poolGroups) ? count($item->config->poolGroups) : 'ALL',
|
||||||
'totalUsers' => $totalUsers >> 0
|
'totalUsers' => $totalUsers >> 0
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -363,7 +363,7 @@ class WorkbenchController extends Controller
|
|||||||
if (!empty($item->importContact)) {
|
if (!empty($item->importContact)) {
|
||||||
$item->config = $item->importContact;
|
$item->config = $item->importContact;
|
||||||
$item->config->devices = json_decode($item->config->devices, true);
|
$item->config->devices = json_decode($item->config->devices, true);
|
||||||
$item->config->pools = json_decode($item->config->pools, true);
|
$item->config->poolGroups = json_decode($item->config->pools, true);
|
||||||
}
|
}
|
||||||
unset($item->importContact, $item->import_contact);
|
unset($item->importContact, $item->import_contact);
|
||||||
break;
|
break;
|
||||||
@@ -505,12 +505,12 @@ class WorkbenchController extends Controller
|
|||||||
$workbench->config = $workbench->trafficConfig;
|
$workbench->config = $workbench->trafficConfig;
|
||||||
$workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
|
$workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
|
||||||
$workbench->config->accountGroups = json_decode($workbench->config->account, true);
|
$workbench->config->accountGroups = json_decode($workbench->config->account, true);
|
||||||
$workbench->config->pools = json_decode($workbench->config->pools, true);
|
$workbench->config->poolGroups = json_decode($workbench->config->pools, true);
|
||||||
$config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $workbench->id])->order('id DESC')->find();
|
$config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $workbench->id])->order('id DESC')->find();
|
||||||
$workbench->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i', $config_item['createTime']) : '--';
|
$workbench->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i', $config_item['createTime']) : '--';
|
||||||
|
|
||||||
//统计
|
//统计
|
||||||
$labels = $workbench->config->pools;
|
$labels = $workbench->config->poolGroups;
|
||||||
$totalUsers = Db::table('s2_wechat_friend')->alias('wf')
|
$totalUsers = Db::table('s2_wechat_friend')->alias('wf')
|
||||||
->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left')
|
->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left')
|
||||||
->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
|
->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
|
||||||
@@ -549,7 +549,7 @@ class WorkbenchController extends Controller
|
|||||||
'dailyAverage' => intval($dailyAverage),
|
'dailyAverage' => intval($dailyAverage),
|
||||||
'totalAccounts' => $totalAccounts,
|
'totalAccounts' => $totalAccounts,
|
||||||
'deviceCount' => count($workbench->config->deviceGroups),
|
'deviceCount' => count($workbench->config->deviceGroups),
|
||||||
'poolCount' => count($workbench->config->pools),
|
'poolCount' => count($workbench->config->poolGroups),
|
||||||
'totalUsers' => $totalUsers >> 0
|
'totalUsers' => $totalUsers >> 0
|
||||||
];
|
];
|
||||||
unset($workbench->trafficConfig, $workbench->traffic_config);
|
unset($workbench->trafficConfig, $workbench->traffic_config);
|
||||||
@@ -559,7 +559,7 @@ class WorkbenchController extends Controller
|
|||||||
if (!empty($workbench->importContact)) {
|
if (!empty($workbench->importContact)) {
|
||||||
$workbench->config = $workbench->importContact;
|
$workbench->config = $workbench->importContact;
|
||||||
$workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
|
$workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
|
||||||
$workbench->config->pools = json_decode($workbench->config->pools, true);
|
$workbench->config->poolGroups = json_decode($workbench->config->pools, true);
|
||||||
}
|
}
|
||||||
unset($workbench->importContact, $workbench->import_contact);
|
unset($workbench->importContact, $workbench->import_contact);
|
||||||
break;
|
break;
|
||||||
@@ -789,7 +789,7 @@ class WorkbenchController extends Controller
|
|||||||
$config->startTime = $param['startTime'];
|
$config->startTime = $param['startTime'];
|
||||||
$config->endTime = $param['endTime'];
|
$config->endTime = $param['endTime'];
|
||||||
$config->devices = json_encode($param['deviceGroups']);
|
$config->devices = json_encode($param['deviceGroups']);
|
||||||
$config->pools = json_encode($param['pools']);
|
$config->pools = json_encode($param['poolGroups']);
|
||||||
$config->account = json_encode($param['accountGroups']);
|
$config->account = json_encode($param['accountGroups']);
|
||||||
$config->updateTime = time();
|
$config->updateTime = time();
|
||||||
$config->save();
|
$config->save();
|
||||||
@@ -799,7 +799,7 @@ class WorkbenchController extends Controller
|
|||||||
$config = WorkbenchImportContact::where('workbenchId', $param['id'])->find();;
|
$config = WorkbenchImportContact::where('workbenchId', $param['id'])->find();;
|
||||||
if ($config) {
|
if ($config) {
|
||||||
$config->devices = json_encode($param['deviceGroups']);
|
$config->devices = json_encode($param['deviceGroups']);
|
||||||
$config->pools = json_encode($param['pools']);
|
$config->pools = json_encode($param['poolGroups']);
|
||||||
$config->num = $param['num'];
|
$config->num = $param['num'];
|
||||||
$config->clearContact = $param['clearContact'];
|
$config->clearContact = $param['clearContact'];
|
||||||
$config->remark = $param['remark'];
|
$config->remark = $param['remark'];
|
||||||
@@ -1450,7 +1450,7 @@ class WorkbenchController extends Controller
|
|||||||
'startTime' => $param['startTime'],
|
'startTime' => $param['startTime'],
|
||||||
'endTime' => $param['endTime'],
|
'endTime' => $param['endTime'],
|
||||||
'targets' => json_encode($param['targets'], JSON_UNESCAPED_UNICODE),
|
'targets' => json_encode($param['targets'], JSON_UNESCAPED_UNICODE),
|
||||||
'pools' => json_encode($param['pools'], JSON_UNESCAPED_UNICODE),
|
'pools' => json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE),
|
||||||
'createTime' => time(),
|
'createTime' => time(),
|
||||||
'updateTime' => time()
|
'updateTime' => time()
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ class GetAddFriendPlanDetailV1Controller extends Controller
|
|||||||
// 解析JSON字段
|
// 解析JSON字段
|
||||||
$sceneConf = json_decode($plan['sceneConf'], true) ?: [];
|
$sceneConf = json_decode($plan['sceneConf'], true) ?: [];
|
||||||
$reqConf = json_decode($plan['reqConf'], true) ?: [];
|
$reqConf = json_decode($plan['reqConf'], true) ?: [];
|
||||||
$reqConf['deveiceGroups'] = $reqConf['device'];
|
$reqConf['deviceGroups'] = $reqConf['device'];
|
||||||
$msgConf = json_decode($plan['msgConf'], true) ?: [];
|
$msgConf = json_decode($plan['msgConf'], true) ?: [];
|
||||||
$tagConf = json_decode($plan['tagConf'], true) ?: [];
|
$tagConf = json_decode($plan['tagConf'], true) ?: [];
|
||||||
|
|
||||||
@@ -139,8 +139,8 @@ class GetAddFriendPlanDetailV1Controller extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!empty($reqConf['deveiceGroups'])){
|
if (!empty($reqConf['deviceGroups'])){
|
||||||
$deveiceGroupsOptions = DeviceModel::alias('d')
|
$deviceGroupsOptions = DeviceModel::alias('d')
|
||||||
->field([
|
->field([
|
||||||
'd.id', 'd.imei', 'd.memo', 'd.alive',
|
'd.id', 'd.imei', 'd.memo', 'd.alive',
|
||||||
'l.wechatId',
|
'l.wechatId',
|
||||||
@@ -149,16 +149,16 @@ class GetAddFriendPlanDetailV1Controller extends Controller
|
|||||||
->leftJoin('device_wechat_login l', 'd.id = l.deviceId and l.alive =' . DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE . ' and l.companyId = d.companyId')
|
->leftJoin('device_wechat_login l', 'd.id = l.deviceId and l.alive =' . DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE . ' and l.companyId = d.companyId')
|
||||||
->leftJoin('wechat_account a', 'l.wechatId = a.wechatId')
|
->leftJoin('wechat_account a', 'l.wechatId = a.wechatId')
|
||||||
->order('d.id desc')
|
->order('d.id desc')
|
||||||
->whereIn('d.id',$reqConf['deveiceGroups'])
|
->whereIn('d.id',$reqConf['deviceGroups'])
|
||||||
->select();
|
->select();
|
||||||
foreach ($deveiceGroupsOptions as &$device) {
|
foreach ($deviceGroupsOptions as &$device) {
|
||||||
$curstomer = WechatCustomerModel::field('friendShip')->where(['wechatId' => $device['wechatId']])->find();
|
$curstomer = WechatCustomerModel::field('friendShip')->where(['wechatId' => $device['wechatId']])->find();
|
||||||
$device['totalFriend'] = $curstomer->friendShip->totalFriend ?? 0;
|
$device['totalFriend'] = $curstomer->friendShip->totalFriend ?? 0;
|
||||||
}
|
}
|
||||||
unset($device);
|
unset($device);
|
||||||
$reqConf['deveiceGroupsOptions'] = $deveiceGroupsOptions;
|
$reqConf['deviceGroupsOptions'] = $deviceGroupsOptions;
|
||||||
}else{
|
}else{
|
||||||
$reqConf['deveiceGroupsOptions'] = [];
|
$reqConf['deviceGroupsOptions'] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -56,11 +56,11 @@ class GetPotentialListWithInCompanyV1Controller extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($device)) {
|
if (!empty($device)) {
|
||||||
$where[] = ['d.deviceId', '=', $device];
|
// $where[] = ['d.deviceId', '=', $device];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($taskId)) {
|
if (!empty($taskId)) {
|
||||||
$where[] = ['t.sceneId', '=', $taskId];
|
//$where[] = ['t.sceneId', '=', $taskId];
|
||||||
}
|
}
|
||||||
$where[] = ['s.companyId', '=', $this->getUserInfo('companyId')];
|
$where[] = ['s.companyId', '=', $this->getUserInfo('companyId')];
|
||||||
|
|
||||||
|
|||||||
@@ -55,11 +55,9 @@ class Workbench extends Validate
|
|||||||
'distributeType' => 'requireIf:type,5|in:1,2',
|
'distributeType' => 'requireIf:type,5|in:1,2',
|
||||||
'maxPerDay' => 'requireIf:type,5|number|min:1',
|
'maxPerDay' => 'requireIf:type,5|number|min:1',
|
||||||
'timeType' => 'requireIf:type,5|in:1,2',
|
'timeType' => 'requireIf:type,5|in:1,2',
|
||||||
'startTime' => 'requireIf:type,5|dateFormat:H:i',
|
|
||||||
'endTime' => 'requireIf:type,5|dateFormat:H:i',
|
|
||||||
'accountGroups' => 'requireIf:type,5|array|min:1',
|
'accountGroups' => 'requireIf:type,5|array|min:1',
|
||||||
// 通用参数
|
// 通用参数
|
||||||
'deveiceGroups' => 'requireIf:type,1,2,5|array',
|
'deviceGroups' => 'requireIf:type,1,2,5|array',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -142,8 +140,8 @@ class Workbench extends Validate
|
|||||||
'timeType.requireIf' => '请选择时间类型',
|
'timeType.requireIf' => '请选择时间类型',
|
||||||
|
|
||||||
// 通用提示
|
// 通用提示
|
||||||
'deveiceGroups.require' => '请选择设备',
|
'deviceGroups.require' => '请选择设备',
|
||||||
'deveiceGroups.array' => '设备格式错误',
|
'deviceGroups.array' => '设备格式错误',
|
||||||
'targetGroups.require' => '请选择目标用户组',
|
'targetGroups.require' => '请选择目标用户组',
|
||||||
'targetGroups.array' => '目标用户组格式错误',
|
'targetGroups.array' => '目标用户组格式错误',
|
||||||
'accountGroups.requireIf' => '流量分发时必须选择分发账号',
|
'accountGroups.requireIf' => '流量分发时必须选择分发账号',
|
||||||
@@ -155,7 +153,7 @@ class Workbench extends Validate
|
|||||||
* 验证场景
|
* 验证场景
|
||||||
*/
|
*/
|
||||||
protected $scene = [
|
protected $scene = [
|
||||||
'create' => ['name', 'type', 'autoStart', 'deveiceGroups', 'targetGroups',
|
'create' => ['name', 'type', 'autoStart', 'deviceGroups', 'targetGroups',
|
||||||
'interval', 'maxLikes', 'startTime', 'endTime', 'contentTypes',
|
'interval', 'maxLikes', 'startTime', 'endTime', 'contentTypes',
|
||||||
'syncInterval', 'syncCount', 'syncType',
|
'syncInterval', 'syncCount', 'syncType',
|
||||||
'pushType', 'startTime', 'endTime', 'maxPerDay', 'pushOrder', 'isLoop', 'status', 'wechatGroups', 'contentGroups',
|
'pushType', 'startTime', 'endTime', 'maxPerDay', 'pushOrder', 'isLoop', 'status', 'wechatGroups', 'contentGroups',
|
||||||
@@ -163,7 +161,7 @@ class Workbench extends Validate
|
|||||||
'groupNameTemplate', 'maxGroupsPerDay', 'groupSizeMin', 'groupSizeMax',
|
'groupNameTemplate', 'maxGroupsPerDay', 'groupSizeMin', 'groupSizeMax',
|
||||||
],
|
],
|
||||||
'update_status' => ['id', 'status'],
|
'update_status' => ['id', 'status'],
|
||||||
'edit' => ['name', 'type', 'autoStart', 'deveiceGroups', 'targetGroups',
|
'edit' => ['name', 'type', 'autoStart', 'deviceGroups', 'targetGroups',
|
||||||
'interval', 'maxLikes', 'startTime', 'endTime', 'contentTypes',
|
'interval', 'maxLikes', 'startTime', 'endTime', 'contentTypes',
|
||||||
'syncInterval', 'syncCount', 'syncType',
|
'syncInterval', 'syncCount', 'syncType',
|
||||||
'pushType', 'startTime', 'endTime', 'maxPerDay', 'pushOrder', 'isLoop', 'status', 'wechatGroups', 'contentGroups',
|
'pushType', 'startTime', 'endTime', 'maxPerDay', 'pushOrder', 'isLoop', 'status', 'wechatGroups', 'contentGroups',
|
||||||
|
|||||||
121
Touchkebao/emoji-mapping.json
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
{
|
||||||
|
"face": {
|
||||||
|
"微笑": "smile",
|
||||||
|
"撇嘴": "pout",
|
||||||
|
"色": "lust",
|
||||||
|
"发呆": "daze",
|
||||||
|
"得意": "proud",
|
||||||
|
"流泪": "cry",
|
||||||
|
"害羞": "shy",
|
||||||
|
"闭嘴": "shut-up",
|
||||||
|
"睡": "sleep",
|
||||||
|
"大哭": "sob",
|
||||||
|
"尴尬": "awkward",
|
||||||
|
"发怒": "angry",
|
||||||
|
"调皮": "naughty",
|
||||||
|
"呲牙": "grin",
|
||||||
|
"惊讶": "surprised",
|
||||||
|
"难过": "sad",
|
||||||
|
"囧": "embarrassed",
|
||||||
|
"抓狂": "crazy",
|
||||||
|
"吐": "vomit",
|
||||||
|
"偷笑": "snicker",
|
||||||
|
"愉快": "happy",
|
||||||
|
"白眼": "roll-eyes",
|
||||||
|
"傲慢": "arrogant",
|
||||||
|
"困": "sleepy",
|
||||||
|
"惊恐": "panic",
|
||||||
|
"憨笑": "silly-smile",
|
||||||
|
"悠闲": "relaxed",
|
||||||
|
"咒骂": "curse",
|
||||||
|
"疑问": "question",
|
||||||
|
"嘘": "shush",
|
||||||
|
"晕": "dizzy",
|
||||||
|
"衰": "unlucky",
|
||||||
|
"骷髅": "skull",
|
||||||
|
"敲打": "knock",
|
||||||
|
"再见": "goodbye",
|
||||||
|
"擦汗": "wipe-sweat",
|
||||||
|
"抠鼻": "pick-nose",
|
||||||
|
"鼓掌": "clap",
|
||||||
|
"坏笑": "evil-smile",
|
||||||
|
"右哼哼": "right-hum",
|
||||||
|
"鄙视": "despise",
|
||||||
|
"委屈": "wronged",
|
||||||
|
"快哭了": "about-to-cry",
|
||||||
|
"阴险": "sinister",
|
||||||
|
"亲亲": "kiss",
|
||||||
|
"可怜": "pitiful",
|
||||||
|
"笑脸": "smiley",
|
||||||
|
"生病": "sick",
|
||||||
|
"脸红": "blush",
|
||||||
|
"破涕为笑": "smile-through-tears",
|
||||||
|
"恐惧": "fear",
|
||||||
|
"失望": "disappointed",
|
||||||
|
"无语": "speechless",
|
||||||
|
"嘿哈": "hey-ha",
|
||||||
|
"捂脸": "facepalm",
|
||||||
|
"机智": "smart",
|
||||||
|
"皱眉": "frown",
|
||||||
|
"耶": "yeah",
|
||||||
|
"吃瓜": "eat-melon",
|
||||||
|
"加油": "cheer-up",
|
||||||
|
"汗": "sweat",
|
||||||
|
"天啊": "oh-my-god",
|
||||||
|
"Emm": "emm",
|
||||||
|
"社会社会": "social",
|
||||||
|
"旺柴": "doge",
|
||||||
|
"好的": "ok",
|
||||||
|
"打脸": "slap-face",
|
||||||
|
"哇": "wow",
|
||||||
|
"翻白眼": "eye-roll",
|
||||||
|
"666": "666",
|
||||||
|
"让我看看": "let-me-see",
|
||||||
|
"叹气": "sigh",
|
||||||
|
"苦涩": "bitter",
|
||||||
|
"裂开": "crack",
|
||||||
|
"奸笑": "wicked-smile"
|
||||||
|
},
|
||||||
|
"gesture": {
|
||||||
|
"握手": "handshake",
|
||||||
|
"胜利": "victory",
|
||||||
|
"抱拳": "fist-salute",
|
||||||
|
"勾引": "beckon",
|
||||||
|
"拳头": "fist",
|
||||||
|
"OK": "ok",
|
||||||
|
"合十": "pray",
|
||||||
|
"强": "strong",
|
||||||
|
"拥抱": "hug",
|
||||||
|
"弱": "weak"
|
||||||
|
},
|
||||||
|
"animal": {
|
||||||
|
"猪头": "pig-head",
|
||||||
|
"跳跳": "jump",
|
||||||
|
"发抖": "shiver",
|
||||||
|
"转圈": "spin"
|
||||||
|
},
|
||||||
|
"blessing": {
|
||||||
|
"庆祝": "celebrate",
|
||||||
|
"礼物": "gift",
|
||||||
|
"红包": "red-envelope",
|
||||||
|
"發": "fortune",
|
||||||
|
"福": "blessing",
|
||||||
|
"烟花": "fireworks",
|
||||||
|
"爆竹": "firecrackers"
|
||||||
|
},
|
||||||
|
"other": {
|
||||||
|
"嘴唇": "lips",
|
||||||
|
"爱心": "heart",
|
||||||
|
"心碎": "broken-heart",
|
||||||
|
"啤酒": "beer",
|
||||||
|
"咖啡": "coffee",
|
||||||
|
"蛋糕": "cake",
|
||||||
|
"凋谢": "wither",
|
||||||
|
"菜刀": "knife",
|
||||||
|
"炸弹": "bomb",
|
||||||
|
"便便": "poop",
|
||||||
|
"太阳": "sun",
|
||||||
|
"月亮": "moon",
|
||||||
|
"玫瑰": "rose"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>存客宝</title>
|
<title>触客宝</title>
|
||||||
<style>
|
<style>
|
||||||
html {
|
html {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |