flyzto 6 mesiacov pred
rodič
commit
3205bf4674

+ 29 - 0
server/init.js

@@ -0,0 +1,29 @@
+const { init: settingInit } = require('./models/Setting');
+const { init: userInit } = require('./models/User');
+const Logs = require('./libs/logs');
+
+(() => {
+  settingInit({
+    innerDefaultAmount: 10000,
+    minProfitAmount: 0,
+    innerRebateRatio: 0,
+    runWorkerEnabled: false,
+  })
+  .then(() => {
+    Logs.out('初始化设置完成');
+  })
+  .catch(err => {
+    Logs.errDev('初始化设置失败:', err);
+  });
+
+  userInit({
+    username: 'admin',
+    password: '123456'
+  })
+  .then(() => {
+    Logs.out('初始化用户完成');
+  })
+  .catch(err => {
+    Logs.errDev('初始化用户失败:', err);
+  });
+})();

+ 50 - 2
server/models/Games.js

@@ -1,3 +1,42 @@
+const mongoose = require('mongoose');
+const { Schema } = mongoose;
+
+const gameSchema = new Schema({
+  leagueId: { type: Number, required: true },
+  eventId: { type: Number, required: true },
+  leagueName: { type: String, required: true },
+  teamHomeName: { type: String, required: true },
+  teamAwayName: { type: String, required: true },
+  timestamp: { type: Number, required: true },
+  matchNumStr: { type: String, required: false }
+}, { _id: false });
+
+const relSchema = new Schema({
+  jc: { type: gameSchema },
+  ps: { type: gameSchema },
+  ob: { type: gameSchema },
+}, { _id: false });
+
+const relationSchema = new Schema({
+  id: { type: Number, required: true },
+  rel: { type: relSchema, required: true },
+}, {
+  toJSON: {
+    transform(doc, ret) {
+      delete ret._id;
+      delete ret.__v;
+    }
+  },
+  toObject: {
+    transform(doc, ret) {
+      delete ret._id;
+      delete ret.__v;
+    }
+  }
+});
+
+const Relation = mongoose.model('Relation', relationSchema);
+
 const { fork } = require('child_process');
 const calcTotalProfit = require('../triangle/totalProfitCalc');
 
@@ -7,7 +46,8 @@ const childOptions = process.env.NODE_ENV == 'development' ? {
 const events_child = fork('./triangle/eventsMatch.js', [], childOptions);
 
 const Logs = require('../libs/logs');
-const Relation = require('./Relation');
+
+const Setting = require('./Setting');
 
 const Request = {
   callbacks: {},
@@ -365,7 +405,12 @@ const getTotalProfit = (sid1, sid2, gold_side_jc) => {
   if (!sol1 || !sol2 || !gold_side_jc) {
     return {};
   }
-  return calcTotalProfit(sol1, sol2, gold_side_jc);
+  const profit = calcTotalProfit(sol1, sol2, gold_side_jc);
+  return { profit, sol1, sol2 };
+}
+
+const getSetting = async () => {
+  return Setting.get();
 }
 
 const getDataFromChild = (type, callback) => {
@@ -382,6 +427,9 @@ events_child.on('message', async (message) => {
     if (type == 'getGamesRelation') {
       responseData = await getGamesRelation(true);
     }
+    else if (type == 'getSetting') {
+      responseData = await getSetting();
+    }
     // else if (type == 'getSolutionHistory') {
     //   responseData = getSolutionHistory();
     // }

+ 0 - 38
server/models/Relation.js

@@ -1,38 +0,0 @@
-const mongoose = require('mongoose');
-const { Schema } = mongoose;
-
-const gameSchema = new Schema({
-  leagueId: { type: Number, required: true },
-  eventId: { type: Number, required: true },
-  leagueName: { type: String, required: true },
-  teamHomeName: { type: String, required: true },
-  teamAwayName: { type: String, required: true },
-  timestamp: { type: Number, required: true },
-  matchNumStr: { type: String, required: false }
-}, { _id: false });
-
-const relSchema = new Schema({
-  jc: { type: gameSchema },
-  ps: { type: gameSchema },
-  ob: { type: gameSchema },
-}, { _id: false });
-
-const relationSchema = new Schema({
-  id: { type: Number, required: true },
-  rel: { type: relSchema, required: true },
-}, {
-  toJSON: {
-    transform(doc, ret) {
-      delete ret._id;
-      delete ret.__v;
-    }
-  },
-  toObject: {
-    transform(doc, ret) {
-      delete ret._id;
-      delete ret.__v;
-    }
-  }
-});
-
-module.exports = mongoose.model('Relation', relationSchema);

+ 70 - 0
server/models/Setting.js

@@ -0,0 +1,70 @@
+const mongoose = require('mongoose');
+const { Schema } = mongoose;
+
+const systemSettingSchema = new Schema({
+  _id: {
+    type: String,
+    default: 'system' // 固定 ID
+  },
+  innerDefaultAmount: {
+    type: Number,
+    required: true,
+    default: 10000
+  },
+  minProfitAmount: {
+    type: Number,
+    required: true,
+    default: 0
+  },
+  innerRebateRatio: {
+    type: Number,
+    required: true,
+    default: 0
+  },
+  runWorkerEnabled: {
+    type: Boolean,
+    required: true,
+    default: false
+  }
+}, {
+  toJSON: {
+    transform(doc, ret) {
+      delete ret._id;
+      delete ret.__v;
+    }
+  },
+  toObject: {
+    transform(doc, ret) {
+      delete ret._id;
+      delete ret.__v;
+    }
+  }
+});
+
+const Setting = mongoose.model('SystemSetting', systemSettingSchema);
+
+const get = async () => {
+  return await Setting.findById('system');
+}
+
+const update = async (fields) => {
+  return await Setting.findByIdAndUpdate(
+    'system',
+    { $set: fields },
+    { upsert: true, new: true }
+  );
+}
+
+const init = async (fields = {}) => {
+  const systemSetting = await Setting.findById('system');
+  if (systemSetting) {
+    return;
+  }
+  const setting = new Setting({
+    _id: 'system',
+    ...fields,
+  });
+  return setting.save();
+}
+
+module.exports = { get, update, init };

+ 98 - 1
server/models/User.js

@@ -1,10 +1,107 @@
 const mongoose = require('mongoose');
 const { Schema } = mongoose;
 
+const bcrypt = require('bcryptjs');
+const jwt = require('jsonwebtoken');
+
 const userSchema = new Schema({
   username: { type: String, required: true, unique: true },
   password: { type: String, required: true },
   roles: { type: [String], default: ['user'] },
 }, { timestamps: true });
 
-module.exports = mongoose.model('User', userSchema);
+const User = mongoose.model('User', userSchema);
+
+const add = async ({ username, password } = {}) => {
+  return User.findOne({ username })
+  .then(existing => {
+    if (existing) {
+      return Promise.reject(new Error('USER_EXISTS'));
+    }
+    return bcrypt.hash(password, 10);
+  })
+  .then(hashedPassword => {
+    const user = new User({ username, password: hashedPassword });
+    return user.save();
+  });
+}
+
+const login = async ({ username, password } = {}) => {
+  return User.findOne({ username })
+  .then(user => {
+    if (!user) {
+      return Promise.reject(new Error('USER_NOT_FOUND'));
+    }
+    return Promise.all([
+      bcrypt.compare(password, user.password),
+      Promise.resolve(user),
+    ]);
+  })
+  .then(([isMatch, user]) => {
+    if (!isMatch) {
+      return Promise.reject(new Error('PASSWORD_ERROR'));
+    }
+    return user;
+  })
+  .then(user => {
+    // 签发 Access Token 和 Refresh Token
+    const accessToken = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '30m' });
+    const refreshToken = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '7d' });
+
+    const userInfo = {
+      uid: user._id.toString(),
+      username: user.username,
+      roles: user.roles ?? ['user'],
+    }
+
+    return { info: userInfo, accessToken, refreshToken };
+  });
+}
+
+const refresh = async (refreshToken) => {
+  const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET);
+  return User.findById(decoded.userId)
+  .then(user => {
+    if (!user) {
+      return Promise.reject(new Error('USER_NOT_FOUND'));
+    }
+
+    // 签发新的 Access Token
+    const newAccessToken = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '30m' });
+    return newAccessToken;
+  });
+}
+
+const info = async (userId) => {
+  return User.findById(userId).select('-password')
+  .then(user => {
+    if (!user) {
+      return Promise.reject(new Error('USER_NOT_FOUND'));
+    }
+    return {
+      uid: user._id.toString(),
+      username: user.username,
+      roles: user.roles ?? ['user'],
+    };
+  });
+}
+
+const init = async ({ username, password } = {}) => {
+  return User.find()
+  .then(users => {
+    if (users.length) {
+      return Promise.reject('USER_EXISTS');
+    }
+    return bcrypt.hash(password, 10);
+  })
+  .then(hashedPassword => {
+    const user = new User({
+      username,
+      password: hashedPassword,
+      roles: ['admin'],
+    });
+    return user.save();
+  });
+}
+
+module.exports = { add, login, refresh, info, init };

+ 2 - 1
server/package.json

@@ -4,7 +4,8 @@
   "main": "server.js",
   "scripts": {
     "dev": "nodemon --inspect server.js",
-    "start": "pm2 start server.js --name sporttery"
+    "start": "pm2 start server.js --name sporttery",
+    "init": "node init.js"
   },
   "keywords": [],
   "author": "",

+ 31 - 0
server/routes/system.js

@@ -0,0 +1,31 @@
+const express = require('express');
+const router = express.Router();
+
+const authMiddleware = require('../middleware/authMiddleware');
+
+const Setting = require('../models/Setting');
+const Logs = require('../libs/logs');
+
+router.get('/get_setting', authMiddleware, (req, res) => {
+  Setting.get()
+  .then(setting => {
+    res.sendSuccess(setting);
+  })
+  .catch(err => {
+    Logs.errDev('获取参数设置失败:', err);
+    res.badRequest(err.message);
+  });
+});
+
+router.post('/update_setting', authMiddleware, (req, res) => {
+  Setting.update(req.body)
+  .then(setting => {
+    res.sendSuccess(setting);
+  })
+  .catch(err => {
+    Logs.errDev('更新参数设置失败:', err);
+    res.badRequest(err.message);
+  });
+});
+
+module.exports = router;

+ 12 - 66
server/routes/user.js

@@ -9,24 +9,14 @@ const User = require('../models/User');
 const Logs = require('../libs/logs');
 
 // 注册
-router.post('/register', async (req, res) => {
+router.post('/add',authMiddleware, async (req, res) => {
   const { username, password } = req.body;
-  User.findOne({ username })
-  .then(existing => {
-    if (existing) {
-      return Promise.reject(new Error('USER_EXISTS'));
-    }
-    return bcrypt.hash(password, 10);
-  })
-  .then(hashedPassword => {
-    const user = new User({ username, password: hashedPassword });
-    return user.save();
-  })
+  User.add({ username, password })
   .then(() => {
-    res.sendSuccess('注册成功');
+    res.sendSuccess('添加成功');
   })
   .catch(err => {
-    Logs.errDev('注册失败:', err);
+    Logs.errDev('添加失败:', err);
     if (err.message === 'USER_EXISTS') {
       return res.badRequest('用户已存在');
     }
@@ -37,26 +27,8 @@ router.post('/register', async (req, res) => {
 // 登录 - 支持原路径 /login 和 mock 服务路径 /login
 router.post('/login', async (req, res) => {
   const { username, password } = req.body;
-  User.findOne({ username })
-  .then(user => {
-    if (!user) {
-      return Promise.reject(new Error('USER_NOT_FOUND'));
-    }
-    return Promise.all([
-      bcrypt.compare(password, user.password),
-      Promise.resolve(user),
-    ]);
-  })
-  .then(([isMatch, user]) => {
-    if (!isMatch) {
-      return Promise.reject(new Error('PASSWORD_ERROR'));
-    }
-    return user;
-  })
-  .then(user => {
-    // 签发 Access Token 和 Refresh Token
-    const accessToken = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '30m' });
-    const refreshToken = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '7d' });
+  User.login({ username, password })
+  .then(({ info, accessToken, refreshToken }) => {
 
     // 设置 refresh token 到 cookie
     res.cookie('jwt', refreshToken, {
@@ -67,12 +39,7 @@ router.post('/login', async (req, res) => {
     });
 
     // 返回格式与前端期望一致
-    res.sendSuccess({
-      accessToken: accessToken,
-      uid: user._id.toString(),
-      username: user.username,
-      roles: user.roles ?? ['user'],
-    });
+    res.sendSuccess({ accessToken, ...info });
 
   })
   .catch(err => {
@@ -96,22 +63,12 @@ router.post('/logout', authMiddleware, (req, res) => {
 // 刷新 Token - 支持 Vben Admin 的 /refresh 路径
 router.post('/refresh', async (req, res) => {
   const refreshToken = req.cookies.jwt;
-
   if (!refreshToken) {
     return res.unauthorized('无效的刷新token');
   }
-  const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET);
-  User.findById(decoded.userId)
-  .then(user => {
-    if (!user) {
-      return Promise.reject(new Error('USER_NOT_FOUND'));
-    }
-
-    // 签发新的 Access Token
-    const newAccessToken = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '30m' });
-
-    // 按照Vben Admin期望的格式返回
-    res.send(newAccessToken);
+  User.refresh(refreshToken)
+  .then(accessToken => {
+    res.send(accessToken);
   })
   .catch(err => {
     Logs.errDev('刷新Token失败:', err);
@@ -126,20 +83,9 @@ router.post('/refresh', async (req, res) => {
 
 // 用户信息 - 支持 mock 服务的 /info 路径
 router.get('/info', authMiddleware, async (req, res) => {
-  User.findById(req.userId).select('-password')
-  .then(user => {
-    if (!user) {
-      return Promise.reject(new Error('USER_NOT_FOUND'));
-    }
-
-    const userInfo = {
-      uid: user._id.toString(),
-      username: user.username,
-      roles: user.roles ?? ['user'],
-    };
-
+  User.info(req.userId)
+  .then(userInfo => {
     res.sendSuccess(userInfo);
-
   })
   .catch(err => {
     Logs.errDev('获取用户信息错误:', err);

+ 4 - 1
server/server.js

@@ -2,8 +2,11 @@ const express = require('express');
 const mongoose = require('mongoose');
 const dotenv = require('dotenv');
 const Logs = require('./libs/logs');
+
 const userRoutes = require('./routes/user');
+const systemRoutes = require('./routes/system');
 const triangleRoutes = require('./routes/triangle');
+
 const cookieParser = require('cookie-parser');
 const app = express();
 
@@ -49,7 +52,7 @@ app.use((req, res, next) => {
 });
 
 app.use(['/api/user', '/api/auth'], userRoutes);
-
+app.use('/api/system', systemRoutes);
 app.use('/api/triangle', triangleRoutes);
 
 // 启动服务

+ 56 - 9
server/triangle/eventsMatch.js

@@ -1,5 +1,5 @@
 const Logs = require('../libs/logs');
-const eventsCombination = require('./trangleCalc');
+const { eventsCombination } = require('./trangleCalc');
 
 const Request = {
   callbacks: {},
@@ -9,8 +9,16 @@ const Request = {
 // const WIN_STEP = 15;
 // const SOL_FREEZ_TIME = 1000 * 30;
 
+const SETTING = {
+  innerDefaultAmount: 10000,
+  minProfitAmount: 0,
+  innerRebateRatio: 0,
+  runWorkerEnabled: false
+}
+
 const GLOBAL_DATA = {
   relationLength: 0,
+  loopStatus: -1,
 };
 
 const getDataFromParent = (type, callback) => {
@@ -49,22 +57,39 @@ const fixFloat = (number, x=2) => {
   return parseFloat(number.toFixed(x));
 }
 
-const getGamesRelation = () => {
+const getSetting = () => {
   return new Promise(resolve => {
-    getDataFromParent('getGamesRelation', (relations) => {
-      resolve(relations);
+    getDataFromParent('getSetting', (setting) => {
+      resolve(setting);
     });
   });
 }
 
-const getSolutionHistory = () => {
+const getGamesRelation = () => {
+  const status = +SETTING.runWorkerEnabled;
+  if (GLOBAL_DATA.loopStatus !== status) {
+    GLOBAL_DATA.loopStatus = status;
+    Logs.out('loop status changed to', status);
+  }
+
+  if (!SETTING.runWorkerEnabled) {
+    return Promise.resolve([]);
+  }
   return new Promise(resolve => {
-    getDataFromParent('getSolutionHistory', (solutions) => {
-      resolve(solutions);
+    getDataFromParent('getGamesRelation', (relations) => {
+      resolve(relations);
     });
   });
 }
 
+// const getSolutionHistory = () => {
+//   return new Promise(resolve => {
+//     getDataFromParent('getSolutionHistory', (solutions) => {
+//       resolve(solutions);
+//     });
+//   });
+// }
+
 const setSolution = (solutions) => {
   postDataToParent('setSolution', solutions);
 }
@@ -101,6 +126,11 @@ const extractOdds = ({ evtime, events, sptime, special }) => {
 const eventMatch = () => {
   getGamesRelation()
   .then(relations => {
+
+    if (!relations?.length) {
+      return;
+    }
+
     const nowTime = Date.now();
     relations = relations.filter(relaiton => {
       const expire = Object.values(relaiton.rel).find(event => event.timestamp <= nowTime);
@@ -144,8 +174,8 @@ const eventMatch = () => {
       return eventsMap;
     });
 
-    const solutions = eventsCombination(passableEvents);
-    postDataToParent('setSolutions', solutions);
+    const solutions = eventsCombination(passableEvents, SETTING);
+    setSolution(solutions);
   })
   .finally(() => {
     setTimeout(() => {
@@ -154,4 +184,21 @@ const eventMatch = () => {
   });
 };
 
+const syncSetting = () => {
+  getSetting()
+  .then(setting => {
+    if (setting) {
+      Object.keys(setting).forEach(key => {
+        SETTING[key] = setting[key];
+      });
+    }
+  })
+  .finally(() => {
+    setTimeout(() => {
+      syncSetting();
+    }, 10000);
+  });
+}
+
+syncSetting();
 eventMatch();

+ 25 - 15
server/triangle/trangleCalc.js

@@ -1,9 +1,11 @@
 const Logs = require('../libs/logs');
 const IOR_KEYS_MAP = require('./iorKeys');
 
-const GOLD_BASE = 10000;
-const WIN_MIN = process.env.NODE_ENV == 'development' ? -10000 : -1000;
-const JC_REBATE_RATIO = 0;
+const SETTING = {
+  innerDefaultAmount: 10000,
+  minProfitAmount: 0,
+  innerRebateRatio: 0,
+}
 
 /**
  * 筛选最优赔率
@@ -86,6 +88,7 @@ const fixFloat = (number, x=2) => {
  * 计算盈利
  */
 const triangleProfitCalc = (goldsInfo, oddsOption) => {
+  const { innerRebateRatio } = SETTING;
   const {
     gold_side_a: x,
     gold_side_b: y,
@@ -108,13 +111,13 @@ const triangleProfitCalc = (goldsInfo, oddsOption) => {
 
   let jc_rebate = 0;
   if (jcIndex == 0) {
-    jc_rebate = x * JC_REBATE_RATIO;
+    jc_rebate = x * innerRebateRatio;
   }
   else if (jcIndex == 1) {
-    jc_rebate = y * JC_REBATE_RATIO;
+    jc_rebate = y * innerRebateRatio;
   }
   else if (jcIndex == 2) {
-    jc_rebate = z * JC_REBATE_RATIO;
+    jc_rebate = z * innerRebateRatio;
   }
 
   let win_side_a = 0, win_side_b = 0, win_side_m = 0;
@@ -151,12 +154,13 @@ const triangleProfitCalc = (goldsInfo, oddsOption) => {
 }
 
 const triangleGoldCalc = (oddsInfo, oddsOption) => {
+  const { innerDefaultAmount } = SETTING;
   const { odds_side_a: a, odds_side_b: b, odds_side_m: c } = oddsInfo;
   if (!a || !b || !c) {
     return;
   }
   const { crossType, jcIndex } = oddsOption;
-  let x = GOLD_BASE;
+  let x = innerDefaultAmount;
   let y = (a + 1) * x / (b + 1);
   let z;
   switch (crossType) {
@@ -183,16 +187,16 @@ const triangleGoldCalc = (oddsInfo, oddsOption) => {
   }
 
   if (jcIndex == 1) {
-    const scale = GOLD_BASE / y;
+    const scale = innerDefaultAmount / y;
     x = x * scale;
-    y = GOLD_BASE;
+    y = innerDefaultAmount;
     z = z * scale;
   }
   else if (jcIndex == 2) {
-    const scale = GOLD_BASE / z;
+    const scale = innerDefaultAmount / z;
     x = x * scale;
     y = y * scale;
-    z = GOLD_BASE;
+    z = innerDefaultAmount;
   }
 
   return {
@@ -207,6 +211,7 @@ const triangleGoldCalc = (oddsInfo, oddsOption) => {
 }
 
 const eventSolutions = (oddsInfo, oddsOption) => {
+  const { innerDefaultAmount } = SETTING;
   const goldsInfo = triangleGoldCalc(oddsInfo, oddsOption);
   if (!goldsInfo) {
     return;
@@ -218,11 +223,16 @@ const eventSolutions = (oddsInfo, oddsOption) => {
     ...profitInfo,
     cross_type: oddsOption.crossType,
     jc_index: oddsOption.jcIndex,
-    jc_base: GOLD_BASE,
+    jc_base: innerDefaultAmount,
   }
 }
 
-const eventsCombination = (passableEvents) => {
+const eventsCombination = (passableEvents, setting) => {
+
+  Object.keys(setting).forEach(key => {
+    SETTING[key] = setting[key];
+  });
+
   const solutions = [];
   passableEvents.forEach(events => {
     const { odds, info } = events;
@@ -248,7 +258,7 @@ const eventsCombination = (passableEvents) => {
         };
         const oddsOption = { crossType, jcIndex };
         const sol = eventSolutions(oddsInfo, oddsOption);
-        if (sol?.win_average > WIN_MIN) {
+        if (sol?.win_average > SETTING.minProfitAmount) {
           const id = info.id;
           const keys = cpr.map(item => `${item.k}`).join('_');
           const sid = `${id}_${keys}`;
@@ -261,4 +271,4 @@ const eventsCombination = (passableEvents) => {
   return solutions;
 }
 
-module.exports = eventsCombination;
+module.exports = { eventsCombination };

+ 0 - 112
spider/index.js

@@ -1,112 +0,0 @@
-const axios = require('axios');
-
-const apiUrl = 'https://webapi.sporttery.cn/gateway/uniform/football/getMatchCalculatorV1.qry?poolCode=hhad,had&channel=c';
-
-const ratioAccept = (ratio) => {
-  if (ratio > 0) {
-    return 'a'
-  }
-  return ''
-}
-
-const ratioString = (ratio) => {
-  ratio = Math.abs(ratio);
-  ratio = ratio.toString();
-  ratio = ratio.replace(/\./, '');
-  return ratio;
-}
-
-const parseEvents = (oddsInfo) => {
-  const { h, d, a, goalLineValue, poolId } = oddsInfo;
-  const gv = +goalLineValue;
-  const events = {};
-  const special = {};
-  const ratio_rh = gv - 0.5;
-  const ratio_rc = -(gv + 0.5);
-
-  if (!gv) {
-    events['ior_mn'] = +d;
-  }
-  else {
-    special[`ior_wm${gv > 0 ? 'c' : 'h'}_${Math.abs(gv)}`] = +d;
-  }
-
-  events[`ior_r${ratioAccept(ratio_rh)}h_${ratioString(ratio_rh)}`] = +h;
-  events[`ior_r${ratioAccept(ratio_rc)}c_${ratioString(ratio_rc)}`] = +a;
-
-  return { events, special };
-}
-
-const getGamesEvents = async () => {
-  return axios.get(apiUrl, {
-    headers: {
-      'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36'
-    },
-    proxy: false,
-  })
-  .then(res => res.data)
-  .then(ret => {
-    const { success, value } = ret;
-    if (!success || !value) {
-      return;
-    }
-    const { matchInfoList } = value;
-    const matchList = matchInfoList?.map(item => {
-      const { subMatchList } = item;
-      return subMatchList.map(match => {
-        const { leagueAllName, leagueId,
-          homeTeamAllName, awayTeamAllName,
-          matchDate, matchTime,
-          oddsList, matchId } = match;
-        const timestamp = new Date(`${matchDate} ${matchTime}`).getTime();
-
-        const oddsValues = { events: {}, special: {} };
-        oddsList.map(parseEvents).forEach(odds => {
-          oddsValues.events = { ...oddsValues.events, ...odds.events };
-          oddsValues.special = { ...oddsValues.special, ...odds.special };
-        });
-
-        const evtime = Date.now();
-        const sptime = Date.now();
-
-        return {
-          leagueId,
-          eventId: matchId,
-          leagueName: leagueAllName,
-          teamHomeName: homeTeamAllName,
-          teamAwayName: awayTeamAllName,
-          timestamp,
-          ...oddsValues,
-          evtime,
-          sptime,
-        }
-      });
-    });
-    return matchList?.flat() ?? [];
-  })
-}
-
-const updateGamesList = ({ platform, games }) => {
-  axios.post('http://127.0.0.1:9055/api/triangle/update_games_list', { platform, games }, { proxy: false })
-  .then(res => res.data)
-  .then(ret => {
-    if (ret.code) {
-      throw new Error(ret.message);
-    }
-  })
-  .catch(error => {
-    console.log('%cupdate game list failed, %s', 'color:#c00', error.message, platform);
-  });
-}
-
-setInterval(() => {
-  getGamesEvents()
-  .then(gamesEvents => {
-    updateGamesList({ platform: 'jc', games: gamesEvents });
-    // console.log(JSON.stringify(gamesEvents, null, 2));
-    // console.log(new Date());
-  })
-  .catch(err => {
-    console.error(err);
-  });
-}, 10000);

+ 0 - 294
spider/package-lock.json

@@ -1,294 +0,0 @@
-{
-  "name": "sporttery-spider",
-  "version": "1.0.0",
-  "lockfileVersion": 3,
-  "requires": true,
-  "packages": {
-    "": {
-      "name": "sporttery-spider",
-      "version": "1.0.0",
-      "license": "ISC",
-      "dependencies": {
-        "axios": "^1.8.4"
-      }
-    },
-    "node_modules/asynckit": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
-      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
-      "license": "MIT"
-    },
-    "node_modules/axios": {
-      "version": "1.8.4",
-      "resolved": "https://registry.npmmirror.com/axios/-/axios-1.8.4.tgz",
-      "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
-      "license": "MIT",
-      "dependencies": {
-        "follow-redirects": "^1.15.6",
-        "form-data": "^4.0.0",
-        "proxy-from-env": "^1.1.0"
-      }
-    },
-    "node_modules/call-bind-apply-helpers": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
-      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
-      "license": "MIT",
-      "dependencies": {
-        "es-errors": "^1.3.0",
-        "function-bind": "^1.1.2"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/combined-stream": {
-      "version": "1.0.8",
-      "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
-      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
-      "license": "MIT",
-      "dependencies": {
-        "delayed-stream": "~1.0.0"
-      },
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
-    "node_modules/delayed-stream": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
-      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=0.4.0"
-      }
-    },
-    "node_modules/dunder-proto": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
-      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
-      "license": "MIT",
-      "dependencies": {
-        "call-bind-apply-helpers": "^1.0.1",
-        "es-errors": "^1.3.0",
-        "gopd": "^1.2.0"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/es-define-property": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
-      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/es-errors": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
-      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/es-object-atoms": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
-      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
-      "license": "MIT",
-      "dependencies": {
-        "es-errors": "^1.3.0"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/es-set-tostringtag": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
-      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
-      "license": "MIT",
-      "dependencies": {
-        "es-errors": "^1.3.0",
-        "get-intrinsic": "^1.2.6",
-        "has-tostringtag": "^1.0.2",
-        "hasown": "^2.0.2"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/follow-redirects": {
-      "version": "1.15.9",
-      "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz",
-      "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
-      "funding": [
-        {
-          "type": "individual",
-          "url": "https://github.com/sponsors/RubenVerborgh"
-        }
-      ],
-      "license": "MIT",
-      "engines": {
-        "node": ">=4.0"
-      },
-      "peerDependenciesMeta": {
-        "debug": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/form-data": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.2.tgz",
-      "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
-      "license": "MIT",
-      "dependencies": {
-        "asynckit": "^0.4.0",
-        "combined-stream": "^1.0.8",
-        "es-set-tostringtag": "^2.1.0",
-        "mime-types": "^2.1.12"
-      },
-      "engines": {
-        "node": ">= 6"
-      }
-    },
-    "node_modules/function-bind": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
-      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
-      "license": "MIT",
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/get-intrinsic": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
-      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
-      "license": "MIT",
-      "dependencies": {
-        "call-bind-apply-helpers": "^1.0.2",
-        "es-define-property": "^1.0.1",
-        "es-errors": "^1.3.0",
-        "es-object-atoms": "^1.1.1",
-        "function-bind": "^1.1.2",
-        "get-proto": "^1.0.1",
-        "gopd": "^1.2.0",
-        "has-symbols": "^1.1.0",
-        "hasown": "^2.0.2",
-        "math-intrinsics": "^1.1.0"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/get-proto": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
-      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
-      "license": "MIT",
-      "dependencies": {
-        "dunder-proto": "^1.0.1",
-        "es-object-atoms": "^1.0.0"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/gopd": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
-      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/has-symbols": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
-      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/has-tostringtag": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
-      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
-      "license": "MIT",
-      "dependencies": {
-        "has-symbols": "^1.0.3"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/hasown": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
-      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
-      "license": "MIT",
-      "dependencies": {
-        "function-bind": "^1.1.2"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/math-intrinsics": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
-      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/mime-db": {
-      "version": "1.52.0",
-      "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
-      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/mime-types": {
-      "version": "2.1.35",
-      "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
-      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
-      "license": "MIT",
-      "dependencies": {
-        "mime-db": "1.52.0"
-      },
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/proxy-from-env": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
-      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
-      "license": "MIT"
-    }
-  }
-}

+ 0 - 15
spider/package.json

@@ -1,15 +0,0 @@
-{
-  "name": "sporttery-spider",
-  "version": "1.0.0",
-  "main": "index.js",
-  "scripts": {
-    "dev": "nodemon index.js",
-    "start": "pm2 start index.js --name sporttery-spider"
-  },
-  "author": "",
-  "license": "ISC",
-  "description": "",
-  "dependencies": {
-    "axios": "^1.8.4"
-  }
-}

+ 6 - 1
web/apps/web-antd/src/locales/langs/en-US/page.json

@@ -13,6 +13,11 @@
   "match": {
     "title": "Match Management",
     "related": "Related Matches",
-    "relatedDesc": "Manage match relation information"
+    "centerOrder": "Center Order"
+  },
+  "system": {
+    "title": "System",
+    "user": "User",
+    "parameter": "Parameter"
   }
 }

+ 6 - 3
web/apps/web-antd/src/locales/langs/zh-CN/page.json

@@ -13,8 +13,11 @@
   "match": {
     "title": "比赛管理",
     "related": "关联比赛",
-    "relatedDesc": "管理比赛关联信息",
-    "centerOrder": "中单记录",
-    "centerOrderDesc": "管理中单记录信息"
+    "centerOrder": "中单记录"
+  },
+  "system": {
+    "title": "系统设置",
+    "user": "用户管理",
+    "parameter": "参数设置"
   }
 }

+ 11 - 0
web/apps/web-antd/src/preferences.ts

@@ -12,4 +12,15 @@ export const overridesPreferences = defineOverridesPreferences({
     enableRefreshToken: true,
     defaultHomePath: '/workspace',
   },
+  theme: {
+    mode: 'light'
+  },
+  transition: {
+    name: 'fade'
+  },
+  widget: {
+    languageToggle: false,
+    notification: false,
+    themeToggle: false,
+  },
 });

+ 34 - 34
web/apps/web-antd/src/router/routes/core.ts

@@ -56,40 +56,40 @@ const coreRoutes: RouteRecordRaw[] = [
           title: $t('page.auth.login'),
         },
       },
-      {
-        name: 'CodeLogin',
-        path: 'code-login',
-        component: () => import('#/views/_core/authentication/code-login.vue'),
-        meta: {
-          title: $t('page.auth.codeLogin'),
-        },
-      },
-      {
-        name: 'QrCodeLogin',
-        path: 'qrcode-login',
-        component: () =>
-          import('#/views/_core/authentication/qrcode-login.vue'),
-        meta: {
-          title: $t('page.auth.qrcodeLogin'),
-        },
-      },
-      {
-        name: 'ForgetPassword',
-        path: 'forget-password',
-        component: () =>
-          import('#/views/_core/authentication/forget-password.vue'),
-        meta: {
-          title: $t('page.auth.forgetPassword'),
-        },
-      },
-      {
-        name: 'Register',
-        path: 'register',
-        component: () => import('#/views/_core/authentication/register.vue'),
-        meta: {
-          title: $t('page.auth.register'),
-        },
-      },
+      // {
+      //   name: 'CodeLogin',
+      //   path: 'code-login',
+      //   component: () => import('#/views/_core/authentication/code-login.vue'),
+      //   meta: {
+      //     title: $t('page.auth.codeLogin'),
+      //   },
+      // },
+      // {
+      //   name: 'QrCodeLogin',
+      //   path: 'qrcode-login',
+      //   component: () =>
+      //     import('#/views/_core/authentication/qrcode-login.vue'),
+      //   meta: {
+      //     title: $t('page.auth.qrcodeLogin'),
+      //   },
+      // },
+      // {
+      //   name: 'ForgetPassword',
+      //   path: 'forget-password',
+      //   component: () =>
+      //     import('#/views/_core/authentication/forget-password.vue'),
+      //   meta: {
+      //     title: $t('page.auth.forgetPassword'),
+      //   },
+      // },
+      // {
+      //   name: 'Register',
+      //   path: 'register',
+      //   component: () => import('#/views/_core/authentication/register.vue'),
+      //   meta: {
+      //     title: $t('page.auth.register'),
+      //   },
+      // },
     ],
   },
 ];

+ 0 - 28
web/apps/web-antd/src/router/routes/modules/demos.ts

@@ -1,28 +0,0 @@
-import type { RouteRecordRaw } from 'vue-router';
-
-import { $t } from '#/locales';
-
-const routes: RouteRecordRaw[] = [
-  {
-    meta: {
-      icon: 'ic:baseline-view-in-ar',
-      keepAlive: true,
-      order: 1000,
-      title: $t('demos.title'),
-    },
-    name: 'Demos',
-    path: '/demos',
-    children: [
-      {
-        meta: {
-          title: $t('demos.antd'),
-        },
-        name: 'AntDesignDemos',
-        path: '/demos/ant-design',
-        component: () => import('#/views/demos/antd/index.vue'),
-      },
-    ],
-  },
-];
-
-export default routes;

+ 2 - 2
web/apps/web-antd/src/router/routes/modules/match.ts

@@ -14,7 +14,7 @@ const routes: RouteRecordRaw[] = [
     children: [
       {
         name: 'RelatedMatch',
-        path: '/related',
+        path: 'related',
         component: () => import('#/views/match/related/index.vue'),
         meta: {
           icon: 'ion:git-compare-outline',
@@ -24,7 +24,7 @@ const routes: RouteRecordRaw[] = [
       },
       {
         name: 'CenterOrder',
-        path: '/solutions',
+        path: 'solutions',
         component: () => import('#/views/match/solutions/index.vue'),
         meta: {
           icon: 'ion:receipt-outline',

+ 37 - 0
web/apps/web-antd/src/router/routes/modules/system.ts

@@ -0,0 +1,37 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+  {
+    meta: {
+      icon: 'ion:settings-outline',
+      order: 4,
+      title: $t('page.system.title'),
+    },
+    name: 'System',
+    path: '/system',
+    children: [
+      {
+        name: 'UserManagement',
+        path: 'user',
+        component: () => import('#/views/system/user/index.vue'),
+        meta: {
+          icon: 'ion:people-outline',
+          title: $t('page.system.user'),
+        },
+      },
+      {
+        name: 'ParameterSettings',
+        path: 'parameter',
+        component: () => import('#/views/system/parameter/index.vue'),
+        meta: {
+          icon: 'ion:options-outline',
+          title: $t('page.system.parameter'),
+        },
+      },
+    ],
+  },
+];
+
+export default routes;

+ 8 - 15
web/apps/web-antd/src/views/match/related/index.vue

@@ -1,8 +1,7 @@
 <script setup>
-import { Page } from '@vben/common-ui';
 import { requestClient } from '#/api/request';
 import { Button, message } from 'ant-design-vue';
-import { ref, reactive, computed, onMounted, useTemplateRef } from 'vue';
+import { ref, reactive, computed, onMounted } from 'vue';
 import dayjs from 'dayjs';
 import MatchItem from '../components/match_item.vue';
 
@@ -209,26 +208,21 @@ const selectGame = (platform, game) => {
   }
 }
 
-const getTopPanelPosition = () => {
-  const topPanel = useTemplateRef('topPanel');
-}
-
 onMounted(() => {
   updateGamesList();
   updateGamesRelations();
-  getTopPanelPosition();
 });
 </script>
 
 <template>
-  <Page>
+
+  <div class="relation-container">
     <div class="top-panel" ref="topPanel">
       <span>竞彩</span>
       <span>平博</span>
       <span>OB</span>
       <i>{{ relationsList.length }}</i>
     </div>
-
     <div class="match-list" v-if="relationsList.length">
       <div class="match-row" v-for="({ id, rel }) in relationsList" :key="id">
         <MatchItem
@@ -238,7 +232,6 @@ onMounted(() => {
           :teamAwayName="rel.jc.teamAwayName"
           :dateTime="rel.jc.dateTime"
           :matchNumStr="rel.jc.matchNumStr" />
-
         <MatchItem  v-if="rel.ps"
           :eventId="rel.ps.eventId"
           :leagueName="rel.ps.leagueName"
@@ -246,7 +239,6 @@ onMounted(() => {
           :teamAwayName="rel.ps.teamAwayName"
           :dateTime="rel.ps.dateTime" />
         <div class="match-item match-item-holder" v-else></div>
-
         <MatchItem v-if="rel.ob"
           :eventId="rel.ob.eventId"
           :leagueName="rel.ob.leagueName"
@@ -254,7 +246,6 @@ onMounted(() => {
           :teamAwayName="rel.ob.teamAwayName"
           :dateTime="rel.ob.dateTime" />
         <div class="match-item match-item-holder" v-else></div>
-
         <div class="match-action">
           <Button type="link" class="action-btn" @click="removeGamesRelation(id)">
             <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
@@ -300,14 +291,16 @@ onMounted(() => {
         </Button>
       </div>
     </div>
-
-
     <div class="list-empty" v-if="!relationsList.length && !jcGamesList.length">暂无数据</div>
+  </div>
 
-  </Page>
 </template>
 
 <style lang="scss" scoped>
+.relation-container {
+  padding: 10px;
+}
+
 .top-panel {
   display: flex;
   margin-bottom: 5px;

+ 8 - 8
web/apps/web-antd/src/views/match/solutions/index.vue

@@ -1,8 +1,7 @@
 <script setup>
-import { Page } from '@vben/common-ui';
 import { requestClient } from '#/api/request';
 import { Button, message, Form, InputNumber, Drawer } from 'ant-design-vue';
-import { ref, reactive, computed, onMounted, onUnmounted, useTemplateRef } from 'vue';
+import { ref, reactive, computed, onMounted, onUnmounted } from 'vue';
 import dayjs from 'dayjs';
 
 import MatchCard from '../components/match_card.vue';
@@ -36,22 +35,23 @@ const headerStyle = computed(() => {
 });
 
 const totalProfitValue = computed(() => {
-  const profit = {};
-  const jcScale = jcOptions.bet / totalProfit.value.jc_base;
+  const { profit, sol1, sol2 } = totalProfit.value;
+  const profitInfo = {};
+  const jcScale = jcOptions.bet / profit.jc_base;
   const jcRebate = jcOptions.bet * jcOptions.rebate / 100;
 
-  Object.keys(totalProfit.value).forEach(key => {
+  Object.keys(profit).forEach(key => {
     if (key == 'win_diff') {
       return;
     }
     if (key.startsWith('gold')) {
-      profit[key] = fixFloat(totalProfit.value[key] * jcScale);
+      profitInfo[key] = fixFloat(profit[key] * jcScale);
     }
     else if (key.startsWith('win_')) {
-      profit[key] = fixFloat(totalProfit.value[key] * jcScale + jcRebate);
+      profitInfo[key] = fixFloat(profit[key] * jcScale + jcRebate);
     }
   });
-  return profit;
+  return profitInfo;
 });
 
 const solutionsList = computed(() => {

+ 136 - 0
web/apps/web-antd/src/views/system/parameter/index.vue

@@ -0,0 +1,136 @@
+<script setup>
+import { Page } from '@vben/common-ui';
+import { requestClient } from '#/api/request';
+import { Button, message, Form, InputNumber, Drawer, Switch } from 'ant-design-vue';
+import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue';
+
+const initialFormState = {
+  innerDefaultAmount: 10000,
+  minProfitAmount: 0,
+  innerRebateRatio: 0,
+  runWorkerEnabled: false
+};
+
+const formState = reactive({ ...initialFormState });
+const isFormChanged = ref(false);
+
+const checkFormChanged = () => {
+  isFormChanged.value = Object.keys(initialFormState).some(key =>
+    formState[key] !== initialFormState[key]
+  );
+};
+
+watch(formState, () => {
+  checkFormChanged();
+}, { deep: true });
+
+const getSetting = async () => {
+  try {
+    const data = await requestClient.get('/system/get_setting');
+    return data;
+  }
+  catch (error) {
+    console.error('Failed to fetch setting:', error);
+    message.error('获取参数设置失败');
+    return {};
+  }
+}
+
+const saveSetting = async () => {
+  try {
+    await requestClient.post('/system/update_setting', formState);
+    isFormChanged.value = false;
+    message.success('保存成功');
+  }
+  catch (error) {
+    console.error('Failed to save setting:', error);
+    message.error('保存失败');
+  }
+}
+
+const syncSetting = () => {
+  getSetting().then(data => {
+    if (data) {
+      Object.assign(formState, data);
+      Object.assign(initialFormState, data);
+      isFormChanged.value = false;
+    }
+  });
+}
+
+onMounted(() => {
+  syncSetting();
+});
+
+onUnmounted(() => {
+
+});
+</script>
+
+<template>
+ <Page title="参数设置">
+    <Form
+      :model="formState"
+      layout="horizontal"
+      :label-col="{ span: 6 }"
+      :wrapper-col="{ span: 18 }"
+      class="parameter-form"
+    >
+      <Form.Item
+        label="内盘默认注额"
+        name="innerDefaultAmount"
+      >
+        <InputNumber
+          v-model:value="formState.innerDefaultAmount"
+          :min="0"
+          :step="1000"
+          style="width: 200px"
+        />
+      </Form.Item>
+
+      <Form.Item
+        label="最小利润额"
+        name="minProfitAmount"
+      >
+        <InputNumber
+          v-model:value="formState.minProfitAmount"
+          :min="-10000"
+          :step="1"
+          style="width: 200px"
+        />
+      </Form.Item>
+
+      <Form.Item
+        label="内盘返点比例"
+        name="innerRebateRatio"
+      >
+        <InputNumber
+          v-model:value="formState.innerRebateRatio"
+          :min="0"
+          :max="100"
+          :step="1"
+          style="width: 200px"
+        />
+      </Form.Item>
+
+      <Form.Item
+        label="后台 Worker 开关"
+        name="runWorkerEnabled"
+      >
+        <Switch v-model:checked="formState.runWorkerEnabled" />
+      </Form.Item>
+
+      <Form.Item :wrapper-col="{ offset: 6, span: 18 }">
+        <Button type="primary" @click="saveSetting" :disabled="!isFormChanged">保存设置</Button>
+      </Form.Item>
+    </Form>
+  </Page>
+</template>
+
+<style scoped>
+
+.parameter-form {
+  max-width: 600px;
+}
+
+</style>

+ 213 - 0
web/apps/web-antd/src/views/system/user/index.vue

@@ -0,0 +1,213 @@
+<script setup>
+import { Page } from '@vben/common-ui';
+import { Button, Table, Modal, Form, Input, Select, message } from 'ant-design-vue';
+import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue';
+import { ref, h } from 'vue';
+
+// 模拟用户数据
+const users = ref([
+  {
+    id: 1,
+    username: 'admin',
+    email: 'admin@example.com',
+    phone: '13800138000',
+    roles: ['admin'],
+  },
+  {
+    id: 2,
+    username: 'user1',
+    email: 'user1@example.com',
+    phone: '13800138001',
+    roles: ['user'],
+  },
+]);
+
+// 表格列定义
+const columns = [
+  {
+    title: '用户名',
+    dataIndex: 'username',
+    key: 'username',
+  },
+  {
+    title: '邮箱',
+    dataIndex: 'email',
+    key: 'email',
+  },
+  {
+    title: '手机号',
+    dataIndex: 'phone',
+    key: 'phone',
+  },
+  {
+    title: '用户权限',
+    dataIndex: 'roles',
+    key: 'roles',
+    customRender: ({ text }) => text.join(', '),
+  },
+  {
+    title: '操作',
+    key: 'action',
+    width: 200,
+    customRender: ({ record }) => {
+      return h('div', { class: 'action-buttons' }, [
+        h(Button, {
+          type: 'link',
+          onClick: () => handleEdit(record)
+        }, () => [h(EditOutlined), ' 编辑']),
+        h(Button, {
+          type: 'link',
+          danger: true,
+          onClick: () => handleDelete(record)
+        }, () => [h(DeleteOutlined), ' 删除'])
+      ]);
+    }
+  },
+];
+
+// 表单相关
+const formRef = ref();
+const formState = ref({
+  username: '',
+  password: '',
+  email: '',
+  phone: '',
+  roles: [],
+});
+const modalVisible = ref(false);
+const isEdit = ref(false);
+const currentUserId = ref(null);
+
+// 角色选项
+const roleOptions = [
+  { label: '管理员', value: 'admin' },
+  { label: '普通用户', value: 'user' },
+];
+
+// 处理添加用户
+const handleAdd = () => {
+  isEdit.value = false;
+  formState.value = {
+    username: '',
+    password: '',
+    email: '',
+    phone: '',
+    roles: [],
+  };
+  modalVisible.value = true;
+};
+
+// 处理编辑用户
+const handleEdit = (record) => {
+  isEdit.value = true;
+  currentUserId.value = record.id;
+  formState.value = {
+    username: record.username,
+    password: '',
+    email: record.email,
+    phone: record.phone,
+    roles: record.roles,
+  };
+  modalVisible.value = true;
+};
+
+// 处理删除用户
+const handleDelete = (record) => {
+  Modal.confirm({
+    title: '确认删除',
+    content: `确定要删除用户 ${record.username} 吗?`,
+    okText: '确认',
+    cancelText: '取消',
+    onOk: () => {
+      message.success('删除成功');
+    },
+  });
+};
+
+// 处理表单提交
+const handleSubmit = () => {
+  formRef.value.validate().then(() => {
+    message.success(isEdit.value ? '编辑成功' : '添加成功');
+    modalVisible.value = false;
+  });
+};
+
+// 表单验证规则
+const rules = {
+  username: [{ required: true, message: '请输入用户名' }],
+  password: [{ required: true, message: '请输入密码' }],
+  email: [
+    { required: true, message: '请输入邮箱' },
+    { type: 'email', message: '请输入正确的邮箱格式' },
+  ],
+  phone: [
+    { required: true, message: '请输入手机号' },
+    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式' },
+  ],
+  roles: [{ required: true, message: '请选择用户权限' }],
+};
+</script>
+
+<template>
+  <Page title="用户管理">
+    <div class="table-operations">
+      <Button type="primary" @click="handleAdd">
+        <PlusOutlined /> 添加用户
+      </Button>
+    </div>
+
+    <Table
+      :columns="columns"
+      :data-source="users"
+      :row-key="record => record.id"
+      :pagination="false"
+    />
+
+    <Modal
+      :title="isEdit ? '编辑用户' : '添加用户'"
+      v-model:visible="modalVisible"
+      @ok="handleSubmit"
+      :maskClosable="false"
+    >
+      <Form
+        ref="formRef"
+        :model="formState"
+        :rules="rules"
+        :label-col="{ span: 6 }"
+        :wrapper-col="{ span: 16 }"
+      >
+        <Form.Item label="用户名" name="username">
+          <Input v-model:value="formState.username" placeholder="请输入用户名" />
+        </Form.Item>
+        <Form.Item label="密码" name="password">
+          <Input.Password v-model:value="formState.password" placeholder="请输入密码" />
+        </Form.Item>
+        <Form.Item label="邮箱" name="email">
+          <Input v-model:value="formState.email" placeholder="请输入邮箱" />
+        </Form.Item>
+        <Form.Item label="手机号" name="phone">
+          <Input v-model:value="formState.phone" placeholder="请输入手机号" />
+        </Form.Item>
+        <Form.Item label="用户权限" name="roles">
+          <Select
+            v-model:value="formState.roles"
+            mode="multiple"
+            placeholder="请选择用户权限"
+            :options="roleOptions"
+          />
+        </Form.Item>
+      </Form>
+    </Modal>
+  </Page>
+</template>
+
+<style lang="scss" scoped>
+.table-operations {
+  margin-bottom: 16px;
+}
+
+.action-buttons {
+  display: flex;
+  gap: 8px;
+}
+</style>

+ 1 - 1
web/apps/web-antd/vite.config.mts

@@ -15,7 +15,7 @@ export default defineConfig(async () => {
           '/api': {
             changeOrigin: true,
             rewrite: (path) => path.replace(/^\/api/, ''),
-            target: 'https://jc.long.bid/api',
+            target: 'http://127.0.0.1:9055/api',
             ws: true,
           },
         },