Compare commits
290
Commits
2b7bc8deab
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af755b390e | ||
|
|
886d23e3db | ||
|
|
94a0cd5f92 | ||
|
|
be93d4ae50 | ||
|
|
9347410909 | ||
|
|
8b5b564292 | ||
|
|
e3ede5f5f2 | ||
|
|
a663c30028 | ||
|
|
b31a77334a | ||
|
|
18ebd245a5 | ||
|
|
26d968ec55 | ||
|
|
2c1bf9b967 | ||
|
|
bbde6cc87f | ||
|
|
bb0b8bd7ef | ||
|
|
710e4cfb90 | ||
|
|
42e0323080 | ||
|
|
0259445866 | ||
|
|
9f9e3c6d04 | ||
|
|
c0285edd4c | ||
|
|
3ae6fdbfb4 | ||
|
|
4a3c8c1938 | ||
|
|
f4c4390352 | ||
|
|
c34b249d7f | ||
|
|
b0b16ae58f | ||
|
|
8e5c8ea24c | ||
|
|
aa3897a651 | ||
|
|
da167a46ef | ||
|
|
7ae8d3e14b | ||
|
|
731a4f8b8d | ||
|
|
da549b19aa | ||
|
|
dd0c661223 | ||
|
|
cb8f5fc2e6 | ||
|
|
ddfc970928 | ||
|
|
9dbd345fc9 | ||
|
|
535ede3c6f | ||
|
|
c714612ace | ||
|
|
037ce89cca | ||
|
|
bea1783c7c | ||
|
|
10eec976c2 | ||
|
|
241e9b0c46 | ||
|
|
3effd6f9be | ||
|
|
b254ef0783 | ||
|
|
2ca14aa1e0 | ||
|
|
1c86c227ee | ||
|
|
1ecbcf540c | ||
|
|
8995bf89d6 | ||
|
|
e587cbc7cd | ||
|
|
7a2b991aa0 | ||
|
|
4e804a17cd | ||
|
|
287e8134db | ||
|
|
e260c015ab | ||
|
|
17015621e1 | ||
|
|
60446146f9 | ||
|
|
83fb0dd2a6 | ||
|
|
2b2969dc92 | ||
|
|
2560fb1ace | ||
|
|
be3be6179f | ||
|
|
f8f938b04c | ||
|
|
d1884d813f | ||
|
|
295077a1e8 | ||
|
|
880d19ec94 | ||
|
|
dc80a20b71 | ||
|
|
afd56a0586 | ||
|
|
0925f01b54 | ||
|
|
dae713eb0a | ||
|
|
33a8d5dd6a | ||
|
|
febc01042e | ||
|
|
1c11e78be0 | ||
|
|
66bedfc7f5 | ||
|
|
218ca53dd2 | ||
|
|
a857b1213c | ||
|
|
a1c466b18b | ||
|
|
1147ea1e48 | ||
|
|
17736d12e2 | ||
|
|
986ee20a9e | ||
|
|
dbf20d66d4 | ||
|
|
77d7700709 | ||
|
|
67823d9644 | ||
|
|
19e7b42c4f | ||
|
|
640a075742 | ||
|
|
f2f719d608 | ||
|
|
d29393692c | ||
|
|
41819feaa1 | ||
|
|
4faa7bb759 | ||
|
|
db02c79d9b | ||
|
|
df841a80f7 | ||
|
|
78aeb77cd6 | ||
|
|
cda3fff6f4 | ||
|
|
42e3e273fe | ||
|
|
fc820ba3b1 | ||
|
|
e7ecc2fd2c | ||
|
|
0cc7a0a91d | ||
|
|
93326936c3 | ||
|
|
0c6d59a062 | ||
|
|
b83657b1ea | ||
|
|
67320a4787 | ||
|
|
667bdac868 | ||
|
|
416107e7f8 | ||
|
|
96256c3366 | ||
|
|
658a7cfc6b | ||
|
|
c9f39910f9 | ||
|
|
00287ca3b7 | ||
|
|
1e2aa98c97 | ||
|
|
ef6522fc74 | ||
|
|
2fe4a4e522 | ||
|
|
96c058ca9a | ||
|
|
2f847923ef | ||
|
|
2b131bdc0c | ||
|
|
8e5f91a68b | ||
|
|
17dd0fc6e9 | ||
|
|
934ec7168e | ||
|
|
a4fce1c895 | ||
|
|
deb6a1d888 | ||
|
|
99944dbad3 | ||
|
|
d22f483ef8 | ||
|
|
e7d9e25f57 | ||
|
|
765a37f82e | ||
|
|
2636c9e0c3 | ||
|
|
957641393c | ||
|
|
fe3cdd8e55 | ||
|
|
a0be0ebc0c | ||
|
|
1666184356 | ||
|
|
61460ce32f | ||
|
|
7f8f160863 | ||
|
|
8422ed1d4b | ||
|
|
2332dfba38 | ||
|
|
c1a11d8f86 | ||
|
|
1c66276bb1 | ||
|
|
e68298d177 | ||
|
|
810038864d | ||
|
|
e6eb95f057 | ||
|
|
c36a043751 | ||
|
|
075a9cc876 | ||
|
|
bb1def88d9 | ||
|
|
eec07ac718 | ||
|
|
0894e86d8c | ||
|
|
907a557c34 | ||
|
|
035f077e0c | ||
|
|
cddb87d922 | ||
|
|
9317daa90d | ||
|
|
4e5afdc272 | ||
|
|
33ff7edc2e | ||
|
|
54a431891a | ||
|
|
7db5bfbee5 | ||
|
|
9644338a59 | ||
|
|
bce4bf9ee3 | ||
|
|
f2b185b731 | ||
|
|
ee870fb9d1 | ||
|
|
0fedb2a3ff | ||
|
|
2fb43b4b1f | ||
|
|
43e6df6f72 | ||
|
|
9ddefa377a | ||
|
|
46c9ce8325 | ||
|
|
0e7cab1ab6 | ||
|
|
bcd821e210 | ||
|
|
32d51a3104 | ||
|
|
fb246dc2e4 | ||
|
|
642c616d23 | ||
|
|
cc19707f29 | ||
|
|
789d4071e8 | ||
|
|
d25929e461 | ||
|
|
5e089029a0 | ||
|
|
768267fd21 | ||
|
|
1bf9e94df3 | ||
|
|
736a59eda8 | ||
|
|
8cadef4bcd | ||
|
|
f29cc83be7 | ||
|
|
2d39bbe3c2 | ||
|
|
10a114cf95 | ||
|
|
9787739cff | ||
|
|
ee1736ccf8 | ||
|
|
68e216209a | ||
|
|
32702677fd | ||
|
|
6d1eb5a72a | ||
|
|
8ad4e7ad42 | ||
|
|
7f72fa0244 | ||
|
|
684762fb6c | ||
|
|
ded303b25a | ||
|
|
6e4aa1a2bb | ||
|
|
a2623df7e2 | ||
|
|
255b88a948 | ||
|
|
d7944b52fa | ||
|
|
5ed7afd0fe | ||
|
|
5c98efcbc5 | ||
|
|
1344217733 | ||
|
|
cce4230f0d | ||
|
|
86e8f08242 | ||
|
|
53512b7f9c | ||
|
|
62150787d0 | ||
|
|
39976c1368 | ||
|
|
23c190b11e | ||
|
|
df99986af6 | ||
|
|
780279b936 | ||
|
|
71acd46cb7 | ||
|
|
7cec1477cd | ||
|
|
26b60f81d4 | ||
|
|
e1527c5bd0 | ||
|
|
f1d6e2f2f6 | ||
|
|
d84b294966 | ||
|
|
8990e31f78 | ||
|
|
d84bf561ac | ||
|
|
0a2ec15518 | ||
|
|
bbeef4536a | ||
|
|
5d52c1cd68 | ||
|
|
5dc6a1ebbd | ||
|
|
5b2463cba0 | ||
|
|
186ee48a5e | ||
|
|
45e41f955f | ||
|
|
2a6b9517d5 | ||
|
|
2268b09f7f | ||
|
|
2fe790aa36 | ||
|
|
8e18dda8c8 | ||
|
|
e5f652d9af | ||
|
|
bc92b16b85 | ||
|
|
b576031346 | ||
|
|
05480e806b | ||
|
|
53d6dfd551 | ||
|
|
3d3c8f17e1 | ||
|
|
efc7f1bf27 | ||
|
|
62cecbcff9 | ||
|
|
b661d333e7 | ||
|
|
1294c3fcf6 | ||
|
|
1986c13796 | ||
|
|
b6ab272f7f | ||
|
|
1fc0483fe8 | ||
|
|
5e49326c89 | ||
|
|
7742b7b21d | ||
|
|
d6a384815d | ||
|
|
5fbb2c9b94 | ||
|
|
207d90b48a | ||
|
|
bf1df93b1f | ||
|
|
3758c8c31c | ||
|
|
912a36978a | ||
|
|
24f17f011d | ||
|
|
a8061efa2f | ||
|
|
385e2550da | ||
|
|
84b7874237 | ||
|
|
e3b523b41c | ||
|
|
24a685ae0c | ||
|
|
090d362a82 | ||
|
|
0669710359 | ||
|
|
55e81a41b6 | ||
|
|
003b195e79 | ||
|
|
069e329d9f | ||
|
|
24e4121c11 | ||
|
|
26c3629f7d | ||
|
|
a5ff990b9e | ||
|
|
c81ea9ebe7 | ||
|
|
a7e226d419 | ||
|
|
cb181564e5 | ||
|
|
5eaeefeb37 | ||
|
|
b98853e82b | ||
|
|
daea412d34 | ||
|
|
200cb8390e | ||
|
|
6ad6ebafb2 | ||
|
|
6373eaf4e5 | ||
|
|
f868d94b08 | ||
|
|
9154a813df | ||
|
|
9d02420a51 | ||
|
|
2cf9867971 | ||
|
|
ca6378961d | ||
|
|
c4dc05f87e | ||
|
|
f9cc37dc20 | ||
|
|
b41d85dc2a | ||
|
|
51cd3a9cf5 | ||
|
|
8bb3f14337 | ||
|
|
33f47efad9 | ||
|
|
0843924e06 | ||
|
|
1a9c7d3655 | ||
|
|
5a3d9a4a11 | ||
|
|
92621b51a0 | ||
|
|
f167532bed | ||
|
|
d91a065976 | ||
|
|
d53cfaadab | ||
|
|
00331148f3 | ||
|
|
0db2942043 | ||
|
|
ee6783ab4f | ||
|
|
0a0bccdc54 | ||
|
|
e56d6da230 | ||
|
|
ff25e0e5cf | ||
|
|
8647444e10 | ||
|
|
a3d8df9ad8 | ||
|
|
fbaf8d150d | ||
|
|
1198def17e | ||
|
|
dbee49727e | ||
|
|
e717df69ba | ||
|
|
30cc11c44e | ||
|
|
c303814c09 | ||
|
|
3672f9b5b4 | ||
|
|
cbd6b72f2f |
No files matched your search
@@ -9,6 +9,8 @@ indent_style = tab
|
||||
indent_size = 4
|
||||
continuation_indent_size = 8
|
||||
end_of_line = lf
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{js, html}]
|
||||
indent_style = space
|
||||
|
||||
@@ -61,3 +61,6 @@ gen_code
|
||||
|
||||
### node ###
|
||||
node_modules
|
||||
|
||||
# Local file-access credentials
|
||||
.file-access/
|
||||
@@ -86,6 +86,11 @@ CREATE TABLE `third_pay_log`
|
||||
COLLATE = utf8mb4_general_ci
|
||||
ROW_FORMAT = DYNAMIC COMMENT ='第三方支付日志';
|
||||
|
||||
CREATE INDEX idx_tpl_tenant_del_ct_id
|
||||
ON third_pay_log (tenant_id, del_flag, create_time, id);
|
||||
CREATE INDEX idx_tpl_tenant_del_source_biz_ct_id
|
||||
ON third_pay_log (tenant_id, del_flag, pay_source_type, biz_type, create_time, id);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for third_key_manage
|
||||
-- ----------------------------
|
||||
@@ -133,4 +138,42 @@ CREATE TABLE `third_zy_id_dict`
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_general_ci
|
||||
ROW_FORMAT = DYNAMIC COMMENT ='正元字典id对应 id + bizType 唯一';
|
||||
ROW_FORMAT = DYNAMIC COMMENT ='正元字典id对应 id + bizType 唯一';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for third_pay_order
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `third_pay_order`;
|
||||
CREATE TABLE `third_pay_order`
|
||||
(
|
||||
`id` bigint NOT NULL COMMENT '主键',
|
||||
|
||||
`pay_platform_type` varchar(32) NOT NULL COMMENT '支付平台类型',
|
||||
`biz_type` varchar(32) NOT NULL COMMENT '支付类型',
|
||||
`biz_id` varchar(128) NOT NULL COMMENT '支付相关id',
|
||||
`user_id` bigint NOT NULL COMMENT '用户id',
|
||||
`user_code` varchar(64) DEFAULT NULL COMMENT '用户code',
|
||||
`price` int NOT NULL COMMENT '支付金额(分)',
|
||||
`status` varchar(128) DEFAULT NULL COMMENT '支付状态',
|
||||
|
||||
`pay_no` varchar(128) DEFAULT NULL COMMENT '支付订单号',
|
||||
`pay_time` datetime DEFAULT NULL COMMENT '支付时间',
|
||||
`pay_res` text DEFAULT NULL COMMENT '支付返回',
|
||||
|
||||
`refund_no` varchar(128) DEFAULT NULL COMMENT '退款订单号',
|
||||
`refund_time` datetime DEFAULT NULL COMMENT '退款时间',
|
||||
`refund_res` text DEFAULT NULL COMMENT '退款返回',
|
||||
|
||||
`expire_time` datetime DEFAULT NULL COMMENT '订单过期时间',
|
||||
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`create_by` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
|
||||
`update_by` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
|
||||
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '删除时间',
|
||||
`tenant_id` bigint(20) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_general_ci
|
||||
ROW_FORMAT = DYNAMIC COMMENT ='第三方支付订单';
|
||||
@@ -13,3 +13,26 @@ CREATE TABLE `hard_panel_setting`
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4 COMMENT ='大屏配置表';
|
||||
|
||||
|
||||
-- app版本控制表
|
||||
CREATE TABLE `app_version`
|
||||
(
|
||||
`id` bigint NOT NULL COMMENT '主键ID',
|
||||
`app_unique_code` varchar(32) DEFAULT NULL COMMENT 'app唯一编码',
|
||||
`platform` varchar(32) NOT NULL COMMENT '平台类型(android、ios)',
|
||||
`version_code` int NOT NULL COMMENT '内部版本号(用于比较大小)',
|
||||
`version_name` varchar(32) NOT NULL COMMENT '展示版本号(字符串)',
|
||||
`update_content` text COMMENT '更新内容/更新日志',
|
||||
`download_url` varchar(512) NOT NULL COMMENT '安装包下载地址(wgt、apk)',
|
||||
`is_force` varchar(32) NOT NULL COMMENT '是否强制更新',
|
||||
`is_silent` varchar(32) NOT NULL COMMENT '是否静默更新(WGT用)',
|
||||
`status` varchar(32) NOT NULL COMMENT '状态(已发布、未发布、已下架)',
|
||||
`create_by` varchar(50) DEFAULT NULL COMMENT '创建者',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_by` varchar(50) DEFAULT NULL COMMENT '更新者',
|
||||
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`del_flag` char(1) DEFAULT '0' COMMENT '是否删除(0=正常 1=删除)',
|
||||
`tenant_id` bigint(20) DEFAULT NULL COMMENT '租户编号',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4 COMMENT ='app版本控制表';
|
||||
@@ -0,0 +1,43 @@
|
||||
CREATE OR REPLACE VIEW pjcloudx_wallet.v_mch_dept_tree AS
|
||||
WITH RECURSIVE
|
||||
root AS (SELECT m.dept_id AS root_dept_id,
|
||||
m.id AS mch_id,
|
||||
m.name AS mch_name
|
||||
FROM pjcloudx_wallet.mch_manage AS m
|
||||
JOIN pjcloudx.sys_dept AS d
|
||||
ON d.dept_id = m.dept_id
|
||||
AND d.del_flag = '0'
|
||||
WHERE m.del_flag = '0'
|
||||
AND m.types = '1'),
|
||||
r (dept_id, parent_id, dept_name, root_dept_id, mch_id, mch_name, level) AS (SELECT d.dept_id,
|
||||
d.parent_id,
|
||||
d.name,
|
||||
root.root_dept_id,
|
||||
root.mch_id,
|
||||
root.mch_name,
|
||||
0
|
||||
FROM pjcloudx.sys_dept AS d
|
||||
JOIN root
|
||||
ON root.root_dept_id = d.dept_id
|
||||
WHERE d.del_flag = '0'
|
||||
UNION ALL
|
||||
SELECT c.dept_id,
|
||||
c.parent_id,
|
||||
c.name,
|
||||
r.root_dept_id,
|
||||
r.mch_id,
|
||||
r.mch_name,
|
||||
r.level + 1
|
||||
FROM pjcloudx.sys_dept AS c
|
||||
JOIN r
|
||||
ON c.parent_id = r.dept_id
|
||||
WHERE c.del_flag = '0')
|
||||
SELECT root_dept_id AS mchDeptId,
|
||||
mch_id AS mchId,
|
||||
mch_name AS mchName,
|
||||
dept_id AS deptId,
|
||||
parent_id AS parentId,
|
||||
dept_name AS deptName,
|
||||
level AS level
|
||||
FROM r
|
||||
ORDER BY mchDeptId, level, deptId;
|
||||
@@ -0,0 +1,44 @@
|
||||
-- 订单识别证据图片表(迁移自 hq-platform 20260823_add_hard_order_evidence_image.sql)。
|
||||
-- 本表独立于 hard_order,退款、撤销和订单逻辑删除均不自动清理证据文件或证据记录。
|
||||
CREATE TABLE IF NOT EXISTS `pjcloudx_hardware`.`hard_order_evidence_image`
|
||||
(
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '证据记录自增主键',
|
||||
`order_id` bigint(20) NOT NULL COMMENT '订单主键,来源hard_order.id',
|
||||
`android_order_code` varchar(128) NOT NULL COMMENT '终端本地稳定订单号快照',
|
||||
`evidence_type` varchar(32) NOT NULL COMMENT '证据类型,FACE_RECOGNITION或MENU_RECOGNITION',
|
||||
`evidence_role` varchar(32) NOT NULL COMMENT '图片角色,FACE_REFERENCE、FACE_LIVE或MENU_LIVE',
|
||||
`sequence_no` int(11) NOT NULL DEFAULT 0 COMMENT '同类型同角色图片序号,从0开始',
|
||||
`user_id` bigint(20) DEFAULT NULL COMMENT '识别用户主键快照',
|
||||
`face_sample_id` varchar(64) DEFAULT NULL COMMENT '虹软人脸样本编号快照',
|
||||
`reference_image_url` varchar(512) DEFAULT NULL COMMENT '识别时后台参考人脸来源地址快照,不作为展示地址',
|
||||
`similarity` decimal(8, 6) DEFAULT NULL COMMENT '识别相似度快照,取值0至1',
|
||||
`similarity_threshold` decimal(8, 6) DEFAULT NULL COMMENT '终端生效的相似度阈值快照,取值0至1',
|
||||
`hard_id` bigint(20) NOT NULL COMMENT '产生证据的设备主键',
|
||||
`captured_at` datetime DEFAULT NULL COMMENT '终端实际采集时间',
|
||||
`sys_file_id` bigint(20) DEFAULT NULL COMMENT '文件中心sys_file记录主键',
|
||||
`storage_file_name` varchar(255) DEFAULT NULL COMMENT '文件中心对象名',
|
||||
`image_url` varchar(512) DEFAULT NULL COMMENT '订单证据图片访问地址',
|
||||
`image_hash` char(64) DEFAULT NULL COMMENT '实际文件SHA-256哈希',
|
||||
`image_size` bigint(20) DEFAULT NULL COMMENT '实际文件大小,单位字节',
|
||||
`image_width` int(11) DEFAULT NULL COMMENT '实际图片宽度,单位像素',
|
||||
`image_height` int(11) DEFAULT NULL COMMENT '实际图片高度,单位像素',
|
||||
`status` varchar(32) NOT NULL COMMENT '证据状态,PENDING、AVAILABLE或UNAVAILABLE',
|
||||
`failure_reason` varchar(500) DEFAULT NULL COMMENT '永久缺失或复制失败摘要,不保存终端完整路径',
|
||||
`app_version` varchar(64) DEFAULT NULL COMMENT '产生证据的终端应用版本',
|
||||
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` varchar(64) DEFAULT NULL COMMENT '修改人',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
|
||||
`del_flag` char(1) DEFAULT '0' COMMENT '逻辑删除标记,0有效,1删除',
|
||||
`tenant_id` bigint(20) NOT NULL COMMENT '租户主键',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_hard_order_evidence_position`
|
||||
(`tenant_id`, `order_id`, `evidence_type`, `evidence_role`, `sequence_no`, `del_flag`),
|
||||
KEY `idx_hard_order_evidence_android_order`
|
||||
(`tenant_id`, `hard_id`, `android_order_code`, `del_flag`),
|
||||
KEY `idx_hard_order_evidence_status`
|
||||
(`tenant_id`, `status`, `update_time`, `del_flag`)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_general_ci
|
||||
COMMENT = '硬件订单识别证据图片';
|
||||
@@ -0,0 +1,23 @@
|
||||
-- 硬件订单新增独立核销时间(迁移自 hq-platform 20260823_add_hard_order_verification_time.sql)。
|
||||
-- 历史二维码核销订单只能使用最后更新时间近似回填;外卖等非核销完成状态不参与回填。
|
||||
-- 新核销订单由业务事务写入准确时间,后续订单更新不再改变该值。
|
||||
|
||||
SET @verification_time_ddl = IF(
|
||||
(SELECT COUNT(*) FROM `information_schema`.`columns`
|
||||
WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_order'
|
||||
AND `column_name` = 'verification_time') = 0,
|
||||
'ALTER TABLE `pjcloudx_hardware`.`hard_order` ADD COLUMN `verification_time` datetime DEFAULT NULL COMMENT ''订单实际核销完成时间,后续订单更新不得覆盖'' AFTER `refund_status`, ADD KEY `idx_hard_order_verification_time` (`tenant_id`, `verification_time`, `del_flag`)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE verification_time_stmt FROM @verification_time_ddl;
|
||||
EXECUTE verification_time_stmt;
|
||||
DEALLOCATE PREPARE verification_time_stmt;
|
||||
|
||||
UPDATE `pjcloudx_hardware`.`hard_order`
|
||||
SET `verification_time` = `update_time`
|
||||
WHERE ((`order_status` = '20' AND `order_type` IN ('1', '2', '3', '4'))
|
||||
OR `pay_type` IN ('5', '6', '50'))
|
||||
AND `verification_time` IS NULL
|
||||
AND `update_time` IS NOT NULL;
|
||||
|
||||
SET @verification_time_ddl = NULL;
|
||||
@@ -0,0 +1,206 @@
|
||||
-- 尔印独立打印终端、终端一对一绑定及持久化打印任务(迁移自 hq-platform 20260904_add_er_yin_printer.sql,仅保留 DDL)。
|
||||
-- 字典(hard_type 类型12、hard_print_type、hard_print_status)与按钮权限请按 db/20260908_权限菜单字典待添加.txt 手动添加。
|
||||
-- 每个结构变更独立判断,可在兼容数据库自动提交部分DDL后安全重跑整个脚本。
|
||||
|
||||
ALTER TABLE `pjcloudx_hardware`.`hard_invoice`
|
||||
MODIFY COLUMN `hard_id` bigint(20) NULL COMMENT '来源业务或核销终端id',
|
||||
MODIFY COLUMN `pic` varchar(512) NULL COMMENT '后端生成的打印PNG地址';
|
||||
|
||||
SET @er_yin_ddl = IF(
|
||||
(SELECT COUNT(*) FROM `information_schema`.`columns`
|
||||
WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
|
||||
AND `column_name` = 'printer_hard_id') = 0,
|
||||
'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD COLUMN `printer_hard_id` bigint(20) NULL COMMENT ''目标尔印打印终端id'' AFTER `hard_id`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE er_yin_stmt FROM @er_yin_ddl;
|
||||
EXECUTE er_yin_stmt;
|
||||
DEALLOCATE PREPARE er_yin_stmt;
|
||||
|
||||
SET @er_yin_ddl = IF(
|
||||
(SELECT COUNT(*) FROM `information_schema`.`columns`
|
||||
WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
|
||||
AND `column_name` = 'print_type') = 0,
|
||||
'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD COLUMN `print_type` char(2) NOT NULL DEFAULT ''1'' COMMENT ''打印类型,1发票、2兑换券'' AFTER `order_id`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE er_yin_stmt FROM @er_yin_ddl;
|
||||
EXECUTE er_yin_stmt;
|
||||
DEALLOCATE PREPARE er_yin_stmt;
|
||||
|
||||
SET @er_yin_ddl = IF(
|
||||
(SELECT COUNT(*) FROM `information_schema`.`columns`
|
||||
WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
|
||||
AND `column_name` = 'print_no') = 0,
|
||||
'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD COLUMN `print_no` int(11) NOT NULL DEFAULT 1 COMMENT ''同订单同类型打印序号'' AFTER `print_type`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE er_yin_stmt FROM @er_yin_ddl;
|
||||
EXECUTE er_yin_stmt;
|
||||
DEALLOCATE PREPARE er_yin_stmt;
|
||||
|
||||
SET @er_yin_ddl = IF(
|
||||
(SELECT COUNT(*) FROM `information_schema`.`columns`
|
||||
WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
|
||||
AND `column_name` = 'retry_version') = 0,
|
||||
'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD COLUMN `retry_version` int(11) NOT NULL DEFAULT 0 COMMENT ''人工失败重试版本'' AFTER `print_no`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE er_yin_stmt FROM @er_yin_ddl;
|
||||
EXECUTE er_yin_stmt;
|
||||
DEALLOCATE PREPARE er_yin_stmt;
|
||||
|
||||
SET @er_yin_ddl = IF(
|
||||
(SELECT COUNT(*) FROM `information_schema`.`columns`
|
||||
WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
|
||||
AND `column_name` = 'attempt_count') = 0,
|
||||
'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD COLUMN `attempt_count` int(11) NOT NULL DEFAULT 0 COMMENT ''终端USB累计尝试次数'' AFTER `retry_version`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE er_yin_stmt FROM @er_yin_ddl;
|
||||
EXECUTE er_yin_stmt;
|
||||
DEALLOCATE PREPARE er_yin_stmt;
|
||||
|
||||
SET @er_yin_ddl = IF(
|
||||
(SELECT COUNT(*) FROM `information_schema`.`columns`
|
||||
WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
|
||||
AND `column_name` = 'failure_stage') = 0,
|
||||
'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD COLUMN `failure_stage` varchar(32) NULL COMMENT ''BINDING、IMAGE_GENERATION、MQTT_DISPATCH或TERMINAL_PRINT'' AFTER `status`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE er_yin_stmt FROM @er_yin_ddl;
|
||||
EXECUTE er_yin_stmt;
|
||||
DEALLOCATE PREPARE er_yin_stmt;
|
||||
|
||||
SET @er_yin_ddl = IF(
|
||||
(SELECT COUNT(*) FROM `information_schema`.`columns`
|
||||
WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
|
||||
AND `column_name` = 'last_error') = 0,
|
||||
'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD COLUMN `last_error` varchar(2000) NULL COMMENT ''最近失败摘要'' AFTER `failure_stage`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE er_yin_stmt FROM @er_yin_ddl;
|
||||
EXECUTE er_yin_stmt;
|
||||
DEALLOCATE PREPARE er_yin_stmt;
|
||||
|
||||
SET @er_yin_ddl = IF(
|
||||
(SELECT COUNT(*) FROM `information_schema`.`columns`
|
||||
WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
|
||||
AND `column_name` = 'printed_at') = 0,
|
||||
'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD COLUMN `printed_at` datetime NULL COMMENT ''终端确认完整USB数据和切纸命令写入成功时间'' AFTER `last_error`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE er_yin_stmt FROM @er_yin_ddl;
|
||||
EXECUTE er_yin_stmt;
|
||||
DEALLOCATE PREPARE er_yin_stmt;
|
||||
|
||||
SET @er_yin_ddl = IF(
|
||||
(SELECT COUNT(*) FROM `information_schema`.`columns`
|
||||
WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
|
||||
AND `column_name` = 'reprint_reason') = 0,
|
||||
'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD COLUMN `reprint_reason` varchar(500) NULL COMMENT ''人工重打或再次核销原因'' AFTER `printed_at`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE er_yin_stmt FROM @er_yin_ddl;
|
||||
EXECUTE er_yin_stmt;
|
||||
DEALLOCATE PREPARE er_yin_stmt;
|
||||
|
||||
-- 唯一索引尚未建立才回填历史发票,避免迁移完成后重跑时覆盖新产生的兑换券任务。
|
||||
SET @er_yin_ddl = IF(
|
||||
(SELECT COUNT(*) FROM `information_schema`.`statistics`
|
||||
WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
|
||||
AND `index_name` = 'uk_hard_invoice_order_type_no') = 0,
|
||||
'UPDATE `pjcloudx_hardware`.`hard_invoice` invoice
|
||||
JOIN (
|
||||
SELECT ranked.`id`, ranked.`print_no`
|
||||
FROM (
|
||||
SELECT `id`, ROW_NUMBER() OVER (
|
||||
PARTITION BY `tenant_id`, `order_id`
|
||||
ORDER BY `create_time`, `id`
|
||||
) AS `print_no`
|
||||
FROM `pjcloudx_hardware`.`hard_invoice`
|
||||
) ranked
|
||||
) ranked_rows ON ranked_rows.`id` = invoice.`id`
|
||||
SET invoice.`print_type` = ''1'',
|
||||
invoice.`print_no` = ranked_rows.`print_no`,
|
||||
invoice.`retry_version` = 0,
|
||||
invoice.`attempt_count` = 0',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE er_yin_stmt FROM @er_yin_ddl;
|
||||
EXECUTE er_yin_stmt;
|
||||
DEALLOCATE PREPARE er_yin_stmt;
|
||||
|
||||
SET @er_yin_ddl = IF(
|
||||
(SELECT COUNT(*) FROM `information_schema`.`statistics`
|
||||
WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
|
||||
AND `index_name` = 'uk_hard_invoice_order_type_no') = 0,
|
||||
'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD UNIQUE KEY `uk_hard_invoice_order_type_no` (`tenant_id`, `order_id`, `print_type`, `print_no`)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE er_yin_stmt FROM @er_yin_ddl;
|
||||
EXECUTE er_yin_stmt;
|
||||
DEALLOCATE PREPARE er_yin_stmt;
|
||||
|
||||
SET @er_yin_ddl = IF(
|
||||
(SELECT COUNT(*) FROM `information_schema`.`statistics`
|
||||
WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
|
||||
AND `index_name` = 'idx_hard_invoice_printer_status') = 0,
|
||||
'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD KEY `idx_hard_invoice_printer_status` (`tenant_id`, `printer_hard_id`, `status`, `update_time`, `del_flag`)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE er_yin_stmt FROM @er_yin_ddl;
|
||||
EXECUTE er_yin_stmt;
|
||||
DEALLOCATE PREPARE er_yin_stmt;
|
||||
|
||||
SET @er_yin_ddl = IF(
|
||||
(SELECT COUNT(*) FROM `information_schema`.`statistics`
|
||||
WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
|
||||
AND `index_name` = 'idx_hard_invoice_source_status') = 0,
|
||||
'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD KEY `idx_hard_invoice_source_status` (`tenant_id`, `hard_id`, `status`, `del_flag`)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE er_yin_stmt FROM @er_yin_ddl;
|
||||
EXECUTE er_yin_stmt;
|
||||
DEALLOCATE PREPARE er_yin_stmt;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pjcloudx_hardware`.`hard_printer_binding`
|
||||
(
|
||||
`id` bigint(20) NOT NULL COMMENT '绑定关系主键',
|
||||
`source_hard_id` bigint(20) NOT NULL COMMENT 'SUPERMARKET或CANTEEN来源业务终端id',
|
||||
`printer_hard_id` bigint(20) NOT NULL COMMENT 'EY_PRINT目标打印终端id',
|
||||
`merchant_id` bigint(20) NOT NULL COMMENT '双方共同所属商户id快照',
|
||||
`dept_id` bigint(20) NOT NULL COMMENT '双方共同所属商户部门id快照',
|
||||
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` varchar(64) DEFAULT NULL COMMENT '修改人',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
|
||||
`tenant_id` bigint(20) NOT NULL COMMENT '租户ID',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_hard_printer_binding_source` (`tenant_id`, `source_hard_id`),
|
||||
UNIQUE KEY `uk_hard_printer_binding_printer` (`tenant_id`, `printer_hard_id`),
|
||||
KEY `idx_hard_printer_binding_merchant` (`tenant_id`, `merchant_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci
|
||||
COMMENT = '业务终端与尔印打印终端一对一绑定';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pjcloudx_hardware`.`hard_printer_config`
|
||||
(
|
||||
`id` bigint(20) NOT NULL COMMENT '配置主键',
|
||||
`printer_hard_id` bigint(20) NOT NULL COMMENT 'EY_PRINT打印终端id',
|
||||
`printer_model` varchar(128) DEFAULT NULL COMMENT '打印机型号',
|
||||
`usb_vendor_id` int(11) DEFAULT NULL COMMENT 'USB Vendor ID十进制值',
|
||||
`usb_product_id` int(11) DEFAULT NULL COMMENT 'USB Product ID十进制值',
|
||||
`paper_width` int(11) NOT NULL DEFAULT 80 COMMENT '纸宽,单位mm,首期固定80',
|
||||
`auto_cut` char(1) NOT NULL DEFAULT '1' COMMENT '自动切刀开关,1开启、0关闭',
|
||||
`image_mode` varchar(16) NOT NULL DEFAULT 'GS_V0' COMMENT '图片指令模式,GS_V0或ESC_STAR',
|
||||
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` varchar(64) DEFAULT NULL COMMENT '修改人',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
|
||||
`tenant_id` bigint(20) NOT NULL COMMENT '租户ID',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_hard_printer_config_printer` (`tenant_id`, `printer_hard_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci
|
||||
COMMENT = '尔印打印终端USB与ESC/POS配置';
|
||||
|
||||
SET @er_yin_ddl = NULL;
|
||||
@@ -0,0 +1,38 @@
|
||||
-- 终端日志文件表(hq-platform 无独立建表脚本,本 DDL 依据 HardTerminalLogFileEntity 与查询场景编写)。
|
||||
-- 记录 Android 终端上传到文件中心的日志文件索引,同一文件重复上传时累加 upload_count 并更新地址。
|
||||
CREATE TABLE IF NOT EXISTS `pjcloudx_hardware`.`hard_terminal_log_file`
|
||||
(
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
|
||||
`hard_id` bigint NOT NULL COMMENT '设备ID(hard_manager.id)',
|
||||
`hard_name` varchar(128) DEFAULT NULL COMMENT '上传时设备名称快照',
|
||||
`unique_code` varchar(128) DEFAULT NULL COMMENT '上传时设备唯一编号快照',
|
||||
`hard_type` char(10) DEFAULT NULL COMMENT '上传时设备类型快照',
|
||||
`merchant_id` bigint DEFAULT NULL COMMENT '上传时商户ID快照',
|
||||
`dept_id` bigint DEFAULT NULL COMMENT '上传时部门ID快照',
|
||||
`log_date` date NOT NULL COMMENT '日志所属日期',
|
||||
`relative_path` varchar(512) NOT NULL COMMENT '终端日志根目录下的相对路径',
|
||||
`original_name` varchar(255) DEFAULT NULL COMMENT '原日志文件名',
|
||||
`file_size` bigint DEFAULT NULL COMMENT '文件大小(字节)',
|
||||
`file_hash` char(32) DEFAULT NULL COMMENT '文件内容哈希(当前使用MD5)',
|
||||
`file_modified_time` datetime DEFAULT NULL COMMENT '终端本地文件最后修改时间',
|
||||
`sys_file_id` bigint DEFAULT NULL COMMENT '文件中心sys_file记录ID',
|
||||
`storage_file_name` varchar(255) DEFAULT NULL COMMENT 'OSS对象文件名',
|
||||
`storage_url` varchar(512) DEFAULT NULL COMMENT '文件访问相对地址',
|
||||
`app_version` varchar(64) DEFAULT NULL COMMENT '上传时应用版本',
|
||||
`sanitize_version` varchar(64) DEFAULT NULL COMMENT '日志脱敏规则版本',
|
||||
`upload_time` datetime DEFAULT NULL COMMENT '最近成功上传时间',
|
||||
`upload_count` int NOT NULL DEFAULT 1 COMMENT '成功上传次数',
|
||||
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
|
||||
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` varchar(64) DEFAULT NULL COMMENT '修改人',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
|
||||
`del_flag` char(1) DEFAULT '0' COMMENT '删除标记',
|
||||
`tenant_id` bigint NOT NULL COMMENT '租户ID',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_hard_terminal_log_position` (`tenant_id`, `hard_id`, `relative_path`, `del_flag`),
|
||||
KEY `idx_hard_terminal_log_date` (`tenant_id`, `hard_id`, `log_date`, `del_flag`),
|
||||
KEY `idx_hard_terminal_log_hash` (`tenant_id`, `file_hash`)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_general_ci COMMENT ='终端日志文件';
|
||||
@@ -0,0 +1,25 @@
|
||||
-- 终端心跳状态表(迁移自 hq-platform 20260730_add_hard_terminal_status.sql)。
|
||||
-- 食堂、超市 Android 终端心跳状态。在线状态由服务端按最后心跳时间实时计算,不冗余落库。
|
||||
CREATE TABLE IF NOT EXISTS `pjcloudx_hardware`.`hard_terminal_status` (
|
||||
`hard_id` bigint NOT NULL COMMENT '设备ID,对应hard_manager.id',
|
||||
`last_online_time` datetime NOT NULL COMMENT '服务端最近收到心跳的时间',
|
||||
`heartbeat_scene` varchar(32) NOT NULL COMMENT '最近心跳场景(ONLINE,AUTO_UPDATE)',
|
||||
`disconnect_reason` varchar(128) DEFAULT NULL COMMENT '计划内断开原因,目前用于标记自动更新重启',
|
||||
`apk_version_name` varchar(64) DEFAULT NULL COMMENT '终端上报的APK展示版本号',
|
||||
`apk_version_code` bigint DEFAULT NULL COMMENT '终端上报的APK内部版本码',
|
||||
`system_uptime_seconds` bigint DEFAULT NULL COMMENT 'Android系统本次启动后运行秒数',
|
||||
`app_uptime_seconds` bigint DEFAULT NULL COMMENT '终端App本次进程运行秒数',
|
||||
`network_type` varchar(32) DEFAULT NULL COMMENT '最近心跳使用的网络类型',
|
||||
`available_storage_bytes` bigint DEFAULT NULL COMMENT '应用存储卷最近可用空间,单位字节',
|
||||
`root_status` varchar(16) DEFAULT NULL COMMENT 'Root状态(unknown,available,unavailable)',
|
||||
`device_owner` tinyint(1) DEFAULT NULL COMMENT '当前应用是否为Device Owner',
|
||||
`kiosk_active` tinyint(1) DEFAULT NULL COMMENT '最近心跳时是否处于Kiosk锁定任务模式',
|
||||
`device_status_json` json DEFAULT NULL COMMENT '打印机、电子秤、读卡器、副屏等可选硬件状态快照',
|
||||
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime NOT NULL COMMENT '创建时间',
|
||||
`update_by` varchar(64) DEFAULT NULL COMMENT '修改人',
|
||||
`update_time` datetime NOT NULL COMMENT '修改时间',
|
||||
`tenant_id` bigint NOT NULL COMMENT '租户ID',
|
||||
PRIMARY KEY (`hard_id`),
|
||||
KEY `idx_hard_terminal_status_tenant_online` (`tenant_id`, `last_online_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='食堂超市终端心跳状态';
|
||||
@@ -0,0 +1,30 @@
|
||||
-- 消费机日志表(迁移自 hq-platform 800pjcloudx_hardware.sql 末段)。
|
||||
-- 记录消费机终端 HTTP 调用日志(req/res/error/耗时),unique_code 唯一。
|
||||
CREATE TABLE IF NOT EXISTS `pjcloudx_hardware`.`hard_xfj_log`
|
||||
(
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
|
||||
`unique_code` varchar(64) NOT NULL COMMENT '设备唯一编号',
|
||||
`hard_type` varchar(32) NOT NULL COMMENT '设备类型',
|
||||
`title` varchar(64) NOT NULL COMMENT '接口标题',
|
||||
`headers` varchar(2048) COMMENT 'headers',
|
||||
`res` varchar(2048) COMMENT 'res',
|
||||
`req` varchar(2048) COMMENT 'req',
|
||||
`error` varchar(2048) COMMENT 'error',
|
||||
`start_time` datetime DEFAULT NULL COMMENT '开始时间',
|
||||
`end_time` datetime DEFAULT NULL COMMENT '结束时间',
|
||||
`cost` int DEFAULT NULL COMMENT '耗时(毫秒)',
|
||||
`create_by` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '修改人',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
|
||||
`del_flag` char(1) COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标记',
|
||||
`tenant_id` bigint DEFAULT NULL COMMENT '租户ID',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_unique_code` (`unique_code`),
|
||||
KEY `idx_hard_type` (`hard_type`),
|
||||
KEY `idx_title` (`title`),
|
||||
KEY `idx_start_time` (`start_time`),
|
||||
KEY `idx_end_time` (`end_time`)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_general_ci COMMENT ='消费机日志';
|
||||
@@ -0,0 +1,7 @@
|
||||
-- 为钱包流水补充财务统计标签和业务备注;历史数据保持为空。
|
||||
-- tags 保存 wallet_transaction_tag 字典值,多个值使用英文逗号分隔;remark 前后端统一限制为 256 个字符。
|
||||
ALTER TABLE wallet_transaction
|
||||
ADD COLUMN tags varchar(512) DEFAULT NULL COMMENT '财务统计标签,多个wallet_transaction_tag字典值使用英文逗号分隔' AFTER balance;
|
||||
|
||||
ALTER TABLE wallet_transaction
|
||||
ADD COLUMN remark varchar(256) DEFAULT NULL COMMENT '流水备注,最多256个字符' AFTER tags;
|
||||
@@ -0,0 +1,49 @@
|
||||
================================================================
|
||||
智能柜订单手动结算(2026-09-08)—— 权限 手动添加清单
|
||||
================================================================
|
||||
说明:第三方(旷世)识别不可用后,智能柜免密订单(payType=9 + 设备类型4)
|
||||
需要在售后订单详情页人工核对视频后手动结算/取消。以下权限需在 pjcloudx
|
||||
主库 sys_menu 手动添加(菜单管理界面配置或执行文末幂等 SQL)。
|
||||
|
||||
----------------------------------------------------------------
|
||||
一、按钮权限(sys_menu,menu_type='2')
|
||||
----------------------------------------------------------------
|
||||
挂载位置建议:挂在「售后订单详情」可访问的角色/菜单下(订单详情页为
|
||||
动态路由 order-refund,无独立菜单,建议挂在 hard 订单相关父菜单下,
|
||||
与 hard_hardwareOrder_view 同级或同父)。
|
||||
|
||||
1. hard_hardwareOrder_manualSettle 订单-智能柜手动结算
|
||||
(售后订单详情页「手动结算」区块:手动结算 + 未拿商品取消订单,共用一个权限)
|
||||
|
||||
----------------------------------------------------------------
|
||||
二、页面菜单
|
||||
----------------------------------------------------------------
|
||||
无新增页面(功能内嵌在已有售后订单详情页 order-refund 中),无需配菜单。
|
||||
|
||||
----------------------------------------------------------------
|
||||
三、字典
|
||||
----------------------------------------------------------------
|
||||
无新增字典。
|
||||
|
||||
----------------------------------------------------------------
|
||||
四、附:可直接执行的幂等 SQL(按需使用,执行前请备份)
|
||||
----------------------------------------------------------------
|
||||
-- 按钮权限(挂在 hard_hardwareOrder_view 所在的父菜单下)
|
||||
SET @menu_id_base = (SELECT GREATEST(COALESCE(MAX(`menu_id`), 0), 2026090801000000000) FROM `pjcloudx`.`sys_menu`);
|
||||
INSERT INTO `pjcloudx`.`sys_menu`
|
||||
(`menu_id`, `name`, `permission`, `parent_id`, `visible`, `sort_order`, `keep_alive`, `embedded`, `menu_type`, `create_by`, `create_time`, `del_flag`, `tenant_id`)
|
||||
SELECT @menu_id_base + ROW_NUMBER() OVER (ORDER BY p.`tenant_id`, p.`menu_id`),
|
||||
'智能柜手动结算', 'hard_hardwareOrder_manualSettle', p.`menu_id`, '1', 20, '0', '0', '2', 'system', NOW(), '0', p.`tenant_id`
|
||||
FROM `pjcloudx`.`sys_menu` p
|
||||
WHERE p.`permission` = 'hard_hardwareOrder_view' AND p.`del_flag` = '0'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `pjcloudx`.`sys_menu` e
|
||||
WHERE e.`tenant_id` = p.`tenant_id` AND e.`permission` = 'hard_hardwareOrder_manualSettle' AND e.`del_flag` = '0'
|
||||
);
|
||||
SET @menu_id_base = NULL;
|
||||
|
||||
----------------------------------------------------------------
|
||||
五、数据库迁移
|
||||
----------------------------------------------------------------
|
||||
无(复用 hard_door_flow.skuCallbackVo / skuCallbackTime 字段记录手动操作留痕,
|
||||
不涉及表结构变更,无需执行任何 SQL)。
|
||||
@@ -0,0 +1,158 @@
|
||||
================================================================
|
||||
hard 模块迁移(2026-09-08)—— 权限/菜单/字典 手动添加清单
|
||||
================================================================
|
||||
说明:以下内容需要在 pjcloudx 主库(sys_menu / sys_dict / sys_dict_item)手动添加,
|
||||
可以直接在「系统管理-菜单管理 / 字典管理」界面配置,也可以执行文末附带的 SQL(幂等,可重复执行)。
|
||||
每项都标注了用途,若目标环境已存在同名项请跳过。
|
||||
|
||||
----------------------------------------------------------------
|
||||
一、按钮权限(sys_menu,menu_type='2')
|
||||
----------------------------------------------------------------
|
||||
挂载位置建议:挂在对应页面的父菜单下(与 hard_hardManager_view 同级)。
|
||||
|
||||
1. hard_hardTerminalLogFile_view 终端日志-查看 (终端日志页面 + 设备详情页日志区)
|
||||
2. hard_hardTerminalLogFile_preview 终端日志-预览
|
||||
3. hard_hardTerminalLogFile_down 终端日志-下载
|
||||
4. hard_hardTerminalLogFile_pull 终端日志-拉取
|
||||
5. hard_hardManager_printer_bind 设备管理-绑定打印机 (设备详情页打印配置区)
|
||||
6. hard_hardManager_export 设备管理-导出 (设备列表导出按钮,若已有则跳过)
|
||||
7. hard_hardInvoice_voucher_reprint 发票凭证-重打兑换券(打印任务弹窗重打,若不需要可不加)
|
||||
|
||||
----------------------------------------------------------------
|
||||
二、页面菜单(sys_menu,menu_type='1',动态路由)
|
||||
----------------------------------------------------------------
|
||||
1. 终端日志(机器日志)
|
||||
路径 path:/hard/hard-terminal-log-file/index
|
||||
权限标识:hard_hardTerminalLogFile_view
|
||||
2. 消费机日志
|
||||
路径 path:/hard/xfj/xfj-log/index
|
||||
权限标识:(查看权限可留空或自定 hard_xfjLog_view,页面本身无按钮权限)
|
||||
|
||||
说明:
|
||||
- 设备详情页 /hard/hard-manager/details 已由前端静态路由注册(isHide),无需配菜单。
|
||||
- 售后订单详情 order-refund 沿用目标库已有菜单,无需改动。
|
||||
|
||||
----------------------------------------------------------------
|
||||
三、字典(sys_dict + sys_dict_item)
|
||||
----------------------------------------------------------------
|
||||
1. hard_type(设备类型,已有字典,补充缺失项):
|
||||
- 9 云版消费机
|
||||
- 12 尔印打印机器
|
||||
(若已有 10/11 云音响等项则不动)
|
||||
2. 新增 hard_print_type(打印凭证类型):
|
||||
- 1 发票
|
||||
- 2 兑换券
|
||||
3. 新增 hard_print_status(打印任务状态):
|
||||
- 0 待生成
|
||||
- 1 待打印
|
||||
- 2 已打印
|
||||
- 3 打印失败
|
||||
- 4 已取消
|
||||
4. 可选:hard_appt_reserve_mode(预约模式,售后详情页预约信息列显示用;
|
||||
目标库若无此字典该列显示空,不影响其他功能)
|
||||
|
||||
----------------------------------------------------------------
|
||||
四、附:可直接执行的幂等 SQL(按需使用,执行前请备份)
|
||||
----------------------------------------------------------------
|
||||
-- 1. hard_type 补类型 9/12(每个已配置 hard_type 的租户幂等插入)
|
||||
SET @dict_item_id_base = (SELECT GREATEST(COALESCE(MAX(`id`), 0), 2026090800000000000) FROM `pjcloudx`.`sys_dict_item`);
|
||||
INSERT INTO `pjcloudx`.`sys_dict_item`
|
||||
(`id`, `dict_id`, `item_value`, `label`, `dict_type`, `description`, `sort_order`, `create_by`, `create_time`, `remarks`, `del_flag`, `tenant_id`)
|
||||
SELECT @dict_item_id_base + ROW_NUMBER() OVER (ORDER BY d.`tenant_id`, item.`item_value`),
|
||||
d.`id`, item.`item_value`, item.`label`, 'hard_type', item.`label`, CAST(item.`item_value` AS SIGNED),
|
||||
'system', NOW(), item.`label`, '0', d.`tenant_id`
|
||||
FROM `pjcloudx`.`sys_dict` d
|
||||
JOIN (
|
||||
SELECT '9' AS `item_value`, '云版消费机' AS `label`
|
||||
UNION ALL SELECT '12', '尔印打印机器'
|
||||
) item
|
||||
WHERE d.`dict_type` = 'hard_type' AND d.`del_flag` = '0'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `pjcloudx`.`sys_dict_item` i
|
||||
WHERE i.`tenant_id` = d.`tenant_id` AND i.`dict_type` = 'hard_type'
|
||||
AND i.`item_value` = item.`item_value` AND i.`del_flag` = '0'
|
||||
);
|
||||
SET @dict_item_id_base = NULL;
|
||||
|
||||
-- 2. hard_print_type / hard_print_status 字典(按租户幂等创建)
|
||||
SET @dict_id_base = (SELECT GREATEST(COALESCE(MAX(`id`), 0), 2026090800000000000) FROM `pjcloudx`.`sys_dict`);
|
||||
INSERT INTO `pjcloudx`.`sys_dict`
|
||||
(`id`, `dict_type`, `description`, `create_by`, `create_time`, `remarks`, `system_flag`, `del_flag`, `tenant_id`)
|
||||
SELECT @dict_id_base + ROW_NUMBER() OVER (ORDER BY tenant.`tenant_id`, t.`dict_type`),
|
||||
t.`dict_type`, t.`description`, 'system', NOW(), t.`description`, '1', '0', tenant.`tenant_id`
|
||||
FROM (SELECT DISTINCT `tenant_id` FROM `pjcloudx`.`sys_dict` WHERE `del_flag` = '0') tenant
|
||||
CROSS JOIN (
|
||||
SELECT 'hard_print_type' AS `dict_type`, '打印凭证类型' AS `description`
|
||||
UNION ALL SELECT 'hard_print_status', '打印任务状态'
|
||||
) t
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `pjcloudx`.`sys_dict` d
|
||||
WHERE d.`tenant_id` = tenant.`tenant_id` AND d.`dict_type` = t.`dict_type` AND d.`del_flag` = '0'
|
||||
);
|
||||
SET @dict_id_base = NULL;
|
||||
|
||||
SET @dict_item_id_base = (SELECT GREATEST(COALESCE(MAX(`id`), 0), 2026090800000000000) FROM `pjcloudx`.`sys_dict_item`);
|
||||
INSERT INTO `pjcloudx`.`sys_dict_item`
|
||||
(`id`, `dict_id`, `item_value`, `label`, `dict_type`, `description`, `sort_order`, `create_by`, `create_time`, `del_flag`, `tenant_id`)
|
||||
SELECT @dict_item_id_base + ROW_NUMBER() OVER (ORDER BY d.`tenant_id`, d.`dict_type`, item.`sort_order`, item.`item_value`),
|
||||
d.`id`, item.`item_value`, item.`label`, d.`dict_type`, item.`label`, item.`sort_order`,
|
||||
'system', NOW(), '0', d.`tenant_id`
|
||||
FROM `pjcloudx`.`sys_dict` d
|
||||
JOIN (
|
||||
SELECT 'hard_print_type' AS `dict_type`, '1' AS `item_value`, '发票' AS `label`, 1 AS `sort_order`
|
||||
UNION ALL SELECT 'hard_print_type', '2', '兑换券', 2
|
||||
UNION ALL SELECT 'hard_print_status', '0', '待生成', 0
|
||||
UNION ALL SELECT 'hard_print_status', '1', '待打印', 1
|
||||
UNION ALL SELECT 'hard_print_status', '2', '已打印', 2
|
||||
UNION ALL SELECT 'hard_print_status', '3', '打印失败', 3
|
||||
UNION ALL SELECT 'hard_print_status', '4', '已取消', 4
|
||||
) item ON item.`dict_type` = d.`dict_type`
|
||||
WHERE d.`del_flag` = '0'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `pjcloudx`.`sys_dict_item` i
|
||||
WHERE i.`tenant_id` = d.`tenant_id` AND i.`dict_type` = d.`dict_type`
|
||||
AND i.`item_value` = item.`item_value` AND i.`del_flag` = '0'
|
||||
);
|
||||
SET @dict_item_id_base = NULL;
|
||||
|
||||
-- 3. 按钮权限(挂到已有父权限所在菜单下,menu_type='2')
|
||||
-- 先在界面确认父菜单,或使用下面按 permission 定位父菜单的幂等插入:
|
||||
SET @menu_id_base = (SELECT GREATEST(COALESCE(MAX(`menu_id`), 0), 2026090800000000000) FROM `pjcloudx`.`sys_menu`);
|
||||
INSERT INTO `pjcloudx`.`sys_menu`
|
||||
(`menu_id`, `name`, `permission`, `parent_id`, `visible`, `sort_order`, `keep_alive`, `embedded`, `menu_type`, `create_by`, `create_time`, `del_flag`, `tenant_id`)
|
||||
SELECT @menu_id_base + ROW_NUMBER() OVER (ORDER BY p.`tenant_id`, p.`menu_id`, item.`permission`),
|
||||
item.`name`, item.`permission`, p.`menu_id`, '1', 20, '0', '0', '2', 'system', NOW(), '0', p.`tenant_id`
|
||||
FROM `pjcloudx`.`sys_menu` p
|
||||
JOIN (
|
||||
SELECT 'hard_hardTerminalLogFile_view' AS `permission`, '终端日志查看' AS `name`, 'hard_hardManager_view' AS `parent_permission`
|
||||
UNION ALL SELECT 'hard_hardTerminalLogFile_preview', '终端日志预览', 'hard_hardManager_view'
|
||||
UNION ALL SELECT 'hard_hardTerminalLogFile_down', '终端日志下载', 'hard_hardManager_view'
|
||||
UNION ALL SELECT 'hard_hardTerminalLogFile_pull', '终端日志拉取', 'hard_hardManager_view'
|
||||
UNION ALL SELECT 'hard_hardManager_printer_bind', '绑定打印机', 'hard_hardManager_view'
|
||||
UNION ALL SELECT 'hard_hardManager_export', '导出设备', 'hard_hardManager_view'
|
||||
UNION ALL SELECT 'hard_hardInvoice_voucher_reprint', '重打兑换券', 'hard_hardInvoice_view'
|
||||
) item ON item.`parent_permission` = p.`permission`
|
||||
WHERE p.`del_flag` = '0'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `pjcloudx`.`sys_menu` e
|
||||
WHERE e.`tenant_id` = p.`tenant_id` AND e.`permission` = item.`permission` AND e.`del_flag` = '0'
|
||||
);
|
||||
SET @menu_id_base = NULL;
|
||||
|
||||
-- 4. 页面菜单(终端日志 / 消费机日志)—— parent_id 请替换为实际的「设备管理/硬件」目录菜单ID
|
||||
-- (建议直接在菜单管理界面添加,下面仅作字段参考)
|
||||
-- INSERT INTO `pjcloudx`.`sys_menu`
|
||||
-- (`menu_id`,`name`,`permission`,`path`,`parent_id`,`icon`,`visible`,`sort_order`,`keep_alive`,`embedded`,`menu_type`,`create_by`,`create_time`,`del_flag`,`tenant_id`)
|
||||
-- VALUES
|
||||
-- (自定义ID, '终端日志', 'hard_hardTerminalLogFile_view', '/hard/hard-terminal-log-file/index', <父菜单ID>, NULL, '1', 100, '0', NULL, '1', 'admin', NOW(), '0', 1),
|
||||
-- (自定义ID, '消费机日志', NULL, '/hard/xfj/xfj-log/index', <父菜单ID>, NULL, '1', 101, '0', NULL, '1', 'admin', NOW(), '0', 1);
|
||||
|
||||
----------------------------------------------------------------
|
||||
五、数据库迁移 SQL 执行顺序(pj_service_cloud/db/ 下本次新增)
|
||||
----------------------------------------------------------------
|
||||
1. 20260908_add_hard_terminal_status.sql
|
||||
2. 20260908_add_hard_terminal_log_file.sql
|
||||
3. 20260908_add_hard_order_evidence_image.sql
|
||||
4. 20260908_add_hard_printer.sql (含 hard_invoice 打印任务化,注意先备份)
|
||||
5. 20260908_add_hard_xfj_log.sql
|
||||
6. 20260908_add_hard_order_verification_time.sql
|
||||
@@ -0,0 +1,18 @@
|
||||
-- 订单分页(POST hard/order/page-join)性能优化配套索引(OceanBase MySQL 模式)
|
||||
-- 背景:HardOrderMapper.xml 已参考 biz-api-cloud-zax 将"每单最新售后"的全表 GROUP BY
|
||||
-- 派生表改写为相关标量子查询:
|
||||
-- select max(id) from hard_after_sale where order_id = ? and del_flag = '0'
|
||||
-- 该写法需要 (order_id, del_flag, id) 前导索引支撑;此前 slowlog_optimization_20260826
|
||||
-- 推荐的 (tenant_id, order_id, id) 因租户列在前导位,无法服务新子查询。
|
||||
-- 执行前请确认同名索引不存在;建议低峰期执行并观察建索引耗时。本文件只包含索引 DDL。
|
||||
|
||||
CREATE INDEX idx_hard_after_sale_order_del_id
|
||||
ON pjcloudx_hardware.hard_after_sale (order_id, del_flag, id);
|
||||
|
||||
-- 说明:
|
||||
-- 1) 若 db/slowlog_optimization_20260826/01_add_indexes.sql 尚未执行,请一并执行其中
|
||||
-- idx_hard_order_tenant_del_ct_uid (tenant_id, del_flag, create_time, user_id):
|
||||
-- 订单列表 ORDER BY create_time desc, user_id desc, id desc 依赖该索引做反向范围扫描
|
||||
-- (InnoDB/OceanBase 二级索引隐含主键 id 后缀,id desc 排序可直接复用,无需额外索引)。
|
||||
-- 2) 20260826 中的 idx_hard_after_sale_tenant_order_id 服务于旧派生表写法;本次新索引
|
||||
-- 上线后如确认无其他统计 SQL 依赖,可自行评估删除,本文件不强制处理。
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 智能柜手动结算「操作快照」字段(2026-09-09)
|
||||
-- 用途:手动结算/取消订单时,在 hard_door_flow 保存完整操作快照
|
||||
-- (操作人id/姓名/时间/购物车明细含商品名单价/合计/支付方式/备注),供事后二次判断复核
|
||||
-- 与旷世识别回调字段(sku_callback_vo)互不干扰。
|
||||
ALTER TABLE hard_door_flow
|
||||
ADD COLUMN manual_settle_vo text DEFAULT NULL COMMENT '手动结算/取消操作快照(操作人+购物车明细+金额+备注)';
|
||||
@@ -0,0 +1,8 @@
|
||||
-- 终端虹软静态图片质量结果列(迁移自 hq-platform/biz-api-cloud-zax db/20260901_add_sys_user_face_quality.sql)。
|
||||
-- 保存终端虹软静态图片质量结果;旧终端和历史人脸保持 NULL,不影响现有人脸业务。
|
||||
ALTER TABLE sys_user_face
|
||||
ADD COLUMN quality_score decimal(5, 4) DEFAULT NULL COMMENT '虹软静态图片质量分,范围0-1' AFTER face_img,
|
||||
ADD COLUMN quality_status varchar(16) DEFAULT NULL COMMENT '终端检测状态:detected/failed' AFTER quality_score,
|
||||
ADD COLUMN quality_checked_at datetime DEFAULT NULL COMMENT '终端完成质量检测的时间' AFTER quality_status,
|
||||
ADD COLUMN quality_error_code int DEFAULT NULL COMMENT '虹软或终端质量检测错误码' AFTER quality_checked_at,
|
||||
ADD COLUMN quality_device_id bigint DEFAULT NULL COMMENT '最后一次上报质量的终端ID' AFTER quality_error_code;
|
||||
@@ -0,0 +1,206 @@
|
||||
-- ================================================================
|
||||
-- 智能柜订单手动结算 —— 测试数据脚本(2026-09-09)
|
||||
-- ================================================================
|
||||
-- 用途:在【测试环境】生成 10 笔可手动结算的智能柜免密订单
|
||||
-- pay_type='9'(智能柜免密) + sys_type='4'(生鲜柜) + order_status='1'(待结算)
|
||||
-- 覆盖表:hard_order(订单)/ hard_auth(用户授权)/ hard_door_flow(智能柜流程)
|
||||
-- 标记:三张表 create_by='test-manual-settle',可重复执行(先清理再生成)
|
||||
-- 执行:mysql -h127.0.0.1 -P33905 -uroot -p pjcloudx_hardware < 本文件
|
||||
--
|
||||
-- 前置条件(缺一不可,脚本会自动挑选并检查):
|
||||
-- 1. 已有智能柜设备:hard_door_manager + hard_door_cabinet 有数据
|
||||
-- 2. 柜内已配置商品:hard_spu_join 有 spu_id 指向有效 hard_spu 的记录
|
||||
-- (否则订单详情页手动结算区显示"未找到该柜的商品配置",无法结算)
|
||||
-- 3. 钱包库存在消费钱包(wallet_type.unique_code='2',同租户优先)
|
||||
-- —— 结算时会真实扣款(force 强制扣款允许透支),并真实生成钱包流水
|
||||
-- 说明:
|
||||
-- - 本地 docker 为分库架构:硬件=pjcloudx_hardware 钱包=pjcloudx_wallet 用户=pjcloudx;
|
||||
-- 单库环境请去掉 SQL 里的 pjcloudx_wallet. / pjcloudx. 库名前缀
|
||||
-- - 订单 order_type='1'(BELOW 线下购买),与生产 TOB 用户走钱包扣款一致;
|
||||
-- 不要用 TOC_VISITOR('8'),那会走支付宝免密扣款,测试环境无第三方单会结算失败
|
||||
-- - 流程 flow_status='50'(已提交旷世识别、等待回调),即旷世跑路后的真实卡死状态
|
||||
-- - 视频为公网示例视频(主摄 vjs.zencdn.net / 辅摄 MDN,浏览器实测可达;
|
||||
-- media.w3.org / Google 桶在部分网络环境会被阻断,勿用),每单主摄/辅摄两路
|
||||
-- - 极老库若报 parent_car_id 不能为 NULL,先执行:
|
||||
-- ALTER TABLE hard_order MODIFY parent_car_id bigint NULL DEFAULT NULL;
|
||||
-- ================================================================
|
||||
|
||||
|
||||
-- ---------- 一、清理旧测试数据(脚本可重复执行) ----------
|
||||
DELETE FROM hard_door_flow WHERE create_by = 'test-manual-settle';
|
||||
DELETE FROM hard_auth WHERE create_by = 'test-manual-settle';
|
||||
DELETE FROM hard_order WHERE create_by = 'test-manual-settle';
|
||||
|
||||
-- ---------- 二、自动挑选基础数据 ----------
|
||||
-- 智能柜:优先挑有商品配置的柜子(hard_door_manager.id / 设备id / 租户 / 设备编号)
|
||||
SELECT dm.id, dm.hard_id, dm.tenant_id, dm.device_code
|
||||
INTO @hard_door_id, @hard_id, @tenant_id, @device_code
|
||||
FROM hard_door_manager dm
|
||||
WHERE dm.del_flag = '0'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM hard_spu_join j
|
||||
WHERE j.hard_door_id = dm.id AND j.del_flag = '0' AND j.spu_id IS NOT NULL
|
||||
)
|
||||
ORDER BY dm.id
|
||||
LIMIT 1;
|
||||
|
||||
-- 货柜号:挑该柜配置商品最多的货柜
|
||||
SELECT j.cabinet_no INTO @cabinet_no
|
||||
FROM hard_spu_join j
|
||||
WHERE j.hard_door_id = @hard_door_id AND j.del_flag = '0' AND j.spu_id IS NOT NULL
|
||||
GROUP BY j.cabinet_no
|
||||
ORDER BY COUNT(*) DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- 商户信息:从设备取
|
||||
SELECT MAX(m.merchant_id), MAX(m.dept_id) INTO @merchant_id, @merchant_dept_id
|
||||
FROM hard_manager m
|
||||
WHERE m.id = @hard_id AND m.del_flag = '0';
|
||||
|
||||
-- 商户类型:优先沿用同商户已有订单的 merchant_type(查不到则为 NULL,不影响结算)
|
||||
SELECT MAX(o.merchant_type) INTO @merchant_type
|
||||
FROM hard_order o
|
||||
WHERE o.merchant_id = @merchant_id AND o.merchant_type IS NOT NULL;
|
||||
|
||||
-- 测试用户 + 消费钱包(wallet_type.unique_code='2'),同租户优先
|
||||
SELECT w.user_id, w.id INTO @user_id, @wallet_id
|
||||
FROM pjcloudx_wallet.wallet_manage w
|
||||
JOIN pjcloudx_wallet.wallet_type t ON t.id = w.wallet_type_id AND t.del_flag = '0' AND t.unique_code = '2'
|
||||
WHERE w.del_flag = '0'
|
||||
ORDER BY (w.tenant_id = @tenant_id) DESC, w.id
|
||||
LIMIT 1;
|
||||
|
||||
-- 用户部门(sys_user 不在本实例时可注释掉,手工 SET @user_dept_id)
|
||||
SELECT MAX(u.dept_id) INTO @user_dept_id
|
||||
FROM pjcloudx.sys_user u
|
||||
WHERE u.user_id = @user_id;
|
||||
|
||||
-- 测试主键基址:各表当前最大 id + 100 亿,避开现有与后续雪花 id
|
||||
SELECT COALESCE(MAX(id), 0) + 10000000000 INTO @order_id_base FROM hard_order;
|
||||
SELECT COALESCE(MAX(id), 0) + 10000000000 INTO @flow_id_base FROM hard_door_flow;
|
||||
SELECT COALESCE(MAX(id), 0) + 10000000000 INTO @auth_id_base FROM hard_auth;
|
||||
|
||||
-- ★ 检查:除 @merchant_type 外下列都不能为 NULL;为 NULL 说明缺前置数据,
|
||||
-- 请手工 SET 对应变量(或先在 柜机管理页 给柜子上架商品)后重跑
|
||||
SELECT @hard_door_id AS 柜配置id,
|
||||
@hard_id AS 设备id,
|
||||
@cabinet_no AS 货柜号,
|
||||
@tenant_id AS 租户,
|
||||
@merchant_id AS 商户id,
|
||||
@merchant_dept_id AS 商户部门,
|
||||
@merchant_type AS 商户类型_可空,
|
||||
@user_id AS 用户id,
|
||||
@wallet_id AS 钱包id,
|
||||
@order_id_base AS 订单id基址;
|
||||
|
||||
-- ---------- 三、生成 10 组 授权 / 流程 / 订单 ----------
|
||||
|
||||
-- 1. 用户授权(hard_auth):已授权、未完成
|
||||
INSERT INTO hard_auth
|
||||
(id, user_id, hard_id, cabinet_no, auth_type, auth_status, auth_time, is_done, create_by, create_time, del_flag, tenant_id)
|
||||
SELECT @auth_id_base + n, @user_id, @hard_id, @cabinet_no, 'TEST', '1',
|
||||
NOW() - INTERVAL (n * 2) HOUR, '0',
|
||||
'test-manual-settle', NOW() - INTERVAL (n * 2) HOUR, '0', @tenant_id
|
||||
FROM (
|
||||
SELECT 1 AS n UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5
|
||||
UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL SELECT 10
|
||||
) seq;
|
||||
|
||||
-- 2. 智能柜流程(hard_door_flow):卡在"已提交识别、等待旷世回调"(flow_status='50')
|
||||
INSERT INTO hard_door_flow
|
||||
(id, user_id, user_type, biz_type, hard_id, hard_door_id, cabinet_no, order_id, auth_id,
|
||||
request_open_dto, request_open_vo, request_open_time,
|
||||
is_open, open_callback_time, open_callback_vo,
|
||||
video_callback_vo, video_callback_time,
|
||||
sku_submit_dto, sku_submit_vo, sku_submit_recog_id, sku_submit_time,
|
||||
flow_status, create_by, create_time, del_flag, tenant_id)
|
||||
SELECT
|
||||
@flow_id_base + n, @user_id, '0', '10', @hard_id, @hard_door_id, @cabinet_no,
|
||||
@order_id_base + n, @auth_id_base + n,
|
||||
-- 请求开门参数/返回
|
||||
CONCAT('{"outOpenNo":"', @flow_id_base + n, '","hardDoorId":', @hard_door_id, ',"cabinetNo":', @cabinet_no, ',"userId":', @user_id, '}'),
|
||||
CONCAT('{"code":0,"msg":"成功","data":{"outOpenNo":"', @flow_id_base + n, '","isOpen":true}}'),
|
||||
NOW() - INTERVAL (n * 2 - 2) MINUTE,
|
||||
-- 开门回调(成功开门)
|
||||
'1', NOW() - INTERVAL (n * 2 - 1) MINUTE,
|
||||
CONCAT('{"eventId":"TEST-EVT-', n, '","outOpenNo":"', @flow_id_base + n, '","deviceCode":"', IFNULL(@device_code, 0), '","cabinetNo":', @cabinet_no, ',"resultCode":"SUCCESS"}'),
|
||||
-- 视频回调(公网示例视频,主摄/辅摄两路)
|
||||
CONCAT('{"eventId":"TEST-EVT-', n, '","outOpenNo":"', @flow_id_base + n, '","deviceCode":"', IFNULL(@device_code, 0),
|
||||
'","cabinetNo":', @cabinet_no, ',"resultCode":"SUCCESS","resultMsg":null,"videoList":[',
|
||||
'{"cameraNo":1,"videoUrl":"https://vjs.zencdn.net/v/oceans.mp4"},',
|
||||
'{"cameraNo":2,"videoUrl":"https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"}]}'),
|
||||
NOW() - INTERVAL (n * 2) HOUR + INTERVAL 3 MINUTE,
|
||||
-- 已提交旷世识别(模拟旷世跑路前的最后一步,识别回调永远不回来)
|
||||
CONCAT('{"recog_id":"', @flow_id_base + n, '","resource_type":"1","container_id":"', @hard_door_id,
|
||||
'","sub_mch_id":"', @order_id_base + n,
|
||||
'","resource_urls":["https://media.w3.org/2010/05/sintel/trailer.mp4"]}'),
|
||||
CONCAT('{"code":0,"is_success":true,"ks_recog_id":"TEST-RECOG-', n, '","msg":"提交成功"}'),
|
||||
CONCAT('TEST-RECOG-', n),
|
||||
NOW() - INTERVAL (n * 2) HOUR + INTERVAL 4 MINUTE,
|
||||
-- 流程状态 50=已提交识别;sku_callback_*/pay_time 留空(未结算)
|
||||
'50', 'test-manual-settle', NOW() - INTERVAL (n * 2) HOUR, '0', @tenant_id
|
||||
FROM (
|
||||
SELECT 1 AS n UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5
|
||||
UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL SELECT 10
|
||||
) seq;
|
||||
|
||||
-- 3. 订单(hard_order):待结算的智能柜免密订单(形态对齐后端 createHardDoorOrder)
|
||||
INSERT INTO hard_order
|
||||
(id, user_id, user_dept_id, merchant_id, merchant_type, merchant_dept_id, hard_id, wallet_id,
|
||||
order_type, order_status, pay_type, sys_type,
|
||||
total_price, extra_price, pay_price, is_offline_order, product_desc,
|
||||
create_by, create_time, del_flag, tenant_id)
|
||||
SELECT @order_id_base + n, @user_id, @user_dept_id, @merchant_id, @merchant_type, @merchant_dept_id,
|
||||
@hard_id, @wallet_id,
|
||||
'1', '1', '9', '4',
|
||||
0, 0, 0, '0', CONCAT('智能柜手动结算测试订单-', n),
|
||||
'test-manual-settle', NOW() - INTERVAL (n * 2) HOUR, '0', @tenant_id
|
||||
FROM (
|
||||
SELECT 1 AS n UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5
|
||||
UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL SELECT 10
|
||||
) seq;
|
||||
|
||||
-- ---------- 四、生成结果检查 ----------
|
||||
SELECT o.id AS 订单id,
|
||||
o.product_desc AS 商品描述,
|
||||
o.order_status AS 订单状态,
|
||||
o.pay_type AS 支付方式,
|
||||
o.sys_type AS 设备类型,
|
||||
f.id AS 流程id,
|
||||
f.flow_status AS 流程状态,
|
||||
f.cabinet_no AS 货柜号,
|
||||
a.id AS 授权id,
|
||||
a.is_done AS 授权完成
|
||||
FROM hard_order o
|
||||
JOIN hard_door_flow f ON f.order_id = o.id
|
||||
JOIN hard_auth a ON a.id = f.auth_id
|
||||
WHERE o.create_by = 'test-manual-settle'
|
||||
ORDER BY o.id;
|
||||
|
||||
-- ================================================================
|
||||
-- 测试入口与验证步骤
|
||||
-- ================================================================
|
||||
-- 1. 用对该订单商户有数据范围的管理员账号登录后台
|
||||
-- 2. 入口一:订单管理 → 未识别订单(hard-recog,本批次 10 笔会出现在列表)→ 订单详情
|
||||
-- 入口二:订单管理 → 订单列表 → 订单详情(售后订单详情页 order-refund)
|
||||
-- 3. 详情页右侧导航应出现「手动结算」:
|
||||
-- - 左侧为所选货柜按层商品(来自 hard_spu_join 配置),可调数量
|
||||
-- - 右侧为已选购物车 + 自动播放的公网示例视频(主摄/辅摄可切换)
|
||||
-- 4. 点「手动结算」→ 确认 → 期望:
|
||||
-- - 订单 order_status 变为 5(已支付)、pay_time/pay_price/total_price 写入
|
||||
-- - 生成 hard_parent_car / hard_car 商品明细,详情页"购物车"区出现商品
|
||||
-- - 智能柜库存扣减(hard_spu.repertory_count / hard_spu_join.repertory_count 减少)
|
||||
-- - hard_door_flow.flow_status 变 70、pay_time 写入、sku_callback_vo 记录手动结算留痕
|
||||
-- - hard_auth.is_done 变 1
|
||||
-- - 钱包库生成一笔 PAYMENT 扣款流水(允许透支)
|
||||
-- 5. 点「用户未拿商品,取消订单」→ 确认 → 期望:order_status 变 30(已取消)、
|
||||
-- flow 的 sku_callback_vo 记录手动取消留痕
|
||||
--
|
||||
-- ---------- 测试完成后的清理 ----------
|
||||
-- 重新执行本脚本第一、二节即可重置 10 笔新订单;
|
||||
-- 已结算订单产生的购物车明细按需清理:
|
||||
-- DELETE pc, c FROM hard_parent_car pc
|
||||
-- LEFT JOIN hard_car c ON c.parent_car_id = pc.id
|
||||
-- WHERE pc.id IN (SELECT parent_car_id FROM hard_order
|
||||
-- WHERE create_by = 'test-manual-settle' AND parent_car_id IS NOT NULL);
|
||||
-- 钱包流水在钱包库 wallet_transaction(biz_type='PAYMENT', biz_id=订单id),按需处理
|
||||
@@ -0,0 +1,42 @@
|
||||
-- =====================================================================
|
||||
-- 打印任务补偿扫描:由 hard 模块内 @Scheduled 改为 quartz 调度中心统一触发
|
||||
-- 接口:GET /hard-invoice/task/process-due(HardInvoiceController#processDueTasks)
|
||||
--
|
||||
-- 上线步骤:
|
||||
-- 1. 按部署环境修改 execute_path(默认为本机直连示例):
|
||||
-- - 直连硬件服务(推荐,调度中心与服务同内网):
|
||||
-- http://<hard服务地址>:6080/hard-invoice/task/process-due
|
||||
-- - 走网关:
|
||||
-- http://<网关地址>:9999/hard/hard-invoice/task/process-due
|
||||
-- 2. 执行本 INSERT;
|
||||
-- 3. 在管理端「工具 / 任务管理」页面找到该任务点击「启动」
|
||||
-- (job_status=1 已发布不会自动运行,启动后变为 2 运行中)。
|
||||
--
|
||||
-- 注意:
|
||||
-- * pjcloud-daemon-quartz 当前 quartz isClustered=false,调度中心必须单实例部署;
|
||||
-- 调度中心多实例会导致同一触发点被多次执行(接口内分布式锁仅兜底重叠执行,
|
||||
-- 不能区分多次正常触发)。
|
||||
-- * 任务必须挂在租户 1 下管理;接口内部已跳过租户过滤,扫描全部租户的任务。
|
||||
-- * 该 SQL 不含 ON DUPLICATE KEY,重复执行会主键冲突;若已在页面调整过任务,
|
||||
-- 请勿重复执行,以免覆盖手工配置。
|
||||
-- =====================================================================
|
||||
INSERT INTO `pjcloudx_job`.`sys_job` (
|
||||
`job_id`, `job_name`, `job_group`, `job_order`, `job_type`, `execute_path`,
|
||||
`cron_expression`, `misfire_policy`, `job_tenant_type`, `job_status`,
|
||||
`job_execute_status`, `create_by`, `tenant_id`, `remark`
|
||||
) VALUES (
|
||||
1949000000000000001,
|
||||
'hard-print-task-process-due',
|
||||
'DEFAULT',
|
||||
'1',
|
||||
'3',
|
||||
'http://127.0.0.1:6080/hard-invoice/task/process-due',
|
||||
'0/10 * * * * ?',
|
||||
'3',
|
||||
'2',
|
||||
'1',
|
||||
'0',
|
||||
'admin',
|
||||
1,
|
||||
'智能柜打印任务补偿扫描(原 HardPrintTaskJob,每10秒扫描待生成/待补偿下发的打印任务)'
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
-- wallet_code_use 补二级索引(饭点卡顿波次 2026-09-14 诊断配套,PolarDB 分布式版 MySQL8 兼容模式)
|
||||
-- 背景:该表此前仅有主键,慢日志实测 UPDATE 全表扫描并锁定全部扫描行:
|
||||
-- UPDATE wallet_code_use SET del_flag = '1'
|
||||
-- WHERE del_flag = '0' AND user_id = ? AND biz_type = '5' AND biz_id = ?
|
||||
-- 扫描 14,653 行、单次耗时最长达 11 秒(波次期间),锁面放大拥堵。
|
||||
-- 索引设计说明:biz_type 为 tinytext 无法进入普通二级索引;user_id + biz_id 组合
|
||||
-- 已可把扫描范围缩到该用户名下几行,biz_type 在索引外过滤,无需进索引。
|
||||
-- idx_wcu_biz 单列索引服务 WalletCodeUseServiceImpl.getInfoByShortCode 按 biz_id 查询。
|
||||
-- 执行前请确认同名索引不存在:SHOW INDEX FROM pjcloudx_wallet.wallet_code_use;
|
||||
-- 表仅约 1.5 万行,Online DDL 秒级完成,无锁风险;本文件只包含索引 DDL。
|
||||
|
||||
ALTER TABLE pjcloudx_wallet.wallet_code_use
|
||||
ADD INDEX idx_wcu_user_biz (user_id, biz_id),
|
||||
ADD INDEX idx_wcu_biz (biz_id);
|
||||
|
||||
-- 说明:
|
||||
-- 1) 全量建库脚本 db/800pjcloudx_wallet.sql 已同步补上这两个索引,新建环境无需再执行本文件。
|
||||
-- 2) 两条索引合并在一条 ALTER 中执行,只做一次表重建;如需拆开单独执行(参考
|
||||
-- hard_order verification_time 加列时的生产执行惯例),可改为两条独立的 CREATE INDEX 语句:
|
||||
-- CREATE INDEX idx_wcu_user_biz ON pjcloudx_wallet.wallet_code_use (user_id, biz_id);
|
||||
-- CREATE INDEX idx_wcu_biz ON pjcloudx_wallet.wallet_code_use (biz_id);
|
||||
@@ -0,0 +1,4 @@
|
||||
-- 商户表新增优惠折扣字段:取值 1-99,写 10 表示 1 折(值越小折扣越大),为空表示无折扣。
|
||||
-- app 端商户列表(/wallet/mch-manage/app/page-join)按"有折扣优先、折扣大(值小)优先"排序。
|
||||
ALTER TABLE mch_manage
|
||||
ADD COLUMN discounts_rate int DEFAULT NULL COMMENT '优惠折扣(1-99,10=1折)' AFTER discounts_price;
|
||||
@@ -0,0 +1,32 @@
|
||||
-- 回填 mch_manage.discounts_rate:
|
||||
-- 从 discounts_price 文案(如"享9.5折"、"享菜品7.8折(不含酒水)"、"全场95折")中提取"折"前面的数字,
|
||||
-- 折算规则与 discounts_rate 语义(1-99,10=1折)一致:
|
||||
-- 小数或个位整数("9.5折"->95、"9折"->90、"0.5折"->5) 乘以10;
|
||||
-- 两位整数("95折")本身即百分比,保持原值 -> 95。
|
||||
-- 仅填充 discounts_rate 为空的行,已配置的值不覆盖;提取不到"数字+折"或折算结果不在 1-99 的行跳过。
|
||||
|
||||
-- ① 预览:执行前先跑这条,人工确认折算结果
|
||||
SELECT id, name, discounts_price,
|
||||
ROUND(CAST(REPLACE(REGEXP_SUBSTR(discounts_price, '[0-9]+(\\.[0-9]+)?折'), '折', '') AS DECIMAL(10, 2))
|
||||
* IF(CAST(REPLACE(REGEXP_SUBSTR(discounts_price, '[0-9]+(\\.[0-9]+)?折'), '折', '') AS DECIMAL(10, 2)) < 10, 10, 1)) AS will_rate
|
||||
FROM mch_manage
|
||||
WHERE discounts_rate IS NULL
|
||||
AND discounts_price REGEXP '[0-9]+(\\.[0-9]+)?折';
|
||||
|
||||
-- ② 执行回填
|
||||
UPDATE mch_manage m
|
||||
JOIN (SELECT id,
|
||||
ROUND(CAST(REPLACE(REGEXP_SUBSTR(discounts_price, '[0-9]+(\\.[0-9]+)?折'), '折', '') AS DECIMAL(10, 2))
|
||||
* IF(CAST(REPLACE(REGEXP_SUBSTR(discounts_price, '[0-9]+(\\.[0-9]+)?折'), '折', '') AS DECIMAL(10, 2)) < 10,
|
||||
10, 1)) AS rate
|
||||
FROM mch_manage
|
||||
WHERE discounts_rate IS NULL
|
||||
AND discounts_price REGEXP '[0-9]+(\\.[0-9]+)?折') t ON t.id = m.id
|
||||
SET m.discounts_rate = t.rate
|
||||
WHERE t.rate BETWEEN 1 AND 99;
|
||||
|
||||
-- ③ 核对:回填结果与原文对照
|
||||
SELECT id, name, discounts_price, discounts_rate
|
||||
FROM mch_manage
|
||||
WHERE discounts_rate IS NOT NULL
|
||||
ORDER BY update_time DESC;
|
||||
@@ -0,0 +1,47 @@
|
||||
-- 钱包流水分页接口慢查询索引优化(生产 PolarDB,只加索引不改代码)
|
||||
-- 影响接口:/wallet-transaction/page-join 及 page-cz / page-jz / page-xf / page-tk / page-th / app/page
|
||||
-- (后端全部走 WalletTransactionMapper.scopePageList + count,SQL 形态一致)
|
||||
--
|
||||
-- 慢因:wallet_transaction 只有 PRIMARY(id)、idx_dept_id(wallet_id)、
|
||||
-- idx_wallet_transaction_user_time(user_id,create_time)、idx_version(wallet_id,version),
|
||||
-- 管理端列表 SQL(WHERE tenant_id=? AND del_flag='0' ORDER BY create_time DESC,user_id DESC LIMIT 10
|
||||
-- + 同条件 COUNT)无任何可用索引 → 全表扫描 + filesort,大表上每次翻页执行两遍。
|
||||
--
|
||||
-- 注意:slowlog_optimization_20260826/01_add_indexes.sql 里提过同样的两条索引,
|
||||
-- 但测试服与本地库均未建过,生产大概率也没有;执行前先按下面步骤核对。
|
||||
--
|
||||
-- ============ 执行前核对 ============
|
||||
-- SHOW INDEX FROM pjcloudx_wallet.wallet_transaction;
|
||||
-- 若下述索引名已存在且列一致,跳过对应条目;列不一致先停下人工确认,不要盲目重建。
|
||||
--
|
||||
-- ============ 执行方式 ============
|
||||
-- InnoDB 二级索引创建默认 ONLINE(不锁 DML),但大表建索引有 IO 消耗,
|
||||
-- 生产 IOPS 上限 8000,请低峰期逐条执行、每条间隔观察,不要并发跑。
|
||||
|
||||
-- P0-1:默认列表 / 时间范围 / COUNT。
|
||||
-- 覆盖 WHERE tenant_id=? AND del_flag='0' 的等值前缀,且 (create_time,user_id) 与
|
||||
-- ORDER BY create_time DESC, user_id DESC 完全同序,可 Backward index scan,LIMIT 10 只读 10 行;
|
||||
-- COUNT 走该索引即为覆盖索引扫描(Using index)。
|
||||
CREATE INDEX idx_wallet_tx_tenant_del_ct_uid
|
||||
ON pjcloudx_wallet.wallet_transaction (tenant_id, del_flag, create_time, user_id);
|
||||
|
||||
-- P0-2:按业务类型过滤的页面(充值/记账/消费/退款/退还)。
|
||||
-- biz_type 为 tinytext,必须前缀索引;稀有类型(如退款)优化器会用本索引 ref 定位后再倒序扫。
|
||||
CREATE INDEX idx_wallet_tx_tenant_del_biz_ct_uid
|
||||
ON pjcloudx_wallet.wallet_transaction (tenant_id, del_flag, biz_type(8), create_time, user_id);
|
||||
|
||||
-- ============ 执行后验证(本地已 EXPLAIN 验证,生产建完再跑一遍确认) ============
|
||||
-- 期望:key = idx_wallet_tx_tenant_del_ct_uid,Extra 含 Backward index scan 且无 Using filesort
|
||||
-- EXPLAIN SELECT a.* FROM pjcloudx_wallet.wallet_transaction a
|
||||
-- LEFT JOIN pjcloudx_wallet.wallet_manage m ON m.id = a.wallet_id AND m.del_flag='0'
|
||||
-- WHERE a.del_flag='0' AND a.tenant_id=1
|
||||
-- ORDER BY a.create_time DESC, a.user_id DESC LIMIT 10;
|
||||
--
|
||||
-- 期望:key = idx_wallet_tx_tenant_del_ct_uid,Extra = Using index
|
||||
-- EXPLAIN SELECT COUNT(*) FROM pjcloudx_wallet.wallet_transaction a
|
||||
-- WHERE a.del_flag='0' AND a.tenant_id=1;
|
||||
--
|
||||
-- 期望:key = idx_wallet_tx_tenant_del_biz_ct_uid,Extra 含 Backward index scan
|
||||
-- EXPLAIN SELECT a.* FROM pjcloudx_wallet.wallet_transaction a
|
||||
-- WHERE a.del_flag='0' AND a.tenant_id=1 AND a.biz_type IN ('9')
|
||||
-- ORDER BY a.create_time DESC, a.user_id DESC LIMIT 10;
|
||||
@@ -0,0 +1,25 @@
|
||||
-- 订单实时统计配套索引;仅在确认执行计划后安排低峰期上线,不由应用自动执行。
|
||||
-- 已有创建时间索引 idx_hard_order_tenant_del_ct_uid 可直接支持创建时间范围过滤。
|
||||
-- 支付时间索引只在没有同名前导列的等效索引时创建,不重复建立宽覆盖索引。
|
||||
SET @statistics_pay_index_exists = (
|
||||
SELECT COUNT(*)
|
||||
FROM (
|
||||
SELECT INDEX_NAME, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX SEPARATOR ',') AS columns_in_order
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = 'pjcloudx_hardware' AND TABLE_NAME = 'hard_order'
|
||||
GROUP BY INDEX_NAME
|
||||
) existing_indexes
|
||||
WHERE columns_in_order = 'tenant_id,del_flag,pay_time'
|
||||
OR columns_in_order LIKE 'tenant_id,del_flag,pay_time,%'
|
||||
);
|
||||
SET @statistics_pay_index_ddl = IF(@statistics_pay_index_exists = 0,
|
||||
'CREATE INDEX idx_hard_order_tenant_del_pay_time ON pjcloudx_hardware.hard_order (tenant_id, del_flag, pay_time)',
|
||||
'SELECT ''支付时间范围索引已存在,跳过'' AS message');
|
||||
PREPARE statistics_pay_index_statement FROM @statistics_pay_index_ddl;
|
||||
EXECUTE statistics_pay_index_statement;
|
||||
DEALLOCATE PREPARE statistics_pay_index_statement;
|
||||
|
||||
-- 上线验收:SHOW INDEX FROM pjcloudx_hardware.hard_order;
|
||||
-- 对接口实际生成的 SQL(含租户、数据权限、日期和商户/部门/钱包筛选)执行 EXPLAIN。
|
||||
-- 确认订单表使用日期范围扫描,且未出现用户/购物车展示关联、全表售后聚合或分组 COUNT。
|
||||
-- 记录近三个月的大租户耗时、扫描行数和并发请求下业务查询延迟;单条统计 SQL 超时为15秒。
|
||||
@@ -0,0 +1,27 @@
|
||||
-- 第三方支付日志分页索引;在 pjcloudx_thirdparty 库执行。
|
||||
-- 默认列表按租户、逻辑删除标识和创建时间查询;组合筛选增加发起方、接口类型。
|
||||
-- req/res/header 等内容的 LIKE '%关键字%' 不使用 B-tree 定位,请配合请求时间缩小范围。
|
||||
|
||||
SET @tpl_index_ddl = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = 'third_pay_log'
|
||||
AND index_name = 'idx_tpl_tenant_del_ct_id') = 0,
|
||||
'ALTER TABLE third_pay_log ADD INDEX idx_tpl_tenant_del_ct_id (tenant_id, del_flag, create_time, id), ALGORITHM=INPLACE, LOCK=NONE',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE tpl_index_stmt FROM @tpl_index_ddl;
|
||||
EXECUTE tpl_index_stmt;
|
||||
DEALLOCATE PREPARE tpl_index_stmt;
|
||||
|
||||
SET @tpl_index_ddl = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = 'third_pay_log'
|
||||
AND index_name = 'idx_tpl_tenant_del_source_biz_ct_id') = 0,
|
||||
'ALTER TABLE third_pay_log ADD INDEX idx_tpl_tenant_del_source_biz_ct_id (tenant_id, del_flag, pay_source_type, biz_type, create_time, id), ALGORITHM=INPLACE, LOCK=NONE',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE tpl_index_stmt FROM @tpl_index_ddl;
|
||||
EXECUTE tpl_index_stmt;
|
||||
DEALLOCATE PREPARE tpl_index_stmt;
|
||||
|
||||
SET @tpl_index_ddl = NULL;
|
||||
@@ -0,0 +1,57 @@
|
||||
-- /hard/hard-total/page-list-cs 配套索引,线上目标为 PolarDB MySQL 8;已在 MySQL 8.0.46 验证。
|
||||
-- 应与本次 SQL 改造一同发布。大表低峰期执行,先核对剩余磁盘和实际版本的在线 DDL 支持。
|
||||
-- 不删除现有索引;只跳过前导列及覆盖列均等效的可见完整 BTREE 索引。
|
||||
-- 窄索引 (tenant_id,del_flag,pay_time) 不能替代本次支付覆盖索引。
|
||||
-- 同一个支付索引覆盖页面的钱包、用户部门和用餐时段筛选,避免这些场景重新逐行读取订单主表。
|
||||
-- PolarDB DDL 行为参考:https://help.aliyun.com/zh/polardb/polardb-for-mysql/user-guide/support-for-ddl-in-polardb-mysql
|
||||
|
||||
SET @hard_cs_pay_cover_exists = (
|
||||
SELECT COUNT(*)
|
||||
FROM (
|
||||
SELECT INDEX_NAME,
|
||||
GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX SEPARATOR ',') AS columns_in_order,
|
||||
SUM(COLUMN_NAME IN ('tenant_id','del_flag','pay_time','order_status','merchant_type','merchant_id','merchant_dept_id','pay_price','wallet_id','user_dept_id','hour_type')) AS covered_columns,
|
||||
SUM(SUB_PART IS NOT NULL) AS prefix_columns
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = 'pjcloudx_hardware' AND TABLE_NAME = 'hard_order'
|
||||
AND INDEX_TYPE = 'BTREE' AND IS_VISIBLE = 'YES'
|
||||
GROUP BY INDEX_NAME
|
||||
) existing_indexes
|
||||
WHERE columns_in_order LIKE 'tenant_id,del_flag,pay_time,%'
|
||||
AND covered_columns = 11 AND prefix_columns = 0
|
||||
);
|
||||
SET @hard_cs_pay_cover_ddl = IF(@hard_cs_pay_cover_exists = 0,
|
||||
'CREATE INDEX idx_hard_order_cs_pay_cover ON pjcloudx_hardware.hard_order (tenant_id,del_flag,pay_time,order_status,merchant_type,merchant_id,merchant_dept_id,pay_price,wallet_id,user_dept_id,hour_type) ALGORITHM=INPLACE LOCK=NONE',
|
||||
'SELECT ''商户支付覆盖索引已存在,跳过'' AS message');
|
||||
PREPARE hard_cs_pay_cover_statement FROM @hard_cs_pay_cover_ddl;
|
||||
EXECUTE hard_cs_pay_cover_statement;
|
||||
DEALLOCATE PREPARE hard_cs_pay_cover_statement;
|
||||
|
||||
SET @hard_cs_refund_audit_exists = (
|
||||
SELECT COUNT(*)
|
||||
FROM (
|
||||
SELECT INDEX_NAME,
|
||||
GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX SEPARATOR ',') AS columns_in_order,
|
||||
SUM(COLUMN_NAME IN ('tenant_id','del_flag','status','audit_time','order_id','refund_price')) AS covered_columns,
|
||||
SUM(SUB_PART IS NOT NULL) AS prefix_columns
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = 'pjcloudx_hardware' AND TABLE_NAME = 'hard_after_sale'
|
||||
AND INDEX_TYPE = 'BTREE' AND IS_VISIBLE = 'YES'
|
||||
GROUP BY INDEX_NAME
|
||||
) existing_indexes
|
||||
WHERE columns_in_order LIKE 'tenant_id,del_flag,status,audit_time,%'
|
||||
AND covered_columns = 6 AND prefix_columns = 0
|
||||
);
|
||||
SET @hard_cs_refund_audit_ddl = IF(@hard_cs_refund_audit_exists = 0,
|
||||
'CREATE INDEX idx_hard_after_sale_cs_audit ON pjcloudx_hardware.hard_after_sale (tenant_id,del_flag,status,audit_time,order_id,refund_price) ALGORITHM=INPLACE LOCK=NONE',
|
||||
'SELECT ''商户退款时间覆盖索引已存在,跳过'' AS message');
|
||||
PREPARE hard_cs_refund_audit_statement FROM @hard_cs_refund_audit_ddl;
|
||||
EXECUTE hard_cs_refund_audit_statement;
|
||||
DEALLOCATE PREPARE hard_cs_refund_audit_statement;
|
||||
|
||||
-- 发布后对真实租户、日期和筛选参数生成的 SQL 执行 EXPLAIN ANALYZE(执行真实查询)。
|
||||
-- 默认时间汇总:支付走 idx_hard_order_cs_pay_cover 范围覆盖扫描;退款先走
|
||||
-- idx_hard_after_sale_cs_audit 的 audit_time 范围,再按主键读取关联订单。
|
||||
-- 定向商户、钱包等筛选允许选择其他更合适的索引,不要求强制使用上述索引。
|
||||
-- SQL 中 NO_GROUP_INDEX / JOIN_PREFIX 提示已在 MySQL 8.0.46 验证;PolarDB 上用实际小版本、租户参数重新验计划。
|
||||
-- 若先建索引再灰度代码,过渡期旧 SQL 可能改变执行计划,应检查旧服务延迟,尽快完成同批灰度。
|
||||
@@ -0,0 +1,111 @@
|
||||
# 商户交易汇总优化与验证记录
|
||||
|
||||
2026-10-22,接口 `POST /hard/hard-total/page-list-cs`。线上数据库由用户确认为 PolarDB MySQL 8 兼容版;本次直接连接的测试数据库为 MySQL Community 8.0.46。本次没有连接生产数据库,也没有重启正在运行的服务。
|
||||
|
||||
## 查询链路与瓶颈
|
||||
|
||||
前端商户统计页面默认查询近三个月,返回整个商户汇总列表。接口名称虽然带 `page`,实际响应为 `List<HardOrderCsVo>`,没有分页 COUNT SQL。
|
||||
|
||||
请求依次经过子商户展开、用户可见商户类型处理、支付汇总、退款汇总、Java 合并、商户及部门信息补全、平台抽成,最后由前端计算表尾合计。商户信息已按 200 个 ID 分批查询;钱包服务补全商户时还会查询部门,因此接口总耗时不能直接等同于两条 SQL 的耗时。
|
||||
|
||||
原查询存在以下开销:
|
||||
|
||||
- 未筛选钱包类型时,支付和退款仍逐条关联 `wallet_manage`。
|
||||
- 售后表只有 `(order_id,del_flag,id)` 索引,无法直接按退款统计时间定位。
|
||||
- 已有支付时间窄索引需要读取订单主表;钱包、用户部门和用餐时段筛选也需要额外字段。
|
||||
- 实测发现优化器可能为商户分组选择商户索引,扫描全部历史订单;增加售后索引后,也可能改为从全部历史订单逐条查售后。仅加索引或仅删关联并不稳定。
|
||||
- Java 合并退款及匹配抽成商户反复遍历列表;前端挂载和切换子表有多个查询触发点。
|
||||
|
||||
## 最终实现
|
||||
|
||||
`HardOrderMapper.xml` 仅在筛选钱包类型时关联钱包表,移除 VO 不使用的 `max(user_dept_id)`,支付聚合使用 `NO_GROUP_INDEX(a)` 避免为分组顺序扫描整个历史商户索引。退款有日期范围且未指定订单、商户、用户或设备时使用 `JOIN_PREFIX(r)`,先扫描售后日期范围;定向筛选保留优化器自行选择驱动表的能力。
|
||||
|
||||
配套脚本 `db/20261022_hard_cs_statistics_indexes.sql` 建立两个索引,并识别同名或异名的等效索引,避免重复创建:
|
||||
|
||||
```sql
|
||||
hard_order (
|
||||
tenant_id, del_flag, pay_time, order_status, merchant_type,
|
||||
merchant_id, merchant_dept_id, pay_price, wallet_id, user_dept_id, hour_type
|
||||
)
|
||||
hard_after_sale (
|
||||
tenant_id, del_flag, status, audit_time, order_id, refund_price
|
||||
)
|
||||
```
|
||||
|
||||
支付索引同时覆盖页面常用筛选,退款索引覆盖售后时间、金额及订单关联字段。新增索引会增加磁盘占用和写入维护成本,脚本保留所有既有索引,未自动删除窄支付时间索引。
|
||||
|
||||
`HardOrderTotalServiceImpl` 使用保留插入顺序的 Map 合并商户,抽成匹配也改为 Map,合并开销为 O(支付商户数 + 退款商户数)。保留每 200 个 ID 的远程查询边界。Controller 和 Service 对超过一秒的请求记录子商户解析、权限、支付查询、退款查询、合并、商户 RPC、部门 RPC 的分段耗时。
|
||||
|
||||
前端统一在异步子表挂载后查询,移除重复的 mounted/tab-click 触发,日/月切换每次只请求一次,并保留手动刷新和部门筛选。
|
||||
|
||||
同时修复两个边界问题:整个区间只有退款时原实现会漏掉全部商户;指定设备类型时原支付 SQL 引用了未关联的 `m` 别名。现在退款商户能显示负净额,设备类型筛选同时作用于支付和退款。
|
||||
|
||||
## 统计口径与正确性
|
||||
|
||||
支付仍使用状态 `5、20、10` 和订单 `pay_time`;退款仍使用完成状态 `50` 和原 SQL 的售后 `audit_time`,独立于订单支付时间。一笔订单多次退款按退款记录分别计次。支付与退款分别汇总后按商户相减,金额使用 BigDecimal,抽成计算及取舍位置保持原实现。
|
||||
|
||||
金额单位、时间范围的包含边界、商户类型排序规则、商户子级展开、用户部门和商户部门的包含/排除、缺失或已删除钱包的历史行为均有回归覆盖。继续使用现有租户及 DataScope 插件;没有将原先分组后的部门权限改为不同的行级语义。
|
||||
|
||||
保留优化前两条 SQL 作为测试对照,36 组筛选逐项比较支付及退款结果。MySQL 测试库现有业务数据另做 9 组对比。HTTP 回归实际执行 Controller、Service、Mapper 和权限插件,外部服务使用替身;没有将该测试当作真实网关或远程服务的性能测量。
|
||||
|
||||
还比较了“退款先按订单归集再关联”的候选。在一单一退款分布、已建立退款索引时,交替测量中直接关联约 175ms、预聚合约 274ms;最终没有将该物化方案用于业务查询。测试资源保留候选,供以后重新评估其他数据分布。
|
||||
|
||||
## 百万级 MySQL 测试
|
||||
|
||||
连接内临时表复制真实业务表的 `SHOW CREATE TABLE`,包括原有索引,再生成 100 万订单、100 万售后及合成钱包数据。数据覆盖两年、两个租户、997 个商户、不同状态及软删除标记,包含日期与支付状态过滤。最终验收使用一笔订单一条售后,避免多次退款的缓存复用使测试过于乐观;多次部分退款另有功能回归及前轮百万级数据验证。
|
||||
|
||||
最终 SQL 和索引下,每个场景先与旧 SQL 比较完整商户金额/次数,再测量三次取中位数。下表时间只包含 Mapper 查询及 SQL,不包含真实权限服务、商户/部门 RPC、网关和浏览器网络。
|
||||
|
||||
| 场景 | 支付中位数 | 退款中位数 | 两条查询合计 |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| 三个月,全部商户 | 144.52ms | 354.35ms | 498.87ms |
|
||||
| 两天范围 | 5.27ms | 10.78ms | 16.05ms |
|
||||
| 指定一个商户 | 4.80ms | 7.12ms | 11.92ms |
|
||||
| 商户部门筛选 | 39.63ms | 593.03ms | 632.66ms |
|
||||
| 用户部门筛选 | 39.51ms | 279.40ms | 318.91ms |
|
||||
| 用餐时段筛选 | 58.90ms | 374.20ms | 433.10ms |
|
||||
| 钱包类型筛选 | 143.37ms | 745.15ms | 888.52ms |
|
||||
|
||||
同一合成数据生成规则的原索引、旧 SQL 阶段,三个月支付约 1461.51ms、退款约 566.49ms,合计约 2028ms。不同轮次受本机并发负载、缓存和临时表统计信息影响,以上为离线阶段对比,不能推算线上接口必然从 5s 降至 0.5s。
|
||||
|
||||
最终实际执行计划:
|
||||
|
||||
```text
|
||||
支付:Covering index range scan on a using idx_hard_order_cs_pay_cover
|
||||
日期范围扫描 111182 条索引记录,筛选后 106348 笔支付,997 个商户
|
||||
退款:Covering index range scan on r using idx_hard_after_sale_cs_audit
|
||||
日期范围 101079 条售后,再按 hard_order PRIMARY 查对应订单
|
||||
钱包筛选:订单仍使用支付覆盖索引,之后按 wallet_manage PRIMARY 检查钱包类型
|
||||
```
|
||||
|
||||
后端验收共 44 项:43 项执行通过,0 失败、0 错误;1 项与本次无关的旧 H2 性能用例默认跳过。本次 MySQL 百万级性能用例已实际执行。前端 19 项测试通过,其中新增 3 项执行页面真实 setup、异步子组件挂载及切换生命周期;页面 SFC 脚本和模板编译通过。
|
||||
|
||||
索引迁移在独立测试 schema 中连续执行,并重命名为异名等效索引再次执行,均未重复建索引。百万级临时表随连接关闭清理,独立 schema 已删除。验收后业务表仍为 833 条订单、158 条售后,业务表没有新增本次候选索引。
|
||||
|
||||
## PolarDB 发布与验收
|
||||
|
||||
PolarDB 官方 [Statement Outline 文档](https://help.aliyun.com/zh/polardb/polardb-for-mysql/user-guide/statement-outline)明确提供 `JOIN_PREFIX` 并说明支持 MySQL 8.0 的 Hint 类型;[DDL 指南](https://help.aliyun.com/zh/polardb/polardb-for-mysql/user-guide/support-for-ddl-in-polardb-mysql)列明 8.0.1/8.0.2 创建二级索引支持并发 DML。脚本使用 `ALGORITHM=INPLACE LOCK=NONE`,不支持时会失败,不会自动降级为锁写入的 COPY。实际建索引开始/结束仍有元数据锁,并消耗 I/O 和 CPU,应安排低峰执行。
|
||||
|
||||
建议先灰度后端查询改造,再在同一发布窗口建立两个配套索引,随后发布前端。不要只给旧服务加索引:本次实测旧 SQL 曾因新增索引改变驱动表,明显变慢。不要自动回滚或删除其他既有索引。
|
||||
|
||||
在实际 PolarDB 节点上记录内核小版本及路由节点,用大租户近三个月、商户、钱包和部门条件执行 EXPLAIN,紧接 SHOW WARNINGS 检查提示是否被忽略;确认默认支付为日期覆盖范围扫描、默认退款先走售后时间索引。EXPLAIN ANALYZE 会实际执行查询,应选择验收时段。若该内核支持 EXPLAIN DDL,也可先预览建索引的算法和并发 DML 行为。
|
||||
|
||||
灰度时比较同一筛选的商户数、支付/退款金额及次数、负净额、表尾合计;结合新增分段日志记录端到端 P50/P95、数据库扫描行数和并发负载。线上执行计划、端到端性能和真实 RPC 的验证仍需在发布环境完成;本次没有使用生产连接或执行生产数据库操作。
|
||||
|
||||
复现后端测试前设置提供的 `MYSQL_HOST、MYSQL_PORT、MYSQL_USER、MYSQL_PWD` 环境变量,不将密码写入文件。在仓库根目录运行:
|
||||
|
||||
```sh
|
||||
HARD_CS_MYSQL_TEST=true HARD_CS_MYSQL_BENCHMARK=true mvn \
|
||||
-pl pjcloud-hardware/pjcloud-hardware-biz -am \
|
||||
-Dtest=HardOrderCsSqlTest,HardOrderCsServiceTest,HardOrderCsMySqlTest,HardBatchExportQueryTest,HardBatchExportFileTest,HardOrderStatisticsSqlTest,HardOrderStatisticsServiceTest,HardOrderStatisticsScopeTest,HardOrderStatisticsControllerTest \
|
||||
-Dhard.cs.benchmark.finalOnly=true \
|
||||
-Dsurefire.failIfNoSpecifiedTests=false -DfailIfNoTests=false test
|
||||
```
|
||||
|
||||
不设置 `hard.cs.benchmark.finalOnly` 可重跑原索引、只加退款索引、完整索引三个阶段,并交替比较预聚合候选,耗时更长。管理端仓库执行下列命令可复现本次 19 项前端回归:
|
||||
|
||||
```sh
|
||||
node --test tests/batch-export.test.cjs tests/hard-sh-query-lifecycle.test.cjs \
|
||||
tests/hard-third-order.test.cjs tests/wallet-ledger-exports.test.cjs \
|
||||
tests/wallet-manage-export.test.cjs
|
||||
```
|
||||
@@ -0,0 +1,88 @@
|
||||
-- /hard/hard-total/page-list-dept 与部门导出配套索引;目标:PolarDB MySQL 8。
|
||||
-- 本脚本只读取索引元数据并按需创建索引,不写业务数据,不删除或替换现有索引。
|
||||
-- 索引名称不是等效判断依据:已有其他名称的可见 BTREE 索引满足条件时同样跳过。
|
||||
-- 若目标名称已被不同定义占用且不存在等效索引,CREATE INDEX 会报错并停止该条;
|
||||
-- 应先核对 SHOW INDEX,不能把不同定义的同名索引当作本次所需索引。
|
||||
-- 大表低峰逐条执行。INPLACE/LOCK=NONE 仍需短暂元数据锁,应先排查长事务,
|
||||
-- 核对实际 PolarDB 小版本的在线 DDL 支持、可用空间和复制延迟,不并行建大表索引。
|
||||
-- PolarDB DDL:https://help.aliyun.com/zh/polardb/polardb-for-mysql/user-guide/support-for-ddl-in-polardb-mysql
|
||||
--
|
||||
-- 代码发布依赖:先发布 Wallet 的 /wallet-transaction/department-statistics 新内部接口,
|
||||
-- 再发布 Hardware。Hardware 新支付聚合直接读取 pjcloudx_wallet.wallet_transaction,
|
||||
-- 两库须位于同一实例,Hardware 应用数据库账号须具备该表 SELECT 权限。
|
||||
-- 测试使用的 root 账号权限不能作为生产应用账号权限的验证依据。
|
||||
|
||||
-- 一、订单按创建时间做范围覆盖扫描。
|
||||
-- 等效索引须以前三列 tenant_id,del_flag,create_time 按此顺序开头,包含以下全部七列,
|
||||
-- 所有显式索引列均为完整列(无前缀截断),且索引可见、类型为 BTREE。
|
||||
-- 后续覆盖列允许不同顺序或额外列;只有前三列的窄索引不能替代本次覆盖索引。
|
||||
SET @hard_dept_order_cover_exists = (
|
||||
SELECT COUNT(*)
|
||||
FROM (
|
||||
SELECT INDEX_NAME,
|
||||
SUM(SEQ_IN_INDEX = 1 AND COLUMN_NAME = 'tenant_id') AS tenant_first,
|
||||
SUM(SEQ_IN_INDEX = 2 AND COLUMN_NAME = 'del_flag') AS deletion_second,
|
||||
SUM(SEQ_IN_INDEX = 3 AND COLUMN_NAME = 'create_time') AS created_third,
|
||||
COUNT(DISTINCT CASE WHEN COLUMN_NAME IN
|
||||
('tenant_id','del_flag','create_time','order_status','wallet_id','user_dept_id','pay_price')
|
||||
THEN COLUMN_NAME END) AS covered_columns,
|
||||
SUM(SUB_PART IS NOT NULL) AS prefix_columns
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = 'pjcloudx_hardware' AND TABLE_NAME = 'hard_order'
|
||||
AND INDEX_TYPE = 'BTREE' AND IS_VISIBLE = 'YES'
|
||||
GROUP BY INDEX_NAME
|
||||
) existing_indexes
|
||||
WHERE tenant_first = 1 AND deletion_second = 1 AND created_third = 1
|
||||
AND covered_columns = 7 AND prefix_columns = 0
|
||||
);
|
||||
SET @hard_dept_order_cover_ddl = IF(@hard_dept_order_cover_exists = 0,
|
||||
'CREATE INDEX idx_hard_order_dept_ct_cover ON pjcloudx_hardware.hard_order (tenant_id,del_flag,create_time,order_status,wallet_id,user_dept_id,pay_price) ALGORITHM=INPLACE LOCK=NONE',
|
||||
'SELECT ''部门订单创建时间覆盖索引已存在,跳过'' AS message');
|
||||
PREPARE hard_dept_order_cover_statement FROM @hard_dept_order_cover_ddl;
|
||||
EXECUTE hard_dept_order_cover_statement;
|
||||
DEALLOCATE PREPARE hard_dept_order_cover_statement;
|
||||
|
||||
-- 二、按租户和业务编号定位少量支付流水,再校验钱包、业务类型、删除状态和部门,取最新有效记录。
|
||||
-- 复用 20261022_wallet_rule_transaction_page_index.sql 中的 idx_wt_tenant_bizid_type,
|
||||
-- 不为部门报表再创建另一条更宽的同类索引。
|
||||
-- 任何以完整 tenant_id,biz_id 两列开头的可见 BTREE 索引均视为可复用;后续列可有前缀。
|
||||
-- biz_type 是 TINYTEXT,新增候选必须写 biz_type(8),该候选不是完整覆盖索引。
|
||||
-- biz_id 高度选择性,用索引定位后执行精确字节匹配及最新有效快照筛选。
|
||||
SET @hard_dept_wallet_biz_lookup_exists = (
|
||||
SELECT COUNT(*)
|
||||
FROM (
|
||||
SELECT INDEX_NAME,
|
||||
SUM(SEQ_IN_INDEX = 1 AND COLUMN_NAME = 'tenant_id' AND SUB_PART IS NULL) AS tenant_first,
|
||||
SUM(SEQ_IN_INDEX = 2 AND COLUMN_NAME = 'biz_id' AND SUB_PART IS NULL) AS complete_biz_second
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = 'pjcloudx_wallet' AND TABLE_NAME = 'wallet_transaction'
|
||||
AND INDEX_TYPE = 'BTREE' AND IS_VISIBLE = 'YES'
|
||||
GROUP BY INDEX_NAME
|
||||
) existing_indexes
|
||||
WHERE tenant_first = 1 AND complete_biz_second = 1
|
||||
);
|
||||
SET @hard_dept_wallet_biz_lookup_ddl = IF(@hard_dept_wallet_biz_lookup_exists = 0,
|
||||
'CREATE INDEX idx_wt_tenant_bizid_type ON pjcloudx_wallet.wallet_transaction (tenant_id,biz_id,biz_type(8)) ALGORITHM=INPLACE LOCK=NONE',
|
||||
'SELECT ''钱包租户业务编号索引已存在,复用并跳过'' AS message');
|
||||
PREPARE hard_dept_wallet_biz_lookup_statement FROM @hard_dept_wallet_biz_lookup_ddl;
|
||||
EXECUTE hard_dept_wallet_biz_lookup_statement;
|
||||
DEALLOCATE PREPARE hard_dept_wallet_biz_lookup_statement;
|
||||
|
||||
-- 三、钱包六类流水合并统计先复用既有 idx_wallet_tx_tenant_del_biz_ct_uid
|
||||
-- (tenant_id,del_flag,biz_type(8),create_time,user_id),本脚本不额外创建宽聚合索引。
|
||||
-- 该索引来自 20261004_wallet_transaction_page_index.sql / 20260826 索引脚本,
|
||||
-- 不是包含 dept_id、price 的覆盖索引。日期范围汇总使用 INDEX 优化器提示建议采用该索引,
|
||||
-- 防止业务号索引仅按 tenant_id 扫描整个租户。请核实同名索引存在且定义正确;
|
||||
-- 提示要求引擎支持 MySQL 8.0.20+ 的 INDEX hint,未知提示或缺失索引时会忽略并回退,
|
||||
-- 因此发布须以实际 PolarDB 的 EXPLAIN 和 SHOW WARNINGS 验收,不能仅凭 SQL 成功。
|
||||
-- 定向钱包或没有合法日期范围时不加此提示。
|
||||
-- 对大批导入/临时造数后索引统计失真,应刷新统计后再核对计划;
|
||||
-- 本脚本不自动对业务大表执行 ANALYZE,生产按维护窗口和实际统计情况处理。
|
||||
--
|
||||
-- 发布验收使用真实租户、日期、部门包含/排除及钱包类型参数执行 EXPLAIN ANALYZE:
|
||||
-- 1. 订单时间范围扫描应能使用创建时间覆盖索引,避免逐条回表读取订单字段。
|
||||
-- 2. 历史流水应通过 tenant_id,biz_id 等值定位,不能逐单扫描全部钱包流水;
|
||||
-- 源 SQL 保留普通 biz_id 字符串等值,额外 BINARY 等值只作精确匹配校验。
|
||||
-- 3. 检查 NO_MERGE 派生表及相关子查询实际 loops,确认历史部门不会被重复求值。
|
||||
-- 4. 钱包合并统计只执行一次;定向钱包或其他高选择性条件允许选择更合适的既有索引。
|
||||
-- 5. 核对页面与部门导出全部金额、次数、删除部门及零交易部门结果后再扩大灰度。
|
||||
@@ -0,0 +1,129 @@
|
||||
# 部门交易统计优化验证记录(2026-10-22)
|
||||
|
||||
> **历史记录:下文验证的是旧订单统计口径的性能优化,已由本次钱包账本统计替代。** 下文的 106 项回归证明与旧口径保持一致,不能作为商户、部门报表已经统一到 `wallet_transaction` 的证据。当前支付与退款均按流水发生时间、历史部门、原始金额和流水条数汇总;生产差额须使用 [只读对账脚本](20261022_wallet_ledger_reconciliation.sql) 按实际租户和权限另行核实。本次未执行生产对账。
|
||||
|
||||
## 范围与数据库
|
||||
|
||||
目标接口:`POST /hard/hard-total/page-list-dept`,同时优化共用统计逻辑的同步/异步部门导出。用户反馈线上超过 30 秒未返回。线上为 PolarDB MySQL 8 兼容版;本次实际连接的是用户提供的本机 MySQL 8.0.46 测试库,未连接或修改生产数据库。
|
||||
|
||||
本次仅新增部门统计链路;保留此前 `page-list-cs` 的改动和工作区其他并行任务的改动。部门前端 `src/views/hard/hard-dept-total/index.vue` 初始化没有商户统计页面的重复请求问题,因此本次不修改部门前端。
|
||||
|
||||
## 根因与实现
|
||||
|
||||
旧链路先查询 UPMS 部门,再依次执行六次 Wallet 汇总 RPC;随后把全部候选订单明细读入 JVM,按每组最多 10000 订单、每 RPC 最多 10 组请求历史流水,Wallet 每组单独查询和排序,最后在 Java 按历史部门累加。订单量和历史流水量同时影响数据库扫描、结果映射、RPC 传输及 JVM 内存。原流水索引不能直接按租户、业务编号定位,订单缺少以租户/删除标记/创建时间开头的覆盖索引。
|
||||
|
||||
新链路:一次 UPMS 部门查询 + 一次 Wallet 条件聚合 RPC + 一次 Hardware 支付部门聚合 SQL。只返回部门级金额、次数,不再回传逐笔订单和流水快照。导出共用这条链路,不再每 200 订单重复请求历史快照;Excel 仍按部门结果分批写入。
|
||||
|
||||
- Wallet 新增内部 `/wallet-transaction/department-statistics`,用 CASE 同时统计六类收支;原内部接口保留。
|
||||
- Hardware 新增 `HardDepartmentStatisticsMapper`,逐单用高选择性业务编号定位最新有效支付流水,回退订单部门后再按部门筛选/汇总。
|
||||
- 原始 `biz_id` 普通等值保证可用索引,额外 BINARY 等值模拟旧 Java Map 的精确字符串匹配;`biz_type` 同样校验精确字节。订单编号用 `concat('', a.id)`,保持完整大整数并让列侧排序规则优先,避免连接与历史列的字符排序规则冲突。
|
||||
- 显式限制订单租户、历史流水租户和钱包表租户,避免带库名前缀的表被租户插件忽略。入口要求有效租户上下文。对于历史异常的跨租户钱包引用,有钱包类型筛选时新查询会按同租户条件排除;这是对异常关联的隔离收紧。
|
||||
- 超过 1 秒的成功统计记录部门 RPC、钱包聚合 RPC、订单聚合、合并各段耗时,便于灰度定位。
|
||||
- 部门/钱包 RPC 失败或 SQL 异常终止统计,不返回假零。空部门结果保留原行为。
|
||||
|
||||
## 保留的统计口径
|
||||
|
||||
| 项目 | 口径 |
|
||||
|---|---|
|
||||
| 订单范围 | `hard_order.create_time`,状态 `5,20,10`;金额 `pay_price`、每单一次,包含零金额 |
|
||||
| 历史部门 | 同租户、同钱包、精确字符串业务编号、PAYMENT=`4`,有效且部门非空的最大流水 ID;快照不限时间和金额符号;没有快照回退 `user_dept_id` |
|
||||
| 部门筛选 | 还原历史部门后才做包含/排除,`isInclude=null` 与包含一致;`deptId` 继续只控制展示部门查询 |
|
||||
| 钱包类型 | 当前钱包类型,标量和列表同时传入时取交集;不新增删除钱包过滤 |
|
||||
| 批量充值/扣减 | BATCH=`2`,严格按金额正/负拆分 |
|
||||
| 单次充值/扣减 | UPDATE=`1` 和 RECHARGE=`3` 合并,严格按金额正/负拆分 |
|
||||
| 退款 | REFUND=`5` 原符号合计,零金额也计次数 |
|
||||
| 退还食堂 | RETURN_CANTEEN=`8` 先带符号求和,组装时取绝对值;零金额也计次数 |
|
||||
| 部门列表 | 保留 UPMS 顺序、已删除标记、零交易部门;支付或退款单边存在也保留 |
|
||||
| 隔离范围 | 保持租户和所选部门条件;未为此接口新增原本不存在的 DataScope 权限规则 |
|
||||
|
||||
部门 Excel 同时修正原有问题:8 个次数合计未累计、Long 类型扣减金额转元时截断小数、转换直接修改原 VO;改为独立导出 Map,全部金额/次数完整累计,并修正消费净金额标题。页面响应仍用原字段和分单位。
|
||||
|
||||
## 正确性与文件验证
|
||||
|
||||
- 新 Hardware SQL 回归 11 项:逐笔旧 SQL + 独立 Java 历史归属为基线,覆盖 21 组可选条件、24 组最终部门组合、时间/状态边界、现金回退、删除钱包、大整数编号、异常字符串和租户隔离。
|
||||
- 新 Hardware Service/Controller 回归 13 项:16 个金额/次数字段、RPC 次数/失败、筛选快照、空/删除部门、退款单边结果、页面与导出一致性和进度。
|
||||
- 新 Wallet SQL 回归 8 项:真实聚合 XML + 生产租户插件,对照原六次聚合,覆盖符号/零金额/钱包类型交集。
|
||||
- 部门真实 Excel 回归:201 部门跨 200 行批边界逐列验证 16 个数值及合计,重复导出不污染统计对象,统计异常不返回部分附件;原订单/商户导出测试保留。
|
||||
- 真实 MySQL 回归 3 项全部通过:既有数据 23 组筛选(14 个初始部门);源 SQL 精确业务号/业务类型、最新有效快照和大整数边界;索引迁移重复、改名、覆盖列重排、窄索引、业务号前缀截断和不可见索引判断。后两项写入只在 TEMPORARY 表或独立 scratch 库。
|
||||
- H2 测试只适配其 AS CHAR/AS BINARY 默认长度为 1 的方言差异;真实 MySQL 测试直接执行源 XML,无字符转换影子 SQL。
|
||||
|
||||
最终联合回归 **106 项实际执行通过,0 失败/错误**,覆盖 18 个测试类。另 2 项独立性能用例未在联合轮启用:原订单统计的百万性能开关未开启;钱包索引候选测试已在独立轮实际执行通过,不把联合轮跳过算作通过。
|
||||
|
||||
联合日志:`/private/tmp/hard-dept-final-verification.log`;Wallet 候选对照日志:`/private/tmp/hard-dept-mysql-wallet-candidates.log`。真实 MySQL 3 项回归和百万部门性能测试均实际启用并执行。索引 scratch 库清理后数量为 0;所有连接临时表在测试结束关闭连接后删除;任务文件凭证检查未发现提供的数据库密码。
|
||||
|
||||
## 真实 MySQL 性能验证
|
||||
|
||||
压测克隆测试库真实表结构及原索引到连接临时表,生成 100 万订单、100 万钱包流水、1 万钱包,时间分布 730 天、两个租户;包含历史部门变化、重复业务号、尾空格、错钱包、区间外快照、现金支付、删除记录及零金额。默认查询最近三个月,再分别测试部门包含、部门排除和钱包类型筛选。每项交替执行新/旧版本 3 次取中位数;计时旧订单链路总预算 30 秒、新 SQL 单次预算不超过 30 秒。部门包含/排除在旧链路仅改变最终 Java 过滤,最终回归复用默认查询已验证的全部门旧结果作为对照,避免重复搬运相同 10 万订单。旧计时样本超时会明确记录;若没有完整旧结果,仅为正确性另给旧链路 90 秒预算,绝不把该额外执行伪装为性能样本。
|
||||
|
||||
**必须代码与索引配套上线。** 原索引下默认三个月旧支付链路 12372.47 ms、新支付 SQL 14921.99 ms,说明仅修改 SQL 不能解决历史流水定位成本;钱包六次汇总则为 2222.59 ms,新一次聚合为 175.51 ms。支付必须添加业务号定位及订单时间覆盖索引后验收,不能仅发布代码。
|
||||
|
||||
造数后必须刷新临时表统计。初轮未刷新时,原空表索引误估每个钱包只有 1 条流水,新支付查询误选钱包编号索引并在新增索引阶段超过 30 秒。ANALYZE 仅作用于本次连接临时表;刷新后支付查询按业务编号定位,默认三个月中位数 550.01 ms。生产不自动执行 ANALYZE,应结合实际统计维护窗口验收计划。
|
||||
|
||||
还发现业务号索引会干扰 Wallet 汇总:它仅按 tenant_id 扫描 90 万行,默认约 1.2 秒。对照现有业务时间索引与普通时间索引后,前者只读取约 62015 行(后者约 110551 行),采用有日期范围且未指定 walletId 时的 INDEX 优化器提示,保留定向钱包自由选择索引。提示针对 MySQL 8.0.20+;不支持提示或没有同名索引时可回退,正确性不依赖提示,但灰度必须确认实际 PolarDB 计划已采用时间范围扫描。
|
||||
|
||||
钱包候选 3 次采样中位数(ms),每项均完整对照原六次查询的 12 个字段:
|
||||
|
||||
| 场景 | 无提示 | 业务类型时间索引 | 普通时间索引 |
|
||||
|---|---:|---:|---:|
|
||||
| 默认三个月 | 966.70 | 186.04 | 179.19 |
|
||||
| 部门包含 | 1016.21 | 83.94 | 129.76 |
|
||||
| 部门排除 | 1161.30 | 130.97 | 164.09 |
|
||||
| 钱包类型 | 1095.75 | 120.85 | 150.13 |
|
||||
|
||||
默认场景约 7ms 的两候选差距不作为决策依据;业务类型时间索引扫描量更少,其余三场景更快,故选择该既有索引。
|
||||
|
||||
最终源 SQL + 配套索引 + 刷新临时统计后的验收结果(ms):
|
||||
|
||||
| 场景 | 旧支付链路 | 新支付聚合 | 旧钱包六次汇总 | 新钱包一次汇总 | 新核心阶段中位数之和 |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| 默认三个月 | 19442.08 | 508.69 | 2222.16 | 159.73 | 668.42 |
|
||||
| 部门包含 | 复用全部门基线 | 477.83 | 2278.88 | 159.31 | 637.14 |
|
||||
| 部门排除 | 复用全部门基线 | 475.08 | 2243.06 | 177.53 | 652.61 |
|
||||
| 钱包类型 | 948.86 | 242.78 | 1762.37 | 140.42 | 383.20 |
|
||||
|
||||
默认场景旧、新共用相同数据与配套索引,核心阶段中位数之和约 **21.66 秒 → 0.67 秒**;“之和”是两个阶段各自 3 次中位数相加,**不是完整 HTTP 接口中位数**。新增索引也会改变旧大 IN 查询的计划,因此不能假定仅给旧链路加索引就会提速。前面原索引的 12.37 秒支付样本与这里 19.44 秒样本属于不同索引/统计阶段,应分开解释。
|
||||
|
||||
所有新支付结果与旧逐单归属一致;钱包全部 12 个统计字段与旧六次 SQL 一致。默认旧流程向 JVM/服务间返回 106348 订单、43922 快照,执行 11 次快照 SQL;新 SQL 返回 57 个支付部门和 25 个钱包部门,服务中保留 UPMS 的零交易部门。钱包类型筛选旧流程返回 34294 订单、14561 快照并执行 4 次快照 SQL。
|
||||
|
||||
最终 EXPLAIN ANALYZE 已确认:
|
||||
|
||||
- 订单走 `idx_hard_order_dept_ct_cover` 创建时间覆盖范围扫描,默认读 111182 索引项、106348 候选订单。
|
||||
- 历史流水走 `idx_wt_tenant_bizid_type` 的 tenant_id + biz_id + biz_type 定位;默认 loops=106348,平均每次仅约 0.432 条候选;四场景均不存在逐单全表扫描。
|
||||
- NO_MERGE 生效,先物化部门结果,只有一个历史标量子查询;外层部门筛选/分组没有再次执行该表达式。
|
||||
- Wallet 走现有 `idx_wallet_tx_tenant_del_biz_ct_uid` 的五个业务类型/日期区间,默认读取约 62015 行后聚合,不再按租户扫描 90 万行。
|
||||
- 缺失索引提示的只读探针在真实 MySQL 返回 warning 3128 并正常生成回退计划,无 SQL 执行错误;PolarDB 是否识别提示仍需按具体版本验收。
|
||||
|
||||
## 索引、权限与发布顺序
|
||||
|
||||
配套脚本:`db/20261022_hard_dept_statistics_indexes.sql`。本次未在任何业务表加索引;脚本实际执行验证仅使用测试专用 scratch 库,结束删除;百万数据仅在当前连接 TEMPORARY 表中生成并建索引,关闭连接自动删除。
|
||||
|
||||
1. 订单候选覆盖索引 `(tenant_id,del_flag,create_time,order_status,wallet_id,user_dept_id,pay_price)`;普通窄时间索引不足以覆盖读取字段。
|
||||
2. 钱包业务编号定位索引 `(tenant_id,biz_id,biz_type(8))`,复用其他任务的 `idx_wt_tenant_bizid_type`,不创建第二条同类宽索引。`biz_type` 为 TINYTEXT,前缀必须保留,该索引不是完整覆盖索引。
|
||||
3. 钱包六类合并统计复用现有 `idx_wallet_tx_tenant_del_biz_ct_uid`,日期范围查询使用前述优化器提示,不追加大宽索引;需确认同名索引及提示在具体 PolarDB 小版本生效。
|
||||
4. 脚本识别其他名称的等效可见索引、完整列前缀,重复运行跳过;同名但定义不同会报错,应核对后处理。大表低峰逐条在线建索引,核对长事务、元数据锁、空间和具体 PolarDB 版本支持。
|
||||
5. **先准备索引,再发布所有 Wallet 实例的新内部接口,最后滚动发布 Hardware。** 原 Wallet 内部 API 未删除,先发布 Wallet 兼容旧 Hardware。
|
||||
6. **Hardware 与 Wallet 库必须同实例;Hardware 生产数据库账号需要 `pjcloudx_wallet.wallet_transaction` 的 SELECT 权限。** 这是新增依赖,原本查询 wallet_manage 的权限不能代替;本地 root 测试不能证明应用账号权限。请用实际 Hardware 账号先执行只读查询和 EXPLAIN。
|
||||
7. 在真实租户上灰度验证默认日期、部门包含/排除、钱包类型的 16 项金额/次数与导出;检查历史流水业务编号索引和相关子查询实际 loops。观察分段日志、DB CPU/IO、RPC 时延后扩大范围。
|
||||
|
||||
本次结果是本机 MySQL 的 SQL、Java 映射/归集和文件回归结果,**不是生产 PolarDB 接口时延承诺**;不包含网关、UPMS/Wallet 的真实网络与连接池排队。大范围无日期查询仍要读取该范围全部相关订单,不能把百万样本耗时直接外推到线上全部历史数据。
|
||||
|
||||
## 复现
|
||||
|
||||
先在 shell 中设置本地测试库的 `MYSQL_HOST`、`MYSQL_PORT`、`MYSQL_USER`、`MYSQL_PWD`,不要把凭证写入仓库。仓库根目录执行:
|
||||
|
||||
```bash
|
||||
HARD_DEPT_MYSQL_TEST=true mvn -o -pl pjcloud-hardware/pjcloud-hardware-biz -am \
|
||||
-Dtest=HardDepartmentStatisticsMySqlTest \
|
||||
-Dsurefire.failIfNoSpecifiedTests=false -DfailIfNoTests=false \
|
||||
'-DargLine=-Xmx768m -Dfile.encoding=UTF-8 -Djava.awt.headless=true' test
|
||||
|
||||
HARD_DEPT_MYSQL_TEST=true HARD_DEPT_MYSQL_BENCHMARK=true \
|
||||
mvn -o -pl pjcloud-hardware/pjcloud-hardware-biz -am \
|
||||
-Dtest=HardDepartmentStatisticsMySqlBenchmarkTest -Dhard.dept.benchmark.finalOnly=true \
|
||||
-Dsurefire.failIfNoSpecifiedTests=false -DfailIfNoTests=false \
|
||||
'-DargLine=-Xmx1536m -Dfile.encoding=UTF-8 -Djava.awt.headless=true' test
|
||||
```
|
||||
|
||||
环境开关未启用时真实 MySQL 测试跳过;不能把跳过视为验证通过。macOS 文件回归需保留 `java.awt.headless=true`,避免 AWT 初始化导致测试 JVM 退出。
|
||||
|
||||
去掉 `hard.dept.benchmark.finalOnly` 可连同原索引阶段一起执行;设置 `hard.dept.benchmark.walletCandidates=true` 可单独复现三个钱包索引选择方案。所有模式仍要求两个 MySQL 环境开关显式开启。
|
||||
@@ -0,0 +1,51 @@
|
||||
-- 不活跃钱包统计专用索引。仅新增索引,不修改业务数据;大表请低峰逐条执行并观察 IO。
|
||||
-- 每段先检查可见 BTREE 索引的列前缀,等价索引已存在时跳过。
|
||||
-- 若同名索引定义不同,CREATE INDEX 将报错,应检查后人工处理,禁止盲目删除重建。
|
||||
|
||||
SET @wallet_inactive_ddl = IF(EXISTS (
|
||||
SELECT 1 FROM (
|
||||
SELECT INDEX_NAME, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS cols
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = 'pjcloudx_wallet' AND TABLE_NAME = 'wallet_transaction'
|
||||
AND IS_VISIBLE = 'YES' AND INDEX_TYPE = 'BTREE'
|
||||
GROUP BY INDEX_NAME
|
||||
) indexes WHERE cols = 'tenant_id,user_id,del_flag,create_time'
|
||||
OR cols LIKE 'tenant_id,user_id,del_flag,create_time,%'
|
||||
), 'SELECT ''wallet inactive: equivalent transaction index exists''',
|
||||
'CREATE INDEX idx_wallet_inactive_tx_user_time ON pjcloudx_wallet.wallet_transaction (tenant_id,user_id,del_flag,create_time)');
|
||||
PREPARE wallet_inactive_stmt FROM @wallet_inactive_ddl;
|
||||
EXECUTE wallet_inactive_stmt;
|
||||
DEALLOCATE PREPARE wallet_inactive_stmt;
|
||||
|
||||
SET @wallet_inactive_ddl = IF(EXISTS (
|
||||
SELECT 1 FROM (
|
||||
SELECT INDEX_NAME, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS cols
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = 'pjcloudx_wallet' AND TABLE_NAME = 'wallet_manage'
|
||||
AND IS_VISIBLE = 'YES' AND INDEX_TYPE = 'BTREE'
|
||||
GROUP BY INDEX_NAME
|
||||
) indexes WHERE cols = 'tenant_id,del_flag,user_id,create_time'
|
||||
OR cols LIKE 'tenant_id,del_flag,user_id,create_time,%'
|
||||
), 'SELECT ''wallet inactive: equivalent user index exists''',
|
||||
'CREATE INDEX idx_wallet_inactive_user ON pjcloudx_wallet.wallet_manage (tenant_id,del_flag,user_id,create_time)');
|
||||
PREPARE wallet_inactive_stmt FROM @wallet_inactive_ddl;
|
||||
EXECUTE wallet_inactive_stmt;
|
||||
DEALLOCATE PREPARE wallet_inactive_stmt;
|
||||
|
||||
SET @wallet_inactive_ddl = IF(EXISTS (
|
||||
SELECT 1 FROM (
|
||||
SELECT INDEX_NAME, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS cols
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = 'pjcloudx_wallet' AND TABLE_NAME = 'wallet_manage'
|
||||
AND IS_VISIBLE = 'YES' AND INDEX_TYPE = 'BTREE'
|
||||
GROUP BY INDEX_NAME
|
||||
) indexes WHERE cols = 'tenant_id,del_flag,dept_id,user_id,create_time'
|
||||
OR cols LIKE 'tenant_id,del_flag,dept_id,user_id,create_time,%'
|
||||
), 'SELECT ''wallet inactive: equivalent department index exists''',
|
||||
'CREATE INDEX idx_wallet_inactive_dept_user ON pjcloudx_wallet.wallet_manage (tenant_id,del_flag,dept_id,user_id,create_time)');
|
||||
PREPARE wallet_inactive_stmt FROM @wallet_inactive_ddl;
|
||||
EXECUTE wallet_inactive_stmt;
|
||||
DEALLOCATE PREPARE wallet_inactive_stmt;
|
||||
|
||||
-- 执行后核对 SHOW INDEX,并对 Mapper 的 countUsers / pageUsers / listWallets
|
||||
-- 分别进行 EXPLAIN ANALYZE,验证近期流水存在性检查和最后流水取首行使用用户时间索引。
|
||||
@@ -0,0 +1,382 @@
|
||||
-- 钱包账本核对草稿(2026-10-22);只含 SELECT 和会话变量 SET,不建表、不改数据。
|
||||
-- 本文件未连接任何数据库、未执行生产核对,不能将下列查询当作已有线上结果。
|
||||
-- 适用 MySQL 8 / PolarDB MySQL 8。三个库须在同一实例且核对账号有 SELECT 权限。
|
||||
-- 使用前只填写本租户编号。NULL 默认不会匹配任何业务数据,避免不知租户时查全库。
|
||||
-- 核对的是用户给出的“全权限、未额外筛选商户/商户类型/钱包类型”场景;
|
||||
-- 若实际账号有行级权限或商户类型限制,应先将相同限制添加到新旧两侧,不能直接拿全租户结果与页面比较。
|
||||
-- 日期使用中国业务时间;请确保会话中 DATETIME/TIMESTAMP 的解释与应用一致,本脚本不更改时区。
|
||||
SET @recon_tenant_id = NULL;
|
||||
SET @recon_from = CAST('2026-09-01 00:00:00' AS DATETIME);
|
||||
SET @recon_to = CAST('2026-10-01 00:00:00' AS DATETIME);
|
||||
SET @recon_excluded_dept_1 = 1875013738914156546;
|
||||
SET @recon_excluded_dept_2 = 1910218102049226753;
|
||||
SET @recon_excluded_dept_3 = 1910218128624336898;
|
||||
SET @recon_excluded_dept_4 = 1942046316044144642;
|
||||
|
||||
-- 用户报告的原页面结果,单位分;用于比较是否复现,绝不代表本次已验证。
|
||||
SET @recon_reported_merchant_cents = 429542126;
|
||||
SET @recon_reported_department_cents = 429394976;
|
||||
SET @recon_reported_merchant_count = 64361;
|
||||
SET @recon_reported_department_count = 64133;
|
||||
|
||||
SELECT @recon_tenant_id AS selected_tenant,
|
||||
@recon_from AS inclusive_start, @recon_to AS exclusive_end,
|
||||
CASE WHEN @recon_tenant_id IS NULL THEN '请先填写租户编号,后续空结果不是零流水结论' ELSE '已指定租户' END AS setup_status,
|
||||
@recon_reported_merchant_cents - @recon_reported_department_cents AS reported_delta_cents,
|
||||
@recon_reported_merchant_count - @recon_reported_department_count AS reported_delta_count;
|
||||
|
||||
-- 1. 新口径原始账本基准:PAYMENT=4 价格取反;REFUND=5 保留原始符号;零金额也计次。
|
||||
-- 部门排除保留 dept_id=NULL,这些交易必须进入“未归属部门/非订单未归属”桶。
|
||||
WITH ledger_base AS (
|
||||
SELECT ledger.*,
|
||||
CASE WHEN ledger.biz_id REGEXP '^[0-9]{1,19}$'
|
||||
THEN CAST(ledger.biz_id AS UNSIGNED) ELSE NULL END AS numeric_biz_id
|
||||
FROM pjcloudx_wallet.wallet_transaction ledger
|
||||
WHERE @recon_tenant_id IS NOT NULL
|
||||
AND ledger.tenant_id = @recon_tenant_id
|
||||
AND ledger.del_flag = '0'
|
||||
AND ledger.biz_type IN ('4', '5')
|
||||
AND ledger.create_time >= @recon_from
|
||||
AND ledger.create_time < @recon_to
|
||||
AND (ledger.dept_id IS NULL OR ledger.dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4))
|
||||
)
|
||||
SELECT COALESCE(-SUM(CASE WHEN biz_type = '4' THEN price ELSE 0 END), 0) AS payment_cents,
|
||||
COUNT(CASE WHEN biz_type = '4' THEN 1 END) AS payment_count,
|
||||
COALESCE(SUM(CASE WHEN biz_type = '5' THEN price ELSE 0 END), 0) AS refund_cents,
|
||||
COUNT(CASE WHEN biz_type = '5' THEN 1 END) AS refund_count,
|
||||
COALESCE(-SUM(price), 0) AS net_cents,
|
||||
COUNT(CASE WHEN biz_type = '4' THEN 1 END)
|
||||
- COUNT(CASE WHEN biz_type = '5' THEN 1 END) AS net_count
|
||||
FROM ledger_base;
|
||||
|
||||
-- 2. 同一批账本按真实商户归属汇总,金额和次数仍完全来自账本。
|
||||
-- 订单/售后只补商户关系,不限制其软删除、状态、支付金额及支付/创建时间。
|
||||
-- 精确业务编号 + 同租户 + 同钱包,未匹配项显示“非订单/未归属”;不能漏掉这个桶。
|
||||
WITH ledger_base AS (
|
||||
SELECT ledger.*,
|
||||
CASE WHEN ledger.biz_id REGEXP '^[0-9]{1,19}$'
|
||||
THEN CAST(ledger.biz_id AS UNSIGNED) ELSE NULL END AS numeric_biz_id
|
||||
FROM pjcloudx_wallet.wallet_transaction ledger
|
||||
WHERE @recon_tenant_id IS NOT NULL
|
||||
AND ledger.tenant_id = @recon_tenant_id
|
||||
AND ledger.del_flag = '0'
|
||||
AND ledger.biz_type IN ('4', '5')
|
||||
AND ledger.create_time >= @recon_from
|
||||
AND ledger.create_time < @recon_to
|
||||
AND (ledger.dept_id IS NULL OR ledger.dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4))
|
||||
),
|
||||
merchant_ledger AS (
|
||||
SELECT ledger.*, ledger_order.merchant_id
|
||||
FROM ledger_base ledger
|
||||
LEFT JOIN pjcloudx_hardware.hard_after_sale ledger_refund
|
||||
ON ledger.biz_type = '5'
|
||||
AND ledger_refund.tenant_id = ledger.tenant_id
|
||||
AND ledger_refund.id = ledger.numeric_biz_id
|
||||
AND CAST(ledger.biz_id AS BINARY) = CAST(CONCAT('', ledger_refund.id) AS BINARY)
|
||||
LEFT JOIN pjcloudx_hardware.hard_order ledger_order
|
||||
ON ledger_order.tenant_id = ledger.tenant_id
|
||||
AND ledger_order.wallet_id = ledger.wallet_id
|
||||
AND ledger_order.id = CASE WHEN ledger.biz_type = '4' THEN ledger.numeric_biz_id
|
||||
WHEN ledger.biz_type = '5' THEN ledger_refund.order_id ELSE NULL END
|
||||
AND (ledger.biz_type = '5'
|
||||
OR CAST(ledger.biz_id AS BINARY) = CAST(CONCAT('', ledger_order.id) AS BINARY))
|
||||
)
|
||||
SELECT merchant_id,
|
||||
CASE WHEN merchant_id IS NULL THEN '非订单/未归属' ELSE CONCAT('商户(', merchant_id, ')') END AS merchant_bucket,
|
||||
COALESCE(-SUM(CASE WHEN biz_type = '4' THEN price ELSE 0 END), 0) AS payment_cents,
|
||||
COUNT(CASE WHEN biz_type = '4' THEN 1 END) AS payment_count,
|
||||
COALESCE(SUM(CASE WHEN biz_type = '5' THEN price ELSE 0 END), 0) AS refund_cents,
|
||||
COUNT(CASE WHEN biz_type = '5' THEN 1 END) AS refund_count,
|
||||
COALESCE(-SUM(price), 0) AS net_cents,
|
||||
COUNT(CASE WHEN biz_type = '4' THEN 1 END)
|
||||
- COUNT(CASE WHEN biz_type = '5' THEN 1 END) AS net_count
|
||||
FROM merchant_ledger
|
||||
GROUP BY merchant_id
|
||||
ORDER BY merchant_id;
|
||||
|
||||
-- 3. 同一批账本按流水历史部门汇总;各组相加应等于第1组、第2组结果。
|
||||
-- 不与当前部门列表做 INNER JOIN,物理缺失部门及 NULL 部门仍保留。
|
||||
WITH ledger_base AS (
|
||||
SELECT ledger.*,
|
||||
CASE WHEN ledger.biz_id REGEXP '^[0-9]{1,19}$'
|
||||
THEN CAST(ledger.biz_id AS UNSIGNED) ELSE NULL END AS numeric_biz_id
|
||||
FROM pjcloudx_wallet.wallet_transaction ledger
|
||||
WHERE @recon_tenant_id IS NOT NULL
|
||||
AND ledger.tenant_id = @recon_tenant_id
|
||||
AND ledger.del_flag = '0'
|
||||
AND ledger.biz_type IN ('4', '5')
|
||||
AND ledger.create_time >= @recon_from
|
||||
AND ledger.create_time < @recon_to
|
||||
AND (ledger.dept_id IS NULL OR ledger.dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4))
|
||||
)
|
||||
SELECT dept_id,
|
||||
CASE WHEN dept_id IS NULL THEN '未归属部门' ELSE CONCAT('历史部门(', dept_id, ')') END AS department_bucket,
|
||||
COALESCE(-SUM(CASE WHEN biz_type = '4' THEN price ELSE 0 END), 0) AS payment_cents,
|
||||
COUNT(CASE WHEN biz_type = '4' THEN 1 END) AS payment_count,
|
||||
COALESCE(SUM(CASE WHEN biz_type = '5' THEN price ELSE 0 END), 0) AS refund_cents,
|
||||
COUNT(CASE WHEN biz_type = '5' THEN 1 END) AS refund_count,
|
||||
COALESCE(-SUM(price), 0) AS net_cents,
|
||||
COUNT(CASE WHEN biz_type = '4' THEN 1 END)
|
||||
- COUNT(CASE WHEN biz_type = '5' THEN 1 END) AS net_count
|
||||
FROM ledger_base
|
||||
GROUP BY dept_id
|
||||
ORDER BY dept_id;
|
||||
|
||||
-- 4. 旧两个支付链路的完整集合差异(仅解释旧“流水金额/次数”,不把退款混入支付差额)。
|
||||
-- 旧商户:订单 pay_time BETWEEN(含右端)、订单 user_dept_id、merchant_id非空、pay_price/每单一次。
|
||||
-- 旧部门:订单 create_time BETWEEN(含右端)、最新有效 PAYMENT 历史部门(不限流水时间/金额符号),
|
||||
-- 无快照则回退订单部门;最后仅输出 UPMS 存在的部门。UPMS 软删除部门仍存在,不能过滤del_flag。
|
||||
-- 该查询故意保留旧 BETWEEN;第1至3组的新账本使用半开区间,两者并非相同口径。
|
||||
-- 本段模拟现有源码的全权限行为;若部署代码版本不同,先比对源码,不可硬解释为生产差额。
|
||||
WITH order_basis AS (
|
||||
SELECT a.id, a.tenant_id, a.wallet_id, a.merchant_id, a.user_dept_id,
|
||||
a.pay_price, a.pay_time, a.create_time,
|
||||
COALESCE((
|
||||
SELECT history.dept_id
|
||||
FROM pjcloudx_wallet.wallet_transaction history
|
||||
WHERE history.tenant_id = a.tenant_id
|
||||
AND history.biz_id = CONCAT('', a.id)
|
||||
AND CAST(history.biz_id AS BINARY) = CAST(CONCAT('', a.id) AS BINARY)
|
||||
AND history.wallet_id = a.wallet_id
|
||||
AND history.biz_type = '4'
|
||||
AND CAST(history.biz_type AS BINARY) = CAST('4' AS BINARY)
|
||||
AND history.del_flag = '0'
|
||||
AND history.dept_id IS NOT NULL
|
||||
ORDER BY history.id DESC LIMIT 1
|
||||
), a.user_dept_id) AS historical_dept_id
|
||||
FROM pjcloudx_hardware.hard_order a
|
||||
WHERE @recon_tenant_id IS NOT NULL
|
||||
AND a.tenant_id = @recon_tenant_id AND a.del_flag = '0'
|
||||
AND a.order_status IN ('5', '20', '10')
|
||||
AND ((a.pay_time BETWEEN @recon_from AND @recon_to)
|
||||
OR (a.create_time BETWEEN @recon_from AND @recon_to))
|
||||
),
|
||||
old_flags AS (
|
||||
SELECT a.*,
|
||||
CASE WHEN a.pay_time BETWEEN @recon_from AND @recon_to THEN 1 ELSE 0 END AS pay_time_inside,
|
||||
CASE WHEN a.create_time BETWEEN @recon_from AND @recon_to THEN 1 ELSE 0 END AS create_time_inside,
|
||||
CASE WHEN a.user_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4) THEN 1 ELSE 0 END AS order_department_inside,
|
||||
CASE WHEN a.historical_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4) THEN 1 ELSE 0 END AS historical_department_inside,
|
||||
CASE WHEN EXISTS (
|
||||
SELECT 1 FROM pjcloudx.sys_dept department
|
||||
WHERE department.tenant_id = a.tenant_id AND department.dept_id = a.historical_dept_id
|
||||
) THEN 1 ELSE 0 END AS historical_department_exists,
|
||||
CASE WHEN a.merchant_id IS NOT NULL
|
||||
AND a.pay_time BETWEEN @recon_from AND @recon_to
|
||||
AND a.user_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4)
|
||||
THEN 1 ELSE 0 END AS old_merchant_match,
|
||||
CASE WHEN a.create_time BETWEEN @recon_from AND @recon_to
|
||||
AND a.historical_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM pjcloudx.sys_dept department
|
||||
WHERE department.tenant_id = a.tenant_id AND department.dept_id = a.historical_dept_id
|
||||
)
|
||||
THEN 1 ELSE 0 END AS old_department_match
|
||||
FROM order_basis a
|
||||
)
|
||||
SELECT COALESCE(SUM(CASE WHEN old_merchant_match = 1 THEN pay_price ELSE 0 END), 0) AS old_merchant_cents,
|
||||
SUM(old_merchant_match) AS old_merchant_count,
|
||||
COALESCE(SUM(CASE WHEN old_department_match = 1 THEN pay_price ELSE 0 END), 0) AS old_department_cents,
|
||||
SUM(old_department_match) AS old_department_count,
|
||||
COALESCE(SUM(COALESCE(pay_price, 0) * (old_merchant_match - old_department_match)), 0) AS reconstructed_delta_cents,
|
||||
COALESCE(SUM(old_merchant_match - old_department_match), 0) AS reconstructed_delta_count,
|
||||
@recon_reported_merchant_cents - @recon_reported_department_cents AS reported_delta_cents,
|
||||
@recon_reported_merchant_count - @recon_reported_department_count AS reported_delta_count,
|
||||
CASE WHEN @recon_tenant_id IS NOT NULL
|
||||
AND COALESCE(SUM(COALESCE(pay_price, 0) * (old_merchant_match - old_department_match)), 0)
|
||||
= @recon_reported_merchant_cents - @recon_reported_department_cents
|
||||
AND COALESCE(SUM(old_merchant_match - old_department_match), 0)
|
||||
= @recon_reported_merchant_count - @recon_reported_department_count
|
||||
THEN '金额及次数差额同时复现,仍需下组逐笔核对' ELSE '未同时复现,检查租户/权限/部署版本/元数据变化' END AS comparison_status
|
||||
FROM old_flags;
|
||||
|
||||
-- 5. 逐笔列出旧链路集合差:正向仅进商户,负向仅进部门。
|
||||
-- signed_difference_cents/count 相加应等于第4组重建差额;同时查看各时间、部门、缺失元数据列。
|
||||
WITH order_basis AS (
|
||||
SELECT a.id, a.tenant_id, a.wallet_id, a.merchant_id, a.user_dept_id,
|
||||
a.pay_price, a.pay_time, a.create_time,
|
||||
COALESCE((
|
||||
SELECT history.dept_id
|
||||
FROM pjcloudx_wallet.wallet_transaction history
|
||||
WHERE history.tenant_id = a.tenant_id
|
||||
AND history.biz_id = CONCAT('', a.id)
|
||||
AND CAST(history.biz_id AS BINARY) = CAST(CONCAT('', a.id) AS BINARY)
|
||||
AND history.wallet_id = a.wallet_id
|
||||
AND history.biz_type = '4'
|
||||
AND CAST(history.biz_type AS BINARY) = CAST('4' AS BINARY)
|
||||
AND history.del_flag = '0'
|
||||
AND history.dept_id IS NOT NULL
|
||||
ORDER BY history.id DESC LIMIT 1
|
||||
), a.user_dept_id) AS historical_dept_id
|
||||
FROM pjcloudx_hardware.hard_order a
|
||||
WHERE @recon_tenant_id IS NOT NULL
|
||||
AND a.tenant_id = @recon_tenant_id AND a.del_flag = '0'
|
||||
AND a.order_status IN ('5', '20', '10')
|
||||
AND ((a.pay_time BETWEEN @recon_from AND @recon_to)
|
||||
OR (a.create_time BETWEEN @recon_from AND @recon_to))
|
||||
),
|
||||
old_flags AS (
|
||||
SELECT a.*,
|
||||
CASE WHEN a.pay_time BETWEEN @recon_from AND @recon_to THEN 1 ELSE 0 END AS pay_time_inside,
|
||||
CASE WHEN a.create_time BETWEEN @recon_from AND @recon_to THEN 1 ELSE 0 END AS create_time_inside,
|
||||
CASE WHEN a.user_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4) THEN 1 ELSE 0 END AS order_department_inside,
|
||||
CASE WHEN a.historical_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4) THEN 1 ELSE 0 END AS historical_department_inside,
|
||||
CASE WHEN EXISTS (
|
||||
SELECT 1 FROM pjcloudx.sys_dept department
|
||||
WHERE department.tenant_id = a.tenant_id AND department.dept_id = a.historical_dept_id
|
||||
) THEN 1 ELSE 0 END AS historical_department_exists,
|
||||
CASE WHEN a.merchant_id IS NOT NULL
|
||||
AND a.pay_time BETWEEN @recon_from AND @recon_to
|
||||
AND a.user_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4)
|
||||
THEN 1 ELSE 0 END AS old_merchant_match,
|
||||
CASE WHEN a.create_time BETWEEN @recon_from AND @recon_to
|
||||
AND a.historical_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM pjcloudx.sys_dept department
|
||||
WHERE department.tenant_id = a.tenant_id AND department.dept_id = a.historical_dept_id
|
||||
)
|
||||
THEN 1 ELSE 0 END AS old_department_match
|
||||
FROM order_basis a
|
||||
)
|
||||
SELECT id AS order_id, merchant_id, wallet_id,
|
||||
pay_time AS old_merchant_time, create_time AS old_department_time,
|
||||
user_dept_id AS old_merchant_department, historical_dept_id AS old_department_department,
|
||||
pay_price AS order_payment_cents,
|
||||
old_merchant_match, old_department_match,
|
||||
CASE WHEN old_merchant_match = 1 THEN '仅旧商户链路' ELSE '仅旧部门链路' END AS difference_direction,
|
||||
COALESCE(pay_price, 0) * (old_merchant_match - old_department_match) AS signed_difference_cents,
|
||||
old_merchant_match - old_department_match AS signed_difference_count,
|
||||
pay_time_inside, create_time_inside, order_department_inside, historical_department_inside,
|
||||
historical_department_exists
|
||||
FROM old_flags
|
||||
WHERE old_merchant_match <> old_department_match
|
||||
ORDER BY difference_direction, id;
|
||||
|
||||
-- 6. 单独隔离“订单支付时间 vs 订单创建时间”差集:其他条件固定为旧商户的订单部门、真实商户及有效订单。
|
||||
-- 这只是时间因素,不与第7组机械相加,时间和部门因素可能交叠。
|
||||
WITH order_basis AS (
|
||||
SELECT a.id, a.tenant_id, a.wallet_id, a.merchant_id, a.user_dept_id,
|
||||
a.pay_price, a.pay_time, a.create_time,
|
||||
COALESCE((
|
||||
SELECT history.dept_id
|
||||
FROM pjcloudx_wallet.wallet_transaction history
|
||||
WHERE history.tenant_id = a.tenant_id
|
||||
AND history.biz_id = CONCAT('', a.id)
|
||||
AND CAST(history.biz_id AS BINARY) = CAST(CONCAT('', a.id) AS BINARY)
|
||||
AND history.wallet_id = a.wallet_id
|
||||
AND history.biz_type = '4'
|
||||
AND CAST(history.biz_type AS BINARY) = CAST('4' AS BINARY)
|
||||
AND history.del_flag = '0'
|
||||
AND history.dept_id IS NOT NULL
|
||||
ORDER BY history.id DESC LIMIT 1
|
||||
), a.user_dept_id) AS historical_dept_id
|
||||
FROM pjcloudx_hardware.hard_order a
|
||||
WHERE @recon_tenant_id IS NOT NULL
|
||||
AND a.tenant_id = @recon_tenant_id AND a.del_flag = '0'
|
||||
AND a.order_status IN ('5', '20', '10')
|
||||
AND ((a.pay_time BETWEEN @recon_from AND @recon_to)
|
||||
OR (a.create_time BETWEEN @recon_from AND @recon_to))
|
||||
),
|
||||
old_flags AS (
|
||||
SELECT a.*,
|
||||
CASE WHEN a.pay_time BETWEEN @recon_from AND @recon_to THEN 1 ELSE 0 END AS pay_time_inside,
|
||||
CASE WHEN a.create_time BETWEEN @recon_from AND @recon_to THEN 1 ELSE 0 END AS create_time_inside,
|
||||
CASE WHEN a.user_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4) THEN 1 ELSE 0 END AS order_department_inside,
|
||||
CASE WHEN a.historical_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4) THEN 1 ELSE 0 END AS historical_department_inside,
|
||||
CASE WHEN EXISTS (
|
||||
SELECT 1 FROM pjcloudx.sys_dept department
|
||||
WHERE department.tenant_id = a.tenant_id AND department.dept_id = a.historical_dept_id
|
||||
) THEN 1 ELSE 0 END AS historical_department_exists,
|
||||
CASE WHEN a.merchant_id IS NOT NULL
|
||||
AND a.pay_time BETWEEN @recon_from AND @recon_to
|
||||
AND a.user_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4)
|
||||
THEN 1 ELSE 0 END AS old_merchant_match,
|
||||
CASE WHEN a.create_time BETWEEN @recon_from AND @recon_to
|
||||
AND a.historical_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM pjcloudx.sys_dept department
|
||||
WHERE department.tenant_id = a.tenant_id AND department.dept_id = a.historical_dept_id
|
||||
)
|
||||
THEN 1 ELSE 0 END AS old_department_match
|
||||
FROM order_basis a
|
||||
)
|
||||
SELECT CASE WHEN pay_time_inside = 1 THEN '仅支付时间命中' ELSE '仅创建时间命中' END AS time_difference,
|
||||
COUNT(*) AS order_count,
|
||||
COALESCE(SUM(pay_price), 0) AS order_payment_cents,
|
||||
COALESCE(SUM(COALESCE(pay_price, 0) * (pay_time_inside - create_time_inside)), 0) AS signed_time_delta_cents,
|
||||
SUM(pay_time_inside - create_time_inside) AS signed_time_delta_count
|
||||
FROM old_flags
|
||||
WHERE merchant_id IS NOT NULL AND order_department_inside = 1
|
||||
AND pay_time_inside <> create_time_inside
|
||||
GROUP BY pay_time_inside, create_time_inside
|
||||
ORDER BY pay_time_inside DESC;
|
||||
|
||||
-- 7. 单独隔离“订单部门快照 vs 最新钱包支付历史部门”差集:统一使用订单支付时间、有效订单及真实商户。
|
||||
-- 对照的是历史快照归属,不以当前用户部门代替;部门物理缺失的影响另看第5组 exists 字段。
|
||||
-- 本组两方向均可能存在,不能因为总差额为正就只查“测试部门迁出”。
|
||||
WITH order_basis AS (
|
||||
SELECT a.id, a.tenant_id, a.wallet_id, a.merchant_id, a.user_dept_id,
|
||||
a.pay_price, a.pay_time, a.create_time,
|
||||
COALESCE((
|
||||
SELECT history.dept_id
|
||||
FROM pjcloudx_wallet.wallet_transaction history
|
||||
WHERE history.tenant_id = a.tenant_id
|
||||
AND history.biz_id = CONCAT('', a.id)
|
||||
AND CAST(history.biz_id AS BINARY) = CAST(CONCAT('', a.id) AS BINARY)
|
||||
AND history.wallet_id = a.wallet_id
|
||||
AND history.biz_type = '4'
|
||||
AND CAST(history.biz_type AS BINARY) = CAST('4' AS BINARY)
|
||||
AND history.del_flag = '0'
|
||||
AND history.dept_id IS NOT NULL
|
||||
ORDER BY history.id DESC LIMIT 1
|
||||
), a.user_dept_id) AS historical_dept_id
|
||||
FROM pjcloudx_hardware.hard_order a
|
||||
WHERE @recon_tenant_id IS NOT NULL
|
||||
AND a.tenant_id = @recon_tenant_id AND a.del_flag = '0'
|
||||
AND a.order_status IN ('5', '20', '10')
|
||||
AND ((a.pay_time BETWEEN @recon_from AND @recon_to)
|
||||
OR (a.create_time BETWEEN @recon_from AND @recon_to))
|
||||
),
|
||||
old_flags AS (
|
||||
SELECT a.*,
|
||||
CASE WHEN a.pay_time BETWEEN @recon_from AND @recon_to THEN 1 ELSE 0 END AS pay_time_inside,
|
||||
CASE WHEN a.create_time BETWEEN @recon_from AND @recon_to THEN 1 ELSE 0 END AS create_time_inside,
|
||||
CASE WHEN a.user_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4) THEN 1 ELSE 0 END AS order_department_inside,
|
||||
CASE WHEN a.historical_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4) THEN 1 ELSE 0 END AS historical_department_inside,
|
||||
CASE WHEN EXISTS (
|
||||
SELECT 1 FROM pjcloudx.sys_dept department
|
||||
WHERE department.tenant_id = a.tenant_id AND department.dept_id = a.historical_dept_id
|
||||
) THEN 1 ELSE 0 END AS historical_department_exists,
|
||||
CASE WHEN a.merchant_id IS NOT NULL
|
||||
AND a.pay_time BETWEEN @recon_from AND @recon_to
|
||||
AND a.user_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4)
|
||||
THEN 1 ELSE 0 END AS old_merchant_match,
|
||||
CASE WHEN a.create_time BETWEEN @recon_from AND @recon_to
|
||||
AND a.historical_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM pjcloudx.sys_dept department
|
||||
WHERE department.tenant_id = a.tenant_id AND department.dept_id = a.historical_dept_id
|
||||
)
|
||||
THEN 1 ELSE 0 END AS old_department_match
|
||||
FROM order_basis a
|
||||
)
|
||||
SELECT id AS order_id, pay_time, wallet_id, merchant_id,
|
||||
user_dept_id AS order_department, historical_dept_id AS wallet_snapshot_department,
|
||||
order_department_inside, historical_department_inside,
|
||||
COALESCE(pay_price, 0) * (order_department_inside - historical_department_inside) AS signed_department_delta_cents,
|
||||
order_department_inside - historical_department_inside AS signed_department_delta_count
|
||||
FROM old_flags
|
||||
WHERE merchant_id IS NOT NULL AND pay_time_inside = 1
|
||||
AND order_department_inside <> historical_department_inside
|
||||
ORDER BY signed_department_delta_count DESC, id;
|
||||
|
||||
-- 8. 结束时刻探针:旧 BETWEEN 会包含 10月1日00:00:00,新账本半开区间排除它。
|
||||
-- 这里只展示边界交易,不把结果预设为任何金额或次数。
|
||||
SELECT ledger.id AS transaction_id, ledger.biz_type, ledger.price AS signed_cents,
|
||||
ledger.create_time, ledger.dept_id, ledger.wallet_id, ledger.biz_id
|
||||
FROM pjcloudx_wallet.wallet_transaction ledger
|
||||
WHERE @recon_tenant_id IS NOT NULL
|
||||
AND ledger.tenant_id = @recon_tenant_id AND ledger.del_flag = '0'
|
||||
AND ledger.biz_type IN ('4', '5') AND ledger.create_time = @recon_to
|
||||
AND (ledger.dept_id IS NULL OR ledger.dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4))
|
||||
ORDER BY ledger.id;
|
||||
@@ -0,0 +1,43 @@
|
||||
# 钱包账本交易统计验证记录(2026-10-22)
|
||||
|
||||
## 结果与口径
|
||||
|
||||
商户、部门和商户月份报表的支付、退款均改为 `wallet_transaction` 聚合:PAYMENT=4 的金额取 `-SUM(price)`,REFUND=5 保留原符号,每条流水分别计次,净额和净次数分别相减。所有报表使用流水 `create_time >= start AND create_time < end`,部门包含/排除使用流水 `dept_id`。
|
||||
|
||||
订单、售后只用于商户归属及原有数据权限,不再根据订单金额、状态、创建或支付时间决定账本收支。零元但无流水的订单不计入,实际零金额流水与冲正记录保留。全部 REFUND 都保留,非订单退款及缺失关联归入空商户 ID 的“非订单/未归属”行;该行仅全局权限、未限定商户范围时可见。普通历史商户资料缺失、部门物理缺失或空部门不会导致账本漏数。
|
||||
|
||||
部门的批量充值/扣减、单次充值/扣减和退还食堂仍保留原计算规则,改在同一账本查询中聚合。其他钱包接口、数据库结构与生产数据均未修改。
|
||||
|
||||
## 接口和导出
|
||||
|
||||
- 日商户和部门接口仍返回原列表结构;月份接口仍分页,支付与退款先合并聚合后分页,包含只有退款的月份。
|
||||
- 现有字段名、金额分单位、租户和商户权限保留。权限在分组前执行;客户端指定商户类型必须与服务端类型授权取交集。
|
||||
- 页面、同步/异步汇总导出共享查询。自然日分片使用相邻半开边界,支付和退款四个原始汇总字段一起累计;抽成在完整合并后计算。
|
||||
- 月导出从账本月份分组产生商户集合,包含未归属和退款单边分组;一次固定查询/权限上下文,分页读取,金额全程以 BigDecimal 分值累计后转元。
|
||||
- 单商户明细导出改为逐条钱包流水,包含原始金额、支付/退款金额与关联业务编号。保留五万笔上限,拒绝空或非正商户 ID,每批 200 笔按流水主键游标读取,完整临时文件生成后响应。
|
||||
- 三个 Panel report 共用商户统计服务,原 createTimeList 映射为账本时间,保留其商户类型和商户筛选。
|
||||
- 前端日期标签、半开边界、逐流水计次说明同步更新;未归属行禁止结账和单商户导出;月表明确“本页合计”。
|
||||
|
||||
## 实际验证
|
||||
|
||||
`mvn -o -pl pjcloud-hardware/pjcloud-hardware-biz -am -DskipTests -Dfile.encoding=UTF-8 compile` 实际通过。
|
||||
|
||||
联合 Maven test 在 testCompile 阶段被原有 `src/test/java/hard/com/bsd/pjcloud/hard/WechatPayTest.java:39` 的未定义 `log` 阻塞,未修改该无关测试。使用该模块 Maven 已解析的依赖类路径、当前 Maven 编译的主代码,在临时目录以 javac 编译本次定向测试,并通过 JUnit Platform 1.10.5 执行:
|
||||
|
||||
- 12 个测试类,共发现 88 项,实际执行 **87 项全部通过,0 失败、0 中止**。
|
||||
- 1 项原订单统计百万行压测因 `hard.order.statistics.performance` 未开启而跳过,不计为通过。
|
||||
- 其中新账本 SQL 13 项执行真实 XML、租户和分页插件,验证商户合计、部门合计与独立原始账本基准一致;覆盖半开边界、多流水、正负冲正、零金额、非订单退款、精确大整数关联、软删/缺失资料、钱包类型、权限和跨租户。
|
||||
- Service 与导出回归覆盖元数据分批查询及失败、半开自然日分片、空商户分组跨片合并、月导出跨 200 分组边界、真实 XLSX 全部金额/次数列和合计、重复导出不污染原统计,以及明细导出跨批及失败不响应残缺文件。
|
||||
- 现有订单统计 SQL、权限、Service 和 Controller 定向回归同时通过;旧订单查询的通过不作为新账本口径正确性依据。
|
||||
|
||||
后端运行日志:`/var/folders/ng/w0f04t956sb_29sq0_pbw63c0000gn/T/pj-wallet-ledger-regression-gyqchw17/test.log`。该路径为临时验证文件,不是发布依赖。
|
||||
|
||||
管理端相关 Node 测试 **15/15 通过**,目标 ESLint 检查与四个 Vue SFC 模板编译通过,两仓库变更的 whitespace 检查通过。
|
||||
|
||||
## 生产核对边界
|
||||
|
||||
SQL 执行正确性使用 H2 的 MySQL 兼容模式;另验证未经替换的 MySQL 源 SQL 能被生产使用的 SQL 解析器解析。未连接生产 MySQL/PolarDB,未执行生产查询计划或新查询的大范围压测,不承诺生产时延。
|
||||
|
||||
线上用户报告的旧金额差 147150 分、次数差 228 尚未逐笔归因。使用 [只读对账脚本](20261022_wallet_ledger_reconciliation.sql),指定租户后核对原始账本、新商户/部门分组、旧订单时间/部门快照双向差集。脚本默认租户 NULL,空结果不能解释为零流水。
|
||||
|
||||
上线前应以实际 Hardware 数据库账号确认跨库 SELECT 权限,并在真实 PolarDB 上检查时间区间扫描、订单/售后主键关联与耗时;本次不自动执行索引或数据变更。新报表不再调用 Wallet 部门聚合 RPC,无需先发布 Wallet 服务。前端和 Hardware 应配套发布以保持时间标签和实际语义一致。本次尚未发布上线。
|
||||
@@ -0,0 +1,46 @@
|
||||
-- /wallet/wallet-rule-transaction/page-join 分页、准确计数及当前页详情索引。
|
||||
-- 先发布配套 SQL 改造才能消除 GROUP BY 和 biz_id 的隐式数字转换。
|
||||
-- 请在 pjcloudx_wallet 库核对 SHOW INDEX;同名同列索引已存在时跳过对应语句,
|
||||
-- 同名但定义不同应先核实。大表低峰逐条执行,观察 IO 和复制延迟。
|
||||
-- LOCK=NONE 仍需要短暂元数据锁;执行前排查长事务,勿并发创建索引。
|
||||
|
||||
-- 默认列表、时间范围、COUNT;等值前缀后按 create_time/id 反向扫描。
|
||||
ALTER TABLE pjcloudx_wallet.wallet_rule_transaction
|
||||
ADD INDEX idx_wrt_tenant_del_ct_id (tenant_id, del_flag, create_time, id),
|
||||
ALGORITHM=INPLACE, LOCK=NONE;
|
||||
|
||||
-- 按转账类型筛选时仍保持稳定时间排序,并使该条件的 COUNT 使用覆盖索引。
|
||||
ALTER TABLE pjcloudx_wallet.wallet_rule_transaction
|
||||
ADD INDEX idx_wrt_tenant_del_biz_ct_id (tenant_id, del_flag, biz_type, create_time, id),
|
||||
ALGORITHM=INPLACE, LOCK=NONE;
|
||||
|
||||
-- 退款/未退款状态查询:避免准确计数时扫描并回表全部转账记录。
|
||||
ALTER TABLE pjcloudx_wallet.wallet_rule_transaction
|
||||
ADD INDEX idx_wrt_tenant_del_refund_ct_id (tenant_id, del_flag, is_refund, create_time, id),
|
||||
ALGORITHM=INPLACE, LOCK=NONE;
|
||||
|
||||
-- 退回日期范围:该筛选不要求传 is_refund,使用独立的日期前缀。
|
||||
ALTER TABLE pjcloudx_wallet.wallet_rule_transaction
|
||||
ADD INDEX idx_wrt_tenant_del_rt_ct_id (tenant_id, del_flag, refund_time, create_time, id),
|
||||
ALGORITHM=INPLACE, LOCK=NONE;
|
||||
|
||||
-- EXISTS 和当前页流水定位;biz_type 是 TINYTEXT,必须使用前缀长度。
|
||||
-- 必须保留 biz_id 的精确字符串等值条件;SIGNED 等值仅用于反向主键定位,不能替代它。
|
||||
ALTER TABLE pjcloudx_wallet.wallet_transaction
|
||||
ADD INDEX idx_wt_tenant_bizid_type (tenant_id, biz_id, biz_type(8)),
|
||||
ALGORITHM=INPLACE, LOCK=NONE;
|
||||
|
||||
-- 部门树、部门权限的流水存在性判断;也覆盖按租户/类型筛选的子查询。
|
||||
ALTER TABLE pjcloudx_wallet.wallet_transaction
|
||||
ADD INDEX idx_wt_tenant_type_dept_bizid (tenant_id, biz_type(8), dept_id, biz_id, user_id),
|
||||
ALGORITHM=INPLACE, LOCK=NONE;
|
||||
|
||||
-- 执行后用实际租户和日期 EXPLAIN 验证:
|
||||
-- SELECT a.id FROM wallet_rule_transaction a
|
||||
-- WHERE a.tenant_id = ? AND a.del_flag = '0'
|
||||
-- AND a.create_time BETWEEN ? AND ?
|
||||
-- ORDER BY a.create_time DESC, a.id DESC LIMIT 10;
|
||||
-- 预期使用 idx_wrt_tenant_del_ct_id,无临时表和 filesort。
|
||||
-- SELECT MIN(t.id) FROM wallet_transaction t
|
||||
-- WHERE t.tenant_id = ? AND t.biz_id = ? AND t.biz_type = '6';
|
||||
-- 预期使用 idx_wt_tenant_bizid_type,通过业务编号定位少量流水。
|
||||
@@ -0,0 +1,32 @@
|
||||
-- /wallet/wallet-manage/qb-total 历史余额统计索引(MySQL 8)。
|
||||
-- 先在钱包库执行本脚本,再发布 Wallet 服务;不修改钱包、流水或余额数据。
|
||||
-- 大流水表建索引会产生 IO/空间开销,请在低峰执行并观察磁盘和复制延迟。
|
||||
-- 显式 GROUP BY tenant_id,del_flag,wallet_id,user_id 与此前缀配合使用跳组扫描。
|
||||
-- create_time 确定月末之前的最新时间,id 解决同秒多笔流水的取值。
|
||||
-- 不替换既有钱包流水/业务查询索引;其他索引承担不同查询场景。
|
||||
|
||||
SET @wallet_qb_ddl = IF(EXISTS (
|
||||
SELECT 1 FROM (
|
||||
SELECT INDEX_NAME, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS cols,
|
||||
SUM(SUB_PART IS NOT NULL) AS prefix_columns,
|
||||
SUM(COLLATION != 'A') AS descending_columns
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = 'pjcloudx_wallet' AND TABLE_NAME = 'wallet_transaction'
|
||||
AND IS_VISIBLE = 'YES' AND INDEX_TYPE = 'BTREE'
|
||||
GROUP BY INDEX_NAME
|
||||
) indexes
|
||||
WHERE prefix_columns = 0
|
||||
AND descending_columns = 0
|
||||
AND (cols = 'tenant_id,del_flag,wallet_id,user_id,create_time,id'
|
||||
OR LEFT(cols, LENGTH('tenant_id,del_flag,wallet_id,user_id,create_time,id') + 1)
|
||||
= 'tenant_id,del_flag,wallet_id,user_id,create_time,id,')
|
||||
), 'SELECT ''wallet qb: equivalent history index exists''',
|
||||
'CREATE INDEX idx_wallet_qb_latest ON pjcloudx_wallet.wallet_transaction (tenant_id,del_flag,wallet_id,user_id,create_time,id) ALGORITHM=INPLACE LOCK=NONE');
|
||||
PREPARE wallet_qb_stmt FROM @wallet_qb_ddl;
|
||||
EXECUTE wallet_qb_stmt;
|
||||
DEALLOCATE PREPARE wallet_qb_stmt;
|
||||
|
||||
-- 同名异构索引会报错,核对后人工处理,不自动删除旧索引。
|
||||
-- 发布后用真实 tenant、月份及筛选参数执行 EXPLAIN ANALYZE:
|
||||
-- 分组子查询应显示 Using index for group-by / Covering index skip scan for grouping,
|
||||
-- 窗口节点处理的是钱包/历史所属用户候选数,不应是整月之前的全部流水数。
|
||||
@@ -0,0 +1,42 @@
|
||||
# 批量充值、扣减防重上线说明
|
||||
|
||||
本次只修代码和新增防重表。脚本不删除历史流水、不回填防重记录、不调整余额。历史差额、补偿扣款必须另行核账;不能根据 biz_id 重复就直接删除或再次扣钱。
|
||||
|
||||
## 修复范围
|
||||
|
||||
- 提交、编辑、删除、回调和补记先锁定申请。已经提交或存在资金流水的申请不能再次发起、重置审批字段或更改金额明细;同一发票只能绑定一次。
|
||||
- 回调在锁内通过“租户 + 申请 + 流程号 + 审核中”条件变更状态,并检查影响行数。状态、发票、整批余额、流水和防重登记使用同一本地事务。任一明细失败全部回滚;钱包缺失不再跳过部分入账。
|
||||
- `addWalletBalance` 在数据库钱包行锁内读取最新余额,检查更新/流水写入结果和整数溢出。类型 2(批量更新余额)与 8(退还各自食堂)按 `tenant_id + wallet_id + biz_type + biz_id` 防重。正金额、负金额、零金额均覆盖。
|
||||
- 类型 1 人工调账仍允许使用同一用户业务号多次操作。未对整张 wallet_transaction 添加 biz_id 唯一限制,也未将本次防重规则扩展到所有业务类型。
|
||||
- 补记仅允许审批通过的申请,与正常回调使用相同金额方向、流水类型和入账防重。草稿、审核中、拒绝的申请不能补记。
|
||||
- 正元同步改到资金事务提交后,并用独立事务执行;失败记录申请号和租户,不再影响已提交的资金。此同步仍为提交后的同步调用,未新增持久化重试队列,也未保证回调响应在 10 秒内完成。
|
||||
|
||||
## 历史数据兼容
|
||||
|
||||
防重表上线时为空。首次收到旧业务号时,会在数据库当前读中查询相同租户/钱包/类型/业务号的历史流水,包括逻辑删除的记录:
|
||||
|
||||
| 情况 | 行为 |
|
||||
| --- | --- |
|
||||
| 无历史流水 | 余额、流水、防重键一起提交 |
|
||||
| 唯一有效流水,金额相同 | 登记原流水,直接返回原结果,余额不变 |
|
||||
| 多条流水、金额不同、流水已删除,或防重登记与流水不一致 | 拒绝处理,整笔回滚并记录异常,需要人工核查 |
|
||||
|
||||
不要通过删除防重表记录或修改审批状态来“重试”。若正元同步失败,应重试同步用户信息;重放一个已处理回调不会再执行同步。恢复历史数据必须以核账结果为依据。
|
||||
|
||||
## 上线顺序
|
||||
|
||||
1. 暂停批量充值/扣减提交、审批回调和补记入口,排空在途请求。保留资金数据备份和当前对账结果。
|
||||
2. 连接目标钱包库,在该库执行 `20261102_wallet_recharge_idempotency.sql`。确认表为 InnoDB,复合主键与脚本一致。脚本可重复执行;不要对历史流水直接添加全局唯一 biz_id。
|
||||
3. 更新并重启**全部钱包服务实例**后恢复入口。新旧版本混用期间,旧实例不会遵守新的防重协议,因此不能在混用时恢复资金写入。
|
||||
4. 在预发布环境使用指定测试账户依次验证充值、记账扣款、退还食堂、重复回调、补记,以及中途失败回滚;对照余额、累计金额、流水数量和申请状态。生产验证须使用经业务确认的真实交易,并进行对应对账。
|
||||
5. 监控“批量入账金额冲突”“批量入账历史流水异常”“充值回调未找到有效申请”“充值回调失败”和“正元同步失败”日志。异常时保留 `tenantId / applyId / processId / taskId / walletId / bizId`,核查后处理。
|
||||
|
||||
回退应用版本也必须先停写并排空请求;旧版本本身没有防重能力。保留新增防重表,不能把删表作为回退步骤。此改动未修改工作流引擎的通知发送机制;历史重复通知或重新发起审批的具体触发原因仍需生产请求/审批日志确认。
|
||||
|
||||
## 本地验证
|
||||
|
||||
`WalletRechargeIdempotencyMySqlTest` 使用真实 MySQL、InnoDB 行锁、项目租户拦截器/自动填充配置、生产 Mapper SQL 和 Spring 事务代理。仅新建并删除 `codex_recharge_*` 隔离测试库,不读取或改动生产数据。需显式提供 `WALLET_RECHARGE_TEST_JDBC_URL`、`MYSQL_USER`、`MYSQL_PWD`,没有连接时该组测试会跳过,不能将跳过当作通过。
|
||||
|
||||
主要覆盖并发/顺序重复入账(正数、负数、零、类型8)、同钱包不同业务并发、旧事务快照、历史重复/删除/金额冲突、流水失败/钱包更新失败回滚、租户隔离、重复人工调账、重复提交/重复回调、整批和发票状态回滚、补记方向与审批限制、发票重复绑定、提交后同步与同步失败隔离。
|
||||
|
||||
2026-11-02 本地验证结果:39 项真实 MySQL 用例及 20 项相关回归用例全部通过(共 59 项,0 失败、0 错误、0 跳过);钱包服务及依赖模块 Maven package 成功。测试库已清理。未部署、未执行生产 SQL。
|
||||
@@ -0,0 +1,11 @@
|
||||
-- 在部署新版 wallet 服务之前,对钱包库执行。只新增空表,不清理或修正历史流水、余额。
|
||||
CREATE TABLE IF NOT EXISTS wallet_transaction_idempotency (
|
||||
tenant_id BIGINT NOT NULL,
|
||||
wallet_id BIGINT NOT NULL,
|
||||
biz_type VARCHAR(8) CHARACTER SET ascii COLLATE ascii_bin NOT NULL COMMENT '仅批量更新2、退还食堂8',
|
||||
biz_id VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
||||
price INT NOT NULL COMMENT '有符号变动金额,分',
|
||||
transaction_id BIGINT NOT NULL,
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (tenant_id, wallet_id, biz_type, biz_id)
|
||||
) ENGINE=InnoDB COMMENT='批量钱包入账幂等键,与余额和流水同事务提交';
|
||||
+179
-15
@@ -177,22 +177,83 @@ CREATE TABLE hard_group
|
||||
DROP TABLE IF EXISTS `hard_invoice`;
|
||||
CREATE TABLE hard_invoice
|
||||
(
|
||||
id bigint NOT NULL COMMENT '编号',
|
||||
dept_id bigint NOT NULL COMMENT '部门id',
|
||||
hard_id bigint NOT NULL COMMENT '设备id',
|
||||
order_id bigint NOT NULL COMMENT '硬件订单id',
|
||||
status char(10) NOT NULL COMMENT '状态',
|
||||
pic varchar(256) NOT NULL COMMENT '发票图片',
|
||||
create_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
|
||||
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
||||
update_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '修改人',
|
||||
update_time datetime DEFAULT NULL COMMENT '修改时间',
|
||||
del_flag char(1) COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标记',
|
||||
tenant_id bigint DEFAULT NULL COMMENT '租户ID',
|
||||
PRIMARY KEY (id)
|
||||
id bigint NOT NULL COMMENT '编号',
|
||||
dept_id bigint NOT NULL COMMENT '部门id',
|
||||
hard_id bigint DEFAULT NULL COMMENT '来源业务或核销终端id',
|
||||
printer_hard_id bigint DEFAULT NULL COMMENT '目标尔印打印终端id',
|
||||
order_id bigint NOT NULL COMMENT '硬件订单id',
|
||||
print_type char(2) NOT NULL DEFAULT '1' COMMENT '打印类型,1发票、2兑换券',
|
||||
print_no int NOT NULL DEFAULT 1 COMMENT '同订单同类型打印序号',
|
||||
retry_version int NOT NULL DEFAULT 0 COMMENT '人工失败重试版本',
|
||||
attempt_count int NOT NULL DEFAULT 0 COMMENT '终端USB累计尝试次数',
|
||||
status char(10) NOT NULL DEFAULT '0' COMMENT '状态,0待生成、1待打印、2已打印、3打印失败、4已取消',
|
||||
failure_stage varchar(32) DEFAULT NULL COMMENT '失败阶段',
|
||||
last_error varchar(2000) DEFAULT NULL COMMENT '最近失败摘要',
|
||||
printed_at datetime DEFAULT NULL COMMENT '终端确认写入成功时间',
|
||||
reprint_reason varchar(500) DEFAULT NULL COMMENT '人工重打或再次核销原因',
|
||||
pic varchar(512) DEFAULT NULL COMMENT '后端生成的打印PNG地址',
|
||||
create_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
|
||||
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
||||
update_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '修改人',
|
||||
update_time datetime DEFAULT NULL COMMENT '修改时间',
|
||||
del_flag char(1) COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标记',
|
||||
tenant_id bigint DEFAULT NULL COMMENT '租户ID',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_hard_invoice_order_type_no (tenant_id, order_id, print_type, print_no),
|
||||
KEY idx_hard_invoice_printer_status (tenant_id, printer_hard_id, status, update_time, del_flag),
|
||||
KEY idx_hard_invoice_source_status (tenant_id, hard_id, status, del_flag)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_general_ci COMMENT ='发票记录';
|
||||
COLLATE = utf8mb4_general_ci COMMENT ='打印任务与凭证记录';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for hard_printer_binding
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `hard_printer_binding`;
|
||||
CREATE TABLE hard_printer_binding
|
||||
(
|
||||
id bigint NOT NULL COMMENT '绑定关系主键',
|
||||
source_hard_id bigint NOT NULL COMMENT 'SUPERMARKET或CANTEEN来源业务终端id',
|
||||
printer_hard_id bigint NOT NULL COMMENT 'EY_PRINT目标打印终端id',
|
||||
merchant_id bigint NOT NULL COMMENT '双方共同所属商户id快照',
|
||||
dept_id bigint NOT NULL COMMENT '双方共同所属商户部门id快照',
|
||||
create_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
|
||||
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
||||
update_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '修改人',
|
||||
update_time datetime DEFAULT NULL COMMENT '修改时间',
|
||||
tenant_id bigint NOT NULL COMMENT '租户ID',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_hard_printer_binding_source (tenant_id, source_hard_id),
|
||||
UNIQUE KEY uk_hard_printer_binding_printer (tenant_id, printer_hard_id),
|
||||
KEY idx_hard_printer_binding_merchant (tenant_id, merchant_id)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_general_ci COMMENT ='业务终端与尔印打印终端一对一绑定';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for hard_printer_config
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `hard_printer_config`;
|
||||
CREATE TABLE hard_printer_config
|
||||
(
|
||||
id bigint NOT NULL COMMENT '配置主键',
|
||||
printer_hard_id bigint NOT NULL COMMENT 'EY_PRINT打印终端id',
|
||||
printer_model varchar(128) DEFAULT NULL COMMENT '打印机型号',
|
||||
usb_vendor_id int DEFAULT NULL COMMENT 'USB Vendor ID十进制值',
|
||||
usb_product_id int DEFAULT NULL COMMENT 'USB Product ID十进制值',
|
||||
paper_width int NOT NULL DEFAULT 80 COMMENT '纸宽,单位mm,首期固定80',
|
||||
auto_cut char(1) NOT NULL DEFAULT '1' COMMENT '自动切刀开关,1开启、0关闭',
|
||||
image_mode varchar(16) NOT NULL DEFAULT 'GS_V0' COMMENT '图片指令模式,GS_V0或ESC_STAR',
|
||||
create_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
|
||||
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
||||
update_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '修改人',
|
||||
update_time datetime DEFAULT NULL COMMENT '修改时间',
|
||||
tenant_id bigint NOT NULL COMMENT '租户ID',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_hard_printer_config_printer (tenant_id, printer_hard_id)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_general_ci COMMENT ='尔印打印终端USB与ESC/POS配置';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for hard_car
|
||||
@@ -311,6 +372,50 @@ CREATE TABLE hard_order
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE = InnoDB COMMENT ='订单表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for hard_order_evidence_image
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `hard_order_evidence_image`;
|
||||
CREATE TABLE hard_order_evidence_image
|
||||
(
|
||||
id bigint NOT NULL AUTO_INCREMENT COMMENT '证据记录自增主键',
|
||||
order_id bigint NOT NULL COMMENT '订单主键,来源hard_order.id',
|
||||
android_order_code varchar(128) NOT NULL COMMENT '终端本地稳定订单号快照',
|
||||
evidence_type varchar(32) NOT NULL COMMENT '证据类型,FACE_RECOGNITION或MENU_RECOGNITION',
|
||||
evidence_role varchar(32) NOT NULL COMMENT '图片角色,FACE_REFERENCE、FACE_LIVE或MENU_LIVE',
|
||||
sequence_no int NOT NULL DEFAULT 0 COMMENT '同类型同角色图片序号,从0开始',
|
||||
user_id bigint DEFAULT NULL COMMENT '识别用户主键快照',
|
||||
face_sample_id varchar(64) DEFAULT NULL COMMENT '虹软人脸样本编号快照',
|
||||
reference_image_url varchar(512) DEFAULT NULL COMMENT '识别时后台参考人脸来源地址快照,不作为展示地址',
|
||||
similarity decimal(8, 6) DEFAULT NULL COMMENT '识别相似度快照,取值0至1',
|
||||
similarity_threshold decimal(8, 6) DEFAULT NULL COMMENT '终端生效的相似度阈值快照,取值0至1',
|
||||
hard_id bigint NOT NULL COMMENT '产生证据的设备主键',
|
||||
captured_at datetime DEFAULT NULL COMMENT '终端实际采集时间',
|
||||
sys_file_id bigint DEFAULT NULL COMMENT '文件中心sys_file记录主键',
|
||||
storage_file_name varchar(255) DEFAULT NULL COMMENT '文件中心对象名',
|
||||
image_url varchar(512) DEFAULT NULL COMMENT '订单证据图片访问地址',
|
||||
image_hash char(64) DEFAULT NULL COMMENT '实际文件SHA-256哈希',
|
||||
image_size bigint DEFAULT NULL COMMENT '实际文件大小,单位字节',
|
||||
image_width int DEFAULT NULL COMMENT '实际图片宽度,单位像素',
|
||||
image_height int DEFAULT NULL COMMENT '实际图片高度,单位像素',
|
||||
status varchar(32) NOT NULL COMMENT '证据状态,PENDING、AVAILABLE或UNAVAILABLE',
|
||||
failure_reason varchar(500) DEFAULT NULL COMMENT '永久缺失或复制失败摘要,不保存终端完整路径',
|
||||
app_version varchar(64) DEFAULT NULL COMMENT '产生证据的终端应用版本',
|
||||
create_by varchar(64) DEFAULT NULL COMMENT '创建人',
|
||||
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
||||
update_by varchar(64) DEFAULT NULL COMMENT '修改人',
|
||||
update_time datetime DEFAULT NULL COMMENT '修改时间',
|
||||
del_flag char(1) DEFAULT '0' COMMENT '逻辑删除标记,0有效,1删除',
|
||||
tenant_id bigint NOT NULL COMMENT '租户主键',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_hard_order_evidence_position
|
||||
(tenant_id, order_id, evidence_type, evidence_role, sequence_no, del_flag),
|
||||
KEY idx_hard_order_evidence_android_order
|
||||
(tenant_id, hard_id, android_order_code, del_flag),
|
||||
KEY idx_hard_order_evidence_status
|
||||
(tenant_id, status, update_time, del_flag)
|
||||
) ENGINE = InnoDB COMMENT ='硬件订单识别证据图片';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for hard_after_sale
|
||||
-- ----------------------------
|
||||
@@ -571,4 +676,63 @@ CREATE TABLE hard_spu_repertory
|
||||
del_flag char(1) COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标记',
|
||||
tenant_id bigint DEFAULT NULL COMMENT '租户ID',
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE = InnoDB COMMENT ='硬件商品库存更新记录';
|
||||
) ENGINE = InnoDB COMMENT ='硬件商品库存更新记录';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for hard_third_mi_order
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS hard_third_mi_order;
|
||||
CREATE TABLE hard_third_mi_order
|
||||
(
|
||||
id bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
|
||||
biz_type tinytext NOT NULL COMMENT '上架类型',
|
||||
user_id bigint NOT NULL COMMENT '用户id',
|
||||
auth_id bigint COMMENT '授权id',
|
||||
flow_id bigint COMMENT '流程id',
|
||||
order_id bigint COMMENT '订单id',
|
||||
out_order_no varchar(128) COMMENT '外部订单号',
|
||||
third_order_no varchar(128) COMMENT '第三方订单号',
|
||||
create_order_time datetime DEFAULT NULL COMMENT '创建订单时间',
|
||||
pay_order_time datetime DEFAULT NULL COMMENT '支付时间',
|
||||
refund_order_time datetime DEFAULT NULL COMMENT '退款时间',
|
||||
order_status tinyint COMMENT '订单状态',
|
||||
org_json text COMMENT '原始json数据',
|
||||
create_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
|
||||
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
||||
update_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '修改人',
|
||||
update_time datetime DEFAULT NULL COMMENT '修改时间',
|
||||
del_flag char(1) COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标记',
|
||||
tenant_id bigint DEFAULT NULL COMMENT '租户ID',
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE = InnoDB COMMENT ='硬件商品免密订单';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for hard_xfj_log
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `hard_xfj_log`;
|
||||
CREATE TABLE hard_xfj_log
|
||||
(
|
||||
id bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
|
||||
unique_code varchar(64) NOT NULL COMMENT '设备唯一编号',
|
||||
hard_type varchar(32) NOT NULL COMMENT '设备类型',
|
||||
title varchar(64) NOT NULL COMMENT '接口标题',
|
||||
headers varchar(2048) COMMENT 'headers',
|
||||
res varchar(2048) COMMENT 'res',
|
||||
req varchar(2048) COMMENT 'req',
|
||||
error varchar(2048) COMMENT 'error',
|
||||
start_time datetime DEFAULT NULL COMMENT '开始时间',
|
||||
end_time datetime DEFAULT NULL COMMENT '结束时间',
|
||||
cost int DEFAULT NULL COMMENT '耗时(毫秒)',
|
||||
create_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
|
||||
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
||||
update_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '修改人',
|
||||
update_time datetime DEFAULT NULL COMMENT '修改时间',
|
||||
del_flag char(1) COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标记',
|
||||
tenant_id bigint DEFAULT NULL COMMENT '租户ID',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY idx_unique_code (unique_code),
|
||||
KEY idx_hard_type (hard_type),
|
||||
KEY idx_title (title),
|
||||
KEY idx_start_time (start_time),
|
||||
KEY idx_end_time (end_time)
|
||||
) ENGINE = InnoDB COMMENT ='消费机日志';
|
||||
@@ -107,6 +107,7 @@ CREATE TABLE mch_manage
|
||||
address_des varchar(256) DEFAULT NULL COMMENT '商户地址',
|
||||
sort int NOT NULL COMMENT '排序',
|
||||
discounts_price varchar(256) NOT NULL COMMENT '优惠价格(没实际作用)',
|
||||
discounts_rate int DEFAULT NULL COMMENT '优惠折扣(1-99,10=1折)',
|
||||
cooperation_status varchar(64) NOT NULL COMMENT '合作状态',
|
||||
mch_count int NOT NULL COMMENT '分店数量',
|
||||
create_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
|
||||
@@ -374,6 +375,8 @@ CREATE TABLE wallet_transaction
|
||||
title varchar(128) NOT NULL COMMENT '流水标题',
|
||||
price int NOT NULL COMMENT '交易金额, 单位分',
|
||||
balance int NOT NULL COMMENT '余额, 单位分',
|
||||
tags varchar(512) DEFAULT NULL COMMENT '财务统计标签,多个wallet_transaction_tag字典值使用英文逗号分隔',
|
||||
remark varchar(256) DEFAULT NULL COMMENT '流水备注,最多256个字符',
|
||||
create_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
|
||||
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
||||
update_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '修改人',
|
||||
@@ -385,6 +388,18 @@ CREATE TABLE wallet_transaction
|
||||
DEFAULT CHARSET = utf8mb4 COMMENT ='会员钱包流水表';
|
||||
CREATE INDEX idx_dept_id ON wallet_transaction (wallet_id);
|
||||
|
||||
-- 批量充值/扣减持久化防重;已有环境执行 20261102_wallet_recharge_idempotency.sql。
|
||||
CREATE TABLE IF NOT EXISTS wallet_transaction_idempotency (
|
||||
tenant_id BIGINT NOT NULL,
|
||||
wallet_id BIGINT NOT NULL,
|
||||
biz_type VARCHAR(8) CHARACTER SET ascii COLLATE ascii_bin NOT NULL COMMENT '仅批量更新2、退还食堂8',
|
||||
biz_id VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
||||
price INT NOT NULL COMMENT '有符号变动金额,分',
|
||||
transaction_id BIGINT NOT NULL,
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (tenant_id, wallet_id, biz_type, biz_id)
|
||||
) ENGINE=InnoDB COMMENT='批量钱包入账幂等键,与余额和流水同事务提交';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for wallet_recharge_apply
|
||||
-- ----------------------------
|
||||
@@ -538,7 +553,9 @@ CREATE TABLE wallet_code_use
|
||||
update_time datetime DEFAULT NULL COMMENT '修改时间',
|
||||
del_flag char(1) COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标记',
|
||||
tenant_id bigint DEFAULT NULL COMMENT '租户ID',
|
||||
PRIMARY KEY (id)
|
||||
PRIMARY KEY (id),
|
||||
INDEX idx_wcu_user_biz (user_id, biz_id),
|
||||
INDEX idx_wcu_biz (biz_id)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4 COMMENT ='二维码使用表';
|
||||
|
||||
|
||||
@@ -1,3 +1,24 @@
|
||||
create table act_cancel
|
||||
(
|
||||
id BIGINT(19) auto_increment comment '活动项id'
|
||||
primary key,
|
||||
mch_id BIGINT(19) not null comment '活动商户id',
|
||||
notice_id BIGINT(19) not null comment '公告id',
|
||||
active_org_price INT(10) null comment '活动原价',
|
||||
active_cancel_price INT(10) not null comment '活动核销价',
|
||||
start_time DATETIME null comment '开始时间',
|
||||
expire_time DATETIME not null comment '到期时间',
|
||||
act_status CHAR(32) not null comment '活动状态(1:正常 2:关闭)',
|
||||
act_title CHAR(32) not null comment '活动标题',
|
||||
act_content CHAR(32) null comment '活动内容',
|
||||
create_by VARCHAR(64) null comment '创建人',
|
||||
create_time DATETIME null comment '创建时间',
|
||||
update_by VARCHAR(64) null comment '修改人',
|
||||
update_time DATETIME null comment '修改时间',
|
||||
del_flag CHAR default '0' null comment '删除标记',
|
||||
tenant_id BIGINT(19) null comment '租户ID'
|
||||
);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for act_cancel_item
|
||||
-- ----------------------------
|
||||
@@ -20,4 +41,77 @@ CREATE TABLE act_cancel_item
|
||||
del_flag char(1) COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标记',
|
||||
tenant_id bigint DEFAULT NULL COMMENT '租户ID',
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE = InnoDB COMMENT ='核销活动项';
|
||||
) ENGINE = InnoDB COMMENT ='核销活动项';
|
||||
|
||||
DROP TABLE IF EXISTS act_coupon;
|
||||
CREATE TABLE act_coupon
|
||||
(
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '优惠券ID',
|
||||
parent_act_id BIGINT(20) DEFAULT NULL COMMENT '父活动id',
|
||||
name VARCHAR(128) NOT NULL COMMENT '优惠券名称',
|
||||
type VARCHAR(64) NOT NULL COMMENT '优惠券类型 (1:满减券 2:折扣券 3:立减券 4:兑换券)',
|
||||
face_value INT DEFAULT 0 COMMENT '面额/折扣 (满减/立减单位:分, 折扣单位:百分比)',
|
||||
min_point INT DEFAULT 0 COMMENT '使用门槛 (满多少分可用, 0表示无门槛)',
|
||||
total_count INT NOT NULL DEFAULT 0 COMMENT '发行总量 (-1表示不限量)',
|
||||
surplus_count INT NOT NULL DEFAULT 0 COMMENT '剩余数量',
|
||||
limit_count INT DEFAULT 1 COMMENT '每人限领数量',
|
||||
limit_type VARCHAR(32) DEFAULT NULL COMMENT '限领类型 (once:活动期间一次 day:每天 week:每周 month:每月 year:每年)',
|
||||
valid_type VARCHAR(32) NOT NULL COMMENT '有效期类型 (1:固定日期 2:领取后生效)',
|
||||
valid_start_time DATETIME DEFAULT NULL COMMENT '有效期开始时间',
|
||||
valid_end_time DATETIME DEFAULT NULL COMMENT '有效期结束时间',
|
||||
valid_days INT DEFAULT 0 COMMENT '领取后有效天数',
|
||||
use_scope_type VARCHAR(32) DEFAULT 0 COMMENT '适用范围 (0:全场通用 1:指定商户 2:指定分类 3:指定商品)',
|
||||
scope_id VARCHAR(2048) DEFAULT NULL COMMENT '适用范围ID集合 (商户ID/分类ID/商品ID, 逗号分隔)',
|
||||
status VARCHAR(32) DEFAULT 0 COMMENT '状态 (0:草稿 1:进行中 2:已结束 3:已作废)',
|
||||
description VARCHAR(512) DEFAULT NULL COMMENT '使用说明',
|
||||
|
||||
create_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
|
||||
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
||||
update_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '修改人',
|
||||
update_time datetime DEFAULT NULL COMMENT '修改时间',
|
||||
del_flag char(1) COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标记',
|
||||
tenant_id bigint DEFAULT NULL COMMENT '租户ID',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_status (status),
|
||||
KEY idx_valid_end_time (valid_end_time)
|
||||
) ENGINE = InnoDB COMMENT ='优惠券活动';
|
||||
|
||||
DROP TABLE IF EXISTS act_coupon_record;
|
||||
CREATE TABLE act_coupon_record
|
||||
(
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '领取记录ID',
|
||||
coupon_id BIGINT(20) NOT NULL COMMENT '优惠券ID',
|
||||
user_id BIGINT(20) NOT NULL COMMENT '用户ID',
|
||||
user_name VARCHAR(64) DEFAULT NULL COMMENT '用户名称',
|
||||
coupon_code VARCHAR(64) NOT NULL COMMENT '券码(唯一标识)',
|
||||
status VARCHAR(32) NOT NULL DEFAULT '0' COMMENT '使用状态 (0:未使用 1:已使用 2:已过期 3:已作废)',
|
||||
|
||||
-- 冗余优惠券快照信息,防止优惠券修改后数据不一致
|
||||
coupon_name VARCHAR(128) NOT NULL COMMENT '优惠券名称',
|
||||
coupon_type VARCHAR(64) NOT NULL COMMENT '优惠券类型',
|
||||
face_value INT DEFAULT 0 COMMENT '面额/折扣',
|
||||
min_point INT DEFAULT 0 COMMENT '使用门槛',
|
||||
|
||||
-- 有效期
|
||||
start_time DATETIME NOT NULL COMMENT '有效开始时间',
|
||||
end_time DATETIME NOT NULL COMMENT '有效结束时间',
|
||||
|
||||
-- 使用信息
|
||||
use_time DATETIME DEFAULT NULL COMMENT '使用时间',
|
||||
order_id VARCHAR(64) DEFAULT NULL COMMENT '关联订单号',
|
||||
use_amount INT DEFAULT 0 COMMENT '抵扣金额(分)',
|
||||
|
||||
create_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
|
||||
create_time datetime DEFAULT NULL COMMENT '领取时间',
|
||||
update_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '修改人',
|
||||
update_time datetime DEFAULT NULL COMMENT '修改时间',
|
||||
del_flag char(1) COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标记',
|
||||
tenant_id bigint DEFAULT NULL COMMENT '租户ID',
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_coupon_code (coupon_code),
|
||||
KEY idx_user_id (user_id),
|
||||
KEY idx_coupon_id (coupon_id),
|
||||
KEY idx_end_time (end_time),
|
||||
KEY idx_status (status)
|
||||
) ENGINE = InnoDB COMMENT ='优惠券领取记录表';
|
||||
+228
-138
@@ -6,26 +6,30 @@ SET FOREIGN_KEY_CHECKS = 0;
|
||||
-- Table structure for app_article
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `app_article`;
|
||||
CREATE TABLE `app_article` (
|
||||
`id` bigint NOT NULL COMMENT '主键',
|
||||
`cid` bigint NOT NULL COMMENT '分类',
|
||||
`title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '标题',
|
||||
`intro` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '简介',
|
||||
`summary` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '摘要',
|
||||
`image` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '封面',
|
||||
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '内容',
|
||||
`author` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '作者',
|
||||
`visit` int unsigned NOT NULL DEFAULT '0' COMMENT '浏览',
|
||||
`sort` int unsigned NOT NULL DEFAULT '50' COMMENT '排序',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`create_by` varchar(32) COLLATE utf8mb4_general_ci NOT NULL COMMENT '创建人',
|
||||
`update_by` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
|
||||
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '删除时间',
|
||||
`tenant_id` bigint(20) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
KEY `cid_idx` (`cid`) USING BTREE COMMENT '分类索引'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='文章资讯表';
|
||||
CREATE TABLE `app_article`
|
||||
(
|
||||
`id` bigint NOT NULL COMMENT '主键',
|
||||
`cid` bigint NOT NULL COMMENT '分类',
|
||||
`title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '标题',
|
||||
`intro` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '简介',
|
||||
`summary` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '摘要',
|
||||
`image` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '封面',
|
||||
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '内容',
|
||||
`author` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '作者',
|
||||
`visit` int unsigned NOT NULL DEFAULT '0' COMMENT '浏览',
|
||||
`sort` int unsigned NOT NULL DEFAULT '50' COMMENT '排序',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`create_by` varchar(32) COLLATE utf8mb4_general_ci NOT NULL COMMENT '创建人',
|
||||
`update_by` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
|
||||
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '删除时间',
|
||||
`tenant_id` bigint(20) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
KEY `cid_idx` (`cid`) USING BTREE COMMENT '分类索引'
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_general_ci
|
||||
ROW_FORMAT = DYNAMIC COMMENT ='文章资讯表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of app_article
|
||||
@@ -37,19 +41,23 @@ COMMIT;
|
||||
-- Table structure for app_article_category
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `app_article_category`;
|
||||
CREATE TABLE `app_article_category` (
|
||||
`id` bigint unsigned NOT NULL COMMENT '主键',
|
||||
`name` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '名称',
|
||||
`sort` smallint unsigned NOT NULL DEFAULT '50' COMMENT '排序',
|
||||
`is_show` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '是否显示: 0=否, 1=是',
|
||||
`del_flag` char(1) COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '是否删除: 0=否, 1=是',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`create_by` varchar(32) COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '创建人',
|
||||
`update_by` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
|
||||
`tenant_id` bigint(20) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='文章分类表';
|
||||
CREATE TABLE `app_article_category`
|
||||
(
|
||||
`id` bigint unsigned NOT NULL COMMENT '主键',
|
||||
`name` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '名称',
|
||||
`sort` smallint unsigned NOT NULL DEFAULT '50' COMMENT '排序',
|
||||
`is_show` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '是否显示: 0=否, 1=是',
|
||||
`del_flag` char(1) COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '是否删除: 0=否, 1=是',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`create_by` varchar(32) COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '创建人',
|
||||
`update_by` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
|
||||
`tenant_id` bigint(20) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_general_ci
|
||||
ROW_FORMAT = DYNAMIC COMMENT ='文章分类表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of app_article_category
|
||||
@@ -61,18 +69,22 @@ COMMIT;
|
||||
-- Table structure for app_article_collect
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `app_article_collect`;
|
||||
CREATE TABLE `app_article_collect` (
|
||||
`id` bigint unsigned NOT NULL COMMENT '主键',
|
||||
`user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
|
||||
`article_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '文章ID',
|
||||
`create_time` datetime NOT NULL COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL COMMENT '更新时间',
|
||||
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '是否删除',
|
||||
`create_by` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
|
||||
`update_by` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
|
||||
`tenant_id` bigint(20) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='文章收藏表';
|
||||
CREATE TABLE `app_article_collect`
|
||||
(
|
||||
`id` bigint unsigned NOT NULL COMMENT '主键',
|
||||
`user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
|
||||
`article_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '文章ID',
|
||||
`create_time` datetime NOT NULL COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL COMMENT '更新时间',
|
||||
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '是否删除',
|
||||
`create_by` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
|
||||
`update_by` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
|
||||
`tenant_id` bigint(20) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_general_ci
|
||||
ROW_FORMAT = DYNAMIC COMMENT ='文章收藏表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of app_article_collect
|
||||
@@ -84,159 +96,237 @@ COMMIT;
|
||||
-- Table structure for app_page
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `app_page`;
|
||||
CREATE TABLE `app_page` (
|
||||
`id` bigint unsigned NOT NULL COMMENT '主键',
|
||||
`page_type` tinyint unsigned NOT NULL DEFAULT '10' COMMENT '页面类型',
|
||||
`page_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '页面名称',
|
||||
`page_data` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '页面数据',
|
||||
`create_by` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
|
||||
`update_by` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '修改人',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
|
||||
`del_flag` char(1) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '删除标记',
|
||||
`tenant_id` bigint(20) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='页面装修表';
|
||||
CREATE TABLE `app_page`
|
||||
(
|
||||
`id` bigint unsigned NOT NULL COMMENT '主键',
|
||||
`page_type` tinyint unsigned NOT NULL DEFAULT '10' COMMENT '页面类型',
|
||||
`page_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '页面名称',
|
||||
`page_data` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '页面数据',
|
||||
`create_by` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
|
||||
`update_by` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '修改人',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
|
||||
`del_flag` char(1) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '删除标记',
|
||||
`tenant_id` bigint(20) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_general_ci
|
||||
ROW_FORMAT = DYNAMIC COMMENT ='页面装修表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of app_page
|
||||
-- ----------------------------
|
||||
BEGIN;
|
||||
INSERT INTO `app_page` VALUES (1, 1, '商城首页', '[{\"title\":\"搜索\",\"name\":\"search\",\"disabled\":1,\"content\":{},\"styles\":{}},{\"title\":\"首页轮播图\",\"name\":\"banner\",\"content\":{\"enabled\":1,\"data\":[{\"image\":\"/api/static/banner01.png\",\"name\":\"\",\"link\":{\"path\":\"/pages/index/index\",\"name\":\"商城首页\",\"type\":\"shop\"}}]},\"styles\":{}},{\"title\":\"导航菜单\",\"name\":\"nav\",\"content\":{\"enabled\":1,\"data\":[{\"image\":\"https://minio.pjcloud.top/oss/app/nav01.png\",\"name\":\"资讯中心\",\"link\":{\"path\":\"/pages/news/news\",\"name\":\"文章资讯\",\"type\":\"shop\"}},{\"image\":\"https://minio.pjcloud.top/oss/app/nav02.png\",\"name\":\"我的收藏\",\"link\":{\"path\":\"/pages/collection/collection\",\"name\":\"我的收藏\",\"type\":\"shop\"}},{\"image\":\"https://minio.pjcloud.top/oss/app/nav03.png\",\"name\":\"个人设置\",\"link\":{\"path\":\"/pages/user_set/user_set\",\"name\":\"个人设置\",\"type\":\"shop\"}},{\"image\":\"https://minio.pjcloud.top/oss/app/nav04.png\",\"name\":\"联系客服\",\"link\":{\"path\":\"/pages/customer_service/customer_service\",\"name\":\"联系客服\",\"type\":\"shop\"}},{\"image\":\"https://minio.pjcloud.top/oss/app/nav05.png\",\"name\":\"关于我们\",\"link\":{\"path\":\"/pages/as_us/as_us\",\"name\":\"关于我们\",\"type\":\"shop\"}}]},\"styles\":{}},{\"id\":\"l84almsk2uhyf\",\"title\":\"资讯\",\"name\":\"news\",\"disabled\":1,\"content\":{},\"styles\":{}}]', NULL, 'admin', NULL, '2023-06-15 09:18:02', '0',1);
|
||||
INSERT INTO `app_page` VALUES (2, 2, '个人中心', '[{\"title\":\"用户信息\",\"name\":\"user-info\",\"disabled\":1,\"content\":{},\"styles\":{}},{\"title\":\"我的服务\",\"name\":\"my-service\",\"content\":{\"style\":2,\"title\":\"服务中心\",\"data\":[{\"image\":\"https://minio.pjcloud.top/oss/app/user_collect.png\",\"name\":\"我的收藏\",\"link\":{\"path\":\"/pages/collection/collection\",\"name\":\"我的收藏\",\"type\":\"shop\"}},{\"image\":\"https://minio.pjcloud.top/oss/app/user_setting.png\",\"name\":\"个人设置\",\"link\":{\"path\":\"/pages/user_set/user_set\",\"name\":\"个人设置\",\"type\":\"shop\"}},{\"image\":\"https://minio.pjcloud.top/oss/app/user_kefu.png\",\"name\":\"联系客服\",\"link\":{\"path\":\"/pages/customer_service/customer_service\",\"name\":\"联系客服\",\"type\":\"shop\"}}]},\"styles\":{}},{\"title\":\"个人中心广告图\",\"name\":\"user-banner\",\"content\":{\"enabled\":1,\"data\":[{\"image\":\"\",\"name\":\"sdds\",\"link\":{\"path\":\"/pages/user/user\",\"name\":\"个人中心\",\"type\":\"shop\"}}]},\"styles\":{}}]', NULL, 'admin', NULL, '2023-06-18 17:00:05', '0',1);
|
||||
INSERT INTO `app_page` VALUES (3, 3, '客服设置', '[{\"title\":\"客服设置\",\"name\":\"customer-service\",\"content\":{\"title\":\"添加客服二维码\",\"time\":\"早上 9:00 - 22:00\",\"mobile\":\"13800138000\",\"qrcode\":\"/admin/sys-file/local/adc5061f99e9440abcd9b22572909c88.jpg\"},\"styles\":{}}]', NULL, 'admin', NULL, '2023-06-14 13:12:19', '0',1);
|
||||
INSERT INTO `app_page`
|
||||
VALUES (1, 1, '商城首页',
|
||||
'[{\"title\":\"搜索\",\"name\":\"search\",\"disabled\":1,\"content\":{},\"styles\":{}},{\"title\":\"首页轮播图\",\"name\":\"banner\",\"content\":{\"enabled\":1,\"data\":[{\"image\":\"/api/static/banner01.png\",\"name\":\"\",\"link\":{\"path\":\"/pages/index/index\",\"name\":\"商城首页\",\"type\":\"shop\"}}]},\"styles\":{}},{\"title\":\"导航菜单\",\"name\":\"nav\",\"content\":{\"enabled\":1,\"data\":[{\"image\":\"https://minio.pjcloud.top/oss/app/nav01.png\",\"name\":\"资讯中心\",\"link\":{\"path\":\"/pages/news/news\",\"name\":\"文章资讯\",\"type\":\"shop\"}},{\"image\":\"https://minio.pjcloud.top/oss/app/nav02.png\",\"name\":\"我的收藏\",\"link\":{\"path\":\"/pages/collection/collection\",\"name\":\"我的收藏\",\"type\":\"shop\"}},{\"image\":\"https://minio.pjcloud.top/oss/app/nav03.png\",\"name\":\"个人设置\",\"link\":{\"path\":\"/pages/user_set/user_set\",\"name\":\"个人设置\",\"type\":\"shop\"}},{\"image\":\"https://minio.pjcloud.top/oss/app/nav04.png\",\"name\":\"联系客服\",\"link\":{\"path\":\"/pages/customer_service/customer_service\",\"name\":\"联系客服\",\"type\":\"shop\"}},{\"image\":\"https://minio.pjcloud.top/oss/app/nav05.png\",\"name\":\"关于我们\",\"link\":{\"path\":\"/pages/as_us/as_us\",\"name\":\"关于我们\",\"type\":\"shop\"}}]},\"styles\":{}},{\"id\":\"l84almsk2uhyf\",\"title\":\"资讯\",\"name\":\"news\",\"disabled\":1,\"content\":{},\"styles\":{}}]',
|
||||
NULL, 'admin', NULL, '2023-06-15 09:18:02', '0', 1);
|
||||
INSERT INTO `app_page`
|
||||
VALUES (2, 2, '个人中心',
|
||||
'[{\"title\":\"用户信息\",\"name\":\"user-info\",\"disabled\":1,\"content\":{},\"styles\":{}},{\"title\":\"我的服务\",\"name\":\"my-service\",\"content\":{\"style\":2,\"title\":\"服务中心\",\"data\":[{\"image\":\"https://minio.pjcloud.top/oss/app/user_collect.png\",\"name\":\"我的收藏\",\"link\":{\"path\":\"/pages/collection/collection\",\"name\":\"我的收藏\",\"type\":\"shop\"}},{\"image\":\"https://minio.pjcloud.top/oss/app/user_setting.png\",\"name\":\"个人设置\",\"link\":{\"path\":\"/pages/user_set/user_set\",\"name\":\"个人设置\",\"type\":\"shop\"}},{\"image\":\"https://minio.pjcloud.top/oss/app/user_kefu.png\",\"name\":\"联系客服\",\"link\":{\"path\":\"/pages/customer_service/customer_service\",\"name\":\"联系客服\",\"type\":\"shop\"}}]},\"styles\":{}},{\"title\":\"个人中心广告图\",\"name\":\"user-banner\",\"content\":{\"enabled\":1,\"data\":[{\"image\":\"\",\"name\":\"sdds\",\"link\":{\"path\":\"/pages/user/user\",\"name\":\"个人中心\",\"type\":\"shop\"}}]},\"styles\":{}}]',
|
||||
NULL, 'admin', NULL, '2023-06-18 17:00:05', '0', 1);
|
||||
INSERT INTO `app_page`
|
||||
VALUES (3, 3, '客服设置',
|
||||
'[{\"title\":\"客服设置\",\"name\":\"customer-service\",\"content\":{\"title\":\"添加客服二维码\",\"time\":\"早上 9:00 - 22:00\",\"mobile\":\"13800138000\",\"qrcode\":\"/admin/sys-file/local/adc5061f99e9440abcd9b22572909c88.jpg\"},\"styles\":{}}]',
|
||||
NULL, 'admin', NULL, '2023-06-14 13:12:19', '0', 1);
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for app_tabbar
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `app_tabbar`;
|
||||
CREATE TABLE `app_tabbar` (
|
||||
`id` bigint unsigned NOT NULL COMMENT '主键',
|
||||
`name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '导航名称',
|
||||
`selected` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '未选图标',
|
||||
`unselected` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '已选图标',
|
||||
`link` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '链接地址',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`create_by` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
|
||||
`update_by` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
|
||||
`del_flag` char(1) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '删除标记',
|
||||
`tenant_id` bigint(20) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='底部装修表';
|
||||
CREATE TABLE `app_tabbar`
|
||||
(
|
||||
`id` bigint unsigned NOT NULL COMMENT '主键',
|
||||
`name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '导航名称',
|
||||
`selected` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '未选图标',
|
||||
`unselected` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '已选图标',
|
||||
`link` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '链接地址',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`create_by` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
|
||||
`update_by` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
|
||||
`del_flag` char(1) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '删除标记',
|
||||
`tenant_id` bigint(20) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_general_ci
|
||||
ROW_FORMAT = DYNAMIC COMMENT ='底部装修表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of app_tabbar
|
||||
-- ----------------------------
|
||||
BEGIN;
|
||||
INSERT INTO `app_tabbar` VALUES (1, '首页', 'https://minio.pjcloud.top/oss/app/tabbar0.png', 'https://minio.pjcloud.top/oss/app/tabbar0_0.png', '{\"path\":\"/pages/index/index\",\"name\":\"商城首页\",\"type\":\"shop\"}', NULL, '2023-06-15 09:16:25', NULL, 'admin', '0',1);
|
||||
INSERT INTO `app_tabbar` VALUES (2, '资讯', 'https://minio.pjcloud.top/oss/app/tabbar1.png', 'https://minio.pjcloud.top/oss/app/tabbar1_1.png', '{\"path\":\"/pages/news/news\",\"name\":\"文章资讯\",\"type\":\"shop\"}', NULL, '2023-06-15 09:16:25', NULL, 'admin', '0',1);
|
||||
INSERT INTO `app_tabbar` VALUES (3, '我的', 'https://minio.pjcloud.top/oss/app/tabbar3.png', 'https://minio.pjcloud.top/oss/app/tabbar3_3.png', '{\"path\":\"/pages/user/user\",\"name\":\"个人中心\",\"type\":\"shop\"}', NULL, '2023-06-15 09:16:25', NULL, 'admin', '0',1);
|
||||
INSERT INTO `app_tabbar`
|
||||
VALUES (1, '首页', 'https://minio.pjcloud.top/oss/app/tabbar0.png', 'https://minio.pjcloud.top/oss/app/tabbar0_0.png',
|
||||
'{\"path\":\"/pages/index/index\",\"name\":\"商城首页\",\"type\":\"shop\"}', NULL, '2023-06-15 09:16:25', NULL,
|
||||
'admin', '0', 1);
|
||||
INSERT INTO `app_tabbar`
|
||||
VALUES (2, '资讯', 'https://minio.pjcloud.top/oss/app/tabbar1.png', 'https://minio.pjcloud.top/oss/app/tabbar1_1.png',
|
||||
'{\"path\":\"/pages/news/news\",\"name\":\"文章资讯\",\"type\":\"shop\"}', NULL, '2023-06-15 09:16:25', NULL,
|
||||
'admin', '0', 1);
|
||||
INSERT INTO `app_tabbar`
|
||||
VALUES (3, '我的', 'https://minio.pjcloud.top/oss/app/tabbar3.png', 'https://minio.pjcloud.top/oss/app/tabbar3_3.png',
|
||||
'{\"path\":\"/pages/user/user\",\"name\":\"个人中心\",\"type\":\"shop\"}', NULL, '2023-06-15 09:16:25', NULL,
|
||||
'admin', '0', 1);
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for app_role
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `app_role`;
|
||||
CREATE TABLE `app_role` (
|
||||
`role_id` bigint(20) NOT NULL,
|
||||
`role_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`role_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`role_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '创建人',
|
||||
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0',
|
||||
`tenant_id` bigint(20) DEFAULT NULL,
|
||||
PRIMARY KEY (`role_id`) USING BTREE,
|
||||
KEY `role_idx1_role_code` (`role_code`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='app角色表';
|
||||
CREATE TABLE `app_role`
|
||||
(
|
||||
`role_id` bigint(20) NOT NULL,
|
||||
`role_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`role_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`role_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '创建人',
|
||||
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0',
|
||||
`tenant_id` bigint(20) DEFAULT NULL,
|
||||
PRIMARY KEY (`role_id`) USING BTREE,
|
||||
KEY `role_idx1_role_code` (`role_code`) USING BTREE
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_general_ci
|
||||
ROW_FORMAT = DYNAMIC COMMENT ='app角色表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of app_role
|
||||
-- ----------------------------
|
||||
BEGIN;
|
||||
INSERT INTO `app_role` VALUES (1, 'app用户', 'APP_USER', 'app用户角色', '', '', '2022-12-07 06:34:18', '2023-03-09 06:34:42', '0', 1);
|
||||
INSERT INTO `app_role`
|
||||
VALUES (1, 'app用户', 'APP_USER', 'app用户角色', '', '', '2022-12-07 06:34:18', '2023-03-09 06:34:42', '0', 1);
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for app_social_details
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `app_social_details`;
|
||||
CREATE TABLE `app_social_details` (
|
||||
`id` bigint(20) NOT NULL COMMENT '主键', -- 主键
|
||||
`type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '社交类型', -- 社交类型
|
||||
`remark` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', -- 备注
|
||||
`app_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '应用ID', -- 应用ID
|
||||
`app_secret` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '应用密钥', -- 应用密钥
|
||||
`redirect_url` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '重定向URL', -- 重定向URL
|
||||
`ext` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '拓展字段', -- 拓展字段
|
||||
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '创建人', -- 创建人
|
||||
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人', -- 修改人
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间
|
||||
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间
|
||||
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标志', -- 删除标志
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC
|
||||
COMMENT='系统社交登录账号表'; -- 系统社交登录账号表
|
||||
CREATE TABLE `app_social_details`
|
||||
(
|
||||
`id` bigint(20) NOT NULL COMMENT '主键', -- 主键
|
||||
`type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '社交类型', -- 社交类型
|
||||
`remark` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', -- 备注
|
||||
`app_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '应用ID', -- 应用ID
|
||||
`app_secret` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '应用密钥', -- 应用密钥
|
||||
`redirect_url` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '重定向URL', -- 重定向URL
|
||||
`ext` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '拓展字段', -- 拓展字段
|
||||
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '创建人', -- 创建人
|
||||
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人', -- 修改人
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间
|
||||
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间
|
||||
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标志', -- 删除标志
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_general_ci
|
||||
ROW_FORMAT = DYNAMIC
|
||||
COMMENT ='系统社交登录账号表';
|
||||
-- 系统社交登录账号表
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of app_social_details
|
||||
-- ----------------------------
|
||||
BEGIN;
|
||||
INSERT INTO `app_social_details` VALUES (1, 'MINI', '小程序登录', 'app_id', 'app_secret', 'http://www.baidu.com123', NULL, '', 'admin', '2022-12-09 01:44:42', '2023-04-03 06:12:30', '0');
|
||||
INSERT INTO `app_social_details`
|
||||
VALUES (1, 'MINI', '小程序登录', 'app_id', 'app_secret', 'http://www.baidu.com123', NULL, '', 'admin',
|
||||
'2022-12-09 01:44:42', '2023-04-03 06:12:30', '0');
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for app_user
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `app_user`;
|
||||
CREATE TABLE `app_user` (
|
||||
`user_id` bigint(20) NOT NULL COMMENT '用户id',
|
||||
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户名',
|
||||
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '密码',
|
||||
`salt` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '盐值',
|
||||
`phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '手机号码',
|
||||
`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '头像图片链接',
|
||||
`nickname` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '拓展字段:昵称',
|
||||
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '拓展字段:姓名',
|
||||
`email` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '拓展字段:邮箱',
|
||||
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
|
||||
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
|
||||
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标志',
|
||||
`tenant_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '所属租户id',
|
||||
`last_modified_time` datetime DEFAULT NULL COMMENT '最后一次密码修改时间',
|
||||
`lock_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '锁定状态',
|
||||
`wx_openid` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '微信登录openId',
|
||||
PRIMARY KEY (`user_id`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='app用户表';
|
||||
CREATE TABLE `app_user`
|
||||
(
|
||||
`user_id` bigint(20) NOT NULL COMMENT '用户id',
|
||||
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户名',
|
||||
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '密码',
|
||||
`salt` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '盐值',
|
||||
`phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '手机号码',
|
||||
`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '头像图片链接',
|
||||
`nickname` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '拓展字段:昵称',
|
||||
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '拓展字段:姓名',
|
||||
`email` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '拓展字段:邮箱',
|
||||
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
|
||||
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
|
||||
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标志',
|
||||
`tenant_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '所属租户id',
|
||||
`last_modified_time` datetime DEFAULT NULL COMMENT '最后一次密码修改时间',
|
||||
`lock_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '锁定状态',
|
||||
`wx_openid` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '微信登录openId',
|
||||
PRIMARY KEY (`user_id`) USING BTREE
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_general_ci
|
||||
ROW_FORMAT = DYNAMIC COMMENT ='app用户表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of app_user
|
||||
-- ----------------------------
|
||||
BEGIN;
|
||||
INSERT INTO `app_user` VALUES (1, 'appuser', '$2a$10$XQu3TmORLqDWayFspQN.U.LigJ5TWPTdXPIn/6SxGHKED3PVpuMH6', NULL, '13054729089', NULL, 'aeizzz', '刘洪磊', 'aeizzz@foxmail.com', '', 'appuser', '2022-12-07 02:59:38', '2023-03-09 15:14:44', '0', 1, NULL, '0', 'oBxPy5EnbDiN-gGEaovCpp_IkrkQ');
|
||||
INSERT INTO `app_user`
|
||||
VALUES (1, 'appuser', '$2a$10$XQu3TmORLqDWayFspQN.U.LigJ5TWPTdXPIn/6SxGHKED3PVpuMH6', NULL, '13054729089', NULL,
|
||||
'aeizzz', '刘洪磊', 'aeizzz@foxmail.com', '', 'appuser', '2022-12-07 02:59:38', '2023-03-09 15:14:44', '0', 1,
|
||||
NULL, '0', 'oBxPy5EnbDiN-gGEaovCpp_IkrkQ');
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for app_user_role
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `app_user_role`;
|
||||
CREATE TABLE `app_user_role` (
|
||||
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
|
||||
`role_id` bigint(20) NOT NULL COMMENT '角色ID',
|
||||
PRIMARY KEY (`user_id`,`role_id`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='用户角色表';
|
||||
CREATE TABLE `app_user_role`
|
||||
(
|
||||
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
|
||||
`role_id` bigint(20) NOT NULL COMMENT '角色ID',
|
||||
PRIMARY KEY (`user_id`, `role_id`) USING BTREE
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_general_ci
|
||||
ROW_FORMAT = DYNAMIC COMMENT ='用户角色表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for app_menu
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `app_menu`;
|
||||
CREATE TABLE `app_menu`
|
||||
(
|
||||
`id` bigint(20) NOT NULL COMMENT '用户id',
|
||||
|
||||
`platform` varchar(32) DEFAULT NULL COMMENT '平台(小程序、支付宝)',
|
||||
`type` varchar(32) DEFAULT NULL COMMENT '菜单类型',
|
||||
`icon` varchar(258) DEFAULT NULL COMMENT '图标',
|
||||
`page` varchar(258) NOT NULL COMMENT '页面路径(包含参数)',
|
||||
`title` varchar(32) NOT NULL COMMENT '页面标题',
|
||||
`visible` varchar(32) NOT NULL COMMENT '是否显示',
|
||||
`sort` int NOT NULL COMMENT '排序',
|
||||
|
||||
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
|
||||
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
|
||||
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标志',
|
||||
`tenant_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '所属租户id',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_general_ci
|
||||
ROW_FORMAT = DYNAMIC COMMENT ='app菜单表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of app_user_role
|
||||
-- ----------------------------
|
||||
BEGIN;
|
||||
INSERT INTO `app_user_role` VALUES (1, 1);
|
||||
INSERT INTO `app_user_role`
|
||||
VALUES (1, 1);
|
||||
COMMIT;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
@@ -0,0 +1,44 @@
|
||||
-- 线上慢 SQL 索引优化(OceanBase MySQL 模式)
|
||||
-- 执行前请确认同名索引不存在;建议低峰期逐条执行并观察建索引耗时。
|
||||
-- 本文件只包含索引 DDL,不包含任何数据修改。
|
||||
|
||||
-- P0:订单列表按租户、删除标记、创建时间倒序分页。
|
||||
CREATE INDEX idx_hard_order_tenant_del_ct_uid
|
||||
ON pjcloudx_hardware.hard_order (tenant_id, del_flag, create_time, user_id);
|
||||
|
||||
-- P0:订单号 + 订单类型 + 状态精确计数。
|
||||
CREATE INDEX idx_hard_order_tenant_del_order_lookup
|
||||
ON pjcloudx_hardware.hard_order (tenant_id, del_flag, order_no, order_type, order_status);
|
||||
|
||||
-- P0:设备侧订单号精确查询。
|
||||
CREATE INDEX idx_hard_order_tenant_del_android_code
|
||||
ON pjcloudx_hardware.hard_order (tenant_id, del_flag, android_order_code);
|
||||
|
||||
-- P0:每个订单取最新售后记录(GROUP BY order_id + MAX(id))。
|
||||
CREATE INDEX idx_hard_after_sale_tenant_order_id
|
||||
ON pjcloudx_hardware.hard_after_sale (tenant_id, order_id, id);
|
||||
|
||||
-- P0:钱包流水按业务类型、时间倒序分页;biz_type 为 TINYTEXT,使用前缀索引。
|
||||
CREATE INDEX idx_wallet_tx_tenant_del_ct_uid
|
||||
ON pjcloudx_wallet.wallet_transaction (tenant_id, del_flag, create_time, user_id);
|
||||
|
||||
-- P0:钱包流水按业务类型、时间倒序分页;biz_type 为 TINYTEXT,使用前缀索引。
|
||||
CREATE INDEX idx_wallet_tx_tenant_del_biz_ct_uid
|
||||
ON pjcloudx_wallet.wallet_transaction (tenant_id, del_flag, biz_type(8), create_time, user_id);
|
||||
|
||||
-- P0:流程历史按办理人查询并按结束时间倒序。
|
||||
CREATE INDEX idx_act_hi_actinst_assignee_end
|
||||
ON pjcloudx_flow.act_hi_actinst (ASSIGNEE_, END_TIME_);
|
||||
|
||||
-- P1:支付状态 + 支付时间统计。先观察 P0 上线后的实际收益,再决定是否执行。
|
||||
-- CREATE INDEX idx_hard_order_tenant_del_status_ptime_mch
|
||||
-- ON pjcloudx_hardware.hard_order (tenant_id, del_flag, order_status, pay_time, merchant_id);
|
||||
|
||||
-- P1:指定商户部门的支付统计。索引较宽,按接口频率决定是否执行。
|
||||
-- CREATE INDEX idx_hard_order_tenant_del_mdept_status_mt_ptime
|
||||
-- ON pjcloudx_hardware.hard_order
|
||||
-- (tenant_id, del_flag, merchant_dept_id, order_status, merchant_type, pay_time);
|
||||
|
||||
-- P1:按钱包所属部门定位钱包 ID;维表较小时收益有限。
|
||||
-- CREATE INDEX idx_wallet_manage_tenant_dept_id
|
||||
-- ON pjcloudx_wallet.wallet_manage (tenant_id, dept_id, id);
|
||||
@@ -0,0 +1,54 @@
|
||||
# 线上慢 SQL 索引优化分析(2026-08-26)
|
||||
|
||||
## 数据范围
|
||||
|
||||
- 来源:`/Users/dengzhangchen/Downloads/rm-1fyg92tzhbt23g9hw_slowlog_*.csv`
|
||||
- 原始记录:638 条;按完整请求字段去重后 608 条;63 个 SQL Hash。
|
||||
- 数据库分布:`pjcloudx_hardware` 451 条、`pjcloudx_wallet` 153 条、`pjcloudx_flow` 4 条。
|
||||
- 主要扫描表:`hard_order`、`hard_after_sale`、`wallet_transaction`、`act_hi_actinst`。
|
||||
- 测试连接:OceanBase CE 4.3.5.5,MySQL 兼容模式;读取 `SHOW CREATE TABLE`、`SHOW INDEX`、`EXPLAIN`,并在测试库临时创建/回滚候选索引;未在生产库执行 DDL。
|
||||
|
||||
## 结论
|
||||
|
||||
### 强推荐,先上线
|
||||
|
||||
| 优先级 | 数据库/表 | 建议索引 | 主要覆盖日志 | 依据 |
|
||||
|---|---|---|---|---|
|
||||
| P0 | `pjcloudx_hardware.hard_order` | `(tenant_id, del_flag, create_time, user_id)` | 订单列表、分页 COUNT,Hash `270048891%7C1391`、`528608244%7C616` 等 | 现有索引的第三列是 `merchant_id` 或 `user_id`,无法同时支撑租户+删除标记后的时间倒序;临时索引验证后由扫描+TOP-N 变为反向范围扫描。 |
|
||||
| P0 | `pjcloudx_hardware.hard_order` | `(tenant_id, del_flag, order_no, order_type, order_status)` | 订单号精确 COUNT,Hash `_1019382644%7C138` | 原执行计划只能在统计索引上按 `order_status` 扫描,再回表过滤订单号/类型;临时索引验证为三列等值范围扫描。 |
|
||||
| P0 | `pjcloudx_hardware.hard_order` | `(tenant_id, del_flag, android_order_code)` | 设备订单号查询,Hash `_986870313%7C628` | 原计划扫描约 98 万行后回表过滤 `android_order_code`;临时索引验证为等值范围扫描。 |
|
||||
| P0 | `pjcloudx_hardware.hard_after_sale` | `(tenant_id, order_id, id)` | 订单列表中的“每单最新售后”子查询,覆盖 233 条相关记录 | 原表只有主键,`GROUP BY order_id + MAX(id)` 为全表扫描;临时索引验证后为有序范围扫描并使用 Merge Group By。 |
|
||||
| P0 | `pjcloudx_wallet.wallet_transaction` | `(tenant_id, del_flag, create_time, user_id)` | 无业务类型条件的钱包流水首页,Hash `698590166%7C408` | 原计划全表扫描并排序;临时索引验证后为反向范围扫描,直接取前 10 行。 |
|
||||
| P0 | `pjcloudx_wallet.wallet_transaction` | `(tenant_id, del_flag, biz_type(8), create_time, user_id)` | 钱包流水按业务类型筛选列表/COUNT,Hash `2036241023%7C466`、`255738862%7C620` 等 | `biz_type` 是 `TINYTEXT`,必须使用前缀索引;临时索引验证后可按租户、删除标记、业务类型及时间范围扫描。 |
|
||||
| P0 | `pjcloudx_flow.act_hi_actinst` | `(ASSIGNEE_, END_TIME_)` | 流程历史待办查询,Hash `_2096433074%7C126` | 原计划全表扫描并排序;临时索引验证后为反向范围扫描,直接满足 `ORDER BY END_TIME_ DESC LIMIT 10`。 |
|
||||
|
||||
### 条件推荐,先观察资源与收益
|
||||
|
||||
| 优先级 | 建议索引 | 适用场景 | 说明 |
|
||||
|---|---|---|---|
|
||||
| P1 | `pjcloudx_hardware.hard_order (tenant_id, del_flag, order_status, pay_time, merchant_id)` | 按支付状态+支付时间汇总,Hash `_1655261703%7C470` 等 | 临时索引可将范围缩小到状态/时间,但仍需要回表和聚合;建议 P0 上线后按 `EXPLAIN` 和实际耗时决定。 |
|
||||
| P1 | `pjcloudx_hardware.hard_order (tenant_id, del_flag, merchant_dept_id, order_status, merchant_type, pay_time)` | 指定商户部门的支付统计,Hash `1634334540%7C358` 等 | 对指定部门过滤收益明显;宽索引写放大较大,先对高频租户/接口评估。 |
|
||||
| P1 | `pjcloudx_wallet.wallet_manage (tenant_id, dept_id, id)` | 钱包按部门聚合 | 仅用于小维表定位钱包 ID;表规模较小时收益有限,适合与钱包统计接口一起验证。 |
|
||||
|
||||
## 暂不建议只靠加索引解决
|
||||
|
||||
- 统计类 SQL 使用派生表、外层权限过滤和 `ORDER BY CASE`,索引只能减少底层扫描,不能消除派生表物化/聚合成本;建议后续改为先过滤再聚合,并移除 COUNT 查询中的无意义排序。
|
||||
- `hard_order` 订单详情列表反复 `LEFT JOIN` 最新售后子查询;P0 索引后仍建议将“最新售后 ID”改为窗口/预聚合或单独维护字段。
|
||||
- `UPDATE hard_door_cabinet ... WHERE id = ? AND del_flag = ? AND tenant_id = ?` 已命中主键 `id`,慢点更可能是锁等待、事务或网络;加索引不能解决,需结合 `SHOW PROCESSLIST`、锁等待和事务耗时排查。
|
||||
|
||||
## 上线步骤
|
||||
|
||||
1. 低峰期逐条执行 `01_add_indexes.sql`,每条 DDL 单独观察完成时间和租户延迟。
|
||||
2. 执行前确认同名索引不存在;OceanBase 建索引可能持续几十秒到数分钟,不要重复提交。
|
||||
3. DDL 完成后对代表性 SQL 执行 `EXPLAIN`,确认使用新索引且不出现 `TABLE FULL SCAN`。
|
||||
4. 观察 15–30 分钟慢 SQL、CPU、IO、事务提交延迟;若写入延迟明显,再评估 P1 索引。
|
||||
5. 统计接口仍慢时,优先改写 SQL,再考虑继续增加宽复合索引。
|
||||
|
||||
## 验证记录
|
||||
|
||||
临时索引均已在测试库创建并回滚,最终检查无 `tmp_%` 索引残留。验证结果:
|
||||
|
||||
- 订单时间分页:`TABLE RANGE SCAN ... Reverse`,不再全表扫描/大范围 TOP-N。
|
||||
- 钱包业务流水:`TABLE RANGE SCAN ... Reverse`,`biz_type(8)` 前缀索引可用。
|
||||
- 最新售后:`MERGE GROUP BY` + `TABLE RANGE SCAN`,不再全表扫描。
|
||||
- 流程历史:`TABLE RANGE SCAN ... Reverse`,不再全表扫描。
|
||||
@@ -0,0 +1,58 @@
|
||||
# 消费机创建订单分段耗时
|
||||
|
||||
外部接口 `/hard/hard-api/ad/create-order` 对应 hard 服务内部
|
||||
`POST /hard-api/ad/create-order`。本次诊断仅观察该请求,不改变请求协议、
|
||||
事务注解、分布式锁、支付顺序、重试和返回值。
|
||||
|
||||
## 日志策略与开销
|
||||
|
||||
- 通过 `System.nanoTime()` 和请求内存中的阶段表计时,不新增 SQL、Redis 或 Feign 请求。
|
||||
- 正常且小于 1 秒的请求不新增摘要;慢请求、失败及阶段内被业务处理的异常输出一条 warning。
|
||||
- 摘要在过滤器链返回或抛错后统一生成,不在扣款写事务中逐阶段输出日志。
|
||||
- 专用 Logback 异步队列容量为 256,`neverBlock=true`。队列满时丢弃诊断摘要,不等待落盘。
|
||||
- 使用已有 `debug.log` 文件;不写入 DTO、人员资料、金额、令牌、付款码、图片或异常消息。
|
||||
|
||||
计时与内存分配存在少量开销,不能承诺零开销。异步队列优先保障请求返回,
|
||||
日志管道持续拥堵时可能缺少部分诊断记录。
|
||||
|
||||
## 取日志与匹配
|
||||
|
||||
部署包含 Java 代码和 `logback-spring.xml` 改动的 hard 服务后,在 `debug.log`
|
||||
搜索 `|终端支付耗时|`。按 `androidOrderCode` 与消费机的 `|支付耗时|` 摘要、
|
||||
原有订单 SQL 对齐;同号重试可通过不同的 `trace` 区分请求。
|
||||
|
||||
`outcome:failed` 是请求未正常完成,不能单凭这一字段判断钱包是否已经扣款;
|
||||
仍需按原有订单对账逻辑确认。旁路通知或证据登记异常被业务自行处理时,
|
||||
请求可能保持 `outcome:success`,异常类型保存在 `stageErrorTypes` 中。
|
||||
|
||||
## 主要阶段
|
||||
|
||||
| 字段 | 范围 |
|
||||
| --- | --- |
|
||||
| `pre_controller` | 请求监听器开始至 Controller 进入,含期间租户检查、认证、解析及校验 |
|
||||
| `controller` | Controller 参数整理、Service 调用和返回对象创建 |
|
||||
| `redis.lock` / `redis.unlock` | 原有 Redis 加锁、持锁检查及解锁 |
|
||||
| `idempotency.lookup` / `idempotency.validate` | 原订单查询及重复请求一致性校验 |
|
||||
| `prepare` | 下单准备整体,含下面的远程及本地查询 |
|
||||
| `prepare.merchant` / `prepare.hardware` | 商户远程查询、设备本地查询 |
|
||||
| `prepare.user` / `prepare.wallet` / `prepare.cart` | 用户、钱包及商品计价准备 |
|
||||
| `transaction` | 原有代理事务调用,包含事务开始、提交及同步 afterCommit |
|
||||
| `db.cart` / `db.orderInsert` | 购物车和订单写入 |
|
||||
| `wallet.debit` | 原有钱包扣款远程调用 |
|
||||
| `db.orderPaid` / `db.cartPaid` | 已支付状态更新 |
|
||||
| `afterCommit.evidence` | 原有同步人脸证据元数据登记 |
|
||||
| `postCommit.notify` | 原有提交后打印/播报分支 |
|
||||
|
||||
`stagesInclusiveMs` 单位为毫秒,父阶段包含子阶段,不能直接相加。
|
||||
其他支付方式仍按原逻辑执行,日志可出现核销、库存及第三方支付阶段。
|
||||
|
||||
## 时间边界
|
||||
|
||||
监听器在 Servlet 过滤器之前记录起点,专用过滤器在原有租户过滤器之后、
|
||||
安全过滤器之前绑定请求追踪,不调整已有过滤器顺序。
|
||||
若请求在更早的租户过滤器中直接拒绝,尚未进入计时过滤器,则不会产生本摘要。
|
||||
|
||||
`totalMs` 截止过滤器链返回,不是客户端端到端耗时;不覆盖网关和入站网络、
|
||||
容器调用请求监听器前的排队、最后的响应传输,以及客户端本地处理。
|
||||
因此“客户端 4500ms、服务摘要 200ms”只能证明还需排查摘要计时区间之外的等待,
|
||||
不能直接认定为 Flutter 程序卡顿。
|
||||
@@ -0,0 +1,228 @@
|
||||
# 文件读取与上传签名接入及发布
|
||||
|
||||
本次保护 `/admin/sys-file/oss/file` 的全部文件。默认 `observe` 只记录签名校验结果;只有加载完整配置并切到 `enforce` 后,普通裸链接才会被拒绝。旧 Dart 图片兼容是独立开关。客户端密钥可提取、UA 可伪造、有效接口地址仍可被模拟请求使用,这是一层应用级校验,不是用户或租户文件授权。
|
||||
|
||||
上传入口 `/admin/sys-file/upload`、`/admin/sys-file/inner-upload`、`/admin/sys-file/qn/upload` 同步使用应用签名和服务端文件白名单。上传复用这套配置和密钥,不增加环境密钥组;读取签名不能用于上传。
|
||||
|
||||
## 配置准备
|
||||
|
||||
服务端配置见 `config.example.yml`。密钥为至少 32 字节随机数据的 Base64 字符串,HMAC 使用 **Base64 解码后的字节**。不要复用 OAuth 客户端信息或密码加密密钥。
|
||||
|
||||
在 `pj_service_cloud` 执行以下命令可为并列的五个项目生成匹配的本地配置:
|
||||
|
||||
```bash
|
||||
python3 scripts/generate-file-access-config.py \
|
||||
--public-base-url https://www.pjzhhq.cn/api \
|
||||
--allowed-origin https://stgl.pj.gov.cn
|
||||
```
|
||||
|
||||
按用户最终要求,所有环境共用一套密钥配置,`admin/h5/mini/flutter/service/package` 各自保留固定密钥;开发、测试、生产不再另建密钥组。只有公网基础地址等部署参数随目标环境填写。脚本统一读取 `.file-access/keys.json`,重复执行不会更换密钥;旧命令的 `--environment` 参数仍接受,但不再切换密钥。
|
||||
|
||||
迁移时沿用原生产密钥组,避免已构建客户端失效。脚本只更新受管理的文件访问配置字段,保留其他业务环境变量;移除旧 Vite 模式文件中的文件访问字段,避免其覆盖公共配置。旧文件备份位于 Java 项目的 `.file-access/migration-backup/`,不参与构建。当前活动配置均被 Git 忽略:
|
||||
|
||||
首次生成只做一次,其他构建机器和 CI 同步这套现有配置。密钥统一后无需随部署环境重新生成。
|
||||
|
||||
| 项目 | 本地文件 | 使用方式 |
|
||||
| --- | --- | --- |
|
||||
| Java | `.file-access/config.yml` | Nacos 共享配置或 Spring 外部配置 |
|
||||
| 后台 | `.env.local` | 所有 Vite 构建模式共用 |
|
||||
| H5 和小程序 | 各项目的 `.env.local` | 所有 Vite 构建模式共用 |
|
||||
| Flutter | `.file-access/config.json` | 所有构建使用 `--dart-define-from-file=.file-access/config.json` |
|
||||
|
||||
前端的配置项为 `VITE_FILE_ACCESS_APP_ID`、`VITE_FILE_ACCESS_KEY_ID`、`VITE_FILE_ACCESS_KEY`、`VITE_FILE_ACCESS_ALLOWED_ORIGINS`。Flutter 去掉 `VITE_` 前缀。Java 的 `clients` 包含 `admin/h5/mini/flutter/service/package`;`service` 和 `package` 密钥只放服务端。
|
||||
|
||||
**自动构建不会取得被 Git 忽略的 `.env.local`。** 后台的 `build`、`build:prod`、`build:docker` 统一使用 `--mode prod`,自动工具执行 `npm run build` / `pnpm build` 时读取 `.env.prod`。自动构建应从已提交的 `.env.prod` 或流水线环境变量取得同一组 admin 配置,继续使用既有密钥;不要为流水线重新生成密钥。缺少必要配置时构建会失败,发布流程应停止,不能继续部署上一次留下的 `dist`。
|
||||
|
||||
H5 使用 uni-app 的 `production` 模式,自动构建配置为该项目的 `.env.production`,其中 appId 为 `h5`,复用既有 H5 密钥。`build`、`build:h5` 显式使用 `uni build -p h5 --mode production`;Dockerfile 的 `pnpm build:h5` 同样读取该文件,默认产物目录为 `dist/build/h5`。自动构建所需的新配置文件须与源码一起提交;本地 `.env.local` 仍可用于开发,不作为 CI 输入。
|
||||
|
||||
`public-base-url` 是外部可访问的网关基础地址,可以带 `/api`,不含 `/admin/sys-file/oss/file`。现有的图片/API 基础地址继续由各端原有配置决定;`allowed-origins` / `ALLOWED_ORIGINS` 只列确实属于本系统的历史域名。上线前必须逐环境核对域名、基础路径、appId、kid 和密钥。相对 `/api/admin/sys-file/oss/file` 按当前文件基础地址解析,绝对历史地址保留其原有域名和部署路径。
|
||||
|
||||
**Java 不会自动读取仓库里的 `.file-access`。** 本地运行可以设置:
|
||||
|
||||
```bash
|
||||
export SPRING_CONFIG_ADDITIONAL_LOCATION=file:/absolute/path/pj_service_cloud/.file-access/config.yml
|
||||
```
|
||||
|
||||
通过现有 Nacos 配置体系加载这套 `file-access` 配置,或把外部配置挂载到各服务并设置上述变量。实际部署路径与公网基础地址应对应目标机器。首次生成默认为 `observe`,已有配置保留当前模式;显式 `--mode` 可更新本地生成配置的模式。先检查配置源优先级;`config.example.yml` 中的环境变量占位符不能代替实际密钥。严格模式要求 `admin/mini/flutter/service/package` 的有效活动密钥及公网基础地址。`h5` 条目可整体省略;一旦配置该条目,密钥也必须完整。未配置 H5 不会自动放行 H5 请求,只有下面明确配置的图片来源可免签。运行中错误配置返回 500,不会降级匿名访问。即使在观察模式,版本签发和服务间读取也需要配置 `package/service` 密钥。
|
||||
|
||||
## 协议
|
||||
|
||||
请求查询参数为 `appId`、`kid`、`ts`、`sign`、`fileName`。签名原文按 LF 连接,无末尾换行:
|
||||
|
||||
```text
|
||||
v1
|
||||
oss-file-read
|
||||
appId
|
||||
kid
|
||||
ts
|
||||
Base64URL无填充编码的UTF-8文件名
|
||||
```
|
||||
|
||||
`ts` 是 Unix 秒;`sign` 是 HMAC-SHA256 的 64 位小写十六进制。`fileName` 只 URL 解码一次,始终放在最后。后端生成的 APK URL 因此仍以 `.apk` 结尾,兼容旧终端的扩展名解析。
|
||||
|
||||
默认普通签名有效 1800 秒,`package` 有效 86400 秒,允许签发时间领先最多 300 秒。`GET /admin/sys-file/oss/access-info` 匿名返回 `R.data.serverTime`(**毫秒**)和 `ttlSeconds`,不包含密钥。各端校准时差并在签名剩余不足 5 分钟时续期;校时失败保留上次时差,不能退回裸链接。传输 URL 缓存在内存里,普通渲染不会每次产生新签名。
|
||||
|
||||
重复关键参数、缺少字段、非规范时间戳或非规范签名为 400;未知客户端/密钥、错误签名、过期、时钟越界为 403;文件不存在为 404,数据库或存储读取失败为 500。GET 和 HEAD 共用校验。普通登录 Token 不会绕过校验。文件响应最多私有缓存 5 分钟且不超过剩余签名寿命;错误响应禁止缓存。已经输出部分字节后的网络/存储中断无法可靠改写 HTTP 状态,应按中断响应处理。
|
||||
|
||||
`block-browser-navigation` 默认为 `true`。严格模式下,浏览器携带 `Sec-Fetch-Mode: navigate` 或 `Sec-Fetch-Dest: document/iframe/frame` 的文件请求返回 403,即使签名正确也不放行;覆盖地址栏粘贴、新窗口打开和直接嵌入文件页面。图片、音视频以及 fetch/XHR 下载继续校验签名后访问。响应的 `Vary` 包含 `Sec-Fetch-Mode` 和 `Sec-Fetch-Dest`,防止浏览器把图片缓存复用为文件页面。观察模式仅记录 `browser_navigation`,不拦截。
|
||||
|
||||
后台会截获本系统文件 `<a>` 链接,通过已有 Blob 下载工具处理;H5/小程序富文本文件链接改走图片预览或下载后打开本地文件。复制出来的接口地址不再承诺可直接在浏览器打开。Flutter 原生下载、Feign 等没有浏览器 Fetch Metadata 的请求仍执行签名/旧 Dart 规则;不支持这些请求头的旧浏览器同样不能靠本规则识别。这项规则用于限制普通浏览器导航,不能防止脚本携带有效签名并模拟请求,也不能阻止已经获准显示的图片被保存。
|
||||
|
||||
## 旧 mobile H5 的图片来源兼容
|
||||
|
||||
旧 H5 无需重新构建或更新。在更新后的后端配置中增加:
|
||||
|
||||
```yaml
|
||||
file-access:
|
||||
public-image-referer-prefixes:
|
||||
- https://www.pjzhhq.cn/mobile/
|
||||
- https://yyfb.idw.jinhua.gov.cn:6443/
|
||||
```
|
||||
|
||||
只对 GET/HEAD 图片读取生效:请求带有单个 `Referer`,其协议、主机、有效端口与配置匹配时,按以下两种范围判断:
|
||||
|
||||
- 带目录的配置(如 `/mobile/`)仍要求 Referer 路径位于该目录下,目录必须以 `/` 结尾。
|
||||
- 根来源配置(如 `https://yyfb.idw.jinhua.gov.cn:6443/`,末尾 `/` 可省略)允许该来源下所有页面,包括浏览器跨域策略只发送根来源的请求。端口必须匹配;未写端口的 HTTPS 表示 443,不会匹配 6443。根来源不能再限制到某个应用目录。
|
||||
|
||||
命中后,JPG/JPEG/PNG/GIF/WebP/BMP/SVG/ICO 可以不带 appId 或签名读取。页面 URL 的 `#/` 不会随 HTTP Referer 发送,也不能写进配置。跨域策略如果省略路径,单纯配置长目录不会自动退回根来源匹配。来源命中的图片请求整体免签,因此旧页面携带的过期签名也不阻止加载。重复关键参数、非法文件名和浏览器页面导航仍优先拒绝;PDF、APK、日志等文件及上传接口不享受此例外。
|
||||
|
||||
这是独立的来源白名单,保留或省略 `clients.h5` 都可使用;不能仅根据 `appId=h5` 放行。新版本允许省略整个 H5 客户端配置,但空密钥、错误密钥配置仍报错,其他五类客户端的必要配置保持严格检查。若删除共享配置中的 H5 条目,所有加载旧版公共配置类的服务也需要同步更新;保留现有 H5 配置同样可以启用来源兼容。
|
||||
|
||||
配置默认空列表;显式清空列表即可恢复图片签名要求。必须部署新 UPMS 和对应的公共依赖后再启用配置,不能用旧后端只删除 H5 密钥。Referer 必须由浏览器和代理保留;没有 Referer 的匿名请求仍返回 403。Referer 可被脚本模拟,这是一层兼容性的来源检查。成功响应私有缓存最多 5 分钟,`Vary` 额外区分 Referer,代理不得忽略该差异复用放行响应。
|
||||
|
||||
根来源支持需要新版 `pjcloud-common-core` 和 UPMS 一起构建发布。建议将这份图片来源白名单放在 UPMS 自己的配置中;若通过共享配置下发给其他启用文件访问配置的服务,这些服务也必须先更新公共依赖,旧版校验类会拒绝根来源。此改动不要求更新 H5 或更换任何密钥。
|
||||
|
||||
## 上传身份与文件类型
|
||||
|
||||
现有登录鉴权及 `@Inner` 内部调用限制保留。每次 POST multipart 上传只允许一个名为 `file` 的文件部分,通过下列请求头传递身份,不把上传凭据放进 URL 或业务字段:
|
||||
|
||||
| 请求头 | 内容 |
|
||||
| --- | --- |
|
||||
| `X-File-App-Id` | `admin/h5/mini/flutter/service`,`package` 不可上传 |
|
||||
| `X-File-Key-Id` | 现有 kid,如 `v1` |
|
||||
| `X-File-Timestamp` | Unix 秒级时间戳 |
|
||||
| `X-File-Nonce` | 16~64 位字母、数字、`_`、`-`,每次请求重新生成 |
|
||||
| `X-File-Signature` | HMAC-SHA256 的 64 位小写十六进制 |
|
||||
|
||||
签名原文按 LF 连接,无末尾换行;密钥仍使用 Base64 解码后的字节:
|
||||
|
||||
```text
|
||||
v1
|
||||
oss-file-upload
|
||||
POST
|
||||
target
|
||||
appId
|
||||
kid
|
||||
ts
|
||||
nonce
|
||||
```
|
||||
|
||||
`target` 分别为 `upload`、`inner-upload`、`qn-upload`,三种签名不能互换。`inner-upload` 仅接受 `service`。签名用于应用身份和本次上传操作校验,文件内容由服务端独立检查。默认有效 300 秒,允许客户端时间领先最多 60 秒;复用现有匿名校时接口。严格模式下 Redis 原子 `SET NX` 消耗 nonce,重复请求返回 403;Redis 或配置不可用返回 500,不自动放行。不增加 Dart UA 上传兼容。
|
||||
|
||||
`file-access.upload` 默认策略如下,省略配置时 Java 使用相同默认值:
|
||||
|
||||
| 文件类别 | 白名单后缀 |
|
||||
| --- | --- |
|
||||
| 图片 | jpg/jpeg/png/gif/webp/bmp/ico/svg |
|
||||
| 文档和安装包 | pdf/apk/doc/docx/xls/xlsx/ppt/pptx |
|
||||
| 文本和压缩包 | txt/csv/zip |
|
||||
| 音视频 | mp4/mov/webm/avi/mp3/wav/m4a/aac/ogg/amr/flac |
|
||||
| 仅内部上传额外允许 | log/gz,供终端日志使用 |
|
||||
|
||||
默认单文件最大 200 MiB,仍受现有网关、反向代理和 Servlet 更低限制约束。SVG 最大 1 MiB,只接受静态内容,拒绝脚本、事件、外部引用和实体。服务层在写存储或文件记录前统一校验后缀、文件头及适用的图片尺寸/ZIP 容器结构;APK 检查二进制 AndroidManifest,Office 新格式检查对应容器入口。存储后缀和 MIME 由服务端规范化,不信任客户端 Content-Type。这里的格式检查不等于病毒或文档宏扫描。
|
||||
|
||||
JPEG、PNG、GIF、BMP、WebP 位图按实际文件头识别和校验:声明后缀与实际格式均须在对应白名单中,符合条件时使用实际格式保存,不修改图片字节。例如名为 `.png` 的合法 WebP 保存为 `.webp`、MIME 为 `image/webp`。声明为 EXE/PDF/SVG 等其他类别的文件不通过这个规则转成图片,SVG 和文档继续执行各自的严格检查。后台单图、多图及七牛图片上传默认支持 WebP。
|
||||
|
||||
缺少或错误身份、过期、重放为 403;重复或畸形头、多个文件部分为 400;不支持类型或内容不符为 415;超过本策略大小为 413。错误响应禁止缓存。文件类型检查在 `observe` 下仍执行,观察模式只放宽应用签名准入。监控为 `file.upload.requests`,标签 `outcome/app/mode`,不记录密钥、签名和登录 Token。
|
||||
|
||||
后台 Axios、通用 el-upload 和富文本上传,H5/小程序的 `uni.uploadFile` 拦截器,以及 Flutter `HttpManage` 为本系统上传路径生成新请求头;导入接口和第三方上传地址不追加本协议。`RemoteFileService` 专属 Feign 拦截器为每次内部上传生成 service 签名。上传成功返回值和保存的文件引用继续为稳定原始地址。
|
||||
|
||||
**已有读取严格模式的环境,先发布客户端及调用文件服务的 Java 服务,再发布 UPMS。** 旧 UPMS 忽略新增头;新版 UPMS 在 `enforce` 下会拒绝没有上传签名的旧网页/服务请求。Java 调用方须随公共依赖一同更新,避免只升级 UPMS。旧 Flutter 当前通过硬件业务接口上传取证和日志,再由 Java 调用文件中心,这条链路无需旧设备立即升级;不要把这一结论扩展到其他直接调用文件中心的旧客户端。必要时切回 `observe` 恢复旧身份请求,文件白名单仍然生效。
|
||||
|
||||
## 入口与数据约束
|
||||
|
||||
| 部分 | 接入位置与行为 |
|
||||
| --- | --- |
|
||||
| Java 文件入口 | `FileAccessGuard`,`SysFileController`,`SysFileServiceImpl`;租户跳过只作用于文件查询并恢复原状态 |
|
||||
| Java 内部读取 | `RemoteFileService.getFile` 通过编码桥接和专属 Feign 拦截器签名,保留文件名中的字面 `%xx`;钱包、硬件导出/日志/取证及发票盖章通过相同签名读取 |
|
||||
| APK | 终端 `/app-version/ad/page` 和 `/app/last-version` 返回对象使用 `package` 签名,后台列表/编辑和数据库仍为原始值;独立静态 APK 地址保持不变 |
|
||||
| 后台 | `src/utils/file-access*.ts`;`FileImage` 保留已加载画面,未加载的懒图片继续续期,打开/切换预览时取有效签名;失败重试仅刷新当前文件,不清空全局签名缓存;上传回显、素材库、头像、视频、版本复制和下载均已接入;KKFileView 在编码之前签名源地址 |
|
||||
| 后台编辑器 | 只更新编辑器 DOM 的资源属性,不替换整篇文档;输出 HTML 剥离临时签名,续期不进入编辑历史 |
|
||||
| H5 和小程序 | `src/sheep/utils/file-access*.ts`、`filePath/parseHtml`、共享预览下载工具及各页面资源属性;公共算法不依赖 DOM/window;富文本图片样式保留 |
|
||||
| Flutter | `FileAccess`、`AppNetworkTool`、`SignedNetworkImage`;主副屏独立初始化,缓存键用规范化原始地址,传输 URL 用签名 |
|
||||
| Flutter APK 重试 | 原生下载收到 403 时重新查询相同 ID、版本码、平台和应用的已发布版本,最多重试一次,不能换成另一个版本继续安装 |
|
||||
|
||||
业务对象、表单、上传返回值、SQLite 人脸 `imagePath`、失败记录和 `referenceImageUrl` 继续保留原始引用。本次没有数据库迁移、SQLite 升级或清空人脸库。新业务展示统一调用文件工具,不要在 API 响应拦截器里递归改写业务字符串。
|
||||
|
||||
第三方 URL、静态资源、`data:`、`blob:`、本地临时文件不加本协议签名。复制访问链接是临时访问地址;编辑、存储文件引用使用原始值。
|
||||
|
||||
### Java 图片合成等内部读取
|
||||
|
||||
文件中心图片使用 `RemoteFileService.getFile(fileName)`,每次调用由专属 Feign 拦截器生成 `service` 签名,并通过服务发现访问 UPMS。业务字段如果保存的是完整或相对文件链接,先用 `FileAccessUrl.fileName(source)` 提取原始文件名:
|
||||
|
||||
```java
|
||||
String fileName = FileAccessUrl.fileName(source);
|
||||
if (fileName == null) throw new IllegalArgumentException("不是文件中心引用");
|
||||
try (Response response = remoteFileService.getFile(fileName)) {
|
||||
if (response.status() < 200 || response.status() >= 300 || response.body() == null) {
|
||||
throw new IOException("文件读取失败");
|
||||
}
|
||||
try (InputStream input = response.body().asInputStream()) {
|
||||
BufferedImage image = ImageIO.read(input);
|
||||
// 校验 image 非空后合成。
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
提取工具支持原始文件名、`/sys-file/oss/file`、`/admin/sys-file/oss/file` 及带网关前缀的 `/admin/sys-file/oss/file` 链接;查询中的文件名只解码一次,重复、缺失或非法文件名拒绝处理。链接中的主机和旧签名不参与内部调用,始终读取当前环境的 UPMS,不会向原链接主机发送 `service` 凭据。
|
||||
|
||||
`WalletRechargeInvoiceServiceImpl.stamp` 已按此方式读取底图。因此 `http://127.0.0.1:9999/admin/sys-file/oss/file?...`、历史域名链接和相对链接都不再依赖 `IMAGE_BASE_URL` 或公网来源列表。非文件中心图片保留原有 HTTP 读取方式,相对静态资源仍使用 `IMAGE_BASE_URL`。调用方和 UPMS 必须加载相同的 `clients.service` 活动密钥;本次修复需要重新构建并发布 Wallet 及更新后的 common-core,无需为本机地址扩大 `allowed-origins`。
|
||||
|
||||
旧终端兼容只接受完整 `Dart/<版本> (dart:io)` UA,且扩展名在 jpg/jpeg/png/gif/webp/bmp/svg/ico 内。完全没有签名参数才可走兼容分支;错误或过期签名不会回退。APK、日志、文档不凭 Dart UA 放行。
|
||||
|
||||
## 发布和回滚
|
||||
|
||||
1. 在配置中心准备匹配的环境配置,保持 `mode: observe` 和 `legacy-dart.enabled: true`。发布 UPMS、integrated,以及包含文件调用的 wallet、hardware、canteen 等服务和统一公共依赖。只更新 UPMS 会遗漏内部读取。
|
||||
2. 发布后台、H5、小程序,并提供携带生产配置的新 Flutter 包。小程序保留原文件域名,真机检查 request/downloadFile 域名配置、图片预览和打开文档。
|
||||
3. 至少观察一个完整业务日,确认登录前 Logo/背景、人员照片、商品/菜品、富文本、附件下载、导出、票据和旧设备更新链路。旧设备必须实测图片 UA 和 Android 原生 APK 下载。
|
||||
4. 通过验收后设置 `mode: enforce`。按当前配置中心机制刷新或重启实例,逐个确认配置已生效;旧网页需刷新,小程序需加载新版本。
|
||||
5. 根据设备版本台账确认覆盖后再关 `legacy-dart.enabled`。没有自动失效日,短期没有旧请求不能证明设备已全部升级。
|
||||
6. 遗漏影响业务时切回 `observe` 并保留诊断;这会重新允许普通未签名请求。无需数据库回滚,也无需四端同时回退。
|
||||
|
||||
代理/CDN 必须把文件路径送到后端校验,保留完整查询参数,不得忽略查询参数缓存文件、强制公共缓存或通过其他公开存储入口绕过该接口。校时接口也必须匿名可达。不要在访问日志中记录完整签名 URL、密钥或登录 Token;可按路径、来源类别和结果计数。
|
||||
|
||||
监控 `file.access.requests`(Prometheus 通常呈现为 `file_access_requests_total`),标签 `outcome/app/mode`。结果包括 `signed`、`missing_signature`、`malformed_signature`、`invalid_signature`、`expired`、`clock_skew`、`legacy_dart`、`unknown_client`、`unknown_key`、`configuration_error` 等。文件查询/存储失败另计 `file.access.read.failures`。拒绝/兼容日志按结果采样,计数器保留完整次数。
|
||||
|
||||
轮换时先在服务端增加新 kid,同时保留旧 kid,再更新客户端构建;服务端主动签发使用 active-kid。Flutter 的旧 kid 需等设备覆盖后才移除。不要用重新运行生成脚本代替轮换流程。
|
||||
|
||||
## 本地验证命令
|
||||
|
||||
三个前端分别执行:
|
||||
|
||||
```bash
|
||||
node --test tests/file-access.test.cjs tests/file-upload.test.cjs
|
||||
```
|
||||
|
||||
后台执行 `pnpm build:prod`,H5 执行 `pnpm build:h5`,小程序执行 `pnpm build:mp-weixin`。跨语言固定向量在各项目 `test-vectors/file-access.json` 和 `test-vectors/file-upload.json`,其中的密钥是公开测试数据,不能用于部署。配置生成器回归执行 `python3 -m unittest discover -s scripts/tests`。
|
||||
|
||||
Java 定向回归:
|
||||
|
||||
```bash
|
||||
mvn -pl pjcloud-upms/pjcloud-upms-biz,pjcloud-integrated/pjcloud-integrated-biz,pjcloud-wallet/pjcloud-wallet-biz,pjcloud-hardware/pjcloud-hardware-biz,pjcloud-canteen/pjcloud-canteen-biz \
|
||||
-am test \
|
||||
-Dtest=FileAccessTest,FileReadTest,FileUploadGuardTest,FileUploadPolicyTest,FileUploadControllerTest,AppVersionFileAccessTest,FileAccessUrlTest,WalletRechargeInvoiceFileAccessTest,WalletManageBatchExportTest,WalletLedgerBatchExportTest,HardBatchExportFileTest,HardBatchExportQueryTest \
|
||||
-Dsurefire.failIfNoSpecifiedTests=false
|
||||
```
|
||||
|
||||
Flutter 使用项目 FVM 3.27.4:
|
||||
|
||||
```bash
|
||||
fvm flutter test --no-pub --dart-define-from-file=.file-access/config.json \
|
||||
test/file_access_test.dart test/file_upload_test.dart test/package_download_test.dart \
|
||||
test/face_sync_plan_test.dart test/face_sync_pipeline_test.dart \
|
||||
test/face_sync_failure_store_test.dart test/face_readiness_cache_test.dart \
|
||||
test/customer_display_balance_test.dart test/customer_display_balance_widget_test.dart
|
||||
fvm flutter build apk --release --no-pub --dart-define-from-file=.file-access/config.json
|
||||
```
|
||||
|
||||
验收结果与待现场验证项见 `verification.md`。不要把本地构建成功或源码中的 Dart UA 推断视为旧安装包验收。
|
||||
@@ -0,0 +1,45 @@
|
||||
# Load through Nacos shared config or Spring additional-location. No usable keys are committed.
|
||||
file-access:
|
||||
mode: observe
|
||||
block-browser-navigation: true
|
||||
ttl-seconds: 1800
|
||||
package-ttl-seconds: 86400
|
||||
future-skew-seconds: 300
|
||||
public-base-url: https://files.example.com/api
|
||||
allowed-origins:
|
||||
- https://legacy-files.example.com
|
||||
# Optional: old H5 needs no update; only GET/HEAD image reads bypass signatures.
|
||||
# Remove this list or set [] to keep all browser sources subject to signing.
|
||||
public-image-referer-prefixes:
|
||||
- https://www.pjzhhq.cn/mobile/
|
||||
# Root origin allows every page on this exact scheme/host/port (trailing slash optional).
|
||||
# Use only when cross-origin browser requests omit the Referer path; requires updated common-core/UPMS.
|
||||
# - https://yyfb.idw.jinhua.gov.cn:6443/
|
||||
legacy-dart:
|
||||
enabled: true
|
||||
extensions: [jpg, jpeg, png, gif, webp, bmp, svg, ico]
|
||||
upload:
|
||||
ttl-seconds: 300
|
||||
max-file-bytes: 209715200 # 200 MiB; gateway/servlet limits may be lower
|
||||
allowed-extensions: [jpg, jpeg, png, gif, webp, bmp, ico, svg, pdf, apk, doc, docx, xls, xlsx, ppt, pptx, txt, csv, zip, mp4, mov, webm, avi, mp3, wav, m4a, aac, ogg, amr, flac]
|
||||
internal-extensions: [log, gz] # service-signed inner-upload only
|
||||
clients:
|
||||
admin:
|
||||
active-kid: v1
|
||||
keys: {v1: '${FILE_ACCESS_ADMIN_V1:}'}
|
||||
h5:
|
||||
# Optional. Remove the whole h5 node if no H5 signing key is configured.
|
||||
active-kid: v1
|
||||
keys: {v1: '${FILE_ACCESS_H5_V1:}'}
|
||||
mini:
|
||||
active-kid: v1
|
||||
keys: {v1: '${FILE_ACCESS_MINI_V1:}'}
|
||||
flutter:
|
||||
active-kid: v1
|
||||
keys: {v1: '${FILE_ACCESS_FLUTTER_V1:}'}
|
||||
service:
|
||||
active-kid: v1
|
||||
keys: {v1: '${FILE_ACCESS_SERVICE_V1:}'}
|
||||
package:
|
||||
active-kid: v1
|
||||
keys: {v1: '${FILE_ACCESS_PACKAGE_V1:}'}
|
||||
@@ -0,0 +1,164 @@
|
||||
# 五端文件签名验证记录
|
||||
|
||||
验证日期:2026-10-29。范围为本地五个项目的实现、定向回归、构建和已登录后台页面检查。未发布服务、修改 Nacos、开启生产拦截、更新真实设备、执行数据库迁移或提交 Git。
|
||||
|
||||
## 已完成
|
||||
|
||||
| 验证 | 结果 |
|
||||
| --- | --- |
|
||||
| Java 请求级与业务回归 | 41 项通过,0 失败;覆盖 UPMS 文件校验/读取、版本地址、钱包和硬件导出,同时编译相关公共模块及 canteen |
|
||||
| 后台自动测试 | 14 项通过,包含文件协议与已有部门导出测试 |
|
||||
| H5 文件自动测试 | 14 项通过 |
|
||||
| 小程序文件自动测试 | 14 项通过 |
|
||||
| 跨语言协议 | Java、三端 TypeScript、Dart 使用相同公开向量;校验 UTF-8 文件名、秒级时间戳、Base64URL 原文和 HMAC 结果 |
|
||||
| URL 与运行时 | 覆盖中文、空格、加号、百分号编码、历史域名、相对/绝对地址、已有 `/api` 前缀、重复签名、第三方地址;前端测试执行实际运行时包装器,H5/小程序测试执行富文本预览组件的 JS 方法;Java 使用真实 SpringMvcContract/Feign 客户端与内存传输验证文件名字面 `%2F/%2B`、重复拦截和参数顺序 |
|
||||
| 后台生产构建 | `pnpm build:prod` 通过 |
|
||||
| H5 构建 | `pnpm build:h5` 通过 |
|
||||
| 微信小程序构建 | `pnpm build:mp-weixin` 通过 |
|
||||
| Flutter 定向回归 | 56 项通过;含签名、安装包一次重试、人脸同步计划/流水线/失败记录/缓存、主副屏余额回归 |
|
||||
| Android Release | FVM Flutter 3.27.4 构建通过,使用 `.file-access/production.json`;APK 约 101.9 MB |
|
||||
| 新增后台 TypeScript 工具 | 独立严格类型检查通过 |
|
||||
| 配置生成器 | 在临时目录重复运行,确认幂等、保留已有文件;开发/测试/生产配置和各客户端密钥相互独立 |
|
||||
| 密钥与原有变更 | 扫描五个项目全部已跟踪及未忽略候选文件,未发现生成密钥泄漏;后台原有补丁可反向校验,Java 原有补丁除追加忽略规则产生的上下文差异外可反向校验 |
|
||||
| Diff 检查 | 保留原有 CRLF 文件格式;使用 `git -c core.whitespace=cr-at-eol diff --check` 通过 |
|
||||
|
||||
APK 位置(相对本仓库):`../pj_supermarket_cloud/build/app/outputs/flutter-apk/app-release.apk`。本次构建尚未在真实终端安装验证;发布前须核对包内环境与服务端配置一致。
|
||||
|
||||
Java 定向测试及各端构建命令见 [接入与发布说明](README.md)。本机详细日志保存在 `/tmp/pj-file-access-work/`,属于临时验证资料,不随仓库发布。
|
||||
|
||||
## 静态检查与既有问题
|
||||
|
||||
使用任务开始前保存的补丁还原临时代码基线,与当前代码运行同一检查后比较:
|
||||
|
||||
| 项目 | 当前结果与基线比较 |
|
||||
| --- | --- |
|
||||
| 后台 `vue-tsc` | 6 条既有解析错误,均在 `src/views/admin/system/tenant/index.vue:83`;与基线完全一致 |
|
||||
| H5 `vue-tsc` | 4 条既有解析错误,在 `src/pages/tabbar/~individualOrder.vue:78-79`;与基线完全一致 |
|
||||
| 小程序 `vue-tsc` | 17 条既有 TS6504(JS Vue 来源与 allowJs 配置);规范化临时目录路径后与基线完全一致 |
|
||||
| Flutter `analyze --no-pub` | 69 条提示/警告;忽略因新增代码产生的行号偏移后,诊断集合与基线完全一致,无新增项 |
|
||||
|
||||
因此不能把本次结果表述为“全项目类型检查通过”。H5/小程序构建仍有 TM UI 类型导出和循环依赖等构建提示;构建已成功,未在本次跨端文件改造中整理无关依赖。
|
||||
|
||||
## 已登录后台检查
|
||||
|
||||
使用用户提供的 `http://localhost:8888/` 已登录会话,检查文件管理页:
|
||||
|
||||
- 本系统有效图片的缩略图与放大预览加载成功,资源请求带 `appId/kid/ts/sign`,`fileName` 位于最后。
|
||||
- 实测发现并修复前端 `/api` 前缀被重复保留的问题:当前开发图片服务器不带 `/api`,相对旧路径应转换为 `/admin/sys-file/oss/file`。配置含 `/api` 的部署仍保留一次前缀。
|
||||
- 旧头像的一条原始文件链接返回 HTTP 200、内容长度为 0,属于当前运行后端/存储的既有异常。新 Java 读取代码已覆盖不存在和存储异常状态,但尚未发布,不能声称现场问题已修复。
|
||||
- 当前后端匿名 `/admin/sys-file/oss/access-info` 返回 424 鉴权错误,说明运行环境尚未具备本次匿名校时接口;签名客户端会保留本地时间/最近偏移,不降级匿名读取。
|
||||
- 点击素材下载后未出现页面错误,但浏览器下载事件未在等待窗口内返回。浏览器工具自动安全检查拒绝打开 `chrome://downloads/`(仅允许 HTTP/HTTPS),未采用其他方式绕过此限制,**下载文件实际落盘未确认**。
|
||||
|
||||
当前运行后端仍接受裸文件链接。上述浏览器结果证明前端签名与展示链路可用,不代表新后端严格校验已在线生效。
|
||||
|
||||
## 发布前仍需完成
|
||||
|
||||
1. 核对每个环境的公网基础地址、历史域名、密钥和活动 kid;Java 不会自动加载 `.file-access/*.yml`,须通过 Nacos 或 `SPRING_CONFIG_ADDITIONAL_LOCATION` 加载。先以 `observe` 发布 UPMS、integrated 和所有实际调用文件服务的模块。
|
||||
2. 用发布后的后端确认匿名校时可达,执行 GET/HEAD、错误参数、错误/过期签名、旧 Dart 兼容、404/500、私有缓存及服务间读取联调。
|
||||
3. 在真实旧 Flutter 安装包上确认图片请求 UA、图片显示及 Android 原生 APK 下载。新版包确认主副屏、缓存命中、人脸重复同步/离线恢复、取证、打印、手动/自动更新;不能用本地源码或单元测试替代设备验收。
|
||||
4. 微信真机验证登录前 Logo/背景、长时间停留和恢复前台、图片预览、文件下载及打开文档;核对微信文件域名配置。后台与 H5 补充对应实际业务操作验收。
|
||||
5. 至少观察一个完整业务日,确认已知入口无遗漏后再切 `enforce`;根据设备版本台账确认升级覆盖后再关闭旧 Dart 图片兼容。
|
||||
|
||||
当前默认保持 `observe` 与旧 Dart 图片兼容开启。回滚方法是恢复 `observe` 并保留诊断日志;业务引用和人脸记录未持久化临时签名,无需数据库回滚。
|
||||
|
||||
## 后台图片闪烁修复补充
|
||||
|
||||
2026-10-29 修复后台单张图片失败重试清空全局签名缓存的问题。重试只刷新当前文件,同一文件的并发重试合并;普通校时保留仍有效的签名。新增公共 `FileImage`,已加载图片在分钟检查、签名续期和恢复前台时保持当前画面;未加载的懒图片继续更新有效地址,放大预览在打开或切换图片时取有效签名。业务引用改变时正常加载新图。
|
||||
|
||||
- 后台定向回归 19 项通过,包含真实组件 setup、模板与 Vue 生命周期测试;模拟停留超过 30 分钟、恢复前台、失败重试、预览和懒加载。
|
||||
- 新组件与文件工具的定向 `vue-tsc` 检查通过;后台生产构建通过。
|
||||
- 已登录商户页实测:原先已加载的 24 张图片跨分钟检查后地址不变,其余慢图片随后加载完成,共 30 张图片成功、无加载占位;放大预览成功且请求带签名。
|
||||
|
||||
## 浏览器直接打开文件地址限制补充
|
||||
|
||||
2026-10-29 新增默认开启的 `block-browser-navigation`。严格模式下,携带浏览器页面导航 Fetch Metadata 的文件请求,即使签名有效也返回 403;页面图片、音视频、fetch/XHR 下载和无浏览器 Metadata 的原生/服务请求继续按原签名规则处理。成功响应 `Vary` 区分导航与子资源请求;拒绝响应 `no-store`。观察模式仅记录 `browser_navigation`。
|
||||
|
||||
- Java 文件访问/读取请求级测试共 12 项通过,覆盖签名正确的导航、GET/HEAD 在读取存储前拒绝、图片与下载放行、原生包下载、Dart UA 不绕过导航限制、开关和观察模式。
|
||||
- 后台 20 项、H5 16 项、小程序 16 项定向测试通过。覆盖文件链接的页面内下载、H5 富文本与普通链接拦截、图片预览、本地文档打开及第三方链接保持原行为。
|
||||
- 后台生产、H5、微信小程序构建通过;后台文件组件/工具定向类型检查通过。
|
||||
- 配置生成器在临时目录验证:新配置含 `block-browser-navigation: true`,默认仍为 `observe`,没有修改实际运行环境配置。
|
||||
|
||||
这项补充尚未部署到文件服务器,不能把本地请求级测试表述为该公网地址已返回 403。上线时代理须保留 `Sec-Fetch-Mode/Dest` 和响应 `Vary`;用新签名或无旧缓存的窗口验证。后端无法收回浏览器已经取得的文件内容。旧浏览器可能不提供 Fetch Metadata,脚本也能模拟请求,因此这项规则不等于逐用户授权或防止已显示图片被保存。
|
||||
|
||||
## 最终调整:统一一套密钥与严格模式联调
|
||||
|
||||
用户明确要求所有环境使用同一套密钥,取代本文前面记录的开发/测试/生产独立密钥方案。沿用已配置的原生产密钥组,未生成新密钥;各 appId 仍使用各自固定密钥,service/package 密钥仍只存在服务端。三个前端统一使用 `.env.local`,Java 使用 `.file-access/config.yml`,Flutter 使用 `.file-access/config.json`。旧模式文件的签名字段已清理并备份,旧命令 `--environment` 不再切换密钥。
|
||||
|
||||
此前严格模式白图的真实原因是:浏览器按开发密钥签名,后端配置的是原生产密钥,实际响应为 `invalid_signature`。本次已通过本地 Nacos REST API 修正 `application-dev.yml`,保留 `enforce`、浏览器导航拦截和旧 Dart 兼容,公网基础地址对齐测试网关。Nacos 完整原配置保存在本机私有临时备份,发布时使用 MD5 比较避免覆盖并发编辑,生产服务配置未修改。
|
||||
|
||||
- 使用真实页面中的同一个签名 URL 联调:页面图片请求 200、应用下载请求 200、浏览器导航请求 403,原因为 `browser_navigation`。
|
||||
- 实际后台重新加载后,首轮观察为 30 张图片中的 24 张加载成功,其余 6 张仍在慢请求中;随后浏览器自动化连接中断,未把该轮全部图片的最终状态作为已验证结果。签名已与统一密钥注册表一致。
|
||||
- 真实 Vite `loadEnv` 验证三个项目共 18 个模式组合,全部使用统一密钥;生成器迁移、重复执行、原密钥保留与错误配置拒绝共 3 项测试通过。
|
||||
- 后台生产、H5、微信小程序构建通过;Flutter 使用统一配置执行签名和安装包定向测试,8 项通过。
|
||||
|
||||
后续构建与配置准备以 README 中的统一路径为准。本文前面提到的分环境配置路径属于历史验证记录,不再作为当前操作指引。
|
||||
|
||||
## 上传身份与文件白名单补充
|
||||
|
||||
2026-10-29 在现有读取协议和同一套固定密钥上增加上传身份校验。普通、内部、七牛三个文件中心上传入口均接入,读取签名不能用于上传;上传签名绑定 POST、目标入口、appId、kid、时间和一次性 nonce。默认 5 分钟有效,严格模式通过 Redis 原子占用 nonce 防止重放;内部上传仅接受 service 身份,Dart UA 不作为上传凭据。
|
||||
|
||||
文件白名单覆盖图片、PDF、APK、Office、TXT/CSV、ZIP 和常用音视频;仅内部上传额外允许 LOG/GZ。服务层在存储前检查后缀、文件头及适用的容器结构、图片尺寸和静态 SVG 内容,并规范存储扩展名与 MIME。默认最大 200 MiB,SVG 最大 1 MiB,现有网关/Servlet 更低限制仍适用。观察模式只放宽签名准入,文件类型检查始终执行。格式识别不代表病毒或文档宏扫描。
|
||||
|
||||
| 验证 | 本次结果 |
|
||||
| --- | --- |
|
||||
| Java 上传测试 | 17 项通过,覆盖跨语言向量、入口隔离、重复头、过期、Dart UA 不放行、40 个并发请求只有一次 nonce 成功、Redis/错误配置失败关闭、实际 Feign multipart 编码、MockMvc 存储前拒绝及观察模式白名单 |
|
||||
| 文件格式 | 覆盖常见图片、PDF、文本、Office 容器、ZIP、APK;拒绝改后缀/伪造 MIME、路径穿越、恶意 SVG、外部实体及过深 XML;最新构建的实际 Flutter Release APK 通过格式校验 |
|
||||
| Java 相关回归 | 52 项通过,覆盖原文件读取、版本地址、日志导出、钱包导出、硬件导出;UPMS、wallet、hardware、integrated、canteen 及所需公共模块编译通过 |
|
||||
| 后台 | 31 项通过,执行实际 Axios 拦截器、通用上传和富文本上传 setup;保留登录/租户头和 multipart 业务字段,失败文件不阻塞同批成功回填,签名不写进保存的富文本 |
|
||||
| H5、小程序 | 各 21 项通过,执行实际 uni.uploadFile 拦截器;覆盖上传向量、每次请求新 nonce、校时及第三方/导入路径不签名 |
|
||||
| Flutter | 签名、上传、安装包和兼容模式定向回归 21 项通过、2 项条件跳过;本次修改的文件工具、HTTP 管理器和上传测试定向 analyze 无诊断 |
|
||||
| 构建 | 后台生产、H5、微信小程序及 Android Release 均通过;Flutter 使用 FVM 3.27.4 和 `.file-access/config.json`,APK 约 101.9 MB |
|
||||
| 配置生成器 | 4 项通过,确认只有一套环境共用密钥、原密钥不变、默认上传策略补齐、已有定制白名单和大小限制不被覆盖 |
|
||||
| TypeScript | 后台全量仍有原有 6 条解析错误,H5 仍有原有 4 条解析错误,小程序仍有原有 17 条 TS6504;后台上传相关组件及其依赖与 HEAD 基线比较均为 115 条既有诊断,无新增诊断,不能表述为全量类型检查通过 |
|
||||
| 源码与密钥 | 五个项目 Git 候选文件未发现当前活动密钥,保留并发的日志导出、Flutter 调试兼容等改动;Diff 空白检查通过 |
|
||||
|
||||
日志保存在本机 `/tmp/pj-upload-security-work/`。本次上传改造没有发布到运行中的 Java 服务,也没有修改 Nacos 或更换密钥。发布后的真实 multipart 联调、微信真机和旧 Flutter 设备验证仍需执行;MockMvc、内存 Feign 和本地构建不能代替现场验收。
|
||||
|
||||
已有 `enforce` 环境先发布客户端及携带新公共依赖的 Java 调用服务,再发布 UPMS,避免仅更新 UPMS 后拒绝旧上传请求。当前旧 Flutter 取证/日志先经过硬件业务接口,再由 Java 调用文件中心,服务端补签后不要求这些旧设备立即升级。`observe` 会允许旧上传身份请求,切到 `enforce` 才真正拦截未签名上传;白名单在两种模式下均有效。发布配置与详细头协议见 README 的“上传身份与文件类型”。
|
||||
|
||||
## 普通位图后缀兼容补充
|
||||
|
||||
2026-10-29 复现商户 Logo 上传失败:用户提供的 `.png` 文件实际为 RIFF/VP8 WebP,800×440、17,436 字节,独立图片解码验证正常。旧规则按 PNG 文件头判断,因此返回 415。
|
||||
|
||||
现对声明为 JPG/JPEG/PNG/GIF/BMP/WebP 的文件先识别实际位图格式,检查实际格式也在白名单后再执行对应内容校验,保存时使用实际扩展名和 MIME。原图片字节不变;限制白名单仍生效,其他文件类别不通过这个规则自动转为图片。后台单图、多图和七牛图片上传默认加入 WebP,并让失败的 Promise 交给 Element Plus 统一处理,避免失败后继续触发成功提示。
|
||||
|
||||
- Java 32 项定向测试通过,含这张原图、公开合成 WebP、严格白名单、脚本/文档不自动转为图片、三个上传入口的 `.webp` 存储结果、原读取鉴权及安装包回归。
|
||||
- 后台 30 项定向测试通过,含三个图片组件的 WebP 选择、错误提示及原始返回地址保存。
|
||||
- 后台生产构建通过;上传相关组件及其依赖的 TypeScript 诊断由既有 115 条减为 114 条,去掉类型展开差异后无新增诊断。
|
||||
- 实际图只通过本机可选测试参数读取,没有作为仓库测试素材提交。验证日志在 `/tmp/pj-upload-image-format-work/`。
|
||||
|
||||
后端识别逻辑需要发布或重启更新后的 UPMS 才会生效;本次未更改运行环境、密钥或 Nacos。
|
||||
|
||||
## 旧 mobile H5 图片免签来源补充
|
||||
|
||||
2026-10-29 按用户要求,仅修改后端,为不能更新的 `https://www.pjzhhq.cn/mobile/` H5 提供图片兼容。新增 `file-access.public-image-referer-prefixes`,默认空列表。请求 Referer 的协议、主机、有效端口和路径前缀全部匹配时,GET/HEAD 常见图片可免签,包括没有 appId 的旧请求;匹配来源携带的旧签名不再影响图片读取。Hash 路由不参与匹配。
|
||||
|
||||
- 配置整体省略 `clients.h5` 可通过新版本校验;配置了空 H5 密钥仍拒绝启动,其他五类必要客户端维持严格要求。仅移除 H5 密钥不会自动放行任意 H5 请求。
|
||||
- 重复参数、非法文件名、浏览器页面导航仍先拒绝;其他来源、PDF/APK/日志/压缩包和上传入口不走该例外。
|
||||
- 已覆盖域名后缀伪造、userinfo、错误协议/端口、相似路径、路径穿越、重复 Referer、开关关闭、GET/HEAD、私有缓存和 `Vary: Referer`。
|
||||
- Java 跨模块 78 项回归通过,0 失败/跳过;配置生成器 4 项通过,确认默认不开启、已有前缀不被重写。
|
||||
- 实际检查线上 `/mobile/` 返回 200,未发现禁用 Referer 的响应头或 HTML meta;这不代替后端部署后的真机验收。
|
||||
|
||||
精确前缀已加入本机被忽略的生成配置,其余配置和密钥保留。生产 Nacos 和线上服务未修改;需要部署新 UPMS/公共依赖并加载来源白名单,H5 无需重新发布。Referer 可被脚本模拟,此功能为应用级来源兼容。
|
||||
|
||||
## 政务 H5 根来源兼容补充
|
||||
|
||||
2026-10-29 实际检查政务 H5 的图片请求:页面位于 `https://yyfb.idw.jinhua.gov.cn:6443/web/mgop/gov-open/zj/2002546649/reserved/index.html#/`,但浏览器使用 `strict-origin-when-cross-origin`,发往文件中心的 Referer 仅为 `https://yyfb.idw.jinhua.gov.cn:6443/`。因此原目录前缀无法命中。
|
||||
|
||||
现有 `public-image-referer-prefixes` 同时支持目录前缀和显式根来源;根来源末尾 `/` 可省略,允许同协议、同主机、同有效端口下所有页面的图片请求。目录配置仍严格匹配目录,不会因 Referer 省略路径而自动扩大范围。仅允许 GET/HEAD 图片、阻止浏览器导航、上传鉴权、私有缓存和 Vary 规则保持不变。
|
||||
|
||||
- 执行 `mvn -pl pjcloud-upms/pjcloud-upms-biz -am test -Dtest=FileAccessTest,FileAccessMobileTest,FileUploadGuardTest -Dsurefire.failIfNoSpecifiedTests=false`:23 项通过,0 失败、错误或跳过,Maven 构建成功。
|
||||
- 新增测试覆盖实际根 Referer、完整页面 Referer、无末尾斜杠的配置、默认/自定义端口、域名伪造、错误协议、重复/缺失 Referer、目录范围不扩大、关闭白名单、导航及非图片拒绝、上传仍需鉴权、GET/HEAD 请求到存储的边界。
|
||||
- 本机验证日志为 `/tmp/pj-file-access-origin-tests.log`。未修改线上配置或发布服务,线上恢复情况需在新版 UPMS 和对应公共依赖发布后确认。若根来源配置共享给其他启用文件配置的服务,也需先更新这些服务的公共依赖。
|
||||
|
||||
## 发票盖章内部读取修复
|
||||
|
||||
2026-10-30 反馈 `WalletRechargeInvoiceServiceImpl.stamp` 访问 `http://127.0.0.1:9999/admin/sys-file/oss/file?fileName=...` 返回 403。原实现使用 `FileAccessSigner.signUrl`,不在公网来源配置中的主机不会被签名,随后 URLConnection 发出的裸请求在严格模式被拒绝。
|
||||
|
||||
盖章现在使用 `FileAccessUrl.fileName` 解析文件引用,再由 `RemoteFileService.getFile` 通过服务发现读取 UPMS,并在请求时生成 `service` 签名。文件中心读取不再依赖 `IMAGE_BASE_URL` 或原链接的主机;文件访问入口及其鉴权规则未修改。
|
||||
|
||||
- 执行 `mvn -pl pjcloud-wallet/pjcloud-wallet-biz,pjcloud-upms/pjcloud-upms-biz -am test -Dtest=WalletRechargeInvoiceFileAccessTest,FileAccessUrlTest,FileAccessTest,FileAccessMobileTest,FileUploadGuardTest -Dsurefire.failIfNoSpecifiedTests=false`:31 项通过,0 失败、错误或跳过,Maven 构建成功。
|
||||
- 新回归覆盖本机网关、历史域名及网关前缀、相对路径、原始文件名、旧签名替换、中文/加号/百分号编码、重复与非法文件名、读取失败不上传、源流关闭、PNG 盖章合成,以及第三方图片请求保持原样。
|
||||
- 本机验证日志为 `/tmp/pj-internal-image-access-tests.log`。测试中的 Feign 文件服务为替身,未访问真实注册中心或存储;未修改 Nacos、密钥或运行中的服务。
|
||||
|
||||
需重新构建并发布 Wallet,包含更新后的 common-core;确认 Wallet 与 UPMS 的 `clients.service` 密钥一致,再在实际环境重新生成发票验收。
|
||||
@@ -0,0 +1,7 @@
|
||||
MYSQL_HOST=127.0.0.1
|
||||
MYSQL_PORT=33905
|
||||
MYSQL_USER=root@sys
|
||||
MYSQL_PWD=bsd.1799
|
||||
|
||||
NACOS_HOST=127.0.0.1
|
||||
NACOS_PORT=8848
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.bsd.pjcloud.app.api.constant;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* app用户类型
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum AppUserTypeEnum {
|
||||
|
||||
MINI_WECHAT("10", "微信小程序用户"),
|
||||
MINI_ALIPAY("20", "支付宝小程序用户"),
|
||||
;
|
||||
private final String type;
|
||||
private final String description;
|
||||
|
||||
public static AppUserTypeEnum valueOfByType(String type) {
|
||||
if (ObjectUtil.isEmpty(type)) return null;
|
||||
return Arrays.stream(values()).filter(v -> ObjectUtil.equals(type, v.getType())).findFirst().get();
|
||||
}
|
||||
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.bsd.pjcloud.app.api.constant.menu;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* app菜单平台
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum AppMenuPlatformEnum {
|
||||
|
||||
WECHAT("10", "微信小程序"),
|
||||
ZZD("30", "浙政钉"),
|
||||
SHPJ("40", "诗画浦江"),
|
||||
;
|
||||
|
||||
private final String type;
|
||||
private final String description;
|
||||
|
||||
public static AppMenuPlatformEnum valueOfByType(String type) {
|
||||
if (ObjectUtil.isEmpty(type)) return null;
|
||||
return Arrays.stream(values()).filter(v -> ObjectUtil.equals(type, v.getType())).findFirst().get();
|
||||
}
|
||||
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.bsd.pjcloud.app.api.constant.menu;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* app菜单类型
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum AppMenuTypeEnum {
|
||||
DASHBOARD_TAB("10", "菜单面板tab"),
|
||||
;
|
||||
|
||||
private final String type;
|
||||
private final String description;
|
||||
|
||||
public static AppMenuTypeEnum valueOfByType(String type) {
|
||||
if (ObjectUtil.isEmpty(type)) return null;
|
||||
return Arrays.stream(values()).filter(v -> ObjectUtil.equals(type, v.getType())).findFirst().get();
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.bsd.pjcloud.app.api.dto;
|
||||
|
||||
import com.bsd.pjcloud.app.api.constant.AppUserTypeEnum;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AppAdminSyncDto {
|
||||
|
||||
private String uniqueCode;
|
||||
|
||||
private AppUserTypeEnum userTypeEnum;
|
||||
|
||||
private Long adminUserId;
|
||||
|
||||
private String username;
|
||||
|
||||
private String avatarImage;
|
||||
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.bsd.pjcloud.app.api.dto;
|
||||
|
||||
import com.bsd.pjcloud.app.api.constant.menu.AppMenuPlatformEnum;
|
||||
import com.bsd.pjcloud.app.api.constant.menu.AppMenuTypeEnum;
|
||||
import com.bsd.pjcloud.app.api.entity.AppMenuEntity;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AppMenuDto extends AppMenuEntity {
|
||||
|
||||
/**
|
||||
* 平台(小程序、支付宝)
|
||||
*/
|
||||
private AppMenuPlatformEnum platformEnum;
|
||||
|
||||
/**
|
||||
* 菜单类型
|
||||
*/
|
||||
private AppMenuTypeEnum typeEnum;
|
||||
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package com.bsd.pjcloud.app.api.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.baomidou.mybatisplus.extension.activerecord.Model;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import com.bsd.pjcloud.common.core.util.TenantTable;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 菜单管理
|
||||
*/
|
||||
@Data
|
||||
@TenantTable
|
||||
@TableName("app_menu")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "菜单管理")
|
||||
public class AppMenuEntity extends Model<AppMenuEntity> {
|
||||
|
||||
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "id")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 平台(小程序、支付宝)
|
||||
*/
|
||||
@Schema(description = "平台(小程序、支付宝)")
|
||||
private String platform;
|
||||
|
||||
/**
|
||||
* 菜单类型
|
||||
*/
|
||||
@Schema(description = "菜单类型")
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 图标
|
||||
*/
|
||||
@Schema(description = "图标")
|
||||
private String icon;
|
||||
|
||||
/**
|
||||
* 页面路径(包含参数)
|
||||
*/
|
||||
@Schema(description = "页面路径(包含参数)")
|
||||
private String page;
|
||||
|
||||
/**
|
||||
* 页面标题
|
||||
*/
|
||||
@Schema(description = "页面标题")
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 是否显示
|
||||
*/
|
||||
@Schema(description = "是否显示")
|
||||
private String visible;
|
||||
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
@Schema(description = "排序")
|
||||
private Integer sort;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Schema(description = "创建人")
|
||||
private String createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 修改人
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
@Schema(description = "修改人")
|
||||
private String updateBy;
|
||||
|
||||
/**
|
||||
* 修改时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
@Schema(description = "修改时间")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/**
|
||||
* 删除标志
|
||||
*/
|
||||
@TableLogic
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Schema(description = "删除标志")
|
||||
private String delFlag;
|
||||
|
||||
/**
|
||||
* 所属租户id
|
||||
*/
|
||||
@Schema(description = "所属租户id")
|
||||
private Long tenantId;
|
||||
}
|
||||
+111
-99
@@ -40,122 +40,134 @@ import java.time.LocalDateTime;
|
||||
@Schema(description = "app用户表")
|
||||
public class AppUser extends Model<AppUser> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* userId
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "userId")
|
||||
private Long userId;
|
||||
/**
|
||||
* userId
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "userId")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@Schema(description = "用户名")
|
||||
private String username;
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
@Schema(description = "用户类型")
|
||||
private String userType;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@Schema(description = "密码")
|
||||
private String password;
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@Schema(description = "用户名")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 随机盐
|
||||
*/
|
||||
@JsonIgnore
|
||||
@Schema(description = "随机盐")
|
||||
private String salt;
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@Schema(description = "密码")
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@Schema(description = "手机号")
|
||||
private String phone;
|
||||
/**
|
||||
* 随机盐
|
||||
*/
|
||||
@JsonIgnore
|
||||
@Schema(description = "随机盐")
|
||||
private String salt;
|
||||
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
@Schema(description = "头像")
|
||||
private String avatar;
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@Schema(description = "手机号")
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
@Schema(description = "拓展字段:昵称")
|
||||
private String nickname;
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
@Schema(description = "头像")
|
||||
private String avatar;
|
||||
|
||||
/**
|
||||
* 姓名
|
||||
*/
|
||||
@Schema(description = "拓展字段:姓名")
|
||||
private String name;
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
@Schema(description = "拓展字段:昵称")
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
@Schema(description = "拓展字段:邮箱")
|
||||
private String email;
|
||||
/**
|
||||
* 姓名
|
||||
*/
|
||||
@Schema(description = "拓展字段:姓名")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
@Schema(description = "创建人")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
@Schema(description = "拓展字段:邮箱")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 修改人
|
||||
*/
|
||||
@Schema(description = "修改人")
|
||||
@TableField(fill = FieldFill.UPDATE)
|
||||
private String updateBy;
|
||||
/**
|
||||
* 第三方登录扩展字段
|
||||
*/
|
||||
@Schema(description = "第三方登录扩展字段")
|
||||
private String loginExtJson;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Schema(description = "创建时间")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
/**
|
||||
* openId
|
||||
*/
|
||||
@Schema(description = "openId")
|
||||
private String openid;
|
||||
|
||||
/**
|
||||
* 修改时间
|
||||
*/
|
||||
@Schema(description = "修改时间")
|
||||
@TableField(fill = FieldFill.UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
@Schema(description = "创建人")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
/**
|
||||
* 删除标记
|
||||
*/
|
||||
@Schema(description = "删除标记,1:已删除,0:正常")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@TableLogic
|
||||
private String delFlag;
|
||||
/**
|
||||
* 修改人
|
||||
*/
|
||||
@Schema(description = "修改人")
|
||||
@TableField(fill = FieldFill.UPDATE)
|
||||
private String updateBy;
|
||||
|
||||
/**
|
||||
* 所属租户
|
||||
*/
|
||||
@Schema(description = "所属租户", hidden = true)
|
||||
private Long tenantId;
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Schema(description = "创建时间")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 最后一次密码修改时间
|
||||
*/
|
||||
@Schema(description = "最后一次密码修改时间")
|
||||
private LocalDateTime lastModifiedTime;
|
||||
/**
|
||||
* 修改时间
|
||||
*/
|
||||
@Schema(description = "修改时间")
|
||||
@TableField(fill = FieldFill.UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/**
|
||||
* 锁定标记
|
||||
*/
|
||||
@Schema(description = "锁定标记")
|
||||
private String lockFlag;
|
||||
/**
|
||||
* 删除标记
|
||||
*/
|
||||
@Schema(description = "删除标记,1:已删除,0:正常")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@TableLogic
|
||||
private String delFlag;
|
||||
|
||||
/**
|
||||
* 微信小程序登录openId
|
||||
*/
|
||||
@Schema(description = "微信小程序登录openId")
|
||||
private String wxOpenid;
|
||||
/**
|
||||
* 所属租户
|
||||
*/
|
||||
@Schema(description = "所属租户", hidden = true)
|
||||
private Long tenantId;
|
||||
|
||||
/**
|
||||
* 最后一次密码修改时间
|
||||
*/
|
||||
@Schema(description = "最后一次密码修改时间")
|
||||
private LocalDateTime lastModifiedTime;
|
||||
|
||||
/**
|
||||
* 锁定标记
|
||||
*/
|
||||
@Schema(description = "锁定标记")
|
||||
private String lockFlag;
|
||||
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
*
|
||||
* Copyright (c) 2018-2025, pjcloud All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: pjcloud
|
||||
*
|
||||
*/
|
||||
|
||||
package com.bsd.pjcloud.app.api.feign;
|
||||
|
||||
import com.bsd.pjcloud.app.api.constant.menu.AppMenuPlatformEnum;
|
||||
import com.bsd.pjcloud.app.api.constant.menu.AppMenuTypeEnum;
|
||||
import com.bsd.pjcloud.app.api.entity.AppMenuEntity;
|
||||
import com.bsd.pjcloud.common.core.constant.ServiceNameConstants;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.feign.annotation.NoToken;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@FeignClient(contextId = "remoteAppMenuService", value = ServiceNameConstants.APP_SERVER)
|
||||
public interface RemoteAppMenuService {
|
||||
|
||||
/**
|
||||
* 与 AppMenuController GET /app-menu/app/list 一致,内部调用
|
||||
*/
|
||||
@NoToken
|
||||
@GetMapping("/app-menu/inner/app/list")
|
||||
R<List<AppMenuEntity>> innerAppList(
|
||||
@RequestParam(value = "typeEnum", required = false) AppMenuTypeEnum typeEnum,
|
||||
@RequestParam(value = "platformEnum", required = false) AppMenuPlatformEnum platformEnum);
|
||||
}
|
||||
+25
-22
@@ -19,6 +19,7 @@
|
||||
|
||||
package com.bsd.pjcloud.app.api.feign;
|
||||
|
||||
import com.bsd.pjcloud.app.api.dto.AppAdminSyncDto;
|
||||
import com.bsd.pjcloud.app.api.dto.AppUserInfo;
|
||||
import com.bsd.pjcloud.common.core.constant.ServiceNameConstants;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
@@ -26,33 +27,35 @@ import com.bsd.pjcloud.common.feign.annotation.NoToken;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
|
||||
/**
|
||||
* @author pjcloud
|
||||
* @date 2018/6/22
|
||||
*/
|
||||
@FeignClient(contextId = "remoteAppUserService", value = ServiceNameConstants.APP_SERVER)
|
||||
public interface RemoteAppUserService {
|
||||
|
||||
/**
|
||||
* 通过用户名查询用户、角色信息
|
||||
* @param username 用户名
|
||||
* @param from 调用标志
|
||||
* @return R
|
||||
*/
|
||||
@NoToken
|
||||
@GetMapping("/appuser/info/{username}")
|
||||
R<AppUserInfo> info(@PathVariable("username") String username);
|
||||
/**
|
||||
* 通过用户名查询用户、角色信息
|
||||
*/
|
||||
@NoToken
|
||||
@GetMapping("/appuser/info/{username}")
|
||||
R<AppUserInfo> info(@PathVariable("username") String username);
|
||||
|
||||
/**
|
||||
* 通过社交账号或手机号查询用户、角色信息
|
||||
* @param inStr appid@code
|
||||
* @param from 调用标志
|
||||
* @return
|
||||
*/
|
||||
@NoToken
|
||||
@GetMapping("/appsocial/info/{inStr}")
|
||||
R<AppUserInfo> social(@PathVariable("inStr") String inStr);
|
||||
@NoToken
|
||||
@GetMapping("/appuser/infoByUserId/{userId}")
|
||||
R<AppUserInfo> infoByUserId(@PathVariable("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 通过社交账号或手机号查询用户、角色信息
|
||||
*/
|
||||
@NoToken
|
||||
@GetMapping("/appsocial/info/{inStr}")
|
||||
R<AppUserInfo> social(@PathVariable("inStr") String inStr);
|
||||
|
||||
/**
|
||||
* 同步admin账户信息为app账户
|
||||
*/
|
||||
@NoToken
|
||||
@PostMapping("/appsocial/sync-admin-account-info")
|
||||
R<AppUserInfo> syncAdminAccountInfo(@RequestBody AppAdminSyncDto appAdminSyncDto);
|
||||
}
|
||||
+1
@@ -1 +1,2 @@
|
||||
com.bsd.pjcloud.app.api.feign.RemoteAppUserService
|
||||
com.bsd.pjcloud.app.api.feign.RemoteAppMenuService
|
||||
@@ -104,12 +104,23 @@
|
||||
<groupId>com.bsd</groupId>
|
||||
<artifactId>pjcloud-common-swagger</artifactId>
|
||||
</dependency>
|
||||
<!-- 引入分布式事务-->
|
||||
<!-- 引入分布式事务 -->
|
||||
<dependency>
|
||||
<groupId>com.bsd</groupId>
|
||||
<artifactId>pjcloud-common-seata</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 支付宝SDK -->
|
||||
<dependency>
|
||||
<groupId>com.alipay.sdk</groupId>
|
||||
<artifactId>alipay-sdk-java</artifactId>
|
||||
<version>4.40.251.ALL</version>
|
||||
</dependency>
|
||||
<!-- 微信登录(小程序) -->
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
|
||||
<version>4.7.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<profiles>
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.bsd.pjcloud.app.config;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
|
||||
import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(MiniWechatConf.class)
|
||||
public class MinWechatConfiguration {
|
||||
|
||||
private final MiniWechatConf properties;
|
||||
|
||||
@Autowired
|
||||
public MinWechatConfiguration(MiniWechatConf properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
public WxMaService wxMaService() {
|
||||
|
||||
WxMaDefaultConfigImpl wxMaDefaultConfig = new WxMaDefaultConfigImpl();
|
||||
wxMaDefaultConfig.setAppid(properties.getAppId());
|
||||
wxMaDefaultConfig.setSecret(properties.getAppSecret());
|
||||
wxMaDefaultConfig.setMsgDataFormat("JSON");
|
||||
|
||||
WxMaService maService = new WxMaServiceImpl();
|
||||
maService.setWxMaConfig(wxMaDefaultConfig);
|
||||
return maService;
|
||||
}
|
||||
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.bsd.pjcloud.app.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 支付宝支付配置
|
||||
*/
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "mini-ali-pay")
|
||||
@Data
|
||||
public class MiniAlipayConf {
|
||||
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
private String appId;
|
||||
|
||||
/**
|
||||
* 私钥
|
||||
*/
|
||||
private String privateKey;
|
||||
|
||||
/**
|
||||
* 公钥
|
||||
*/
|
||||
private String publicKey;
|
||||
|
||||
/**
|
||||
* 支付宝公钥
|
||||
*/
|
||||
private String alipayPublicKey;
|
||||
|
||||
/**
|
||||
* 回调地址
|
||||
*/
|
||||
private String callbackUrl;
|
||||
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.bsd.pjcloud.app.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* 微信小程序配置
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "mini-wechat-pay")
|
||||
public class MiniWechatConf {
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
private String appId;
|
||||
|
||||
private String appSecret;
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
package com.bsd.pjcloud.app.controller;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.app.api.constant.menu.AppMenuPlatformEnum;
|
||||
import com.bsd.pjcloud.app.api.constant.menu.AppMenuTypeEnum;
|
||||
import com.bsd.pjcloud.app.api.entity.AppMenuEntity;
|
||||
import com.bsd.pjcloud.common.core.constant.enums.YesNoEnum;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.log.annotation.SysLog;
|
||||
import com.bsd.pjcloud.common.security.annotation.Inner;
|
||||
import com.bsd.pjcloud.app.service.AppMenuService;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import com.bsd.pjcloud.common.excel.annotation.ResponseExcel;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单管理
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/app-menu")
|
||||
@Tag(description = "app-menu", name = "菜单管理管理")
|
||||
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
|
||||
public class AppMenuController {
|
||||
|
||||
private final AppMenuService appMenuService;
|
||||
|
||||
/* ************************************* start app start ************************************* */
|
||||
|
||||
/**
|
||||
* App 端菜单列表(显式接收 enum,与 Feign 一致,避免 ParameterObject 继承 Entity 绑参歧义)
|
||||
*/
|
||||
@Operation(summary = "App菜单列表", description = "按平台/类型查询可见菜单")
|
||||
@GetMapping("/app/list")
|
||||
public R<List<AppMenuEntity>> appList(
|
||||
@RequestParam(value = "typeEnum", required = false) AppMenuTypeEnum typeEnum,
|
||||
@RequestParam(value = "platformEnum", required = false) AppMenuPlatformEnum platformEnum) {
|
||||
return listVisibleAppMenus(typeEnum, platformEnum);
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务间调用:与 {@link #appList} 数据一致,需 Feign 携带内部调用头(见 {@code @NoToken} 与 {@link Inner})。
|
||||
*/
|
||||
@Inner(value = true)
|
||||
@Operation(summary = "内部-App菜单列表", description = "供 integrated 等经 Feign 拉取,与 /app-menu/app/list 一致")
|
||||
@GetMapping("/inner/app/list")
|
||||
public R<List<AppMenuEntity>> innerAppList(
|
||||
@RequestParam(value = "typeEnum", required = false) AppMenuTypeEnum typeEnum,
|
||||
@RequestParam(value = "platformEnum", required = false) AppMenuPlatformEnum platformEnum) {
|
||||
return listVisibleAppMenus(typeEnum, platformEnum);
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅按 platform/type/visible 查询,使用干净 Entity,避免 DTO 脏字段污染条件。
|
||||
*/
|
||||
private R<List<AppMenuEntity>> listVisibleAppMenus(AppMenuTypeEnum typeEnum, AppMenuPlatformEnum platformEnum) {
|
||||
AppMenuEntity query = new AppMenuEntity();
|
||||
query.setVisible(YesNoEnum.YES.getCode());
|
||||
if (ObjectUtil.isNotEmpty(platformEnum)) {
|
||||
query.setPlatform(platformEnum.getType());
|
||||
}
|
||||
if (ObjectUtil.isNotEmpty(typeEnum)) {
|
||||
query.setType(typeEnum.getType());
|
||||
}
|
||||
List<AppMenuEntity> list = appMenuService.scopeList(query, null);
|
||||
log.info("[app-menu/list] platformEnum={} platform={} typeEnum={} type={} size={}",
|
||||
platformEnum,
|
||||
query.getPlatform(),
|
||||
typeEnum,
|
||||
query.getType(),
|
||||
list == null ? 0 : list.size());
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
/* ************************************* end app end ************************************* */
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
*/
|
||||
@Operation(summary = "分页查询", description = "分页查询")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@pms.hasPermission('app_appMenu_view')")
|
||||
public R getAppMenuPage(@ParameterObject Page page, @ParameterObject AppMenuEntity dto) {
|
||||
return R.ok(appMenuService.scopePage(page, dto, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询菜单管理
|
||||
*/
|
||||
@Operation(summary = "通过id查询", description = "通过id查询")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@pms.hasPermission('app_appMenu_view')")
|
||||
public R getById(@PathVariable("id") Long id) {
|
||||
return R.ok(appMenuService.getById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增菜单管理
|
||||
*/
|
||||
@Operation(summary = "新增菜单管理", description = "新增菜单管理")
|
||||
@SysLog("新增菜单管理")
|
||||
@PostMapping
|
||||
@PreAuthorize("@pms.hasPermission('app_appMenu_add')")
|
||||
public R save(@RequestBody AppMenuEntity appMenu) {
|
||||
return R.ok(appMenuService.save(appMenu));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改菜单管理
|
||||
*/
|
||||
@Operation(summary = "修改菜单管理", description = "修改菜单管理")
|
||||
@SysLog("修改菜单管理")
|
||||
@PutMapping
|
||||
@PreAuthorize("@pms.hasPermission('app_appMenu_edit')")
|
||||
public R updateById(@RequestBody AppMenuEntity appMenu) {
|
||||
return R.ok(appMenuService.updateById(appMenu));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id删除菜单管理
|
||||
*/
|
||||
@Operation(summary = "通过id删除菜单管理", description = "通过id删除菜单管理")
|
||||
@SysLog("通过id删除菜单管理")
|
||||
@DeleteMapping
|
||||
@PreAuthorize("@pms.hasPermission('app_appMenu_del')")
|
||||
public R removeById(@RequestBody Long[] ids) {
|
||||
return R.ok(appMenuService.removeBatchByIds(CollUtil.toList(ids)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel 表格
|
||||
*/
|
||||
@ResponseExcel
|
||||
@GetMapping("/export")
|
||||
@PreAuthorize("@pms.hasPermission('app_appMenu_export')")
|
||||
public List<AppMenuEntity> export(AppMenuEntity appMenu, Long[] ids) {
|
||||
return appMenuService.list(Wrappers.lambdaQuery(appMenu).in(ArrayUtil.isNotEmpty(ids), AppMenuEntity::getId, ids));
|
||||
}
|
||||
|
||||
}
|
||||
+84
-82
@@ -20,6 +20,8 @@ package com.bsd.pjcloud.app.controller;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.app.api.dto.AppAdminSyncDto;
|
||||
import com.bsd.pjcloud.app.api.dto.AppUserInfo;
|
||||
import com.bsd.pjcloud.app.api.entity.AppSocialDetails;
|
||||
import com.bsd.pjcloud.app.service.AppSocialDetailsService;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
@@ -28,6 +30,8 @@ import com.bsd.pjcloud.common.excel.annotation.ResponseExcel;
|
||||
import com.bsd.pjcloud.common.log.annotation.SysLog;
|
||||
import com.bsd.pjcloud.common.security.annotation.HasPermission;
|
||||
import com.bsd.pjcloud.common.security.annotation.Inner;
|
||||
import com.bsd.pjcloud.common.security.service.PjcloudUser;
|
||||
import com.bsd.pjcloud.common.security.util.SecurityUtils;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
@@ -45,95 +49,93 @@ import java.util.List;
|
||||
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
|
||||
public class AppSocialDetailsController {
|
||||
|
||||
private final AppSocialDetailsService appSocialDetailsService;
|
||||
private final AppSocialDetailsService appSocialDetailsService;
|
||||
|
||||
/**
|
||||
* 社交登录账户简单分页查询
|
||||
* @param page 分页对象
|
||||
* @param appSocialDetails 社交登录
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/page")
|
||||
public R getSocialDetailsPage(Page page, AppSocialDetails appSocialDetails) {
|
||||
return R.ok(appSocialDetailsService.page(page, Wrappers.query(appSocialDetails)));
|
||||
}
|
||||
/* ************************************* start 设备端 start ************************************* */
|
||||
|
||||
/**
|
||||
* 信息
|
||||
* @param id id
|
||||
* @return R
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public R getinfo(@PathVariable("id") Long id) {
|
||||
return R.ok(appSocialDetailsService.getById(id));
|
||||
}
|
||||
/**
|
||||
* 同步admin账户信息为app账户
|
||||
*/
|
||||
@Inner(value = true)
|
||||
@PostMapping("/sync-admin-account-info")
|
||||
public R<AppUserInfo> syncAdminAccountInfo(@RequestBody AppAdminSyncDto dto) {
|
||||
return R.ok(appSocialDetailsService.syncAdminAccountInfo(dto));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存
|
||||
* @param appSocialDetails
|
||||
* @return R
|
||||
*/
|
||||
@SysLog("保存三方信息")
|
||||
@PostMapping
|
||||
@HasPermission("app_social_details_add")
|
||||
public R save(@Valid @RequestBody AppSocialDetails appSocialDetails) {
|
||||
return R.ok(appSocialDetailsService.save(appSocialDetails));
|
||||
}
|
||||
/* ************************************* end 设备端 end ************************************* */
|
||||
|
||||
/**
|
||||
* 修改
|
||||
* @param appSocialDetails
|
||||
* @return R
|
||||
*/
|
||||
@SysLog("修改三方信息")
|
||||
@PutMapping
|
||||
@HasPermission("app_social_details_edit")
|
||||
public R updateById(@Validated({ ValidGroup.Update.class }) @RequestBody AppSocialDetails appSocialDetails) {
|
||||
appSocialDetailsService.updateById(appSocialDetails);
|
||||
return R.ok(Boolean.TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
* @param ids
|
||||
* @return R
|
||||
*/
|
||||
@SysLog("删除三方信息")
|
||||
@DeleteMapping
|
||||
@HasPermission("app_social_details_del")
|
||||
public R removeById(@RequestBody Long[] ids) {
|
||||
return R.ok(appSocialDetailsService.removeBatchByIds(CollUtil.toList(ids)));
|
||||
}
|
||||
/**
|
||||
* 社交登录账户简单分页查询
|
||||
*/
|
||||
@GetMapping("/page")
|
||||
public R getSocialDetailsPage(Page page, AppSocialDetails appSocialDetails) {
|
||||
return R.ok(appSocialDetailsService.page(page, Wrappers.query(appSocialDetails)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过社交账号、手机号查询用户、角色信息
|
||||
* @param inStr appid@code
|
||||
* @return
|
||||
*/
|
||||
@Inner
|
||||
@GetMapping("/info/{inStr}")
|
||||
public R getUserInfo(@PathVariable String inStr) {
|
||||
return R.ok(appSocialDetailsService.getUserInfo(inStr));
|
||||
}
|
||||
/**
|
||||
* 信息
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public R getinfo(@PathVariable("id") Long id) {
|
||||
return R.ok(appSocialDetailsService.getById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定社交账号
|
||||
* @param state 类型
|
||||
* @param code code
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/bind")
|
||||
public R bindSocial(String state, String code) {
|
||||
return R.ok(appSocialDetailsService.bindSocial(state, code));
|
||||
}
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
@SysLog("保存三方信息")
|
||||
@PostMapping
|
||||
@HasPermission("app_social_details_add")
|
||||
public R save(@Valid @RequestBody AppSocialDetails appSocialDetails) {
|
||||
return R.ok(appSocialDetailsService.save(appSocialDetails));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出
|
||||
*/
|
||||
@GetMapping("/export")
|
||||
@ResponseExcel
|
||||
public List<AppSocialDetails> export(AppSocialDetails appSocialDetails) {
|
||||
return appSocialDetailsService.list(Wrappers.query(appSocialDetails));
|
||||
}
|
||||
/**
|
||||
* 修改
|
||||
*/
|
||||
@SysLog("修改三方信息")
|
||||
@PutMapping
|
||||
@HasPermission("app_social_details_edit")
|
||||
public R updateById(@Validated({ValidGroup.Update.class}) @RequestBody AppSocialDetails appSocialDetails) {
|
||||
appSocialDetailsService.updateById(appSocialDetails);
|
||||
return R.ok(Boolean.TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
@SysLog("删除三方信息")
|
||||
@DeleteMapping
|
||||
@HasPermission("app_social_details_del")
|
||||
public R removeById(@RequestBody Long[] ids) {
|
||||
return R.ok(appSocialDetailsService.removeBatchByIds(CollUtil.toList(ids)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过社交账号、手机号查询用户、角色信息
|
||||
*/
|
||||
@Inner
|
||||
@GetMapping("/info/{inStr}")
|
||||
public R getUserInfo(@PathVariable String inStr) {
|
||||
return R.ok(appSocialDetailsService.getUserInfo(inStr));
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定社交账号
|
||||
*/
|
||||
@PostMapping("/bind")
|
||||
public R bindSocial(String state, String code) {
|
||||
return R.ok(appSocialDetailsService.bindSocial(state, code));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出
|
||||
*/
|
||||
@GetMapping("/export")
|
||||
@ResponseExcel
|
||||
public List<AppSocialDetails> export(AppSocialDetails appSocialDetails) {
|
||||
return appSocialDetailsService.list(Wrappers.query(appSocialDetails));
|
||||
}
|
||||
|
||||
}
|
||||
+16
-107
@@ -24,6 +24,7 @@ import com.bsd.pjcloud.admin.api.dto.UserTokenDTO;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysUser;
|
||||
import com.bsd.pjcloud.admin.api.feign.RemoteTokenService;
|
||||
import com.bsd.pjcloud.admin.api.feign.RemoteUserService;
|
||||
import com.bsd.pjcloud.app.api.dto.AppUserDTO;
|
||||
import com.bsd.pjcloud.app.service.AppUserService;
|
||||
import com.bsd.pjcloud.canteen.api.feign.RemoteCanteenSpuService;
|
||||
import com.bsd.pjcloud.canteen.api.feign.RemoteDailyMenuService;
|
||||
@@ -33,13 +34,13 @@ import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity;
|
||||
import com.bsd.pjcloud.canteen.vo.app.CanteenOnlineSpuAppVO;
|
||||
import com.bsd.pjcloud.canteen.vo.app.DailyMenuAppVO;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.log.annotation.SysLog;
|
||||
import com.bsd.pjcloud.common.security.annotation.Inner;
|
||||
import com.bsd.pjcloud.thirdparty.api.feign.RemoterThirdPartyAuthService;
|
||||
import com.bsd.pjcloud.thirdparty.dto.SHJNSSODTO;
|
||||
import com.bsd.pjcloud.thirdparty.dto.UrlTokenDTO;
|
||||
import com.bsd.pjcloud.thirdparty.dto.ZZDUserDTO;
|
||||
import com.bsd.pjcloud.thirdparty.entity.ThirdPartyUserEntity;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -88,111 +89,18 @@ public class AppUserAutoLoginController {
|
||||
private final RemoteOnlineCanteenSpuService remoteOnlineCanteenSpuService;
|
||||
private final RemoteDailyMenuService remoteDailyMenuService;
|
||||
|
||||
/*
|
||||
*
|
||||
* @param token 浙政钉前端获取的token
|
||||
* @return
|
||||
/**
|
||||
* 路径参数历史命名为 token,实为浙政钉 H5 {@code authCode},转发 thirdParty 统一单点(含短信核验等逻辑)。
|
||||
*/
|
||||
@GetMapping("/zzdsso/{token}")
|
||||
@Operation(summary = "浙政钉单点登录")
|
||||
@Inner(value = false)
|
||||
@GlobalTransactional
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
//todo 引入seata分布式事务
|
||||
public R zzdsso(@PathVariable(name = "token") String token) {
|
||||
|
||||
//获取数字政务token通过浙政钉前端获取的token,调试阶段先写死
|
||||
String json = "{" +
|
||||
"\"username\": \"李宗相\"," +
|
||||
"\"organizationId\": 2," +
|
||||
"\"userId\": 1340," +
|
||||
"\"nickName\": \"李宗相\"," +
|
||||
"\"mobile\": \"18576496089\"," +
|
||||
"\"regionCode\": null," +
|
||||
"\"ext\": {" +
|
||||
"\"employeeCode\": \"GE_ae12bdf21598498eb48bf2cf2c45a3e1\"," +
|
||||
"\"organizationCode\": \"GO_63e77077d5e041789bbbe1d0d20bcf41\"," +
|
||||
"\"nickNameCn\": \"李宗相\"," +
|
||||
"\"openid\": \"1b51f0e11fc73b8adddcbaf7982daab6\"," +
|
||||
"\"realmId\": \"196729\"," +
|
||||
"\"mobile\": null" +
|
||||
"}" +
|
||||
"}";
|
||||
|
||||
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
ZZDUserDTO zzdUserDTO;
|
||||
try {
|
||||
zzdUserDTO = objectMapper.readValue(json, ZZDUserDTO.class);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
//暂时先注释,模拟一下就行
|
||||
// R zzdsso = remoterThirdPartyAuthService.zzdsso(token);
|
||||
// ZZDUserDTO zzdUserDTO = (ZZDUserDTO) zzdsso.getData();
|
||||
ZZDUserDTO.Ext ext = zzdUserDTO.getExt();
|
||||
String employeeCode = ext.getEmployeeCode();
|
||||
String mobile = zzdUserDTO.getMobile();
|
||||
|
||||
//两种方式,手机号+员工钉钉号,查到相同就视为同一个人,然后登录
|
||||
//先系统表里面查看是否有,在第三方数据库表里面看下是否有
|
||||
|
||||
//1.根据手机号或者钉钉号先查询是否有这个人
|
||||
R<List<SysUser>> userByMobileOrEmployeeCode = remoteUserService.getUserByMobileOrEmployeeCode(employeeCode, mobile);
|
||||
List<SysUser> sysUserList = userByMobileOrEmployeeCode.getData();
|
||||
|
||||
if(!CollectionUtils.isEmpty(sysUserList)){
|
||||
//登录成功生成token
|
||||
UserTokenDTO userTokenDTO = new UserTokenDTO();
|
||||
userTokenDTO.setSysUser(sysUserList.get(0));
|
||||
remoteTokenService.generateToken(userTokenDTO);
|
||||
}
|
||||
//2.根据浙政钉的员工钉钉号查询是否有这个人
|
||||
R<List<ThirdPartyUserEntity>> thirdPartyUserEntityR=remoterThirdPartyAuthService.getUserByEmployeeCode(employeeCode);
|
||||
List<ThirdPartyUserEntity> thirdPartyUserEntityList = thirdPartyUserEntityR.getData();
|
||||
|
||||
UserDTO userDTO = new UserDTO();
|
||||
//写死,新加入到部门表的全部归类到其他部门里面,不按照浙政钉的部门
|
||||
userDTO.setDeptId(1865990541445263361L);
|
||||
userDTO.setUsername(zzdUserDTO.getUsername());
|
||||
userDTO.setName(zzdUserDTO.getUsername());
|
||||
userDTO.setNickname(zzdUserDTO.getUsername());
|
||||
//浙里办用户
|
||||
userDTO.setSystemType("1");
|
||||
//取上面的手机号
|
||||
userDTO.setPhone(mobile);
|
||||
userDTO.setDingtalkEmployeeCode(employeeCode);
|
||||
//随便写死给个密码
|
||||
userDTO.setPassword("pj"+mobile);
|
||||
/* if(!CollectionUtils.isEmpty(thirdPartyUserEntityList)){
|
||||
//todo 这一步现在是没用的,这个最终要考虑是否通过浙政钉里面的人员来限制移动端的使用用户来决定
|
||||
//第三方数据库里面有
|
||||
//这里只在第三方库里查到,所以要对系统用户表进行新增当前用户,插入一条信息进去
|
||||
R<Boolean> booleanR = remoteUserService.registerUser(userDTO);
|
||||
//通过唯一手机号查询到刚注册的用户信息,生成token
|
||||
R<List<SysUser>> userByMobile=remoteUserService.getUserByMobile(mobile);
|
||||
SysUser sysUser = userByMobile.getData().get(0);
|
||||
UserTokenDTO userTokenDTO = new UserTokenDTO();
|
||||
userTokenDTO.setSysUser(sysUser);
|
||||
//登录成功,根据新注册的用户信息返回token
|
||||
remoteTokenService.generateToken(userTokenDTO);
|
||||
}
|
||||
else {
|
||||
//两个表都没查到人员不能够使用移动端的系统
|
||||
}*/
|
||||
|
||||
// log.info("用户不存在,尝试创建用户");
|
||||
|
||||
R<Boolean> booleanR = remoteUserService.registerUser(userDTO);
|
||||
//通过唯一手机号查询到刚注册的用户信息,生成token
|
||||
|
||||
R<List<SysUser>> userByMobile=remoteUserService.getUserByMobile(mobile);
|
||||
SysUser sysUser = userByMobile.getData().get(0);
|
||||
int i=8/0;
|
||||
UserTokenDTO userTokenDTO = new UserTokenDTO();
|
||||
userTokenDTO.setSysUser(sysUser);
|
||||
//登录成功,根据新注册的用户信息返回token
|
||||
return remoteTokenService.generateToken(userTokenDTO);
|
||||
UrlTokenDTO body = new UrlTokenDTO();
|
||||
body.setAuthCode(token);
|
||||
return remoterThirdPartyAuthService.zzdsso(body);
|
||||
}
|
||||
|
||||
|
||||
@@ -387,12 +295,13 @@ public class AppUserAutoLoginController {
|
||||
// return appUserService.importUser(excelVOList, bindingResult);
|
||||
// }
|
||||
//
|
||||
// @Operation(summary = "注册app用户表", description = "注册app用户表")
|
||||
// @SysLog("注册app用户表")
|
||||
// @Inner(value = false)
|
||||
// @PostMapping("/register")
|
||||
// public R register(@RequestBody AppUserDTO appUser) {
|
||||
// return appUserService.registerAppUser(appUser);
|
||||
// }
|
||||
|
||||
//@Operation(summary = "注册app用户表", description = "注册app用户表")
|
||||
//@SysLog("注册app用户表")
|
||||
//@Inner(value = false)
|
||||
//@PostMapping("/register")
|
||||
//public R register(@RequestBody AppUserDTO appUser) {
|
||||
// return appUserService.registerAppUser(appUser);
|
||||
//}
|
||||
|
||||
}
|
||||
+109
-147
@@ -22,6 +22,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.admin.api.dto.UserInfo;
|
||||
import com.bsd.pjcloud.admin.api.feign.RemoteUserService;
|
||||
import com.bsd.pjcloud.app.api.dto.AppUserDTO;
|
||||
import com.bsd.pjcloud.app.api.dto.AppUserInfo;
|
||||
import com.bsd.pjcloud.app.api.entity.AppUser;
|
||||
import com.bsd.pjcloud.app.api.feign.RemoteAppUserService;
|
||||
import com.bsd.pjcloud.app.api.vo.AppUserExcelVO;
|
||||
@@ -61,155 +62,116 @@ import java.util.List;
|
||||
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
|
||||
public class AppUserController {
|
||||
|
||||
private final AppUserService appUserService;
|
||||
|
||||
|
||||
@Inner
|
||||
@GetMapping("/info/{username}")
|
||||
public R info(@PathVariable String username) {
|
||||
AppUser user = appUserService.getOne(Wrappers.<AppUser>query()
|
||||
.lambda()
|
||||
.eq(AppUser::getUsername, username)
|
||||
.or()
|
||||
.eq(AppUser::getPhone, username));
|
||||
if (user == null) {
|
||||
return R.failed(MsgUtils.getMessage(ErrorCodes.APP_USER_USERINFO_EMPTY, username));
|
||||
}
|
||||
return R.ok(appUserService.findUserInfo(user));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户全部信息
|
||||
* @return 用户信息
|
||||
*/
|
||||
@GetMapping(value = { "/info" })
|
||||
public R info() {
|
||||
String username = SecurityUtils.getUser().getUsername();
|
||||
AppUser user = appUserService.getOne(Wrappers.<AppUser>query().lambda().eq(AppUser::getUsername, username));
|
||||
if (user == null) {
|
||||
return R.failed(MsgUtils.getMessage(ErrorCodes.SYS_USER_QUERY_ERROR));
|
||||
}
|
||||
return R.ok(appUserService.findUserInfo(user));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
* @param page 分页对象
|
||||
* @param appUserDTO app用户表
|
||||
* @return
|
||||
*/
|
||||
@Operation(summary = "分页查询", description = "分页查询")
|
||||
@GetMapping("/page")
|
||||
public R getAppUserPage(Page page, AppUserDTO appUserDTO) {
|
||||
return R.ok(appUserService.getUsersWithRolePage(page, appUserDTO));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询app用户表
|
||||
* @param userId id
|
||||
* @return R
|
||||
*/
|
||||
@Operation(summary = "通过id查询", description = "通过id查询")
|
||||
@GetMapping("/details/{userId}")
|
||||
public R getById(@PathVariable("userId") Long userId) {
|
||||
return R.ok(appUserService.selectUserVoById(userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return R
|
||||
*/
|
||||
@Operation(summary = "通过userName查询", description = "通过userName查询")
|
||||
@GetMapping("/details")
|
||||
public R getByUserName(AppUser user) {
|
||||
AppUser one = appUserService.getOne(Wrappers.query(user), false);
|
||||
return R.ok(one == null ? null : CommonConstants.SUCCESS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增app用户表
|
||||
* @param appUser app用户表
|
||||
* @return R
|
||||
*/
|
||||
// @Operation(summary = "新增app用户表", description = "新增app用户表")
|
||||
// @SysLog("新增app用户表")
|
||||
// @PostMapping
|
||||
// @HasPermission("app_appuser_add")
|
||||
// public R save(@RequestBody AppUserDTO appUser) {
|
||||
// appUserService.saveUser(appUser);
|
||||
// return R.ok();
|
||||
// }
|
||||
|
||||
/**
|
||||
* 修改app用户表
|
||||
* @param appUser app用户表
|
||||
* @return R
|
||||
*/
|
||||
@Operation(summary = "修改app用户表", description = "修改app用户表")
|
||||
@SysLog("修改app用户表")
|
||||
@PutMapping
|
||||
@HasPermission("app_appuser_edit")
|
||||
public R updateById(@RequestBody AppUserDTO appUser) {
|
||||
return R.ok(appUserService.updateUser(appUser));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id删除app用户表
|
||||
* @param ids userIds
|
||||
* @return R
|
||||
*/
|
||||
@Operation(summary = "通过ids删除app用户表", description = "通过ids删除app用户表")
|
||||
@SysLog("通过id删除app用户表")
|
||||
@DeleteMapping
|
||||
@HasPermission("app_appuser_del")
|
||||
public R removeById(@RequestBody Long[] ids) {
|
||||
return R.ok(appUserService.deleteAppUserByIds(ids));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel 表格
|
||||
// * @param appUser 查询条件
|
||||
* @return excel 文件流
|
||||
*/
|
||||
// @ResponseExcel
|
||||
// @GetMapping("/export")
|
||||
// @HasPermission("app_appuser_export")
|
||||
// public List<AppUserExcelVO> export(AppUserDTO appUser) {
|
||||
// return appUserService.listUser(appUser);
|
||||
// }
|
||||
|
||||
@SysLog("修改个人信息")
|
||||
@PutMapping("/edit")
|
||||
public R updateUserInfo(@Valid @RequestBody AppUserDTO userDto) {
|
||||
if (UserTypeEnum.TOC.getStatus().equals(SecurityUtils.getUser().getUserType())) {
|
||||
userDto.setUserId(SecurityUtils.getUser().getId());
|
||||
// TOC 以手机号为key
|
||||
userDto.setUsername(SecurityUtils.getUser().getUsername());
|
||||
}
|
||||
return appUserService.updateUserInfo(userDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入用户
|
||||
* @param excelVOList 用户列表
|
||||
* @param bindingResult 错误信息列表
|
||||
* @return R
|
||||
*/
|
||||
// @PostMapping("/import")
|
||||
// @HasPermission("app_appuser_export")
|
||||
// public R importUser(@RequestExcel List<AppUserExcelVO> excelVOList, BindingResult bindingResult) {
|
||||
// return appUserService.importUser(excelVOList, bindingResult);
|
||||
// }
|
||||
|
||||
// @Operation(summary = "注册app用户表", description = "注册app用户表")
|
||||
// @SysLog("注册app用户表")
|
||||
// @Inner(value = false)
|
||||
// @PostMapping("/register")
|
||||
// public R register(@RequestBody AppUserDTO appUser) {
|
||||
// return appUserService.registerAppUser(appUser);
|
||||
// }
|
||||
|
||||
private final AppUserService appUserService;
|
||||
|
||||
/* ************************************* start app start ************************************* */
|
||||
|
||||
/**
|
||||
* 获取当前用户全部信息
|
||||
*/
|
||||
@GetMapping(value = {"/info"})
|
||||
public R<AppUserInfo> info() {
|
||||
Long userId = SecurityUtils.getUser().getId();
|
||||
AppUser user = appUserService.getOne(Wrappers.<AppUser>query().lambda().eq(AppUser::getUserId, userId));
|
||||
if (user == null) {
|
||||
return R.failed(MsgUtils.getMessage(ErrorCodes.SYS_USER_QUERY_ERROR));
|
||||
}
|
||||
return R.ok(appUserService.findUserInfo(user));
|
||||
}
|
||||
|
||||
/* ************************************* end app end ************************************* */
|
||||
|
||||
/* ************************************* start 远程调用 start ************************************* */
|
||||
|
||||
@Inner
|
||||
@GetMapping("/info/{username}")
|
||||
public R info(@PathVariable String username) {
|
||||
AppUser user = appUserService.getOne(Wrappers.<AppUser>query()
|
||||
.lambda()
|
||||
.eq(AppUser::getUsername, username)
|
||||
.or()
|
||||
.eq(AppUser::getPhone, username));
|
||||
if (user == null) {
|
||||
return R.failed(MsgUtils.getMessage(ErrorCodes.APP_USER_USERINFO_EMPTY, username));
|
||||
}
|
||||
return R.ok(appUserService.findUserInfo(user));
|
||||
}
|
||||
|
||||
@Inner
|
||||
@GetMapping("/infoByUserId/{userId}")
|
||||
public R<AppUserInfo> infoByUserId(@PathVariable Long userId) {
|
||||
AppUser user = appUserService.getOne(Wrappers.<AppUser>query()
|
||||
.lambda()
|
||||
.eq(AppUser::getUserId, userId)
|
||||
);
|
||||
if (user == null) return R.failed();
|
||||
return R.ok(appUserService.findUserInfo(user));
|
||||
}
|
||||
|
||||
/* ************************************* end 远程调用 end ************************************* */
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
*/
|
||||
@Operation(summary = "分页查询", description = "分页查询")
|
||||
@GetMapping("/page")
|
||||
@HasPermission("app_appuser_page")
|
||||
public R getAppUserPage(Page page, AppUserDTO appUserDTO) {
|
||||
return R.ok(appUserService.getUsersWithRolePage(page, appUserDTO));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询app用户表
|
||||
*/
|
||||
@Operation(summary = "通过id查询", description = "通过id查询")
|
||||
@GetMapping("/details/{userId}")
|
||||
public R getById(@PathVariable("userId") Long userId) {
|
||||
return R.ok(appUserService.selectUserVoById(userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return R
|
||||
*/
|
||||
@Operation(summary = "通过userName查询", description = "通过userName查询")
|
||||
@GetMapping("/details")
|
||||
public R getByUserName(AppUser user) {
|
||||
AppUser one = appUserService.getOne(Wrappers.query(user), false);
|
||||
return R.ok(one == null ? null : CommonConstants.SUCCESS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改app用户表
|
||||
*/
|
||||
@Operation(summary = "修改app用户表", description = "修改app用户表")
|
||||
@SysLog("修改app用户表")
|
||||
@PutMapping
|
||||
@HasPermission("app_appuser_edit")
|
||||
public R updateById(@RequestBody AppUserDTO appUser) {
|
||||
return R.ok(appUserService.updateUser(appUser));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id删除app用户表
|
||||
*/
|
||||
@Operation(summary = "通过ids删除app用户表", description = "通过ids删除app用户表")
|
||||
@SysLog("通过id删除app用户表")
|
||||
@DeleteMapping
|
||||
@HasPermission("app_appuser_del")
|
||||
public R removeById(@RequestBody Long[] ids) {
|
||||
return R.ok(appUserService.deleteAppUserByIds(ids));
|
||||
}
|
||||
|
||||
|
||||
@SysLog("修改个人信息")
|
||||
@PutMapping("/edit")
|
||||
public R updateUserInfo(@Valid @RequestBody AppUserDTO userDto) {
|
||||
if (UserTypeEnum.TOC.getStatus().equals(SecurityUtils.getUser().getUserType())) {
|
||||
userDto.setUserId(SecurityUtils.getUser().getId());
|
||||
// TOC 以手机号为key
|
||||
userDto.setUsername(SecurityUtils.getUser().getUsername());
|
||||
}
|
||||
return appUserService.updateUserInfo(userDto);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package com.bsd.pjcloud.app.handler;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.alipay.api.AlipayClient;
|
||||
import com.alipay.api.AlipayConfig;
|
||||
import com.alipay.api.DefaultAlipayClient;
|
||||
import com.alipay.api.request.AlipaySystemOauthTokenRequest;
|
||||
import com.alipay.api.response.AlipaySystemOauthTokenResponse;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.bsd.pjcloud.app.api.constant.AppUserTypeEnum;
|
||||
import com.bsd.pjcloud.app.api.dto.AppUserInfo;
|
||||
import com.bsd.pjcloud.app.api.entity.AppUser;
|
||||
import com.bsd.pjcloud.app.config.MiniAlipayConf;
|
||||
import com.bsd.pjcloud.app.mapper.AppSocialDetailsMapper;
|
||||
import com.bsd.pjcloud.app.service.AppUserService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 支付宝小程序
|
||||
*/
|
||||
@Slf4j
|
||||
@Component("MINI_ALIPAY")
|
||||
@AllArgsConstructor
|
||||
public class MiniAlipayLoginHandler extends AbstractLoginHandler {
|
||||
|
||||
private final MiniAlipayConf miniAlipayConf;
|
||||
|
||||
private final AppUserService appUserService;
|
||||
|
||||
private final AppSocialDetailsMapper appSocialDetailsMapper;
|
||||
|
||||
static private AlipayClient alipayClient = null;
|
||||
|
||||
public AlipayClient getAlipay() {
|
||||
if (ObjectUtil.isNotEmpty(miniAlipayConf)) return getAlipayClient();
|
||||
return getAlipayClient();
|
||||
}
|
||||
|
||||
private synchronized AlipayClient getAlipayClient() {
|
||||
if (alipayClient == null) {
|
||||
try {
|
||||
alipayClient = new DefaultAlipayClient(getAlipayConfig());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("初始化支付宝客户端失败error:{}", e.getMessage());
|
||||
}
|
||||
}
|
||||
return alipayClient;
|
||||
}
|
||||
|
||||
private AlipayConfig getAlipayConfig() {
|
||||
AlipayConfig alipayConfig = new AlipayConfig();
|
||||
alipayConfig.setServerUrl("https://openapi.alipay.com/gateway.do");
|
||||
alipayConfig.setAppId(miniAlipayConf.getAppId());
|
||||
alipayConfig.setPrivateKey(miniAlipayConf.getPrivateKey());
|
||||
alipayConfig.setFormat("json");
|
||||
alipayConfig.setAlipayPublicKey(miniAlipayConf.getAlipayPublicKey());
|
||||
alipayConfig.setCharset("UTF-8");
|
||||
alipayConfig.setSignType("RSA2");
|
||||
return alipayConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支付宝openId
|
||||
*/
|
||||
public AlipaySystemOauthTokenResponse getOpenId(String code, String refreshToken) {
|
||||
AlipaySystemOauthTokenRequest request = new AlipaySystemOauthTokenRequest();
|
||||
// 设置刷新令牌
|
||||
if (ObjectUtil.isNotEmpty(refreshToken)) {
|
||||
request.setRefreshToken(refreshToken);
|
||||
request.setGrantType("refresh_token");
|
||||
}
|
||||
// 设置授权码
|
||||
if (ObjectUtil.isNotEmpty(code)) {
|
||||
request.setCode(code);
|
||||
request.setGrantType("authorization_code");
|
||||
}
|
||||
try {
|
||||
AlipaySystemOauthTokenResponse response = getAlipay().execute(request);
|
||||
if (!response.isSuccess()) {
|
||||
log.error("|获取支付宝openId|错误error:{}", response.getSubMsg());
|
||||
}
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("|获取支付宝openId|意外的错误error:{}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过用户传入获取唯一标识
|
||||
*/
|
||||
@Override
|
||||
public String identify(String loginStr) {
|
||||
AlipaySystemOauthTokenResponse res = getOpenId(loginStr, null);
|
||||
if (ObjectUtil.isEmpty(res) || ObjectUtil.isEmpty(res.getOpenId())) {
|
||||
return null;
|
||||
}
|
||||
return res.getOpenId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
@Override
|
||||
public AppUserInfo info(String openId) {
|
||||
AppUser user = appUserService.getOne(Wrappers.<AppUser>query().lambda().eq(AppUser::getOpenid, openId));
|
||||
if (ObjectUtil.isEmpty(user)) {
|
||||
log.info("支付宝小程序openId:{},创建新的用户", openId);
|
||||
String name = "支付宝游客-" + openId;
|
||||
AppUser appUser = new AppUser();
|
||||
appUser.setOpenid(openId);
|
||||
appUser.setUserType(AppUserTypeEnum.MINI_ALIPAY.getType());
|
||||
appUser.setUsername(name);
|
||||
appUser.setName(name);
|
||||
appUser.setCreateBy(openId);
|
||||
appUser.setUpdateBy(openId);
|
||||
appUserService.saveOrUpdate(appUser);
|
||||
return appUserService.findUserInfo(user);
|
||||
}
|
||||
if (ObjectUtil.isEmpty(user.getLoginExtJson())) {
|
||||
|
||||
}
|
||||
return appUserService.findUserInfo(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定逻辑
|
||||
*/
|
||||
@Override
|
||||
public Boolean bind(AppUser user, String identify) {
|
||||
user.setOpenid(identify);
|
||||
appUserService.updateById(user);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+30
-38
@@ -17,18 +17,15 @@
|
||||
|
||||
package com.bsd.pjcloud.app.handler;
|
||||
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.bsd.pjcloud.app.api.constant.AppUserTypeEnum;
|
||||
import com.bsd.pjcloud.app.api.dto.AppUserInfo;
|
||||
import com.bsd.pjcloud.app.api.entity.AppSocialDetails;
|
||||
import com.bsd.pjcloud.app.api.entity.AppUser;
|
||||
import com.bsd.pjcloud.app.mapper.AppSocialDetailsMapper;
|
||||
import com.bsd.pjcloud.app.service.AppUserService;
|
||||
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
|
||||
import com.bsd.pjcloud.common.core.constant.enums.LoginTypeEnum;
|
||||
import com.bsd.pjcloud.common.core.exception.CheckedException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -36,20 +33,19 @@ import org.springframework.stereotype.Component;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author pjcloud
|
||||
* @date 2019年11月02日
|
||||
* <p>
|
||||
* 微信小程序
|
||||
*/
|
||||
@Slf4j
|
||||
@Component("APP-MINI")
|
||||
@Component("MINI_WECHAT")
|
||||
@AllArgsConstructor
|
||||
public class MiniAppLoginHandler extends AbstractLoginHandler {
|
||||
public class MiniWechatLoginHandler extends AbstractLoginHandler {
|
||||
|
||||
private final AppUserService appUserService;
|
||||
|
||||
private final AppSocialDetailsMapper appSocialDetailsMapper;
|
||||
|
||||
private final WxMaService wxMaService;
|
||||
|
||||
/**
|
||||
* 小程序登录传入code
|
||||
* <p>
|
||||
@@ -60,20 +56,16 @@ public class MiniAppLoginHandler extends AbstractLoginHandler {
|
||||
*/
|
||||
@Override
|
||||
public String identify(String code) {
|
||||
AppSocialDetails condition = new AppSocialDetails();
|
||||
condition.setType(LoginTypeEnum.MINI_APP.getType());
|
||||
AppSocialDetails socialDetails = appSocialDetailsMapper.selectOne(new QueryWrapper<>(condition));
|
||||
|
||||
String url = String.format(SecurityConstants.MINI_APP_AUTHORIZATION_CODE_URL, socialDetails.getAppId(),
|
||||
socialDetails.getAppSecret(), code);
|
||||
String result = HttpUtil.get(url);
|
||||
log.debug("微信小程序响应报文:{}", result);
|
||||
|
||||
if (JSONUtil.parseObj(result).containsKey("openid")) {
|
||||
return JSONUtil.parseObj(result).get("openid").toString();
|
||||
} else {
|
||||
log.error("微信小程序登录失败:{}", result);
|
||||
throw new CheckedException("微信小程序登录失败");
|
||||
try {
|
||||
WxMaJscode2SessionResult session = wxMaService.getUserService().getSessionInfo(code);
|
||||
if (ObjectUtil.isNotEmpty(session) && ObjectUtil.isNotEmpty(session.getOpenid())) {
|
||||
return session.getOpenid();
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("微信小程序登录失败:{}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,23 +77,23 @@ public class MiniAppLoginHandler extends AbstractLoginHandler {
|
||||
*/
|
||||
@Override
|
||||
public AppUserInfo info(String openId) {
|
||||
AppUser user = appUserService.getOne(Wrappers.<AppUser>query().lambda().eq(AppUser::getWxOpenid, openId));
|
||||
|
||||
// 未注册用户自动注册
|
||||
if (Objects.isNull(user)) {
|
||||
log.info("微信小程序未绑定:{},创建新的用户", openId);
|
||||
AppUser user = appUserService.getOne(Wrappers.<AppUser>query().lambda().eq(AppUser::getOpenid, openId));
|
||||
if (ObjectUtil.isEmpty(user)) {
|
||||
log.info("微信小程序openId:{},创建新的用户", openId);
|
||||
String name = "微信游客-" + openId;
|
||||
AppUser appUser = new AppUser();
|
||||
appUser.setWxOpenid(openId);
|
||||
appUser.setUsername(openId);
|
||||
appUser.setOpenid(openId);
|
||||
appUser.setUserType(AppUserTypeEnum.MINI_ALIPAY.getType());
|
||||
appUser.setUsername(name);
|
||||
appUser.setName(name);
|
||||
appUser.setCreateBy(openId);
|
||||
appUser.setUpdateBy(openId);
|
||||
appUserService.saveOrUpdate(appUser);
|
||||
|
||||
AppUserInfo appUserDTO = new AppUserInfo();
|
||||
appUserDTO.setAppUser(appUser);
|
||||
return appUserDTO;
|
||||
return appUserService.findUserInfo(user);
|
||||
}
|
||||
if (ObjectUtil.isEmpty(user.getLoginExtJson())) {
|
||||
|
||||
}
|
||||
return appUserService.findUserInfo(user);
|
||||
}
|
||||
|
||||
@@ -114,7 +106,7 @@ public class MiniAppLoginHandler extends AbstractLoginHandler {
|
||||
*/
|
||||
@Override
|
||||
public Boolean bind(AppUser user, String identify) {
|
||||
user.setWxOpenid(identify);
|
||||
user.setOpenid(identify);
|
||||
appUserService.updateById(user);
|
||||
return true;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.bsd.pjcloud.app.mapper;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.app.api.entity.AppMenuEntity;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScope;
|
||||
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface AppMenuMapper extends PjcloudBaseMapper<AppMenuEntity> {
|
||||
|
||||
default LambdaQueryWrapper conditionWrapper(AppMenuEntity dto) {
|
||||
LambdaQueryWrapper<AppMenuEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper
|
||||
.eq(ObjectUtil.isNotEmpty(dto.getPlatform()), AppMenuEntity::getPlatform, dto.getPlatform())
|
||||
.eq(ObjectUtil.isNotEmpty(dto.getType()), AppMenuEntity::getType, dto.getType())
|
||||
.eq(ObjectUtil.isNotEmpty(dto.getVisible()), AppMenuEntity::getVisible, dto.getVisible())
|
||||
// 后台文案:排序越大越靠前
|
||||
.orderByDesc(AppMenuEntity::getSort)
|
||||
.orderByDesc(AppMenuEntity::getCreateTime);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
default List<AppMenuEntity> scopeList(AppMenuEntity dto, DataScope dataScope) {
|
||||
return selectList(conditionWrapper(dto));
|
||||
}
|
||||
|
||||
default Page<AppMenuEntity> scopePage(Page page, AppMenuEntity dto, DataScope dataScope) {
|
||||
return selectPage(page, conditionWrapper(dto));
|
||||
}
|
||||
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.bsd.pjcloud.app.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.bsd.pjcloud.app.api.entity.AppMenuEntity;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScope;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AppMenuService extends IService<AppMenuEntity> {
|
||||
|
||||
List<AppMenuEntity> scopeList(AppMenuEntity dto, DataScope dataScope);
|
||||
|
||||
Page<AppMenuEntity> scopePage(Page page, AppMenuEntity dto, DataScope dataScope);
|
||||
}
|
||||
+14
-16
@@ -18,30 +18,28 @@
|
||||
package com.bsd.pjcloud.app.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.bsd.pjcloud.app.api.dto.AppAdminSyncDto;
|
||||
import com.bsd.pjcloud.app.api.dto.AppUserInfo;
|
||||
import com.bsd.pjcloud.app.api.entity.AppSocialDetails;
|
||||
|
||||
/**
|
||||
* 系统社交登录账号表
|
||||
*
|
||||
* @author pjcloud
|
||||
* @date 2018-08-16 21:30:41
|
||||
*/
|
||||
public interface AppSocialDetailsService extends IService<AppSocialDetails> {
|
||||
|
||||
/**
|
||||
* 绑定社交账号
|
||||
* @param state 类型
|
||||
* @param code code
|
||||
* @return
|
||||
*/
|
||||
Boolean bindSocial(String state, String code);
|
||||
|
||||
/**
|
||||
* 根据入参查询用户信息
|
||||
* @param inStr
|
||||
* @return
|
||||
*/
|
||||
AppUserInfo getUserInfo(String inStr);
|
||||
/**
|
||||
* 同步admin账户信息为app账户
|
||||
*/
|
||||
AppUserInfo syncAdminAccountInfo(AppAdminSyncDto dto);
|
||||
|
||||
/**
|
||||
* 绑定社交账号
|
||||
*/
|
||||
Boolean bindSocial(String state, String code);
|
||||
|
||||
/**
|
||||
* 根据入参查询用户信息
|
||||
*/
|
||||
AppUserInfo getUserInfo(String inStr);
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.bsd.pjcloud.app.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bsd.pjcloud.app.api.entity.AppMenuEntity;
|
||||
import com.bsd.pjcloud.app.mapper.AppMenuMapper;
|
||||
import com.bsd.pjcloud.app.service.AppMenuService;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScope;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单管理
|
||||
*/
|
||||
@Service
|
||||
public class AppMenuServiceImpl extends ServiceImpl<AppMenuMapper, AppMenuEntity> implements AppMenuService {
|
||||
|
||||
@Override
|
||||
public List<AppMenuEntity> scopeList(AppMenuEntity dto, DataScope dataScope) {
|
||||
return baseMapper.scopeList(dto, dataScope);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<AppMenuEntity> scopePage(Page page, AppMenuEntity dto, DataScope dataScope) {
|
||||
return baseMapper.scopePage(page, dto, dataScope);
|
||||
}
|
||||
|
||||
}
|
||||
+56
-34
@@ -17,8 +17,12 @@
|
||||
|
||||
package com.bsd.pjcloud.app.service.impl;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringPool;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bsd.pjcloud.app.api.constant.AppUserTypeEnum;
|
||||
import com.bsd.pjcloud.app.api.dto.AppAdminSyncDto;
|
||||
import com.bsd.pjcloud.app.api.dto.AppUserInfo;
|
||||
import com.bsd.pjcloud.app.api.entity.AppSocialDetails;
|
||||
import com.bsd.pjcloud.app.api.entity.AppUser;
|
||||
@@ -26,6 +30,7 @@ import com.bsd.pjcloud.app.handler.LoginHandler;
|
||||
import com.bsd.pjcloud.app.mapper.AppSocialDetailsMapper;
|
||||
import com.bsd.pjcloud.app.mapper.AppUserMapper;
|
||||
import com.bsd.pjcloud.app.service.AppSocialDetailsService;
|
||||
import com.bsd.pjcloud.app.service.AppUserService;
|
||||
import com.bsd.pjcloud.common.core.constant.CacheConstants;
|
||||
import com.bsd.pjcloud.common.security.util.SecurityUtils;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -42,45 +47,62 @@ import java.util.Map;
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
@Service("appSocialDetailsService")
|
||||
public class AppSocialDetailsServiceImpl extends ServiceImpl<AppSocialDetailsMapper, AppSocialDetails>
|
||||
implements AppSocialDetailsService {
|
||||
public class AppSocialDetailsServiceImpl extends ServiceImpl<AppSocialDetailsMapper, AppSocialDetails> implements AppSocialDetailsService {
|
||||
|
||||
private final Map<String, LoginHandler> loginHandlerMap;
|
||||
private final Map<String, LoginHandler> loginHandlerMap;
|
||||
|
||||
private final CacheManager cacheManager;
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
private final AppUserMapper appUserMapper;
|
||||
private final AppUserMapper appUserMapper;
|
||||
|
||||
/**
|
||||
* 绑定社交账号
|
||||
* @param type type
|
||||
* @param code code
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public Boolean bindSocial(String type, String code) {
|
||||
LoginHandler loginHandler = loginHandlerMap.get(type);
|
||||
// 绑定逻辑
|
||||
String identify = loginHandler.identify(code);
|
||||
AppUser user = appUserMapper.selectById(SecurityUtils.getUser().getId());
|
||||
loginHandler.bind(user, identify);
|
||||
private final AppUserService appUserService;
|
||||
|
||||
// 更新緩存
|
||||
cacheManager.getCache(CacheConstants.USER_DETAILS_MINI).evict(user.getUsername());
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
@Override
|
||||
public AppUserInfo syncAdminAccountInfo(AppAdminSyncDto dto) {
|
||||
AppUser user = appUserService.getOne(Wrappers.<AppUser>query().lambda()
|
||||
.eq(AppUser::getOpenid, dto.getUniqueCode())
|
||||
.eq(AppUser::getUserType, dto.getUserTypeEnum().getType())
|
||||
);
|
||||
if (ObjectUtil.isNotEmpty(user)) {
|
||||
return appUserService.findUserInfo(user);
|
||||
}
|
||||
AppUser appUser = new AppUser();
|
||||
appUser.setOpenid(dto.getUniqueCode());
|
||||
appUser.setUserType(AppUserTypeEnum.MINI_ALIPAY.getType());
|
||||
appUser.setUsername(dto.getUsername());
|
||||
appUser.setName(dto.getUsername());
|
||||
appUser.setAvatar(dto.getAvatarImage());
|
||||
appUser.setCreateBy(dto.getUniqueCode());
|
||||
appUser.setUpdateBy(dto.getUniqueCode());
|
||||
appUserService.saveOrUpdate(appUser);
|
||||
return appUserService.findUserInfo(appUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据入参查询用户信息
|
||||
* @param inStr TYPE@code
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public AppUserInfo getUserInfo(String inStr) {
|
||||
String[] inStrs = inStr.split(StringPool.AT);
|
||||
String type = inStrs[0];
|
||||
String loginStr = inStr.substring(type.length() + 1);
|
||||
return loginHandlerMap.get(type).handle(loginStr);
|
||||
}
|
||||
/**
|
||||
* 绑定社交账号
|
||||
*/
|
||||
@Override
|
||||
public Boolean bindSocial(String type, String code) {
|
||||
LoginHandler loginHandler = loginHandlerMap.get(type);
|
||||
// 绑定逻辑
|
||||
String identify = loginHandler.identify(code);
|
||||
AppUser user = appUserMapper.selectById(SecurityUtils.getUser().getId());
|
||||
loginHandler.bind(user, identify);
|
||||
|
||||
// 更新緩存
|
||||
cacheManager.getCache(CacheConstants.USER_DETAILS_MINI).evict(user.getUsername());
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据入参查询用户信息
|
||||
*/
|
||||
@Override
|
||||
public AppUserInfo getUserInfo(String inStr) {
|
||||
String[] inStrs = inStr.split(StringPool.AT);
|
||||
String type = inStrs[0];
|
||||
String loginStr = inStr.substring(type.length() + 1);
|
||||
return loginHandlerMap.get(type).handle(loginStr);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,13 +6,25 @@ spring:
|
||||
name: @artifactId@
|
||||
cloud:
|
||||
nacos:
|
||||
username: @nacos.username@
|
||||
password: @nacos.password@
|
||||
username: nacos
|
||||
password: Aa@bsd.321
|
||||
discovery:
|
||||
server-addr: ${NACOS_HOST:pjcloud-register}:${NACOS_PORT:8848}
|
||||
server-addr: ${NACOS_HOST:pjcloud-register}:${NACOS_PORT:18849}
|
||||
config:
|
||||
server-addr: ${spring.cloud.nacos.discovery.server-addr}
|
||||
config:
|
||||
import:
|
||||
- optional:nacos:application-@profiles.active@.yml
|
||||
- optional:nacos:${spring.application.name}-@profiles.active@.yml
|
||||
|
||||
mini-ali-pay:
|
||||
app-id: 2021005197658445
|
||||
private-key: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCTYHY0AW2cjbHoOinD8f4CKMKasDmbRw4CEMasG5IzckmeuTqut3GmxxRRls62zlXRVOCDYgnGhzEhD/nMYx0TMBX2mYo+fH9XUMtZMKY+p2rPTXTlPnGfbQ4edUYofR0tq1ug+wHOD5/jxuwO1zPfB7t8dX3gNa4M43fTPK2mPDeo/UiRB45DPjUWvbMX59ltYYMgmAW0TAbXmRAGKFxrmX3HddJK0jSwEpTcHaRV3C6FrYV48vWHEliviVBUsLRIpwv3T3FYZm0a19LcGQtybeThXaaCYrj7Lwoffk8v8DL2g28IAeiFtjjkWbPFKKMCsSe/LVkvdQTuhncuDayTAgMBAAECggEAJvreiMbP7RZ1Nc0cet0op/2L60otEmrqw5UYVzE7rCvQWRD4+nk+sDP7pdDTf/p3LFTcxSNLHp1nWy1UjsCwUgDn7j+SFxDqxXdzMXXyoqO5VndQMcogV2CK2Xd/RPpCm2+1XpDFW3E6gw58JTi6WYU8C6Ev1/uau4H2kYJ9E3h2W0P0PyY+Q9QNHEpG2hLQdiWLHz3xzmSNSDXiknEg/Whew7IMVKNgxg5VVEBTerwCH5Grvn56CKx6acb6JlU+VFsmW3J1tMYLgxVhJWVVAK6NltmxE5apIxTkG3wEOMuuBCZHQ1Lm0rrGKJh8WmYRs6ho5wSjHYG3hyKRm/0TEQKBgQDcDDPyOWw/o9ZqB3N7qxUi63V7wU7X8QL/CmQO4tu/ng/4Drm8QESjk+ib3O3pCC7xwcte/U1Otw3AmSSj1CWvT8a0yLJM9GVOJwZFVyzy6yVFPyn6l4ndRdargyFokM2lgVTgEHo4WSMbLQsA8/KTB3anqo6bYLjqjsBgY49fpQKBgQCrdLMh5ayJOUK9UeBHXr0c/OLDzwoSUVMyNLfX3pIBC21meFxigNMiy+UKpslM2QMH1yyfwsUKv/i7y1kRFj9Go7pYU0HxglfHecllqXPunWJYuSm38x8WWAG/oqcLPGSlJjHYhU6IAkDuHHaW5DKLxr0x2DkZ/c4TcyYal9Ol1wKBgD4Lls4S0On02W1WIoLZT4FkIOE/NWsAAxdEp8fmwY17FQedp1NkT7ZL60efeNm/O/Vg7mYfSCAMz2ziAYnMNMAE34N0H4pBUxtBN6xvkzX/hafL0ptdBTuGCKydsdrZEZM8L2FqY/vlbleoULT5FAnPDe7i0YkQMbO5gpbrG68dAoGAFv+tc7OSREn5CNotwoUTywqlBG0GRdTA2zA1JPDHFvrIbwFgKxpqt/UvwREEWZmWjXuwkbGXqsx7ef1Z8i/QxxH5UhXA97trOOe+OzPSyEQ2R/52eSVUmzOs8F7KF3i/a2PHNesT0Ky0tvv8cFsl6Jf9Eu2VI0ZnWTaN+u04x7sCgYEAlhpr3RSJk2EnLPR3BxKboTxSGsK/2U4xz/WqX96B+lQ5h7NSMJC35vZJ5dm2vHtLcucHaK49YNsJdq+nzhhwxDz+Dd/5rcIp7AcdHqDsVLHYIN7JWxa9QiC3gvrnWsij1DShydOLpGMYveFz0LyRIUDW4CTs55oDFsIR4HdwtM0=
|
||||
public-key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk2B2NAFtnI2x6Dopw/H+AijCmrA5m0cOAhDGrBuSM3JJnrk6rrdxpscUUZbOts5V0VTgg2IJxocxIQ/5zGMdEzAV9pmKPnx/V1DLWTCmPqdqz0105T5xn20OHnVGKH0dLatboPsBzg+f48bsDtcz3we7fHV94DWuDON30zytpjw3qP1IkQeOQz41Fr2zF+fZbWGDIJgFtEwG15kQBihca5l9x3XSStI0sBKU3B2kVdwuha2FePL1hxJYr4lQVLC0SKcL909xWGZtGtfS3BkLcm3k4V2mgmK4+y8KH35PL/Ay9oNvCAHohbY45FmzxSijArEnvy1ZL3UE7oZ3Lg2skwIDAQAB
|
||||
alipay-public-key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqnCB+N172m5gxf1Rolfx5LErSiVwQCh0XQdDrVy6aTuVF5EtdqahSHtADyEPsBWudMBhzCfqpoZvfnvH7a+PED6uZ3DoXt7ETh8TmNM5yc+WTnD8pSphQvuHDJ3zMks489KNxiX6sXag71k9H1MXklJGPOUaljHqy/QlRn2ZLT7pFgaFH9LEcVoQBPIOurUn95078h848RFD30jZFW3fakxUGAGVVZaHH62z5OEWOmcA+CwxsEDMa1XMt02RGXgAAq4WOj+BqOGiFmhM8rnrUMchTyvx4umOzGvbF2Y9irN3aBZ5lfg+bzYcJpIOMzlM/V00LnEWzCblJzXNyH4hJQIDAQAB
|
||||
callback-url: https://rj4jv5rwqs.by.takin.cc/hard/hard-ali-pay/callback
|
||||
# callback-url: https://www.pjzhhq.cn/api/hard/hard-ali-pay/callback
|
||||
|
||||
mini-wechat-pay:
|
||||
app-id: wx95481f43b14565e5
|
||||
app-secret: 0aee1289b5ae14c0c69b8f5e9a4e518e
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<mapper namespace="com.bsd.pjcloud.app.mapper.AppMenuMapper">
|
||||
|
||||
</mapper>
|
||||
@@ -6,10 +6,10 @@ spring:
|
||||
name: @artifactId@
|
||||
cloud:
|
||||
nacos:
|
||||
username: @nacos.username@
|
||||
password: @nacos.password@
|
||||
username: nacos
|
||||
password: Aa@bsd.321
|
||||
discovery:
|
||||
server-addr: ${NACOS_HOST:pjcloud-register}:${NACOS_PORT:8848}
|
||||
server-addr: ${NACOS_HOST:pjcloud-register}:${NACOS_PORT:18849}
|
||||
config:
|
||||
server-addr: ${spring.cloud.nacos.discovery.server-addr}
|
||||
config:
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.bsd.pjcloud.canteen.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 收银终端按日期范围查询每日菜谱的请求参数。
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "收银终端每日菜谱日期范围查询参数")
|
||||
public class DailyMenuDateRangeQueryDTO {
|
||||
|
||||
/** 菜谱起始日期,查询闭区间包含当天。 */
|
||||
@NotNull(message = "菜谱开始日期不能为空")
|
||||
@Schema(description = "菜谱起始日期,格式为 yyyy-MM-dd", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDate startDate;
|
||||
|
||||
/** 菜谱结束日期,查询闭区间包含当天。 */
|
||||
@NotNull(message = "菜谱结束日期不能为空")
|
||||
@Schema(description = "菜谱结束日期,格式为 yyyy-MM-dd", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDate endDate;
|
||||
|
||||
/** 餐段协议编码;为空时查询全部餐段。 */
|
||||
@Schema(description = "餐段编码;为空表示全部餐段")
|
||||
private Integer hourType;
|
||||
|
||||
/** 菜品分类 ID;为空时查询全部分类。 */
|
||||
@Schema(description = "菜品分类 ID;为空表示全部分类")
|
||||
private Long categoryId;
|
||||
|
||||
/** 收银终端所属商户 ID,用于隔离不同商户的每日菜谱。 */
|
||||
@NotNull(message = "每日菜谱商户不能为空")
|
||||
@Schema(description = "收银终端所属商户 ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Long merchantId;
|
||||
}
|
||||
+4
@@ -119,4 +119,8 @@ public class CanteenCategoryEntity extends Model<CanteenCategoryEntity> {
|
||||
*/
|
||||
@Schema(description = "商家id")
|
||||
private Long merchantId;
|
||||
|
||||
|
||||
@Schema(description = "是否涨价")
|
||||
private Boolean isPriceIncreased;
|
||||
}
|
||||
+124
-117
@@ -10,6 +10,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import com.bsd.pjcloud.common.core.util.TenantTable;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
@@ -26,182 +27,188 @@ import java.time.LocalDateTime;
|
||||
public class CanteenSpuEntity extends Model<CanteenSpuEntity> {
|
||||
|
||||
|
||||
/**
|
||||
* 商品 SPU 编号,自增
|
||||
*/
|
||||
/**
|
||||
* 商品 SPU 编号,自增
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description="商品 SPU 编号,自增")
|
||||
@Schema(description = "商品 SPU 编号,自增")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 商品名称
|
||||
*/
|
||||
@Schema(description="商品名称")
|
||||
/**
|
||||
* 商品名称
|
||||
*/
|
||||
@Schema(description = "商品名称")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 商品简介
|
||||
*/
|
||||
@Schema(description="商品简介")
|
||||
/**
|
||||
* 商品简介
|
||||
*/
|
||||
@Schema(description = "商品简介")
|
||||
private String introduction;
|
||||
|
||||
/**
|
||||
* 商品详情
|
||||
*/
|
||||
@Schema(description="商品详情")
|
||||
/**
|
||||
* 商品详情
|
||||
*/
|
||||
@Schema(description = "商品详情")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 商品分类编号
|
||||
*/
|
||||
@Schema(description="商品分类编号")
|
||||
/**
|
||||
* 商品分类编号
|
||||
*/
|
||||
@Schema(description = "商品分类编号")
|
||||
private Long categoryId;
|
||||
|
||||
/**
|
||||
* 商品封面图
|
||||
*/
|
||||
@Schema(description="商品封面图")
|
||||
/**
|
||||
* 商品封面图
|
||||
*/
|
||||
@Schema(description = "商品封面图")
|
||||
private String picUrl;
|
||||
|
||||
/**
|
||||
* 商品轮播图地址
|
||||
数组,以逗号分隔
|
||||
最多上传15张
|
||||
*/
|
||||
@Schema(description="商品轮播图")
|
||||
/**
|
||||
* 商品轮播图地址
|
||||
* 数组,以逗号分隔
|
||||
* 最多上传15张
|
||||
*/
|
||||
@Schema(description = "商品轮播图")
|
||||
private String sliderPicUrls;
|
||||
|
||||
/**
|
||||
* 排序字段
|
||||
*/
|
||||
@Schema(description="排序字段")
|
||||
/**
|
||||
* 排序字段
|
||||
*/
|
||||
@Schema(description = "排序字段")
|
||||
private Integer sort;
|
||||
|
||||
/**
|
||||
* 商品状态: 1 上架(开启) 0 下架(禁用) -1 回收
|
||||
*/
|
||||
@Schema(description="商品状态: 1 上架(开启) 0 下架(禁用) -1 回收")
|
||||
/**
|
||||
* 商品状态: 1 上架(开启) 0 下架(禁用) -1 回收
|
||||
*/
|
||||
@Schema(description = "商品状态: 1 上架(开启) 0 下架(禁用) -1 回收")
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 规格类型:0 单规格 1 多规格
|
||||
*/
|
||||
@Schema(description="规格类型:0 单规格 1 多规格")
|
||||
/**
|
||||
* 规格类型:0 单规格 1 多规格
|
||||
*/
|
||||
@Schema(description = "规格类型:0 单规格 1 多规格")
|
||||
private Boolean specType;
|
||||
|
||||
/**
|
||||
* 商品价格,单位使用:分
|
||||
*/
|
||||
@Schema(description="商品价格,单位使用:分")
|
||||
/**
|
||||
* 商品价格,单位使用:分
|
||||
*/
|
||||
@Schema(description = "商品价格,单位使用:分")
|
||||
// @JsonSerialize(using = MoneySerializer.class) // 分 -> 元
|
||||
// @JsonDeserialize(using = MoneyDeserializer.class) // 元 -> 分
|
||||
private Integer price;
|
||||
|
||||
/**
|
||||
* 市场价,单位使用:分
|
||||
*/
|
||||
@Schema(description="市场价,单位使用:分")
|
||||
/**
|
||||
* 市场价,单位使用:分
|
||||
*/
|
||||
@Schema(description = "市场价,单位使用:分")
|
||||
// @JsonSerialize(using = MoneySerializer.class) // 分 -> 元
|
||||
// @JsonDeserialize(using = MoneyDeserializer.class) // 元 -> 分
|
||||
private Integer marketPrice;
|
||||
|
||||
/**
|
||||
* 成本价,单位: 分
|
||||
*/
|
||||
@Schema(description="成本价,单位: 分")
|
||||
/**
|
||||
* 成本价,单位: 分
|
||||
*/
|
||||
@Schema(description = "成本价,单位: 分")
|
||||
// @JsonSerialize(using = MoneySerializer.class) // 分 -> 元
|
||||
// @JsonDeserialize(using = MoneyDeserializer.class) // 元 -> 分
|
||||
private Integer costPrice;
|
||||
|
||||
/**
|
||||
* 库存
|
||||
*/
|
||||
@Schema(description="库存")
|
||||
/**
|
||||
* 库存
|
||||
*/
|
||||
@Schema(description = "库存")
|
||||
private Integer stock;
|
||||
|
||||
/**
|
||||
* 赠送积分
|
||||
*/
|
||||
@Schema(description="赠送积分")
|
||||
/**
|
||||
* 赠送积分
|
||||
*/
|
||||
@Schema(description = "赠送积分")
|
||||
private Integer giveIntegral;
|
||||
|
||||
/**
|
||||
* 商品销量
|
||||
*/
|
||||
@Schema(description="商品销量")
|
||||
/**
|
||||
* 商品销量
|
||||
*/
|
||||
@Schema(description = "商品销量")
|
||||
private Integer salesCount;
|
||||
|
||||
/**
|
||||
* 虚拟销量
|
||||
*/
|
||||
@Schema(description="虚拟销量")
|
||||
/**
|
||||
* 虚拟销量
|
||||
*/
|
||||
@Schema(description = "虚拟销量")
|
||||
private Integer virtualSalesCount;
|
||||
|
||||
/**
|
||||
* 商品点击量
|
||||
*/
|
||||
@Schema(description="商品点击量")
|
||||
/**
|
||||
* 商品点击量
|
||||
*/
|
||||
@Schema(description = "商品点击量")
|
||||
private Integer browseCount;
|
||||
|
||||
|
||||
@TableLogic
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Schema(description = "是否删除 -1:已删除 0:正常")
|
||||
private String delFlag;
|
||||
|
||||
@TableLogic
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Schema(description = "是否删除 -1:已删除 0:正常")
|
||||
private String delFlag;
|
||||
|
||||
/**
|
||||
* 系统类型 2每日 3在线订餐
|
||||
*/
|
||||
@Schema(description="系统类型 2每日 3在线订餐")
|
||||
/**
|
||||
* 系统类型 2每日 3在线订餐
|
||||
*/
|
||||
@Schema(description = "系统类型 2每日 3在线订餐")
|
||||
private Long systemType;
|
||||
|
||||
/**
|
||||
* 商户编号
|
||||
*/
|
||||
@Schema(description="商户编号")
|
||||
/**
|
||||
* 商户编号
|
||||
*/
|
||||
@Schema(description = "商户编号")
|
||||
private Long merchantId;
|
||||
|
||||
/**
|
||||
* 部门id
|
||||
*/
|
||||
@Schema(description="部门id")
|
||||
/**
|
||||
* 部门id
|
||||
*/
|
||||
@Schema(description = "部门id")
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Schema(description="创建人")
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Schema(description = "创建人")
|
||||
private String createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Schema(description="创建时间")
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
@Schema(description="更新人")
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
@Schema(description = "更新人")
|
||||
private String updateBy;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
@Schema(description="更新时间")
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/**
|
||||
* 租户编号
|
||||
*/
|
||||
@Schema(description="租户编号")
|
||||
/**
|
||||
* 租户编号
|
||||
*/
|
||||
@Schema(description = "租户编号")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description="是否显示")
|
||||
private String isShow;
|
||||
@Schema(description = "是否显示")
|
||||
private String isShow;
|
||||
|
||||
@Schema(description = "是否涨价")
|
||||
private String isPriceIncreased;
|
||||
|
||||
@Schema(description = "大于等于的价格")
|
||||
@TableField(exist = false)
|
||||
private Long gePrice;
|
||||
}
|
||||
+13
@@ -1,7 +1,10 @@
|
||||
package com.bsd.pjcloud.canteen.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Data
|
||||
public class ADCanteenDailyMenuListVO {
|
||||
/**
|
||||
@@ -9,6 +12,11 @@ public class ADCanteenDailyMenuListVO {
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 菜谱供应日期,yyyy-MM-dd;收银终端多日同步时以此归属到具体日期
|
||||
*/
|
||||
private LocalDate saleDate;
|
||||
|
||||
/**
|
||||
* spuid
|
||||
*/
|
||||
@@ -44,4 +52,9 @@ public class ADCanteenDailyMenuListVO {
|
||||
* 排序
|
||||
*/
|
||||
private Integer sort;
|
||||
|
||||
/**
|
||||
* 是否涨价
|
||||
*/
|
||||
private String isPriceIncreased;
|
||||
}
|
||||
+13
-12
@@ -15,71 +15,72 @@ public class ADCanteenSpuListVO {
|
||||
/**
|
||||
* 商品 SPU 编号,自增
|
||||
*/
|
||||
@Schema(description="商品 SPU 编号,自增")
|
||||
@Schema(description = "商品 SPU 编号,自增")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 商品名称
|
||||
*/
|
||||
@Schema(description="商品名称")
|
||||
@Schema(description = "商品名称")
|
||||
private String name;
|
||||
|
||||
|
||||
/**
|
||||
* 商品分类编号
|
||||
*/
|
||||
@Schema(description="商品分类编号")
|
||||
@Schema(description = "商品分类编号")
|
||||
private Long categoryId;
|
||||
|
||||
/**
|
||||
* 商品封面图
|
||||
*/
|
||||
@Schema(description="商品封面图")
|
||||
@Schema(description = "商品封面图")
|
||||
private String picUrl;
|
||||
|
||||
|
||||
/**
|
||||
* 商品价格,单位使用:分
|
||||
*/
|
||||
@Schema(description="商品价格,单位使用:分")
|
||||
@Schema(description = "商品价格,单位使用:分")
|
||||
private Integer price;
|
||||
|
||||
|
||||
/**
|
||||
* 市场价,单位使用:分
|
||||
*/
|
||||
@Schema(description="市场价,单位使用:分")
|
||||
@Schema(description = "市场价,单位使用:分")
|
||||
private Integer marketPrice;
|
||||
|
||||
|
||||
/**
|
||||
* 库存
|
||||
*/
|
||||
@Schema(description="库存")
|
||||
@Schema(description = "库存")
|
||||
private Integer stock;
|
||||
|
||||
|
||||
/**
|
||||
* 商品销量
|
||||
*/
|
||||
@Schema(description="商品销量")
|
||||
@Schema(description = "商品销量")
|
||||
private Integer salesCount;
|
||||
|
||||
|
||||
/**
|
||||
* 商户编号
|
||||
*/
|
||||
@Schema(description="商户编号")
|
||||
@Schema(description = "商户编号")
|
||||
private Long merchantId;
|
||||
|
||||
/**
|
||||
* 部门id
|
||||
*/
|
||||
@Schema(description="部门id")
|
||||
@Schema(description = "部门id")
|
||||
private Long deptId;
|
||||
|
||||
@Schema(description = "是否涨价")
|
||||
private String isPriceIncreased;
|
||||
|
||||
|
||||
@Schema(description="餐段类型")
|
||||
@Schema(description = "餐段类型")
|
||||
private String categoryType;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.bsd.pjcloud.canteen.vo.app;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "套餐组成项(App)")
|
||||
public class OnlineSpuComboItemVO {
|
||||
|
||||
@Schema(description = "组成菜品 SPU id")
|
||||
private Long childSpuId;
|
||||
|
||||
@Schema(description = "组成菜品名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "组成菜品封面")
|
||||
private String picUrl;
|
||||
|
||||
@Schema(description = "份数")
|
||||
private Integer quantity;
|
||||
|
||||
@Schema(description = "排序")
|
||||
private Integer sortOrder;
|
||||
|
||||
@Schema(description = "组成菜品单价(分,仅展示)")
|
||||
private Integer price;
|
||||
}
|
||||
+15
@@ -10,6 +10,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.canteen.dto.CanteenDailyMenuCVDTO;
|
||||
import com.bsd.pjcloud.canteen.dto.DailyMenuAll;
|
||||
import com.bsd.pjcloud.canteen.dto.DailyMenuDateRangeQueryDTO;
|
||||
import com.bsd.pjcloud.canteen.dto.app.DailyMenuDTO;
|
||||
import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity;
|
||||
import com.bsd.pjcloud.canteen.service.CanteenSpuService;
|
||||
@@ -198,6 +199,20 @@ public class CanteenDailyMenuController {
|
||||
return R.ok(canteenDailyMenuService.getCanteenMenuListScope(day, hourType, categoryId,merchantId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按闭区间(含起止两天)查询收银终端多日菜谱,字段与单日接口一致,
|
||||
* 每条记录额外携带供应日期 saleDate,便于终端按日期落库。
|
||||
*
|
||||
* @param request 日期范围、可选筛选条件和收银终端所属商户
|
||||
* @return 指定日期范围内的每日菜谱列表;无数据时返回空列表
|
||||
*/
|
||||
@PostMapping("/ad/getCanteenListByDateRange")
|
||||
@SysLog("按日期范围查询每日菜品列表")
|
||||
public R<List<ADCanteenDailyMenuListVO>> getCanteenListByDateRange(
|
||||
@Validated @RequestBody DailyMenuDateRangeQueryDTO request) {
|
||||
return R.ok(canteenDailyMenuService.getCanteenMenuListByDateRange(request));
|
||||
}
|
||||
|
||||
// --------------APP---------------
|
||||
|
||||
|
||||
|
||||
+11
@@ -1,6 +1,7 @@
|
||||
package com.bsd.pjcloud.canteen.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.canteen.dto.DailyMenuDateRangeQueryDTO;
|
||||
import com.bsd.pjcloud.canteen.dto.app.DailyMenuDTO;
|
||||
import com.bsd.pjcloud.canteen.vo.ADCanteenDailyMenuListVO;
|
||||
import com.bsd.pjcloud.canteen.vo.DailyMenuVO;
|
||||
@@ -31,6 +32,16 @@ public interface CanteenDailyMenuMapper extends PjcloudBaseMapper<CanteenDailyMe
|
||||
List<ADCanteenDailyMenuListVO> getCanteenMenuList(@Param("day") String day, @Param("hourType") Integer hourType,
|
||||
@Param("categoryId") Long categoryId, @Param("merchantId") Long merchantId,DataScope dataScope);
|
||||
|
||||
/**
|
||||
* 按闭区间查询收银终端多日菜谱展示数据,字段与单日查询保持一致。
|
||||
*
|
||||
* @param request 日期范围、可选筛选条件和商户 ID
|
||||
* @param dataScope 当前数据权限范围
|
||||
* @return 按日期、餐段和商品排序的菜谱列表
|
||||
*/
|
||||
List<ADCanteenDailyMenuListVO> getCanteenMenuListByDateRange(@Param("request") DailyMenuDateRangeQueryDTO request,
|
||||
DataScope dataScope);
|
||||
|
||||
|
||||
|
||||
Page<DailyMenuAppVO> selectTimeFoodPage(@Param("page")Page page, DailyMenuDTO dailyMenuDTO);
|
||||
|
||||
+10
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.bsd.pjcloud.canteen.dto.CanteenDailyMenuCVDTO;
|
||||
import com.bsd.pjcloud.canteen.dto.DailyMenuAll;
|
||||
import com.bsd.pjcloud.canteen.dto.DailyMenuDateRangeQueryDTO;
|
||||
import com.bsd.pjcloud.canteen.dto.app.DailyMenuDTO;
|
||||
import com.bsd.pjcloud.canteen.entity.CanteenDailyMenuEntity;
|
||||
import com.bsd.pjcloud.canteen.vo.ADCanteenDailyMenuListVO;
|
||||
@@ -51,6 +52,15 @@ public interface CanteenDailyMenuService extends IService<CanteenDailyMenuEntity
|
||||
*/
|
||||
List<ADCanteenDailyMenuListVO> getCanteenMenuListScope(String day, Integer hourType, Long categoryId, Long merchantId);
|
||||
|
||||
/**
|
||||
* 按闭区间查询收银终端多日菜谱,返回字段与单日查询保持一致,
|
||||
* 每条记录额外携带供应日期,供终端按日期分桶落库。
|
||||
*
|
||||
* @param request 日期范围、可选筛选条件和收银终端所属商户
|
||||
* @return 按日期、餐段和商品排序的菜谱列表
|
||||
*/
|
||||
List<ADCanteenDailyMenuListVO> getCanteenMenuListByDateRange(DailyMenuDateRangeQueryDTO request);
|
||||
|
||||
Page selectTimeFoodPage(Page page, DailyMenuDTO dailyMenuDTO);
|
||||
|
||||
|
||||
|
||||
+21
@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bsd.pjcloud.canteen.dto.CanteenDailyMenuCVDTO;
|
||||
import com.bsd.pjcloud.canteen.dto.DailyMenuAll;
|
||||
import com.bsd.pjcloud.canteen.dto.DailyMenuDateRangeQueryDTO;
|
||||
import com.bsd.pjcloud.canteen.dto.app.DailyMenuDTO;
|
||||
import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity;
|
||||
import com.bsd.pjcloud.canteen.mapper.CanteenSpuMapper;
|
||||
@@ -142,6 +143,26 @@ public class CanteenDailyMenuServiceImpl extends ServiceImpl<CanteenDailyMenuMap
|
||||
return baseMapper.getCanteenMenuList(day, hourType, categoryId,merchantId, DataScope.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 按闭区间查询收银终端多日菜谱。
|
||||
*
|
||||
* @param request 日期范围、可选筛选条件和收银终端所属商户
|
||||
* @return 按日期、餐段和商品排序升序排列的菜谱列表
|
||||
*/
|
||||
@Override
|
||||
public List<ADCanteenDailyMenuListVO> getCanteenMenuListByDateRange(DailyMenuDateRangeQueryDTO request) {
|
||||
if (request == null || request.getStartDate() == null || request.getEndDate() == null) {
|
||||
throw new IllegalArgumentException("菜谱开始日期与结束日期不能为空");
|
||||
}
|
||||
if (request.getMerchantId() == null) {
|
||||
throw new IllegalArgumentException("每日菜谱商户不能为空");
|
||||
}
|
||||
if (request.getEndDate().isBefore(request.getStartDate())) {
|
||||
throw new IllegalArgumentException("菜谱结束日期不能早于开始日期");
|
||||
}
|
||||
return baseMapper.getCanteenMenuListByDateRange(request, DataScope.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page selectTimeFoodPage(Page page, DailyMenuDTO dailyMenuDTO) {
|
||||
Page<DailyMenuAppVO> pageData = baseMapper.selectTimeFoodPage(page, dailyMenuDTO);
|
||||
|
||||
+19
-13
@@ -1,5 +1,6 @@
|
||||
package com.bsd.pjcloud.canteen.service.impl;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
@@ -46,6 +47,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
private final CanteenCategoryMapper canteenCategoryMapper;
|
||||
private final CanteenStockMapper canteenStockMapper;
|
||||
private final CanteenOnlineSpuMapper canteenOnlineSpuMapper;
|
||||
|
||||
@Override
|
||||
public Object selectFoodList(LambdaQueryWrapper<CanteenSpuEntity> wrapper) {
|
||||
return baseMapper.selectListByScope(wrapper, DataScope.of());
|
||||
@@ -59,11 +61,11 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
// 查询当前库存
|
||||
CanteenSpuEntity spu = baseMapper.selectById(item.getId());
|
||||
if (spu == null) {
|
||||
R.failed(spu.getName()+"商品不存在");
|
||||
R.failed(spu.getName() + "商品不存在");
|
||||
}
|
||||
// 检查库存是否足够
|
||||
if (item.getIncrCount() < 0 && Math.abs(item.getIncrCount()) > spu.getStock()) {
|
||||
R.failed(spu.getName()+"库存不足,无法完成操作");
|
||||
R.failed(spu.getName() + "库存不足,无法完成操作");
|
||||
}
|
||||
int i = baseMapper.updateStock(item.getId(), item.getIncrCount());
|
||||
// if (item.getIncrCount() <= 0) {
|
||||
@@ -89,7 +91,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
Map<Long, List<CanteenCategoryEntity>> listMap = canteenCategoryEntities.stream().collect(Collectors.groupingBy(CanteenCategoryEntity::getId));
|
||||
|
||||
bean.forEach(item -> {
|
||||
if (null!=listMap.get(item.getCategoryId()).get(0).getCategoryType()){
|
||||
if (null != listMap.get(item.getCategoryId()).get(0).getCategoryType()) {
|
||||
item.setCategoryType(listMap.get(item.getCategoryId()).get(0).getCategoryType());
|
||||
}
|
||||
});
|
||||
@@ -127,7 +129,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
LambdaQueryWrapper<CanteenCategoryEntity> canteenCategoryEntityLambdaQueryWrapper = new LambdaQueryWrapper<>();
|
||||
canteenCategoryEntityLambdaQueryWrapper.like(CanteenCategoryEntity::getCategoryType, canteenDailySpuDTO.getHourType());
|
||||
List<CanteenCategoryEntity> canteenCategoryEntities = canteenCategoryMapper.selectList(canteenCategoryEntityLambdaQueryWrapper);
|
||||
List<Long> caIds = canteenCategoryEntities.stream().map(CanteenCategoryEntity::getId).collect(Collectors.toList());
|
||||
List<Long> caIds = canteenCategoryEntities.stream().map(CanteenCategoryEntity::getId).collect(Collectors.toList());
|
||||
|
||||
wrapper.in(!CollectionUtils.isEmpty(caIds), CanteenSpuEntity::getCategoryId, caIds);
|
||||
// wrapper.orderByAsc(CanteenSpuEntity::getSort);
|
||||
@@ -164,32 +166,32 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
// 查询SPU分页数据
|
||||
Page<CanteenSpuEntity> pageData = baseMapper.selectPageByScope(page, wrapper, DataScope.of());
|
||||
List<CanteenSpuEntity> records = pageData.getRecords();
|
||||
|
||||
|
||||
// 构建返回的VO列表
|
||||
List<CanteenSpuStockVO> resultList = new ArrayList<>();
|
||||
|
||||
|
||||
if (!CollectionUtils.isEmpty(records)) {
|
||||
// 获取所有SPU ID
|
||||
List<Long> spuIds = records.stream()
|
||||
.map(CanteenSpuEntity::getId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
|
||||
// 查询对应的库存信息
|
||||
LambdaQueryWrapper<CanteenStockEntity> stockWrapper = Wrappers.lambdaQuery();
|
||||
stockWrapper.in(CanteenStockEntity::getItemId, spuIds);
|
||||
stockWrapper.eq(CanteenStockEntity::getItemType, "2"); // 假设"成品"表示菜品的类型
|
||||
List<CanteenStockEntity> stockList = canteenStockMapper.selectList(stockWrapper);
|
||||
|
||||
|
||||
// 将库存信息映射到SPU ID
|
||||
Map<Long, CanteenStockEntity> stockMap = stockList.stream()
|
||||
.collect(Collectors.toMap(CanteenStockEntity::getItemId, stock -> stock, (v1, v2) -> v1));
|
||||
|
||||
|
||||
// 构建返回结果
|
||||
for (CanteenSpuEntity spu : records) {
|
||||
CanteenSpuStockVO vo = new CanteenSpuStockVO();
|
||||
vo.setName(spu.getName());
|
||||
vo.setPicUrl(spu.getPicUrl());
|
||||
|
||||
|
||||
// 设置库存信息
|
||||
CanteenStockEntity stock = stockMap.get(spu.getId());
|
||||
if (stock != null) {
|
||||
@@ -205,11 +207,11 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
vo.setInventoryUpperLimit(0);
|
||||
vo.setInventoryLowerLimit(0);
|
||||
}
|
||||
|
||||
|
||||
resultList.add(vo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 构建分页返回对象
|
||||
Page<CanteenSpuStockVO> resultPage = new Page<>();
|
||||
resultPage.setRecords(resultList);
|
||||
@@ -217,7 +219,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
resultPage.setSize(pageData.getSize());
|
||||
resultPage.setCurrent(pageData.getCurrent());
|
||||
resultPage.setPages(pageData.getPages());
|
||||
|
||||
|
||||
return resultPage;
|
||||
}
|
||||
|
||||
@@ -226,6 +228,10 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
LambdaQueryWrapper<CanteenSpuEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.like(StrUtil.isNotBlank(canteenSpu.getName()), CanteenSpuEntity::getName, canteenSpu.getName());
|
||||
wrapper.eq(Objects.nonNull(canteenSpu.getCategoryId()), CanteenSpuEntity::getCategoryId, canteenSpu.getCategoryId());
|
||||
wrapper.eq(Objects.nonNull(canteenSpu.getIsPriceIncreased()), CanteenSpuEntity::getIsPriceIncreased, canteenSpu.getIsPriceIncreased());
|
||||
if (ObjectUtil.isNotEmpty(canteenSpu.getGePrice()) && canteenSpu.getGePrice() > 0) {
|
||||
wrapper.ge(CanteenSpuEntity::getPrice, canteenSpu.getGePrice() * 100);
|
||||
}
|
||||
wrapper.orderByAsc(CanteenSpuEntity::getSort).orderByDesc(CanteenSpuEntity::getId);
|
||||
|
||||
LambdaQueryWrapper<CanteenOnlineSpuEntity> canteenOnlineSpuEntityLambdaQueryWrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
@@ -6,10 +6,10 @@ spring:
|
||||
name: @artifactId@
|
||||
cloud:
|
||||
nacos:
|
||||
username: @nacos.username@
|
||||
password: @nacos.password@
|
||||
username: nacos
|
||||
password: Aa@bsd.321
|
||||
discovery:
|
||||
server-addr: ${NACOS_HOST:pjcloud-register}:${NACOS_PORT:8848}
|
||||
server-addr: ${NACOS_HOST:pjcloud-register}:${NACOS_PORT:18849}
|
||||
config:
|
||||
server-addr: ${spring.cloud.nacos.discovery.server-addr}
|
||||
config:
|
||||
|
||||
+48
-8
@@ -19,6 +19,7 @@
|
||||
|
||||
<resultMap id="adCanteenDailyMenuListVO" type="com.bsd.pjcloud.canteen.vo.ADCanteenDailyMenuListVO">
|
||||
<id property="id" column="id"/>
|
||||
<result property="saleDate" column="start_time"/>
|
||||
<result property="name" column="name"/>
|
||||
<result property="spuId" column="spu_id"/>
|
||||
<result property="hourType" column="hour_type"/>
|
||||
@@ -28,34 +29,40 @@
|
||||
<result property="stock" column="stock"/>
|
||||
<result property="sort" column="sort"/>
|
||||
<result property="hourType" column="hour_type"/>
|
||||
<result property="isPriceIncreased" column="is_price_increased"/>
|
||||
</resultMap>
|
||||
<select id="selectTimeFood" resultType="com.bsd.pjcloud.canteen.vo.DailyMenuVO">
|
||||
SELECT
|
||||
DISTINCT(sp.id),
|
||||
t.id as tid,
|
||||
<select id="selectTimeFood"
|
||||
resultType="com.bsd.pjcloud.canteen.vo.DailyMenuVO">
|
||||
SELECT DISTINCT
|
||||
sp.id,
|
||||
t.id AS tid,
|
||||
sp.name,
|
||||
sp.pic_url,
|
||||
t.hour_type,
|
||||
sp.price,
|
||||
sp.category_id
|
||||
sp.category_id,
|
||||
sp.sort
|
||||
FROM canteen_daily_menu t
|
||||
LEFT JOIN canteen_spu sp
|
||||
on t.spu_id=sp.id
|
||||
ON t.spu_id = sp.id
|
||||
<where>
|
||||
AND t.merchant_id = #{merchantId}
|
||||
|
||||
<if test="categoryId != null">
|
||||
AND sp.category_id = #{categoryId}
|
||||
</if>
|
||||
|
||||
AND sp.del_flag = 0
|
||||
AND t.start_time = #{time}
|
||||
And t.del_flag = 0
|
||||
AND t.del_flag = 0
|
||||
</where>
|
||||
order by sp.sort asc
|
||||
ORDER BY sp.sort ASC
|
||||
</select>
|
||||
<select id="getCanteenMenuList" resultMap="adCanteenDailyMenuListVO">
|
||||
SELECT
|
||||
sp.id as spu_id,
|
||||
t.id,
|
||||
t.start_time,
|
||||
sp.name,
|
||||
sp.pic_url,
|
||||
t.hour_type,
|
||||
@@ -63,6 +70,7 @@
|
||||
sp.stock,
|
||||
sp.category_id,
|
||||
sp.sort,
|
||||
sp.is_price_increased,
|
||||
t.dept_id,
|
||||
t.create_by
|
||||
FROM canteen_daily_menu t
|
||||
@@ -84,6 +92,38 @@
|
||||
</if>
|
||||
</where>
|
||||
</select>
|
||||
<select id="getCanteenMenuListByDateRange" resultMap="adCanteenDailyMenuListVO">
|
||||
SELECT
|
||||
sp.id as spu_id,
|
||||
t.id,
|
||||
t.start_time,
|
||||
sp.name,
|
||||
sp.pic_url,
|
||||
t.hour_type,
|
||||
sp.price,
|
||||
sp.stock,
|
||||
sp.category_id,
|
||||
sp.sort,
|
||||
sp.is_price_increased,
|
||||
<!-- 数据权限拦截器会在当前查询外层按部门和创建人过滤,必须在临时结果集中透传这两个字段。 -->
|
||||
t.dept_id,
|
||||
t.create_by
|
||||
FROM canteen_daily_menu t
|
||||
LEFT JOIN canteen_spu sp
|
||||
on t.spu_id=sp.id
|
||||
<where>
|
||||
sp.del_flag = 0 and t.del_flag = 0
|
||||
AND t.start_time BETWEEN #{request.startDate} AND #{request.endDate}
|
||||
<if test="request.categoryId != null">
|
||||
AND sp.category_id = #{request.categoryId}
|
||||
</if>
|
||||
<if test="request.hourType != null">
|
||||
AND t.hour_type = #{request.hourType}
|
||||
</if>
|
||||
AND t.merchant_id = #{request.merchantId}
|
||||
</where>
|
||||
order by t.start_time asc, t.hour_type asc, sp.sort asc
|
||||
</select>
|
||||
<select id="selectTimeFoodPage" resultType="com.bsd.pjcloud.canteen.vo.app.DailyMenuAppVO"
|
||||
parameterType="com.bsd.pjcloud.canteen.dto.app.DailyMenuDTO">
|
||||
SELECT m.id,
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# 通用异步任务模块
|
||||
|
||||
`pjcloud-common-async-task` 提供可由各业务模块复用的异步事务执行、Redis 进度保存和统一进度查询接口。
|
||||
|
||||
## 接入方式
|
||||
|
||||
业务模块增加依赖后,Spring Boot 会自动装配 `AsyncTaskExecutorService`、`AsyncTaskProgressService` 和查询接口:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.bsd</groupId>
|
||||
<artifactId>pjcloud-common-async-task</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
提交任务时由业务方指定稳定的 Redis 业务命名空间、展示名称和总数。接口同步返回初始进度,业务动作进入公共模块的受控专用线程池并在单个事务中执行:
|
||||
|
||||
```java
|
||||
AsyncTaskProgressVO progress = asyncTaskExecutorService.submit(
|
||||
"user:import:teacher", "老师用户导入", users.size(),
|
||||
reporter -> users.forEach(user -> {
|
||||
importUser(user);
|
||||
reporter.advance("正在导入:" + user.getName());
|
||||
}));
|
||||
```
|
||||
|
||||
导出等需要在成功终态返回结果文件的任务使用兼容扩展入口,原有无结果任务的 `submit` 契约保持不变:
|
||||
|
||||
```java
|
||||
AsyncTaskProgressVO progress = asyncTaskExecutorService.submitWithResult(
|
||||
"user:export", "用户信息导出", 3, reporter -> {
|
||||
String fileUrl = exportAndUpload(reporter);
|
||||
return AsyncTaskResult.file(fileUrl, "用户信息.xlsx");
|
||||
});
|
||||
```
|
||||
|
||||
查询数据本身耗时且提交接口需要立即返回时,可以先用大于零的占位总数创建任务,再在异步线程查询完成后更新真实总数:
|
||||
|
||||
```java
|
||||
AsyncTaskProgressVO progress = asyncTaskExecutorService.submitWithResult(
|
||||
"user:export", "用户信息导出", 1, reporter -> {
|
||||
List<UserVO> users = queryUsers();
|
||||
reporter.updateTotal(users.size(), "已查询 " + users.size() + " 位用户");
|
||||
return exportUsers(users, reporter);
|
||||
});
|
||||
```
|
||||
|
||||
调用端统一轮询:
|
||||
|
||||
```text
|
||||
GET /async-task/progress?redisKey={业务命名空间}&taskId={任务ID}
|
||||
```
|
||||
|
||||
## Redis 租户 Key 约定
|
||||
|
||||
项目 Redis 基础设施会根据当前租户上下文自动为 Redis Key 增加租户前缀。业务代码只负责定义业务命名空间和业务唯一标识,**不需要也不得再次手工拼接 `tenantId`**。
|
||||
|
||||
正确的业务 Key:
|
||||
|
||||
```text
|
||||
async:task:progress:user:import:teacher:{userId}:{taskId}
|
||||
```
|
||||
|
||||
错误的业务 Key:
|
||||
|
||||
```text
|
||||
async:task:progress:user:import:teacher:{tenantId}:{userId}:{taskId}
|
||||
```
|
||||
|
||||
错误示例会导致租户维度重复出现在最终 Redis Key 中,并可能造成同一业务在不同调用方式下无法读取彼此写入的数据。
|
||||
|
||||
“业务 Key 不拼接租户 ID”不代表异步线程不需要租户上下文。异步任务在访问 Redis、数据库或其他租户资源前,仍需恢复 `TenantContextHolder`,并在任务结束后清理上下文。本模块执行器已经统一处理这一步,业务动作不应重复设置租户上下文。
|
||||
|
||||
## 事务与进度语义
|
||||
|
||||
- 整个业务动作由一个 Spring 事务包裹,业务抛出异常时整体回滚。
|
||||
- `completed` 表示当前执行位置;失败并回滚时不代表最终成功写入数量。
|
||||
- Redis 进度默认保留 24 小时,供页面刷新或短时重连后继续查询。
|
||||
- 任务进度按当前登录用户隔离,调用端不能查询其他用户创建的任务。
|
||||
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.bsd</groupId>
|
||||
<artifactId>pjcloud-common</artifactId>
|
||||
<version>5.7.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>pjcloud-common-async-task</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<description>pjcloud 通用异步任务与 Redis 进度管理</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.bsd</groupId>
|
||||
<artifactId>pjcloud-common-excel</artifactId>
|
||||
</dependency>
|
||||
<!--统一响应、线程池及基础类型-->
|
||||
<dependency>
|
||||
<groupId>com.bsd</groupId>
|
||||
<artifactId>pjcloud-common-core</artifactId>
|
||||
</dependency>
|
||||
<!--Redis 模板与异步线程所需的租户上下文-->
|
||||
<dependency>
|
||||
<groupId>com.bsd</groupId>
|
||||
<artifactId>pjcloud-common-data</artifactId>
|
||||
</dependency>
|
||||
<!--登录用户、安全上下文及非 Web Feign Token 传递-->
|
||||
<dependency>
|
||||
<groupId>com.bsd</groupId>
|
||||
<artifactId>pjcloud-common-security</artifactId>
|
||||
</dependency>
|
||||
<!--通用异步任务事务边界-->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-tx</artifactId>
|
||||
</dependency>
|
||||
<!--通用进度查询接口-->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webmvc</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package com.bsd.pjcloud.common.asynctask.config;
|
||||
|
||||
import com.bsd.pjcloud.common.asynctask.service.AsyncTaskExecutorService;
|
||||
import com.bsd.pjcloud.common.asynctask.service.AsyncTaskProgressService;
|
||||
import com.bsd.pjcloud.common.asynctask.service.impl.DefaultAsyncTaskExecutorService;
|
||||
import com.bsd.pjcloud.common.asynctask.service.impl.DefaultAsyncTaskProgressService;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.task.TaskExecutor;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
/**
|
||||
* 通用异步任务自动配置。
|
||||
* <p>业务模块引入本公共模块后,在 Redis 和事务模板均可用时自动装配专用线程池、进度服务与事务执行器。</p>
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@AutoConfigureAfter(name = {
|
||||
"com.bsd.pjcloud.common.data.cache.RedisTemplateConfiguration",
|
||||
"org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration"
|
||||
})
|
||||
@ConditionalOnClass({RedisTemplate.class, TransactionTemplate.class})
|
||||
@ConditionalOnBean({RedisTemplate.class, TransactionTemplate.class})
|
||||
public class AsyncTaskAutoConfiguration {
|
||||
|
||||
/** 公共异步任务专用工作线程池的 Bean 名称,业务模块可按该名称替换默认线程池。 */
|
||||
public static final String ASYNC_TASK_WORKER_EXECUTOR_BEAN_NAME = "commonAsyncTaskWorkerExecutor";
|
||||
|
||||
/** 默认核心线程数,兼顾常规导入导出并发与业务服务资源占用。 */
|
||||
private static final int DEFAULT_CORE_POOL_SIZE = 2;
|
||||
|
||||
/** 默认最大线程数,限制大批量异步任务同时占用数据库连接和远程调用资源。 */
|
||||
private static final int DEFAULT_MAX_POOL_SIZE = 8;
|
||||
|
||||
/** 默认等待队列容量,超过线程与队列总容量后由执行器拒绝并把任务标记为提交失败。 */
|
||||
private static final int DEFAULT_QUEUE_CAPACITY = 200;
|
||||
|
||||
/** 服务关闭时等待正在执行任务结束的最长秒数,超过后由容器继续关闭流程。 */
|
||||
private static final int DEFAULT_SHUTDOWN_AWAIT_SECONDS = 30;
|
||||
|
||||
/** 公共异步任务线程名称前缀,便于线程转储和日志定位。 */
|
||||
private static final String DEFAULT_THREAD_NAME_PREFIX = "common-async-task-";
|
||||
|
||||
/**
|
||||
* 装配公共异步任务专用线程池,避免 Spring Boot 默认线程池因其他业务 Executor 存在而不创建。
|
||||
*
|
||||
* @return 已完成容量与优雅停机配置的工作线程池
|
||||
*/
|
||||
@Bean(name = ASYNC_TASK_WORKER_EXECUTOR_BEAN_NAME)
|
||||
@ConditionalOnMissingBean(name = ASYNC_TASK_WORKER_EXECUTOR_BEAN_NAME)
|
||||
public TaskExecutor commonAsyncTaskWorkerExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(DEFAULT_CORE_POOL_SIZE);
|
||||
executor.setMaxPoolSize(DEFAULT_MAX_POOL_SIZE);
|
||||
executor.setQueueCapacity(DEFAULT_QUEUE_CAPACITY);
|
||||
executor.setThreadNamePrefix(DEFAULT_THREAD_NAME_PREFIX);
|
||||
executor.setWaitForTasksToCompleteOnShutdown(true);
|
||||
executor.setAwaitTerminationSeconds(DEFAULT_SHUTDOWN_AWAIT_SECONDS);
|
||||
return executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 装配默认 Redis 进度服务,业务模块可通过声明同类型 Bean 完整替换默认实现。
|
||||
*
|
||||
* @param redisTemplate 项目统一配置的 Redis 操作模板
|
||||
* @return 通用异步任务 Redis 进度服务
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public AsyncTaskProgressService asyncTaskProgressService(RedisTemplate<String, Object> redisTemplate) {
|
||||
return new DefaultAsyncTaskProgressService(redisTemplate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 装配默认异步事务执行器,显式使用公共异步任务专用线程池以隔离其他业务线程池。
|
||||
*
|
||||
* @param asyncTaskWorkerExecutor 公共异步任务专用工作线程池
|
||||
* @param asyncTaskProgressService Redis 任务进度服务
|
||||
* @param transactionTemplate Spring 编程式事务模板
|
||||
* @return 通用异步事务任务执行器
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public AsyncTaskExecutorService asyncTaskExecutorService(
|
||||
@Qualifier(ASYNC_TASK_WORKER_EXECUTOR_BEAN_NAME) TaskExecutor asyncTaskWorkerExecutor,
|
||||
AsyncTaskProgressService asyncTaskProgressService, TransactionTemplate transactionTemplate) {
|
||||
return new DefaultAsyncTaskExecutorService(asyncTaskWorkerExecutor, asyncTaskProgressService, transactionTemplate);
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.bsd.pjcloud.common.asynctask.config;
|
||||
|
||||
import com.bsd.pjcloud.common.asynctask.controller.AsyncTaskProgressController;
|
||||
import com.bsd.pjcloud.common.asynctask.service.AsyncTaskProgressService;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* 通用异步任务 Servlet Web 接口自动配置。
|
||||
* <p>仅在 Web 应用已装配 Redis 进度服务时注册统一查询接口,非 Web 任务消费者不会引入无用端点。</p>
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@AutoConfigureAfter(AsyncTaskAutoConfiguration.class)
|
||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
|
||||
@ConditionalOnBean(AsyncTaskProgressService.class)
|
||||
public class AsyncTaskWebAutoConfiguration {
|
||||
|
||||
/**
|
||||
* 装配统一进度查询接口,所有业务模块共享 {@code /async-task/progress} 契约。
|
||||
*
|
||||
* @param asyncTaskProgressService Redis 任务进度服务
|
||||
* @return 通用异步任务进度控制器
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public AsyncTaskProgressController asyncTaskProgressController(AsyncTaskProgressService asyncTaskProgressService) {
|
||||
return new AsyncTaskProgressController(asyncTaskProgressService);
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.bsd.pjcloud.common.asynctask.constant;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 通用异步任务进度状态协议。
|
||||
* <p>状态编码会写入 Redis 并返回调用端,已有编码不得随意变更。</p>
|
||||
*/
|
||||
@Getter
|
||||
public enum AsyncTaskStatusEnum {
|
||||
|
||||
/** 等待后台线程开始执行,Redis 与接口编码为 {@code PENDING}。 */
|
||||
PENDING("PENDING", "等待执行"),
|
||||
|
||||
/** 后台线程正在处理任务,Redis 与接口编码为 {@code RUNNING}。 */
|
||||
RUNNING("RUNNING", "执行中"),
|
||||
|
||||
/** 任务已完整执行成功,Redis 与接口编码为 {@code SUCCESS}。 */
|
||||
SUCCESS("SUCCESS", "执行成功"),
|
||||
|
||||
/** 任务执行异常,Redis 与接口编码为 {@code FAILED}。 */
|
||||
FAILED("FAILED", "执行失败");
|
||||
|
||||
/** Redis 与调用端接口共同使用的固定状态编码。 */
|
||||
private final String status;
|
||||
|
||||
/** 当前状态未提供自定义消息时使用的中文说明。 */
|
||||
private final String description;
|
||||
|
||||
/**
|
||||
* 创建异步任务状态定义。
|
||||
*
|
||||
* @param status Redis 与调用端接口共同使用的固定状态编码
|
||||
* @param description 当前状态的默认中文说明
|
||||
*/
|
||||
AsyncTaskStatusEnum(String status, String description) {
|
||||
this.status = status;
|
||||
this.description = description;
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.bsd.pjcloud.common.asynctask.controller;
|
||||
|
||||
import com.bsd.pjcloud.common.asynctask.service.AsyncTaskProgressService;
|
||||
import com.bsd.pjcloud.common.asynctask.vo.AsyncTaskProgressVO;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 通用异步任务进度查询接口。
|
||||
* <p>任务业务 Key 按登录用户隔离,租户 ID 显式写入服务端任务键,只能查询本人创建且尚未过期的任务。</p>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/async-task")
|
||||
@Tag(description = "async-task", name = "通用异步任务进度")
|
||||
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
|
||||
public class AsyncTaskProgressController {
|
||||
|
||||
/** 通用 Redis 任务进度服务。 */
|
||||
private final AsyncTaskProgressService asyncTaskProgressService;
|
||||
|
||||
/**
|
||||
* 创建异步任务进度查询接口。
|
||||
*
|
||||
* @param asyncTaskProgressService 通用 Redis 任务进度服务
|
||||
*/
|
||||
public AsyncTaskProgressController(AsyncTaskProgressService asyncTaskProgressService) {
|
||||
this.asyncTaskProgressService = asyncTaskProgressService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前用户的异步任务进度。
|
||||
*
|
||||
* @param redisKey 业务指定的 Redis Key 命名空间
|
||||
* @param taskId 上传或任务提交接口返回的不透明任务 ID
|
||||
* @return 当前任务进度;任务不存在或已过期时返回失败响应
|
||||
*/
|
||||
@GetMapping("/progress")
|
||||
@Operation(summary = "查询异步任务进度")
|
||||
public R<AsyncTaskProgressVO> progress(@RequestParam String redisKey, @RequestParam String taskId) {
|
||||
AsyncTaskProgressVO progress = asyncTaskProgressService.get(redisKey, taskId);
|
||||
return progress == null ? R.failed("异步任务不存在或已过期") : R.ok(progress);
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.bsd.pjcloud.common.asynctask.service;
|
||||
|
||||
/**
|
||||
* 通用异步任务业务动作。
|
||||
* <p>动作在后台事务中执行,抛出异常会回滚整个动作并把 Redis 任务标记为失败。</p>
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface AsyncTaskAction {
|
||||
|
||||
/**
|
||||
* 执行具体异步业务。
|
||||
*
|
||||
* @param reporter 逐项进度报告器,业务完成一个计数项目后调用一次
|
||||
*/
|
||||
void execute(AsyncTaskProgressReporter reporter);
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.bsd.pjcloud.common.asynctask.service;
|
||||
|
||||
/** 允许展示给任务发起人的受控错误;禁止传入 SQL、Token 或底层异常文本。 */
|
||||
public class AsyncTaskException extends RuntimeException {
|
||||
/** 使用业务可读提示终止任务,不暴露底层异常。 */
|
||||
public AsyncTaskException(String message) { super(message); }
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.bsd.pjcloud.common.asynctask.service;
|
||||
|
||||
import com.bsd.pjcloud.common.asynctask.vo.AsyncTaskProgressVO;
|
||||
|
||||
/**
|
||||
* 通用异步事务任务提交服务。
|
||||
* <p>同步创建 Redis 任务后,把业务动作交给后台线程执行,并透传当前租户和登录用户上下文。</p>
|
||||
*/
|
||||
public interface AsyncTaskExecutorService {
|
||||
|
||||
/**
|
||||
* 创建并提交一个异步事务任务。
|
||||
*
|
||||
* @param redisKey 业务指定的 Redis Key 命名空间
|
||||
* @param taskName 任务展示名称
|
||||
* @param total 需要处理的总项目数
|
||||
* @param action 后台事务中执行的业务动作
|
||||
* @return 等待执行的初始任务进度,调用方可立即把任务 ID 返回客户端
|
||||
*/
|
||||
AsyncTaskProgressVO submit(String redisKey, String taskName, int total, AsyncTaskAction action);
|
||||
|
||||
/**
|
||||
* 创建并提交一个可在成功终态携带结果文件的异步事务任务。
|
||||
*
|
||||
* @param redisKey 业务指定的 Redis Key 命名空间
|
||||
* @param taskName 任务展示名称
|
||||
* @param total 需要处理的总项目数
|
||||
* @param action 后台事务中执行并返回结果文件信息的业务动作
|
||||
* @return 等待执行的初始任务进度,调用方可立即把任务 ID 返回客户端
|
||||
*/
|
||||
default AsyncTaskProgressVO submitWithResult(String redisKey, String taskName, int total, AsyncTaskResultAction action) {
|
||||
throw new UnsupportedOperationException("当前异步任务执行器不支持返回任务结果");
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.bsd.pjcloud.common.asynctask.service;
|
||||
|
||||
/**
|
||||
* 异步任务逐项进度报告器。
|
||||
* <p>业务每完成一个可计数项目后调用一次,由通用执行器递增 Redis 进度。</p>
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface AsyncTaskProgressReporter {
|
||||
|
||||
/**
|
||||
* 在后台任务取得真实数据量后更新总项目数。
|
||||
* <p>适用于查询本身耗时、必须先返回任务 ID 的导出场景;固定总数任务无需调用。</p>
|
||||
*
|
||||
* @param total 后台查询得到的真实总项目数,必须大于零
|
||||
* @param message 总数更新后的进度说明;为空时使用通用说明
|
||||
*/
|
||||
default void updateTotal(int total, String message) {
|
||||
throw new UnsupportedOperationException("当前异步任务进度报告器不支持动态更新总数");
|
||||
}
|
||||
|
||||
/**
|
||||
* 报告一个项目已处理完成。
|
||||
*
|
||||
* @param message 当前项目完成后的进度说明;为空时使用通用说明
|
||||
*/
|
||||
void advance(String message);
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package com.bsd.pjcloud.common.asynctask.service;
|
||||
|
||||
import com.bsd.pjcloud.common.asynctask.vo.AsyncTaskProgressVO;
|
||||
|
||||
/**
|
||||
* 通用异步任务 Redis 进度服务。
|
||||
* <p>业务 Key 按当前用户隔离,租户 ID 显式写入服务端任务键;调用方只提供业务命名空间和任务 ID。</p>
|
||||
*/
|
||||
public interface AsyncTaskProgressService {
|
||||
|
||||
/**
|
||||
* 创建等待执行的异步任务并写入 Redis。
|
||||
*
|
||||
* @param redisKey 业务指定的 Redis Key 命名空间
|
||||
* @param taskName 任务展示名称
|
||||
* @param total 需要处理的总项目数,必须大于零
|
||||
* @return 已写入 Redis 的初始任务进度
|
||||
*/
|
||||
AsyncTaskProgressVO create(String redisKey, String taskName, int total);
|
||||
|
||||
/**
|
||||
* 将任务标记为执行中。
|
||||
*
|
||||
* @param redisKey 业务指定的 Redis Key 命名空间
|
||||
* @param taskId 不透明任务 ID
|
||||
*/
|
||||
void start(String redisKey, String taskId);
|
||||
|
||||
/**
|
||||
* 在任务开始后更新真实总项目数并重新计算完成百分比。
|
||||
*
|
||||
* @param redisKey 业务指定的 Redis Key 命名空间
|
||||
* @param taskId 不透明任务 ID
|
||||
* @param total 后台查询得到的真实总项目数,必须大于零
|
||||
* @param message 总数更新后的进度说明
|
||||
*/
|
||||
default void updateTotal(String redisKey, String taskId, int total, String message) {
|
||||
throw new UnsupportedOperationException("当前异步任务进度服务不支持动态更新总数");
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务执行进度。
|
||||
*
|
||||
* @param redisKey 业务指定的 Redis Key 命名空间
|
||||
* @param taskId 不透明任务 ID
|
||||
* @param completed 当前已处理项目数
|
||||
* @param message 当前进度说明
|
||||
*/
|
||||
void update(String redisKey, String taskId, int completed, String message);
|
||||
|
||||
/**
|
||||
* 将任务标记为成功并把进度置为 100%。
|
||||
*
|
||||
* @param redisKey 业务指定的 Redis Key 命名空间
|
||||
* @param taskId 不透明任务 ID
|
||||
* @param message 最终成功说明
|
||||
*/
|
||||
void success(String redisKey, String taskId, String message);
|
||||
|
||||
/**
|
||||
* 将任务标记为成功,并在实现支持时保存业务动作返回的结果文件信息。
|
||||
* <p>默认实现退化为原有成功状态写入,保证既有自定义进度服务无需同步修改;
|
||||
* 需要向调用端返回附件的实现应覆写本方法。</p>
|
||||
*
|
||||
* @param redisKey 业务指定的 Redis Key 命名空间
|
||||
* @param taskId 不透明任务 ID
|
||||
* @param message 最终成功说明
|
||||
* @param result 业务动作返回的可选结果文件信息
|
||||
*/
|
||||
default void success(String redisKey, String taskId, String message, AsyncTaskResult result) {
|
||||
success(redisKey, taskId, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将任务标记为失败并保留失败前执行进度。
|
||||
*
|
||||
* @param redisKey 业务指定的 Redis Key 命名空间
|
||||
* @param taskId 不透明任务 ID
|
||||
* @param message 对调用端安全的失败说明
|
||||
*/
|
||||
void fail(String redisKey, String taskId, String message);
|
||||
|
||||
/**
|
||||
* 查询当前用户创建的异步任务进度。
|
||||
*
|
||||
* @param redisKey 业务指定的 Redis Key 命名空间
|
||||
* @param taskId 不透明任务 ID
|
||||
* @return 任务进度;任务不存在、已过期或不属于当前用户时返回 {@code null}
|
||||
*/
|
||||
AsyncTaskProgressVO get(String redisKey, String taskId);
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.bsd.pjcloud.common.asynctask.service;
|
||||
|
||||
/**
|
||||
* 通用异步任务成功结果。
|
||||
* <p>导入等无附件任务可返回 {@link #empty()},导出任务可返回下载地址和文件名称;结果只会在任务成功终态写入进度对象。</p>
|
||||
*
|
||||
* @param fileUrl 任务生成文件的下载地址;无结果文件时为 {@code null}
|
||||
* @param fileName 下载时使用的业务文件名称;无结果文件时为 {@code null}
|
||||
*/
|
||||
public record AsyncTaskResult(String fileUrl, String fileName) {
|
||||
|
||||
/**
|
||||
* 创建通用异步任务成功结果。
|
||||
*
|
||||
* @param fileUrl 任务生成文件的下载地址;无结果文件时为 {@code null}
|
||||
* @param fileName 下载时使用的业务文件名称;无结果文件时为 {@code null}
|
||||
*/
|
||||
public AsyncTaskResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建不携带结果文件的成功结果。
|
||||
*
|
||||
* @return 无文件地址和名称的任务结果
|
||||
*/
|
||||
public static AsyncTaskResult empty() {
|
||||
return new AsyncTaskResult(null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建携带下载文件的成功结果。
|
||||
*
|
||||
* @param fileUrl 项目现有文件中心返回的文件下载地址
|
||||
* @param fileName 下载时使用的业务文件名称
|
||||
* @return 包含结果文件信息的任务结果
|
||||
*/
|
||||
public static AsyncTaskResult file(String fileUrl, String fileName) {
|
||||
return new AsyncTaskResult(fileUrl, fileName);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 976 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user