Merge branch 'yongpxu-dev' into yongxu-dev3
# Conflicts: # Touchkebao/src/pages/pc/ckbox/powerCenter/sop-send/index.module.scss resolved by yongpxu-dev version # Touchkebao/src/pages/pc/ckbox/powerCenter/sop-send/index.tsx resolved by yongpxu-dev version
1
.gitignore
vendored
@@ -9,3 +9,4 @@ Store_vue/.specstory/
|
||||
Store_vue/unpackage/
|
||||
Store_vue/.vscode/
|
||||
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: "微笑",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/微笑.png",
|
||||
path: "/assets/face/smile.png",
|
||||
},
|
||||
撇嘴: {
|
||||
name: "撇嘴",
|
||||
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: "发呆",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/发呆.png",
|
||||
path: "/assets/face/daze.png",
|
||||
},
|
||||
得意: {
|
||||
name: "得意",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/得意.png",
|
||||
path: "/assets/face/smug.png",
|
||||
},
|
||||
流泪: {
|
||||
name: "流泪",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/流泪.png",
|
||||
path: "/assets/face/crying.png",
|
||||
},
|
||||
害羞: {
|
||||
name: "害羞",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/害羞.png",
|
||||
path: "/assets/face/shy.png",
|
||||
},
|
||||
闭嘴: {
|
||||
name: "闭嘴",
|
||||
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: "大哭",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/大哭.png",
|
||||
path: "/assets/face/wail.png",
|
||||
},
|
||||
尴尬: {
|
||||
name: "尴尬",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/尴尬.png",
|
||||
path: "/assets/face/awkward.png",
|
||||
},
|
||||
发怒: {
|
||||
name: "发怒",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/发怒.png",
|
||||
path: "/assets/face/angry.png",
|
||||
},
|
||||
调皮: {
|
||||
name: "调皮",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/调皮.png",
|
||||
path: "/assets/face/naughty.png",
|
||||
},
|
||||
呲牙: {
|
||||
name: "呲牙",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/呲牙.png",
|
||||
path: "/assets/face/grin.png",
|
||||
},
|
||||
惊讶: {
|
||||
name: "惊讶",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/惊讶.png",
|
||||
path: "/assets/face/surprised.png",
|
||||
},
|
||||
难过: {
|
||||
name: "难过",
|
||||
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: "抓狂",
|
||||
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: "偷笑",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/偷笑.png",
|
||||
path: "/assets/face/snicker.png",
|
||||
},
|
||||
愉快: {
|
||||
name: "愉快",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/愉快.png",
|
||||
path: "/assets/face/happy.png",
|
||||
},
|
||||
白眼: {
|
||||
name: "白眼",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/白眼.png",
|
||||
path: "/assets/face/roll-eyes.png",
|
||||
},
|
||||
傲慢: {
|
||||
name: "傲慢",
|
||||
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: "惊恐",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/惊恐.png",
|
||||
path: "/assets/face/panic.png",
|
||||
},
|
||||
憨笑: {
|
||||
name: "憨笑",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/憨笑.png",
|
||||
path: "/assets/face/silly-smile.png",
|
||||
},
|
||||
悠闲: {
|
||||
name: "悠闲",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/悠闲.png",
|
||||
path: "/assets/face/leisurely.png",
|
||||
},
|
||||
咒骂: {
|
||||
name: "咒骂",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/咒骂.png",
|
||||
path: "/assets/face/curse.png",
|
||||
},
|
||||
疑问: {
|
||||
name: "疑问",
|
||||
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: "骷髅",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/骷髅.png",
|
||||
path: "/assets/face/skull.png",
|
||||
},
|
||||
敲打: {
|
||||
name: "敲打",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/敲打.png",
|
||||
path: "/assets/face/knock.png",
|
||||
},
|
||||
再见: {
|
||||
name: "再见",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/再见.png",
|
||||
path: "/assets/face/goodbye.png",
|
||||
},
|
||||
擦汗: {
|
||||
name: "擦汗",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/擦汗.png",
|
||||
path: "/assets/face/wipe-sweat.png",
|
||||
},
|
||||
抠鼻: {
|
||||
name: "抠鼻",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/抠鼻.png",
|
||||
path: "/assets/face/pick-nose.png",
|
||||
},
|
||||
鼓掌: {
|
||||
name: "鼓掌",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/鼓掌.png",
|
||||
path: "/assets/face/clap.png",
|
||||
},
|
||||
坏笑: {
|
||||
name: "坏笑",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/坏笑.png",
|
||||
path: "/assets/face/evil-smile.png",
|
||||
},
|
||||
右哼哼: {
|
||||
name: "右哼哼",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/右哼哼.png",
|
||||
path: "/assets/face/right-hum.png",
|
||||
},
|
||||
鄙视: {
|
||||
name: "鄙视",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/鄙视.png",
|
||||
path: "/assets/face/despise.png",
|
||||
},
|
||||
委屈: {
|
||||
name: "委屈",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/委屈.png",
|
||||
path: "/assets/face/wronged.png",
|
||||
},
|
||||
快哭了: {
|
||||
name: "快哭了",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/快哭了.png",
|
||||
path: "/assets/face/about-to-cry.png",
|
||||
},
|
||||
阴险: {
|
||||
name: "阴险",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/阴险.png",
|
||||
path: "/assets/face/sinister.png",
|
||||
},
|
||||
亲亲: {
|
||||
name: "亲亲",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/亲亲.png",
|
||||
path: "/assets/face/kiss.png",
|
||||
},
|
||||
可怜: {
|
||||
name: "可怜",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/可怜.png",
|
||||
path: "/assets/face/pitiful.png",
|
||||
},
|
||||
笑脸: {
|
||||
name: "笑脸",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/笑脸.png",
|
||||
path: "/assets/face/smiley.png",
|
||||
},
|
||||
生病: {
|
||||
name: "生病",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/生病.png",
|
||||
path: "/assets/face/sick.png",
|
||||
},
|
||||
脸红: {
|
||||
name: "脸红",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/脸红.png",
|
||||
path: "/assets/face/blush.png",
|
||||
},
|
||||
破涕为笑: {
|
||||
name: "破涕为笑",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/破涕为笑.png",
|
||||
path: "/assets/face/tears-to-smile.png",
|
||||
},
|
||||
恐惧: {
|
||||
name: "恐惧",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/恐惧.png",
|
||||
path: "/assets/face/fear.png",
|
||||
},
|
||||
失望: {
|
||||
name: "失望",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/失望.png",
|
||||
path: "/assets/face/disappointed.png",
|
||||
},
|
||||
无语: {
|
||||
name: "无语",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/无语.png",
|
||||
path: "/assets/face/speechless.png",
|
||||
},
|
||||
嘿哈: {
|
||||
name: "嘿哈",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/嘿哈.png",
|
||||
path: "/assets/face/hey-ha.png",
|
||||
},
|
||||
捂脸: {
|
||||
name: "捂脸",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/捂脸.png",
|
||||
path: "/assets/face/facepalm.png",
|
||||
},
|
||||
机智: {
|
||||
name: "机智",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/机智.png",
|
||||
path: "/assets/face/smart.png",
|
||||
},
|
||||
皱眉: {
|
||||
name: "皱眉",
|
||||
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: "吃瓜",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/吃瓜.png",
|
||||
path: "/assets/face/eat-melon.png",
|
||||
},
|
||||
加油: {
|
||||
name: "加油",
|
||||
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: "天啊",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/天啊.png",
|
||||
path: "/assets/face/oh-my.png",
|
||||
},
|
||||
Emm: {
|
||||
name: "Emm",
|
||||
@@ -437,28 +477,32 @@ const EMOJI_DATA: Record<EmojiName, EmojiInfo> = {
|
||||
社会社会: {
|
||||
name: "社会社会",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/社会社会.png",
|
||||
path: "/assets/face/social.png",
|
||||
},
|
||||
旺柴: {
|
||||
name: "旺柴",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/旺柴.png",
|
||||
path: "/assets/face/doge.png",
|
||||
},
|
||||
好的: {
|
||||
name: "好的",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/好的.png",
|
||||
path: "/assets/face/good.png",
|
||||
},
|
||||
打脸: {
|
||||
name: "打脸",
|
||||
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: "翻白眼",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/翻白眼.png",
|
||||
path: "/assets/face/eye-roll.png",
|
||||
},
|
||||
"666": {
|
||||
name: "666",
|
||||
@@ -468,54 +512,54 @@ const EMOJI_DATA: Record<EmojiName, EmojiInfo> = {
|
||||
让我看看: {
|
||||
name: "让我看看",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/让我看看.png",
|
||||
path: "/assets/face/let-me-see.png",
|
||||
},
|
||||
叹气: {
|
||||
name: "叹气",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/叹气.png",
|
||||
path: "/assets/face/sigh.png",
|
||||
},
|
||||
苦涩: {
|
||||
name: "苦涩",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/苦涩.png",
|
||||
path: "/assets/face/bitter.png",
|
||||
},
|
||||
裂开: {
|
||||
name: "裂开",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/裂开.png",
|
||||
path: "/assets/face/crack.png",
|
||||
},
|
||||
奸笑: {
|
||||
name: "奸笑",
|
||||
category: EmojiCategory.FACE,
|
||||
path: "/assets/face/奸笑.png",
|
||||
path: "/assets/face/sly-smile.png",
|
||||
},
|
||||
|
||||
// 手势表情
|
||||
握手: {
|
||||
name: "握手",
|
||||
category: EmojiCategory.GESTURE,
|
||||
path: "/assets/gesture/握手.png",
|
||||
path: "/assets/gesture/handshake.png",
|
||||
},
|
||||
胜利: {
|
||||
name: "胜利",
|
||||
category: EmojiCategory.GESTURE,
|
||||
path: "/assets/gesture/胜利.png",
|
||||
path: "/assets/gesture/victory.png",
|
||||
},
|
||||
抱拳: {
|
||||
name: "抱拳",
|
||||
category: EmojiCategory.GESTURE,
|
||||
path: "/assets/gesture/抱拳.png",
|
||||
path: "/assets/gesture/fist-salute.png",
|
||||
},
|
||||
勾引: {
|
||||
name: "勾引",
|
||||
category: EmojiCategory.GESTURE,
|
||||
path: "/assets/gesture/勾引.png",
|
||||
path: "/assets/gesture/beckon.png",
|
||||
},
|
||||
拳头: {
|
||||
name: "拳头",
|
||||
category: EmojiCategory.GESTURE,
|
||||
path: "/assets/gesture/拳头.png",
|
||||
path: "/assets/gesture/fist.png",
|
||||
},
|
||||
OK: {
|
||||
name: "OK",
|
||||
@@ -525,148 +569,148 @@ const EMOJI_DATA: Record<EmojiName, EmojiInfo> = {
|
||||
合十: {
|
||||
name: "合十",
|
||||
category: EmojiCategory.GESTURE,
|
||||
path: "/assets/gesture/合十.png",
|
||||
path: "/assets/gesture/pray.png",
|
||||
},
|
||||
强: {
|
||||
name: "强",
|
||||
category: EmojiCategory.GESTURE,
|
||||
path: "/assets/gesture/强.png",
|
||||
path: "/assets/gesture/strong.png",
|
||||
},
|
||||
拥抱: {
|
||||
name: "拥抱",
|
||||
category: EmojiCategory.GESTURE,
|
||||
path: "/assets/gesture/拥抱.png",
|
||||
path: "/assets/gesture/hug.png",
|
||||
},
|
||||
弱: {
|
||||
name: "弱",
|
||||
category: EmojiCategory.GESTURE,
|
||||
path: "/assets/gesture/弱.png",
|
||||
path: "/assets/gesture/weak.png",
|
||||
},
|
||||
|
||||
// 动物表情
|
||||
猪头: {
|
||||
name: "猪头",
|
||||
category: EmojiCategory.ANIMAL,
|
||||
path: "/assets/animal/猪头.png",
|
||||
path: "/assets/animal/pig.png",
|
||||
},
|
||||
跳跳: {
|
||||
name: "跳跳",
|
||||
category: EmojiCategory.ANIMAL,
|
||||
path: "/assets/animal/跳跳.png",
|
||||
path: "/assets/animal/jump.png",
|
||||
},
|
||||
发抖: {
|
||||
name: "发抖",
|
||||
category: EmojiCategory.ANIMAL,
|
||||
path: "/assets/animal/发抖.png",
|
||||
path: "/assets/animal/tremble.png",
|
||||
},
|
||||
转圈: {
|
||||
name: "转圈",
|
||||
category: EmojiCategory.ANIMAL,
|
||||
path: "/assets/animal/转圈.png",
|
||||
path: "/assets/animal/circle.png",
|
||||
},
|
||||
|
||||
// 祝福表情
|
||||
庆祝: {
|
||||
name: "庆祝",
|
||||
category: EmojiCategory.BLESSING,
|
||||
path: "/assets/blessing/庆祝.png",
|
||||
path: "/assets/blessing/celebrate.png",
|
||||
},
|
||||
礼物: {
|
||||
name: "礼物",
|
||||
category: EmojiCategory.BLESSING,
|
||||
path: "/assets/blessing/礼物.png",
|
||||
path: "/assets/blessing/gift.png",
|
||||
},
|
||||
红包: {
|
||||
name: "红包",
|
||||
category: EmojiCategory.BLESSING,
|
||||
path: "/assets/blessing/红包.png",
|
||||
path: "/assets/blessing/red-envelope.png",
|
||||
},
|
||||
發: {
|
||||
name: "發",
|
||||
category: EmojiCategory.BLESSING,
|
||||
path: "/assets/blessing/發.png",
|
||||
path: "/assets/blessing/get-rich.png",
|
||||
},
|
||||
福: {
|
||||
name: "福",
|
||||
category: EmojiCategory.BLESSING,
|
||||
path: "/assets/blessing/福.png",
|
||||
path: "/assets/blessing/fortune.png",
|
||||
},
|
||||
烟花: {
|
||||
name: "烟花",
|
||||
category: EmojiCategory.BLESSING,
|
||||
path: "/assets/blessing/烟花.png",
|
||||
path: "/assets/blessing/fireworks.png",
|
||||
},
|
||||
爆竹: {
|
||||
name: "爆竹",
|
||||
category: EmojiCategory.BLESSING,
|
||||
path: "/assets/blessing/爆竹.png",
|
||||
path: "/assets/blessing/firecrackers.png",
|
||||
},
|
||||
|
||||
// 其他表情
|
||||
嘴唇: {
|
||||
name: "嘴唇",
|
||||
category: EmojiCategory.OTHER,
|
||||
path: "/assets/other/嘴唇.png",
|
||||
path: "/assets/other/lips.png",
|
||||
},
|
||||
爱心: {
|
||||
name: "爱心",
|
||||
category: EmojiCategory.OTHER,
|
||||
path: "/assets/other/爱心.png",
|
||||
path: "/assets/other/heart.png",
|
||||
},
|
||||
心碎: {
|
||||
name: "心碎",
|
||||
category: EmojiCategory.OTHER,
|
||||
path: "/assets/other/心碎.png",
|
||||
path: "/assets/other/broken-heart.png",
|
||||
},
|
||||
啤酒: {
|
||||
name: "啤酒",
|
||||
category: EmojiCategory.OTHER,
|
||||
path: "/assets/other/啤酒.png",
|
||||
path: "/assets/other/beer.png",
|
||||
},
|
||||
咖啡: {
|
||||
name: "咖啡",
|
||||
category: EmojiCategory.OTHER,
|
||||
path: "/assets/other/咖啡.png",
|
||||
path: "/assets/other/coffee.png",
|
||||
},
|
||||
蛋糕: {
|
||||
name: "蛋糕",
|
||||
category: EmojiCategory.OTHER,
|
||||
path: "/assets/other/蛋糕.png",
|
||||
path: "/assets/other/cake.png",
|
||||
},
|
||||
凋谢: {
|
||||
name: "凋谢",
|
||||
category: EmojiCategory.OTHER,
|
||||
path: "/assets/other/凋谢.png",
|
||||
path: "/assets/other/wither.png",
|
||||
},
|
||||
菜刀: {
|
||||
name: "菜刀",
|
||||
category: EmojiCategory.OTHER,
|
||||
path: "/assets/other/菜刀.png",
|
||||
path: "/assets/other/knife.png",
|
||||
},
|
||||
炸弹: {
|
||||
name: "炸弹",
|
||||
category: EmojiCategory.OTHER,
|
||||
path: "/assets/other/炸弹.png",
|
||||
path: "/assets/other/bomb.png",
|
||||
},
|
||||
便便: {
|
||||
name: "便便",
|
||||
category: EmojiCategory.OTHER,
|
||||
path: "/assets/other/便便.png",
|
||||
path: "/assets/other/poop.png",
|
||||
},
|
||||
太阳: {
|
||||
name: "太阳",
|
||||
category: EmojiCategory.OTHER,
|
||||
path: "/assets/other/太阳.png",
|
||||
path: "/assets/other/sun.png",
|
||||
},
|
||||
月亮: {
|
||||
name: "月亮",
|
||||
category: EmojiCategory.OTHER,
|
||||
path: "/assets/other/月亮.png",
|
||||
path: "/assets/other/moon.png",
|
||||
},
|
||||
玫瑰: {
|
||||
name: "玫瑰",
|
||||
category: EmojiCategory.OTHER,
|
||||
path: "/assets/other/玫瑰.png",
|
||||
path: "/assets/other/rose.png",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -784,12 +784,6 @@ const TrafficPoolDetail: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 添加新标签按钮 */}
|
||||
<Button block color="primary" className={styles.addTagBtn}>
|
||||
<TagOutlined />
|
||||
添加新标签
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,57 +1,94 @@
|
||||
import React from "react";
|
||||
import { Popup, Selector } from "antd-mobile";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Popup, Selector, Button } from "antd-mobile";
|
||||
import { fetchPackageOptions } from "./api";
|
||||
import type { PackageOption } from "./data";
|
||||
|
||||
interface BatchAddModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
packageOptions: PackageOption[];
|
||||
batchTarget: string;
|
||||
setBatchTarget: (v: string) => void;
|
||||
selectedCount: number;
|
||||
onConfirm: () => void;
|
||||
onConfirm: (data: {
|
||||
packageOptions: PackageOption[];
|
||||
selectedPackageId: string;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
const BatchAddModal: React.FC<BatchAddModalProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
packageOptions = [],
|
||||
batchTarget,
|
||||
setBatchTarget,
|
||||
selectedCount,
|
||||
onConfirm,
|
||||
}) => (
|
||||
// <Modal visible={visible} title="批量加入分组" onConfirm={onConfirm}>
|
||||
// <div style={{ marginBottom: 12 }}>
|
||||
// <div>选择目标分组</div>
|
||||
// <Selector
|
||||
// options={packageOptions.map(p => ({ label: p.name, value: p.id }))}
|
||||
// value={[batchTarget]}
|
||||
// onChange={v => setBatchTarget(v[0])}
|
||||
// />
|
||||
// </div>
|
||||
// <div style={{ color: "#888", fontSize: 13 }}>
|
||||
// 将选中的{selectedCount}个用户加入所选分组
|
||||
// </div>
|
||||
// </Modal>
|
||||
<Popup
|
||||
visible={visible}
|
||||
onMaskClick={() => onClose()}
|
||||
position="bottom"
|
||||
bodyStyle={{ height: "80vh" }}
|
||||
>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div>选择目标分组</div>
|
||||
<Selector
|
||||
options={packageOptions.map(p => ({ label: p.name, value: p.id }))}
|
||||
value={[batchTarget]}
|
||||
onChange={v => setBatchTarget(v[0])}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ color: "#888", fontSize: 13 }}>
|
||||
将选中的{selectedCount}个用户加入所选分组
|
||||
</div>
|
||||
</Popup>
|
||||
);
|
||||
}) => {
|
||||
const [packageOptions, setPackageOptions] = useState<PackageOption[]>([]);
|
||||
const [selectedPackageId, setSelectedPackageId] = useState<string>("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 获取分组选项
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setLoading(true);
|
||||
fetchPackageOptions()
|
||||
.then(res => {
|
||||
setPackageOptions(res.list || []);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("获取分组选项失败:", error);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!selectedPackageId) {
|
||||
// 可以添加提示
|
||||
return;
|
||||
}
|
||||
onConfirm({
|
||||
packageOptions,
|
||||
selectedPackageId,
|
||||
});
|
||||
};
|
||||
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;
|
||||
|
||||
@@ -1,24 +1,77 @@
|
||||
import React from "react";
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { Card, Button } from "antd-mobile";
|
||||
import { fetchTrafficPoolList } from "./api";
|
||||
import type { TrafficPoolUser } from "./data";
|
||||
|
||||
interface DataAnalysisPanelProps {
|
||||
stats: {
|
||||
showStats: boolean;
|
||||
setShowStats: (v: boolean) => void;
|
||||
onConfirm: (stats: {
|
||||
total: number;
|
||||
highValue: number;
|
||||
added: number;
|
||||
pending: number;
|
||||
failed: number;
|
||||
addSuccessRate: number;
|
||||
};
|
||||
showStats: boolean;
|
||||
setShowStats: (v: boolean) => void;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
const DataAnalysisPanel: React.FC<DataAnalysisPanelProps> = ({
|
||||
stats,
|
||||
showStats,
|
||||
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;
|
||||
return (
|
||||
<div
|
||||
@@ -30,46 +83,54 @@ const DataAnalysisPanel: React.FC<DataAnalysisPanelProps> = ({
|
||||
boxShadow: "0 2px 8px rgba(0,0,0,0.04)",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", gap: 16, marginBottom: 12 }}>
|
||||
<Card style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 18, fontWeight: 600, color: "#1677ff" }}>
|
||||
{stats.total}
|
||||
{loading ? (
|
||||
<div style={{ textAlign: "center", padding: 20 }}>
|
||||
加载统计数据中...
|
||||
</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 style={{ fontSize: 13, color: "#888" }}>总用户数</div>
|
||||
</Card>
|
||||
<Card style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 18, fontWeight: 600, color: "#eb2f96" }}>
|
||||
{stats.highValue}
|
||||
<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>
|
||||
<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
|
||||
size="small"
|
||||
style={{ marginTop: 12 }}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
|
||||
import { Popup } from "antd-mobile";
|
||||
import { Select, Button } from "antd";
|
||||
import DeviceSelection from "@/components/DeviceSelection";
|
||||
import type { UserStatus, ScenarioOption } from "./data";
|
||||
import type { ScenarioOption } from "./data";
|
||||
import { fetchScenarioOptions, fetchPackageOptions } from "./api";
|
||||
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
||||
|
||||
@@ -10,13 +10,21 @@ interface FilterModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: (filters: {
|
||||
deviceIds: string[];
|
||||
packageId: string;
|
||||
scenarioId: string;
|
||||
selectedDevices: DeviceSelectionItem[]; // 更新为 deviceld
|
||||
packageld: number; // 更新为 packageld
|
||||
sceneId: number; // 更新为 sceneId
|
||||
userValue: number;
|
||||
userStatus: number;
|
||||
addStatus: number; // 更新为 addStatus
|
||||
}) => void;
|
||||
scenarioOptions: ScenarioOption[];
|
||||
// 初始筛选值
|
||||
initialFilters?: {
|
||||
selectedDevices: DeviceSelectionItem[];
|
||||
packageId: number;
|
||||
scenarioId: number;
|
||||
userValue: number;
|
||||
userStatus: number;
|
||||
};
|
||||
}
|
||||
|
||||
const valueLevelOptions = [
|
||||
@@ -37,17 +45,35 @@ const FilterModal: React.FC<FilterModalProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
onConfirm,
|
||||
initialFilters,
|
||||
}) => {
|
||||
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 [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(() => {
|
||||
if (visible) {
|
||||
fetchScenarioOptions()
|
||||
@@ -72,11 +98,11 @@ const FilterModal: React.FC<FilterModalProps> = ({
|
||||
|
||||
const handleApply = () => {
|
||||
const params = {
|
||||
deviceIds: selectedDevices.map(d => d.id.toString()),
|
||||
packageId,
|
||||
scenarioId,
|
||||
selectedDevices: selectedDevices, // 更新为 deviceld
|
||||
packageld: packageId, // 更新为 packageld
|
||||
sceneId: scenarioId, // 更新为 sceneId
|
||||
userValue,
|
||||
userStatus,
|
||||
addStatus: userStatus, // 更新为 addStatus
|
||||
};
|
||||
console.log(params);
|
||||
|
||||
@@ -86,8 +112,8 @@ const FilterModal: React.FC<FilterModalProps> = ({
|
||||
|
||||
const handleReset = () => {
|
||||
setSelectedDevices([]);
|
||||
setPackageId("");
|
||||
setScenarioId("");
|
||||
setPackageId(0);
|
||||
setScenarioId(0);
|
||||
setUserValue(0);
|
||||
setUserStatus(0);
|
||||
};
|
||||
@@ -119,7 +145,7 @@ const FilterModal: React.FC<FilterModalProps> = ({
|
||||
value={packageId}
|
||||
onChange={setPackageId}
|
||||
options={[
|
||||
{ label: "全部流量池", value: "" },
|
||||
{ label: "全部流量池", value: 0 },
|
||||
...packageOptions.map(p => ({ label: p.name, value: p.id })),
|
||||
]}
|
||||
/>
|
||||
@@ -131,7 +157,7 @@ const FilterModal: React.FC<FilterModalProps> = ({
|
||||
value={scenarioId}
|
||||
onChange={setScenarioId}
|
||||
options={[
|
||||
{ label: "全部场景", value: "" },
|
||||
{ label: "全部场景", value: 0 },
|
||||
...scenarioOptions.map(s => ({ label: s.name, value: s.id })),
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -16,3 +16,19 @@ export async function fetchScenarioOptions() {
|
||||
export async function fetchPackageOptions() {
|
||||
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,
|
||||
BarChartOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Toast } from "antd-mobile";
|
||||
import { Input, Button, Checkbox, Pagination } from "antd";
|
||||
import styles from "./index.module.scss";
|
||||
import { Empty, Avatar } from "antd-mobile";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
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 FilterModal from "./FilterModal";
|
||||
import BatchAddModal from "./BatchAddModal";
|
||||
|
||||
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
||||
const defaultAvatar =
|
||||
"https://cdn.jsdelivr.net/gh/maokaka/static/avatar-default.png";
|
||||
|
||||
const TrafficPoolList: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
loading,
|
||||
list,
|
||||
page,
|
||||
setPage,
|
||||
total,
|
||||
search,
|
||||
setSearch,
|
||||
showFilter,
|
||||
setShowFilter,
|
||||
packageOptions,
|
||||
scenarioOptions,
|
||||
setSelectedDevices,
|
||||
setPackageId,
|
||||
setScenarioId,
|
||||
setUserValue,
|
||||
setUserStatus,
|
||||
selectedIds,
|
||||
handleSelectAll,
|
||||
handleSelect,
|
||||
batchModal,
|
||||
setBatchModal,
|
||||
batchTarget,
|
||||
setBatchTarget,
|
||||
handleBatchAdd,
|
||||
showStats,
|
||||
setShowStats,
|
||||
stats,
|
||||
getList,
|
||||
} = 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 [scenarioOptions, setScenarioOptions] = useState<ScenarioOption[]>([]);
|
||||
|
||||
// 公共筛选条件状态
|
||||
const [filterParams, setFilterParams] = useState({
|
||||
selectedDevices: [] as DeviceSelectionItem[],
|
||||
packageId: 0,
|
||||
scenarioId: 0,
|
||||
userValue: 0,
|
||||
userStatus: 0,
|
||||
});
|
||||
|
||||
// 批量相关
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||
const [batchModal, setBatchModal] = useState(false);
|
||||
|
||||
// 数据分析
|
||||
const [showStats, setShowStats] = useState(false);
|
||||
|
||||
// 获取列表
|
||||
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);
|
||||
@@ -57,16 +161,25 @@ const TrafficPoolList: React.FC = () => {
|
||||
const debouncedSearch = useCallback(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setSearch(searchInput);
|
||||
// 搜索时重置到第一页并请求列表
|
||||
setPage(1);
|
||||
getList({ keyword: searchInput, page: 1 });
|
||||
}, 500); // 500ms 防抖延迟
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchInput, setSearch]);
|
||||
}, [searchInput]);
|
||||
|
||||
useEffect(() => {
|
||||
const cleanup = debouncedSearch();
|
||||
return cleanup;
|
||||
}, [debouncedSearch]);
|
||||
|
||||
const handSearch = (value: string) => {
|
||||
setSearchInput(value);
|
||||
setSelectedIds([]);
|
||||
debouncedSearch();
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
loading={loading}
|
||||
@@ -89,14 +202,14 @@ const TrafficPoolList: React.FC = () => {
|
||||
<Input
|
||||
placeholder="搜索计划名称"
|
||||
value={searchInput}
|
||||
onChange={e => setSearchInput(e.target.value)}
|
||||
onChange={e => handSearch(e.target.value)}
|
||||
prefix={<SearchOutlined />}
|
||||
allowClear
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={getList}
|
||||
onClick={() => getList()}
|
||||
loading={loading}
|
||||
size="large"
|
||||
icon={<ReloadOutlined />}
|
||||
@@ -104,9 +217,12 @@ const TrafficPoolList: React.FC = () => {
|
||||
</div>
|
||||
{/* 数据分析面板 */}
|
||||
<DataAnalysisPanel
|
||||
stats={stats}
|
||||
showStats={showStats}
|
||||
setShowStats={setShowStats}
|
||||
onConfirm={statsData => {
|
||||
// 可以在这里处理统计数据,比如更新本地状态或发送到父组件
|
||||
console.log("收到统计数据:", statsData);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 批量操作栏 */}
|
||||
@@ -114,7 +230,7 @@ const TrafficPoolList: React.FC = () => {
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "8px 12px",
|
||||
padding: "8px 12px 8px 26px",
|
||||
background: "#fff",
|
||||
borderBottom: "1px solid #f0f0f0",
|
||||
}}
|
||||
@@ -140,6 +256,18 @@ const TrafficPoolList: React.FC = () => {
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{searchInput.length > 0 && (
|
||||
<>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
style={{ marginLeft: 16 }}
|
||||
onClick={() => setBatchModal(true)}
|
||||
>
|
||||
导入当前搜索结果
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<div style={{ flex: 1 }} />
|
||||
<Button
|
||||
size="small"
|
||||
@@ -158,7 +286,10 @@ const TrafficPoolList: React.FC = () => {
|
||||
pageSize={20}
|
||||
total={total}
|
||||
showSizeChanger={false}
|
||||
onChange={setPage}
|
||||
onChange={newPage => {
|
||||
setPage(newPage);
|
||||
getList({ page: newPage });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
@@ -167,35 +298,40 @@ const TrafficPoolList: React.FC = () => {
|
||||
<BatchAddModal
|
||||
visible={batchModal}
|
||||
onClose={() => setBatchModal(false)}
|
||||
packageOptions={packageOptions}
|
||||
batchTarget={batchTarget}
|
||||
setBatchTarget={setBatchTarget}
|
||||
selectedCount={selectedIds.length}
|
||||
onConfirm={handleBatchAdd}
|
||||
onConfirm={data => {
|
||||
// 处理批量加入逻辑
|
||||
handleBatchAdd(data);
|
||||
}}
|
||||
/>
|
||||
{/* 筛选弹窗 */}
|
||||
<FilterModal
|
||||
visible={showFilter}
|
||||
onClose={() => setShowFilter(false)}
|
||||
onConfirm={filters => {
|
||||
// 更新筛选条件
|
||||
setSelectedDevices(
|
||||
filters.deviceIds.map(id => ({
|
||||
id: parseInt(id),
|
||||
memo: "",
|
||||
imei: "",
|
||||
wechatId: "",
|
||||
status: "offline" as const,
|
||||
})),
|
||||
);
|
||||
setPackageId(filters.packageId ? parseInt(filters.packageId) : 0);
|
||||
setScenarioId(filters.scenarioId ? parseInt(filters.scenarioId) : 0);
|
||||
setUserValue(filters.userValue);
|
||||
setUserStatus(filters.userStatus);
|
||||
// 重新获取列表
|
||||
getList();
|
||||
// 更新公共筛选条件状态
|
||||
const newFilterParams = {
|
||||
selectedDevices: filters.selectedDevices,
|
||||
packageId: filters.packageld,
|
||||
scenarioId: filters.sceneId,
|
||||
userValue: filters.userValue,
|
||||
userStatus: filters.addStatus,
|
||||
};
|
||||
|
||||
setFilterParams(newFilterParams);
|
||||
// 重置到第一页并请求列表
|
||||
setPage(1);
|
||||
getList({
|
||||
page: 1,
|
||||
packageld: newFilterParams.packageId,
|
||||
sceneId: newFilterParams.scenarioId,
|
||||
userValue: newFilterParams.userValue,
|
||||
addStatus: newFilterParams.userStatus,
|
||||
deviceld: newFilterParams.selectedDevices.map(d => d.id).join(),
|
||||
});
|
||||
}}
|
||||
scenarioOptions={scenarioOptions}
|
||||
initialFilters={filterParams}
|
||||
/>
|
||||
<div className={styles.listWrap}>
|
||||
{list.length === 0 && !loading ? (
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface Allocation {
|
||||
/** 设备id */
|
||||
deviceGroups: number[];
|
||||
/** 流量池 */
|
||||
pools?: JSON | null;
|
||||
poolGroups?: number[];
|
||||
|
||||
/** 分配数量 */
|
||||
num?: number | null;
|
||||
@@ -72,7 +72,7 @@ export interface ContactImportTaskConfig {
|
||||
id: number;
|
||||
workbenchId: number;
|
||||
devices: number[];
|
||||
pools: number[];
|
||||
poolGroups: number[];
|
||||
num: number;
|
||||
clearContact: number;
|
||||
remarkType: number;
|
||||
@@ -114,7 +114,7 @@ export interface CreateContactImportTaskData {
|
||||
type: number;
|
||||
config: {
|
||||
devices: number[];
|
||||
pools: number[];
|
||||
poolGroups: number[];
|
||||
num: number;
|
||||
clearContact: 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 { PlusOutlined, MinusOutlined } from "@ant-design/icons";
|
||||
import { Button, Input, message, TimePicker, Select, Switch } from "antd";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import DeviceSelection from "@/components/DeviceSelection";
|
||||
import PoolSelection from "@/components/PoolSelection";
|
||||
import {
|
||||
createContactImportTask,
|
||||
updateContactImportTask,
|
||||
fetchContactImportTaskDetail,
|
||||
} from "./api";
|
||||
import { Allocation } from "./data";
|
||||
|
||||
import { PoolSelectionItem } from "@/components/PoolSelection/data";
|
||||
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
||||
import style from "./index.module.scss";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
@@ -27,7 +29,7 @@ const ContactImportForm: React.FC = () => {
|
||||
type: 6, // 任务类型,固定为6
|
||||
workbenchId: 1, // 默认工作台ID
|
||||
deviceGroups: [] as number[],
|
||||
pools: [] as any[],
|
||||
poolGroups: [] as number[],
|
||||
num: 50,
|
||||
clearContact: 0,
|
||||
remarkType: 0,
|
||||
@@ -35,7 +37,8 @@ const ContactImportForm: React.FC = () => {
|
||||
startTime: dayjs("09:00", "HH:mm"),
|
||||
endTime: dayjs("21:00", "HH:mm"),
|
||||
// 保留原有字段用于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;
|
||||
|
||||
try {
|
||||
@@ -59,6 +71,7 @@ const ContactImportForm: React.FC = () => {
|
||||
|
||||
// 构造设备选择组件需要的数据格式
|
||||
const deviceGroupsOptions = config.deviceGroupsOptions || [];
|
||||
const poolGroupsOptions = config.poolGroupsOptions || [];
|
||||
|
||||
setFormData({
|
||||
name: data.name || "",
|
||||
@@ -67,7 +80,7 @@ const ContactImportForm: React.FC = () => {
|
||||
workbenchId: config.workbenchId || 1,
|
||||
deviceGroups:
|
||||
deviceGroupsOptions.map((device: any) => device.id) || [],
|
||||
pools: config.pools ? JSON.parse(JSON.stringify(config.pools)) : [],
|
||||
poolGroups: config.poolGroups || [],
|
||||
num: config.num || 50,
|
||||
clearContact: config.clearContact || 0,
|
||||
remarkType: config.remarkType || 0,
|
||||
@@ -75,6 +88,7 @@ const ContactImportForm: React.FC = () => {
|
||||
startTime: config.startTime ? dayjs(config.startTime, "HH:mm") : null,
|
||||
endTime: config.endTime ? dayjs(config.endTime, "HH:mm") : null,
|
||||
deviceGroupsOptions,
|
||||
poolGroupsOptions,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -84,7 +98,7 @@ const ContactImportForm: React.FC = () => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [id, navigate]);
|
||||
|
||||
// 更新表单数据
|
||||
const handleUpdateFormData = (data: Partial<typeof formData>) => {
|
||||
@@ -125,7 +139,7 @@ const ContactImportForm: React.FC = () => {
|
||||
type: formData.type,
|
||||
workbenchId: formData.workbenchId,
|
||||
deviceGroups: formData.deviceGroups,
|
||||
pools: JSON.parse(JSON.stringify(formData.pools)),
|
||||
poolGroups: formData.poolGroups,
|
||||
num: formData.num,
|
||||
clearContact: formData.clearContact,
|
||||
remarkType: formData.remarkType,
|
||||
@@ -161,7 +175,7 @@ const ContactImportForm: React.FC = () => {
|
||||
type: 6,
|
||||
workbenchId: 1,
|
||||
deviceGroups: [],
|
||||
pools: [],
|
||||
poolGroups: [],
|
||||
num: 50,
|
||||
clearContact: 0,
|
||||
remarkType: 0,
|
||||
@@ -169,6 +183,7 @@ const ContactImportForm: React.FC = () => {
|
||||
startTime: dayjs("09:00", "HH:mm"),
|
||||
endTime: dayjs("21:00", "HH:mm"),
|
||||
deviceGroupsOptions: [],
|
||||
poolGroupsOptions: [],
|
||||
});
|
||||
};
|
||||
|
||||
@@ -176,7 +191,7 @@ const ContactImportForm: React.FC = () => {
|
||||
if (isEdit) {
|
||||
loadTaskDetail();
|
||||
}
|
||||
}, [id, isEdit]);
|
||||
}, [id, isEdit, loadTaskDetail]);
|
||||
|
||||
return (
|
||||
<Layout
|
||||
@@ -227,6 +242,17 @@ const ContactImportForm: React.FC = () => {
|
||||
<div className={style.counterTip}>选择要分配联系人的设备</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.formLabel}>分配数量</div>
|
||||
<div className={style.stepperContainer}>
|
||||
|
||||
@@ -35,7 +35,7 @@ export interface ContactImportTaskConfig {
|
||||
id: number;
|
||||
workbenchId: number;
|
||||
devices: number[];
|
||||
pools: number[];
|
||||
poolGroups: number[];
|
||||
num: number;
|
||||
clearContact: number;
|
||||
remarkType: number;
|
||||
@@ -77,7 +77,7 @@ export interface CreateContactImportTaskData {
|
||||
type: number;
|
||||
config: {
|
||||
devices: number[];
|
||||
pools: number[];
|
||||
poolGroups: number[];
|
||||
num: number;
|
||||
clearContact: number;
|
||||
remarkType: number;
|
||||
|
||||
@@ -27,7 +27,7 @@ export interface TrafficDistributionConfig {
|
||||
accountGroupsOptions: any[];
|
||||
deviceGroups: any[];
|
||||
deviceGroupsOptions: any[];
|
||||
pools: any[];
|
||||
poolGroups: any[];
|
||||
exp: number;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
@@ -58,7 +58,7 @@ export interface TrafficDistributionFormData {
|
||||
deviceGroupsOptions: any[];
|
||||
accountGroups: any[];
|
||||
accountGroupsOptions: any[];
|
||||
pools: any[];
|
||||
poolGroups: any[];
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Form,
|
||||
Input,
|
||||
Button,
|
||||
Radio,
|
||||
Slider,
|
||||
TimePicker,
|
||||
message,
|
||||
Checkbox,
|
||||
} from "antd";
|
||||
import { SearchOutlined } from "@ant-design/icons";
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Form, Input, Button, Radio, Slider, TimePicker, message } from "antd";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import style from "./index.module.scss";
|
||||
import StepIndicator from "@/components/StepIndicator";
|
||||
@@ -19,38 +9,16 @@ import AccountSelection from "@/components/AccountSelection";
|
||||
import { AccountItem } from "@/components/AccountSelection/data";
|
||||
import DeviceSelection from "@/components/DeviceSelection";
|
||||
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
||||
import PoolSelection from "@/components/PoolSelection";
|
||||
import { PoolSelectionItem } from "@/components/PoolSelection/data";
|
||||
import {
|
||||
getTrafficDistributionDetail,
|
||||
updateTrafficDistribution,
|
||||
createTrafficDistribution,
|
||||
} from "./api";
|
||||
import type {
|
||||
TrafficDistributionDetail,
|
||||
TrafficDistributionFormData,
|
||||
} from "./data";
|
||||
import type { TrafficDistributionFormData } from "./data";
|
||||
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 = [
|
||||
{ id: 1, title: "基本信息", subtitle: "基本信息" },
|
||||
{ id: 2, title: "目标设置", subtitle: "目标设置" },
|
||||
@@ -73,22 +41,20 @@ const TrafficDistributionForm: React.FC = () => {
|
||||
const [deviceGroupsOptions, setDeviceGroupsOptions] = useState<any[]>([]);
|
||||
const [accountGroups, setAccountGroups] = useState<any[]>([]);
|
||||
const [accountGroupsOptions, setAccountGroupsOptions] = useState<any[]>([]);
|
||||
const [distributeType, setDistributeType] = useState(1);
|
||||
const [maxPerDay, setMaxPerDay] = useState(50);
|
||||
const [timeType, setTimeType] = useState(1);
|
||||
const [timeRange, setTimeRange] = useState<any>(null);
|
||||
// 使用 Form 管理字段,配合 useWatch 读取值
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
|
||||
const [selectedPools, setSelectedPools] = useState<string[]>([]);
|
||||
const [poolSearch, setPoolSearch] = useState("");
|
||||
const [targetSelectionTab, setTargetSelectionTab] = useState<
|
||||
"device" | "account"
|
||||
>("device");
|
||||
const [poolGroupsOptions, setPoolGroupsOptions] = useState<
|
||||
PoolSelectionItem[]
|
||||
>([]);
|
||||
|
||||
// 编辑时的详情数据
|
||||
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 = () => {
|
||||
@@ -98,20 +64,12 @@ const TrafficDistributionForm: React.FC = () => {
|
||||
return `流量分发 ${dateStr} ${timeStr}`;
|
||||
};
|
||||
|
||||
// 获取详情数据
|
||||
useEffect(() => {
|
||||
if (isEdit && id) {
|
||||
fetchDetail();
|
||||
}
|
||||
}, [isEdit, id]);
|
||||
|
||||
const fetchDetail = async () => {
|
||||
const fetchDetail = useCallback(async () => {
|
||||
if (!id) return;
|
||||
|
||||
setDetailLoading(true);
|
||||
try {
|
||||
const detail = await getTrafficDistributionDetail(id);
|
||||
setDetailData(detail);
|
||||
|
||||
// 回填表单数据
|
||||
const config = detail.config;
|
||||
@@ -122,11 +80,6 @@ const TrafficDistributionForm: React.FC = () => {
|
||||
timeType: config.timeType,
|
||||
});
|
||||
|
||||
// 设置状态
|
||||
setDistributeType(config.distributeType);
|
||||
setMaxPerDay(config.maxPerDay);
|
||||
setTimeType(config.timeType);
|
||||
|
||||
// 设置账号组数据
|
||||
setAccountGroups(config.accountGroups || []);
|
||||
setAccountGroupsOptions(config.accountGroupsOptions || []);
|
||||
@@ -136,6 +89,8 @@ const TrafficDistributionForm: React.FC = () => {
|
||||
setDeviceGroups(config.deviceGroups || []);
|
||||
setDeviceGroupsOptions(config.deviceGroupsOptions || []);
|
||||
setSelectedDevices(config.deviceGroupsOptions || []);
|
||||
//设置流量池
|
||||
setPoolGroupsOptions(config.poolGroupsOptions || []);
|
||||
|
||||
// 设置时间范围 - 使用dayjs格式
|
||||
if (config.timeType === 2 && config.startTime && config.endTime) {
|
||||
@@ -147,22 +102,37 @@ const TrafficDistributionForm: React.FC = () => {
|
||||
// 使用dayjs创建时间对象
|
||||
const startTime = dayjs().hour(startHour).minute(startMinute).second(0);
|
||||
const endTime = dayjs().hour(endHour).minute(endMinute).second(0);
|
||||
setTimeRange([startTime, endTime]);
|
||||
form.setFieldsValue({ timeRange: [startTime, endTime] });
|
||||
}
|
||||
|
||||
// 设置流量池
|
||||
setSelectedPools(config.pools.map((pool: any) => pool.id || pool));
|
||||
// 设置流量池 - 交由 PoolSelection 控件受控
|
||||
} catch (error) {
|
||||
console.error("获取详情失败:", error);
|
||||
message.error("获取详情失败");
|
||||
} finally {
|
||||
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);
|
||||
try {
|
||||
// 校验流量池至少选择一个
|
||||
if (!poolGroupsOptions || poolGroupsOptions.length === 0) {
|
||||
message.error("请至少选择一个流量池");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
// 如果没有传递values参数,从表单中获取
|
||||
const formValues = values || form.getFieldsValue();
|
||||
|
||||
@@ -173,18 +143,22 @@ const TrafficDistributionForm: React.FC = () => {
|
||||
source: "",
|
||||
sourceIcon: "",
|
||||
description: "",
|
||||
distributeType: distributeType,
|
||||
maxPerDay: maxPerDay,
|
||||
timeType: timeType,
|
||||
distributeType: formValues.distributeType,
|
||||
maxPerDay: formValues.maxPerDay,
|
||||
timeType: formValues.timeType,
|
||||
startTime:
|
||||
timeType === 2 && timeRange?.[0] ? timeRange[0].format("HH:mm") : "",
|
||||
formValues.timeType === 2 && formValues.timeRange?.[0]
|
||||
? formValues.timeRange[0].format("HH:mm")
|
||||
: "",
|
||||
endTime:
|
||||
timeType === 2 && timeRange?.[1] ? timeRange[1].format("HH:mm") : "",
|
||||
deviceGroups: deviceGroups,
|
||||
formValues.timeType === 2 && formValues.timeRange?.[1]
|
||||
? formValues.timeRange[1].format("HH:mm")
|
||||
: "",
|
||||
deviceGroups: deviceGroupsOptions.map(v => v.id),
|
||||
deviceGroupsOptions: deviceGroupsOptions,
|
||||
accountGroups: accountGroups,
|
||||
accountGroups: accountGroupsOptions.map(v => v.id),
|
||||
accountGroupsOptions: accountGroupsOptions,
|
||||
pools: selectedPools,
|
||||
poolGroups: poolGroupsOptions.map(v => v.id),
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
@@ -216,15 +190,26 @@ const TrafficDistributionForm: React.FC = () => {
|
||||
.catch(() => {
|
||||
// 验证失败,不进行下一步
|
||||
});
|
||||
} else if (current === 1) {
|
||||
// 第二步:目标设置至少需要选择设备或客服之一
|
||||
const hasDevice =
|
||||
Array.isArray(selectedDevices) && selectedDevices.length > 0;
|
||||
if (!hasDevice) {
|
||||
message.error("请至少选择一个设备");
|
||||
return;
|
||||
}
|
||||
setCurrent(cur => cur + 1);
|
||||
} else {
|
||||
setCurrent(cur => cur + 1);
|
||||
}
|
||||
};
|
||||
const prev = () => setCurrent(cur => cur - 1);
|
||||
|
||||
// 过滤流量池
|
||||
const filteredPools = poolList.filter(pool => pool.name.includes(poolSearch));
|
||||
const handPoolAction = params => {
|
||||
setPoolGroupsOptions(params);
|
||||
};
|
||||
|
||||
// 移除未使用的输入同步函数
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
@@ -262,204 +247,136 @@ const TrafficDistributionForm: React.FC = () => {
|
||||
>
|
||||
<div className={style.formPage}>
|
||||
<div className={style.formBody}>
|
||||
{current === 0 && (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
name: isEdit ? "" : generateDefaultName(),
|
||||
distributeType: 1,
|
||||
maxPerDay: 50,
|
||||
timeType: 1,
|
||||
}}
|
||||
disabled={detailLoading}
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
name: isEdit ? "" : generateDefaultName(),
|
||||
distributeType: 1,
|
||||
maxPerDay: 50,
|
||||
timeType: 1,
|
||||
}}
|
||||
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
|
||||
label="计划名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入计划名称" }]}
|
||||
name="maxPerDay"
|
||||
noStyle
|
||||
rules={[{ required: true, message: "请设置每日最大分配量" }]}
|
||||
>
|
||||
<Input placeholder="流量分发 20250724 1700" maxLength={30} />
|
||||
<Slider min={1} max={100} className={style.slider} />
|
||||
</Form.Item>
|
||||
<Form.Item label="分配方式" name="distributeType" required>
|
||||
<Radio.Group
|
||||
value={distributeType}
|
||||
onChange={e => setDistributeType(e.target.value)}
|
||||
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} 人/天</span>
|
||||
</div>
|
||||
<Slider
|
||||
min={1}
|
||||
max={100}
|
||||
value={maxPerDay}
|
||||
onChange={setMaxPerDay}
|
||||
className={style.slider}
|
||||
<div className={style.sliderDesc}>限制每天最多分配的流量数量</div>
|
||||
</Form.Item>
|
||||
<Form.Item label="时间限制" name="timeType" required>
|
||||
<Radio.Group className={style.radioGroup}>
|
||||
<Radio value={1}>全天分配</Radio>
|
||||
<Radio value={2}>自定义时间段</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
{timeType === 2 && (
|
||||
<Form.Item
|
||||
label=""
|
||||
name="timeRange"
|
||||
required
|
||||
dependencies={["timeType"]}
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (getFieldValue("timeType") === 1) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (value && value.length === 2) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error("请选择开始和结束时间"));
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<TimePicker.RangePicker
|
||||
format="HH:mm"
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
<div className={style.sliderDesc}>
|
||||
限制每天最多分配的流量数量
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item label="时间限制" name="timeType" required>
|
||||
<Radio.Group
|
||||
value={timeType}
|
||||
onChange={e => setTimeType(e.target.value)}
|
||||
className={style.radioGroup}
|
||||
>
|
||||
<Radio value={1}>全天分配</Radio>
|
||||
<Radio value={2}>自定义时间段</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
{timeType === 2 && (
|
||||
<Form.Item label="" required>
|
||||
<div className={style.timeRangeWrap}>
|
||||
<div>
|
||||
<span>开始时间</span>
|
||||
<TimePicker
|
||||
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>
|
||||
)}
|
||||
)}
|
||||
|
||||
<div className={style.sectionTitle}>客服选择</div>
|
||||
<div className={style.formBlock}>
|
||||
<AccountSelection
|
||||
selectedOptions={accountGroupsOptions}
|
||||
onSelect={accounts => {
|
||||
setAccountGroupsOptions(accounts);
|
||||
}}
|
||||
placeholder="请选择客服"
|
||||
showSelectedList={true}
|
||||
selectedListMaxHeight={300}
|
||||
accountGroups={accountGroups}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
{current === 1 && (
|
||||
<div>
|
||||
<div className={style.sectionTitle}>目标设置</div>
|
||||
|
||||
{/* Tab 切换 */}
|
||||
<div className={style.tabWrapper}>
|
||||
<div className={style.tabList}>
|
||||
<div
|
||||
className={`${style.tabItem} ${
|
||||
targetSelectionTab === "device" ? style.tabActive : ""
|
||||
}`}
|
||||
onClick={() => setTargetSelectionTab("device")}
|
||||
>
|
||||
设备选择
|
||||
</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 className={style.formBlock}>
|
||||
<DeviceSelection
|
||||
selectedOptions={deviceGroupsOptions}
|
||||
onSelect={devices => {
|
||||
setSelectedDevices(devices);
|
||||
setDeviceGroupsOptions(devices);
|
||||
}}
|
||||
placeholder="请选择设备"
|
||||
showSelectedList={true}
|
||||
selectedListMaxHeight={300}
|
||||
deviceGroups={deviceGroups}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{current === 2 && (
|
||||
<div>
|
||||
<div className={style.sectionTitle}>流量池选择</div>
|
||||
<div className={style.formBlock}>
|
||||
<Input
|
||||
placeholder="搜索流量池"
|
||||
value={poolSearch}
|
||||
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>
|
||||
<PoolSelection
|
||||
selectedOptions={poolGroupsOptions}
|
||||
onSelect={handPoolAction}
|
||||
placeholder="请选择流量池"
|
||||
showSelectedList={true}
|
||||
selectedListMaxHeight={300}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ export function toggleDistributionRuleStatus(
|
||||
|
||||
// 删除计划
|
||||
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;
|
||||
account: (string | number)[];
|
||||
devices: string[];
|
||||
pools: string[];
|
||||
poolGroups: string[];
|
||||
exp: number;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
|
||||
@@ -272,7 +272,7 @@ const TrafficDistributionList: React.FC = () => {
|
||||
onClick={() => showPoolList(item)}
|
||||
>
|
||||
<div style={{ fontSize: 18, fontWeight: 600 }}>
|
||||
{item.config?.pools?.length || 0}
|
||||
{item.config?.poolGroups?.length || 0}
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: "#888" }}>流量池</div>
|
||||
</div>
|
||||
|
||||
@@ -28,32 +28,25 @@ class DouBaoAI
|
||||
}
|
||||
|
||||
|
||||
public function text()
|
||||
|
||||
public function text($params = [])
|
||||
{
|
||||
$this->__init();
|
||||
|
||||
$content = input('content','');
|
||||
if (empty($content)){
|
||||
if (empty($params)){
|
||||
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 = 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::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::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) {
|
||||
$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();
|
||||
}else{
|
||||
$user = $payload;
|
||||
@@ -84,9 +86,41 @@ class LoginController extends Controller
|
||||
$kefuData['self'] = $self;
|
||||
Db::name('users')->where('id', $user['id'])->update(['passwordLocal' => localEncrypt($params['password']),'updateTime' => time()]);
|
||||
}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']);
|
||||
$userData['member'] = $user;
|
||||
|
||||
|
||||
@@ -53,6 +53,9 @@ class MessageController extends BaseController
|
||||
->field('id,nickname,avatar')
|
||||
->find();
|
||||
$v['msgInfo'] = $friend;
|
||||
$v['unreadCount'] = Db::table('s2_wechat_message')
|
||||
->where(['wechatFriendId' => $v['wechatFriendId'],'isRead' => 0])
|
||||
->count();
|
||||
}
|
||||
|
||||
if (!empty($v['wechatChatroomId'])){
|
||||
@@ -61,12 +64,87 @@ class MessageController extends BaseController
|
||||
->field('id,nickname,chatroomAvatar as avatar')
|
||||
->find();
|
||||
$v['msgInfo'] = $chatroom;
|
||||
$v['unreadCount'] = Db::table('s2_wechat_message')
|
||||
->where(['wechatChatroomId' => $v['wechatChatroomId'],'isRead' => 0])
|
||||
->count();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
unset($v);
|
||||
|
||||
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;
|
||||
|
||||
use app\ai\controller\DouBaoAI;
|
||||
use app\chukebao\controller\TokensRecordController as tokensRecord;
|
||||
use library\ResponseHelper;
|
||||
use think\Db;
|
||||
|
||||
@@ -22,12 +24,121 @@ class WechatChatroomController extends BaseController
|
||||
$total = $query->count();
|
||||
|
||||
|
||||
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']) : '';
|
||||
// 提取所有聊天室ID,用于批量查询
|
||||
$chatroomIds = array_column($list, 'id');
|
||||
|
||||
|
||||
// 一次性查询所有聊天室的未读消息数量
|
||||
$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);
|
||||
|
||||
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();
|
||||
|
||||
|
||||
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']) : '';
|
||||
// 提取所有好友ID
|
||||
$friendIds = array_column($list, 'id');
|
||||
|
||||
// 一次性查询所有好友的未读消息数量
|
||||
$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);
|
||||
|
||||
|
||||
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('mobile-login', 'app\common\controller\Auth@mobileLogin'); // 手机号验证码登录
|
||||
Route::post('code', 'app\common\controller\Auth@SendCodeController'); // 发送验证码
|
||||
|
||||
// 需要JWT认证的接口
|
||||
Route::get('info', 'app\common\controller\Auth@info')->middleware(['jwt']); // 获取用户信息
|
||||
Route::post('refresh', 'app\common\controller\Auth@refresh')->middleware(['jwt']); // 刷新令牌
|
||||
@@ -22,4 +21,13 @@ Route::group('v1/', function () {
|
||||
})->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->endTime = $param['endTime'];
|
||||
$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->createTime = time();
|
||||
$config->updateTime = time();
|
||||
@@ -159,7 +159,7 @@ class WorkbenchController extends Controller
|
||||
$config = new WorkbenchImportContact;
|
||||
$config->workbenchId = $workbench->id;
|
||||
$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->clearContact = $param['clearContact'];
|
||||
$config->remark = $param['remark'];
|
||||
@@ -314,13 +314,13 @@ class WorkbenchController extends Controller
|
||||
if (!empty($item->trafficConfig)) {
|
||||
$item->config = $item->trafficConfig;
|
||||
$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);
|
||||
$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']) : '--';
|
||||
|
||||
//统计
|
||||
$labels = $item->config->pools;
|
||||
$labels = $item->config->poolGroups;
|
||||
$totalUsers = Db::table('s2_wechat_friend')->alias('wf')
|
||||
->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left')
|
||||
->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
|
||||
@@ -352,7 +352,7 @@ class WorkbenchController extends Controller
|
||||
'dailyAverage' => intval($dailyAverage),
|
||||
'totalAccounts' => $totalAccounts,
|
||||
'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
|
||||
];
|
||||
}
|
||||
@@ -363,7 +363,7 @@ class WorkbenchController extends Controller
|
||||
if (!empty($item->importContact)) {
|
||||
$item->config = $item->importContact;
|
||||
$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);
|
||||
break;
|
||||
@@ -505,12 +505,12 @@ class WorkbenchController extends Controller
|
||||
$workbench->config = $workbench->trafficConfig;
|
||||
$workbench->config->deviceGroups = json_decode($workbench->config->devices, 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();
|
||||
$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')
|
||||
->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left')
|
||||
->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
|
||||
@@ -549,7 +549,7 @@ class WorkbenchController extends Controller
|
||||
'dailyAverage' => intval($dailyAverage),
|
||||
'totalAccounts' => $totalAccounts,
|
||||
'deviceCount' => count($workbench->config->deviceGroups),
|
||||
'poolCount' => count($workbench->config->pools),
|
||||
'poolCount' => count($workbench->config->poolGroups),
|
||||
'totalUsers' => $totalUsers >> 0
|
||||
];
|
||||
unset($workbench->trafficConfig, $workbench->traffic_config);
|
||||
@@ -559,7 +559,7 @@ class WorkbenchController extends Controller
|
||||
if (!empty($workbench->importContact)) {
|
||||
$workbench->config = $workbench->importContact;
|
||||
$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);
|
||||
break;
|
||||
@@ -789,7 +789,7 @@ class WorkbenchController extends Controller
|
||||
$config->startTime = $param['startTime'];
|
||||
$config->endTime = $param['endTime'];
|
||||
$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->updateTime = time();
|
||||
$config->save();
|
||||
@@ -799,7 +799,7 @@ class WorkbenchController extends Controller
|
||||
$config = WorkbenchImportContact::where('workbenchId', $param['id'])->find();;
|
||||
if ($config) {
|
||||
$config->devices = json_encode($param['deviceGroups']);
|
||||
$config->pools = json_encode($param['pools']);
|
||||
$config->pools = json_encode($param['poolGroups']);
|
||||
$config->num = $param['num'];
|
||||
$config->clearContact = $param['clearContact'];
|
||||
$config->remark = $param['remark'];
|
||||
@@ -1450,7 +1450,7 @@ class WorkbenchController extends Controller
|
||||
'startTime' => $param['startTime'],
|
||||
'endTime' => $param['endTime'],
|
||||
'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(),
|
||||
'updateTime' => time()
|
||||
]);
|
||||
|
||||
@@ -120,7 +120,7 @@ class GetAddFriendPlanDetailV1Controller extends Controller
|
||||
// 解析JSON字段
|
||||
$sceneConf = json_decode($plan['sceneConf'], true) ?: [];
|
||||
$reqConf = json_decode($plan['reqConf'], true) ?: [];
|
||||
$reqConf['deveiceGroups'] = $reqConf['device'];
|
||||
$reqConf['deviceGroups'] = $reqConf['device'];
|
||||
$msgConf = json_decode($plan['msgConf'], true) ?: [];
|
||||
$tagConf = json_decode($plan['tagConf'], true) ?: [];
|
||||
|
||||
@@ -139,8 +139,8 @@ class GetAddFriendPlanDetailV1Controller extends Controller
|
||||
}
|
||||
|
||||
|
||||
if (!empty($reqConf['deveiceGroups'])){
|
||||
$deveiceGroupsOptions = DeviceModel::alias('d')
|
||||
if (!empty($reqConf['deviceGroups'])){
|
||||
$deviceGroupsOptions = DeviceModel::alias('d')
|
||||
->field([
|
||||
'd.id', 'd.imei', 'd.memo', 'd.alive',
|
||||
'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('wechat_account a', 'l.wechatId = a.wechatId')
|
||||
->order('d.id desc')
|
||||
->whereIn('d.id',$reqConf['deveiceGroups'])
|
||||
->whereIn('d.id',$reqConf['deviceGroups'])
|
||||
->select();
|
||||
foreach ($deveiceGroupsOptions as &$device) {
|
||||
foreach ($deviceGroupsOptions as &$device) {
|
||||
$curstomer = WechatCustomerModel::field('friendShip')->where(['wechatId' => $device['wechatId']])->find();
|
||||
$device['totalFriend'] = $curstomer->friendShip->totalFriend ?? 0;
|
||||
}
|
||||
unset($device);
|
||||
$reqConf['deveiceGroupsOptions'] = $deveiceGroupsOptions;
|
||||
$reqConf['deviceGroupsOptions'] = $deviceGroupsOptions;
|
||||
}else{
|
||||
$reqConf['deveiceGroupsOptions'] = [];
|
||||
$reqConf['deviceGroupsOptions'] = [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -56,11 +56,11 @@ class GetPotentialListWithInCompanyV1Controller extends BaseController
|
||||
}
|
||||
|
||||
if (!empty($device)) {
|
||||
$where[] = ['d.deviceId', '=', $device];
|
||||
// $where[] = ['d.deviceId', '=', $device];
|
||||
}
|
||||
|
||||
if (!empty($taskId)) {
|
||||
$where[] = ['t.sceneId', '=', $taskId];
|
||||
//$where[] = ['t.sceneId', '=', $taskId];
|
||||
}
|
||||
$where[] = ['s.companyId', '=', $this->getUserInfo('companyId')];
|
||||
|
||||
|
||||
@@ -55,11 +55,9 @@ class Workbench extends Validate
|
||||
'distributeType' => 'requireIf:type,5|in:1,2',
|
||||
'maxPerDay' => 'requireIf:type,5|number|min:1',
|
||||
'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',
|
||||
// 通用参数
|
||||
'deveiceGroups' => 'requireIf:type,1,2,5|array',
|
||||
'deviceGroups' => 'requireIf:type,1,2,5|array',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -142,8 +140,8 @@ class Workbench extends Validate
|
||||
'timeType.requireIf' => '请选择时间类型',
|
||||
|
||||
// 通用提示
|
||||
'deveiceGroups.require' => '请选择设备',
|
||||
'deveiceGroups.array' => '设备格式错误',
|
||||
'deviceGroups.require' => '请选择设备',
|
||||
'deviceGroups.array' => '设备格式错误',
|
||||
'targetGroups.require' => '请选择目标用户组',
|
||||
'targetGroups.array' => '目标用户组格式错误',
|
||||
'accountGroups.requireIf' => '流量分发时必须选择分发账号',
|
||||
@@ -155,7 +153,7 @@ class Workbench extends Validate
|
||||
* 验证场景
|
||||
*/
|
||||
protected $scene = [
|
||||
'create' => ['name', 'type', 'autoStart', 'deveiceGroups', 'targetGroups',
|
||||
'create' => ['name', 'type', 'autoStart', 'deviceGroups', 'targetGroups',
|
||||
'interval', 'maxLikes', 'startTime', 'endTime', 'contentTypes',
|
||||
'syncInterval', 'syncCount', 'syncType',
|
||||
'pushType', 'startTime', 'endTime', 'maxPerDay', 'pushOrder', 'isLoop', 'status', 'wechatGroups', 'contentGroups',
|
||||
@@ -163,7 +161,7 @@ class Workbench extends Validate
|
||||
'groupNameTemplate', 'maxGroupsPerDay', 'groupSizeMin', 'groupSizeMax',
|
||||
],
|
||||
'update_status' => ['id', 'status'],
|
||||
'edit' => ['name', 'type', 'autoStart', 'deveiceGroups', 'targetGroups',
|
||||
'edit' => ['name', 'type', 'autoStart', 'deviceGroups', 'targetGroups',
|
||||
'interval', 'maxLikes', 'startTime', 'endTime', 'contentTypes',
|
||||
'syncInterval', 'syncCount', 'syncType',
|
||||
'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>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>存客宝</title>
|
||||
<title>触客宝</title>
|
||||
<style>
|
||||
html {
|
||||
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 |