Merge pull request 'main' (#129) from main into prod

Reviewed-on: #129
This commit is contained in:
charles 2026-06-22 14:39:42 +08:00
commit a8bb1140cb
20 changed files with 1949 additions and 55 deletions

60
b/account_config.sql Normal file
View File

@ -0,0 +1,60 @@
/*
Navicat Premium Data Transfer
Source Server : kbossdev
Source Server Type : MariaDB
Source Server Version : 100622
Source Host : db:3306
Source Schema : kboss_dev
Target Server Type : MariaDB
Target Server Version : 100622
File Encoding : 65001
Date: 18/06/2026 17:11:12
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for account_config
-- ----------------------------
DROP TABLE IF EXISTS `account_config`;
CREATE TABLE `account_config` (
`id` varchar(32) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT 'id',
`partytype` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '参与方类型',
`subjectname` varchar(32) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '科目名称',
`del_flg` varchar(1) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT '0' COMMENT '删除标志',
`create_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT '创建时间戳',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci COMMENT = '账户配置表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of account_config
-- ----------------------------
INSERT INTO `account_config` VALUES ('acc001', '本机构', '资金账号', '0', '2023-08-03 17:40:34');
INSERT INTO `account_config` VALUES ('acc002', '本机构', '折扣收入', '0', '2023-08-03 17:40:34');
INSERT INTO `account_config` VALUES ('acc003', '本机构', '底价收入', '0', '2023-08-03 17:40:34');
INSERT INTO `account_config` VALUES ('acc004', '本机构', '客户折扣支出', '0', '2023-08-03 17:40:34');
INSERT INTO `account_config` VALUES ('acc005', '本机构', '存放供应商资金', '0', '2023-08-03 17:40:34');
INSERT INTO `account_config` VALUES ('acc006', '本机构', '分销商代付费销售', '0', '2023-08-03 17:40:34');
INSERT INTO `account_config` VALUES ('acc007', '本机构', '支付宝手续费', '0', '2023-08-03 17:40:34');
INSERT INTO `account_config` VALUES ('acc008', '客户', '业务账', '0', '2023-08-03 17:40:34');
INSERT INTO `account_config` VALUES ('acc009', '供应商', '待结转折扣销售收入', '0', '2023-08-03 17:40:34');
INSERT INTO `account_config` VALUES ('acc010', '供应商', '待结转代付费销售收入', '0', '2023-08-03 17:40:34');
INSERT INTO `account_config` VALUES ('acc011', '供应商', '待结转底价销售收入', '0', '2023-08-03 17:40:34');
INSERT INTO `account_config` VALUES ('acc012', '分销商', '分销商存放资金', '0', '2023-08-03 17:40:34');
INSERT INTO `account_config` VALUES ('acc013', '分销商', '待结转折扣支出', '0', '2023-08-03 17:40:34');
INSERT INTO `account_config` VALUES ('acc014', '分销商', '待结转代付费支出', '0', '2023-08-03 17:40:34');
INSERT INTO `account_config` VALUES ('acc015', '分销商', '待结转底价支出', '0', '2023-08-03 17:40:34');
INSERT INTO `account_config` VALUES ('acc016', '分销商', '代付费销售', '0', '2023-08-03 17:40:34');
INSERT INTO `account_config` VALUES ('acc017', '客户', '算力券', '0', '2025-03-31 11:36:11');
INSERT INTO `account_config` VALUES ('acc019', '算力券发行方', '算力券发放', '0', '2025-04-08 11:21:33');
INSERT INTO `account_config` VALUES ('acc020', '算力券发行方', '算力券消费', '0', '2025-04-08 11:21:43');
INSERT INTO `account_config` VALUES ('acc021', '算力券承销方', '待结算算力券', '0', '2025-04-08 11:29:56');
INSERT INTO `account_config` VALUES ('acc022', '本机构', '算力劵临时账户', '0', '2025-04-10 16:34:46');
INSERT INTO `account_config` VALUES ('acc023', '分销商', '算力劵临时账户', '0', '2025-04-10 16:34:55');
INSERT INTO `account_config` VALUES ('acc024', '供应商', '算力劵临时账户', '0', '2025-04-10 16:35:03');
SET FOREIGN_KEY_CHECKS = 1;

131
b/accounting_config.sql Normal file
View File

@ -0,0 +1,131 @@
/*
Navicat Premium Data Transfer
Source Server : kbossdev
Source Server Type : MariaDB
Source Server Version : 100622
Source Host : db:3306
Source Schema : kboss_dev
Target Server Type : MariaDB
Target Server Version : 100622
File Encoding : 65001
Date: 18/06/2026 17:11:43
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for accounting_config
-- ----------------------------
DROP TABLE IF EXISTS `accounting_config`;
CREATE TABLE `accounting_config` (
`id` varchar(32) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT 'id',
`action` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '交易',
`specstr` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '特性',
`accounting_orgtype` varchar(32) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '账务机构',
`accounting_dir` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '记账方向',
`orgtype` varchar(32) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '机构类型',
`subjectname` varchar(21) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '科目名',
`amt_pattern` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '金额模板',
`del_flg` varchar(1) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT '0' COMMENT '删除标志',
`create_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT '创建时间戳',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci COMMENT = '记账配置表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of accounting_config
-- ----------------------------
INSERT INTO `accounting_config` VALUES ('accc-01', '充值', '充值', '客户所在机构', '', '本机构', '资金账号', '${交易金额}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-02', '充值', '充值', '客户所在机构', '', '客户', '业务账', '${交易金额}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-03', '充值', '充值', '分销商机构', '', '本机构', '资金账号', '${交易金额}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-04', '充值', '充值', '分销商机构', '', '分销商', '分销商存放资金', '${交易金额}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-05', '交易', '付费-折扣', '客户所在机构', '', '客户', '业务账', '${交易金额}$ * ${客户折扣}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-06', '交易', '付费-折扣', '客户所在机构', '', '本机构', '折扣收入', '${交易金额}$ * (${客户折扣}$ - ${本方折扣}$)', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-07', '交易', '付费-折扣', '客户所在机构', '', '供应商', '待结转折扣销售收入', '${交易金额}$ * ${本方折扣}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-08', '交易', '付费-折扣-折扣', '分销商机构', '', '分销商', '分销商存放资金', '${交易金额}$ * ${分销商折扣}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-09', '交易', '付费-折扣-折扣', '分销商机构', '', '本机构', '折扣收入', '${交易金额}$ * (${分销商折扣}$ - ${本方折扣}$)', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-10', '交易', '付费-折扣-折扣', '分销商机构', '', '供应商', '待结转折扣销售收入', '${交易金额}$ * ${本方折扣}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-11', '交易', '付费-折扣-代付费', '分销商机构', '', '分销商', '分销商存放资金', '${交易金额}$ * ${客户折扣}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-12', '交易', '付费-折扣-代付费', '分销商机构', '', '本机构', '折扣收入', '${交易金额}$ * (${客户折扣}$ - ${本方折扣}$)', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-13', '交易', '付费-折扣-代付费', '分销商机构', '', '供应商', '待结转折扣销售收入', '${交易金额}$ * ${本方折扣}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-14', '交易', '付费-折扣-代付费', '分销商机构', '', '本机构', '分销商代付费销售', '${交易金额}$ * ${客户折扣}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-15', '交易', '付费-折扣-代付费', '分销商机构', '', '分销商', '代付费销售', '${交易金额}$ * ${客户折扣}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-16', '交易', '付费-代付费', '客户所在机构', '', '客户', '业务账', '${交易金额}$ * ${客户折扣}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-17', '交易', '付费-代付费', '客户所在机构', '', '本机构', '客户折扣支出', '${交易金额}$ * (1 - ${客户折扣}$)', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-18', '交易', '付费-代付费', '客户所在机构', '', '供应商', '待结转代付费销售收入', '${交易金额}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-19', '交易', '付费-代付费-代付费', '分销商机构', '', '分销商', '分销商存放资金', '${交易金额}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-20', '交易', '付费-代付费-代付费', '分销商机构', '', '供应商', '待结转代付费销售收入', '${交易金额}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-21', '交易', '付费-代付费-代付费', '分销商机构', '', '本机构', '分销商代付费销售', '${交易金额}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-22', '交易', '付费-代付费-代付费', '分销商机构', '', '分销商', '代付费销售', '${交易金额}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-23', '交易', '付费-底价', '客户所在机构', '', '客户', '业务账', '${客户售价}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-24', '交易', '付费-底价', '客户所在机构', '', '本机构', '底价收入', '${客户售价}$ - ${进价}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-25', '交易', '付费-底价', '客户所在机构', '', '供应商', '待结转底价销售收入', '${进价}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-26', '交易', '付费-底价-底价', '分销商机构', '', '分销商', '分销商存放资金', '${分销商售价}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-27', '交易', '付费-底价-底价', '分销商机构', '', '本机构', '底价收入', '${分销商售价}$ - ${进价}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-28', '交易', '付费-底价-底价', '分销商机构', '', '供应商', '待结转底价销售收入', '${进价}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-29', '交易', '付费-底价-底价', '分销商机构', '', '分销商', '分销商存放资金', '${客户售价}$ * ${分销商售价}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-30', '交易', '付费-底价-底价', '分销商机构', '', '本机构', '底价收入', '${客户售价}$ * ${分销商售价}$ - ${进价}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-31', '交易', '付费-底价-底价', '分销商机构', '', '供应商', '待结转底价销售收入', '${进价}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-32', '交易', '付费-底价-代付费', '分销商机构', '', '分销商', '分销商存放资金', '${客户售价}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-33', '交易', '付费-底价-代付费', '分销商机构', '', '本机构', '底价收入', '${客户售价}$ - ${进价}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-34', '交易', '付费-底价-代付费', '分销商机构', '', '供应商', '待结转底价销售收入', '${进价}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-35', '交易', '付费-底价-代付费', '分销商机构', '', '本机构', '分销商代付费销售', '${客户售价}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-36', '交易', '付费-底价-代付费', '分销商机构', '', '分销商', '代付费销售', '${客户售价}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-37', '结算', '结算-折扣-实时', NULL, '', '供应商', '待结转折扣销售收入', '${贷-供应商-待结转折扣销售收入}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-38', '结算', '结算-折扣-实时', NULL, '', '本机构', '资金账号', '${贷-供应商-待结转折扣销售收入}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-39', '结算', '结算-代付费-实时', NULL, '', '供应商', '待结转代付费销售收入', '${贷-供应商-待结转代付费销售收入}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-40', '结算', '结算-代付费-实时', NULL, '', '本机构', '资金账号', '${贷-供应商-待结转代付费销售收入}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-41', '结算', '结算-底价-实时', NULL, '', '供应商', '待结转底价销售收入', '${贷-供应商-待结转底价销售收入}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-42', '结算', '结算-底价-实时', NULL, '', '本机构', '资金账号', '${贷-供应商-待结转底价销售收入}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-43', '结算', '结算-折扣', NULL, '', '供应商', '待结转折扣销售收入', '${交易金额}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-44', '结算', '结算-折扣', NULL, '', '本机构', '资金账号', '${交易金额}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-45', '结算', '结算-代付费', NULL, '', '供应商', '待结转代付费销售收入', '${交易金额}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-46', '结算', '结算-代付费', NULL, '', '本机构', '资金账号', '${交易金额}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-47', '结算', '结算-底价', NULL, '', '供应商', '待结转底价销售收入', '${交易金额}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-48', '结算', '结算-底价', NULL, '', '本机构', '资金账号', '${交易金额}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-49', '充值', '支付宝充值', '客户所在机构', '', '本机构', '资金账号', '${交易金额}$ - ${手续费}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-50', '充值', '支付宝充值', '客户所在机构', '', '本机构', '支付宝手续费', '${手续费}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-51', '充值', '支付宝充值', '客户所在机构', '', '客户', '业务账', '${交易金额}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-52', '充值', '支付宝充值', '分销商机构', '', '本机构', '资金账号', '${交易金额}$ - ${手续费}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-53', '充值', '支付宝充值', '分销商机构', '', '分销商', '分销商存放资金', '${交易金额}$ - ${手续费}$', '0', '2023-08-03 17:40:34');
INSERT INTO `accounting_config` VALUES ('accc-54', '算力劵发行', '算力劵充值', '算力劵承销方', '', '算力劵发行方', '算力劵发行', '${交易金额}$', '0', '2025-04-08 17:33:03');
INSERT INTO `accounting_config` VALUES ('accc-55', '算力劵发行', '算力劵充值', '算力劵承销方', '', '本机构', '算力劵临时账户', '${交易金额}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-56', '算力劵发放', '算力劵充值', '分销商机构', '', '本机构', '算力劵临时账户', '${交易金额}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-57', '算力劵发放', '算力劵充值', '分销商机构', '', '分销商', '算力劵临时账户', '${交易金额}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-58', '算力劵发放', '算力劵充值', '客户所在机构', '', '本机构', '算力劵临时账户', '${交易金额}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-59', '算力劵发放', '算力劵充值', '客户所在机构', '', '客户', '算力劵', '${交易金额}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-60', '算力劵交易', '算力券-付费-折扣', '客户所在机构', '', '客户', '算力劵', '${交易金额}*${客户折扣}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-61', '算力劵交易', '算力券-付费-折扣', '客户所在机构', '', '本机构', '算力劵临时账户', '${交易金额}*${客户折扣}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-62', '算力劵交易', '算力券-付费-折扣', '客户所在机构', '', '供应商', '算力劵临时账户', '${交易金额}*${客户折扣}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-63', '算力劵交易', '算力券-付费-折扣', '客户所在机构', '', '本机构', '折扣收入', '${交易金额}*(${客户折扣}-${本方折扣})$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-64', '算力劵交易', '算力券-付费-折扣', '客户所在机构', '', '供应商', '待结转折扣销售收入', '${交易金额}$*${分销商折扣}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-65', '算力劵交易', '算力券-付费-折扣-折扣', '分销商机构', '', '本机构', '算力劵临时账户', '${交易金额}$*${客户折扣}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-66', '算力劵交易', '算力券-付费-折扣-折扣', '分销商机构', '', '供应商', '算力劵临时账户', '${交易金额}$*${客户折扣}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-67', '算力劵交易', '算力券-付费-折扣-折扣', '分销商机构', '', '本机构', '折扣收入', '${交易金额}$*(${分销商折扣}$ -${本方折扣}$)', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-68', '算力劵交易', '算力券-付费-折扣-折扣', '分销商机构', '', '供应商', '待结转折扣销售收入', '${交易金额}$*${本方折扣}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-69', '算力劵交易', '算力券-付费-折扣-折扣', '分销商机构', '', '分销商', '分销商存放资金', '${交易金额}$*${分销商折扣}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-70', '算力劵交易', '算力券-付费-底价', '客户所在机构', '', '客户', '算力劵', '${客户售价}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-71', '算力劵交易', '算力券-付费-底价', '客户所在机构', '', '本机构', '算力劵临时账户', '${客户售价}$ - ${进价}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-72', '算力劵交易', '算力券-付费-底价', '客户所在机构', '', '供应商', '算力劵临时账户', '${客户售价}$ - ${进价}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-73', '算力劵交易', '算力券-付费-底价', '客户所在机构', '', '本机构', '底价收入', '${客户售价}$ - ${进价}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-74', '算力劵交易', '算力券-付费-底价', '客户所在机构', '', '供应商', '待结转底价销售收入', '${进价}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-75', '算力劵交易', '算力券-付费-底价-底价', '分销商机构', '', '本机构', '算力劵临时账户', '${客户售价}$ - ${进价}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-76', '算力劵交易', '算力券-付费-底价-底价', '分销商机构', '', '供应商', '算力劵临时账户', '${进价}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-77', '算力劵交易', '算力券-付费-底价-底价', '分销商机构', '', '本机构', '底价收入', '${客户售价}$ - ${进价}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-78', '算力劵交易', '算力券-付费-底价-底价', '分销商机构', '', '供应商', '待结转底价销售收入', '${进价}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-79', '算力劵交易', '算力券-付费-底价-底价', '分销商机构', '', '分销商', '分销商存放资金', '${客户售价}$ * ${分销商售价}$ - ${进价}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-80', '算力劵交易', '算力券-付费-底价', '客户所在机构', '', '客户', '算力劵', '${交易金额}$ * ${客户折扣}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-81', '算力劵交易', '算力券-付费-底价', '客户所在机构', '', '本机构', '算力劵临时账户', '${交易金额}$ * ${客户折扣}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-82', '算力劵交易', '算力券-付费-代付费', '客户所在机构', '', '供应商', '算力劵临时账户', '${交易金额}$ * ${客户折扣}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-83', '算力劵交易', '算力券-付费-代付费', '客户所在机构', '', '本机构', '底价收入', '${交易金额}$ * (1 - ${客户折扣}$)', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-84', '算力劵交易', '算力券-付费-代付费', '客户所在机构', '', '供应商', '待结转底价销售收入', '${交易金额}$ * ${客户折扣}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-85', '算力劵交易', '算力券-付费-代付费-代付费', '分销商机构', '', '本机构', '算力劵临时账户', '${交易金额}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-86', '算力劵交易', '算力券-付费-代付费-代付费', '分销商机构', '', '供应商', '算力劵临时账户', '${交易金额}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-87', '算力劵交易', '算力券-付费-代付费-代付费', '分销商机构', '', '本机构', '底价收入', '${交易金额}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-88', '算力劵交易', '算力券-付费-代付费-代付费', '分销商机构', '', '供应商', '待结转底价销售收入', '${交易金额}$', '0', '2025-04-08 17:37:44');
INSERT INTO `accounting_config` VALUES ('accc-89', '算力劵交易', '算力券-付费-代付费-代付费', '分销商机构', '', '分销商', '分销商存放资金', '${交易金额}$', '0', '2025-04-08 17:37:44');
SET FOREIGN_KEY_CHECKS = 1;

View File

@ -2396,6 +2396,67 @@ CREATE TABLE `subject` (
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci COMMENT = '科目表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for finance_settlement
-- ----------------------------
DROP TABLE IF EXISTS `finance_settlement`;
CREATE TABLE `finance_settlement` (
`id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'id',
`settlement_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '结算单号',
`accounting_orgid` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账本机构id',
`counterparty_type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '对手方类型 supplier/reseller',
`counterparty_orgid` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '对手方机构id',
`counterparty_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '对手方名称',
`period_type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账期类型 day/month',
`period_start` date NULL DEFAULT NULL COMMENT '账期开始日期',
`period_end` date NULL DEFAULT NULL COMMENT '账期结束日期',
`sales_amount` double(18, 8) NULL DEFAULT 0.00000000 COMMENT '销售金额',
`settlement_amount` double(18, 8) NULL DEFAULT 0.00000000 COMMENT '结算金额',
`platform_income_amount` double(18, 8) NULL DEFAULT 0.00000000 COMMENT '平台收入',
`bill_count` int(11) NULL DEFAULT 0 COMMENT '账单数量',
`status` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'draft' COMMENT 'draft/approving/approved/rejected/settled/failed/cancelled',
`approval_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '审批id',
`approval_status` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '审批状态',
`failure_reason` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '失败原因',
`settled_at` datetime NULL DEFAULT NULL COMMENT '结算记账时间',
`created_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人',
`del_flg` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志',
`create_at` datetime NULL DEFAULT current_timestamp() COMMENT '创建时间',
`update_at` datetime NULL DEFAULT current_timestamp() COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `finance_settlement_un`(`accounting_orgid`, `counterparty_type`, `counterparty_orgid`, `period_start`, `period_end`, `del_flg`) USING BTREE,
INDEX `finance_settlement_idx1`(`accounting_orgid`, `status`) USING BTREE,
INDEX `finance_settlement_idx2`(`counterparty_type`, `counterparty_orgid`) USING BTREE,
INDEX `finance_settlement_idx3`(`period_start`, `period_end`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '财务结算单主表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for finance_settlement_detail
-- ----------------------------
DROP TABLE IF EXISTS `finance_settlement_detail`;
CREATE TABLE `finance_settlement_detail` (
`id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'id',
`settlement_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '结算单id',
`bill_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账单id',
`order_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单id',
`bill_date` date NULL DEFAULT NULL COMMENT '账单日期',
`customerid` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户id',
`providerid` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '供应商机构id',
`productid` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '产品id',
`business_op` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '业务操作',
`sale_mode` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '销售模式 0折扣/1代付费/2底价',
`sales_amount` double(18, 8) NULL DEFAULT 0.00000000 COMMENT '销售金额',
`settlement_amount` double(18, 8) NULL DEFAULT 0.00000000 COMMENT '结算金额',
`platform_income_amount` double(18, 8) NULL DEFAULT 0.00000000 COMMENT '平台收入',
`amount_source` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'bill_detail' COMMENT '金额来源',
`del_flg` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志',
`create_at` datetime NULL DEFAULT current_timestamp() COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `finance_settlement_detail_idx1`(`settlement_id`) USING BTREE,
INDEX `finance_settlement_detail_idx2`(`bill_id`) USING BTREE,
INDEX `finance_settlement_detail_idx3`(`providerid`, `productid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '财务结算单明细快照' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for transfer_record
-- ----------------------------

View File

@ -0,0 +1,301 @@
# -*- coding: utf-8 -*-
"""财务结算审批回调。"""
import datetime
DBNAME = 'kboss'
def _period_settle_mode(period_type):
if period_type == 'day':
return '1'
if period_type == 'month':
return '3'
return '0'
async def _business_date():
try:
return await get_business_date(sor=None)
except Exception:
return datetime.datetime.now().strftime('%Y-%m-%d')
async def _settle_supplier(sor, settlement):
rows = await sor.sqlExe("""
SELECT sale_mode, SUM(settlement_amount) AS amount
FROM finance_settlement_detail
WHERE settlement_id=${settlement_id}$
AND del_flg='0'
GROUP BY sale_mode
""", {'settlement_id': settlement['id']})
settle_date = await _business_date()
settle_mode = _period_settle_mode(settlement.get('period_type'))
failures = []
for row in rows:
amount = round(float(row.get('amount') or 0), 8)
if amount == 0:
continue
sale_mode = row.get('sale_mode') or '0'
try:
settle_log = {
'accounting_orgid': settlement.get('accounting_orgid'),
'providerid': settlement.get('counterparty_orgid'),
'settle_date': settle_date,
'settle_mode': settle_mode,
'sale_mode': sale_mode,
'settle_amt': amount,
'business_op': 'SETTLE',
}
ai = SettleAccounting(settle_log)
await ai.accounting(sor)
except Exception as e:
failures.append('sale_mode=%s: %s' % (sale_mode, str(e)))
if failures:
return False, '; '.join(failures)
return True, None
async def _get_account(sor, accounting_orgid, orgid, subjectname):
rows = await sor.sqlExe("""
SELECT a.*
FROM account a
INNER JOIN subject s ON a.subjectid=s.id
WHERE a.accounting_orgid=${accounting_orgid}$
AND a.orgid=${orgid}$
AND s.name=${subjectname}$
AND a.del_flg='0'
AND s.del_flg='0'
LIMIT 1
""", {
'accounting_orgid': accounting_orgid,
'orgid': orgid,
'subjectname': subjectname,
})
return rows[0] if rows else None
async def _latest_balance(sor, accountid):
rows = await sor.sqlExe("""
SELECT balance
FROM acc_balance
WHERE accountid=${accountid}$ AND del_flg='0'
ORDER BY acc_date DESC
LIMIT 1
""", {'accountid': accountid})
if not rows or rows[0].get('balance') is None:
return 0.0
return float(rows[0].get('balance') or 0)
def _next_balance(account, balance, acc_dir, amount):
balance_at = account.get('balance_at')
if (
(balance_at == '0' and acc_dir == '1')
or (balance_at == '1' and acc_dir == '0')
):
return balance - amount
return balance + amount
async def _write_balance(sor, accountid, acc_date, balance):
rows = await sor.sqlExe("""
SELECT id
FROM acc_balance
WHERE accountid=${accountid}$ AND acc_date=${acc_date}$ AND del_flg='0'
LIMIT 1
""", {'accountid': accountid, 'acc_date': acc_date})
if rows:
await sor.U('acc_balance', {
'id': rows[0]['id'],
'balance': balance,
})
else:
await sor.C('acc_balance', {
'id': uuid(),
'accountid': accountid,
'acc_date': acc_date,
'balance': balance,
'del_flg': '0',
'create_at': datetime.datetime.now(),
})
async def _write_reseller_leg(
sor,
billid,
acc_date,
accounting_orgid,
participantid,
participanttype,
subjectname,
accounting_dir,
amount,
description,
):
account = await _get_account(sor, accounting_orgid, participantid, subjectname)
if not account:
raise Exception(
'未找到账户 accounting_orgid=%s, orgid=%s, subject=%s'
% (accounting_orgid, participantid, subjectname)
)
acc_dir = '0' if accounting_dir == '借' else '1'
old_balance = await _latest_balance(sor, account['id'])
new_balance = _next_balance(account, old_balance, acc_dir, amount)
await _write_balance(sor, account['id'], acc_date, new_balance)
await sor.C('bill_detail', {
'id': uuid(),
'accounting_orgid': accounting_orgid,
'billid': billid,
'description': description,
'participantid': participantid,
'participanttype': participanttype,
'subjectname': subjectname,
'accounting_dir': accounting_dir,
'amount': amount,
'del_flg': '0',
'create_at': datetime.datetime.now(),
})
logid = uuid()
await sor.C('accounting_log', {
'id': logid,
'accountid': account['id'],
'acc_date': acc_date,
'acc_timestamp': datetime.datetime.now(),
'acc_dir': acc_dir,
'summary': 'SETTLE',
'amount': amount,
'billid': billid,
'del_flg': '0',
'create_at': datetime.datetime.now(),
})
await sor.C('acc_detail', {
'id': uuid(),
'accountid': account['id'],
'acc_date': acc_date,
'acc_timestamp': datetime.datetime.now(),
'acc_dir': acc_dir,
'summary': 'SETTLE',
'amount': amount,
'balance': new_balance,
'acclogid': logid,
'del_flg': '0',
'create_at': datetime.datetime.now(),
})
async def _settle_reseller(sor, settlement):
amount = round(float(settlement.get('settlement_amount') or 0), 8)
if amount == 0:
return True, None
if amount < 0:
return False, '分销商结算金额为负数,需先人工确认红冲口径'
accounting_orgid = settlement.get('accounting_orgid')
reseller_orgid = settlement.get('counterparty_orgid')
settle_date = await _business_date()
billid = uuid()
description = '分销商结算-%s' % settlement.get('settlement_no')
await sor.C('bill', {
'id': billid,
'customerid': reseller_orgid,
'business_op': 'SETTLE',
'amount': amount,
'bill_date': settle_date,
'bill_timestamp': datetime.datetime.now(),
'bill_state': '1',
'del_flg': '0',
'create_at': datetime.datetime.now(),
})
# 结算分录:借记分销商存放资金,贷记本机构资金账号。
await _write_reseller_leg(
sor,
billid,
settle_date,
accounting_orgid,
reseller_orgid,
'分销商',
'分销商存放资金',
'借',
amount,
description,
)
await _write_reseller_leg(
sor,
billid,
settle_date,
accounting_orgid,
accounting_orgid,
'本机构',
'资金账号',
'贷',
amount,
description,
)
return True, None
async def finance_settlement_apv_callback(ns={}):
approval_id = ns.get('approval_id') or ns.get('apv_id')
status = ns.get('status')
if not approval_id or not status:
return {'status': False, 'msg': '缺少 approval_id/apv_id 或 status'}
status_map = {
'start': 'approving',
'agree': 'approved',
'refuse': 'rejected',
'terminate': 'cancelled',
}
next_status = status_map.get(status, status)
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
try:
rows = await sor.R('finance_settlement', {'approval_id': approval_id, 'del_flg': '0'})
if not rows:
return {'status': False, 'msg': '未找到结算审批数据'}
now_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
results = []
for settlement in rows:
update_data = {
'id': settlement['id'],
'approval_status': status,
'status': next_status,
'update_at': now_time,
}
if status != 'agree':
await sor.U('finance_settlement', update_data)
results.append({'settlement_id': settlement['id'], 'status': next_status})
continue
if settlement.get('status') == 'settled':
results.append({'settlement_id': settlement['id'], 'status': 'settled'})
continue
if settlement.get('counterparty_type') == 'supplier':
ok, err = await _settle_supplier(sor, settlement)
else:
ok, err = await _settle_reseller(sor, settlement)
if ok:
update_data['status'] = 'settled'
update_data['settled_at'] = now_time
update_data['failure_reason'] = None
results.append({'settlement_id': settlement['id'], 'status': 'settled'})
else:
update_data['status'] = 'failed'
update_data['failure_reason'] = err
results.append({
'settlement_id': settlement['id'],
'status': 'failed',
'failure_reason': err,
})
await sor.U('finance_settlement', update_data)
return {'status': True, 'msg': 'ok', 'data': results}
except Exception as e:
return {'status': False, 'msg': '审批回调处理失败, %s' % str(e)}
ret = await finance_settlement_apv_callback(params_kw)
return ret

View File

@ -0,0 +1,283 @@
# -*- coding: utf-8 -*-
"""创建财务结算单并保存明细快照。"""
import datetime
DBNAME = 'kboss'
def _round_money(v):
return round(float(v or 0), 8)
def _is_reverse_op(op):
return str(op or '').upper().endswith('_REVERSE')
def _sale_mode_from_subject(subjectname):
text = str(subjectname or '')
if '代付费' in text or '返佣' in text:
return '1'
if '底价' in text or '低价' in text:
return '2'
return '0'
def _validate_params(ns):
for key in ('accounting_orgid', 'counterparty_type', 'counterparty_orgid'):
if not ns.get(key):
return None, '缺少 %s' % key
if ns.get('counterparty_type') not in ('supplier', 'reseller'):
return None, 'counterparty_type 仅支持 supplier / reseller'
period_type = ns.get('period_type') or 'day'
if period_type not in ('day', 'month'):
return None, 'period_type 仅支持 day / month'
period_start = ns.get('period_start') or ns.get('start_date')
period_end = ns.get('period_end') or ns.get('end_date')
if not period_start or not period_end:
return None, '缺少 period_start / period_end'
return {
'accounting_orgid': ns.get('accounting_orgid'),
'counterparty_type': ns.get('counterparty_type'),
'counterparty_orgid': ns.get('counterparty_orgid'),
'period_type': period_type,
'period_start': period_start,
'period_end': period_end,
'userid': ns.get('userid') or ns.get('user_id'),
}, None
async def _ensure_schema(sor):
await sor.sqlExe("""
CREATE TABLE IF NOT EXISTS finance_settlement (
id varchar(32) NOT NULL,
settlement_no varchar(64),
accounting_orgid varchar(32),
counterparty_type varchar(16),
counterparty_orgid varchar(32),
counterparty_name varchar(255),
period_type varchar(16),
period_start date,
period_end date,
sales_amount double(18,8) DEFAULT 0,
settlement_amount double(18,8) DEFAULT 0,
platform_income_amount double(18,8) DEFAULT 0,
bill_count int DEFAULT 0,
status varchar(16) DEFAULT 'draft',
approval_id varchar(64),
approval_status varchar(16),
failure_reason varchar(1000),
settled_at datetime,
created_by varchar(32),
del_flg varchar(1) DEFAULT '0',
create_at datetime DEFAULT CURRENT_TIMESTAMP,
update_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY finance_settlement_un (accounting_orgid, counterparty_type, counterparty_orgid, period_start, period_end, del_flg)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""", {})
await sor.sqlExe("""
CREATE TABLE IF NOT EXISTS finance_settlement_detail (
id varchar(32) NOT NULL,
settlement_id varchar(32),
bill_id varchar(32),
order_id varchar(32),
bill_date date,
customerid varchar(32),
providerid varchar(32),
productid varchar(32),
business_op varchar(64),
sale_mode varchar(1),
sales_amount double(18,8) DEFAULT 0,
settlement_amount double(18,8) DEFAULT 0,
platform_income_amount double(18,8) DEFAULT 0,
amount_source varchar(32) DEFAULT 'bill_detail',
del_flg varchar(1) DEFAULT '0',
create_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY finance_settlement_detail_idx1 (settlement_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""", {})
async def _existing_settlement(sor, args):
rows = await sor.sqlExe("""
SELECT id, settlement_no, status
FROM finance_settlement
WHERE accounting_orgid=${accounting_orgid}$
AND counterparty_type=${counterparty_type}$
AND counterparty_orgid=${counterparty_orgid}$
AND period_start=${period_start}$
AND period_end=${period_end}$
AND del_flg='0'
LIMIT 1
""", args)
return rows[0] if rows else None
async def _counterparty_name(sor, args):
if args['counterparty_type'] == 'supplier':
rows = await sor.sqlExe("""
SELECT COALESCE(p.name, o.orgname) AS name
FROM organization o
LEFT JOIN provider p ON p.orgid=o.id AND p.del_flg='0'
WHERE o.id=${counterparty_orgid}$ AND o.del_flg='0'
LIMIT 1
""", args)
else:
rows = await sor.sqlExe("""
SELECT orgname AS name FROM organization
WHERE id=${counterparty_orgid}$ AND del_flg='0'
LIMIT 1
""", args)
return rows[0].get('name') if rows else None
async def _fetch_source_rows(sor, args):
filters = [
"b.del_flg='0'",
"b.bill_state='1'",
"b.bill_date >= ${period_start}$",
"b.bill_date <= ${period_end}$",
]
if args['counterparty_type'] == 'supplier':
filters.append('b.providerid=${counterparty_orgid}$')
settlement_join = """
LEFT JOIN bill_detail sd ON sd.billid=b.id
AND sd.accounting_orgid=${accounting_orgid}$
AND sd.del_flg='0'
AND sd.accounting_dir='贷'
AND sd.subjectname LIKE '待结转%'
"""
else:
filters.append('cust.parentid=${counterparty_orgid}$')
settlement_join = """
LEFT JOIN bill_detail sd ON sd.billid=b.id
AND sd.accounting_orgid=${accounting_orgid}$
AND sd.del_flg='0'
AND sd.accounting_dir='借'
AND sd.subjectname='分销商存放资金'
AND sd.participantid=cust.parentid
"""
sql = """
SELECT b.id AS bill_id, b.orderid AS order_id, b.bill_date,
b.customerid, b.providerid, b.productid, b.business_op,
b.amount AS sales_amount,
COALESCE(sd.amount, 0) AS settlement_amount,
sd.subjectname AS settlement_subject,
COALESCE((
SELECT SUM(bd.amount)
FROM bill_detail bd
WHERE bd.billid=b.id
AND bd.accounting_orgid=${accounting_orgid}$
AND bd.del_flg='0'
AND bd.accounting_dir='贷'
AND bd.subjectname IN ('折扣收入', '底价收入')
), 0) AS platform_income_amount
FROM bill b
INNER JOIN organization cust ON cust.id=b.customerid AND cust.del_flg='0'
%s
WHERE %s
ORDER BY b.bill_date DESC, b.create_at DESC
""" % (settlement_join, ' AND '.join(filters))
rows = await sor.sqlExe(sql, args)
items = []
seen_platform_bill = set()
for row in rows:
sign = -1 if _is_reverse_op(row.get('business_op')) else 1
bill_id = row.get('bill_id')
platform_income = float(row.get('platform_income_amount') or 0)
if bill_id in seen_platform_bill:
platform_income = 0
seen_platform_bill.add(bill_id)
items.append({
'bill_id': bill_id,
'order_id': row.get('order_id'),
'bill_date': str(row.get('bill_date'))[:10] if row.get('bill_date') else None,
'customerid': row.get('customerid'),
'providerid': row.get('providerid'),
'productid': row.get('productid'),
'business_op': row.get('business_op'),
'sale_mode': _sale_mode_from_subject(row.get('settlement_subject')),
'sales_amount': _round_money(sign * float(row.get('sales_amount') or 0)),
'settlement_amount': _round_money(sign * float(row.get('settlement_amount') or 0)),
'platform_income_amount': _round_money(sign * platform_income),
'amount_source': 'bill_detail',
})
return items
def _summary(items):
return {
'sales_amount': _round_money(sum(i.get('sales_amount') or 0 for i in items)),
'settlement_amount': _round_money(sum(i.get('settlement_amount') or 0 for i in items)),
'platform_income_amount': _round_money(sum(i.get('platform_income_amount') or 0 for i in items)),
'bill_count': len({i.get('bill_id') for i in items if i.get('bill_id')}),
}
async def finance_settlement_create(ns={}):
args, err = _validate_params(ns)
if err:
return {'status': False, 'msg': err}
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
try:
await _ensure_schema(sor)
existing = await _existing_settlement(sor, args)
if existing:
return {'status': False, 'msg': '该账期已存在结算单', 'data': existing}
items = await _fetch_source_rows(sor, args)
if not items:
return {'status': False, 'msg': '该账期无可结算已记账账单'}
summary = _summary(items)
settlement_id = uuid()
settlement_no = 'FS%s%s' % (
datetime.datetime.now().strftime('%Y%m%d%H%M%S'),
str(settlement_id)[-6:],
)
master = {
'id': settlement_id,
'settlement_no': settlement_no,
'accounting_orgid': args['accounting_orgid'],
'counterparty_type': args['counterparty_type'],
'counterparty_orgid': args['counterparty_orgid'],
'counterparty_name': await _counterparty_name(sor, args),
'period_type': args['period_type'],
'period_start': args['period_start'],
'period_end': args['period_end'],
'sales_amount': summary['sales_amount'],
'settlement_amount': summary['settlement_amount'],
'platform_income_amount': summary['platform_income_amount'],
'bill_count': summary['bill_count'],
'status': 'draft',
'created_by': args.get('userid'),
'del_flg': '0',
'create_at': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'update_at': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
}
await sor.C('finance_settlement', master)
for item in items:
detail = dict(item)
detail['id'] = uuid()
detail['settlement_id'] = settlement_id
detail['del_flg'] = '0'
detail['create_at'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
await sor.C('finance_settlement_detail', detail)
return {
'status': True,
'msg': 'ok',
'data': {
'settlement_id': settlement_id,
'settlement_no': settlement_no,
'status': 'draft',
'summary': summary,
},
}
except Exception as e:
return {'status': False, 'msg': '创建结算单失败, %s' % str(e)}
ret = await finance_settlement_create(params_kw)
return ret

View File

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
"""财务结算单详情查询。"""
DBNAME = 'kboss'
def _parse_page(ns, default_page=1, default_size=100, max_size=500):
try:
current_page = int(ns.get('current_page', default_page) or default_page)
except (TypeError, ValueError):
current_page = default_page
try:
page_size = int(ns.get('page_size', default_size) or default_size)
except (TypeError, ValueError):
page_size = default_size
current_page = max(1, current_page)
page_size = max(1, min(page_size, max_size))
return current_page, page_size, (current_page - 1) * page_size
async def finance_settlement_detail(ns={}):
settlement_id = ns.get('settlement_id') or ns.get('id')
if not settlement_id:
return {'status': False, 'msg': '缺少 settlement_id'}
current_page, page_size, offset = _parse_page(ns)
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
try:
rows = await sor.R('finance_settlement', {'id': settlement_id, 'del_flg': '0'})
if not rows:
return {'status': False, 'msg': '结算单不存在'}
count_rows = await sor.sqlExe("""
SELECT COUNT(*) AS total_count
FROM finance_settlement_detail
WHERE settlement_id=${settlement_id}$ AND del_flg='0'
""", {'settlement_id': settlement_id})
total_count = count_rows[0]['total_count'] if count_rows else 0
detail_rows = await sor.sqlExe("""
SELECT *
FROM finance_settlement_detail
WHERE settlement_id=${settlement_id}$ AND del_flg='0'
ORDER BY bill_date DESC, create_at DESC
LIMIT %d OFFSET %d
""" % (page_size, offset), {'settlement_id': settlement_id})
return {
'status': True,
'msg': 'ok',
'data': {
'settlement': rows[0],
'detail_total_count': total_count,
'current_page': current_page,
'page_size': page_size,
'items': detail_rows,
},
}
except Exception as e:
return {'status': False, 'msg': '查询结算单详情失败, %s' % str(e)}
ret = await finance_settlement_detail(params_kw)
return ret

View File

@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
"""财务结算单列表查询。"""
DBNAME = 'kboss'
def _parse_page(ns, default_page=1, default_size=20, max_size=200):
try:
current_page = int(ns.get('current_page', default_page) or default_page)
except (TypeError, ValueError):
current_page = default_page
try:
page_size = int(ns.get('page_size', default_size) or default_size)
except (TypeError, ValueError):
page_size = default_size
current_page = max(1, current_page)
page_size = max(1, min(page_size, max_size))
return current_page, page_size, (current_page - 1) * page_size
async def finance_settlement_list(ns={}):
accounting_orgid = ns.get('accounting_orgid')
if not accounting_orgid:
return {'status': False, 'msg': '缺少 accounting_orgid'}
current_page, page_size, offset = _parse_page(ns)
conditions = ["accounting_orgid=${accounting_orgid}$", "del_flg='0'"]
params = {'accounting_orgid': accounting_orgid}
for key in ('counterparty_type', 'counterparty_orgid', 'status', 'period_type'):
if ns.get(key):
conditions.append('%s=${%s}$' % (key, key))
params[key] = ns[key]
if ns.get('start_date'):
conditions.append('period_end >= ${start_date}$')
params['start_date'] = ns['start_date']
if ns.get('end_date'):
conditions.append('period_start <= ${end_date}$')
params['end_date'] = ns['end_date']
where_sql = ' AND '.join(conditions)
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
try:
count_rows = await sor.sqlExe(
'SELECT COUNT(*) AS total_count FROM finance_settlement WHERE %s' % where_sql,
params,
)
total_count = count_rows[0]['total_count'] if count_rows else 0
sql = """
SELECT *
FROM finance_settlement
WHERE %s
ORDER BY period_end DESC, create_at DESC
LIMIT %d OFFSET %d
""" % (where_sql, page_size, offset)
rows = await sor.sqlExe(sql, params)
return {
'status': True,
'msg': 'ok',
'data': {
'total_count': total_count,
'current_page': current_page,
'page_size': page_size,
'items': rows,
},
}
except Exception as e:
return {'status': False, 'msg': '查询结算单失败, %s' % str(e)}
ret = await finance_settlement_list(params_kw)
return ret

View File

@ -0,0 +1,89 @@
/*
SQL
1. finance_settlement
2. finance_settlement_detail
3. sale_mode
- provider_settle_data
-
account(accounting_orgid=, orgid=, subject=)
account(accounting_orgid=, orgid=, subject=)
*/
CREATE TABLE IF NOT EXISTS `finance_settlement` (
`id` varchar(32) NOT NULL COMMENT 'id',
`settlement_no` varchar(64) DEFAULT NULL COMMENT '结算单号',
`accounting_orgid` varchar(32) DEFAULT NULL COMMENT '账本机构id',
`counterparty_type` varchar(16) DEFAULT NULL COMMENT '对手方类型 supplier/reseller',
`counterparty_orgid` varchar(32) DEFAULT NULL COMMENT '对手方机构id',
`counterparty_name` varchar(255) DEFAULT NULL COMMENT '对手方名称',
`period_type` varchar(16) DEFAULT NULL COMMENT '账期类型 day/month',
`period_start` date DEFAULT NULL COMMENT '账期开始日期',
`period_end` date DEFAULT NULL COMMENT '账期结束日期',
`sales_amount` double(18,8) DEFAULT 0.00000000 COMMENT '销售金额',
`settlement_amount` double(18,8) DEFAULT 0.00000000 COMMENT '结算金额',
`platform_income_amount` double(18,8) DEFAULT 0.00000000 COMMENT '平台收入',
`bill_count` int(11) DEFAULT 0 COMMENT '账单数量',
`status` varchar(16) DEFAULT 'draft' COMMENT 'draft/approving/approved/rejected/settled/failed/cancelled',
`approval_id` varchar(64) DEFAULT NULL COMMENT '审批id',
`approval_status` varchar(16) DEFAULT NULL COMMENT '审批状态',
`failure_reason` varchar(1000) DEFAULT NULL COMMENT '失败原因',
`settled_at` datetime DEFAULT NULL COMMENT '结算记账时间',
`created_by` varchar(32) DEFAULT NULL COMMENT '创建人',
`del_flg` varchar(1) DEFAULT '0' COMMENT '删除标志',
`create_at` datetime DEFAULT current_timestamp() COMMENT '创建时间',
`update_at` datetime DEFAULT current_timestamp() COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `finance_settlement_un` (`accounting_orgid`, `counterparty_type`, `counterparty_orgid`, `period_start`, `period_end`, `del_flg`),
KEY `finance_settlement_idx1` (`accounting_orgid`, `status`),
KEY `finance_settlement_idx2` (`counterparty_type`, `counterparty_orgid`),
KEY `finance_settlement_idx3` (`period_start`, `period_end`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='财务结算单主表';
CREATE TABLE IF NOT EXISTS `finance_settlement_detail` (
`id` varchar(32) NOT NULL COMMENT 'id',
`settlement_id` varchar(32) DEFAULT NULL COMMENT '结算单id',
`bill_id` varchar(32) DEFAULT NULL COMMENT '账单id',
`order_id` varchar(32) DEFAULT NULL COMMENT '订单id',
`bill_date` date DEFAULT NULL COMMENT '账单日期',
`customerid` varchar(32) DEFAULT NULL COMMENT '客户id',
`providerid` varchar(32) DEFAULT NULL COMMENT '供应商机构id',
`productid` varchar(32) DEFAULT NULL COMMENT '产品id',
`business_op` varchar(64) DEFAULT NULL COMMENT '业务操作',
`sale_mode` varchar(1) DEFAULT NULL COMMENT '销售模式 0折扣/1代付费/2底价',
`sales_amount` double(18,8) DEFAULT 0.00000000 COMMENT '销售金额',
`settlement_amount` double(18,8) DEFAULT 0.00000000 COMMENT '结算金额',
`platform_income_amount` double(18,8) DEFAULT 0.00000000 COMMENT '平台收入',
`amount_source` varchar(32) DEFAULT 'bill_detail' COMMENT '金额来源',
`del_flg` varchar(1) DEFAULT '0' COMMENT '删除标志',
`create_at` datetime DEFAULT current_timestamp() COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `finance_settlement_detail_idx1` (`settlement_id`),
KEY `finance_settlement_detail_idx2` (`bill_id`),
KEY `finance_settlement_detail_idx3` (`providerid`, `productid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='财务结算单明细快照';
ALTER TABLE `finance_settlement_detail`
ADD COLUMN IF NOT EXISTS `sale_mode` varchar(1) DEFAULT NULL COMMENT '销售模式 0折扣/1代付费/2底价' AFTER `business_op`;
-- 执行分销商结算前,可用以下 SQL 检查目标账户是否存在:
-- SELECT a.*
-- FROM account a
-- JOIN subject s ON a.subjectid=s.id
-- WHERE a.accounting_orgid='<上级机构orgid>'
-- AND a.orgid='<分销商orgid>'
-- AND s.name='分销商存放资金'
-- AND a.del_flg='0'
-- AND s.del_flg='0';
--
-- SELECT a.*
-- FROM account a
-- JOIN subject s ON a.subjectid=s.id
-- WHERE a.accounting_orgid='<上级机构orgid>'
-- AND a.orgid='<上级机构orgid>'
-- AND s.name='资金账号'
-- AND a.del_flg='0'
-- AND s.del_flg='0';

View File

@ -0,0 +1,281 @@
# -*- coding: utf-8 -*-
"""财务结算单生成前预览。"""
DBNAME = 'kboss'
def _round_money(v):
return round(float(v or 0), 8)
def _is_reverse_op(op):
return str(op or '').upper().endswith('_REVERSE')
def _sale_mode_from_subject(subjectname):
text = str(subjectname or '')
if '代付费' in text or '返佣' in text:
return '1'
if '底价' in text or '低价' in text:
return '2'
return '0'
def _parse_page(ns, default_page=1, default_size=50, max_size=500):
try:
current_page = int(ns.get('current_page', default_page) or default_page)
except (TypeError, ValueError):
current_page = default_page
try:
page_size = int(ns.get('page_size', default_size) or default_size)
except (TypeError, ValueError):
page_size = default_size
current_page = max(1, current_page)
page_size = max(1, min(page_size, max_size))
return current_page, page_size, (current_page - 1) * page_size
def _validate_params(ns):
required = ('accounting_orgid', 'counterparty_type', 'counterparty_orgid')
for key in required:
if not ns.get(key):
return None, '缺少 %s' % key
counterparty_type = ns.get('counterparty_type')
if counterparty_type not in ('supplier', 'reseller'):
return None, 'counterparty_type 仅支持 supplier / reseller'
period_type = ns.get('period_type') or 'day'
if period_type not in ('day', 'month'):
return None, 'period_type 仅支持 day / month'
period_start = ns.get('period_start') or ns.get('start_date')
period_end = ns.get('period_end') or ns.get('end_date')
if not period_start or not period_end:
return None, '缺少 period_start / period_end'
return {
'accounting_orgid': ns.get('accounting_orgid'),
'counterparty_type': counterparty_type,
'counterparty_orgid': ns.get('counterparty_orgid'),
'period_type': period_type,
'period_start': period_start,
'period_end': period_end,
}, None
async def _ensure_schema(sor):
await sor.sqlExe("""
CREATE TABLE IF NOT EXISTS finance_settlement (
id varchar(32) NOT NULL,
settlement_no varchar(64),
accounting_orgid varchar(32),
counterparty_type varchar(16),
counterparty_orgid varchar(32),
counterparty_name varchar(255),
period_type varchar(16),
period_start date,
period_end date,
sales_amount double(18,8) DEFAULT 0,
settlement_amount double(18,8) DEFAULT 0,
platform_income_amount double(18,8) DEFAULT 0,
bill_count int DEFAULT 0,
status varchar(16) DEFAULT 'draft',
approval_id varchar(64),
approval_status varchar(16),
failure_reason varchar(1000),
settled_at datetime,
created_by varchar(32),
del_flg varchar(1) DEFAULT '0',
create_at datetime DEFAULT CURRENT_TIMESTAMP,
update_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY finance_settlement_un (accounting_orgid, counterparty_type, counterparty_orgid, period_start, period_end, del_flg)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""", {})
await sor.sqlExe("""
CREATE TABLE IF NOT EXISTS finance_settlement_detail (
id varchar(32) NOT NULL,
settlement_id varchar(32),
bill_id varchar(32),
order_id varchar(32),
bill_date date,
customerid varchar(32),
providerid varchar(32),
productid varchar(32),
business_op varchar(64),
sale_mode varchar(1),
sales_amount double(18,8) DEFAULT 0,
settlement_amount double(18,8) DEFAULT 0,
platform_income_amount double(18,8) DEFAULT 0,
amount_source varchar(32) DEFAULT 'bill_detail',
del_flg varchar(1) DEFAULT '0',
create_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY finance_settlement_detail_idx1 (settlement_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""", {})
async def _existing_settlement(sor, args):
rows = await sor.sqlExe("""
SELECT id, settlement_no, status
FROM finance_settlement
WHERE accounting_orgid=${accounting_orgid}$
AND counterparty_type=${counterparty_type}$
AND counterparty_orgid=${counterparty_orgid}$
AND period_start=${period_start}$
AND period_end=${period_end}$
AND del_flg='0'
LIMIT 1
""", args)
return rows[0] if rows else None
async def _counterparty_name(sor, args):
if args['counterparty_type'] == 'supplier':
rows = await sor.sqlExe("""
SELECT COALESCE(p.name, o.orgname) AS name
FROM organization o
LEFT JOIN provider p ON p.orgid=o.id AND p.del_flg='0'
WHERE o.id=${counterparty_orgid}$ AND o.del_flg='0'
LIMIT 1
""", args)
else:
rows = await sor.sqlExe("""
SELECT orgname AS name FROM organization
WHERE id=${counterparty_orgid}$ AND del_flg='0'
LIMIT 1
""", args)
return rows[0].get('name') if rows else None
async def _fetch_source_rows(sor, args):
params = dict(args)
filters = [
"b.del_flg='0'",
"b.bill_state='1'",
"b.bill_date >= ${period_start}$",
"b.bill_date <= ${period_end}$",
]
if args['counterparty_type'] == 'supplier':
filters.append('b.providerid=${counterparty_orgid}$')
settlement_join = """
LEFT JOIN bill_detail sd ON sd.billid=b.id
AND sd.accounting_orgid=${accounting_orgid}$
AND sd.del_flg='0'
AND sd.accounting_dir='贷'
AND sd.subjectname LIKE '待结转%'
"""
settlement_expr = 'COALESCE(sd.amount, 0)'
sale_mode_expr = 'sd.subjectname'
else:
filters.append('cust.parentid=${counterparty_orgid}$')
settlement_join = """
LEFT JOIN bill_detail sd ON sd.billid=b.id
AND sd.accounting_orgid=${accounting_orgid}$
AND sd.del_flg='0'
AND sd.accounting_dir='借'
AND sd.subjectname='分销商存放资金'
AND sd.participantid=cust.parentid
"""
settlement_expr = 'COALESCE(sd.amount, 0)'
sale_mode_expr = 'sd.subjectname'
where_sql = ' AND '.join(filters)
sql = """
SELECT b.id AS bill_id,
b.orderid AS order_id,
b.bill_date,
b.customerid,
b.providerid,
b.productid,
b.business_op,
b.amount AS sales_amount,
%s AS settlement_amount,
%s AS settlement_subject,
COALESCE((
SELECT SUM(bd.amount)
FROM bill_detail bd
WHERE bd.billid=b.id
AND bd.accounting_orgid=${accounting_orgid}$
AND bd.del_flg='0'
AND bd.accounting_dir='贷'
AND bd.subjectname IN ('折扣收入', '底价收入')
), 0) AS platform_income_amount
FROM bill b
INNER JOIN organization cust ON cust.id=b.customerid AND cust.del_flg='0'
%s
WHERE %s
ORDER BY b.bill_date DESC, b.create_at DESC
""" % (settlement_expr, sale_mode_expr, settlement_join, where_sql)
rows = await sor.sqlExe(sql, params)
items = []
seen_platform_bill = set()
for row in rows:
sign = -1 if _is_reverse_op(row.get('business_op')) else 1
bill_id = row.get('bill_id')
platform_income = float(row.get('platform_income_amount') or 0)
if bill_id in seen_platform_bill:
platform_income = 0
seen_platform_bill.add(bill_id)
items.append({
'bill_id': bill_id,
'order_id': row.get('order_id'),
'bill_date': str(row.get('bill_date'))[:10] if row.get('bill_date') else None,
'customerid': row.get('customerid'),
'providerid': row.get('providerid'),
'productid': row.get('productid'),
'business_op': row.get('business_op'),
'sale_mode': _sale_mode_from_subject(row.get('settlement_subject')),
'sales_amount': _round_money(sign * float(row.get('sales_amount') or 0)),
'settlement_amount': _round_money(sign * float(row.get('settlement_amount') or 0)),
'platform_income_amount': _round_money(sign * platform_income),
'amount_source': 'bill_detail',
})
return items
def _summary(items):
return {
'sales_amount': _round_money(sum(i.get('sales_amount') or 0 for i in items)),
'settlement_amount': _round_money(sum(i.get('settlement_amount') or 0 for i in items)),
'platform_income_amount': _round_money(sum(i.get('platform_income_amount') or 0 for i in items)),
'bill_count': len({i.get('bill_id') for i in items if i.get('bill_id')}),
}
async def finance_settlement_preview(ns={}):
args, err = _validate_params(ns)
if err:
return {'status': False, 'msg': err}
current_page, page_size, offset = _parse_page(ns)
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
try:
await _ensure_schema(sor)
existing = await _existing_settlement(sor, args)
items = await _fetch_source_rows(sor, args)
total_count = len(items)
return {
'status': True,
'msg': 'ok',
'data': {
'accounting_orgid': args['accounting_orgid'],
'counterparty_type': args['counterparty_type'],
'counterparty_orgid': args['counterparty_orgid'],
'counterparty_name': await _counterparty_name(sor, args),
'period_type': args['period_type'],
'period_start': args['period_start'],
'period_end': args['period_end'],
'existing_settlement': existing,
'can_create': existing is None and total_count > 0,
'summary': _summary(items),
'total_count': total_count,
'current_page': current_page,
'page_size': page_size,
'items': items[offset:offset + page_size],
},
}
except Exception as e:
return {'status': False, 'msg': '预览失败, %s' % str(e)}
ret = await finance_settlement_preview(params_kw)
return ret

View File

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
"""财务结算单提交审批。"""
import datetime
import json
DBNAME = 'kboss'
async def _finance_phone(sor, orgid):
rows = await sor.sqlExe("""
SELECT u.mobile
FROM users u
LEFT JOIN userrole ur ON u.id=ur.userid
LEFT JOIN role r ON ur.roleid=r.id
WHERE r.role='财务'
AND u.orgid=${orgid}$
AND ur.del_flg='0'
AND r.del_flg='0'
AND u.del_flg='0'
LIMIT 1
""", {'orgid': orgid})
return rows[0]['mobile'] if rows else None
async def finance_settlement_submit(ns={}):
settlement_id = ns.get('settlement_id') or ns.get('id')
userid = ns.get('userid') or ns.get('user_id')
business_name = ns.get('business_name') or '财务结算'
if not settlement_id or not userid:
return {'status': False, 'msg': '缺少 settlement_id 或 userid'}
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
try:
rows = await sor.R('finance_settlement', {'id': settlement_id, 'del_flg': '0'})
if not rows:
return {'status': False, 'msg': '结算单不存在'}
settlement = rows[0]
if settlement.get('status') not in ('draft', 'rejected', 'failed'):
return {'status': False, 'msg': '当前状态不可提交审批'}
phone = await _finance_phone(sor, settlement.get('accounting_orgid'))
if not phone:
return {'status': False, 'msg': '未找到当前机构财务审批人'}
business_rows = await sor.R('apv_business', {'business_name': business_name, 'del_flg': '0'})
if not business_rows:
return {'status': False, 'msg': '审批业务不存在: %s' % business_name}
form_component = {
'title': '财务结算审批',
'detail': {
'data': '%s %s-%s %s 金额:%s' % (
settlement.get('counterparty_name'),
settlement.get('period_start'),
settlement.get('period_end'),
settlement.get('counterparty_type'),
settlement.get('settlement_amount'),
)
},
}
apv_json = json.dumps({
'table': 'finance_settlement',
'settlement_id': settlement_id,
'settlement_no': settlement.get('settlement_no'),
}, ensure_ascii=False)
resp = await issue_approve(
phone,
settlement.get('accounting_orgid'),
userid,
business_rows[0]['id'],
form_component,
settlement.get('counterparty_orgid'),
settlement_id,
apv_json,
)
if not resp.get('status'):
return {'status': False, 'msg': '发起审批失败, %s' % resp.get('msg')}
now_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
await sor.U('finance_settlement', {
'id': settlement_id,
'status': 'approving',
'approval_status': 'start',
'approval_id': resp.get('instanceId'),
'created_by': userid,
'failure_reason': None,
'update_at': now_time,
})
return {
'status': True,
'msg': 'ok',
'data': {
'settlement_id': settlement_id,
'approval_id': resp.get('instanceId'),
'status': 'approving',
},
}
except Exception as e:
return {'status': False, 'msg': '提交审批失败, %s' % str(e)}
ret = await finance_settlement_submit(params_kw)
return ret

View File

@ -0,0 +1,225 @@
# -*- coding: utf-8 -*-
"""
财务结算汇总查询。
按供应商或分销商聚合日结/月结应结金额、平台收入和销售额。
金额以已记账 bill_detail 为准,不使用未记账估算。
"""
DBNAME = 'kboss'
INCOME_SUBJECTS = ('折扣收入', '底价收入')
PARENT_SETTLE_SUBJECT = '分销商存放资金'
def _round_money(v):
return round(float(v or 0), 8)
def _is_reverse_op(op):
return str(op or '').upper().endswith('_REVERSE')
def _parse_page(ns, default_page=1, default_size=20, max_size=200):
try:
current_page = int(ns.get('current_page', default_page) or default_page)
except (TypeError, ValueError):
current_page = default_page
try:
page_size = int(ns.get('page_size', default_size) or default_size)
except (TypeError, ValueError):
page_size = default_size
current_page = max(1, current_page)
page_size = max(1, min(page_size, max_size))
return current_page, page_size, (current_page - 1) * page_size
def _period_key(bill_date, period_type):
text = str(bill_date or '')[:10]
if period_type == 'month':
return text[:7]
return text
def _validate_params(ns):
debug(ns)
accounting_orgid = ns.get('accounting_orgid')
counterparty_type = ns.get('counterparty_type')
period_type = ns.get('period_type') or 'day'
start_date = ns.get('start_date') or ns.get('period_start')
end_date = ns.get('end_date') or ns.get('period_end')
if not accounting_orgid:
return None, '缺少 accounting_orgid'
if counterparty_type not in ('supplier', 'reseller'):
return None, 'counterparty_type 仅支持 supplier / reseller'
if period_type not in ('day', 'month'):
return None, 'period_type 仅支持 day / month'
if not start_date or not end_date:
return None, '缺少 start_date / end_date'
return {
'accounting_orgid': accounting_orgid,
'counterparty_type': counterparty_type,
'period_type': period_type,
'start_date': start_date,
'end_date': end_date,
'counterparty_orgid': ns.get('counterparty_orgid'),
}, None
async def _fetch_source_rows(sor, args):
params = {
'accounting_orgid': args['accounting_orgid'],
'start_date': args['start_date'],
'end_date': args['end_date'],
}
filters = [
"b.del_flg = '0'",
"b.bill_state = '1'",
"b.bill_date >= ${start_date}$",
"b.bill_date <= ${end_date}$",
]
if args['counterparty_type'] == 'supplier':
counterparty_expr = 'b.providerid'
name_expr = 'COALESCE(pv.name, cp.orgname)'
join_sql = """
LEFT JOIN provider pv ON pv.orgid = b.providerid AND pv.del_flg = '0'
LEFT JOIN organization cp ON cp.id = b.providerid AND cp.del_flg = '0'
"""
settlement_expr = """
SELECT SUM(bd.amount)
FROM bill_detail bd
WHERE bd.billid = b.id
AND bd.accounting_orgid = ${accounting_orgid}$
AND bd.del_flg = '0'
AND bd.accounting_dir = '贷'
AND bd.subjectname LIKE '待结转%'
"""
if args.get('counterparty_orgid'):
filters.append('b.providerid = ${counterparty_orgid}$')
params['counterparty_orgid'] = args['counterparty_orgid']
else:
counterparty_expr = 'cust.parentid'
name_expr = 'cp.orgname'
join_sql = "LEFT JOIN organization cp ON cp.id = cust.parentid AND cp.del_flg = '0'"
settlement_expr = """
SELECT SUM(bd.amount)
FROM bill_detail bd
WHERE bd.billid = b.id
AND bd.accounting_orgid = ${accounting_orgid}$
AND bd.del_flg = '0'
AND bd.accounting_dir = '借'
AND bd.subjectname = '分销商存放资金'
AND bd.participantid = cust.parentid
"""
filters.append("cust.parentid IS NOT NULL AND cust.parentid != ''")
if args.get('counterparty_orgid'):
filters.append('cust.parentid = ${counterparty_orgid}$')
params['counterparty_orgid'] = args['counterparty_orgid']
where_sql = ' AND '.join(filters)
sql = """
SELECT b.id AS bill_id,
b.orderid AS order_id,
b.bill_date,
b.customerid,
b.providerid,
b.productid,
b.business_op,
b.amount AS sales_amount,
%s AS counterparty_orgid,
%s AS counterparty_name,
COALESCE((%s), 0) AS settlement_amount,
COALESCE((
SELECT SUM(bd.amount)
FROM bill_detail bd
WHERE bd.billid = b.id
AND bd.accounting_orgid = ${accounting_orgid}$
AND bd.del_flg = '0'
AND bd.accounting_dir = '贷'
AND bd.subjectname IN ('折扣收入', '底价收入')
), 0) AS platform_income_amount
FROM bill b
INNER JOIN organization cust ON cust.id = b.customerid AND cust.del_flg = '0'
%s
WHERE %s
ORDER BY b.bill_date DESC, b.create_at DESC
""" % (counterparty_expr, name_expr, settlement_expr, join_sql, where_sql)
return await sor.sqlExe(sql, params)
def _aggregate_rows(rows, period_type):
buckets = {}
for row in rows:
counterparty_orgid = row.get('counterparty_orgid')
if not counterparty_orgid:
continue
period = _period_key(row.get('bill_date'), period_type)
key = (period, counterparty_orgid)
if key not in buckets:
buckets[key] = {
'period': period,
'counterparty_orgid': counterparty_orgid,
'counterparty_name': row.get('counterparty_name'),
'sales_amount': 0.0,
'settlement_amount': 0.0,
'platform_income_amount': 0.0,
'bill_count': 0,
}
sign = -1 if _is_reverse_op(row.get('business_op')) else 1
bucket = buckets[key]
bucket['sales_amount'] += sign * float(row.get('sales_amount') or 0)
bucket['settlement_amount'] += sign * float(row.get('settlement_amount') or 0)
bucket['platform_income_amount'] += sign * float(row.get('platform_income_amount') or 0)
bucket['bill_count'] += 1
items = []
for item in buckets.values():
item['sales_amount'] = _round_money(item['sales_amount'])
item['settlement_amount'] = _round_money(item['settlement_amount'])
item['platform_income_amount'] = _round_money(item['platform_income_amount'])
items.append(item)
return sorted(items, key=lambda x: (x['period'], x['settlement_amount']), reverse=True)
def _summary(items):
return {
'sales_amount': _round_money(sum(i.get('sales_amount') or 0 for i in items)),
'settlement_amount': _round_money(sum(i.get('settlement_amount') or 0 for i in items)),
'platform_income_amount': _round_money(sum(i.get('platform_income_amount') or 0 for i in items)),
'bill_count': sum(i.get('bill_count') or 0 for i in items),
}
async def finance_settlement_summary(ns={}):
args, err = _validate_params(ns)
if err:
return {'status': False, 'msg': err}
current_page, page_size, offset = _parse_page(ns)
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
try:
rows = await _fetch_source_rows(sor, args)
items = _aggregate_rows(rows, args['period_type'])
total_count = len(items)
page_items = items[offset:offset + page_size]
return {
'status': True,
'msg': 'ok',
'data': {
'accounting_orgid': args['accounting_orgid'],
'counterparty_type': args['counterparty_type'],
'period_type': args['period_type'],
'start_date': args['start_date'],
'end_date': args['end_date'],
'summary': _summary(items),
'total_count': total_count,
'current_page': current_page,
'page_size': page_size,
'items': page_items,
},
}
except Exception as e:
return {'status': False, 'msg': '查询失败, %s' % str(e)}
ret = await finance_settlement_summary(params_kw)
return ret

View File

@ -35,6 +35,8 @@ async def model_management_search(ns={}):
offset = (current_page - 1) * page_size
conditions = ['1=1']
if ns.get('model_name'):
ns['display_name'] = ns.get('model_name')
if ns.get('display_name'):
display_name = ns.get('display_name')
# 模糊查询

View File

@ -103,6 +103,34 @@ def _group_key(dt, group_by):
return dt.strftime('%Y-%m-%d')
def _round_amount(value):
return round(float(value or 0), 8)
def _format_amount(value):
return '%.8f' % _round_amount(value)
def _format_amount_item(item):
formatted = dict(item)
formatted['amount'] = _format_amount(formatted.get('amount'))
return formatted
def _format_amount_items(items):
return [_format_amount_item(item) for item in items]
def _format_amount_summary(summary):
formatted = dict(summary)
formatted['amount'] = _format_amount(formatted.get('amount'))
return formatted
def _usage_time_sort_key(item):
return str(item.get('last_usage_time') or '')
def _normalize_usage_row(row, bill_amount_map=None):
usage = _parse_usage_content(row.get('usage_content'))
orderid = row.get('orderid')
@ -118,7 +146,7 @@ def _normalize_usage_row(row, bill_amount_map=None):
'prompt_tokens': int(usage.get('prompt_tokens') or 0),
'completion_tokens': int(usage.get('completion_tokens') or 0),
'total_tokens': int(usage.get('total_tokens') or 0),
'amount': round(amount, 8),
'amount': _round_amount(amount),
'bill_status': row.get('bill_status'),
'orderid': orderid,
'usage_time': row.get('created_at'),
@ -303,17 +331,21 @@ def _aggregate_admin_summary(items, user_map, org_map):
bucket['prompt_tokens'] += item.get('prompt_tokens') or 0
bucket['completion_tokens'] += item.get('completion_tokens') or 0
bucket['total_tokens'] += item.get('total_tokens') or 0
bucket['amount'] = round(bucket['amount'] + float(item.get('amount') or 0), 8)
bucket['amount'] = _round_amount(bucket['amount'] + float(item.get('amount') or 0))
bucket['request_count'] += 1
usage_time = item.get('usage_time')
if usage_time:
bucket['last_usage_time'] = usage_time
if (
not bucket.get('last_usage_time')
or str(usage_time) > str(bucket['last_usage_time'])
):
bucket['last_usage_time'] = usage_time
if (
not bucket.get('first_usage_time')
or str(usage_time) < str(bucket['first_usage_time'])
):
bucket['first_usage_time'] = usage_time
return sorted(buckets.values(), key=lambda x: x['amount'], reverse=True)
return sorted(buckets.values(), key=_usage_time_sort_key, reverse=True)
def _aggregate_items(items, group_by=None):
@ -346,7 +378,7 @@ def _aggregate_items(items, group_by=None):
bucket['prompt_tokens'] += item.get('prompt_tokens') or 0
bucket['completion_tokens'] += item.get('completion_tokens') or 0
bucket['total_tokens'] += item.get('total_tokens') or 0
bucket['amount'] = round(bucket['amount'] + float(item.get('amount') or 0), 8)
bucket['amount'] = _round_amount(bucket['amount'] + float(item.get('amount') or 0))
bucket['request_count'] += 1
return sorted(buckets.values(), key=lambda x: x['period'], reverse=True)
@ -358,7 +390,7 @@ def _summarize(items):
'prompt_tokens': sum(i.get('prompt_tokens') or 0 for i in items),
'completion_tokens': sum(i.get('completion_tokens') or 0 for i in items),
'total_tokens': sum(i.get('total_tokens') or 0 for i in items),
'amount': round(sum(float(i.get('amount') or 0) for i in items), 8),
'amount': _round_amount(sum(float(i.get('amount') or 0) for i in items)),
}
@ -425,9 +457,9 @@ async def model_usage_user_report(ns={}):
'userid': userid,
'start_time': _format_datetime(start_dt),
'end_time': _format_datetime(end_dt),
'summary': _summarize(all_items),
'summary': _format_amount_summary(_summarize(all_items)),
'group_by': group_by,
'groups': grouped,
'groups': _format_amount_items(grouped),
},
}
@ -445,11 +477,11 @@ async def model_usage_user_report(ns={}):
'userid': userid,
'start_time': _format_datetime(start_dt),
'end_time': _format_datetime(end_dt),
'summary': _summarize(all_items),
'summary': _format_amount_summary(_summarize(all_items)),
'total_count': total_count,
'current_page': current_page,
'page_size': page_size,
'items': items,
'items': _format_amount_items(items),
},
}
except Exception as e:
@ -515,7 +547,7 @@ async def model_usage_admin_report(ns={}):
'orgid': orgid,
'start_time': _format_datetime(start_dt),
'end_time': _format_datetime(end_dt),
'summary': _summarize([]),
'summary': _format_amount_summary(_summarize([])),
'total_count': 0,
'current_page': current_page,
'page_size': page_size,
@ -526,7 +558,7 @@ async def model_usage_admin_report(ns={}):
'orgid': orgid,
'start_time': _format_datetime(start_dt),
'end_time': _format_datetime(end_dt),
'summary': _summarize([]),
'summary': _format_amount_summary(_summarize([])),
'group_by': group_by,
'groups': [],
}
@ -560,9 +592,9 @@ async def model_usage_admin_report(ns={}):
'orgid': orgid,
'start_time': _format_datetime(start_dt),
'end_time': _format_datetime(end_dt),
'summary': _summarize(all_items),
'summary': _format_amount_summary(_summarize(all_items)),
'group_by': group_by,
'groups': grouped,
'groups': _format_amount_items(grouped),
},
}
@ -577,11 +609,11 @@ async def model_usage_admin_report(ns={}):
'orgid': orgid,
'start_time': _format_datetime(start_dt),
'end_time': _format_datetime(end_dt),
'summary': _summarize(all_items),
'summary': _format_amount_summary(_summarize(all_items)),
'total_count': total_count,
'current_page': current_page,
'page_size': page_size,
'items': page_items,
'items': _format_amount_items(page_items),
},
}
except Exception as e:

58
b/subject.sql Normal file
View File

@ -0,0 +1,58 @@
/*
Navicat Premium Data Transfer
Source Server : kbossdev
Source Server Type : MariaDB
Source Server Version : 100622
Source Host : db:3306
Source Schema : kboss_dev
Target Server Type : MariaDB
Target Server Version : 100622
File Encoding : 65001
Date: 18/06/2026 17:12:02
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for subject
-- ----------------------------
DROP TABLE IF EXISTS `subject`;
CREATE TABLE `subject` (
`id` varchar(32) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT 'id',
`name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '科目名称',
`balance_side` varchar(1) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '余额方向',
`del_flg` varchar(1) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT '0' COMMENT '删除标志',
`create_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT '创建时间戳',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci COMMENT = '科目表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of subject
-- ----------------------------
INSERT INTO `subject` VALUES ('subject001', '资金账号', '0', '0', '2023-08-03 17:40:34');
INSERT INTO `subject` VALUES ('subject002', '折扣收入', '1', '0', '2023-08-03 17:40:34');
INSERT INTO `subject` VALUES ('subject003', '底价收入', '1', '0', '2023-08-03 17:40:34');
INSERT INTO `subject` VALUES ('subject004', '客户折扣支出', '0', '0', '2023-08-03 17:40:34');
INSERT INTO `subject` VALUES ('subject005', '存放供应商资金', '0', '0', '2023-08-03 17:40:34');
INSERT INTO `subject` VALUES ('subject006', '分销商代付费销售', '0', '0', '2023-08-03 17:40:34');
INSERT INTO `subject` VALUES ('subject007', '支付宝手续费', '0', '0', '2023-08-03 17:40:34');
INSERT INTO `subject` VALUES ('subject008', '业务账', '1', '0', '2023-08-03 17:40:34');
INSERT INTO `subject` VALUES ('subject009', '待结转折扣销售收入', '1', '0', '2023-08-03 17:40:34');
INSERT INTO `subject` VALUES ('subject010', '待结转代付费销售收入', '1', '0', '2023-08-03 17:40:34');
INSERT INTO `subject` VALUES ('subject011', '待结转底价销售收入', '1', '0', '2023-08-03 17:40:34');
INSERT INTO `subject` VALUES ('subject012', '分销商存放资金', '1', '0', '2023-08-03 17:40:34');
INSERT INTO `subject` VALUES ('subject013', '待结转折扣支出', '1', '0', '2023-08-03 17:40:34');
INSERT INTO `subject` VALUES ('subject014', '待结转代付费支出', '1', '0', '2023-08-03 17:40:34');
INSERT INTO `subject` VALUES ('subject015', '待结转底价支出', '1', '0', '2023-08-03 17:40:34');
INSERT INTO `subject` VALUES ('subject016', '代付费销售', '1', '0', '2023-08-03 17:40:34');
INSERT INTO `subject` VALUES ('subject017', '算力券', '1', '0', '2025-03-31 11:50:49');
INSERT INTO `subject` VALUES ('subject018', '算力券发放', '0', '0', '2025-04-08 11:20:16');
INSERT INTO `subject` VALUES ('subject019', '算力券消费', '1', '0', '2025-04-08 11:20:32');
INSERT INTO `subject` VALUES ('subject020', '待结算算力券', '0', '0', '2025-04-08 11:29:02');
INSERT INTO `subject` VALUES ('subject021', '算力劵临时账户', '0', '0', '2025-04-08 17:24:30');
SET FOREIGN_KEY_CHECKS = 1;

View File

@ -12,6 +12,8 @@
size="small"
prefix-icon="el-icon-search"
placeholder="请输入模型名称"
@keyup.enter.native="$emit('search')"
@clear="$emit('search')"
></el-input>
</el-form-item>
<el-form-item label="模型类型">
@ -21,6 +23,8 @@
filterable
size="small"
placeholder="请选择模型类型"
@change="$emit('search')"
@clear="$emit('search')"
>
<el-option
v-for="item in modelTypeOptions"
@ -37,6 +41,8 @@
filterable
size="small"
placeholder="请选择供应商"
@change="$emit('search')"
@clear="$emit('search')"
>
<el-option
v-for="item in providerOptions"

View File

@ -6,7 +6,7 @@
<div class="decor decor--deep-purple"></div>
<section class="case-hero">
<img class="case-hero__bg" :src="bannerImg" alt="">
<img class="case-hero__bg" :src="bannerImg" alt="" @load="refreshTabsOrigin">
<div class="case-container">
<div class="case-hero__content">
<h1>投策智能体</h1>
@ -233,7 +233,8 @@ export default {
mounted() {
this.$nextTick(() => {
const scroller = this.getScrollContainer()
this.tabsOriginTop = this.getElementTopInScroller(this.$refs.caseTabs, scroller)
this.resetScrollTop(scroller)
this.refreshTabsOrigin()
scroller.addEventListener('scroll', this.handlePageScroll, { passive: true })
this.handlePageScroll()
})
@ -245,6 +246,18 @@ export default {
getScrollContainer() {
return document.getElementById('homeOut') || window
},
resetScrollTop(scroller) {
if (scroller === window) {
window.scrollTo(0, 0)
} else {
scroller.scrollTop = 0
}
this.isTabsFixed = false
},
refreshTabsOrigin() {
const scroller = this.getScrollContainer()
this.tabsOriginTop = this.getElementTopInScroller(this.$refs.caseTabs, scroller)
},
getScrollOffset() {
const tabs = this.$el.querySelector('.case-tabs')
const tabsHeight = tabs ? tabs.offsetHeight : 0
@ -311,8 +324,34 @@ export default {
box-sizing: border-box;
}
.decision-case-page h1,
.decision-case-page h2,
.decision-case-page h3,
.decision-case-page h4,
.decision-case-page p {
padding: 0;
margin-top: 0;
font-family: inherit;
}
.decision-case-page button {
font-family: inherit;
line-height: 1.5;
cursor: pointer;
}
.decision-case-page svg {
display: block;
}
.decision-case-page img {
display: block;
max-width: none;
}
.case-container {
width: min(100%, 1280px);
width: 100%;
max-width: 1280px;
margin: 0 auto;
padding: 0 24px;
}

View File

@ -232,7 +232,24 @@
computed: {
filteredModelList() {
const selectedStatus = this.activeStatus === 'pending' ? 0 : 1
return this.modelList.filter(model => Number(model.listing_status) === selectedStatus)
const modelName = this.normalizeSearchText(this.searchForm.name)
const modelType = this.normalizeSearchText(this.searchForm.type)
const provider = this.normalizeSearchText(this.searchForm.provider)
return this.modelList.filter(model => {
if (Number(model.listing_status) !== selectedStatus) return false
const displayName = this.normalizeSearchText(this.getModelDisplayName(model))
const id = this.normalizeSearchText(this.getModelId(model))
const type = this.normalizeSearchText(this.getModelType(model))
const modelProvider = this.normalizeSearchText(this.getProvider(model))
const nameMatched = !modelName || displayName.includes(modelName) || id.includes(modelName)
const typeMatched = !modelType || type === modelType || type.includes(modelType)
const providerMatched = !provider || modelProvider === provider || modelProvider.includes(provider)
return nameMatched && typeMatched && providerMatched
})
},
pagedModelList() {
const start = (this.currentPage - 1) * this.pageSize
@ -282,6 +299,9 @@
if (this.searchForm.provider) params.provider = this.searchForm.provider
return params
},
normalizeSearchText(value) {
return String(value || '').trim().toLowerCase()
},
extractModelData(res) {
const data = res?.data ?? res
if (data?.model_list) return data

View File

@ -24,7 +24,10 @@
<div class="stat-title">{{ item.label }}</div>
<i :class="item.icon"></i>
</div>
<div class="stat-value">{{ item.value }}</div>
<div class="stat-value">
{{ item.value }}
<span v-if="item.unit" class="stat-unit">{{ item.unit }}</span>
</div>
<div class="stat-desc">{{ item.desc }}</div>
</div>
</div>
@ -91,18 +94,35 @@
<el-tag size="mini" type="info">{{ scope.row.model || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="request_count" label="请求次数" width="100"></el-table-column>
<el-table-column prop="prompt_tokens" label="输入Token" min-width="110">
<template slot-scope="scope">{{ formatNumber(scope.row.prompt_tokens) }}</template>
<el-table-column prop="request_count" label="请求次数" width="110">
<template slot-scope="scope">
<span class="usage-number">{{ formatNumber(scope.row.request_count) }}</span>
<span class="usage-unit"></span>
</template>
</el-table-column>
<el-table-column prop="completion_tokens" label="输出Token" min-width="110">
<template slot-scope="scope">{{ formatNumber(scope.row.completion_tokens) }}</template>
<el-table-column prop="prompt_tokens" label="输入Token" min-width="130">
<template slot-scope="scope">
<span class="usage-number">{{ formatNumber(scope.row.prompt_tokens) }}</span>
<span class="usage-unit">Token</span>
</template>
</el-table-column>
<el-table-column prop="total_tokens" label="总Token" min-width="110">
<template slot-scope="scope">{{ formatNumber(scope.row.total_tokens) }}</template>
<el-table-column prop="completion_tokens" label="输出Token" min-width="130">
<template slot-scope="scope">
<span class="usage-number">{{ formatNumber(scope.row.completion_tokens) }}</span>
<span class="usage-unit">Token</span>
</template>
</el-table-column>
<el-table-column prop="amount" label="费用(元)" min-width="110">
<template slot-scope="scope">¥{{ formatAmount(scope.row.amount) }}</template>
<el-table-column prop="total_tokens" label="总Token" min-width="130">
<template slot-scope="scope">
<span class="usage-number">{{ formatNumber(scope.row.total_tokens) }}</span>
<span class="usage-unit">Token</span>
</template>
</el-table-column>
<el-table-column prop="amount" label="费用" min-width="110">
<template slot-scope="scope">
<span class="usage-amount">¥{{ formatAmount(scope.row.amount) }}</span>
<span class="usage-unit"></span>
</template>
</el-table-column>
<el-table-column prop="last_usage_time" label="最近使用时间" min-width="160" show-overflow-tooltip></el-table-column>
@ -158,9 +178,9 @@ export default {
computed: {
statCards() {
return [
{ label: '请求次数', value: this.formatNumber(this.summary.request_count), desc: '当前筛选范围', type: 'purple', icon: 'el-icon-s-promotion' },
{ label: 'Token消耗', value: this.formatNumber(this.summary.total_tokens), desc: `输入 ${this.formatNumber(this.summary.prompt_tokens)} / 输出 ${this.formatNumber(this.summary.completion_tokens)}`, type: 'green', icon: 'el-icon-coin' },
{ label: 'Token总费用', value: `¥${this.formatAmount(this.summary.amount)}`, desc: '按调用记录汇总', type: 'orange', icon: 'el-icon-wallet' }
{ label: '请求次数', value: this.formatNumber(this.summary.request_count), unit: '次', desc: '当前筛选范围', type: 'purple', icon: 'el-icon-s-promotion' },
{ label: 'Token消耗', value: this.formatNumber(this.summary.total_tokens), unit: 'Token', desc: `输入 ${this.formatNumber(this.summary.prompt_tokens)} Token / 输出 ${this.formatNumber(this.summary.completion_tokens)} Token`, type: 'green', icon: 'el-icon-coin' },
{ label: 'Token总费用', value: `¥${this.formatAmount(this.summary.amount)}`, unit: '元', desc: '按调用记录汇总', type: 'orange', icon: 'el-icon-wallet' }
]
},
filterTimeText() {
@ -245,7 +265,7 @@ export default {
return Number(value || 0).toLocaleString()
},
formatAmount(value) {
return Number(value || 0).toFixed(6).replace(/0+$/, '').replace(/\.$/, '.00')
return Number(value || 0).toFixed(2)
}
}
}
@ -400,6 +420,13 @@ export default {
line-height: 1;
}
.stat-unit {
margin-left: 6px;
color: #667085;
font-size: 14px;
font-weight: 500;
}
.stat-desc {
margin-top: 10px;
color: #98a2b3;
@ -505,6 +532,19 @@ export default {
}
}
.usage-number,
.usage-amount {
color: #344054;
font-weight: 600;
}
.usage-unit {
margin-left: 4px;
color: #98a2b3;
font-size: 12px;
font-weight: 400;
}
@media (max-width: 900px) {
.stat-grid {
grid-template-columns: 1fr;

View File

@ -191,12 +191,12 @@ export default {
try {
const res = await reqApikeyList(params)
if (res && res.status === false) {
throw new Error(res.msg || '获取 API Key 列表失败')
// throw new Error(res.msg || ' API Key ')
}
this.tokenList = this.normalizeApiKeyList(res)
} catch (error) {
this.tokenList = []
this.$message.error(error && error.message ? error.message : '获取 API Key 列表失败')
// this.$message.error(error && error.message ? error.message : ' API Key ')
} finally {
this.tableLoading = false
}

View File

@ -24,7 +24,10 @@
<span>{{ item.label }}</span>
<i :class="item.icon"></i>
</div>
<strong>{{ item.value }}</strong>
<strong>
{{ item.value }}
<span v-if="item.unit" class="stat-unit">{{ item.unit }}</span>
</strong>
<em>{{ item.desc }}</em>
</div>
</div>
@ -35,7 +38,10 @@
<h3>Token 使用概览</h3>
<span>{{ rangeLabel }}</span>
</div>
<div class="token-total">{{ formatNumber(summary.total_tokens) }}</div>
<div class="token-total">
{{ formatNumber(summary.total_tokens) }}
<span>Token</span>
</div>
<p> Token 消耗</p>
<div ref="tokenRatioChart" class="chart-box token-ratio-chart"></div>
</div>
@ -85,11 +91,17 @@
<el-table v-loading="loading" :data="usageList" class="usage-table" style="width: 100%">
<el-table-column prop="model" label="模型名称" min-width="180" show-overflow-tooltip></el-table-column>
<!-- <el-table-column prop="request_count" label="调用次数" width="120"></el-table-column> -->
<el-table-column prop="prompt_tokens" label="输入Token" min-width="140"></el-table-column>
<el-table-column prop="completion_tokens" label="输出Token" min-width="140"></el-table-column>
<el-table-column prop="total_tokens" label="总Token" min-width="140"></el-table-column>
<el-table-column prop="amount" label="费用" width="130">
<template slot-scope="scope">¥ {{ formatAmount(scope.row.amount) }}</template>
<el-table-column prop="prompt_tokens" label="输入TokenToken" min-width="150">
<template slot-scope="scope">{{ formatNumber(scope.row.prompt_tokens) }} Token</template>
</el-table-column>
<el-table-column prop="completion_tokens" label="输出TokenToken" min-width="150">
<template slot-scope="scope">{{ formatNumber(scope.row.completion_tokens) }} Token</template>
</el-table-column>
<el-table-column prop="total_tokens" label="总TokenToken" min-width="150">
<template slot-scope="scope">{{ formatNumber(scope.row.total_tokens) }} Token</template>
</el-table-column>
<el-table-column prop="amount" label="费用(元)" width="140">
<template slot-scope="scope">¥ {{ formatAmount(scope.row.amount) }} </template>
</el-table-column>
<el-table-column prop="usage_time" label="使用时间" min-width="170" show-overflow-tooltip></el-table-column>
</el-table>
@ -129,10 +141,10 @@ export default {
amount: 0
},
statCards: [
{ label: '总消耗 Token', value: '0', desc: '当前筛选范围', type: 'primary', icon: 'el-icon-coin' },
{ label: '调用次数', value: '0', desc: '当前筛选范围', type: 'success', icon: 'el-icon-s-promotion' },
{ label: ' 费用', value: '¥ 0.00', desc: '按当前单价估算', type: 'warning', icon: 'el-icon-wallet' },
{ label: '输入/输出 Token', value: '0 / 0', desc: 'Prompt / Completion', type: 'purple', icon: 'el-icon-pie-chart' }
{ label: '总消耗 Token', value: '0', unit: 'Token', desc: '当前筛选范围', type: 'primary', icon: 'el-icon-coin' },
{ label: '调用次数', value: '0', unit: '次', desc: '当前筛选范围', type: 'success', icon: 'el-icon-s-promotion' },
{ label: '预估费用', value: '¥ 0.00', unit: '元', desc: '按当前单价估算', type: 'warning', icon: 'el-icon-wallet' },
{ label: '输入/输出 Token', value: '0 / 0', unit: 'Token', desc: 'Prompt / Completion', type: 'purple', icon: 'el-icon-pie-chart' }
],
usageList: [],
total: 0,
@ -259,17 +271,17 @@ export default {
},
updateStatCards() {
this.statCards = [
{ label: '总消耗 Token', value: this.formatNumber(this.summary.total_tokens), desc: '当前筛选范围', type: 'primary', icon: 'el-icon-coin' },
{ label: '调用次数', value: this.formatNumber(this.summary.request_count), desc: '当前筛选范围', type: 'success', icon: 'el-icon-s-promotion' },
{ label: '预估费用', value: `¥ ${this.formatAmount(this.summary.amount)}`, desc: '按当前单价估算', type: 'warning', icon: 'el-icon-wallet' },
{ label: '输入/输出 Token', value: `${this.formatNumber(this.summary.prompt_tokens)} / ${this.formatNumber(this.summary.completion_tokens)}`, desc: 'Prompt / Completion', type: 'purple', icon: 'el-icon-pie-chart' }
{ label: '总消耗 Token', value: this.formatNumber(this.summary.total_tokens), unit: 'Token', desc: '当前筛选范围', type: 'primary', icon: 'el-icon-coin' },
{ label: '调用次数', value: this.formatNumber(this.summary.request_count), unit: '次', desc: '当前筛选范围', type: 'success', icon: 'el-icon-s-promotion' },
{ label: '预估费用', value: `¥ ${this.formatAmount(this.summary.amount)}`, unit: '元', desc: '按当前单价估算', type: 'warning', icon: 'el-icon-wallet' },
{ label: '输入/输出 Token', value: `${this.formatNumber(this.summary.prompt_tokens)} / ${this.formatNumber(this.summary.completion_tokens)}`, unit: 'Token', desc: 'Prompt / Completion', type: 'purple', icon: 'el-icon-pie-chart' }
]
},
formatNumber(value) {
return Number(value || 0).toLocaleString()
},
formatAmount(value) {
return Number(value || 0).toFixed(6).replace(/0+$/, '').replace(/\.$/, '.00')
return Number(value || 0).toFixed(2)
},
initCharts() {
if (this.$refs.tokenRatioChart && !this.tokenRatioChart) {
@ -361,15 +373,23 @@ export default {
return [
item.name,
`${this.formatNumber(item.value)} Token`,
`调用次数:${this.formatNumber(rankItem.request_count)}`,
`预估费用:¥ ${this.formatAmount(rankItem.amount)}`
`调用次数:${this.formatNumber(rankItem.request_count)}`,
`预估费用:¥ ${this.formatAmount(rankItem.amount)}`
].join('<br/>')
}
},
xAxis: {
type: 'value',
name: 'Token',
nameTextStyle: {
color: '#909399',
padding: [0, 0, 0, 8]
},
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
formatter: value => this.formatNumber(value)
},
splitLine: {
lineStyle: {
color: '#edf1f7'
@ -406,7 +426,7 @@ export default {
position: 'right',
color: '#344054',
fontSize: 11,
formatter: params => this.formatNumber(params.value)
formatter: params => `${this.formatNumber(params.value)} Token`
}
}
]
@ -573,6 +593,14 @@ export default {
font-size: 26px;
}
.stat-unit {
display: inline;
margin-left: 6px;
color: #667085;
font-size: 13px;
font-weight: 500;
}
&:before {
position: absolute;
top: 0;
@ -659,6 +687,13 @@ export default {
font-size: 34px;
font-weight: 700;
line-height: 1.2;
span {
margin-left: 8px;
color: #667085;
font-size: 14px;
font-weight: 500;
}
}
.token-ratio-card {