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
This commit is contained in:
超级老白兔
2025-09-25 11:31:41 +08:00
350 changed files with 10348 additions and 5984 deletions

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@ Store_vue/.specstory/
Store_vue/unpackage/
Store_vue/.vscode/
SuperAdmin/.specstory/
Cunkebao/dist

1
Cunkebao/.env.local Normal file
View File

@@ -0,0 +1 @@
NEXT_PUBLIC_API_BASE_URL= http://yishi.com

View File

@@ -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"
]
}
}

File diff suppressed because one or more lines are too long

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 488 KiB

View File

@@ -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"
}
]
}

View File

@@ -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);
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -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",
},
};

View File

@@ -784,12 +784,6 @@ const TrafficPoolDetail: React.FC = () => {
</div>
)}
</Card>
{/* 添加新标签按钮 */}
<Button block color="primary" className={styles.addTagBtn}>
<TagOutlined />
&nbsp;
</Button>
</div>
)}
</div>

View File

@@ -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;

View File

@@ -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 }}

View File

@@ -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 })),
]}
/>

View File

@@ -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");
}

View File

@@ -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,
};
}

View File

@@ -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 ? (

View File

@@ -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;

View File

@@ -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}>

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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");
}
// 获取流量分发规则详情

View File

@@ -113,7 +113,7 @@ export interface DistributionRule {
endTime: string;
account: (string | number)[];
devices: string[];
pools: string[];
poolGroups: string[];
exp: number;
createTime: string;
updateTime: string;

View File

@@ -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>

View File

@@ -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]]);
}
}
}

View File

@@ -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');
});
});

View 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']);
}
}
}

View 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());
}
}
}

View 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());
}
}
}

View File

@@ -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;

View File

@@ -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]);
}
}

View 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,'获取成功');
}
}

View 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());
}
}
}

View File

@@ -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; // 重新抛出异常,让重试机制处理
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);

View 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';
}

View 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';
}

View 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';
}

View 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';
}

View 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';
}

View 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>");
}
}

View 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>");
}
}

View 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());
}
}
}

View 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>");
}
}
}

View File

@@ -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');

View 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 方式 POSTtext/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]);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace app\common\model;
use think\Model;
class Order extends Model
{
// 设置数据表名
protected $name = 'order';
}

View 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);
}
}

View 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);
}
}

View 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;
}
}

View File

@@ -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()
]);

View File

@@ -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'] = [];
}

View File

@@ -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')];

View File

@@ -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',

View 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"
}
}

View File

@@ -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;

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Some files were not shown because too many files have changed in this diff Show More