9 Commits

Author SHA1 Message Date
danaisuiyuan
9a4a5f2339 feat(dashboard): archive daily report from page data
Generate the standalone daily report HTML from the dashboard data already loaded in the H5 page, keeping the archived page visually aligned with the mobile dashboard.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 13:21:35 +08:00
danaisuiyuan
403ffe0fde feat(dashboard): add boss dashboard H5 and APIs
Implement the mobile dashboard frontend, admin overview APIs, report archive export, and local dev proxy so the boss dashboard can run against real backend data while preserving MSW demos.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 13:07:55 +08:00
danaisuiyuan
693c66c258 fix(integral): 防止个人奖金重复生成积分
将个人奖金转积分流程改为先写唯一流水再加积分,并用 wa_selfbonus_logid 唯一索引兜底多入口并发场景;同时补充历史重复数据修复与索引落地 SQL 脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 13:07:55 +08:00
apple
f43950eabf fix(integral-external): 外部用户列表用户 ID 筛选默认可空
Replace el-input-number (min clamped to 1) with text uidStr and only
send uid when a valid positive integer is entered.

Made-with: Cursor
2026-04-27 13:44:24 +08:00
apple
708bf9af48 feat(sxsy80): 外部用户 UID 筛选与积分明细展示
User list API accepts uid; admin external pages tighten filters and
integral log maps self-bonus rows via wa_selfbonus_log for display.

Made-with: Cursor
2026-04-27 13:30:05 +08:00
apple
5c4450c417 docs(sxsy80): 迁移文档、清理脚本与 wa_merchandise 提取工具
补充数据迁移说明与 cleanup SQL;增加从 dump 提取保留 id 与生成 INSERT 的脚本及产物。

Made-with: Cursor
2026-04-27 12:13:19 +08:00
apple
901bf6f500 docs(czrt6): 池州瑞棠数据迁移说明、dump 与可执行 SQL
新增从 dump 生成的 INSERT 脚本、按主键预删后执行的 pymysql 运行器,并在文档中记录执行方式与结果摘要。

Made-with: Cursor
2026-04-27 12:13:12 +08:00
apple
e7ffbbf302 docs(sxsy80): 补充文档、合同资源与数据清理脚本
- 更新前后端 sign_contract_sxsy80.pdf
- 增加 com-sxsy80 说明、数据迁移与 SQL/执行脚本
- 增加 com-xsj33 数据迁移说明与 docs 下合同源文件

Made-with: Cursor
2026-04-26 16:50:36 +08:00
apple
3c6ec4ed73 feat(sxsy80): 太原树英商贸环境与域名配置
- 新增 application-sxsy80.yml(前后端),MySQL/Redis 指向 106.14.132.80,imagePath 与 sync shop_14
- 默认 profile 切换为 sxsy80
- 合同 PDF 与落库域名 https://sxsy.cichude.com
- uni-app:积分域 sxsy-jf、抢购跳转 sxsy、静态合同路径
- backend-adminend:VUE_APP_BASE_API 指向 sxsy-jf
- 分支 sxsy80 基于 origin/czcf82

Made-with: Cursor
2026-04-26 16:37:46 +08:00
84 changed files with 14021 additions and 405 deletions

1
.gitignore vendored
View File

@@ -15,6 +15,7 @@ frontend/yarn.lock
# Backend
backend/target/
backend/**/target/
backend/**/logs/
backend/**/*.log
backend/.idea/

View File

@@ -8,8 +8,11 @@ ENV = 'development'
# VUE_APP_BASE_API = 'http://jfanyueadmin.szxingming.com'
# VUE_APP_BASE_API = 'http://jfadmin.wenjinhui.com'
# VUE_APP_BASE_API = 'http://jfadmin-bsy.bosenyuan.com'
# sxsy80 项目(太原树英商贸)
VUE_APP_BASE_API = 'https://sxsy-jf.cichude.com'
# czcf82 项目(池州春芳商贸)
VUE_APP_BASE_API = 'https://czcf-jf.uj345.com'
# VUE_APP_BASE_API = 'https://czcf-jf.uj345.com'
# hapr191 项目(淮安鹏然商贸)
# VUE_APP_BASE_API = 'http://jfadmin.hapengran.com'

View File

@@ -8,8 +8,11 @@ ENV = 'production'
# miao33 项目
# VUE_APP_BASE_API = 'http://jfadmin.xiashengjun.com'
# sxsy80 项目(太原树英商贸)
VUE_APP_BASE_API = 'https://sxsy-jf.cichude.com'
# czcf82 项目(池州春芳商贸)
VUE_APP_BASE_API = 'https://czcf-jf.uj345.com'
# VUE_APP_BASE_API = 'https://czcf-jf.uj345.com'
# hapr191 项目(淮安鹏然商贸)
# VUE_APP_BASE_API = 'http://jfadmin.hapengran.com'

View File

@@ -45,7 +45,7 @@
<div class="container mb10">
<el-form inline size="small" :model="searchForm" label-width="96px">
<el-row>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
<el-col :xs="24" :sm="12" :md="8" :lg="5" :xl="5">
<el-form-item label="用户ID">
<el-input
v-model="searchForm.uidStr"
@@ -56,7 +56,7 @@
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
<el-col :xs="24" :sm="12" :md="8" :lg="5" :xl="5">
<el-form-item label="用户名称:">
<el-input
v-model="searchForm.nickName"
@@ -67,7 +67,7 @@
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
<el-col :xs="24" :sm="12" :md="8" :lg="5" :xl="5">
<el-form-item label="手机号:">
<el-input
v-model="searchForm.phone"
@@ -78,7 +78,7 @@
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
<el-col :xs="24" :sm="12" :md="10" :lg="8" :xl="8">
<el-form-item label="时间选择:">
<el-date-picker
v-model="timeVal"
@@ -90,11 +90,12 @@
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
class="date-range-width"
@change="onchangeTime"
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
<el-col :xs="24" :sm="12" :md="6" :lg="4" :xl="4">
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
@@ -119,7 +120,6 @@
<span>{{ scope.row.nickName || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" min-width="150" show-overflow-tooltip />
<el-table-column label="积分变动" min-width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.type === 1 ? 'success' : 'danger'" size="small">
@@ -356,7 +356,10 @@ export default {
text-align: right;
}
.filter-input {
width: 180px;
width: 150px;
}
.date-range-width {
width: 350px;
}
.overview-card--all .hint-text {
display: block;

View File

@@ -5,7 +5,7 @@
<div class="container">
<el-form inline size="small" :model="userFrom" label-width="90px">
<el-row>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
<el-col :xs="24" :sm="12" :md="6" :lg="5" :xl="5">
<el-form-item label="用户搜索:">
<el-input
v-model="userFrom.keywords"
@@ -16,7 +16,18 @@
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
<el-col :xs="24" :sm="12" :md="6" :lg="5" :xl="5">
<el-form-item label="用户ID">
<el-input
v-model="userFrom.uidStr"
placeholder="可选,留空查全部"
clearable
class="selWidth"
@keyup.enter.native="seachList"
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="10" :lg="8" :xl="8">
<el-form-item label="时间选择:">
<el-date-picker
v-model="timeVal"
@@ -27,11 +38,12 @@
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
class="date-range-width"
@change="onchangeTime"
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
<el-col :xs="24" :sm="12" :md="6" :lg="5" :xl="5">
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="small" @click="seachList">搜索</el-button>
<el-button size="small" @click="handleReset">重置</el-button>
@@ -114,6 +126,7 @@ export default {
},
userFrom: {
keywords: '',
uidStr: '',
dateLimit: '',
page: 1,
limit: 15,
@@ -128,6 +141,14 @@ export default {
getList() {
this.listLoading = true;
const params = { ...this.userFrom };
delete params.uidStr;
const uidParsed =
this.userFrom.uidStr === '' || this.userFrom.uidStr == null
? null
: parseInt(String(this.userFrom.uidStr).trim(), 10);
if (uidParsed != null && !Number.isNaN(uidParsed) && uidParsed > 0) {
params.uid = uidParsed;
}
if (!params.keywords) delete params.keywords;
if (!params.dateLimit) delete params.dateLimit;
@@ -146,7 +167,7 @@ export default {
this.getList();
},
handleReset() {
this.userFrom = { keywords: '', dateLimit: '', page: 1, limit: 15 };
this.userFrom = { keywords: '', uidStr: '', dateLimit: '', page: 1, limit: 15 };
this.timeVal = [];
this.getList();
},
@@ -201,6 +222,9 @@ export default {
text-align: right;
}
.selWidth {
width: 200px;
width: 160px;
}
.date-range-width {
width: 350px;
}
</style>

View File

@@ -148,6 +148,8 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
.antMatchers("/api/admin/store/product/copy/**").permitAll()
.antMatchers("/api/admin/merchandise/select").permitAll()
.antMatchers("/api/admin/merchandise/update").permitAll()
// 老板驾驶舱独立 H5 页面接口,本机演示和报表归档使用
.antMatchers("/api/admin/dashboard/**").permitAll()
// 积分模块外部免认证只读接口(供 /integral-external/* 页面调用)
.antMatchers("/api/external/integral/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证

View File

@@ -0,0 +1,50 @@
package com.zbkj.admin.controller;
import com.zbkj.common.response.dashboard.BossDashboardResponse;
import com.zbkj.common.result.CommonResult;
import com.zbkj.service.service.BossDashboardService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.nio.charset.StandardCharsets;
/**
* 老板经营驾驶舱
*/
@Slf4j
@RestController
@RequestMapping("api/admin/dashboard")
@Api(tags = "老板经营驾驶舱")
public class BossDashboardController {
@Autowired
private BossDashboardService bossDashboardService;
@ApiOperation(value = "老板驾驶舱概览")
@RequestMapping(value = "/overview", method = RequestMethod.GET)
public CommonResult<BossDashboardResponse> overview(@RequestParam(value = "date", required = false) String date) {
return CommonResult.success(bossDashboardService.overview(date));
}
@ApiOperation(value = "生成经营日报归档 HTML")
@RequestMapping(value = "/daily-report/archive", method = RequestMethod.GET)
public ResponseEntity<byte[]> dailyReportArchive(@RequestParam(value = "date", required = false) String date) {
BossDashboardResponse overview = bossDashboardService.overview(date);
String html = bossDashboardService.dailyReportArchiveHtml(date);
String filename = "dashboard-daily-report-" + overview.getBusinessDate() + ".html";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(new MediaType("text", "html", StandardCharsets.UTF_8));
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"");
return ResponseEntity.ok().headers(headers).body(html.getBytes(StandardCharsets.UTF_8));
}
}

View File

@@ -0,0 +1,59 @@
# CRMEB 相关配置
crmeb:
captchaOn: false # 是否开启行为验证码
asyncConfig: true #是否同步config表数据到redis
server:
port: 30032
sync:
source-id: shop_14
target-mer-id: 14
spring:
datasource:
name: yangtangyoupin
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://106.14.132.80:3306/${spring.datasource.name}?useUnicode=true&serverTimezone=GMT%2B8&characterEncoding=utf8
username: yangtangyoupin
password: 5Fn8eWrbYFtAhCZw
redis:
host: 106.14.132.80 #地址
port: 6379 #端口
password: '123456'
timeout: 10000 # 连接超时时间(毫秒)
database: 2 #默认数据库
jedis:
pool:
max-active: 200 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 10 # 连接池中的最大空闲连接
min-idle: 0 # 连接池中的最小空闲连接
time-between-eviction-runs: -1 #逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1
second:
database: 2 # 微信accessToken存储库
debug: true
logging:
level:
io.swagger.*: error
com.zbjk.crmeb: debug
org.springframework.boot.autoconfigure: ERROR
config: classpath:logback-spring.xml
file:
path: ./logs
# mybatis 配置
mybatis-plus:
# 配置sql打印日志
configuration:
log-impl:
#swagger 配置
swagger:
basic:
enable: true #是否开启界面
check: false #是否打开验证
username: crmeb #访问swagger的账号
password: crmeb.com #访问swagger的密码

View File

@@ -38,7 +38,7 @@ server:
spring:
profiles:
active: czcf82
active: sxsy80
servlet:
multipart:
max-file-size: 50MB #设置单个文件大小

View File

@@ -37,6 +37,9 @@ public class UserSearchRequest implements Serializable {
@ApiModelProperty(value = "关键字")
private String keywords;
@ApiModelProperty(value = "用户uid")
private Integer uid;
@ApiModelProperty(value = "时间")
private String dateLimit;

View File

@@ -10,6 +10,7 @@ import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
@@ -51,7 +52,7 @@ public class UserIntegralRecordResponse implements Serializable {
private String title;
@ApiModelProperty(value = "积分")
private Integer integral;
private BigDecimal integral;
@ApiModelProperty(value = "剩余")
private Integer balance;

View File

@@ -0,0 +1,106 @@
package com.zbkj.common.response.dashboard;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* 老板驾驶舱响应对象
*/
@Data
@ApiModel(value = "BossDashboardResponse", description = "老板驾驶舱响应对象")
public class BossDashboardResponse {
@ApiModelProperty(value = "业务日期")
private String businessDate;
@ApiModelProperty(value = "生成时间")
private String generatedAt;
@ApiModelProperty(value = "经营摘要")
private String summary;
@ApiModelProperty(value = "核心指标")
private List<KpiMetric> kpis = new ArrayList<>();
@ApiModelProperty(value = "资金池指标")
private List<KpiMetric> fundPool = new ArrayList<>();
@ApiModelProperty(value = "今日节点快报")
private List<TodaySnapshot> snapshots = new ArrayList<>();
@ApiModelProperty(value = "近 7 天趋势")
private List<TrendPoint> trends = new ArrayList<>();
@ApiModelProperty(value = "高价值用户排行")
private List<RankItem> userRanks = new ArrayList<>();
@ApiModelProperty(value = "团队贡献排行")
private List<RankItem> teamRanks = new ArrayList<>();
@ApiModelProperty(value = "高货值未成交商品排行")
private List<RankItem> productRanks = new ArrayList<>();
@ApiModelProperty(value = "风险预警")
private List<RiskAlert> risks = new ArrayList<>();
@Data
public static class KpiMetric {
private String key;
private String title;
private Object value;
private String unit;
private String trendLabel;
private BigDecimal trendValue;
private String status = "normal";
private Boolean featured = false;
}
@Data
public static class TodaySnapshot {
private String slot;
private String title;
private String status;
private String generatedAt;
private String message;
private Integer purchaseUsers = 0;
private Integer orderCount = 0;
private BigDecimal dealAmount = BigDecimal.ZERO;
private BigDecimal paidAmount = BigDecimal.ZERO;
private Integer newMerchandiseCount = 0;
private BigDecimal selfBonusChange = BigDecimal.ZERO;
private BigDecimal shareBonusChange = BigDecimal.ZERO;
}
@Data
public static class TrendPoint {
private String date;
private BigDecimal amount = BigDecimal.ZERO;
private Integer orders = 0;
private Integer newUsers = 0;
private BigDecimal bonus = BigDecimal.ZERO;
}
@Data
public static class RankItem {
private String id;
private String name;
private BigDecimal value = BigDecimal.ZERO;
private String description;
private String badge;
}
@Data
public static class RiskAlert {
private String id;
private String level;
private String type;
private String title;
private String description;
private String discoveredAt;
}
}

View File

@@ -73,7 +73,7 @@ public class WaUserController {
FileInputStream fileInputStream = null;
try {
// 读取模板PDF文件
Resource resource = new ClassPathResource("pdf/sign_contract_czcf82.pdf");
Resource resource = new ClassPathResource("pdf/sign_contract_sxsy80.pdf");
InputStream pdfInputStream = resource.getInputStream();
document = PDDocument.load(pdfInputStream);
pdfInputStream.close();
@@ -199,7 +199,7 @@ public class WaUserController {
// user.setContract("https://anyue.szxingming.com/"+pdfResultVo.getUrl());
// user.setContract("https://xiashengjun.com/"+pdfResultVo.getUrl());
// user.setContract("https://ccd.cichude.com/"+pdfResultVo.getUrl());
user.setContract("https://czcf.uj345.com/"+pdfResultVo.getUrl());
user.setContract("https://sxsy.cichude.com/"+pdfResultVo.getUrl());
waUsersDao.updateById(user);
}
return CommonResult.success(pdfResultVo);

View File

@@ -0,0 +1,54 @@
crmeb:
imagePath: /www/wwwroot/sxsy.cichude.com/ # 太原树英商贸 服务器图片路径 斜杠结尾
asyncConfig: true #是否同步config表数据到redis
server:
port: 30031
spring:
datasource:
name: yangtangyoupin
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://106.14.132.80:3306/${spring.datasource.name}?useUnicode=true&serverTimezone=GMT%2B8&characterEncoding=utf8
username: yangtangyoupin
password: 5Fn8eWrbYFtAhCZw
redis:
host: 106.14.132.80 #地址
port: 6379 #端口
password: '123456'
timeout: 10000 # 连接超时时间(毫秒)
database: 2 #默认数据库
jedis:
pool:
max-active: 200 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 10 # 连接池中的最大空闲连接
min-idle: 0 # 连接池中的最小空闲连接
time-between-eviction-runs: -1 #逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1
second:
database: 3 # 微信accessToken存储库
debug: true
logging:
level:
io.swagger.*: error
com.zbjk.crmeb: debug
org.springframework.boot.autoconfigure: ERROR
config: classpath:logback-spring.xml
file:
path: ./logs
# mybatis 配置
mybatis-plus:
# 配置sql打印日志
configuration:
log-impl:
#swagger 配置
swagger:
basic:
enable: true #是否开启界面
check: false #是否打开验证
username: crmeb #访问swagger的账号
password: crmeb.com #访问swagger的密码

View File

@@ -32,7 +32,7 @@ server:
spring:
profiles:
active: czcf82
active: sxsy80
servlet:
multipart:
max-file-size: 50MB #设置单个文件大小

View File

@@ -0,0 +1,25 @@
package com.zbkj.service.service;
import com.zbkj.common.response.dashboard.BossDashboardResponse;
/**
* 老板经营驾驶舱服务
*/
public interface BossDashboardService {
/**
* 获取老板经营驾驶舱数据
*
* @param date 业务日期,格式 yyyy-MM-dd为空时默认上一个工作日
* @return BossDashboardResponse
*/
BossDashboardResponse overview(String date);
/**
* 生成经营日报归档 HTML
*
* @param date 业务日期,格式 yyyy-MM-dd为空时默认上一个工作日
* @return standalone HTML
*/
String dailyReportArchiveHtml(String date);
}

View File

@@ -0,0 +1,564 @@
package com.zbkj.service.service.impl;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zbkj.common.model.consignment.WaMerchandise;
import com.zbkj.common.model.consignment.WaOrder;
import com.zbkj.common.model.consignment.WaSelfbonusLog;
import com.zbkj.common.model.consignment.WaSharebonusLog;
import com.zbkj.common.model.consignment.WaUsers;
import com.zbkj.common.model.consignment.WaWithdraw;
import com.zbkj.common.response.dashboard.BossDashboardResponse;
import com.zbkj.service.dao.consignment.WaMerchandiseDao;
import com.zbkj.service.dao.consignment.WaOrderDao;
import com.zbkj.service.dao.consignment.WaSelfbonusLogDao;
import com.zbkj.service.dao.consignment.WaSharebonusLogDao;
import com.zbkj.service.dao.consignment.WaUsersDao;
import com.zbkj.service.dao.consignment.WaWithdrawDao;
import com.zbkj.service.service.BossDashboardService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 老板经营驾驶舱服务实现
*/
@Service
public class BossDashboardServiceImpl implements BossDashboardService {
@Resource
private WaOrderDao waOrderDao;
@Resource
private WaMerchandiseDao waMerchandiseDao;
@Resource
private WaUsersDao waUsersDao;
@Resource
private WaSelfbonusLogDao waSelfbonusLogDao;
@Resource
private WaSharebonusLogDao waSharebonusLogDao;
@Resource
private WaWithdrawDao waWithdrawDao;
@Override
public BossDashboardResponse overview(String date) {
DateTime businessDate = StrUtil.isBlank(date) ? previousWorkday(DateUtil.date()) : DateUtil.parseDate(date);
DateTime previousDate = previousWorkday(businessDate);
DateRange businessRange = dayRange(businessDate);
DateRange previousRange = dayRange(previousDate);
DailyMetrics metrics = buildDailyMetrics(businessRange);
DailyMetrics previousMetrics = buildDailyMetrics(previousRange);
BossDashboardResponse response = new BossDashboardResponse();
response.setBusinessDate(businessDate.toString("yyyy-MM-dd"));
response.setGeneratedAt(DateUtil.formatDateTime(new Date()));
response.setSummary(buildSummary(metrics));
response.getKpis().add(metric("dealAmount", "上个工作日成交额", metrics.dealAmount, "", "较上一工作日", ratio(metrics.dealAmount, previousMetrics.dealAmount), statusByRatio(metrics.dealAmount, previousMetrics.dealAmount), true));
response.getKpis().add(metric("orderCount", "上个工作日订单数", metrics.orderCount, "", "较上一工作日", ratio(metrics.orderCount, previousMetrics.orderCount), statusByRatio(metrics.orderCount, previousMetrics.orderCount), false));
response.getKpis().add(metric("purchaseUsers", "采购用户", metrics.purchaseUsers, "", "较上一工作日", ratio(metrics.purchaseUsers, previousMetrics.purchaseUsers), statusByRatio(metrics.purchaseUsers, previousMetrics.purchaseUsers), false));
response.getKpis().add(metric("newUsers", "新增用户", metrics.newUsers, "", "较上一工作日", ratio(metrics.newUsers, previousMetrics.newUsers), statusByRatio(metrics.newUsers, previousMetrics.newUsers), false));
response.getKpis().add(metric("newMerchandise", "新增寄售商品", metrics.newMerchandiseCount, "", "较上一工作日", ratio(metrics.newMerchandiseCount, previousMetrics.newMerchandiseCount), statusByRatio(metrics.newMerchandiseCount, previousMetrics.newMerchandiseCount), false));
response.getKpis().add(metric("selfBonus", "个人奖金发放", metrics.selfBonus, "", "较上一工作日", ratio(metrics.selfBonus, previousMetrics.selfBonus), "normal", false));
response.getKpis().add(metric("shareBonus", "推广奖金发放", metrics.shareBonus, "", "较上一工作日", ratio(metrics.shareBonus, previousMetrics.shareBonus), "normal", false));
response.getKpis().add(metric("pendingAmount", "待支付/待结算", metrics.pendingAmount, "", "需关注", null, metrics.pendingAmount.compareTo(BigDecimal.ZERO) > 0 ? "warning" : "normal", false));
buildFundPool(response);
buildSnapshots(response);
buildTrends(response, businessDate);
buildRanks(response);
buildRisks(response);
return response;
}
@Override
public String dailyReportArchiveHtml(String date) {
BossDashboardResponse data = overview(date);
StringBuilder html = new StringBuilder();
html.append("<!doctype html><html lang=\"zh-CN\"><head><meta charset=\"utf-8\">");
html.append("<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">");
html.append("<title>经营日报归档 - ").append(escape(data.getBusinessDate())).append("</title>");
html.append("<style>");
html.append(":root{color:#132033;background:#fff6f1;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at top left,rgba(255,91,54,.18),transparent 30rem),#fff6f1}.page{max-width:820px;margin:0 auto;padding:28px 18px 40px}.hero{color:#fff;padding:26px;border-radius:0 0 28px 28px;background:linear-gradient(145deg,#ff5b36,#ff8b52),radial-gradient(circle at 90% 10%,rgba(255,176,0,.42),transparent 18rem);box-shadow:0 16px 40px rgba(255,91,54,.14)}.eyebrow{margin:0;color:rgba(255,255,255,.76);font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.hero h1{margin:12px 0 8px;font-size:32px;line-height:1.1}.hero p{margin:0;color:rgba(255,255,255,.82);line-height:1.7}.meta{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px}.meta span{padding:7px 12px;border-radius:999px;background:rgba(255,255,255,.16);border:1px solid rgba(255,255,255,.2);font-size:12px;font-weight:700}.section{margin-top:16px;padding:18px;background:#fff;border:1px solid rgba(19,32,51,.08);border-radius:24px;box-shadow:0 10px 28px rgba(22,47,80,.08)}.section h2{margin:0 0 14px;font-size:20px}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.card{padding:14px;border-radius:18px;background:#f6f9fb}.card small{display:block;color:#6b7a90}.card strong{display:block;margin-top:6px;font-size:22px}.list{display:grid;gap:10px}.item{padding:12px;border-radius:16px;background:#f6f9fb}.item strong{display:block}.item small,.item p{color:#6b7a90}.risk-red strong{color:#dc2626}.risk-yellow strong{color:#ffb000}.footer{margin-top:18px;color:#6b7a90;font-size:12px;text-align:center}@media(max-width:560px){.grid{grid-template-columns:1fr}.hero h1{font-size:28px}}");
html.append("</style></head><body><main class=\"page\">");
html.append("<header class=\"hero\"><p class=\"eyebrow\">Daily Report Archive</p><h1>经营日报归档</h1>");
html.append("<p>").append(escape(data.getSummary())).append("</p><div class=\"meta\">");
html.append("<span>数据日期:").append(escape(data.getBusinessDate())).append("</span>");
html.append("<span>生成时间:").append(escape(data.getGeneratedAt())).append("</span>");
html.append("<span>归档类型Standalone HTML</span></div></header>");
appendMetricsSection(html, "核心经营指标", data.getKpis());
appendTrendSection(html, data);
appendMetricsSection(html, "资金池摘要", data.getFundPool());
appendRankSection(html, "高价值用户", data.getUserRanks());
appendRankSection(html, "团队贡献排行", data.getTeamRanks());
appendRankSection(html, "高货值未成交商品", data.getProductRanks());
appendRiskSection(html, data);
html.append("<p class=\"footer\">本归档由经营驾驶舱实时数据生成,可独立保存和打开。</p>");
html.append("</main></body></html>");
return html.toString();
}
private DailyMetrics buildDailyMetrics(DateRange range) {
DailyMetrics metrics = new DailyMetrics();
metrics.dealAmount = sumOrderAmount(range.start, range.end, true, null);
metrics.orderCount = countOrders(range.start, range.end, null);
metrics.purchaseUsers = distinctBuyerCount(range.start, range.end, null);
metrics.newUsers = countUsers(range.start, range.end);
metrics.newMerchandiseCount = countMerchandise(range.start, range.end);
metrics.selfBonus = sumSelfBonus(range.start, range.end);
metrics.shareBonus = sumShareBonus(range.start, range.end);
metrics.pendingAmount = sumOrderAmount(range.start, range.end, false, null);
return metrics;
}
private void buildFundPool(BossDashboardResponse response) {
BigDecimal money = sumUsersDecimal("money");
BigDecimal coupon = sumUsersDecimal("coupon");
BigDecimal selfBonus = sumUsersDecimal("self_bonus");
BigDecimal shareBonus = sumUsersDecimal("share_bonus");
BigDecimal score = sumUsersDecimal("score");
BigDecimal pendingWithdraw = sumWithdrawAmount(0);
Integer pendingWithdrawCount = countWithdraw(0);
response.getFundPool().add(metric("balance", "余额总额", money, "", null, null, "normal", false));
response.getFundPool().add(metric("coupon", "优惠券总额", coupon, "", null, null, "normal", false));
response.getFundPool().add(metric("selfBonusPool", "个人奖金总额", selfBonus, "", null, null, selfBonus.compareTo(BigDecimal.ZERO) > 0 ? "warning" : "normal", false));
response.getFundPool().add(metric("shareBonusPool", "推广奖金总额", shareBonus, "", null, null, "normal", false));
response.getFundPool().add(metric("integral", "积分总额", score, "", null, null, "normal", false));
response.getFundPool().add(metric("withdrawPending", "待审核提现", pendingWithdraw, "", pendingWithdrawCount + "", null, pendingWithdrawCount > 0 ? "danger" : "normal", false));
}
private void buildSnapshots(BossDashboardResponse response) {
DateTime today = DateUtil.date();
DateRange morningRange = range(today, "00:00:00", "10:15:59");
DateRange afternoonRange = range(today, "10:16:00", "14:55:59");
DailyMetrics morningMetrics = buildDailyMetrics(morningRange);
BossDashboardResponse.TodaySnapshot morning = snapshot("1015", "10:15 上午快报", morningRange.end, "上午抢购节点已完成,上一日寄卖商品消化情况请关注成交额、付款和采购用户。", morningMetrics, "success");
response.getSnapshots().add(morning);
String afternoonStatus = new Date().after(afternoonRange.end) ? "success" : "pending";
String afternoonMessage = "success".equals(afternoonStatus)
? "下午寄卖/转卖节点已完成,请关注用户抢购商品的再次上架与转卖承接。"
: "下午寄卖/转卖节点尚未生成,预计 14:55 后可查看用户抢购商品的再次上架情况。";
DailyMetrics afternoonMetrics = "success".equals(afternoonStatus) ? buildDailyMetrics(afternoonRange) : new DailyMetrics();
response.getSnapshots().add(snapshot("1455", "14:55 下午快报", afternoonRange.end, afternoonMessage, afternoonMetrics, afternoonStatus));
}
private void buildTrends(BossDashboardResponse response, DateTime businessDate) {
for (int i = 6; i >= 0; i--) {
DateTime date = DateUtil.offsetDay(businessDate, -i);
DailyMetrics metrics = buildDailyMetrics(dayRange(date));
BossDashboardResponse.TrendPoint point = new BossDashboardResponse.TrendPoint();
point.setDate(date.toString("MM-dd"));
point.setAmount(metrics.dealAmount);
point.setOrders(metrics.orderCount);
point.setNewUsers(metrics.newUsers);
point.setBonus(metrics.selfBonus.add(metrics.shareBonus));
response.getTrends().add(point);
}
}
private void buildRanks(BossDashboardResponse response) {
QueryWrapper<WaUsers> userWrapper = new QueryWrapper<WaUsers>()
.select("id", "nickname", "mobile", "self_bonus", "share_bonus", "coupon", "score")
.orderByDesc("IFNULL(self_bonus,0) + IFNULL(share_bonus,0) + IFNULL(coupon,0)")
.last("limit 3");
List<WaUsers> users = waUsersDao.selectList(userWrapper);
for (int i = 0; i < users.size(); i++) {
WaUsers user = users.get(i);
BigDecimal value = defaultDecimal(user.getSelfBonus()).add(defaultDecimal(user.getShareBonus())).add(defaultDecimal(user.getCoupon()));
response.getUserRanks().add(rank("u" + user.getId(), displayName(user), value, maskMobile(user.getMobile()), i == 0 ? "高价值" : null));
}
QueryWrapper<WaUsers> teamWrapper = new QueryWrapper<WaUsers>()
.select("pid as id", "COUNT(id) as memberCount", "IFNULL(SUM(self_bonus),0) as selfBonus", "IFNULL(SUM(share_bonus),0) as shareBonus")
.isNotNull("pid")
.gt("pid", 0)
.groupBy("pid")
.orderByDesc("IFNULL(SUM(self_bonus),0) + IFNULL(SUM(share_bonus),0)")
.last("limit 3");
List<Map<String, Object>> teams = waUsersDao.selectMaps(teamWrapper);
for (int i = 0; i < teams.size(); i++) {
Map<String, Object> team = teams.get(i);
BigDecimal selfBonus = decimal(team.get("selfBonus"));
BigDecimal shareBonus = decimal(team.get("shareBonus"));
String leaderId = stringValue(team.get("id"));
WaUsers leader = waUsersDao.selectById(leaderId);
String leaderName = leader == null ? "团队 " + leaderId : displayName(leader);
response.getTeamRanks().add(rank("t" + leaderId, leaderName, selfBonus.add(shareBonus), "成员 " + intValue(team.get("memberCount")) + "", i == 0 ? "TOP1" : null));
}
QueryWrapper<WaMerchandise> productWrapper = new QueryWrapper<WaMerchandise>()
.select("id", "title", "price", "created_at")
.eq("status", 1)
.orderByDesc("price")
.last("limit 3");
List<WaMerchandise> products = waMerchandiseDao.selectList(productWrapper);
for (int i = 0; i < products.size(); i++) {
WaMerchandise product = products.get(i);
response.getProductRanks().add(rank("p" + product.getId(), StrUtil.isBlank(product.getTitle()) ? "未命名商品" : product.getTitle(), defaultDecimal(product.getPrice()), "高货值待成交", i == 0 ? "滞销" : null));
}
}
private void buildRisks(BossDashboardResponse response) {
BigDecimal pendingWithdraw = sumWithdrawAmount(0);
if (pendingWithdraw.compareTo(BigDecimal.ZERO) > 0) {
response.getRisks().add(risk("r1", "red", "资金", "待审核提现", "当前待审核提现 " + pendingWithdraw + " 元,建议今日处理。"));
}
Integer pendingOrders = countPendingOrders();
if (pendingOrders > 0) {
response.getRisks().add(risk("r2", "yellow", "订单", "待支付订单未处理", "当前存在 " + pendingOrders + " 笔待支付订单,请关注付款转化。"));
}
Integer hiddenProducts = countHiddenMerchandise();
if (hiddenProducts > 0) {
response.getRisks().add(risk("r3", "gray", "商品", "隐藏寄售商品", "当前存在 " + hiddenProducts + " 个隐藏寄售商品,可按需核查。"));
}
}
private BossDashboardResponse.KpiMetric metric(String key, String title, Object value, String unit, String trendLabel, BigDecimal trendValue, String status, Boolean featured) {
BossDashboardResponse.KpiMetric metric = new BossDashboardResponse.KpiMetric();
metric.setKey(key);
metric.setTitle(title);
metric.setValue(value);
metric.setUnit(unit);
metric.setTrendLabel(trendLabel);
metric.setTrendValue(trendValue);
metric.setStatus(status);
metric.setFeatured(featured);
return metric;
}
private BossDashboardResponse.TodaySnapshot snapshot(String slot, String title, Date generatedAt, String message, DailyMetrics metrics, String status) {
BossDashboardResponse.TodaySnapshot snapshot = new BossDashboardResponse.TodaySnapshot();
snapshot.setSlot(slot);
snapshot.setTitle(title);
snapshot.setStatus(status);
snapshot.setGeneratedAt(DateUtil.formatDateTime(generatedAt));
snapshot.setMessage(message);
snapshot.setPurchaseUsers(metrics.purchaseUsers);
snapshot.setOrderCount(metrics.orderCount);
snapshot.setDealAmount(metrics.dealAmount);
snapshot.setPaidAmount(metrics.dealAmount);
snapshot.setNewMerchandiseCount(metrics.newMerchandiseCount);
snapshot.setSelfBonusChange(metrics.selfBonus);
snapshot.setShareBonusChange(metrics.shareBonus);
return snapshot;
}
private BossDashboardResponse.RankItem rank(String id, String name, BigDecimal value, String description, String badge) {
BossDashboardResponse.RankItem rank = new BossDashboardResponse.RankItem();
rank.setId(id);
rank.setName(name);
rank.setValue(value);
rank.setDescription(description);
rank.setBadge(badge);
return rank;
}
private BossDashboardResponse.RiskAlert risk(String id, String level, String type, String title, String description) {
BossDashboardResponse.RiskAlert risk = new BossDashboardResponse.RiskAlert();
risk.setId(id);
risk.setLevel(level);
risk.setType(type);
risk.setTitle(title);
risk.setDescription(description);
risk.setDiscoveredAt(DateUtil.format(new Date(), "HH:mm"));
return risk;
}
private void appendMetricsSection(StringBuilder html, String title, List<BossDashboardResponse.KpiMetric> metrics) {
html.append("<section class=\"section\"><h2>").append(escape(title)).append("</h2><div class=\"grid\">");
for (BossDashboardResponse.KpiMetric metric : metrics) {
html.append("<article class=\"card\"><small>").append(escape(metric.getTitle())).append("</small>");
html.append("<strong>").append(formatMetric(metric.getValue(), metric.getUnit())).append("</strong>");
if (StrUtil.isNotBlank(metric.getTrendLabel()) || metric.getTrendValue() != null) {
html.append("<small>").append(escape(metric.getTrendLabel()));
if (metric.getTrendValue() != null) {
html.append(" ").append(metric.getTrendValue()).append("%");
}
html.append("</small>");
}
html.append("</article>");
}
html.append("</div></section>");
}
private void appendTrendSection(StringBuilder html, BossDashboardResponse data) {
html.append("<section class=\"section\"><h2>最近 7 天趋势</h2><div class=\"list\">");
for (BossDashboardResponse.TrendPoint point : data.getTrends()) {
html.append("<div class=\"item\"><strong>").append(escape(point.getDate())).append("").append(formatMoney(point.getAmount())).append("</strong>");
html.append("<small>").append(point.getOrders()).append(" 单 / 新增用户 ").append(point.getNewUsers()).append(" / 奖金 ").append(formatMoney(point.getBonus())).append("</small></div>");
}
html.append("</div></section>");
}
private void appendRankSection(StringBuilder html, String title, List<BossDashboardResponse.RankItem> ranks) {
html.append("<section class=\"section\"><h2>").append(escape(title)).append("</h2><div class=\"list\">");
if (ranks.isEmpty()) {
html.append("<div class=\"item\"><strong>暂无数据</strong><small>当前实时数据未生成该排行。</small></div>");
}
for (int i = 0; i < ranks.size(); i++) {
BossDashboardResponse.RankItem rank = ranks.get(i);
html.append("<div class=\"item\"><strong>").append(i + 1).append(". ").append(escape(rank.getName())).append(" · ").append(formatMoney(rank.getValue())).append("</strong>");
html.append("<small>").append(escape(rank.getDescription()));
if (StrUtil.isNotBlank(rank.getBadge())) {
html.append(" / ").append(escape(rank.getBadge()));
}
html.append("</small></div>");
}
html.append("</div></section>");
}
private void appendRiskSection(StringBuilder html, BossDashboardResponse data) {
html.append("<section class=\"section\"><h2>风险预警</h2><div class=\"list\">");
if (data.getRisks().isEmpty()) {
html.append("<div class=\"item\"><strong>暂无风险</strong><small>当前实时数据未触发风险预警。</small></div>");
}
for (BossDashboardResponse.RiskAlert risk : data.getRisks()) {
html.append("<div class=\"item risk-").append(escape(risk.getLevel())).append("\"><strong>");
html.append(escape(risk.getType())).append(" / ").append(escape(risk.getTitle())).append("</strong>");
html.append("<p>").append(escape(risk.getDescription())).append("</p>");
html.append("<small>发现时间:").append(escape(risk.getDiscoveredAt())).append("</small></div>");
}
html.append("</div></section>");
}
private BigDecimal sumOrderAmount(Date start, Date end, Boolean paidOnly, Boolean isResell) {
QueryWrapper<WaOrder> wrapper = new QueryWrapper<WaOrder>().select("IFNULL(SUM(total_money),0) as total").between("buy_time", start, end);
wrapper.eq("is_cancel", 0);
if (Boolean.TRUE.equals(paidOnly)) {
wrapper.ge("status", 1);
} else if (Boolean.FALSE.equals(paidOnly)) {
wrapper.eq("status", 0);
}
if (isResell != null) {
wrapper.eq("is_resell", isResell ? 1 : 0);
}
return aggregateDecimal(waOrderDao.selectMaps(wrapper), "total");
}
private Integer countOrders(Date start, Date end, Boolean isResell) {
QueryWrapper<WaOrder> wrapper = new QueryWrapper<WaOrder>().between("buy_time", start, end).eq("is_cancel", 0);
if (isResell != null) {
wrapper.eq("is_resell", isResell ? 1 : 0);
}
return waOrderDao.selectCount(wrapper);
}
private Integer distinctBuyerCount(Date start, Date end, Boolean isResell) {
QueryWrapper<WaOrder> wrapper = new QueryWrapper<WaOrder>().select("COUNT(DISTINCT buyer_id) as total").between("buy_time", start, end).eq("is_cancel", 0);
if (isResell != null) {
wrapper.eq("is_resell", isResell ? 1 : 0);
}
return aggregateInt(waOrderDao.selectMaps(wrapper), "total");
}
private Integer countUsers(Date start, Date end) {
return waUsersDao.selectCount(new QueryWrapper<WaUsers>().between("join_time", start, end));
}
private Integer countMerchandise(Date start, Date end) {
return waMerchandiseDao.selectCount(new QueryWrapper<WaMerchandise>().between("created_at", start, end));
}
private BigDecimal sumSelfBonus(Date start, Date end) {
QueryWrapper<WaSelfbonusLog> wrapper = new QueryWrapper<WaSelfbonusLog>().select("IFNULL(SUM(money),0) as total").eq("type", 1).between("created_at", start, end);
return aggregateDecimal(waSelfbonusLogDao.selectMaps(wrapper), "total");
}
private BigDecimal sumShareBonus(Date start, Date end) {
QueryWrapper<WaSharebonusLog> wrapper = new QueryWrapper<WaSharebonusLog>().select("IFNULL(SUM(money),0) as total").eq("type", 1).between("created_at", start, end);
return aggregateDecimal(waSharebonusLogDao.selectMaps(wrapper), "total");
}
private BigDecimal sumUsersDecimal(String column) {
QueryWrapper<WaUsers> wrapper = new QueryWrapper<WaUsers>().select("IFNULL(SUM(" + column + "),0) as total");
return aggregateDecimal(waUsersDao.selectMaps(wrapper), "total");
}
private BigDecimal sumWithdrawAmount(Integer status) {
QueryWrapper<WaWithdraw> wrapper = new QueryWrapper<WaWithdraw>().select("IFNULL(SUM(money),0) as total").eq("status", status);
return aggregateDecimal(waWithdrawDao.selectMaps(wrapper), "total");
}
private Integer countWithdraw(Integer status) {
return waWithdrawDao.selectCount(new QueryWrapper<WaWithdraw>().eq("status", status));
}
private Integer countPendingOrders() {
return waOrderDao.selectCount(new QueryWrapper<WaOrder>().eq("status", 0).eq("is_cancel", 0));
}
private Integer countHiddenMerchandise() {
return waMerchandiseDao.selectCount(new QueryWrapper<WaMerchandise>().eq("is_show", 0));
}
private BigDecimal ratio(BigDecimal current, BigDecimal previous) {
if (previous == null || previous.compareTo(BigDecimal.ZERO) == 0) {
return current != null && current.compareTo(BigDecimal.ZERO) > 0 ? BigDecimal.valueOf(100) : BigDecimal.ZERO;
}
return current.subtract(previous).multiply(BigDecimal.valueOf(100)).divide(previous, 1, RoundingMode.HALF_UP);
}
private BigDecimal ratio(Integer current, Integer previous) {
return ratio(BigDecimal.valueOf(current == null ? 0 : current), BigDecimal.valueOf(previous == null ? 0 : previous));
}
private String statusByRatio(BigDecimal current, BigDecimal previous) {
BigDecimal value = ratio(current, previous);
return value.compareTo(BigDecimal.ZERO) >= 0 ? "success" : "warning";
}
private String statusByRatio(Integer current, Integer previous) {
return statusByRatio(BigDecimal.valueOf(current == null ? 0 : current), BigDecimal.valueOf(previous == null ? 0 : previous));
}
private String buildSummary(DailyMetrics metrics) {
if (metrics.dealAmount.compareTo(BigDecimal.ZERO) == 0 && metrics.orderCount == 0) {
return "当前日期暂无成交数据,请关注抢购与寄卖节点是否正常生成。";
}
return "上个工作日成交 " + metrics.dealAmount + " 元,订单 " + metrics.orderCount + " 单,采购用户 " + metrics.purchaseUsers + " 人;请重点关注待支付、提现和寄售供给。";
}
private DateRange dayRange(DateTime date) {
return new DateRange(DateUtil.beginOfDay(date), DateUtil.endOfDay(date));
}
private DateTime previousWorkday(DateTime referenceDate) {
DateTime date = DateUtil.offsetDay(referenceDate, -1);
while (isWeekend(date)) {
date = DateUtil.offsetDay(date, -1);
}
return date;
}
private boolean isWeekend(DateTime date) {
int dayOfWeek = DateUtil.dayOfWeek(date);
return dayOfWeek == Calendar.SATURDAY || dayOfWeek == Calendar.SUNDAY;
}
private DateRange range(DateTime date, String startTime, String endTime) {
String day = date.toString("yyyy-MM-dd");
return new DateRange(DateUtil.parse(day + " " + startTime), DateUtil.parse(day + " " + endTime));
}
private BigDecimal aggregateDecimal(List<Map<String, Object>> maps, String key) {
if (maps == null || maps.isEmpty()) {
return BigDecimal.ZERO;
}
return decimal(maps.get(0).get(key));
}
private Integer aggregateInt(List<Map<String, Object>> maps, String key) {
if (maps == null || maps.isEmpty()) {
return 0;
}
return intValue(maps.get(0).get(key));
}
private BigDecimal decimal(Object value) {
if (value == null) {
return BigDecimal.ZERO;
}
if (value instanceof BigDecimal) {
return (BigDecimal) value;
}
return new BigDecimal(String.valueOf(value));
}
private BigDecimal defaultDecimal(BigDecimal value) {
return value == null ? BigDecimal.ZERO : value;
}
private Integer intValue(Object value) {
if (value == null) {
return 0;
}
if (value instanceof Number) {
return ((Number) value).intValue();
}
return Integer.parseInt(String.valueOf(value));
}
private String stringValue(Object value) {
return value == null ? "" : String.valueOf(value);
}
private String displayName(WaUsers user) {
if (StrUtil.isNotBlank(user.getNickname())) {
return user.getNickname();
}
if (StrUtil.isNotBlank(user.getUsername())) {
return user.getUsername();
}
return "用户 " + user.getId();
}
private String maskMobile(String mobile) {
if (StrUtil.isBlank(mobile) || mobile.length() < 7) {
return "手机号未完善";
}
return mobile.substring(0, 3) + "****" + mobile.substring(mobile.length() - 4);
}
private String formatMetric(Object value, String unit) {
if ("".equals(unit)) {
return formatMoney(decimal(value));
}
if (value == null) {
return "--";
}
return escape(String.valueOf(value)) + (unit == null ? "" : escape(unit));
}
private String formatMoney(BigDecimal value) {
return "¥" + defaultDecimal(value).setScale(2, RoundingMode.HALF_UP).toPlainString();
}
private String escape(String value) {
if (value == null) {
return "";
}
return value.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#39;");
}
private static class DailyMetrics {
private BigDecimal dealAmount = BigDecimal.ZERO;
private Integer orderCount = 0;
private Integer purchaseUsers = 0;
private Integer newUsers = 0;
private Integer newMerchandiseCount = 0;
private BigDecimal selfBonus = BigDecimal.ZERO;
private BigDecimal shareBonus = BigDecimal.ZERO;
private BigDecimal pendingAmount = BigDecimal.ZERO;
}
private static class DateRange {
private Date start;
private Date end;
private DateRange(Date start, Date end) {
this.start = start;
this.end = end;
}
}
}

View File

@@ -1,265 +1,22 @@
package com.zbkj.service.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.zbkj.common.model.consignment.WaSelfbonusLog;
import com.zbkj.common.model.user.User;
import com.zbkj.common.model.user.UserIntegralRecord;
import com.zbkj.common.utils.CrmebUtil;
import com.zbkj.service.dao.UserDao;
import com.zbkj.service.dao.UserIntegralRecordDao;
import com.zbkj.service.dao.consignment.WaSelfbonusLogDao;
import com.zbkj.service.service.UserService;
import com.zbkj.service.service.WaSelfbonusSyncService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 用户积分并发安全服务实现类
* 专门处理高并发场景下的积分更新,避免数据库锁等待超时
* 保留并发安全服务 bean 名称兼容历史调用。
* 实际逻辑统一委托到 waSelfbonusSyncService避免双实现规则漂移。
*/
@Slf4j
@Service("userIntegralConcurrencyService")
public class UserIntegralConcurrencyServiceImpl {
@Autowired
private WaSelfbonusLogDao waSelfbonusLogDao;
private WaSelfbonusSyncService waSelfbonusSyncService;
@Autowired
private UserIntegralRecordDao userIntegralRecordDao;
@Autowired
private UserDao userDao;
@Autowired
private UserService userService;
// 使用ConcurrentHashMap来缓存正在处理的用户ID防止重复处理
private final ConcurrentHashMap<Integer, Object> processingUsers = new ConcurrentHashMap<>();
/**
* 同步个人奖金变动到用户积分 - 并发安全版本
* 根据个人奖金变动记录为对应的用户增加积分奖金金额的50%
*/
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> syncSelfbonusToIntegral() {
log.info("开始同步个人奖金变动到用户积分(并发安全版)");
int successCount = 0;
int skipCount = 0;
int failCount = 0;
try {
// 查询最新的个人奖金变动记录
LambdaQueryWrapper<WaSelfbonusLog> bonusLogWrapper = new LambdaQueryWrapper<>();
bonusLogWrapper.orderByDesc(WaSelfbonusLog::getCreatedAt); // 按创建时间倒序
bonusLogWrapper.last("LIMIT 500"); // 限制查询500条避免一次性处理过多
List<WaSelfbonusLog> bonusLogList = waSelfbonusLogDao.selectList(bonusLogWrapper);
for (WaSelfbonusLog bonusLog : bonusLogList) {
try {
// 检查该奖金记录是否已经处理过
LambdaQueryWrapper<UserIntegralRecord> checkWrapper = new LambdaQueryWrapper<>();
checkWrapper.eq(UserIntegralRecord::getWaSelfbonusLogid, bonusLog.getId());
Integer existCount = userIntegralRecordDao.selectCount(checkWrapper);
if (existCount != null && existCount > 0) {
log.debug("奖金记录已处理,跳过: bonusLogId={}, userId={}", bonusLog.getId(), bonusLog.getUserId());
skipCount++;
continue;
}
// 获取用户ID
Integer ebUserId = bonusLog.getUserId();
// 使用同步块确保同一用户不会被重复处理
Object lock = processingUsers.computeIfAbsent(ebUserId, k -> new Object());
synchronized (lock) {
try {
// 再次检查积分记录是否已存在(双重检查)
existCount = userIntegralRecordDao.selectCount(checkWrapper);
if (existCount != null && existCount > 0) {
log.debug("奖金记录已在其他线程处理,跳过: bonusLogId={}", bonusLog.getId());
skipCount++;
continue;
}
// 查询用户信息
User user = userDao.selectById(ebUserId);
if (user == null) {
log.warn("未找到对应的系统用户,跳过: waUserId={}", bonusLog.getUserId());
skipCount++;
continue;
}
// 验证奖金类型和金额
if (!isValidBonusLog(bonusLog)) {
log.debug("奖金记录不符合处理条件,跳过: bonusLogId={}, type={}, amount={}",
bonusLog.getId(), bonusLog.getType(), bonusLog.getMoney());
skipCount++;
continue;
}
// 计算积分值
BigDecimal integralValue = calculateIntegralValue(bonusLog.getMoney());
if (integralValue.compareTo(BigDecimal.ZERO) <= 0) {
log.debug("计算出的积分为0或负数跳过: bonusLogId={}, integralValue={}",
bonusLog.getId(), integralValue);
skipCount++;
continue;
}
// 使用CAS方式更新积分避免锁竞争
Boolean updateResult = updateIntegralWithRetry(user.getUid(), integralValue, "add", 3);
if (!updateResult) {
log.error("更新用户积分失败(重试后): userId={}, integralValue={}", user.getUid(), integralValue);
failCount++;
continue;
}
// 插入积分记录
UserIntegralRecord integralRecord = createUserIntegralRecord(user.getUid(), bonusLog, integralValue);
int insertResult = userIntegralRecordDao.insert(integralRecord);
if (insertResult <= 0) {
log.error("插入积分记录失败: userId={}, bonusLogId={}", user.getUid(), bonusLog.getId());
failCount++;
continue;
}
successCount++;
log.info("成功同步奖金到积分: bonusLogId={}, userId={}, bonusAmount={}, integralValue={}",
bonusLog.getId(), user.getUid(), bonusLog.getMoney(), integralValue);
} finally {
// 清理锁对象
processingUsers.remove(ebUserId);
}
}
} catch (Exception e) {
failCount++;
log.error("处理奖金记录失败: bonusLogId={}, error={}", bonusLog.getId(), e.getMessage(), e);
}
}
Map<String, Object> result = new HashMap<>();
result.put("total", bonusLogList.size());
result.put("successCount", successCount);
result.put("skipCount", skipCount);
result.put("failCount", failCount);
log.info("同步个人奖金变动到用户积分完成(并发安全版): 总数={}, 成功={}, 跳过={}, 失败={}",
bonusLogList.size(), successCount, skipCount, failCount);
result.put("message", "同步完成");
return result;
} catch (Exception e) {
log.error("同步个人奖金变动到用户积分异常", e);
Map<String, Object> result = new HashMap<>();
result.put("message", "同步失败: " + e.getMessage());
result.put("error", e.getMessage());
return result;
}
}
/**
* 验证奖金记录是否符合处理条件
*/
private boolean isValidBonusLog(WaSelfbonusLog bonusLog) {
// 只处理收入类型type=1
if (bonusLog.getType() == null || bonusLog.getType() != 1) {
return false;
}
// 验证奖金金额有效
if (bonusLog.getMoney() == null || bonusLog.getMoney().compareTo(BigDecimal.ZERO) <= 0) {
return false;
}
return true;
}
/**
* 计算积分值
*/
private BigDecimal calculateIntegralValue(BigDecimal bonusAmount) {
// 计算积分:奖金金额 * 50%,向下取整
BigDecimal integralDecimal = bonusAmount.multiply(new BigDecimal("0.5"));
return integralDecimal.setScale(3, RoundingMode.DOWN);
}
/**
* 创建积分记录对象
*/
private UserIntegralRecord createUserIntegralRecord(Integer userId, WaSelfbonusLog bonusLog, BigDecimal integralValue) {
User user = userDao.selectById(userId); // 重新查询用户获取最新积分
Integer newIntegral = user != null && user.getIntegral() != null ? user.getIntegral().intValue() : 0;
UserIntegralRecord integralRecord = new UserIntegralRecord();
integralRecord.setUid(userId);
integralRecord.setLinkId(String.valueOf(bonusLog.getId())); // 关联奖金记录ID
integralRecord.setLinkType("selfbonus"); // 关联类型:个人奖金
integralRecord.setType(1); // 类型1-增加
integralRecord.setTitle("个人奖金奖励");
integralRecord.setIntegral(integralValue);
integralRecord.setBalance(newIntegral); // 实际上应该是更新后的积分,这里可能需要调整
integralRecord.setMark(String.format("个人奖金变动奖励,奖金金额:%.3f,积分:%d",
bonusLog.getMoney(), integralValue.intValue()));
integralRecord.setStatus(3); // 状态3-完成
integralRecord.setWaSelfbonusLogid(bonusLog.getId()); // 关联个人奖金记录ID
integralRecord.setCreateTime(new Date());
integralRecord.setUpdateTime(new Date());
return integralRecord;
}
/**
* 带重试机制的积分更新
*/
private Boolean updateIntegralWithRetry(Integer uid, BigDecimal integral, String type, int maxRetries) {
int attempts = 0;
Exception lastException = null;
while (attempts < maxRetries) {
try {
attempts++;
// 直接更新积分,不再依赖乐观锁
Boolean result = userService.operationIntegral(uid, integral, BigDecimal.ZERO, type);
if (result) {
return true;
} else {
log.warn("积分更新失败,准备重试 (attempt {}/{})", attempts, maxRetries);
}
} catch (Exception e) {
lastException = e;
log.warn("积分更新异常,准备重试 (attempt {}/{}), error: {}", attempts, maxRetries, e.getMessage());
// 如果是数据库锁等待超时,等待一段时间再重试
if (e.getMessage() != null && e.getMessage().contains("Lock wait timeout")) {
try {
Thread.sleep(100 * attempts); // 指数退避
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
}
log.error("积分更新达到最大重试次数仍然失败,最后一次异常: ", lastException);
return false;
return waSelfbonusSyncService.syncSelfbonusToIntegral();
}
}

View File

@@ -19,9 +19,11 @@ import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.zbkj.common.utils.CrmebDateUtil;
import com.zbkj.common.vo.DateLimitUtilVo;
import com.zbkj.common.model.consignment.WaSelfbonusLog;
import com.zbkj.common.model.user.User;
import com.zbkj.common.model.user.UserIntegralRecord;
import com.zbkj.service.dao.UserIntegralRecordDao;
import com.zbkj.service.dao.consignment.WaSelfbonusLogDao;
import com.zbkj.service.service.UserIntegralRecordService;
import com.zbkj.service.service.UserService;
import org.slf4j.Logger;
@@ -33,7 +35,9 @@ import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -62,6 +66,9 @@ public class UserIntegralRecordServiceImpl extends ServiceImpl<UserIntegralRecor
@Autowired
private UserService userService;
@Autowired
private WaSelfbonusLogDao waSelfbonusLogDao;
@Autowired
private TransactionTemplate transactionTemplate;
@@ -155,7 +162,8 @@ public class UserIntegralRecordServiceImpl extends ServiceImpl<UserIntegralRecor
LambdaQueryWrapper<UserIntegralRecord> lqw = Wrappers.lambdaQuery();
lqw.select(UserIntegralRecord::getId, UserIntegralRecord::getTitle, UserIntegralRecord::getBalance, UserIntegralRecord::getIntegral,
UserIntegralRecord::getMark, UserIntegralRecord::getUid, UserIntegralRecord::getUpdateTime,
UserIntegralRecord::getType, UserIntegralRecord::getLinkType, UserIntegralRecord::getStatus, UserIntegralRecord::getCreateTime);
UserIntegralRecord::getType, UserIntegralRecord::getLinkType, UserIntegralRecord::getStatus, UserIntegralRecord::getCreateTime,
UserIntegralRecord::getWaSelfbonusLogid);
lqw.eq(UserIntegralRecord::getStatus, IntegralRecordConstants.INTEGRAL_RECORD_STATUS_COMPLETE);
String nameKey = StrUtil.isNotBlank(request.getNickName()) ? request.getNickName() : request.getKeywords();
@@ -213,9 +221,28 @@ public class UserIntegralRecordServiceImpl extends ServiceImpl<UserIntegralRecor
if (CollUtil.isEmpty(list)) {
return CommonPage.copyPageInfo(page, CollUtil.newArrayList());
}
List<Integer> selfBonusLogIds = list.stream()
.map(UserIntegralRecord::getWaSelfbonusLogid)
.filter(ObjectUtil::isNotNull)
.distinct()
.collect(Collectors.toList());
Map<Integer, BigDecimal> selfBonusIntegralMap = new HashMap<>();
if (CollUtil.isNotEmpty(selfBonusLogIds)) {
waSelfbonusLogDao.selectBatchIds(selfBonusLogIds).forEach(log -> {
if (ObjectUtil.isNotNull(log) && ObjectUtil.isNotNull(log.getMoney())) {
selfBonusIntegralMap.put(log.getId(), log.getMoney().multiply(new BigDecimal("0.5")).setScale(3, RoundingMode.DOWN));
}
});
}
List<UserIntegralRecordResponse> responseList = list.stream().map(i -> {
UserIntegralRecordResponse response = new UserIntegralRecordResponse();
BeanUtils.copyProperties(i, response);
if (ObjectUtil.isNotNull(i.getWaSelfbonusLogid())) {
BigDecimal convertedIntegral = selfBonusIntegralMap.get(i.getWaSelfbonusLogid());
if (ObjectUtil.isNotNull(convertedIntegral)) {
response.setIntegral(convertedIntegral);
}
}
// 获取用户昵称
User user = userService.getById(i.getUid());
if (ObjectUtil.isNotNull(user)) {

View File

@@ -142,6 +142,10 @@ public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserS
map.put("isPromoter", request.getIsPromoter() ? 1 : 0);
}
if (request.getUid() != null) {
map.put("uid", request.getUid());
}
if (StrUtil.isNotBlank(request.getGroupId())) {
List<Integer> groupIdList = CrmebUtil.stringToArray(request.getGroupId());
map.put("groupIdList", groupIdList);

View File

@@ -11,8 +11,8 @@ import com.zbkj.service.service.UserService;
import com.zbkj.service.service.WaSelfbonusSyncService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import java.math.BigDecimal;
@@ -44,12 +44,14 @@ public class WaSelfbonusServiceImpl implements WaSelfbonusSyncService {
@Autowired
private UserService userService;
@Autowired
private TransactionTemplate transactionTemplate;
/**
* 同步个人奖金变动到用户积分
* 根据个人奖金变动记录为对应的用户增加积分奖金金额的50%
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> syncSelfbonusToIntegral() {
log.info("开始同步个人奖金变动到用户积分");
@@ -66,101 +68,16 @@ public class WaSelfbonusServiceImpl implements WaSelfbonusSyncService {
for (WaSelfbonusLog bonusLog : bonusLogList) {
try {
// 检查该奖金记录是否已经处理过(通过 waSelfbonusLogid 字段查询积分记录)
LambdaQueryWrapper<UserIntegralRecord> checkWrapper = new LambdaQueryWrapper<>();
checkWrapper.eq(UserIntegralRecord::getWaSelfbonusLogid, bonusLog.getId());
Integer existCount = userIntegralRecordDao.selectCount(checkWrapper);
if (existCount != null && existCount > 0) {
// 已处理过,跳过
log.debug("奖金记录已处理,跳过: bonusLogId={}, userId={}", bonusLog.getId(), bonusLog.getUserId());
ProcessStatus processStatus = transactionTemplate.execute(status -> processBonusLogAtomically(bonusLog));
if (processStatus == ProcessStatus.SKIP) {
skipCount++;
continue;
}
// 根据 wa_users 的 user_id 查找对应的 eb_user 表的 uid
// 注意wa_users.id 对应 eb_user.uid在同步时已建立关联
Integer ebUserId = bonusLog.getUserId(); // wa_users.id 就是 eb_user.uid
// 查询 eb_user 表中的用户
User user = userDao.selectById(ebUserId);
if (user == null) {
log.warn("未找到对应的系统用户,跳过: waUserId={}", bonusLog.getUserId());
skipCount++;
if (processStatus == ProcessStatus.SUCCESS) {
successCount++;
continue;
}
// 计算积分值个人奖金变更金额的50%(只处理收入类型的奖金变动)
if (bonusLog.getType() == null || bonusLog.getType() != 1) {
// 只处理收入类型type=1支出类型不处理
log.debug("跳过非收入类型的奖金变动: bonusLogId={}, type={}", bonusLog.getId(), bonusLog.getType());
skipCount++;
continue;
}
// 奖金金额(收入为正数)
BigDecimal bonusAmount = bonusLog.getMoney();
if (bonusAmount == null || bonusAmount.compareTo(BigDecimal.ZERO) <= 0) {
log.debug("奖金金额无效,跳过: bonusLogId={}, money={}", bonusLog.getId(), bonusAmount);
skipCount++;
continue;
}
// 计算积分:奖金金额 * 50%,向下取整
BigDecimal integralDecimal = bonusAmount.multiply(new BigDecimal("0.5"));
BigDecimal integralValue = integralDecimal.setScale(3, RoundingMode.DOWN);
if (integralValue.compareTo(BigDecimal.ZERO) <= 0) {
log.debug("计算出的积分为0跳过: bonusLogId={}, integralValue={}", bonusLog.getId(), integralValue);
skipCount++;
continue;
}
// 更新用户积分 - 不再需要当前积分值作为乐观锁条件
Boolean updateResult = userService.operationIntegral(
user.getUid(),
integralValue,
BigDecimal.valueOf(0), // 不再使用当前积分作为乐观锁条件
"add"
);
if (!updateResult) {
log.error("更新用户积分失败: userId={}, integralValue={}", user.getUid(), integralValue);
failCount++;
continue;
}
// 重新查询用户获取最新积分
user = userDao.selectById(user.getUid());
Integer newIntegral = user.getIntegral() != null ? user.getIntegral().intValue() : 0;
// 新增积分记录
UserIntegralRecord integralRecord = new UserIntegralRecord();
integralRecord.setUid(user.getUid());
integralRecord.setLinkId(String.valueOf(bonusLog.getId())); // 关联奖金记录ID
integralRecord.setLinkType("selfbonus"); // 关联类型:个人奖金
integralRecord.setType(1); // 类型1-增加
integralRecord.setTitle("个人奖金奖励");
integralRecord.setIntegral(integralValue);
integralRecord.setBalance(newIntegral);
integralRecord.setMark(String.format("个人奖金变动奖励,奖金金额:%.3f,积分:%d",
bonusAmount, integralValue.intValue()));
integralRecord.setStatus(3); // 状态3-完成
integralRecord.setWaSelfbonusLogid(bonusLog.getId()); // 关联个人奖金记录ID
integralRecord.setCreateTime(new Date());
integralRecord.setUpdateTime(new Date());
int insertResult = userIntegralRecordDao.insert(integralRecord);
if (insertResult <= 0) {
log.error("插入积分记录失败: userId={}, bonusLogId={}", user.getUid(), bonusLog.getId());
failCount++;
continue;
}
successCount++;
log.info("成功同步奖金到积分: bonusLogId={}, userId={}, bonusAmount={}, integralValue={}",
bonusLog.getId(), user.getUid(), bonusAmount, integralValue);
failCount++;
} catch (Exception e) {
failCount++;
@@ -188,47 +105,104 @@ public class WaSelfbonusServiceImpl implements WaSelfbonusSyncService {
return result;
}
}
/**
* 带重试机制的用户积分更新
* 原子处理单条奖金日志:
* 1) 先插入积分流水(受唯一索引保护)
* 2) 插入成功后再更新用户总积分
* 3) 回填本条流水的 balance
*/
private Boolean updateUserIntegralWithRetry(Integer uid, BigDecimal integralValue, int maxRetries) {
int attempts = 0;
Exception lastException = null;
while (attempts < maxRetries) {
try {
attempts++;
Boolean result = userService.operationIntegral(
uid,
integralValue,
BigDecimal.ZERO, // 不再使用当前积分作为乐观锁条件
"add"
);
if (result) {
return true;
} else {
log.warn("积分更新失败,准备重试 (attempt {}/{})", attempts, maxRetries);
}
} catch (Exception e) {
lastException = e;
log.warn("积分更新异常,准备重试 (attempt {}/{}), error: {}", attempts, maxRetries, e.getMessage());
// 如果是数据库锁等待超时,等待一段时间再重试
if (e.getMessage() != null && e.getMessage().contains("Lock wait timeout")) {
try {
Thread.sleep(100 * attempts); // 指数退避
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
private ProcessStatus processBonusLogAtomically(WaSelfbonusLog bonusLog) {
if (isAlreadyProcessed(bonusLog.getId())) {
log.debug("奖金记录已处理,跳过: bonusLogId={}, userId={}", bonusLog.getId(), bonusLog.getUserId());
return ProcessStatus.SKIP;
}
log.error("积分更新达到最大重试次数仍然失败,最后一次异常: ", lastException);
return false;
Integer ebUserId = bonusLog.getUserId();
User user = userDao.selectById(ebUserId);
if (user == null) {
log.warn("未找到对应的系统用户,跳过: waUserId={}", bonusLog.getUserId());
return ProcessStatus.SKIP;
}
if (bonusLog.getType() == null || bonusLog.getType() != 1) {
log.debug("跳过非收入类型的奖金变动: bonusLogId={}, type={}", bonusLog.getId(), bonusLog.getType());
return ProcessStatus.SKIP;
}
BigDecimal bonusAmount = bonusLog.getMoney();
if (bonusAmount == null || bonusAmount.compareTo(BigDecimal.ZERO) <= 0) {
log.debug("奖金金额无效,跳过: bonusLogId={}, money={}", bonusLog.getId(), bonusAmount);
return ProcessStatus.SKIP;
}
BigDecimal integralValue = bonusAmount.multiply(new BigDecimal("0.5")).setScale(3, RoundingMode.DOWN);
if (integralValue.compareTo(BigDecimal.ZERO) <= 0) {
log.debug("计算出的积分为0跳过: bonusLogId={}, integralValue={}", bonusLog.getId(), integralValue);
return ProcessStatus.SKIP;
}
UserIntegralRecord integralRecord = buildIntegralRecord(user.getUid(), bonusLog, bonusAmount, integralValue);
try {
int insertResult = userIntegralRecordDao.insert(integralRecord);
if (insertResult <= 0) {
throw new IllegalStateException("插入积分记录失败");
}
} catch (DuplicateKeyException duplicateKeyException) {
// 数据库唯一索引兜底,保证多入口并发只处理一次
log.info("奖金记录并发重复处理,已跳过: bonusLogId={}, userId={}", bonusLog.getId(), user.getUid());
return ProcessStatus.SKIP;
}
Boolean updateResult = userService.operationIntegral(
user.getUid(),
integralValue,
BigDecimal.ZERO,
"add"
);
if (!updateResult) {
throw new IllegalStateException(String.format("更新用户积分失败: userId=%s, integralValue=%s", user.getUid(), integralValue));
}
User latestUser = userDao.selectById(user.getUid());
Integer latestIntegral = latestUser != null && latestUser.getIntegral() != null ? latestUser.getIntegral().intValue() : 0;
integralRecord.setBalance(latestIntegral);
integralRecord.setUpdateTime(new Date());
userIntegralRecordDao.updateById(integralRecord);
log.info("成功同步奖金到积分: bonusLogId={}, userId={}, bonusAmount={}, integralValue={}",
bonusLog.getId(), user.getUid(), bonusAmount, integralValue);
return ProcessStatus.SUCCESS;
}
private boolean isAlreadyProcessed(Integer waSelfbonusLogId) {
LambdaQueryWrapper<UserIntegralRecord> checkWrapper = new LambdaQueryWrapper<>();
checkWrapper.eq(UserIntegralRecord::getWaSelfbonusLogid, waSelfbonusLogId);
Integer existCount = userIntegralRecordDao.selectCount(checkWrapper);
return existCount != null && existCount > 0;
}
private UserIntegralRecord buildIntegralRecord(Integer uid, WaSelfbonusLog bonusLog, BigDecimal bonusAmount, BigDecimal integralValue) {
UserIntegralRecord integralRecord = new UserIntegralRecord();
integralRecord.setUid(uid);
integralRecord.setLinkId(String.valueOf(bonusLog.getId()));
integralRecord.setLinkType("selfbonus");
integralRecord.setType(1);
integralRecord.setTitle("个人奖金奖励");
integralRecord.setIntegral(integralValue);
integralRecord.setBalance(0);
integralRecord.setMark(String.format("个人奖金变动奖励,奖金金额:%.3f,积分:%.3f",
bonusAmount, integralValue));
integralRecord.setStatus(3);
integralRecord.setWaSelfbonusLogid(bonusLog.getId());
integralRecord.setCreateTime(new Date());
integralRecord.setUpdateTime(new Date());
return integralRecord;
}
private enum ProcessStatus {
SUCCESS,
SKIP
}
}

View File

@@ -29,6 +29,9 @@
<if test="isPromoter != null and isPromoter !='' or isPromoter == 0 ">
and u.is_promoter = #{isPromoter}
</if>
<if test="uid != null">
and u.uid = #{uid}
</if>
<if test="groupId != null and groupId !='' ">
and u.group_id in
<foreach item="group_id" collection="groupIdList" open="(" separator="," close=")">

View File

@@ -0,0 +1,23 @@
-- Add strong idempotency guard for selfbonus -> integral conversion.
-- This index guarantees one wa_selfbonus_log can map to at most one integral record.
-- Prerequisite: clear duplicate wa_selfbonus_logid rows first.
-- Pre-checks
SELECT wa_selfbonus_logid, COUNT(*) AS cnt
FROM eb_user_integral_record
WHERE wa_selfbonus_logid IS NOT NULL
GROUP BY wa_selfbonus_logid
HAVING COUNT(*) > 1
LIMIT 20;
SELECT COUNT(*) AS zero_cnt
FROM eb_user_integral_record
WHERE wa_selfbonus_logid = 0;
-- Apply unique index
ALTER TABLE eb_user_integral_record
ADD UNIQUE KEY uk_integral_selfbonus_log (wa_selfbonus_logid);
-- Verify index exists
SHOW INDEX FROM eb_user_integral_record
WHERE Key_name = 'uk_integral_selfbonus_log';

View File

@@ -0,0 +1,130 @@
-- Purpose:
-- 1) Find duplicate selfbonus integral rows generated from same wa_selfbonus_log
-- 2) Backup affected data
-- 3) Keep min(id), delete duplicate rows
-- 4) Resync eb_user.integral from remaining ledger rows
-- 5) Rebuild integer balance snapshot for affected users
--
-- Notes:
-- - Designed for MySQL 5.7
-- - Run during low traffic window
-- - Review backup table names before execution
-- 0) Preview duplicate groups
SELECT uid,
wa_selfbonus_logid,
link_id,
COUNT(*) AS cnt,
SUM(integral) AS total_integral,
MIN(id) AS keep_id,
GROUP_CONCAT(id ORDER BY id) AS record_ids
FROM eb_user_integral_record
WHERE link_type = 'selfbonus'
AND type = 1
AND wa_selfbonus_logid IS NOT NULL
GROUP BY uid, wa_selfbonus_logid, link_id
HAVING COUNT(*) > 1;
-- 1) Backup duplicate rows and affected users
DROP TABLE IF EXISTS backup_euir_selfbonus_dups_20260511_0959;
CREATE TABLE backup_euir_selfbonus_dups_20260511_0959 AS
SELECT e.*
FROM eb_user_integral_record e
JOIN (
SELECT uid, wa_selfbonus_logid, link_id, MIN(id) AS keep_id, COUNT(*) AS cnt
FROM eb_user_integral_record
WHERE link_type = 'selfbonus'
AND type = 1
AND wa_selfbonus_logid IS NOT NULL
GROUP BY uid, wa_selfbonus_logid, link_id
HAVING COUNT(*) > 1
) d
ON d.uid = e.uid
AND d.wa_selfbonus_logid = e.wa_selfbonus_logid
AND d.link_id = e.link_id;
DROP TABLE IF EXISTS backup_eb_user_integral_before_fix_20260511_0959;
CREATE TABLE backup_eb_user_integral_before_fix_20260511_0959 AS
SELECT u.*
FROM eb_user u
WHERE u.uid IN (
SELECT DISTINCT uid FROM backup_euir_selfbonus_dups_20260511_0959
);
-- 2) Deduplicate + resync in one transaction
START TRANSACTION;
DROP TEMPORARY TABLE IF EXISTS tmp_dup_groups;
CREATE TEMPORARY TABLE tmp_dup_groups AS
SELECT uid, wa_selfbonus_logid, link_id, MIN(id) AS keep_id
FROM eb_user_integral_record
WHERE link_type = 'selfbonus'
AND type = 1
AND wa_selfbonus_logid IS NOT NULL
GROUP BY uid, wa_selfbonus_logid, link_id
HAVING COUNT(*) > 1;
DROP TEMPORARY TABLE IF EXISTS tmp_affected_uids;
CREATE TEMPORARY TABLE tmp_affected_uids AS
SELECT DISTINCT uid FROM tmp_dup_groups;
DELETE e
FROM eb_user_integral_record e
JOIN tmp_dup_groups d
ON d.uid = e.uid
AND d.wa_selfbonus_logid = e.wa_selfbonus_logid
AND d.link_id = e.link_id
WHERE e.id <> d.keep_id;
UPDATE eb_user u
JOIN (
SELECT r.uid, COALESCE(SUM(r.integral), 0) AS sum_integral
FROM eb_user_integral_record r
JOIN tmp_affected_uids t ON t.uid = r.uid
GROUP BY r.uid
) s ON s.uid = u.uid
SET u.integral = s.sum_integral;
SET @run_uid := 0;
SET @run_bal := 0;
UPDATE eb_user_integral_record e
JOIN (
SELECT t.id, FLOOR(t.running) AS new_balance
FROM (
SELECT s.id,
s.uid,
(@run_bal := IF(@run_uid = s.uid, @run_bal + s.integral, s.integral)) AS running,
(@run_uid := s.uid) AS uid_guard
FROM (
SELECT id, uid, integral
FROM eb_user_integral_record
WHERE uid IN (SELECT uid FROM tmp_affected_uids)
ORDER BY uid, id
) s
) t
) x ON x.id = e.id
SET e.balance = x.new_balance;
COMMIT;
-- 3) Post-checks
SELECT COUNT(*) AS remaining_dup_groups
FROM (
SELECT 1
FROM eb_user_integral_record
WHERE link_type = 'selfbonus'
AND type = 1
AND wa_selfbonus_logid IS NOT NULL
GROUP BY uid, wa_selfbonus_logid, link_id
HAVING COUNT(*) > 1
) a;
SELECT u.uid, u.integral, s.sum_integral
FROM eb_user u
JOIN (
SELECT uid, SUM(integral) AS sum_integral
FROM eb_user_integral_record
GROUP BY uid
) s ON s.uid = u.uid
WHERE u.uid IN (SELECT DISTINCT uid FROM backup_euir_selfbonus_dups_20260511_0959)
ORDER BY u.uid;

View File

@@ -0,0 +1,55 @@
# 公司名称:夏盛军商贸
## mysql数据库配置信息
host ip: 39.106.63.33
datasource:
name: yangtangyoupin
username: yangtangyoupin
password: 5Fn8eWrbYFtAhCZw
## 数据删除任务
- 用户数据范围暨**用户id集**wa_users表中的用户id集
- 查询wa_withdraw表中用户id不在用户数据范围内的记录并写一个删除这些数据的sql
### 1) 查询核对 SQL
```sql
-- 统计孤立提现记录数
SELECT COUNT(*) AS orphan_cnt
FROM wa_withdraw w
WHERE NOT EXISTS (
SELECT 1 FROM wa_users u WHERE u.id = w.user_id
);
-- 抽样查看前 100 条,核对是否确实需要清理
SELECT w.*
FROM wa_withdraw w
WHERE NOT EXISTS (
SELECT 1 FROM wa_users u WHERE u.id = w.user_id
)
ORDER BY w.id
LIMIT 100;
```
### 2) 删除 SQL
```sql
DELETE w FROM wa_withdraw w
WHERE NOT EXISTS (
SELECT 1 FROM wa_users u WHERE u.id = w.user_id
);
```
### 3) 操作备注
- 建议在执行前对 wa_withdraw 做一次备份:`CREATE TABLE wa_withdraw_bak_20260423 AS SELECT * FROM wa_withdraw;`
- 建议先跑 `COUNT(*)``LIMIT 100` 人工核对,再执行 `DELETE`
- 全程放入事务中执行,确认无误后再 `COMMIT`
## 相关文件

View File

@@ -0,0 +1,5 @@
VITE_APP_ENV=development
VITE_API_BASE_URL=/api/admin
VITE_MOCK_ENABLED=false
VITE_APP_TITLE=经营驾驶舱
VITE_BUILD_VERSION=local

24
dashboard-frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,122 @@
# Dashboard Frontend
独立 H5 经营驾驶舱前端项目。第一阶段只使用本地 Mock 数据,不对接后端 API。
## 技术栈
- React 19
- TypeScript
- Vite
- antd-mobile
- TanStack Query
- Zustand
- Axios
- ECharts
- MSW
- Vitest
## 本地开发
```bash
nvm use --delete-prefix v24.14.1
pnpm install
pnpm dev
```
默认访问:
```text
http://localhost:5174/h5/dashboard/boss
```
## 第一阶段范围
- H5 移动端老板驾驶舱首页
- 昨日经营核心 KPI
- 今日 10:15 / 14:55 节点快报 Mock
- 近 7 天交易趋势
- 用户、团队、商品排行
- 风险预警摘要
- 底部 Tab 导航
- MSW Mock 数据
## 校验
```bash
pnpm typecheck
pnpm test -- --run
pnpm build
```
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

View File

@@ -0,0 +1,22 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist', 'public/mockServiceWorker.js']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
globals: globals.browser,
},
},
])

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>dashboard-frontend</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,50 @@
{
"name": "dashboard-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"typecheck": "tsc -b --noEmit",
"test": "vitest"
},
"dependencies": {
"@tanstack/react-query": "^5.100.9",
"antd-mobile": "^5.42.3",
"antd-mobile-icons": "^0.3.0",
"axios": "^1.16.0",
"echarts": "^6.0.0",
"msw": "^2.14.5",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-router-dom": "^7.15.0",
"zustand": "^5.0.13"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^24.12.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^10.2.1",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.5.0",
"jsdom": "^29.1.1",
"typescript": "~6.0.2",
"typescript-eslint": "^8.58.2",
"vite": "^8.0.10",
"vitest": "^4.1.5"
},
"msw": {
"workerDirectory": [
"public"
]
}
}

3461
dashboard-frontend/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,349 @@
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker.
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
*/
const PACKAGE_VERSION = '2.14.5'
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
addEventListener('install', function () {
self.skipWaiting()
})
addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim())
})
addEventListener('message', async function (event) {
const clientId = Reflect.get(event.source || {}, 'id')
if (!clientId || !self.clients) {
return
}
const client = await self.clients.get(clientId)
if (!client) {
return
}
const allClients = await self.clients.matchAll({
type: 'window',
})
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
})
break
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: {
packageVersion: PACKAGE_VERSION,
checksum: INTEGRITY_CHECKSUM,
},
})
break
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId)
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: {
client: {
id: client.id,
frameType: client.frameType,
},
},
})
break
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId)
const remainingClients = allClients.filter((client) => {
return client.id !== clientId
})
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister()
}
break
}
}
})
addEventListener('fetch', function (event) {
const requestInterceptedAt = Date.now()
// Bypass navigation requests.
if (event.request.mode === 'navigate') {
return
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (
event.request.cache === 'only-if-cached' &&
event.request.mode !== 'same-origin'
) {
return
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been terminated (still remains active until the next reload).
if (activeClientIds.size === 0) {
return
}
const requestId = crypto.randomUUID()
event.respondWith(handleRequest(event, requestId, requestInterceptedAt))
})
/**
* @param {FetchEvent} event
* @param {string} requestId
* @param {number} requestInterceptedAt
*/
async function handleRequest(event, requestId, requestInterceptedAt) {
const client = await resolveMainClient(event)
const requestCloneForEvents = event.request.clone()
const response = await getResponse(
event,
client,
requestId,
requestInterceptedAt,
)
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
const serializedRequest = await serializeRequest(requestCloneForEvents)
// Clone the response so both the client and the library could consume it.
const responseClone = response.clone()
sendToClient(
client,
{
type: 'RESPONSE',
payload: {
isMockedResponse: IS_MOCKED_RESPONSE in response,
request: {
id: requestId,
...serializedRequest,
},
response: {
type: responseClone.type,
status: responseClone.status,
statusText: responseClone.statusText,
headers: Object.fromEntries(responseClone.headers.entries()),
body: responseClone.body,
},
},
},
responseClone.body ? [serializedRequest.body, responseClone.body] : [],
)
}
return response
}
/**
* Resolve the main client for the given event.
* Client that issues a request doesn't necessarily equal the client
* that registered the worker. It's with the latter the worker should
* communicate with during the response resolving phase.
* @param {FetchEvent} event
* @returns {Promise<Client | undefined>}
*/
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId)
if (activeClientIds.has(event.clientId)) {
return client
}
if (client?.frameType === 'top-level') {
return client
}
const allClients = await self.clients.matchAll({
type: 'window',
})
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible'
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id)
})
}
/**
* @param {FetchEvent} event
* @param {Client | undefined} client
* @param {string} requestId
* @param {number} requestInterceptedAt
* @returns {Promise<Response>}
*/
async function getResponse(event, client, requestId, requestInterceptedAt) {
// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
const requestClone = event.request.clone()
function passthrough() {
// Cast the request headers to a new Headers instance
// so the headers can be manipulated with.
const headers = new Headers(requestClone.headers)
// Remove the "accept" header value that marked this request as passthrough.
// This prevents request alteration and also keeps it compliant with the
// user-defined CORS policies.
const acceptHeader = headers.get('accept')
if (acceptHeader) {
const values = acceptHeader.split(',').map((value) => value.trim())
const filteredValues = values.filter(
(value) => value !== 'msw/passthrough',
)
if (filteredValues.length > 0) {
headers.set('accept', filteredValues.join(', '))
} else {
headers.delete('accept')
}
}
return fetch(requestClone, { headers })
}
// Bypass mocking when the client is not active.
if (!client) {
return passthrough()
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return passthrough()
}
// Notify the client that a request has been intercepted.
const serializedRequest = await serializeRequest(event.request)
const clientMessage = await sendToClient(
client,
{
type: 'REQUEST',
payload: {
id: requestId,
interceptedAt: requestInterceptedAt,
...serializedRequest,
},
},
[serializedRequest.body],
)
switch (clientMessage.type) {
case 'MOCK_RESPONSE': {
return respondWithMock(clientMessage.data)
}
case 'PASSTHROUGH': {
return passthrough()
}
}
return passthrough()
}
/**
* @param {Client} client
* @param {any} message
* @param {Array<Transferable>} transferrables
* @returns {Promise<any>}
*/
function sendToClient(client, message, transferrables = []) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error)
}
resolve(event.data)
}
client.postMessage(message, [
channel.port2,
...transferrables.filter(Boolean),
])
})
}
/**
* @param {Response} response
* @returns {Response}
*/
function respondWithMock(response) {
// Setting response status code to 0 is a no-op.
// However, when responding with a "Response.error()", the produced Response
// instance will have status code set to 0. Since it's not possible to create
// a Response instance with status code 0, handle that use-case separately.
if (response.status === 0) {
return Response.error()
}
const mockedResponse = new Response(response.body, response)
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
value: true,
enumerable: true,
})
return mockedResponse
}
/**
* @param {Request} request
*/
async function serializeRequest(request) {
return {
url: request.url,
mode: request.mode,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: await request.arrayBuffer(),
keepalive: request.keepalive,
}
}

View File

@@ -0,0 +1,27 @@
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'
import { AppProviders } from './app/providers/AppProviders'
import { MobileLayout } from './app/layouts/MobileLayout'
import { BossDashboardPage } from './features/boss-dashboard/pages/BossDashboardPage'
import { DailyReportPage, ProfilePage, RiskCenterPage, TodaySnapshotPage } from './features/boss-dashboard/pages/OperationsPages'
function App() {
return (
<AppProviders>
<BrowserRouter>
<Routes>
<Route path="/" element={<Navigate to="/h5/dashboard/boss" replace />} />
<Route element={<MobileLayout />}>
<Route path="/h5/dashboard/boss" element={<BossDashboardPage />} />
<Route path="/h5/dashboard/daily-report" element={<DailyReportPage />} />
<Route path="/h5/dashboard/today-snapshot" element={<TodaySnapshotPage />} />
<Route path="/h5/dashboard/risk-center" element={<RiskCenterPage />} />
<Route path="/h5/dashboard/profile" element={<ProfilePage />} />
</Route>
<Route path="*" element={<Navigate to="/h5/dashboard/boss" replace />} />
</Routes>
</BrowserRouter>
</AppProviders>
)
}
export default App

View File

@@ -0,0 +1,34 @@
import { AppOutline, BellOutline, FileOutline, HistogramOutline, UserOutline } from 'antd-mobile-icons'
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
import { SafeArea, TabBar } from 'antd-mobile'
const tabs = [
{ key: '/h5/dashboard/boss', title: '首页', icon: <AppOutline /> },
{ key: '/h5/dashboard/daily-report', title: '日报', icon: <FileOutline /> },
{ key: '/h5/dashboard/today-snapshot', title: '快报', icon: <HistogramOutline /> },
{ key: '/h5/dashboard/risk-center', title: '风险', icon: <BellOutline /> },
{ key: '/h5/dashboard/profile', title: '我的', icon: <UserOutline /> },
]
export function MobileLayout() {
const location = useLocation()
const navigate = useNavigate()
const activeKey = tabs.find((tab) => location.pathname.startsWith(tab.key))?.key ?? tabs[0].key
return (
<div className="mobile-shell">
<main className="mobile-main">
<Outlet />
</main>
<nav className="bottom-nav" aria-label="Dashboard mobile navigation">
<TabBar activeKey={activeKey} onChange={(key) => navigate(key)}>
{tabs.map((tab) => (
<TabBar.Item key={tab.key} icon={tab.icon} title={tab.title} />
))}
</TabBar>
<SafeArea position="bottom" />
</nav>
</div>
)
}

View File

@@ -0,0 +1,26 @@
import { ConfigProvider } from 'antd-mobile'
import zhCN from 'antd-mobile/es/locales/zh-CN'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import type { ReactNode } from 'react'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
retry: 1,
refetchOnWindowFocus: false,
},
},
})
type AppProvidersProps = {
children: ReactNode
}
export function AppProviders({ children }: AppProvidersProps) {
return (
<ConfigProvider locale={zhCN}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</ConfigProvider>
)
}

View File

@@ -0,0 +1,76 @@
import * as echarts from 'echarts/core'
import { GridComponent, TooltipComponent } from 'echarts/components'
import { BarChart, LineChart } from 'echarts/charts'
import { CanvasRenderer } from 'echarts/renderers'
import { useEffect, useMemo, useRef } from 'react'
import type { TrendPoint } from '../../features/boss-dashboard/types'
echarts.use([GridComponent, TooltipComponent, LineChart, BarChart, CanvasRenderer])
type MiniTrendChartProps = {
data: TrendPoint[]
}
export function MiniTrendChart({ data }: MiniTrendChartProps) {
const chartRef = useRef<HTMLDivElement | null>(null)
const option = useMemo(
() => ({
color: ['#ff5b36', '#ffb000'],
grid: { left: 8, right: 8, top: 24, bottom: 18, containLabel: true },
tooltip: {
trigger: 'axis',
confine: true,
valueFormatter: (value: number | string) => Number(value).toLocaleString('zh-CN'),
},
xAxis: {
type: 'category',
data: data.map((item) => item.date),
axisLine: { show: false },
axisTick: { show: false },
},
yAxis: [
{ type: 'value', show: false },
{ type: 'value', show: false },
],
series: [
{
name: '成交额',
type: 'line',
smooth: true,
yAxisIndex: 0,
data: data.map((item) => item.amount),
symbol: 'circle',
symbolSize: 5,
lineStyle: { width: 3 },
areaStyle: { opacity: 0.08 },
},
{
name: '订单数',
type: 'bar',
yAxisIndex: 1,
data: data.map((item) => item.orders),
barWidth: 8,
borderRadius: 6,
},
],
}),
[data],
)
useEffect(() => {
if (!chartRef.current) return undefined
const chart = echarts.init(chartRef.current)
chart.setOption(option)
const resize = () => chart.resize()
window.addEventListener('resize', resize)
return () => {
window.removeEventListener('resize', resize)
chart.dispose()
}
}, [option])
return <div className="mini-trend-chart" ref={chartRef} aria-label="近 7 天交易趋势图" />
}

View File

@@ -0,0 +1,32 @@
import { Skeleton } from 'antd-mobile'
import { formatMetricValue, formatTrend } from '../../utils/format'
import type { KpiMetric } from '../../features/boss-dashboard/types'
type KpiCardProps = {
metric: KpiMetric
loading?: boolean
}
export function KpiCard({ metric, loading }: KpiCardProps) {
if (loading) {
return (
<article className="kpi-card">
<Skeleton.Title animated />
<Skeleton.Paragraph lineCount={1} animated />
</article>
)
}
return (
<article className={`kpi-card kpi-card--${metric.status} ${metric.featured ? 'kpi-card--featured' : ''}`}>
<p className="kpi-title">{metric.title}</p>
<strong className="kpi-value">{formatMetricValue(metric.value, metric.unit)}</strong>
{(metric.trendLabel || metric.trendValue !== undefined) && (
<p className="kpi-trend">
{metric.trendLabel}
{metric.trendValue !== undefined && <span>{formatTrend(metric.trendValue)}</span>}
</p>
)}
</article>
)
}

View File

@@ -0,0 +1,18 @@
import { useQuery } from '@tanstack/react-query'
import { getApiData, getBlob } from '../../services/http/client'
import type { DashboardOverview } from './types'
export const dashboardQueryKeys = {
overview: (date?: string) => ['dashboard', 'overview', date ?? 'default'] as const,
}
export function useDashboardOverview(date?: string) {
return useQuery({
queryKey: dashboardQueryKeys.overview(date),
queryFn: () => getApiData<DashboardOverview>(date ? `/dashboard/overview?date=${date}` : '/dashboard/overview'),
})
}
export function downloadDailyReportArchive(date?: string) {
return getBlob(date ? `/dashboard/daily-report/archive?date=${date}` : '/dashboard/daily-report/archive')
}

View File

@@ -0,0 +1,215 @@
import { formatMetricValue, formatMoney, formatNumber } from '../../utils/format'
import type { DashboardOverview, MetricStatus, RankItem, RiskAlert, RiskLevel, SnapshotSlot, TodaySnapshot } from './types'
const snapshotTitle: Record<SnapshotSlot, string> = {
'1015': '上午抢购快报',
'1455': '下午寄卖/转卖快报',
}
const snapshotDescription: Record<SnapshotSlot, string> = {
'1015': '用户集中抢购上一天用户寄卖的商品,重点看成交、付款和采购用户是否达标。',
'1455': '用户把上午抢到的商品继续寄卖或转卖,重点看新增寄售供给和奖金变化是否正常。',
}
const metricStatusText: Record<MetricStatus, string> = {
normal: '正常',
success: '达标',
warning: '关注',
danger: '异常',
}
const riskLevelText: Record<RiskLevel, string> = {
red: '红色',
yellow: '黄色',
gray: '灰色',
}
function escapeHtml(value: unknown): string {
return String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;')
}
function serializeStaticData(data: DashboardOverview): string {
return JSON.stringify(data, null, 2).replaceAll('<', '\\u003c').replaceAll('>', '\\u003e')
}
function renderMetricGrid(metrics: DashboardOverview['kpis']): string {
return metrics
.map(
(metric) => `
<article class="card metric-card">
<span>${escapeHtml(metricStatusText[metric.status])}</span>
<h3>${escapeHtml(metric.title)}</h3>
<strong>${escapeHtml(formatMetricValue(metric.value, metric.unit))}</strong>
${metric.trendLabel ? `<p>${escapeHtml(metric.trendLabel)} ${escapeHtml(metric.trendValue ?? '')}%</p>` : ''}
</article>`,
)
.join('')
}
function renderSnapshots(snapshots: TodaySnapshot[]): string {
return snapshots
.map((snapshot) => {
const bonusChange = Number(snapshot.selfBonusChange) + Number(snapshot.shareBonusChange)
return `
<article class="card snapshot-card">
<div class="card-title-row">
<div>
<span>${escapeHtml(snapshot.slot)}</span>
<h3>${escapeHtml(snapshotTitle[snapshot.slot])}</h3>
</div>
<mark>${escapeHtml(snapshot.status)}</mark>
</div>
<p>${escapeHtml(snapshotDescription[snapshot.slot])}</p>
<p class="message">${escapeHtml(snapshot.message)}</p>
${snapshot.generatedAt ? `<small>生成时间:${escapeHtml(snapshot.generatedAt)}</small>` : ''}
<div class="snapshot-grid">
<span>用户<strong>${escapeHtml(formatNumber(snapshot.purchaseUsers))}人</strong></span>
<span>订单<strong>${escapeHtml(formatNumber(snapshot.orderCount))}单</strong></span>
<span>成交额<strong>${escapeHtml(formatMoney(snapshot.dealAmount))}</strong></span>
<span>已支付<strong>${escapeHtml(formatMoney(snapshot.paidAmount))}</strong></span>
<span>商品<strong>${escapeHtml(formatNumber(snapshot.newMerchandiseCount))}件</strong></span>
<span>奖金<strong>${escapeHtml(formatMoney(bonusChange))}</strong></span>
</div>
</article>`
})
.join('')
}
function renderRanks(title: string, ranks: RankItem[]): string {
return `
<section class="section">
<h2>${escapeHtml(title)}</h2>
<div class="rank-list">
${ranks
.map(
(rank, index) => `
<div class="rank-item">
<b>${index + 1}</b>
<span>
<strong>${escapeHtml(rank.name)}</strong>
<small>${escapeHtml(rank.description)}</small>
</span>
<em>${escapeHtml(formatMoney(rank.value))}</em>
</div>`,
)
.join('')}
</div>
</section>`
}
function renderRisks(risks: RiskAlert[]): string {
return risks
.map(
(risk) => `
<article class="risk risk--${risk.level}">
<div>
<mark>${escapeHtml(riskLevelText[risk.level])}</mark>
<span>${escapeHtml(risk.type)}</span>
<time>${escapeHtml(risk.discoveredAt)}</time>
</div>
<strong>${escapeHtml(risk.title)}</strong>
<p>${escapeHtml(risk.description)}</p>
</article>`,
)
.join('')
}
export function buildDailyReportArchiveHtml(data: DashboardOverview): string {
const generatedAt = new Date().toLocaleString('zh-CN', { hour12: false })
return `<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>经营日报归档 - ${escapeHtml(data.businessDate)}</title>
<style>
:root { --bg: #fff6f1; --surface: #fff; --surface-soft: #f6f9fb; --text: #132033; --muted: #6b7a90; --border: rgba(19, 32, 51, .08); --primary: #ff5b36; --warning: #ffb000; --danger: #dc2626; --shadow: 0 16px 40px rgba(255, 91, 54, .14); --radius-xl: 28px; --radius-lg: 20px; --radius-md: 14px; }
* { box-sizing: border-box; }
body { min-width: 320px; margin: 0; color: var(--text); background: radial-gradient(circle at top left, rgba(255, 91, 54, .2), transparent 28rem), var(--bg); font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; -webkit-font-smoothing: antialiased; }
main { width: min(100%, 430px); min-height: 100svh; margin: 0 auto; padding: 14px 14px 24px; background: var(--bg); box-shadow: 0 0 0 1px rgba(19, 32, 51, .04); }
.hero { position: relative; overflow: hidden; padding: 20px; color: #fff; background: linear-gradient(145deg, rgba(255, 91, 54, .98), rgba(255, 139, 82, .92)), radial-gradient(circle at 90% 10%, rgba(255, 176, 0, .42), transparent 18rem); border-radius: 0 0 var(--radius-xl) var(--radius-xl); box-shadow: var(--shadow); }
.hero p { margin: 0; color: rgba(255, 255, 255, .76); line-height: 1.6; }
.eyebrow, .card-title-row span, .metric-card > span { margin: 0; color: rgba(255, 255, 255, .68); font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
h1, h2, h3 { margin: 0; }
h1 { margin: 12px 0 8px; font-size: 28px; line-height: 1.12; }
h2 { font-size: 18px; margin-bottom: 12px; }
h3 { font-size: 16px; }
.meta { display: grid; gap: 8px; margin-top: 16px; }
.meta span { display: inline-flex; width: max-content; padding: 7px 12px; color: rgba(255, 255, 255, .86); font-size: 12px; font-weight: 700; background: rgba(255, 255, 255, .14); border: 1px solid rgba(255, 255, 255, .18); border-radius: 999px; }
.section { margin-top: 14px; padding: 16px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-xl); box-shadow: 0 10px 28px rgba(22, 47, 80, .08); }
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.card { padding: 14px; background: var(--surface-soft); border: 0; border-radius: var(--radius-lg); }
.metric-card { min-height: 112px; background: var(--surface); border: 1px solid var(--border); box-shadow: 0 10px 28px rgba(22, 47, 80, .08); }
.metric-card > span { color: var(--muted); }
.metric-card strong { display: block; margin-top: 8px; color: var(--text); font-size: 22px; line-height: 1.08; word-break: break-all; }
.metric-card p { margin: 8px 0 0; color: var(--muted); font-size: 12px; }
.snapshot-stack, .trend-list, .rank-list, .risk-list { display: grid; gap: 10px; margin-top: 14px; }
.card-title-row, .rank-item, .risk div { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.card-title-row span { color: var(--muted); }
mark { padding: 3px 8px; color: var(--primary); font-size: 12px; font-weight: 700; background: #fff0eb; border: 0; border-radius: 999px; }
.snapshot-card p { margin: 12px 0 0; color: var(--muted); font-size: 13px; line-height: 1.55; }
.snapshot-card .message { margin: 10px 0 8px; color: var(--text); font-weight: 700; line-height: 1.55; }
.snapshot-card small { color: var(--muted); }
.snapshot-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-top: 12px; }
.snapshot-grid span, .trend-item, .rank-item, .risk { padding: 12px; background: var(--surface-soft); border: 0; border-radius: var(--radius-md); }
.snapshot-grid span { color: var(--muted); font-size: 12px; }
.snapshot-grid strong { display: block; margin-top: 4px; color: var(--text); font-size: 15px; }
.trend-item { display: grid; gap: 4px; }
.trend-item span { color: var(--muted); font-size: 13px; }
.rank-item b { width: 28px; height: 28px; display: inline-grid; place-items: center; border-radius: 10px; color: #fff; background: var(--primary); }
.rank-item span { flex: 1; }
.rank-item small { display: block; margin-top: 3px; color: var(--muted); line-height: 1.45; }
.rank-item em { color: var(--primary); font-size: 13px; font-style: normal; font-weight: 800; }
.risk strong { display: block; margin-top: 10px; }
.risk p { margin: 6px 0 0; color: var(--muted); line-height: 1.5; }
.risk--red mark { color: #991b1b; background: #fee2e2; }
.risk--yellow mark { color: #92400e; background: #fef3c7; }
.risk--gray mark { color: #475569; background: #e2e8f0; }
</style>
</head>
<body>
<main>
<section class="hero">
<p class="eyebrow">Daily Report Archive</p>
<h1>经营日报归档 ${escapeHtml(data.businessDate)}</h1>
<p>${escapeHtml(data.summary)}</p>
<div class="meta">
<span>业务日期:${escapeHtml(data.businessDate)}</span>
<span>数据生成:${escapeHtml(data.generatedAt)}</span>
<span>归档生成:${escapeHtml(generatedAt)}</span>
</div>
</section>
<section class="section"><h2>核心指标</h2><div class="grid">${renderMetricGrid(data.kpis)}</div></section>
<section class="section"><h2>资金池摘要</h2><div class="grid">${renderMetricGrid(data.fundPool)}</div></section>
<section class="section"><h2>今日快报</h2><div class="snapshot-stack">${renderSnapshots(data.snapshots)}</div></section>
<section class="section">
<h2>最近趋势</h2>
<div class="trend-list">
${data.trends
.map(
(trend) => `
<div class="trend-item">
<strong>${escapeHtml(trend.date)}</strong>
<span>成交 ${escapeHtml(formatMoney(trend.amount))}</span>
<span>订单 ${escapeHtml(formatNumber(trend.orders))} 单</span>
<span>奖金 ${escapeHtml(formatMoney(trend.bonus))}</span>
</div>`,
)
.join('')}
</div>
</section>
${renderRanks('高价值用户', data.userRanks)}
${renderRanks('团队贡献排行', data.teamRanks)}
${renderRanks('高货值未成交商品', data.productRanks)}
<section class="section"><h2>风险预警</h2><div class="risk-list">${renderRisks(data.risks)}</div></section>
<script id="dashboard-static-data" type="application/json">${serializeStaticData(data)}</script>
</main>
</body>
</html>`
}

View File

@@ -0,0 +1,38 @@
import { RightOutline } from 'antd-mobile-icons'
import { formatMoney, formatNumber } from '../../../utils/format'
import type { RankItem } from '../types'
type RankListProps = {
title: string
items: RankItem[]
valueType?: 'money' | 'number'
}
export function RankList({ title, items, valueType = 'money' }: RankListProps) {
return (
<section className="section-block">
<div className="section-title-row">
<div>
<p className="section-kicker">Top 3</p>
<h2>{title}</h2>
</div>
<button className="text-button" type="button">
<RightOutline />
</button>
</div>
<div className="rank-list">
{items.map((item, index) => (
<button className="rank-item" key={item.id} type="button">
<span className="rank-index">{index + 1}</span>
<span className="rank-content">
<strong>{item.name}</strong>
<small>{item.description}</small>
</span>
<span className="rank-value">{valueType === 'money' ? formatMoney(item.value) : formatNumber(item.value)}</span>
</button>
))}
</div>
</section>
)
}

View File

@@ -0,0 +1,42 @@
import { Tag } from 'antd-mobile'
import type { RiskAlert, RiskLevel } from '../types'
type RiskAlertSectionProps = {
risks: RiskAlert[]
}
const levelMeta: Record<RiskLevel, { color: 'danger' | 'warning' | 'default'; label: string }> = {
red: { color: 'danger', label: '红色' },
yellow: { color: 'warning', label: '黄色' },
gray: { color: 'default', label: '灰色' },
}
export function RiskAlertSection({ risks }: RiskAlertSectionProps) {
return (
<section className="section-block">
<div className="section-title-row">
<div>
<p className="section-kicker">Risk</p>
<h2></h2>
</div>
<span className="risk-count">{risks.length} </span>
</div>
<div className="risk-list">
{risks.map((risk) => {
const meta = levelMeta[risk.level]
return (
<button className="risk-item" key={risk.id} type="button">
<div className="risk-header">
<Tag color={meta.color}>{meta.label}</Tag>
<span>{risk.type}</span>
<time>{risk.discoveredAt}</time>
</div>
<strong>{risk.title}</strong>
<p>{risk.description}</p>
</button>
)
})}
</div>
</section>
)
}

View File

@@ -0,0 +1,64 @@
import { CapsuleTabs, Tag } from 'antd-mobile'
import { useState } from 'react'
import { formatMoney, formatNumber } from '../../../utils/format'
import type { SnapshotSlot, TodaySnapshot } from '../types'
type TodaySnapshotSectionProps = {
snapshots: TodaySnapshot[]
}
const statusMap = {
pending: { color: 'default', label: '待生成' },
success: { color: 'success', label: '已生成' },
failed: { color: 'danger', label: '生成失败' },
temporary: { color: 'warning', label: '临时数据' },
} as const
export function TodaySnapshotSection({ snapshots }: TodaySnapshotSectionProps) {
const [activeSlot, setActiveSlot] = useState<SnapshotSlot>('1015')
const activeSnapshot = snapshots.find((snapshot) => snapshot.slot === activeSlot) ?? snapshots[0]
const status = statusMap[activeSnapshot.status]
return (
<section className="section-block snapshot-section">
<div className="section-title-row">
<div>
<p className="section-kicker"></p>
<h2> / </h2>
</div>
<Tag color={status.color}>{status.label}</Tag>
</div>
<CapsuleTabs activeKey={activeSlot} onChange={(key) => setActiveSlot(key as SnapshotSlot)}>
{snapshots.map((snapshot) => (
<CapsuleTabs.Tab title={snapshot.title.replace(' ', '')} key={snapshot.slot} />
))}
</CapsuleTabs>
<div className="snapshot-card">
<p className="snapshot-message">{activeSnapshot.message}</p>
{activeSnapshot.generatedAt && <p className="snapshot-time">{activeSnapshot.generatedAt}</p>}
<div className="snapshot-grid">
<span>
<strong>{formatNumber(activeSnapshot.purchaseUsers)}</strong>
</span>
<span>
<strong>{formatNumber(activeSnapshot.orderCount)}</strong>
</span>
<span>
<strong>{formatMoney(activeSnapshot.dealAmount)}</strong>
</span>
<span>
<strong>{formatMoney(activeSnapshot.paidAmount)}</strong>
</span>
<span>
<strong>{formatNumber(activeSnapshot.newMerchandiseCount)}</strong>
</span>
<span>
<strong>{formatMoney(Number(activeSnapshot.selfBonusChange) + Number(activeSnapshot.shareBonusChange))}</strong>
</span>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,104 @@
import type { DashboardOverview } from './types'
export const dashboardMock: DashboardOverview = {
businessDate: '2026-05-10',
generatedAt: '2026-05-11 00:10:12',
summary: '昨日成交保持稳定,采购用户略有增长;资金风险主要集中在大额待提现和积分比例异常。',
kpis: [
{ key: 'dealAmount', title: '昨日成交额', value: 1289360.42, unit: '元', trendLabel: '较前日', trendValue: 8.6, status: 'success', featured: true },
{ key: 'orderCount', title: '昨日订单数', value: 1842, unit: '单', trendLabel: '较前日', trendValue: 4.1, status: 'success' },
{ key: 'purchaseUsers', title: '采购用户', value: 936, unit: '人', trendLabel: '较前日', trendValue: 2.7, status: 'success' },
{ key: 'newUsers', title: '新增用户', value: 318, unit: '人', trendLabel: '较前日', trendValue: -3.2, status: 'warning' },
{ key: 'newMerchandise', title: '新增寄售商品', value: 472, unit: '件', trendLabel: '较前日', trendValue: 12.4, status: 'success' },
{ key: 'selfBonus', title: '个人奖金发放', value: 168230.36, unit: '元', trendLabel: '较前日', trendValue: 6.8, status: 'normal' },
{ key: 'shareBonus', title: '推广奖金发放', value: 82460.18, unit: '元', trendLabel: '较前日', trendValue: 1.9, status: 'normal' },
{ key: 'pendingAmount', title: '待支付/待结算', value: 95620.11, unit: '元', trendLabel: '需关注', status: 'warning' },
],
fundPool: [
{ key: 'balance', title: '余额总额', value: 728903.22, unit: '元', status: 'normal' },
{ key: 'coupon', title: '优惠券总额', value: 391082.88, unit: '元', status: 'normal' },
{ key: 'selfBonusPool', title: '个人奖金总额', value: 836942.14, unit: '元', status: 'warning' },
{ key: 'shareBonusPool', title: '推广奖金总额', value: 295402.77, unit: '元', status: 'normal' },
{ key: 'integral', title: '积分总额', value: 418471.07, unit: '分', status: 'normal' },
{ key: 'withdrawPending', title: '待审核提现', value: 63200, unit: '元', status: 'danger' },
],
snapshots: [
{
slot: '1015',
title: '10:15 上午快报',
status: 'success',
generatedAt: '2026-05-11 10:15:08',
message: '上午抢购节点已完成,上一日寄卖商品消化情况良好,采购用户和成交额略高于昨日同节点。',
purchaseUsers: 421,
orderCount: 756,
dealAmount: 526880.2,
paidAmount: 498320.5,
newMerchandiseCount: 185,
selfBonusChange: 64230.3,
shareBonusChange: 31820.1,
},
{
slot: '1455',
title: '14:55 下午快报',
status: 'pending',
message: '下午寄卖/转卖节点尚未生成,预计 14:55 后可查看用户抢购商品的再次上架情况。',
purchaseUsers: 0,
orderCount: 0,
dealAmount: 0,
paidAmount: 0,
newMerchandiseCount: 0,
selfBonusChange: 0,
shareBonusChange: 0,
},
],
trends: [
{ date: '05-04', amount: 948000, orders: 1390, newUsers: 226, bonus: 186000 },
{ date: '05-05', amount: 1024000, orders: 1512, newUsers: 251, bonus: 194000 },
{ date: '05-06', amount: 1119000, orders: 1604, newUsers: 287, bonus: 205000 },
{ date: '05-07', amount: 1086000, orders: 1542, newUsers: 243, bonus: 198000 },
{ date: '05-08', amount: 1198000, orders: 1731, newUsers: 302, bonus: 221000 },
{ date: '05-09', amount: 1187200, orders: 1769, newUsers: 329, bonus: 229000 },
{ date: '05-10', amount: 1289360, orders: 1842, newUsers: 318, bonus: 250690 },
],
userRanks: [
{ id: 'u1', name: '刘先生', value: 96520, description: '个人奖金 + 推广奖金 + 积分折算', badge: '高价值' },
{ id: 'u2', name: '陈女士', value: 81230, description: '昨日采购 12 单', badge: '活跃' },
{ id: 'u3', name: '周先生', value: 75880, description: '团队新增 18 人' },
],
teamRanks: [
{ id: 't1', name: '华东一队', value: 386200, description: '成交额第一,团队收益 4.8 万', badge: 'TOP1' },
{ id: 't2', name: '苏州团队', value: 318760, description: '采购用户 182 人' },
{ id: 't3', name: '扬州团队', value: 287500, description: '新增成员 36 人' },
],
productRanks: [
{ id: 'p1', name: '高端礼盒 A 款', value: 128800, description: '上架 7 天未成交', badge: '滞销' },
{ id: 'p2', name: '精选组合 B 款', value: 98600, description: '高货值待成交' },
{ id: 'p3', name: '会员专享 C 款', value: 83500, description: '浏览高,成交低' },
],
risks: [
{
id: 'r1',
level: 'red',
type: '资金',
title: '大额待审核提现',
description: '当前待审核提现 6.32 万,建议今日处理。',
discoveredAt: '11:00',
},
{
id: 'r2',
level: 'yellow',
type: '积分',
title: '积分与个人奖金比例异常',
description: '发现 3 名用户积分未接近个人奖金的 1/2。',
discoveredAt: '10:40',
},
{
id: 'r3',
level: 'gray',
type: '数据',
title: '用户资料不一致',
description: 'wa_users 与 eb_user 有 5 条手机号不一致。',
discoveredAt: '09:55',
},
],
}

View File

@@ -0,0 +1,105 @@
import { Button, DotLoading, ErrorBlock } from 'antd-mobile'
import { KpiCard } from '../../../components/kpi/KpiCard'
import { MiniTrendChart } from '../../../components/charts/MiniTrendChart'
import { formatMoney } from '../../../utils/format'
import { useDashboardOverview } from '../api'
import { RankList } from '../components/RankList'
import { RiskAlertSection } from '../components/RiskAlertSection'
import { TodaySnapshotSection } from '../components/TodaySnapshotSection'
export function BossDashboardPage() {
const { data, isLoading, isError, refetch } = useDashboardOverview()
if (isLoading) {
return (
<section className="loading-page">
<DotLoading color="primary" />
<p>...</p>
</section>
)
}
if (isError || !data) {
return (
<section className="error-page">
<ErrorBlock status="default" title="驾驶舱加载失败" description="后端接口暂不可用,请确认服务、登录态或接口权限后重试。" />
<Button color="primary" onClick={() => void refetch()}>
</Button>
</section>
)
}
const coreKpis = data.kpis.slice(0, 4)
const moreKpis = data.kpis.slice(4)
return (
<section className="dashboard-page">
<header className="dashboard-hero">
<div className="hero-topline">
<span></span>
<button type="button"></button>
</div>
<p className="eyebrow"> {data.businessDate}</p>
<h1></h1>
<p className="hero-summary">{data.summary}</p>
<div className="hero-metric">
<span></span>
<strong>{formatMoney(data.kpis[0]?.value)}</strong>
<small>{data.generatedAt}</small>
</div>
</header>
<section className="kpi-grid" aria-label="核心经营指标">
{coreKpis.map((metric) => (
<KpiCard key={metric.key} metric={metric} />
))}
</section>
<section className="section-block compact-section">
<div className="section-title-row">
<div>
<p className="section-kicker">More</p>
<h2></h2>
</div>
</div>
<div className="kpi-grid kpi-grid--compact">
{moreKpis.map((metric) => (
<KpiCard key={metric.key} metric={metric} />
))}
</div>
</section>
<TodaySnapshotSection snapshots={data.snapshots} />
<section className="section-block">
<div className="section-title-row">
<div>
<p className="section-kicker">Trend</p>
<h2> 7 </h2>
</div>
</div>
<MiniTrendChart data={data.trends} />
</section>
<section className="section-block compact-section">
<div className="section-title-row">
<div>
<p className="section-kicker">Fund</p>
<h2></h2>
</div>
</div>
<div className="kpi-grid kpi-grid--compact">
{data.fundPool.map((metric) => (
<KpiCard key={metric.key} metric={metric} />
))}
</div>
</section>
<RankList title="高价值用户" items={data.userRanks} />
<RankList title="团队贡献排行" items={data.teamRanks} />
<RankList title="高货值未成交商品" items={data.productRanks} />
<RiskAlertSection risks={data.risks} />
</section>
)
}

View File

@@ -0,0 +1,470 @@
import { Button, CapsuleTabs, DotLoading, ErrorBlock, Tag, Toast } from 'antd-mobile'
import { useMemo, useState } from 'react'
import { MiniTrendChart } from '../../../components/charts/MiniTrendChart'
import { KpiCard } from '../../../components/kpi/KpiCard'
import { formatMoney, formatNumber } from '../../../utils/format'
import { useDashboardOverview } from '../api'
import { buildDailyReportArchiveHtml } from '../archive'
import type { DashboardOverview, RiskLevel, SnapshotSlot, TodaySnapshot } from '../types'
const snapshotStatusMeta = {
pending: { color: 'default', label: '待生成' },
success: { color: 'success', label: '已生成' },
failed: { color: 'danger', label: '失败' },
temporary: { color: 'warning', label: '临时' },
} as const
const riskLevelMeta: Record<RiskLevel, { color: 'danger' | 'warning' | 'default'; label: string }> = {
red: { color: 'danger', label: '红色' },
yellow: { color: 'warning', label: '黄色' },
gray: { color: 'default', label: '灰色' },
}
const snapshotSlotMeta: Record<
SnapshotSlot,
{
title: string
subtitle: string
metricLabels: {
primaryUsers: string
primaryOrders: string
amount: string
paidAmount: string
merchandise: string
bonus: string
}
checklist: string[]
}
> = {
'1015': {
title: '上午抢购快报',
subtitle: '用户集中抢购上一天用户寄卖的商品,重点看成交、付款和采购用户是否达标。',
metricLabels: {
primaryUsers: '抢购用户',
primaryOrders: '抢购订单',
amount: '抢购成交额',
paidAmount: '已支付金额',
merchandise: '成交商品',
bonus: '相关奖金',
},
checklist: ['抢购成交额是否低于昨日同节点', '采购用户是否异常回落', '付款金额与成交额是否明显偏离', '高货值寄卖商品是否完成消化'],
},
'1455': {
title: '下午寄卖/转卖快报',
subtitle: '用户把上午抢到的商品继续寄卖或转卖,重点看新增寄售供给和奖金变化是否正常。',
metricLabels: {
primaryUsers: '寄卖用户',
primaryOrders: '转卖订单',
amount: '转卖成交额',
paidAmount: '回款金额',
merchandise: '新增寄售',
bonus: '奖金变化',
},
checklist: ['抢购商品是否按预期转入寄卖', '新增寄售商品是否满足下午供给', '个人奖金与推广奖金是否同步变化', '转卖回款是否出现异常延迟'],
},
}
function QueryState({
isLoading,
isError,
refetch,
title,
}: {
isLoading: boolean
isError: boolean
refetch: () => void
title: string
}) {
if (isLoading) {
return (
<section className="loading-page">
<DotLoading color="primary" />
<p>{title}...</p>
</section>
)
}
if (isError) {
return (
<section className="error-page">
<ErrorBlock status="default" title={`${title}加载失败`} description="后端接口暂不可用,请确认服务、登录态或接口权限后重试。" />
<Button color="primary" onClick={refetch}>
</Button>
</section>
)
}
return null
}
function OperationsHeader({
kicker,
title,
description,
extra,
}: {
kicker: string
title: string
description: string
extra?: string
}) {
return (
<header className="operations-header">
<p className="eyebrow">{kicker}</p>
<h1>{title}</h1>
<p>{description}</p>
{extra && <span>{extra}</span>}
</header>
)
}
function SnapshotDetailCard({ snapshot }: { snapshot: TodaySnapshot }) {
const status = snapshotStatusMeta[snapshot.status]
const slotMeta = snapshotSlotMeta[snapshot.slot]
return (
<article className="snapshot-detail-card">
<div className="section-title-row">
<div>
<p className="section-kicker">{snapshot.slot}</p>
<h2>{slotMeta.title}</h2>
</div>
<Tag color={status.color}>{status.label}</Tag>
</div>
<p className="snapshot-detail-subtitle">{slotMeta.subtitle}</p>
<p className="snapshot-detail-message">{snapshot.message}</p>
{snapshot.generatedAt && <p className="snapshot-time">{snapshot.generatedAt}</p>}
<div className="snapshot-grid snapshot-grid--wide">
<span>
{slotMeta.metricLabels.primaryUsers}
<strong>{formatNumber(snapshot.purchaseUsers)}</strong>
</span>
<span>
{slotMeta.metricLabels.primaryOrders}
<strong>{formatNumber(snapshot.orderCount)}</strong>
</span>
<span>
{slotMeta.metricLabels.amount}
<strong>{formatMoney(snapshot.dealAmount)}</strong>
</span>
<span>
{slotMeta.metricLabels.paidAmount}
<strong>{formatMoney(snapshot.paidAmount)}</strong>
</span>
<span>
{slotMeta.metricLabels.merchandise}
<strong>{formatNumber(snapshot.newMerchandiseCount)}</strong>
</span>
<span>
{slotMeta.metricLabels.bonus}
<strong>{formatMoney(Number(snapshot.selfBonusChange) + Number(snapshot.shareBonusChange))}</strong>
</span>
</div>
</article>
)
}
function buildDailyReports(data: DashboardOverview) {
return data.trends
.slice(-4)
.reverse()
.map((trend, index) => ({
...trend,
status: index === 0 ? '已生成' : '历史快照',
bonusRate: Number(trend.amount) > 0 ? (Number(trend.bonus) / Number(trend.amount)) * 100 : 0,
}))
}
export function DailyReportPage() {
const { data, isLoading, isError, refetch } = useDashboardOverview()
const [isArchiving, setIsArchiving] = useState(false)
const state = <QueryState isLoading={isLoading} isError={isError || !data} refetch={() => void refetch()} title="经营日报" />
if (!data) return state
const reports = buildDailyReports(data)
const handleArchive = async () => {
try {
setIsArchiving(true)
const html = buildDailyReportArchiveHtml(data)
const blob = new Blob([html], { type: 'text/html;charset=utf-8' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `dashboard-daily-report-${data.businessDate}.html`
document.body.appendChild(link)
link.click()
link.remove()
URL.revokeObjectURL(url)
Toast.show({ icon: 'success', content: '归档 HTML 已生成' })
} catch {
Toast.show({ icon: 'fail', content: '归档生成失败,请稍后重试' })
} finally {
setIsArchiving(false)
}
}
return (
<section className="operations-page">
<OperationsHeader
kicker="Daily Report"
title="经营日报"
description="按日沉淀成交、订单、用户与奖金变化,方便老板回看最近经营节奏。"
extra={`最新数据:${data.businessDate}`}
/>
<section className="section-block compact-section">
<div className="section-title-row">
<div>
<p className="section-kicker">Workday</p>
<h2></h2>
</div>
<Tag color="success"></Tag>
</div>
<div className="kpi-grid kpi-grid--compact">
{data.kpis.slice(0, 4).map((metric) => (
<KpiCard key={metric.key} metric={metric} />
))}
</div>
</section>
<section className="section-block">
<div className="section-title-row">
<div>
<p className="section-kicker">Trend</p>
<h2> 7 </h2>
</div>
</div>
<MiniTrendChart data={data.trends} />
</section>
<section className="section-block">
<div className="section-title-row">
<div>
<p className="section-kicker">Archive</p>
<h2></h2>
</div>
<button className="text-button" type="button" disabled={isArchiving} onClick={() => void handleArchive()}>
{isArchiving ? '生成中...' : '生成归档'}
</button>
</div>
<div className="report-list">
{reports.map((report) => (
<button className="report-item" key={report.date} type="button">
<span>
<strong>{report.date}</strong>
<small>{report.status}</small>
</span>
<span>
<strong>{formatMoney(report.amount)}</strong>
<small>
{formatNumber(report.orders)} / {formatNumber(report.bonusRate, 1)}%
</small>
</span>
</button>
))}
</div>
</section>
</section>
)
}
export function TodaySnapshotPage() {
const { data, isLoading, isError, refetch } = useDashboardOverview()
const state = <QueryState isLoading={isLoading} isError={isError || !data} refetch={() => void refetch()} title="今日快报" />
if (!data) return state
return (
<section className="operations-page">
<OperationsHeader
kicker="Today Snapshot"
title="今日快报"
description="10:15 看上一日寄卖商品的抢购结果14:55 看抢到商品的寄卖/转卖承接情况。"
extra="节点状态随 Mock 场景切换"
/>
<section className="section-block snapshot-page-section">
<div className="section-title-row">
<div>
<p className="section-kicker">Timeline</p>
<h2></h2>
</div>
</div>
<div className="snapshot-stack">
{data.snapshots.map((snapshot) => (
<SnapshotDetailCard key={snapshot.slot} snapshot={snapshot} />
))}
</div>
</section>
<section className="section-block">
<div className="section-title-row">
<div>
<p className="section-kicker">Checklist</p>
<h2></h2>
</div>
</div>
<div className="check-list">
{data.snapshots.flatMap((snapshot) =>
snapshotSlotMeta[snapshot.slot].checklist.map((item) => (
<span key={`${snapshot.slot}-${item}`}>
<strong>{snapshot.slot === '1015' ? '上午' : '下午'}</strong>
{item}
</span>
)),
)}
</div>
</section>
</section>
)
}
export function RiskCenterPage() {
const { data, isLoading, isError, refetch } = useDashboardOverview()
const [activeLevel, setActiveLevel] = useState<RiskLevel | 'all'>('all')
const state = <QueryState isLoading={isLoading} isError={isError || !data} refetch={() => void refetch()} title="风险中心" />
const filteredRisks = useMemo(() => {
if (!data) return []
if (activeLevel === 'all') return data.risks
return data.risks.filter((risk) => risk.level === activeLevel)
}, [activeLevel, data])
if (!data) return state
const dangerousFunds = data.fundPool.filter((metric) => metric.status === 'warning' || metric.status === 'danger')
return (
<section className="operations-page">
<OperationsHeader
kicker="Risk Center"
title="风险中心"
description="把资金、积分与数据一致性风险集中处理,优先看红色和黄色事项。"
extra={`${data.risks.length} 条待关注`}
/>
<section className="risk-summary-grid" aria-label="风险概览">
{(['red', 'yellow', 'gray'] as const).map((level) => {
const meta = riskLevelMeta[level]
const count = data.risks.filter((risk) => risk.level === level).length
return (
<button className={`risk-summary-card risk-summary-card--${level}`} key={level} type="button" onClick={() => setActiveLevel(level)}>
<Tag color={meta.color}>{meta.label}</Tag>
<strong>{count}</strong>
<span></span>
</button>
)
})}
</section>
<section className="section-block">
<CapsuleTabs activeKey={activeLevel} onChange={(key) => setActiveLevel(key as RiskLevel | 'all')}>
<CapsuleTabs.Tab title="全部" key="all" />
<CapsuleTabs.Tab title="红色" key="red" />
<CapsuleTabs.Tab title="黄色" key="yellow" />
<CapsuleTabs.Tab title="灰色" key="gray" />
</CapsuleTabs>
<div className="risk-list">
{filteredRisks.map((risk) => {
const meta = riskLevelMeta[risk.level]
return (
<button className="risk-item" key={risk.id} type="button">
<div className="risk-header">
<Tag color={meta.color}>{meta.label}</Tag>
<span>{risk.type}</span>
<time>{risk.discoveredAt}</time>
</div>
<strong>{risk.title}</strong>
<p>{risk.description}</p>
</button>
)
})}
</div>
</section>
<section className="section-block compact-section">
<div className="section-title-row">
<div>
<p className="section-kicker">Fund Watch</p>
<h2></h2>
</div>
</div>
<div className="kpi-grid kpi-grid--compact">
{dangerousFunds.map((metric) => (
<KpiCard key={metric.key} metric={metric} />
))}
</div>
</section>
</section>
)
}
export function ProfilePage() {
const { data, isLoading, isError, refetch } = useDashboardOverview()
const state = <QueryState isLoading={isLoading} isError={isError || !data} refetch={() => void refetch()} title="我的" />
if (!data) return state
return (
<section className="operations-page">
<OperationsHeader
kicker="Profile"
title="我的"
description="展示当前驾驶舱权限、数据环境与演示版本,方便联调时确认口径。"
extra="老板驾驶舱 H5"
/>
<section className="profile-card">
<div className="profile-avatar" aria-hidden="true">
</div>
<div>
<h2></h2>
<p></p>
</div>
</section>
<section className="section-block">
<div className="section-title-row">
<div>
<p className="section-kicker">Environment</p>
<h2></h2>
</div>
<Tag color="warning">Mock</Tag>
</div>
<div className="info-list">
<span>
<small></small>
<strong>{data.businessDate}</strong>
</span>
<span>
<small></small>
<strong>{data.generatedAt}</strong>
</span>
<span>
<small>API </small>
<strong>{import.meta.env.VITE_MOCK_ENABLED === 'false' ? '真实接口' : 'Mock 演示'}</strong>
</span>
</div>
</section>
<section className="section-block">
<div className="section-title-row">
<div>
<p className="section-kicker">Permissions</p>
<h2></h2>
</div>
</div>
<div className="check-list">
<span></span>
<span></span>
<span></span>
<span></span>
</div>
</section>
</section>
)
}

View File

@@ -0,0 +1,72 @@
export type MetricStatus = 'normal' | 'success' | 'warning' | 'danger'
export type SnapshotStatus = 'pending' | 'success' | 'failed' | 'temporary'
export type SnapshotSlot = '1015' | '1455'
export type KpiMetric = {
key: string
title: string
value: number | string | null
unit?: string
trendLabel?: string
trendValue?: number | string
status: MetricStatus
featured?: boolean
}
export type TodaySnapshot = {
slot: SnapshotSlot
title: string
status: SnapshotStatus
generatedAt?: string
message: string
purchaseUsers: number
orderCount: number
dealAmount: number | string
paidAmount: number | string
newMerchandiseCount: number
selfBonusChange: number | string
shareBonusChange: number | string
}
export type TrendPoint = {
date: string
amount: number | string
orders: number
newUsers: number
bonus: number | string
}
export type RankItem = {
id: string
name: string
value: number | string
description: string
badge?: string
}
export type RiskLevel = 'red' | 'yellow' | 'gray'
export type RiskAlert = {
id: string
level: RiskLevel
type: string
title: string
description: string
discoveredAt: string
}
export type DashboardOverview = {
businessDate: string
generatedAt: string
summary: string
kpis: KpiMetric[]
fundPool: KpiMetric[]
snapshots: TodaySnapshot[]
trends: TrendPoint[]
userRanks: RankItem[]
teamRanks: RankItem[]
productRanks: RankItem[]
risks: RiskAlert[]
}

View File

@@ -0,0 +1,18 @@
import { Empty } from 'antd-mobile'
type PlaceholderPageProps = {
title: string
description: string
}
export function PlaceholderPage({ title, description }: PlaceholderPageProps) {
return (
<section className="placeholder-page">
<div className="mobile-page-header">
<p className="eyebrow"></p>
<h1>{title}</h1>
</div>
<Empty description={description} />
</section>
)
}

View File

@@ -0,0 +1,589 @@
:root {
--bg: #fff6f1;
--surface: #ffffff;
--surface-soft: #f6f9fb;
--text: #132033;
--muted: #6b7a90;
--border: rgba(19, 32, 51, 0.08);
--primary: #ff5b36;
--primary-deep: #f04a2a;
--primary-soft: #fff0eb;
--success: #14a46c;
--warning: #ffb000;
--danger: #dc2626;
--shadow: 0 16px 40px rgba(255, 91, 54, 0.14);
--radius-xl: 28px;
--radius-lg: 20px;
--radius-md: 14px;
font-family:
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--adm-color-primary: var(--primary);
color: var(--text);
background: var(--bg);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
background:
radial-gradient(circle at top left, rgba(255, 91, 54, 0.2), transparent 28rem),
var(--bg);
}
button {
font: inherit;
}
button:focus-visible {
outline: 2px solid rgba(255, 91, 54, 0.72);
outline-offset: 2px;
}
#root {
width: min(100%, 430px);
min-height: 100svh;
margin: 0 auto;
background: var(--bg);
box-shadow: 0 0 0 1px rgba(19, 32, 51, 0.04);
}
.mobile-shell {
min-height: 100svh;
position: relative;
}
.mobile-main {
min-height: 100svh;
padding-bottom: calc(74px + env(safe-area-inset-bottom));
}
.bottom-nav {
position: fixed;
right: 0;
bottom: 0;
left: 0;
z-index: 20;
width: min(100%, 430px);
margin: 0 auto;
background: rgba(255, 255, 255, 0.94);
border-top: 1px solid var(--border);
backdrop-filter: blur(16px);
}
.dashboard-page {
padding: 14px 14px 24px;
}
.dashboard-hero {
position: relative;
overflow: hidden;
padding: 20px;
color: #fff;
background:
linear-gradient(145deg, rgba(255, 91, 54, 0.98), rgba(255, 139, 82, 0.92)),
radial-gradient(circle at 90% 10%, rgba(255, 176, 0, 0.42), transparent 18rem);
border-radius: 0 0 var(--radius-xl) var(--radius-xl);
box-shadow: var(--shadow);
}
.hero-topline,
.section-title-row,
.risk-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.hero-topline span,
.eyebrow,
.section-kicker {
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.hero-topline button {
min-height: 34px;
padding: 0 14px;
color: #fff;
background: rgba(255, 255, 255, 0.16);
border: 1px solid rgba(255, 255, 255, 0.24);
border-radius: 999px;
}
.dashboard-hero h1 {
margin: 18px 0 8px;
font-size: 30px;
line-height: 1.1;
}
.hero-summary {
margin: 0;
color: rgba(255, 255, 255, 0.78);
font-size: 14px;
line-height: 1.6;
}
.hero-metric {
margin-top: 20px;
padding: 16px;
background: rgba(255, 255, 255, 0.14);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: var(--radius-lg);
}
.hero-metric span,
.hero-metric small {
display: block;
color: rgba(255, 255, 255, 0.72);
font-size: 12px;
}
.hero-metric strong {
display: block;
margin: 6px 0;
font-size: 32px;
line-height: 1;
}
.kpi-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
margin-top: 12px;
}
.kpi-card {
min-height: 112px;
padding: 14px;
text-align: left;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: 0 10px 28px rgba(22, 47, 80, 0.08);
}
.kpi-card--featured {
grid-column: span 2;
}
.kpi-title,
.kpi-trend,
.section-kicker,
.snapshot-time,
.rank-content small,
.risk-item p {
margin: 0;
color: var(--muted);
}
.kpi-value {
display: block;
margin-top: 8px;
color: var(--text);
font-size: 22px;
line-height: 1.08;
word-break: break-all;
}
.kpi-trend {
margin-top: 8px;
font-size: 12px;
}
.kpi-trend span {
margin-left: 6px;
color: var(--primary);
font-weight: 700;
}
.kpi-card--success .kpi-trend span {
color: var(--success);
}
.kpi-card--warning .kpi-trend span {
color: var(--warning);
}
.kpi-card--danger .kpi-value,
.kpi-card--danger .kpi-trend span {
color: var(--danger);
}
.section-block {
margin-top: 14px;
padding: 16px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-xl);
box-shadow: 0 10px 28px rgba(22, 47, 80, 0.08);
}
.section-title-row h2 {
margin: 2px 0 0;
font-size: 18px;
}
.compact-section .kpi-grid {
margin-top: 12px;
}
.snapshot-section .adm-capsule-tabs {
margin: 14px 0;
}
.snapshot-card {
padding: 14px;
background: var(--surface-soft);
border-radius: var(--radius-lg);
}
.snapshot-message {
margin: 0 0 8px;
font-weight: 700;
line-height: 1.5;
}
.snapshot-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
margin-top: 14px;
}
.snapshot-grid span {
min-height: 64px;
padding: 10px;
color: var(--muted);
font-size: 12px;
background: #fff;
border-radius: var(--radius-md);
}
.snapshot-grid strong {
display: block;
margin-top: 5px;
color: var(--text);
font-size: 15px;
}
.mini-trend-chart {
width: 100%;
height: 240px;
}
.text-button,
.rank-item,
.risk-item {
appearance: none;
border: 0;
background: none;
}
.text-button {
display: inline-flex;
align-items: center;
min-height: 36px;
color: var(--primary);
font-size: 13px;
font-weight: 700;
}
.text-button:disabled {
color: var(--muted);
cursor: not-allowed;
}
.rank-list,
.risk-list {
display: grid;
gap: 10px;
margin-top: 14px;
}
.rank-item,
.risk-item {
width: 100%;
min-height: 58px;
padding: 12px;
text-align: left;
background: var(--surface-soft);
border-radius: var(--radius-md);
}
.rank-item {
display: grid;
grid-template-columns: 28px 1fr auto;
align-items: center;
gap: 10px;
}
.rank-index {
display: inline-grid;
width: 26px;
height: 26px;
place-items: center;
color: #fff;
font-weight: 800;
background: var(--text);
border-radius: 10px;
}
.rank-content strong,
.rank-content small {
display: block;
}
.rank-value {
color: var(--primary);
font-size: 13px;
font-weight: 800;
}
.risk-count {
color: var(--danger);
font-weight: 800;
}
.risk-header {
justify-content: flex-start;
color: var(--muted);
font-size: 12px;
}
.risk-header time {
margin-left: auto;
}
.risk-item strong {
display: block;
margin: 10px 0 4px;
font-size: 15px;
}
.risk-item p {
line-height: 1.5;
}
.placeholder-page,
.loading-page,
.error-page {
padding: 24px 16px;
}
.mobile-page-header h1 {
margin: 4px 0 24px;
}
.operations-page {
padding: 14px 14px 24px;
}
.operations-header {
position: relative;
overflow: hidden;
padding: 20px;
color: #fff;
background:
linear-gradient(145deg, rgba(255, 91, 54, 0.98), rgba(255, 139, 82, 0.92)),
radial-gradient(circle at 90% 10%, rgba(255, 176, 0, 0.42), transparent 18rem);
border-radius: 0 0 var(--radius-xl) var(--radius-xl);
box-shadow: var(--shadow);
}
.operations-header h1 {
margin: 12px 0 8px;
font-size: 28px;
line-height: 1.12;
}
.operations-header p {
margin: 0;
color: rgba(255, 255, 255, 0.76);
line-height: 1.6;
}
.operations-header .eyebrow {
color: rgba(255, 255, 255, 0.68);
}
.operations-header span {
display: inline-flex;
margin-top: 16px;
padding: 7px 12px;
color: rgba(255, 255, 255, 0.86);
font-size: 12px;
font-weight: 700;
background: rgba(255, 255, 255, 0.14);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 999px;
}
.report-list,
.check-list,
.info-list {
display: grid;
gap: 10px;
margin-top: 14px;
}
.report-item {
display: grid;
grid-template-columns: 82px 1fr;
gap: 12px;
width: 100%;
padding: 13px;
text-align: left;
background: var(--surface-soft);
border: 0;
border-radius: var(--radius-md);
}
.report-item span,
.info-list span {
display: grid;
gap: 4px;
}
.report-item small,
.info-list small,
.profile-card p {
color: var(--muted);
line-height: 1.45;
}
.snapshot-stack {
display: grid;
gap: 12px;
margin-top: 14px;
}
.snapshot-detail-card {
padding: 14px;
background: var(--surface-soft);
border-radius: var(--radius-lg);
}
.snapshot-detail-subtitle {
margin: 12px 0 0;
color: var(--muted);
font-size: 13px;
line-height: 1.55;
}
.snapshot-detail-message {
margin: 10px 0 8px;
font-weight: 700;
line-height: 1.55;
}
.snapshot-grid--wide {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.check-list span,
.info-list span {
padding: 12px;
background: var(--surface-soft);
border-radius: var(--radius-md);
}
.check-list span {
color: var(--text);
font-weight: 700;
}
.check-list strong {
margin-right: 8px;
color: var(--primary);
}
.risk-summary-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
margin-top: 12px;
}
.risk-summary-card {
display: grid;
min-height: 104px;
padding: 12px;
text-align: left;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: 0 10px 28px rgba(22, 47, 80, 0.08);
}
.risk-summary-card strong {
margin-top: 8px;
font-size: 28px;
line-height: 1;
}
.risk-summary-card span:last-child {
color: var(--muted);
font-size: 12px;
}
.risk-summary-card--red strong {
color: var(--danger);
}
.risk-summary-card--yellow strong {
color: var(--warning);
}
.risk-summary-card--gray strong {
color: var(--muted);
}
.profile-card {
display: grid;
grid-template-columns: 58px 1fr;
align-items: center;
gap: 14px;
margin-top: 14px;
padding: 16px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-xl);
box-shadow: 0 10px 28px rgba(22, 47, 80, 0.08);
}
.profile-card h2,
.profile-card p {
margin: 0;
}
.profile-avatar {
display: grid;
width: 58px;
height: 58px;
place-items: center;
color: #fff;
font-size: 22px;
font-weight: 800;
background: linear-gradient(145deg, var(--primary), var(--warning));
border-radius: 22px;
}
.loading-page,
.error-page {
display: grid;
min-height: 60svh;
place-content: center;
gap: 14px;
text-align: center;
}

View File

@@ -0,0 +1,20 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import 'antd-mobile/es/global'
import './index.css'
import App from './App.tsx'
const startApp = async () => {
if (import.meta.env.VITE_MOCK_ENABLED !== 'false') {
const { worker } = await import('./services/mock/browser')
await worker.start({ onUnhandledRequest: 'bypass' })
}
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
}
void startApp()

View File

@@ -0,0 +1,26 @@
import axios from 'axios'
export const httpClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL ?? '',
timeout: 8000,
})
export type ApiResponse<T> = {
code: number
message?: string
msg?: string
data: T
}
export async function getApiData<T>(url: string): Promise<T> {
const response = await httpClient.get<ApiResponse<T>>(url)
if (response.data.code !== 0 && response.data.code !== 200) {
throw new Error(response.data.msg ?? response.data.message ?? '接口请求失败')
}
return response.data.data
}
export async function getBlob(url: string): Promise<Blob> {
const response = await httpClient.get<Blob>(url, { responseType: 'blob' })
return response.data
}

View File

@@ -0,0 +1,4 @@
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)

View File

@@ -0,0 +1,58 @@
import { http, HttpResponse } from 'msw'
import { dashboardMock } from '../../features/boss-dashboard/mock'
function buildArchiveHtml() {
return `<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>经营日报归档 - ${dashboardMock.businessDate}</title>
<style>
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #132033; background: #fff6f1; }
main { max-width: 820px; margin: 0 auto; padding: 28px 18px 40px; }
header { color: #fff; padding: 26px; border-radius: 0 0 28px 28px; background: linear-gradient(145deg, #ff5b36, #ff8b52); }
section { margin-top: 16px; padding: 18px; background: #fff; border-radius: 24px; }
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
article { padding: 14px; border-radius: 18px; background: #f6f9fb; }
small { color: #6b7a90; }
</style>
</head>
<body>
<main>
<header>
<p>Daily Report Archive</p>
<h1>经营日报归档</h1>
<p>${dashboardMock.summary}</p>
<small>数据日期:${dashboardMock.businessDate} / 生成时间:${dashboardMock.generatedAt}</small>
</header>
<section>
<h2>核心经营指标</h2>
<div class="grid">
${dashboardMock.kpis
.map((metric) => `<article><small>${metric.title}</small><h3>${metric.value}${metric.unit ?? ''}</h3><small>${metric.trendLabel ?? ''}</small></article>`)
.join('')}
</div>
</section>
</main>
</body>
</html>`
}
export const handlers = [
http.get('/api/admin/dashboard/overview', () => {
return HttpResponse.json({
code: 0,
msg: 'success',
data: dashboardMock,
})
}),
http.get('/api/admin/dashboard/daily-report/archive', () => {
return new HttpResponse(buildArchiveHtml(), {
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Content-Disposition': `attachment; filename="dashboard-daily-report-${dashboardMock.businessDate}.html"`,
},
})
}),
]

View File

@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest'
import { formatMetricValue, formatMoney, formatNumber, formatTrend } from './format'
describe('format helpers', () => {
it('formats money with yuan symbol and two decimals', () => {
expect(formatMoney(1289360.4)).toBe('¥1,289,360.40')
})
it('formats metric values based on unit', () => {
expect(formatMetricValue(418471.07, '分')).toBe('418,471.070')
expect(formatMetricValue(936, '人')).toBe('936人')
})
it('uses placeholder for empty values', () => {
expect(formatNumber(null)).toBe('--')
})
it('adds plus sign for positive trend values', () => {
expect(formatTrend(8.6)).toBe('+8.6%')
expect(formatTrend(-3.2)).toBe('-3.2%')
})
})

View File

@@ -0,0 +1,33 @@
export function formatMoney(value: number | string | null | undefined): string {
if (value === null || value === undefined || value === '') return '--'
const numberValue = Number(value)
if (Number.isNaN(numberValue)) return String(value)
return `¥${numberValue.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`
}
export function formatNumber(value: number | string | null | undefined, digits = 0): string {
if (value === null || value === undefined || value === '') return '--'
const numberValue = Number(value)
if (Number.isNaN(numberValue)) return String(value)
return numberValue.toLocaleString('zh-CN', {
minimumFractionDigits: digits,
maximumFractionDigits: digits,
})
}
export function formatMetricValue(value: number | string | null, unit?: string): string {
if (unit === '元') return formatMoney(value)
if (unit === '分') return formatNumber(value, 3)
return `${formatNumber(value)}${unit ?? ''}`
}
export function formatTrend(value?: number | string): string {
if (value === undefined || value === '') return ''
const numberValue = Number(value)
if (Number.isNaN(numberValue)) return String(value)
const prefix = numberValue > 0 ? '+' : ''
return `${prefix}${numberValue.toFixed(1)}%`
}

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "esnext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"module": "esnext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,17 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',
port: 5174,
proxy: {
'/api': {
target: 'http://localhost:30032',
changeOrigin: true,
},
},
},
})

View File

@@ -0,0 +1,61 @@
# 公司名称:池州瑞棠商贸
## mysql数据库配置信息
host ip: 101.37.101.6
datasource:
name: yangtangyoupin
username: yangtangyoupin
password: 5Fn8eWrbYFtAhCZw
---
## 数据迁移任务
- **用户id数据范围**92827, 92738, 93140, 93150
- 提取wa_users表中id在用户id数据范围的记录
- 提取eb_user表中uid在用户id数据范围的记录
- wa_merchandise
提取“created_at >= 2026-04-16”并且user_id在用户id数据范围的状态为”未售“的寄售商品删除其余数据
(当前库表字段为 `user_id` 表示卖家,实现时按 `user_id` 与日期、状态`status`=1的条件过滤。
- wa_selfbonus_log
提取 `user_id` 在用户id数据范围内的记录
- wa_sharebonus_log
提取 `user_id` 在用户id数据范围内的记录
- wa_coupon_log
提取 `user_id` 在用户id数据范围内的记录
- eb_user_integral_record
提取用户在数据范围内的记录;表字段为 `uid`(与 `wa_users.id` / `eb_user.uid` 对应),实现按 `uid` 过滤。
---
## 批量 INSERT SQL从 dump 生成)
- 生成文件:`docs/sql/com-czrt6-data-imgration-2_inserts.sql`(含 `SET NAMES utf8mb4;` 及各表 `INSERT`,按依赖顺序:`wa_users``eb_user` → …)。
- 重新生成:`python3 docs/sql/generate_com_czrt6_data_imgration_2_inserts.py [可选: dump.sql路径]`(默认使用下文 dump 路径)。
- **主键/外键**:导入前请备份;若目标库已有相同主键需先处理或改用 `REPLACE` / 调整自增。
- **wa_merchandise**:当前 dump 中满足日期与 `user_id` 的记录均为 **已售status=0**,按文档「未售 status=1」筛选为 **0 行**SQL 中该段仅有说明注释。
## 在目标库执行迁移
```bash
export CZRT6_DB_PASSWORD='(见上文 datasource.password'
python3 docs/sql/run_com_czrt6_data_imgration_2.py
```
- 默认连接:`101.37.101.6` / `yangtangyoupin`(可用 `CZRT6_DB_HOST` 等覆盖)。
- 脚本会 `SET FOREIGN_KEY_CHECKS=0`,按 INSERT 中出现的 **主键** 先删后插(避免他人占用同 `id` 导致冲突),再执行 `com-czrt6-data-imgration-2_inserts.sql` 中全部 `INSERT`
- **已于 2026-04-26 执行成功**`wa_users`/`eb_user` 各 4 行;`wa_selfbonus_log` 142`wa_sharebonus_log` 343`wa_coupon_log` 40`eb_user_integral_record` 174`wa_merchandise` 无 INSERT。
## 相关文件
- 提取数据源 dump`docs/czcf82-yangtangyoupin_2026-04-26_10-25-02_mysql_data.sql`
- 生成 INSERT`docs/sql/generate_com_czrt6_data_imgration_2_inserts.py``docs/sql/com-czrt6-data-imgration-2_inserts.sql`
- 在目标库执行:`docs/sql/run_com_czrt6_data_imgration_2.py`

View File

@@ -0,0 +1,66 @@
# 公司名称:太原树英商贸
## mysql数据库配置信息
host ip: 106.14.132.80
datasource:
name: yangtangyoupin
username: yangtangyoupin
password: 5Fn8eWrbYFtAhCZw
## 数据清理任务
- **数据范围**:用户 id 集(`wa_users`
`92566,92801,92839,93004,92637,92965,93093,93096,93116,92787,93121,93129,92884,93007,93020,93094,93099,93110,92638`
- 保留wa_users表中id在用户id数据范围的 ,删除其余用户数据
- 保留eb_user表中uid在用户id数据范围的 ,删除其余用户数据
- wa_order
清空wa_order表中数据
- wa_merchandise
从源数据dump文件中提取“created_at >= 2026-04-22”并且seller_id或buyer_id在用户id数据范围的寄售商品删除其余数据
(当前库表字段为 `user_id` 表示卖家,实现时按 `user_id` 与日期条件过滤。)
- wa_selfbonus_log
只保留 `user_id` 在用户id数据范围内的记录删除其余数据
- wa_sharebonus_log
只保留 `user_id` 在用户id数据范围内的记录删除其余数据
- wa_coupon_log
只保留 `user_id` 在用户id数据范围内的记录删除其余数据
- wa_withdraw
清空wa_withdraw表中数据
- eb_store_order
清空eb_store_order表中数据
- eb_user_integral_record
只保留用户在名单内的记录;表字段为 `uid`(与 `wa_users.id` / `eb_user.uid` 对应),实现按 `uid` 过滤。
## 执行脚本
- **`wa_merchandise` 批量 INSERT从 dump 筛条件后生成)**
- 生成结果:`docs/sql/wa_merchandise_insert_from_dump_sxsy80.sql`(当前 **18** 行,与 dump 中 `INSERT INTO wa_merchandise` 一致)。
- 重新生成:`python3 docs/sql/generate_wa_merchandise_insert_from_dump.py /path/to/ccd-yangtangyoupin_*.sql`(不传参时默认使用仓库上级 `integral-shop/db/ccd-yangtangyoupin_2026-04-26_10-25-01_mysql_data.sql`)。
- 导入前注意主键冲突:若目标库已有相同 `id`,需先删改或改用 `REPLACE INTO` / 调整自增策略。
- SQL`docs/sql/com-sxsy80-data-cleanup.sql`
- 本机 Homebrew `mysql` 9 客户端不支持 `mysql_native_password`,可用 `pip install pymysql` 后执行:
```bash
export YTYP_DB_PASSWORD='(见上文 datasource.password'
python3 docs/sql/run_com_sxsy80_cleanup.py
```
- 已于 **2026-04-26** 对远程库执行并成功 `COMMIT`(首轮:`wa_merchandise` 按 2026-04-24 条件删除 2114 行;`wa_selfbonus_log` 1592`wa_sharebonus_log` 1399`wa_coupon_log` 171`eb_user_integral_record` 1613`eb_user` 80`wa_users` 80`wa_order` / `wa_withdraw` / `eb_store_order``TRUNCATE`)。
- **2026-04-26 二次**:按文档将 `wa_merchandise` 日期阈值改为 **2026-04-22** 重新执行(见 `docs/sql/com-sxsy80-wa_merchandise-only.sql`),仅影响该表;**删除 0 行**(上轮已按 04-24 清理,现存行均满足「>= 04-22 且卖家在名单」;若 04-2204-23 且卖家在名单的数据曾被误删,需从备份恢复库后再用 04-22 规则全量重跑)。
- **2026-04-26 三次**:按「从源 dump 提取」规则:从 `ccd-yangtangyoupin_2026-04-26_10-25-01_mysql_data.sql` 解析 `created_at >= 2026-04-22``user_id` 在名单内的 **18**`id`,执行 `DELETE ... WHERE id NOT IN (...)`;保留 id 列表见 `docs/sql/wa_merchandise_keep_ids_from_dump_sxsy80.txt`。**删除 0 行**(当前库中 `wa_merchandise` 已仅含上述 id 子集,与 dump 规则一致)。
## 相关文件
- 源数据dump文件 'integral-shop/db/ccd-yangtangyoupin_2026-04-26_10-25-01_mysql_data.sql'

50
docs/com-sxsy80.md Normal file
View File

@@ -0,0 +1,50 @@
## 公司名称: 太原树英商贸, host ip: 106.14.132.80
---
### backend/crmeb-front模块变更
- 1. profile: sxsy80
- 2. profile file: application-sxsy80.yml, mysql和redis主机ip修改
- 3. **PDF合同模板文件路径**pdf/sign_contract_sxsy80.pdf
- 4. 用户PDF合同url地址前缀/落库域名https://sxsy.cichude.com/
- 5. imagePath: /www/wwwroot/sxsy.cichude.com/
### uniapp前端配置变更
- 1. 积分商城domainhttps://sxsy-jf.cichude.com
- 2. 抢购页面跳转地址https://sxsy.cichude.com
- 3. **PDF合同预览文件路径** /static/sign_contract_sxsy80.pdf
---
### backend/crmeb-admin模块变更
- 1. profile: sxsy80
- 2. profile file: application-sxsy80.yml, mysql和redis主机ip修改sync: source-id: shop_14 target-mer-id: 14
### 积分商城后台backend-adminend配置变更
- 1. backend-adminend/.env.development文件中VUE_APP_BASE_API改为https://sxsy-jf.cichude.com
- 2. backend-adminend/.env.production文件中VUE_APP_BASE_API改为https://sxsy-jf.cichude.com
---
### **修改任务**
- 新建分支sxsy80合并czcf82分支的最新代码到该分支并根据上述信息修改相关需要变更项使符合该新公司项目环境
## 相关文件
、、、启动积分商城api服务
cd /www/wwwroot/javaapi
nohup java -Xms128m -Xmx256m -jar miao-front-2.2.jar > front.log & tail -f front.log
、、、
、、、启动积分商城后台api服务
cd /www/wwwroot/javaapi
nohup java -Xms128m -Xmx256m -jar miao-admin-2.2.jar > admin.log & tail -f admin.log
、、、

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,737 @@
-- 池州瑞棠商贸 / com-czrt6-data-imgration-2.md
-- 源 dump: czcf82-yangtangyoupin_2026-04-26_10-25-02_mysql_data.sql
-- 用户范围: [92738, 92827, 93140, 93150]
-- wa_merchandise: created_at >= 2026-04-16 且 user_id 在范围且 status=1未售
-- 执行前请备份;注意外键与主键冲突,按需调整顺序或使用 SET FOREIGN_KEY_CHECKS=0
SET NAMES utf8mb4;
-- ---------- `wa_users` (4 rows) ----------
INSERT INTO `wa_users` VALUES
(92738,93104,'17756627812','胡晓彩','17756627812','9e13831c1275043d00fb5dde39f28d3b','LJvXO2','1','/app/admin/avatar.png','bdu4ra',1,NULL,0.000,1544.000,17113.588,4792.613,0,'2026-04-24 09:23:27','114.103.38.148','2025-11-13 14:41:43','36.61.252.77',NULL,'2025-11-13 14:41:43','2026-04-24 09:23:27',1,NULL,0,'https://czcf.uj345.com/crmebimage/public/user/2026/04/18/e3c0ff944f944a9cbe580fd88fdd1d7be1itbd9eh5.pdf',0,1,NULL,NULL),
(92827,92738,'18956691177','王兵启','18956691177','c742a1d8995ebadbef7016b6f83ffbfa','IuqYhH','1','/app/admin/avatar.png','yijeac',1,NULL,0.000,2376.000,13563.808,6626.224,0,'2026-04-23 12:06:03','112.122.255.122','2025-12-04 19:13:00','220.205.249.43',NULL,'2025-12-04 19:13:00','2026-04-23 12:06:03',1,NULL,0,'https://czcf.uj345.com/crmebimage/public/user/2026/04/18/a0b565773c2d45d39256e1e8c2992450j4w7frgpns.pdf',0,1,NULL,NULL),
(93140,92827,'13635661721','柯美燕','13635661721','ac3ed6421b3b7ca72c57a8c2b6ebebb1','IG9j6q','1','/app/admin/avatar.png','t1tdj8',1,NULL,0.000,0.000,4787.975,0.000,0,'2026-04-23 12:07:23','112.122.255.122','2026-03-23 14:10:31','36.161.161.145',NULL,'2026-03-23 14:10:31','2026-04-23 12:07:23',1,NULL,0,'https://czcf.uj345.com/crmebimage/public/user/2026/04/18/963f7f7b6a294351a20244a706f7b4eb4gorcbhsg3.pdf',0,1,NULL,NULL),
(93150,92738,'13856372733','王珍华','13856372733','be2c21121092d4175b227e1403719b7a','Jr0FNi','1','/app/admin/avatar.png','knadzl',1,NULL,0.000,0.000,4168.197,0.000,0,'2026-04-24 09:38:53','36.161.140.102','2026-03-26 09:13:07','142.171.243.42',NULL,'2026-03-26 09:13:07','2026-04-24 09:38:53',1,NULL,0,'https://czcf.uj345.com/crmebimage/public/user/2026/04/18/748a78ed247b42519b249344c0d3ce4bef7cxiyacx.pdf',0,1,NULL,NULL);
-- ---------- `eb_user` (4 rows) ----------
INSERT INTO `eb_user` VALUES
(92738,'17756627812','5c4ef5c98420f21c3c6ddf62c8d9e0a9','','','','',NULL,'','','胡晓彩','/app/admin/avatar.png','17756627812','36.61.252.77','114.103.36.38',0.00,0.00,927.778,0,0,1,1,92544,'2026-02-16 01:30:28','H5',0,15,0,'',0,'h5','2025-11-13 06:41:43','2026-04-22 02:02:01','2026-04-25 08:08:06',NULL,'/0/',0,NULL,1,'CN',NULL),
(92827,'18956691177','c742a1d8995ebadbef7016b6f83ffbfa','','','','',NULL,'','','王兵启','/app/admin/avatar.png','18956691177','220.205.249.43','220.205.248.11',0.00,0.00,572.957,0,0,1,1,92738,'2026-02-16 01:30:28','H5',0,12,0,'',0,'h5','2025-12-04 11:13:00','2026-04-21 02:11:01','2026-04-23 04:06:51',NULL,'/0/',0,NULL,1,'CN',NULL),
(93140,'13635661721','ac3ed6421b3b7ca72c57a8c2b6ebebb1','','','','',NULL,'','','柯美燕','/app/admin/avatar.png','13635661721','36.161.161.145','36.161.161.145',0.00,0.00,13.982,0,0,1,1,92827,'2026-03-23 06:20:01','H5',0,3,0,'',0,'h5','2026-03-23 06:10:31','2026-04-21 11:06:17','2026-04-23 04:07:51',NULL,'/0/',0,NULL,1,'CN',NULL),
(93150,'13856372733','be2c21121092d4175b227e1403719b7a','','','','',NULL,'','','138****2733','/app/admin/avatar.png','13856372733','142.171.243.42','142.171.243.42',0.00,0.00,2084.094,0,0,1,1,92738,'2026-03-26 01:20:01','H5',0,0,0,'',0,'h5','2026-03-26 01:13:07','2026-04-22 02:58:01','2026-04-24 03:39:27',NULL,'/0/',0,NULL,1,'CN',NULL);
-- ---------- `wa_merchandise` (0 rows) ----------
-- 无匹配行dump 中满足「日期 + user_id 在范围」的寄售商品均为 status=0已售
-- 若业务上「未售」应对应其它字段,请改脚本 filter 后重新 python3 本脚本。
-- ---------- `wa_selfbonus_log` (142 rows) ----------
INSERT INTO `wa_selfbonus_log` VALUES
(33,92738,1,276.300,0.000,276.300,'今日收益','2026-02-19 10:22:17','2026-02-19 10:22:17'),
(36,92827,1,320.877,0.000,320.877,'今日收益','2026-02-19 10:27:47','2026-02-19 10:27:47'),
(88,92738,1,237.557,276.300,513.857,'今日收益','2026-02-20 10:21:47','2026-02-20 10:21:47'),
(97,92827,1,301.248,320.877,622.125,'今日收益','2026-02-20 11:06:44','2026-02-20 11:06:44'),
(133,92738,1,244.684,513.857,758.541,'今日收益','2026-02-23 10:00:26','2026-02-23 10:00:26'),
(142,92827,1,340.418,622.125,962.543,'今日收益','2026-02-23 10:01:35','2026-02-23 10:01:35'),
(207,92738,1,251.543,758.541,1010.084,'今日收益','2026-02-24 10:04:13','2026-02-24 10:04:13'),
(246,92827,1,316.764,962.543,1279.307,'今日收益','2026-02-24 14:59:48','2026-02-24 14:59:48'),
(283,92738,1,253.466,1010.084,1263.550,'今日收益','2026-02-25 10:14:34','2026-02-25 10:14:34'),
(291,92827,1,283.955,1279.307,1563.262,'今日收益','2026-02-25 11:20:10','2026-02-25 11:20:10'),
(316,92738,1,354.843,1263.550,1618.393,'今日收益','2026-02-26 10:01:21','2026-02-26 10:01:21'),
(361,92827,1,267.899,1563.262,1831.161,'今日收益','2026-02-26 12:51:45','2026-02-26 12:51:45'),
(399,92738,1,238.889,1618.393,1857.282,'今日收益','2026-02-27 10:04:50','2026-02-27 10:04:50'),
(403,92827,1,296.815,1831.161,2127.976,'今日收益','2026-02-27 10:05:33','2026-02-27 10:05:33'),
(446,92827,1,287.673,2127.976,2415.649,'今日收益','2026-03-02 10:01:29','2026-03-02 10:01:29'),
(493,92738,1,253.048,1857.282,2110.330,'今日收益','2026-03-02 14:57:17','2026-03-02 14:57:17'),
(525,92827,1,294.994,2415.649,2710.643,'今日收益','2026-03-03 10:10:35','2026-03-03 10:10:35'),
(554,92738,1,261.969,2110.330,2372.299,'今日收益','2026-03-03 13:53:59','2026-03-03 13:53:59'),
(602,92738,1,300.355,2372.299,2672.654,'今日收益','2026-03-04 10:05:17','2026-03-04 10:05:17'),
(611,92827,1,323.003,2710.643,3033.646,'今日收益','2026-03-04 12:45:31','2026-03-04 12:45:31'),
(647,92827,1,313.586,3033.646,3347.232,'今日收益','2026-03-05 10:02:50','2026-03-05 10:02:50'),
(648,92738,1,312.959,2672.654,2985.613,'今日收益','2026-03-05 10:02:58','2026-03-05 10:02:58'),
(701,92738,1,288.050,2985.613,3273.663,'今日收益','2026-03-06 10:01:16','2026-03-06 10:01:16'),
(703,92827,1,285.559,3347.232,3632.791,'今日收益','2026-03-06 10:01:42','2026-03-06 10:01:42'),
(765,92827,1,300.552,3632.791,3933.343,'今日收益','2026-03-09 10:01:43','2026-03-09 10:01:43'),
(767,92738,1,319.257,3273.663,3592.920,'今日收益','2026-03-09 10:02:06','2026-03-09 10:02:06'),
(854,92738,1,316.367,3592.920,3909.287,'今日收益','2026-03-10 10:18:58','2026-03-10 10:18:58'),
(891,92827,1,308.000,3933.343,4241.343,'今日收益','2026-03-10 15:41:35','2026-03-10 15:41:35'),
(914,92738,1,353.801,3909.287,4263.088,'今日收益','2026-03-11 10:03:58','2026-03-11 10:03:58'),
(927,92827,1,312.289,4241.343,4553.632,'今日收益','2026-03-11 10:07:28','2026-03-11 10:07:28'),
(1005,92738,1,359.326,4263.088,4622.414,'今日收益','2026-03-12 12:15:59','2026-03-12 12:15:59'),
(1034,92827,1,370.834,4553.632,4924.466,'今日收益','2026-03-13 10:01:39','2026-03-13 10:01:39'),
(1060,92738,1,257.456,4622.414,4879.870,'今日收益','2026-03-13 10:06:32','2026-03-13 10:06:32'),
(1100,92738,1,369.122,4879.870,5248.992,'今日收益','2026-03-16 10:04:15','2026-03-16 10:04:15'),
(1117,92827,1,378.232,4924.466,5302.698,'今日收益','2026-03-16 10:26:44','2026-03-16 10:26:44'),
(1145,92738,1,333.858,5248.992,5582.850,'今日收益','2026-03-17 10:01:26','2026-03-17 10:01:26'),
(1146,92738,1,337.137,5582.850,5919.987,'今日收益','2026-03-17 10:02:14','2026-03-17 10:02:14'),
(1177,92827,1,384.261,5302.698,5686.959,'今日收益','2026-03-17 14:54:26','2026-03-17 14:54:26'),
(1204,92827,1,401.267,5686.959,6088.226,'今日收益','2026-03-18 10:05:05','2026-03-18 10:05:05'),
(1216,92738,1,367.767,5919.987,6287.754,'今日收益','2026-03-18 10:13:52','2026-03-18 10:13:52'),
(1217,92738,1,362.028,6287.754,6649.782,'今日收益','2026-03-18 10:15:17','2026-03-18 10:15:17'),
(1257,92738,1,300.354,6649.782,6950.136,'今日收益','2026-03-19 10:03:28','2026-03-19 10:03:28'),
(1261,92827,1,372.889,6088.226,6461.115,'今日收益','2026-03-19 10:04:21','2026-03-19 10:04:21'),
(1274,92738,1,242.103,6950.136,7192.239,'今日收益','2026-03-19 10:07:25','2026-03-19 10:07:25'),
(1282,92738,1,256.426,7192.239,7448.665,'今日收益','2026-03-19 10:42:03','2026-03-19 10:42:03'),
(1284,92738,1,206.652,7448.665,7655.317,'今日收益','2026-03-19 10:44:39','2026-03-19 10:44:39'),
(1308,92738,1,305.916,7655.317,7961.233,'今日收益','2026-03-20 10:01:20','2026-03-20 10:01:20'),
(1317,92738,1,216.604,7961.233,8177.837,'今日收益','2026-03-20 10:03:06','2026-03-20 10:03:06'),
(1326,92738,1,284.709,8177.837,8462.546,'今日收益','2026-03-20 10:04:49','2026-03-20 10:04:49'),
(1349,92827,1,310.284,6461.115,6771.399,'今日收益','2026-03-20 10:54:18','2026-03-20 10:54:18'),
(1364,92827,1,297.887,6771.399,7069.286,'今日收益','2026-03-23 10:02:02','2026-03-23 10:02:02'),
(1370,92738,1,204.579,8462.546,8667.125,'今日收益','2026-03-23 10:02:44','2026-03-23 10:02:44'),
(1392,92738,1,228.340,8667.125,8895.465,'今日收益','2026-03-23 10:05:46','2026-03-23 10:05:46'),
(1396,92738,1,375.760,8895.465,9271.225,'今日收益','2026-03-23 10:07:30','2026-03-23 10:07:30'),
(1436,92827,1,289.210,7069.286,7358.496,'今日收益','2026-03-24 10:02:50','2026-03-24 10:02:50'),
(1437,92738,1,243.563,9271.225,9514.788,'今日收益','2026-03-24 10:02:59','2026-03-24 10:02:59'),
(1441,92738,1,264.552,9514.788,9779.340,'今日收益','2026-03-24 10:03:42','2026-03-24 10:03:42'),
(1446,93140,1,203.733,0.000,203.733,'今日收益','2026-03-24 10:04:48','2026-03-24 10:04:48'),
(1506,92738,1,242.677,9779.340,10022.017,'今日收益','2026-03-25 10:04:47','2026-03-25 10:04:47'),
(1514,93140,1,214.257,203.733,417.990,'今日收益','2026-03-25 10:05:50','2026-03-25 10:05:50'),
(1525,92827,1,254.832,7358.496,7613.328,'今日收益','2026-03-25 10:22:51','2026-03-25 10:22:51'),
(1536,92738,1,371.323,10022.017,10393.340,'今日收益','2026-03-25 11:33:09','2026-03-25 11:33:09'),
(1581,93150,1,207.318,0.000,207.318,'今日收益','2026-03-26 10:04:44','2026-03-26 10:04:44'),
(1584,92738,1,285.201,10393.340,10678.541,'今日收益','2026-03-26 10:05:39','2026-03-26 10:05:39'),
(1586,92827,1,237.163,7613.328,7850.491,'今日收益','2026-03-26 10:06:45','2026-03-26 10:06:45'),
(1595,93140,1,227.758,417.990,645.748,'今日收益','2026-03-26 10:08:44','2026-03-26 10:08:44'),
(1617,92738,1,293.757,10678.541,10972.298,'今日收益','2026-03-27 09:59:16','2026-03-27 09:59:16'),
(1631,93150,1,211.460,207.318,418.778,'今日收益','2026-03-27 10:01:43','2026-03-27 10:01:43'),
(1634,92827,1,256.999,7850.491,8107.490,'今日收益','2026-03-27 10:02:06','2026-03-27 10:02:06'),
(1673,93140,1,230.255,645.748,876.003,'今日收益','2026-03-27 13:28:42','2026-03-27 13:28:42'),
(1736,92738,1,278.462,10972.298,11250.760,'今日收益','2026-03-30 12:08:02','2026-03-30 12:08:02'),
(1742,93150,1,204.579,418.778,623.357,'今日收益','2026-03-30 14:15:45','2026-03-30 14:15:45'),
(1750,92827,1,344.311,8107.490,8451.801,'今日收益','2026-03-30 15:12:11','2026-03-30 15:12:11'),
(1752,93140,1,237.163,876.003,1113.166,'今日收益','2026-03-30 15:43:43','2026-03-30 15:43:43'),
(1776,93140,1,248.878,1113.166,1362.044,'今日收益','2026-03-31 10:05:47','2026-03-31 10:05:47'),
(1780,93150,1,226.542,623.357,849.899,'今日收益','2026-03-31 10:06:28','2026-03-31 10:06:28'),
(1783,92738,1,229.019,11250.760,11479.779,'今日收益','2026-03-31 10:07:59','2026-03-31 10:07:59'),
(1813,92827,1,382.715,8451.801,8834.516,'今日收益','2026-03-31 15:36:15','2026-03-31 15:36:15'),
(1829,93150,1,235.733,849.899,1085.632,'今日收益','2026-04-01 10:01:22','2026-04-01 10:01:22'),
(1840,92827,1,376.894,8834.516,9211.410,'今日收益','2026-04-01 10:03:07','2026-04-01 10:03:07'),
(1856,92738,1,283.193,11479.779,11762.972,'今日收益','2026-04-01 10:06:09','2026-04-01 10:06:09'),
(1886,93140,1,233.338,1362.044,1595.382,'今日收益','2026-04-01 19:36:07','2026-04-01 19:36:07'),
(1912,93150,1,206.091,1085.632,1291.723,'今日收益','2026-04-02 10:03:50','2026-04-02 10:03:50'),
(1927,93140,1,259.154,1595.382,1854.536,'今日收益','2026-04-02 10:06:26','2026-04-02 10:06:26'),
(1946,92827,1,377.353,9211.410,9588.763,'今日收益','2026-04-02 15:49:54','2026-04-02 15:49:54'),
(1947,92738,1,299.551,11762.972,12062.523,'今日收益','2026-04-02 15:54:57','2026-04-02 15:54:57'),
(1960,93150,1,210.445,1291.723,1502.168,'今日收益','2026-04-03 10:00:25','2026-04-03 10:00:25'),
(1968,92738,1,300.440,12062.523,12362.963,'今日收益','2026-04-03 10:01:30','2026-04-03 10:01:30'),
(1988,93140,1,250.255,1854.536,2104.791,'今日收益','2026-04-03 10:04:29','2026-04-03 10:04:29'),
(2019,92827,1,335.127,9588.763,9923.890,'今日收益','2026-04-03 15:02:24','2026-04-03 15:02:24'),
(2049,93140,1,215.374,2104.791,2320.165,'今日收益','2026-04-06 10:04:10','2026-04-06 10:04:10'),
(2051,92827,1,322.813,9923.890,10246.703,'今日收益','2026-04-06 10:04:24','2026-04-06 10:04:24'),
(2077,92738,1,317.794,12362.963,12680.757,'今日收益','2026-04-06 10:49:38','2026-04-06 10:49:38'),
(2089,93150,1,209.676,1502.168,1711.844,'今日收益','2026-04-06 11:49:33','2026-04-06 11:49:33'),
(2135,92738,1,221.195,12680.757,12901.952,'今日收益','2026-04-07 10:03:17','2026-04-07 10:03:17'),
(2140,92738,1,278.140,12901.952,13180.092,'今日收益','2026-04-07 10:07:04','2026-04-07 10:07:04'),
(2172,92827,1,310.762,10246.703,10557.465,'今日收益','2026-04-07 10:21:46','2026-04-07 10:21:46'),
(2185,93140,1,211.024,2320.165,2531.189,'今日收益','2026-04-07 14:02:40','2026-04-07 14:02:40'),
(2195,93150,1,205.562,1711.844,1917.406,'今日收益','2026-04-07 14:14:36','2026-04-07 14:14:36'),
(2243,93140,1,328.298,2531.189,2859.487,'今日收益','2026-04-08 10:11:11','2026-04-08 10:11:11'),
(2246,92827,1,229.959,10557.465,10787.424,'今日收益','2026-04-08 10:15:58','2026-04-08 10:15:58'),
(2262,92738,1,310.931,13180.092,13491.023,'今日收益','2026-04-08 11:54:15','2026-04-08 11:54:15'),
(2268,92738,1,225.814,13491.023,13716.837,'今日收益','2026-04-08 13:26:46','2026-04-08 13:26:46'),
(2285,93150,1,212.357,1917.406,2129.763,'今日收益','2026-04-08 15:19:27','2026-04-08 15:19:27'),
(2314,93150,1,206.172,2129.763,2335.935,'今日收益','2026-04-09 10:04:21','2026-04-09 10:04:21'),
(2318,92827,1,335.324,10787.424,11122.748,'今日收益','2026-04-09 10:05:33','2026-04-09 10:05:33'),
(2353,92738,1,230.255,13716.837,13947.092,'今日收益','2026-04-09 10:51:18','2026-04-09 10:51:18'),
(2358,92738,1,278.617,13947.092,14225.709,'今日收益','2026-04-09 13:02:54','2026-04-09 13:02:54'),
(2368,93140,1,235.345,2859.487,3094.832,'今日收益','2026-04-09 14:54:20','2026-04-09 14:54:20'),
(2389,93150,1,224.623,2335.935,2560.558,'今日收益','2026-04-10 10:02:34','2026-04-10 10:02:34'),
(2426,93140,1,246.083,3094.832,3340.915,'今日收益','2026-04-10 11:10:32','2026-04-10 11:10:32'),
(2427,92827,1,339.578,11122.748,11462.326,'今日收益','2026-04-10 11:11:59','2026-04-10 11:11:59'),
(2432,92738,1,229.771,14225.709,14455.480,'今日收益','2026-04-10 12:35:21','2026-04-10 12:35:21'),
(2435,92738,1,286.976,14455.480,14742.456,'今日收益','2026-04-10 13:29:42','2026-04-10 13:29:42'),
(2466,93150,1,237.510,2560.558,2798.068,'今日收益','2026-04-13 10:01:15','2026-04-13 10:01:15'),
(2470,93140,1,250.117,3340.915,3591.032,'今日收益','2026-04-13 10:01:37','2026-04-13 10:01:37'),
(2488,92827,1,360.222,11462.326,11822.548,'今日收益','2026-04-13 10:05:28','2026-04-13 10:05:28'),
(2499,92738,1,272.650,14742.456,15015.106,'今日收益','2026-04-13 10:12:09','2026-04-13 10:12:09'),
(2521,92738,1,244.278,15015.106,15259.384,'今日收益','2026-04-13 16:38:03','2026-04-13 16:38:03'),
(2528,93150,1,239.010,2798.068,3037.078,'今日收益','2026-04-14 09:59:25','2026-04-14 09:59:25'),
(2554,93140,1,244.278,3591.032,3835.310,'今日收益','2026-04-14 10:04:11','2026-04-14 10:04:11'),
(2572,92827,1,366.418,11822.548,12188.966,'今日收益','2026-04-14 11:31:02','2026-04-14 11:31:02'),
(2587,92738,1,251.103,15259.384,15510.487,'今日收益','2026-04-14 15:14:04','2026-04-14 15:14:04'),
(2589,92738,1,250.364,15510.487,15760.851,'今日收益','2026-04-14 15:29:02','2026-04-14 15:29:02'),
(2612,92738,1,261.781,15760.851,16022.632,'今日收益','2026-04-15 10:01:28','2026-04-15 10:01:28'),
(2622,93150,1,209.413,3037.078,3246.491,'今日收益','2026-04-15 10:01:59','2026-04-15 10:01:59'),
(2630,92827,1,357.418,12188.966,12546.384,'今日收益','2026-04-15 10:02:36','2026-04-15 10:02:36'),
(2646,93140,1,266.585,3835.310,4101.895,'今日收益','2026-04-15 10:05:15','2026-04-15 10:05:15'),
(2687,93140,1,253.565,4101.895,4355.460,'今日收益','2026-04-16 10:00:08','2026-04-16 10:00:08'),
(2695,92738,1,278.349,16022.632,16300.981,'今日收益','2026-04-16 10:01:07','2026-04-16 10:01:07'),
(2707,92827,1,342.077,12546.384,12888.461,'今日收益','2026-04-16 10:02:45','2026-04-16 10:02:45'),
(2750,93150,1,212.264,3246.491,3458.755,'今日收益','2026-04-16 14:27:29','2026-04-16 14:27:29'),
(2770,92738,1,261.172,16300.981,16562.153,'今日收益','2026-04-20 10:00:45','2026-04-20 10:00:45'),
(2776,93140,1,207.324,4355.460,4562.784,'今日收益','2026-04-20 10:03:10','2026-04-20 10:03:10'),
(2793,93150,1,280.203,3458.755,3738.958,'今日收益','2026-04-20 14:09:16','2026-04-20 14:09:16'),
(2794,92827,1,332.683,12888.461,13221.144,'今日收益','2026-04-20 14:46:53','2026-04-20 14:46:53'),
(2824,92738,1,274.358,16562.153,16836.511,'今日收益','2026-04-21 10:02:07','2026-04-21 10:02:07'),
(2829,93140,1,225.191,4562.784,4787.975,'今日收益','2026-04-21 10:03:34','2026-04-21 10:03:34'),
(2834,92827,1,342.664,13221.144,13563.808,'今日收益','2026-04-21 10:10:39','2026-04-21 10:10:39'),
(2859,93150,1,215.062,3738.958,3954.020,'今日收益','2026-04-21 14:14:48','2026-04-21 14:14:48'),
(2881,92738,1,277.077,16836.511,17113.588,'今日收益','2026-04-22 10:01:47','2026-04-22 10:01:47'),
(2898,93150,1,214.177,3954.020,4168.197,'今日收益','2026-04-22 10:57:10','2026-04-22 10:57:10');
-- ---------- `wa_sharebonus_log` (343 rows) ----------
INSERT INTO `wa_sharebonus_log` VALUES
(310275,92738,1,81.811,0.000,81.811,'今日收益','2025-12-01 10:03:27','2025-12-01 10:03:27'),
(310381,92738,1,82.516,81.811,164.327,'今日收益','2025-12-02 10:04:00','2025-12-02 10:04:00'),
(310697,92738,1,98.437,164.327,262.764,'今日收益','2025-12-04 10:08:12','2025-12-04 10:08:12'),
(311673,92738,1,95.570,262.764,358.334,'今日收益','2025-12-15 10:02:05','2025-12-15 10:02:05'),
(311812,92738,1,87.198,358.334,445.532,'今日收益','2025-12-15 15:09:36','2025-12-15 15:09:36'),
(311829,92738,1,87.357,445.532,532.889,'今日收益','2025-12-16 10:00:50','2025-12-16 10:00:50'),
(312051,92738,1,85.369,532.889,618.258,'今日收益','2025-12-17 10:06:36','2025-12-17 10:06:36'),
(312072,92738,1,94.475,618.258,712.733,'今日收益','2025-12-17 10:08:43','2025-12-17 10:08:43'),
(312232,92738,1,93.633,712.733,806.366,'今日收益','2025-12-18 10:09:10','2025-12-18 10:09:10'),
(312258,92738,1,130.045,806.366,936.411,'今日收益','2025-12-18 10:19:00','2025-12-18 10:19:00'),
(312295,92738,1,93.669,936.411,1030.080,'今日收益','2025-12-18 14:54:04','2025-12-18 14:54:04'),
(312382,92738,1,144.693,1030.080,1174.773,'今日收益','2025-12-19 10:07:24','2025-12-19 10:07:24'),
(312406,92738,1,107.415,1174.773,1282.188,'今日收益','2025-12-19 10:10:20','2025-12-19 10:10:20'),
(312460,92738,1,83.586,1282.188,1365.774,'今日收益','2025-12-19 15:06:36','2025-12-19 15:06:36'),
(312554,92738,1,83.889,1365.774,1449.663,'今日收益','2025-12-22 10:07:08','2025-12-22 10:07:08'),
(312630,92738,1,100.418,1449.663,1550.081,'今日收益','2025-12-22 11:08:59','2025-12-22 11:08:59'),
(312633,92738,1,107.565,1550.081,1657.646,'今日收益','2025-12-22 11:09:08','2025-12-22 11:09:08'),
(312729,92738,1,82.416,1657.646,1740.062,'今日收益','2025-12-23 10:05:19','2025-12-23 10:05:19'),
(312744,92738,1,92.719,1740.062,1832.781,'今日收益','2025-12-23 10:06:50','2025-12-23 10:06:50'),
(312848,92738,1,98.118,1832.781,1930.899,'今日收益','2025-12-23 17:24:28','2025-12-23 17:24:28'),
(312865,92738,1,83.153,1930.899,2014.052,'今日收益','2025-12-24 10:01:33','2025-12-24 10:01:33'),
(313079,92738,1,140.245,2014.052,2154.297,'今日收益','2025-12-25 10:03:14','2025-12-25 10:03:14'),
(313114,92738,1,81.426,2154.297,2235.723,'今日收益','2025-12-25 10:06:02','2025-12-25 10:06:02'),
(313207,92738,1,99.241,2235.723,2334.964,'今日收益','2025-12-25 13:23:17','2025-12-25 13:23:17'),
(313266,92738,1,91.052,2334.964,2426.016,'今日收益','2025-12-26 10:03:22','2025-12-26 10:03:22'),
(313326,92738,1,104.739,2426.016,2530.755,'今日收益','2025-12-26 10:05:18','2025-12-26 10:05:18'),
(313497,92738,1,129.364,2530.755,2660.119,'今日收益','2025-12-29 10:07:17','2025-12-29 10:07:17'),
(313529,92738,1,83.338,2660.119,2743.457,'今日收益','2025-12-29 10:09:06','2025-12-29 10:09:06'),
(313581,92738,1,94.002,2743.457,2837.459,'今日收益','2025-12-29 14:42:55','2025-12-29 14:42:55'),
(313661,92738,1,94.025,2837.459,2931.484,'今日收益','2025-12-30 10:05:34','2025-12-30 10:05:34'),
(313726,92738,1,128.815,2931.484,3060.299,'今日收益','2025-12-30 10:17:25','2025-12-30 10:17:25'),
(313802,92738,1,90.495,3060.299,3150.794,'今日收益','2025-12-31 10:00:31','2025-12-31 10:00:31'),
(313822,92738,1,93.210,3150.794,3244.004,'今日收益','2025-12-31 10:02:43','2025-12-31 10:02:43'),
(313972,92738,1,138.472,3244.004,3382.476,'今日收益','2025-12-31 13:52:20','2025-12-31 13:52:20'),
(314034,92738,1,111.846,3382.476,3494.322,'今日收益','2026-01-01 10:03:53','2026-01-01 10:03:53'),
(314050,92738,1,115.047,3494.322,3609.369,'今日收益','2026-01-01 10:05:28','2026-01-01 10:05:28'),
(314102,92738,1,94.588,3609.369,3703.957,'今日收益','2026-01-01 10:14:04','2026-01-01 10:14:04'),
(314195,92738,1,121.716,3703.957,3825.673,'今日收益','2026-01-02 10:01:42','2026-01-02 10:01:42'),
(314268,92738,1,105.800,3825.673,3931.473,'今日收益','2026-01-02 10:06:40','2026-01-02 10:06:40'),
(314340,92738,1,95.205,3931.473,4026.678,'今日收益','2026-01-02 10:39:16','2026-01-02 10:39:16'),
(314431,92738,1,100.144,4026.678,4126.822,'今日收益','2026-01-05 10:03:43','2026-01-05 10:03:43'),
(314513,92738,1,118.657,4126.822,4245.479,'今日收益','2026-01-05 10:08:31','2026-01-05 10:08:31'),
(314582,92738,1,89.535,4245.479,4335.014,'今日收益','2026-01-05 15:49:58','2026-01-05 15:49:58'),
(314609,92738,1,122.217,4335.014,4457.231,'今日收益','2026-01-06 10:01:44','2026-01-06 10:01:44'),
(314675,92738,1,112.496,4457.231,4569.727,'今日收益','2026-01-06 10:06:04','2026-01-06 10:06:04'),
(314764,92738,1,85.393,4569.727,4655.120,'今日收益','2026-01-06 12:27:13','2026-01-06 12:27:13'),
(314782,92827,1,93.589,0.000,93.589,'今日收益','2026-01-06 15:31:08','2026-01-06 15:31:08'),
(314787,92827,1,96.111,93.589,189.700,'今日收益','2026-01-07 10:00:24','2026-01-07 10:00:24'),
(314820,92738,1,133.002,4655.120,4788.122,'今日收益','2026-01-07 10:04:27','2026-01-07 10:04:27'),
(314840,92738,1,114.083,4788.122,4902.205,'今日收益','2026-01-07 10:05:19','2026-01-07 10:05:19'),
(314874,92738,1,106.456,4902.205,5008.661,'今日收益','2026-01-07 10:07:32','2026-01-07 10:07:32'),
(314990,92738,1,109.649,5008.661,5118.310,'今日收益','2026-01-08 10:01:22','2026-01-08 10:01:22'),
(314996,92738,1,122.366,5118.310,5240.676,'今日收益','2026-01-08 10:01:33','2026-01-08 10:01:33'),
(315161,92738,1,81.937,5240.676,5322.613,'今日收益','2026-01-08 15:25:39','2026-01-08 15:25:39'),
(315166,92827,1,97.837,189.700,287.537,'今日收益','2026-01-08 15:39:57','2026-01-08 15:39:57'),
(315200,92738,1,114.083,5322.613,5436.696,'今日收益','2026-01-09 10:02:45','2026-01-09 10:02:45'),
(315223,92827,1,94.814,287.537,382.351,'今日收益','2026-01-09 10:04:47','2026-01-09 10:04:47'),
(315293,92738,1,140.760,5436.696,5577.456,'今日收益','2026-01-09 10:10:28','2026-01-09 10:10:28'),
(315345,92738,1,95.168,5577.456,5672.624,'今日收益','2026-01-09 12:59:07','2026-01-09 12:59:07'),
(315388,92738,1,116.094,5672.624,5788.718,'今日收益','2026-01-12 10:01:50','2026-01-12 10:01:50'),
(315418,92827,1,86.035,382.351,468.386,'今日收益','2026-01-12 10:03:34','2026-01-12 10:03:34'),
(315469,92738,1,112.939,5788.718,5901.657,'今日收益','2026-01-12 10:07:09','2026-01-12 10:07:09'),
(315493,92738,1,97.227,5901.657,5998.884,'今日收益','2026-01-12 10:18:02','2026-01-12 10:18:02'),
(315607,92738,1,100.588,5998.884,6099.472,'今日收益','2026-01-13 10:02:13','2026-01-13 10:02:13'),
(315671,92738,1,133.318,6099.472,6232.790,'今日收益','2026-01-13 10:05:10','2026-01-13 10:05:10'),
(315765,92827,1,96.846,468.386,565.232,'今日收益','2026-01-13 15:25:31','2026-01-13 15:25:31'),
(315795,92738,1,103.148,6232.790,6335.938,'今日收益','2026-01-14 10:01:50','2026-01-14 10:01:50'),
(315840,92827,1,91.433,565.232,656.665,'今日收益','2026-01-14 10:04:08','2026-01-14 10:04:08'),
(315873,92738,1,99.288,6335.938,6435.226,'今日收益','2026-01-14 10:06:57','2026-01-14 10:06:57'),
(315898,92738,1,137.724,6435.226,6572.950,'今日收益','2026-01-14 10:10:51','2026-01-14 10:10:51'),
(315957,92738,1,87.717,6572.950,6660.667,'今日收益','2026-01-14 13:17:42','2026-01-14 13:17:42'),
(316016,92738,1,105.224,6660.667,6765.891,'今日收益','2026-01-15 10:02:37','2026-01-15 10:02:37'),
(316036,92738,1,98.352,6765.891,6864.243,'今日收益','2026-01-15 10:03:21','2026-01-15 10:03:21'),
(316048,92738,1,91.561,6864.243,6955.804,'今日收益','2026-01-15 10:04:12','2026-01-15 10:04:12'),
(316088,92738,1,138.356,6955.804,7094.160,'今日收益','2026-01-15 10:06:30','2026-01-15 10:06:30'),
(316170,92827,1,90.349,656.665,747.014,'今日收益','2026-01-15 15:12:30','2026-01-15 15:12:30'),
(316200,92738,1,97.137,7094.160,7191.297,'今日收益','2026-01-16 10:00:51','2026-01-16 10:00:51'),
(316207,92827,1,94.585,747.014,841.599,'今日收益','2026-01-16 10:01:28','2026-01-16 10:01:28'),
(316307,92738,1,98.072,7191.297,7289.369,'今日收益','2026-01-16 10:12:28','2026-01-16 10:12:28'),
(316344,92738,1,144.774,7289.369,7434.143,'今日收益','2026-01-16 10:31:56','2026-01-16 10:31:56'),
(316355,92738,1,96.923,7434.143,7531.066,'今日收益','2026-01-16 11:04:40','2026-01-16 11:04:40'),
(316443,92738,1,124.589,7531.066,7655.655,'今日收益','2026-01-19 10:04:09','2026-01-19 10:04:09'),
(316504,92738,1,109.001,7655.655,7764.656,'今日收益','2026-01-19 10:09:12','2026-01-19 10:09:12'),
(316530,92827,1,97.946,841.599,939.545,'今日收益','2026-01-19 10:14:59','2026-01-19 10:14:59'),
(316535,92738,1,97.310,7764.656,7861.966,'今日收益','2026-01-19 10:15:18','2026-01-19 10:15:18'),
(316616,92738,1,86.203,7861.966,7948.169,'今日收益','2026-01-19 20:11:57','2026-01-19 20:11:57'),
(316653,92827,1,93.798,939.545,1033.343,'今日收益','2026-01-20 10:03:11','2026-01-20 10:03:11'),
(316673,92738,1,114.761,7948.169,8062.930,'今日收益','2026-01-20 10:04:17','2026-01-20 10:04:17'),
(316728,92738,1,86.558,8062.930,8149.488,'今日收益','2026-01-20 10:07:56','2026-01-20 10:07:56'),
(316757,92738,1,85.162,8149.488,8234.650,'今日收益','2026-01-20 10:13:46','2026-01-20 10:13:46'),
(316847,92738,1,89.372,8234.650,8324.022,'今日收益','2026-01-21 10:01:01','2026-01-21 10:01:01'),
(316900,92738,1,107.534,8324.022,8431.556,'今日收益','2026-01-21 10:04:04','2026-01-21 10:04:04'),
(316943,92827,1,94.196,1033.343,1127.539,'今日收益','2026-01-21 10:09:12','2026-01-21 10:09:12'),
(317036,92738,1,87.717,8431.556,8519.273,'今日收益','2026-01-21 14:46:26','2026-01-21 14:46:26'),
(317211,92738,1,110.381,8519.273,8629.654,'今日收益','2026-01-22 10:31:18','2026-01-22 10:31:18'),
(317231,92827,1,97.928,1127.539,1225.467,'今日收益','2026-01-22 12:11:17','2026-01-22 12:11:17'),
(317237,92738,1,91.286,8629.654,8720.940,'今日收益','2026-01-22 12:19:30','2026-01-22 12:19:30'),
(317303,92738,1,92.308,8720.940,8813.248,'今日收益','2026-01-23 10:01:44','2026-01-23 10:01:44'),
(317324,92738,1,148.786,8813.248,8962.034,'今日收益','2026-01-23 10:03:36','2026-01-23 10:03:36'),
(317360,92738,1,140.226,8962.034,9102.260,'今日收益','2026-01-23 10:05:19','2026-01-23 10:05:19'),
(317392,92738,1,82.225,9102.260,9184.485,'今日收益','2026-01-23 10:08:44','2026-01-23 10:08:44'),
(317396,92827,1,99.751,1225.467,1325.218,'今日收益','2026-01-23 10:09:48','2026-01-23 10:09:48'),
(317530,92738,1,130.927,9184.485,9315.412,'今日收益','2026-01-26 10:01:52','2026-01-26 10:01:52'),
(317613,92738,1,155.033,9315.412,9470.445,'今日收益','2026-01-26 10:08:28','2026-01-26 10:08:28'),
(317631,92827,1,96.597,1325.218,1421.815,'今日收益','2026-01-26 10:12:00','2026-01-26 10:12:00'),
(317682,92738,1,92.271,9470.445,9562.716,'今日收益','2026-01-26 10:56:37','2026-01-26 10:56:37'),
(317690,92738,1,84.692,9562.716,9647.408,'今日收益','2026-01-26 11:48:50','2026-01-26 11:48:50'),
(317744,92738,1,145.376,9647.408,9792.784,'今日收益','2026-01-27 10:01:20','2026-01-27 10:01:20'),
(317757,92738,1,94.233,9792.784,9887.017,'今日收益','2026-01-27 10:02:07','2026-01-27 10:02:07'),
(317850,92738,1,133.040,9887.017,10020.057,'今日收益','2026-01-27 10:13:37','2026-01-27 10:13:37'),
(317853,92827,1,95.205,1421.815,1517.020,'今日收益','2026-01-27 10:14:17','2026-01-27 10:14:17'),
(317957,92738,1,87.232,10020.057,10107.289,'今日收益','2026-01-27 15:52:46','2026-01-27 15:52:46'),
(318015,92738,1,159.968,10107.289,10267.257,'今日收益','2026-01-28 10:02:54','2026-01-28 10:02:54'),
(318027,92827,1,93.171,1517.020,1610.191,'今日收益','2026-01-28 10:03:35','2026-01-28 10:03:35'),
(318061,92738,1,97.890,10267.257,10365.147,'今日收益','2026-01-28 10:04:39','2026-01-28 10:04:39'),
(318068,92738,1,117.884,10365.147,10483.031,'今日收益','2026-01-28 10:05:24','2026-01-28 10:05:24'),
(318155,92738,1,86.475,10483.031,10569.506,'今日收益','2026-01-28 14:45:29','2026-01-28 14:45:29'),
(318225,92738,1,83.730,10569.506,10653.236,'今日收益','2026-01-29 10:02:29','2026-01-29 10:02:29'),
(318310,92738,1,154.229,10653.236,10807.465,'今日收益','2026-01-29 10:07:34','2026-01-29 10:07:34'),
(318416,92738,1,132.177,10807.465,10939.642,'今日收益','2026-01-29 15:57:44','2026-01-29 15:57:44'),
(318417,92827,1,98.370,1610.191,1708.561,'今日收益','2026-01-29 16:01:42','2026-01-29 16:01:42'),
(318452,92738,1,82.436,10939.642,11022.078,'今日收益','2026-01-30 10:01:25','2026-01-30 10:01:25'),
(318481,92827,1,95.321,1708.561,1803.882,'今日收益','2026-01-30 10:03:20','2026-01-30 10:03:20'),
(318569,92738,1,130.385,11022.078,11152.463,'今日收益','2026-01-30 10:07:08','2026-01-30 10:07:08'),
(318598,92738,1,81.982,11152.463,11234.445,'今日收益','2026-01-30 10:38:51','2026-01-30 10:38:51'),
(318662,92738,1,99.971,11234.445,11334.416,'今日收益','2026-02-02 10:02:22','2026-02-02 10:02:22'),
(318789,92738,1,149.737,11334.416,11484.153,'今日收益','2026-02-02 10:21:02','2026-02-02 10:21:02'),
(318809,92827,1,94.493,1803.882,1898.375,'今日收益','2026-02-02 10:37:14','2026-02-02 10:37:14'),
(318815,92738,1,161.709,11484.153,11645.862,'今日收益','2026-02-02 11:41:41','2026-02-02 11:41:41'),
(318978,92738,1,90.435,11645.862,11736.297,'今日收益','2026-02-03 10:06:31','2026-02-03 10:06:31'),
(319003,92738,1,103.206,11736.297,11839.503,'今日收益','2026-02-03 10:11:34','2026-02-03 10:11:34'),
(319019,92738,1,154.229,11839.503,11993.732,'今日收益','2026-02-03 10:25:37','2026-02-03 10:25:37'),
(319055,92827,1,101.126,1898.375,1999.501,'今日收益','2026-02-03 14:14:56','2026-02-03 14:14:56'),
(319090,92738,1,85.826,11993.732,12079.558,'今日收益','2026-02-04 10:00:13','2026-02-04 10:00:13'),
(319248,92738,1,104.638,12079.558,12184.196,'今日收益','2026-02-04 10:15:38','2026-02-04 10:15:38'),
(319249,92738,1,144.022,12184.196,12328.218,'今日收益','2026-02-04 10:15:45','2026-02-04 10:15:45'),
(319267,92827,1,97.580,1999.501,2097.081,'今日收益','2026-02-04 10:41:44','2026-02-04 10:41:44'),
(319315,92738,1,82.927,12328.218,12411.145,'今日收益','2026-02-05 10:00:30','2026-02-05 10:00:30'),
(319412,92738,1,148.342,12411.145,12559.487,'今日收益','2026-02-05 10:04:23','2026-02-05 10:04:23'),
(319436,92827,1,100.665,2097.081,2197.746,'今日收益','2026-02-05 10:06:50','2026-02-05 10:06:50'),
(319477,92738,1,100.665,12559.487,12660.152,'今日收益','2026-02-05 10:17:26','2026-02-05 10:17:26'),
(319589,92738,1,92.353,12660.152,12752.505,'今日收益','2026-02-06 10:02:05','2026-02-06 10:02:05'),
(319642,92738,1,143.299,12752.505,12895.804,'今日收益','2026-02-06 10:03:56','2026-02-06 10:03:56'),
(319668,92738,1,84.265,12895.804,12980.069,'今日收益','2026-02-06 10:06:45','2026-02-06 10:06:45'),
(319804,92827,1,88.191,2197.746,2285.937,'今日收益','2026-02-09 10:02:00','2026-02-09 10:02:00'),
(319840,92738,1,153.318,12980.069,13133.387,'今日收益','2026-02-09 10:03:19','2026-02-09 10:03:19'),
(319877,92738,1,92.994,13133.387,13226.381,'今日收益','2026-02-09 10:05:08','2026-02-09 10:05:08'),
(320002,92827,1,96.138,2285.937,2382.075,'今日收益','2026-02-10 10:00:46','2026-02-10 10:00:46'),
(320005,92738,1,95.784,13226.381,13322.165,'今日收益','2026-02-10 10:01:04','2026-02-10 10:01:04'),
(320084,92738,1,141.877,13322.165,13464.042,'今日收益','2026-02-10 10:05:35','2026-02-10 10:05:35'),
(320213,92738,1,104.361,13464.042,13568.403,'今日收益','2026-02-19 10:02:30','2026-02-19 10:02:30'),
(320240,92738,1,128.351,13568.403,13696.754,'今日收益','2026-02-19 10:27:47','2026-02-19 10:27:47'),
(320259,92827,1,99.441,2382.075,2481.516,'今日收益','2026-02-19 18:23:24','2026-02-19 18:23:24'),
(320296,92738,1,120.499,13696.754,13817.253,'今日收益','2026-02-20 11:06:44','2026-02-20 11:06:44'),
(320301,92738,1,100.482,13817.253,13917.735,'今日收益','2026-02-20 11:25:03','2026-02-20 11:25:03'),
(320319,92827,1,102.480,2481.516,2583.996,'今日收益','2026-02-20 15:03:11','2026-02-20 15:03:11'),
(320338,92738,1,136.167,13917.735,14053.902,'今日收益','2026-02-23 10:01:35','2026-02-23 10:01:35'),
(320347,92738,1,97.687,14053.902,14151.589,'今日收益','2026-02-23 10:02:57','2026-02-23 10:02:57'),
(320364,92827,1,107.062,2583.996,2691.058,'今日收益','2026-02-23 11:56:55','2026-02-23 11:56:55'),
(320406,92738,1,98.433,14151.589,14250.022,'今日收益','2026-02-24 10:06:11','2026-02-24 10:06:11'),
(320439,92738,1,126.706,14250.022,14376.728,'今日收益','2026-02-24 14:59:48','2026-02-24 14:59:48'),
(320440,92827,1,114.561,2691.058,2805.619,'今日收益','2026-02-24 15:00:00','2026-02-24 15:00:00'),
(320450,92827,1,137.803,2805.619,2943.422,'今日收益','2026-02-25 10:00:49','2026-02-25 10:00:49'),
(320482,92738,1,113.582,14376.728,14490.310,'今日收益','2026-02-25 11:20:10','2026-02-25 11:20:10'),
(320501,92738,1,93.685,14490.310,14583.995,'今日收益','2026-02-25 14:53:33','2026-02-25 14:53:33'),
(320534,92738,1,106.949,14583.995,14690.944,'今日收益','2026-02-26 10:05:58','2026-02-26 10:05:58'),
(320541,92827,1,108.201,2943.422,3051.623,'今日收益','2026-02-26 10:13:01','2026-02-26 10:13:01'),
(320554,92738,1,107.160,14690.944,14798.104,'今日收益','2026-02-26 12:51:45','2026-02-26 12:51:45'),
(320564,92827,1,84.704,3051.623,3136.327,'今日收益','2026-02-26 16:01:38','2026-02-26 16:01:38'),
(320582,92827,1,93.486,3136.327,3229.813,'今日收益','2026-02-27 10:02:17','2026-02-27 10:02:17'),
(320588,92827,1,98.610,3229.813,3328.423,'今日收益','2026-02-27 10:03:53','2026-02-27 10:03:53'),
(320594,92738,1,118.726,14798.104,14916.830,'今日收益','2026-02-27 10:05:33','2026-02-27 10:05:33'),
(320635,92738,1,115.069,14916.830,15031.899,'今日收益','2026-03-02 10:01:29','2026-03-02 10:01:29'),
(320642,92827,1,113.245,3328.423,3441.668,'今日收益','2026-03-02 10:02:19','2026-03-02 10:02:19'),
(320657,92827,1,89.863,3441.668,3531.531,'今日收益','2026-03-02 10:05:54','2026-03-02 10:05:54'),
(320667,92738,1,82.054,15031.899,15113.953,'今日收益','2026-03-02 10:11:32','2026-03-02 10:11:32'),
(320710,92827,1,118.234,3531.531,3649.765,'今日收益','2026-03-03 10:07:12','2026-03-03 10:07:12'),
(320711,92738,1,117.997,15113.953,15231.950,'今日收益','2026-03-03 10:10:35','2026-03-03 10:10:35'),
(320731,92827,1,101.442,3649.765,3751.207,'今日收益','2026-03-03 10:50:28','2026-03-03 10:50:28'),
(320749,92738,1,162.591,15231.950,15394.541,'今日收益','2026-03-03 22:00:19','2026-03-03 22:00:19'),
(320766,92827,1,107.931,3751.207,3859.138,'今日收益','2026-03-04 10:02:03','2026-03-04 10:02:03'),
(320777,92738,1,106.619,15394.541,15501.160,'今日收益','2026-03-04 10:03:39','2026-03-04 10:03:39'),
(320781,92827,1,116.866,3859.138,3976.004,'今日收益','2026-03-04 10:04:14','2026-03-04 10:04:14'),
(320792,92738,1,159.751,15501.160,15660.911,'今日收益','2026-03-04 10:10:22','2026-03-04 10:10:22'),
(320796,92738,1,129.201,15660.911,15790.112,'今日收益','2026-03-04 12:45:31','2026-03-04 12:45:31'),
(320832,92738,1,125.434,15790.112,15915.546,'今日收益','2026-03-05 10:02:50','2026-03-05 10:02:50'),
(320838,92827,1,116.952,3976.004,4092.956,'今日收益','2026-03-05 10:03:35','2026-03-05 10:03:35'),
(320848,92827,1,112.745,4092.956,4205.701,'今日收益','2026-03-05 10:06:50','2026-03-05 10:06:50'),
(320859,92738,1,157.551,15915.546,16073.097,'今日收益','2026-03-05 10:52:27','2026-03-05 10:52:27'),
(320889,92738,1,114.223,16073.097,16187.320,'今日收益','2026-03-06 10:01:42','2026-03-06 10:01:42'),
(320913,92827,1,128.939,4205.701,4334.640,'今日收益','2026-03-06 10:07:54','2026-03-06 10:07:54'),
(320935,92827,1,113.765,4334.640,4448.405,'今日收益','2026-03-06 12:34:01','2026-03-06 12:34:01'),
(320937,92738,1,157.224,16187.320,16344.544,'今日收益','2026-03-06 14:49:37','2026-03-06 14:49:37'),
(320949,92738,1,120.221,16344.544,16464.765,'今日收益','2026-03-09 10:01:43','2026-03-09 10:01:43'),
(320995,92738,1,88.662,16464.765,16553.427,'今日收益','2026-03-09 12:37:42','2026-03-09 12:37:42'),
(320996,92738,1,141.856,16553.427,16695.283,'今日收益','2026-03-09 12:44:26','2026-03-09 12:44:26'),
(321061,92738,1,141.182,16695.283,16836.465,'今日收益','2026-03-10 11:56:21','2026-03-10 11:56:21'),
(321067,92827,1,132.286,4448.405,4580.691,'今日收益','2026-03-10 13:38:54','2026-03-10 13:38:54'),
(321069,92738,1,118.985,16836.465,16955.450,'今日收益','2026-03-10 14:26:28','2026-03-10 14:26:28'),
(321075,92738,1,123.200,16955.450,17078.650,'今日收益','2026-03-10 15:41:35','2026-03-10 15:41:35'),
(321085,92738,1,94.062,17078.650,17172.712,'今日收益','2026-03-11 10:01:35','2026-03-11 10:01:35'),
(321101,92827,1,130.343,4580.691,4711.034,'今日收益','2026-03-11 10:04:04','2026-03-11 10:04:04'),
(321102,92738,1,139.544,17172.712,17312.256,'今日收益','2026-03-11 10:04:12','2026-03-11 10:04:12'),
(321113,92738,1,124.916,17312.256,17437.172,'今日收益','2026-03-11 10:07:28','2026-03-11 10:07:28'),
(321147,92738,1,139.544,17437.172,17576.716,'今日收益','2026-03-12 10:00:25','2026-03-12 10:00:25'),
(321211,92738,1,91.322,17576.716,17668.038,'今日收益','2026-03-12 16:23:06','2026-03-12 16:23:06'),
(321218,92738,1,148.334,17668.038,17816.372,'今日收益','2026-03-13 10:01:39','2026-03-13 10:01:39'),
(321234,92738,1,91.133,17816.372,17907.505,'今日收益','2026-03-13 10:03:48','2026-03-13 10:03:48'),
(321246,92738,1,143.348,17907.505,18050.853,'今日收益','2026-03-13 10:07:46','2026-03-13 10:07:46'),
(321280,92738,2,-3378.000,18050.853,14672.853,'用户提现','2026-03-14 15:55:10','2026-03-14 15:55:10'),
(321302,92738,1,151.293,14672.853,14824.146,'今日收益','2026-03-16 10:26:44','2026-03-16 10:26:44'),
(321309,92738,1,136.389,14824.146,14960.535,'今日收益','2026-03-16 14:54:12','2026-03-16 14:54:12'),
(321318,92738,1,82.172,14960.535,15042.707,'今日收益','2026-03-16 15:04:29','2026-03-16 15:04:29'),
(321326,92738,1,143.550,15042.707,15186.257,'今日收益','2026-03-17 10:00:56','2026-03-17 10:00:56'),
(321353,92738,1,91.282,15186.257,15277.539,'今日收益','2026-03-17 10:39:37','2026-03-17 10:39:37'),
(321363,92738,1,153.705,15277.539,15431.244,'今日收益','2026-03-17 14:54:26','2026-03-17 14:54:26'),
(321373,92738,2,-738.000,15431.244,14693.244,'用户提现','2026-03-17 16:44:49','2026-03-17 16:44:49'),
(321374,92738,2,-1342.000,14693.244,13351.244,'用户提现','2026-03-17 16:45:06','2026-03-17 16:45:06'),
(321382,92738,1,140.326,13351.244,13491.570,'今日收益','2026-03-18 10:01:12','2026-03-18 10:01:12'),
(321395,92738,1,160.507,13491.570,13652.077,'今日收益','2026-03-18 10:05:05','2026-03-18 10:05:05'),
(321409,92738,1,111.212,13652.077,13763.289,'今日收益','2026-03-18 10:15:23','2026-03-18 10:15:23'),
(321421,92738,2,-1460.000,13763.289,12303.289,'用户提现','2026-03-18 14:22:46','2026-03-18 14:22:46'),
(321438,92738,1,131.966,12303.289,12435.255,'今日收益','2026-03-19 10:01:08','2026-03-19 10:01:08'),
(321447,92738,1,89.158,12435.255,12524.413,'今日收益','2026-03-19 10:02:31','2026-03-19 10:02:31'),
(321455,92738,1,149.156,12524.413,12673.569,'今日收益','2026-03-19 10:04:21','2026-03-19 10:04:21'),
(321488,92738,2,-2011.000,12673.569,10662.569,'用户提现','2026-03-19 16:08:31','2026-03-19 16:08:31'),
(321530,92738,1,93.283,10662.569,10755.852,'今日收益','2026-03-20 10:05:14','2026-03-20 10:05:14'),
(321539,92738,1,118.407,10755.852,10874.259,'今日收益','2026-03-20 10:06:59','2026-03-20 10:06:59'),
(321546,92738,1,124.114,10874.259,10998.373,'今日收益','2026-03-20 10:54:18','2026-03-20 10:54:18'),
(321556,92738,2,-1614.000,10998.373,9384.373,'用户提现','2026-03-20 16:28:35','2026-03-20 16:28:35'),
(321563,92738,1,119.155,9384.373,9503.528,'今日收益','2026-03-23 10:02:02','2026-03-23 10:02:02'),
(321586,92738,1,127.837,9503.528,9631.365,'今日收益','2026-03-23 10:05:06','2026-03-23 10:05:06'),
(321617,92738,2,-1617.000,9631.365,8014.365,'用户提现','2026-03-23 15:27:54','2026-03-23 15:27:54'),
(321621,92738,1,92.105,8014.365,8106.470,'今日收益','2026-03-23 17:30:01','2026-03-23 17:30:01'),
(321630,92738,1,92.755,8106.470,8199.225,'今日收益','2026-03-24 10:01:28','2026-03-24 10:01:28'),
(321640,92738,1,115.684,8199.225,8314.909,'今日收益','2026-03-24 10:02:50','2026-03-24 10:02:50'),
(321650,92827,1,81.493,4711.034,4792.527,'今日收益','2026-03-24 10:04:48','2026-03-24 10:04:48'),
(321684,92738,1,94.243,8314.909,8409.152,'今日收益','2026-03-24 15:01:36','2026-03-24 15:01:36'),
(321690,92738,1,94.868,8409.152,8504.020,'今日收益','2026-03-24 18:53:00','2026-03-24 18:53:00'),
(321717,92738,1,135.622,8504.020,8639.642,'今日收益','2026-03-25 10:05:33','2026-03-25 10:05:33'),
(321720,92827,1,85.703,4792.527,4878.230,'今日收益','2026-03-25 10:05:50','2026-03-25 10:05:50'),
(321731,92738,1,101.933,8639.642,8741.575,'今日收益','2026-03-25 10:22:51','2026-03-25 10:22:51'),
(321736,92738,1,92.545,8741.575,8834.120,'今日收益','2026-03-25 10:36:25','2026-03-25 10:36:25'),
(321787,92738,1,82.927,8834.120,8917.047,'今日收益','2026-03-26 10:04:44','2026-03-26 10:04:44'),
(321791,92738,1,133.268,8917.047,9050.315,'今日收益','2026-03-26 10:05:43','2026-03-26 10:05:43'),
(321792,92738,1,94.865,9050.315,9145.180,'今日收益','2026-03-26 10:06:45','2026-03-26 10:06:45'),
(321801,92827,1,91.103,4878.230,4969.333,'今日收益','2026-03-26 10:08:44','2026-03-26 10:08:44'),
(321823,92738,1,89.420,9145.180,9234.600,'今日收益','2026-03-26 18:58:58','2026-03-26 18:58:58'),
(321834,92738,1,132.022,9234.600,9366.622,'今日收益','2026-03-27 10:01:17','2026-03-27 10:01:17'),
(321838,92738,1,84.584,9366.622,9451.206,'今日收益','2026-03-27 10:01:43','2026-03-27 10:01:43'),
(321841,92738,1,102.799,9451.206,9554.005,'今日收益','2026-03-27 10:02:06','2026-03-27 10:02:06'),
(321856,92738,1,88.676,9554.005,9642.681,'今日收益','2026-03-27 10:08:06','2026-03-27 10:08:06'),
(321880,92827,1,92.102,4969.333,5061.435,'今日收益','2026-03-27 13:28:42','2026-03-27 13:28:42'),
(321898,92738,1,87.977,9642.681,9730.658,'今日收益','2026-03-30 10:03:24','2026-03-30 10:03:24'),
(321903,92738,1,126.148,9730.658,9856.806,'今日收益','2026-03-30 10:06:28','2026-03-30 10:06:28'),
(321950,92738,1,81.832,9856.806,9938.638,'今日收益','2026-03-30 14:15:45','2026-03-30 14:15:45'),
(321957,92738,1,137.724,9938.638,10076.362,'今日收益','2026-03-30 15:12:11','2026-03-30 15:12:11'),
(321960,92827,1,94.865,5061.435,5156.300,'今日收益','2026-03-30 15:43:43','2026-03-30 15:43:43'),
(321962,92738,1,89.735,10076.362,10166.097,'今日收益','2026-03-31 10:00:10','2026-03-31 10:00:10'),
(321984,92827,1,99.551,5156.300,5255.851,'今日收益','2026-03-31 10:05:47','2026-03-31 10:05:47'),
(321988,92738,1,90.617,10166.097,10256.714,'今日收益','2026-03-31 10:06:28','2026-03-31 10:06:28'),
(322015,92738,1,141.856,10256.714,10398.570,'今日收益','2026-03-31 12:26:53','2026-03-31 12:26:53'),
(322022,92738,1,153.086,10398.570,10551.656,'今日收益','2026-03-31 15:36:15','2026-03-31 15:36:15'),
(322033,92738,1,92.427,10551.656,10644.083,'今日收益','2026-04-01 10:00:11','2026-04-01 10:00:11'),
(322038,92738,1,94.293,10644.083,10738.376,'今日收益','2026-04-01 10:01:22','2026-04-01 10:01:22'),
(322049,92738,1,150.758,10738.376,10889.134,'今日收益','2026-04-01 10:03:07','2026-04-01 10:03:07'),
(322088,92738,1,146.545,10889.134,11035.679,'今日收益','2026-04-01 14:55:31','2026-04-01 14:55:31'),
(322095,92827,1,93.335,5255.851,5349.186,'今日收益','2026-04-01 19:36:07','2026-04-01 19:36:07'),
(322106,92738,1,89.420,11035.679,11125.099,'今日收益','2026-04-02 10:02:02','2026-04-02 10:02:02'),
(322120,92738,1,82.436,11125.099,11207.535,'今日收益','2026-04-02 10:03:50','2026-04-02 10:03:50'),
(322123,92738,1,120.672,11207.535,11328.207,'今日收益','2026-04-02 10:04:46','2026-04-02 10:04:46'),
(322135,92827,1,103.662,5349.186,5452.848,'今日收益','2026-04-02 10:06:26','2026-04-02 10:06:26'),
(322152,92738,2,-599.000,11328.207,10729.207,'用户提现','2026-04-02 12:28:46','2026-04-02 12:28:46'),
(322154,92738,1,150.941,10729.207,10880.148,'今日收益','2026-04-02 15:49:54','2026-04-02 15:49:54'),
(322169,92738,1,84.178,10880.148,10964.326,'今日收益','2026-04-03 10:00:25','2026-04-03 10:00:25'),
(322188,92738,1,123.415,10964.326,11087.741,'今日收益','2026-04-03 10:02:40','2026-04-03 10:02:40'),
(322197,92827,1,100.102,5452.848,5552.950,'今日收益','2026-04-03 10:04:29','2026-04-03 10:04:29'),
(322227,92738,1,134.051,11087.741,11221.792,'今日收益','2026-04-03 15:02:24','2026-04-03 15:02:24'),
(322240,92738,2,-601.000,11221.792,10620.792,'用户提现','2026-04-03 22:07:29','2026-04-03 22:07:29'),
(322259,92827,1,86.150,5552.950,5639.100,'今日收益','2026-04-06 10:04:10','2026-04-06 10:04:10'),
(322261,92738,1,129.125,10620.792,10749.917,'今日收益','2026-04-06 10:04:24','2026-04-06 10:04:24'),
(322286,92738,1,123.781,10749.917,10873.698,'今日收益','2026-04-06 10:47:08','2026-04-06 10:47:08'),
(322301,92738,1,83.870,10873.698,10957.568,'今日收益','2026-04-06 11:49:33','2026-04-06 11:49:33'),
(322326,92738,2,-636.000,10957.568,10321.568,'用户提现','2026-04-06 16:27:15','2026-04-06 16:27:15'),
(322335,92738,1,132.999,10321.568,10454.567,'今日收益','2026-04-07 10:00:36','2026-04-07 10:00:36'),
(322384,92738,1,124.305,10454.567,10578.872,'今日收益','2026-04-07 10:21:46','2026-04-07 10:21:46'),
(322397,92827,1,84.410,5639.100,5723.510,'今日收益','2026-04-07 14:02:40','2026-04-07 14:02:40'),
(322406,92738,1,82.225,10578.872,10661.097,'今日收益','2026-04-07 14:14:36','2026-04-07 14:14:36'),
(322418,92738,2,-999.000,10661.097,9662.097,'用户提现','2026-04-07 18:12:22','2026-04-07 18:12:22'),
(322455,92827,1,131.319,5723.510,5854.829,'今日收益','2026-04-08 10:11:11','2026-04-08 10:11:11'),
(322459,92738,1,91.983,9662.097,9754.080,'今日收益','2026-04-08 10:15:58','2026-04-08 10:15:58'),
(322480,92738,2,-1073.000,9754.080,8681.080,'用户提现','2026-04-08 13:20:52','2026-04-08 13:20:52'),
(322490,92738,1,134.859,8681.080,8815.939,'今日收益','2026-04-08 14:46:40','2026-04-08 14:46:40'),
(322499,92738,1,84.943,8815.939,8900.882,'今日收益','2026-04-08 15:19:27','2026-04-08 15:19:27'),
(322529,92738,1,82.469,8900.882,8983.351,'今日收益','2026-04-09 10:04:21','2026-04-09 10:04:21'),
(322533,92738,1,134.130,8983.351,9117.481,'今日收益','2026-04-09 10:05:33','2026-04-09 10:05:33'),
(322561,92738,1,135.259,9117.481,9252.740,'今日收益','2026-04-09 10:25:44','2026-04-09 10:25:44'),
(322571,92738,2,-1018.000,9252.740,8234.740,'用户提现','2026-04-09 12:25:50','2026-04-09 12:25:50'),
(322588,92827,1,94.138,5854.829,5948.967,'今日收益','2026-04-09 14:54:20','2026-04-09 14:54:20'),
(322601,92738,1,96.682,8234.740,8331.422,'今日收益','2026-04-10 10:01:17','2026-04-10 10:01:17'),
(322607,92738,1,89.849,8331.422,8421.271,'今日收益','2026-04-10 10:02:34','2026-04-10 10:02:34'),
(322609,92738,1,92.706,8421.271,8513.977,'今日收益','2026-04-10 10:02:47','2026-04-10 10:02:47'),
(322628,92738,1,130.835,8513.977,8644.812,'今日收益','2026-04-10 10:06:13','2026-04-10 10:06:13'),
(322644,92827,1,98.433,5948.967,6047.400,'今日收益','2026-04-10 11:10:32','2026-04-10 11:10:32'),
(322645,92738,1,135.831,8644.812,8780.643,'今日收益','2026-04-10 11:11:59','2026-04-10 11:11:59'),
(322672,92738,2,-1033.000,8780.643,7747.643,'用户提现','2026-04-10 18:12:40','2026-04-10 18:12:40'),
(322688,92738,1,95.004,7747.643,7842.647,'今日收益','2026-04-13 10:01:15','2026-04-13 10:01:15'),
(322692,92827,1,100.047,6047.400,6147.447,'今日收益','2026-04-13 10:01:37','2026-04-13 10:01:37'),
(322700,92738,1,142.551,7842.647,7985.198,'今日收益','2026-04-13 10:04:09','2026-04-13 10:04:09'),
(322710,92738,1,144.089,7985.198,8129.287,'今日收益','2026-04-13 10:05:28','2026-04-13 10:05:28'),
(322724,92738,1,124.241,8129.287,8253.528,'今日收益','2026-04-13 10:14:29','2026-04-13 10:14:29'),
(322733,92738,2,-1034.000,8253.528,7219.528,'用户提现','2026-04-13 13:06:37','2026-04-13 13:06:37'),
(322736,92738,1,127.491,7219.528,7347.019,'今日收益','2026-04-13 13:16:32','2026-04-13 13:16:32'),
(322754,92738,1,95.604,7347.019,7442.623,'今日收益','2026-04-14 09:59:25','2026-04-14 09:59:25'),
(322771,92738,1,123.113,7442.623,7565.736,'今日收益','2026-04-14 10:02:18','2026-04-14 10:02:18'),
(322780,92827,1,97.711,6147.447,6245.158,'今日收益','2026-04-14 10:04:11','2026-04-14 10:04:11'),
(322782,92738,1,121.781,7565.736,7687.517,'今日收益','2026-04-14 10:04:25','2026-04-14 10:04:25'),
(322798,92738,1,146.567,7687.517,7834.084,'今日收益','2026-04-14 11:31:02','2026-04-14 11:31:02'),
(322801,92738,1,138.803,7834.084,7972.887,'今日收益','2026-04-14 11:44:53','2026-04-14 11:44:53'),
(322811,92738,2,-1003.000,7972.887,6969.887,'用户提现','2026-04-14 14:35:05','2026-04-14 14:35:05'),
(322834,92738,2,-3372.000,6969.887,3597.887,'用户提现','2026-04-15 09:45:19','2026-04-15 09:45:19'),
(322835,92738,1,3372.000,3597.887,6969.887,'提现退还','2026-04-15 09:48:16','2026-04-15 09:48:16'),
(322836,92738,2,-3378.000,6969.887,3591.887,'用户提现','2026-04-15 09:52:14','2026-04-15 09:52:14'),
(322850,92738,1,143.205,3591.887,3735.092,'今日收益','2026-04-15 10:01:49','2026-04-15 10:01:49'),
(322854,92738,1,83.765,3735.092,3818.857,'今日收益','2026-04-15 10:01:59','2026-04-15 10:01:59'),
(322862,92738,1,142.967,3818.857,3961.824,'今日收益','2026-04-15 10:02:36','2026-04-15 10:02:36'),
(322873,92738,1,101.622,3961.824,4063.446,'今日收益','2026-04-15 10:03:20','2026-04-15 10:03:20'),
(322876,92738,1,131.315,4063.446,4194.761,'今日收益','2026-04-15 10:04:01','2026-04-15 10:04:01'),
(322878,92827,1,106.634,6245.158,6351.792,'今日收益','2026-04-15 10:05:15','2026-04-15 10:05:15'),
(322924,92827,1,101.426,6351.792,6453.218,'今日收益','2026-04-16 10:00:08','2026-04-16 10:00:08'),
(322944,92738,1,136.831,4194.761,4331.592,'今日收益','2026-04-16 10:02:45','2026-04-16 10:02:45'),
(322959,92738,1,110.787,4331.592,4442.379,'今日收益','2026-04-16 10:05:28','2026-04-16 10:05:28'),
(322967,92738,1,130.610,4442.379,4572.989,'今日收益','2026-04-16 10:08:41','2026-04-16 10:08:41'),
(322989,92738,2,-557.000,4572.989,4015.989,'用户提现','2026-04-16 14:15:26','2026-04-16 14:15:26'),
(322990,92738,1,84.906,4015.989,4100.895,'今日收益','2026-04-16 14:27:29','2026-04-16 14:27:29'),
(322995,92738,1,137.802,4100.895,4238.697,'今日收益','2026-04-16 15:15:45','2026-04-16 15:15:45'),
(323020,92827,1,82.930,6453.218,6536.148,'今日收益','2026-04-20 10:03:10','2026-04-20 10:03:10'),
(323037,92738,1,112.081,4238.697,4350.778,'今日收益','2026-04-20 14:09:16','2026-04-20 14:09:16'),
(323038,92738,1,133.073,4350.778,4483.851,'今日收益','2026-04-20 14:46:53','2026-04-20 14:46:53'),
(323074,92827,1,90.076,6536.148,6626.224,'今日收益','2026-04-21 10:03:34','2026-04-21 10:03:34'),
(323079,92738,1,137.066,4483.851,4620.917,'今日收益','2026-04-21 10:10:39','2026-04-21 10:10:39'),
(323099,92738,1,86.025,4620.917,4706.942,'今日收益','2026-04-21 14:14:48','2026-04-21 14:14:48'),
(323135,92738,1,85.671,4706.942,4792.613,'今日收益','2026-04-22 10:57:10','2026-04-22 10:57:10');
-- ---------- `wa_coupon_log` (40 rows) ----------
INSERT INTO `wa_coupon_log` VALUES
(255345,92827,1,88.000,0.000,88.000,'今日收益','2025-12-18 23:59:59','2025-12-19 00:05:02'),
(255380,92827,1,88.000,88.000,176.000,'今日收益','2025-12-19 23:59:59','2025-12-20 00:05:02'),
(255412,92827,1,88.000,176.000,264.000,'今日收益','2025-12-22 23:59:59','2025-12-23 00:05:01'),
(255446,92827,1,88.000,264.000,352.000,'今日收益','2025-12-23 23:59:59','2025-12-24 00:05:02'),
(255517,92827,1,88.000,352.000,440.000,'今日收益','2025-12-25 23:59:59','2025-12-26 00:05:02'),
(255552,92827,1,88.000,440.000,528.000,'今日收益','2025-12-26 23:59:59','2025-12-27 00:05:02'),
(255586,92827,1,88.000,528.000,616.000,'今日收益','2025-12-29 23:59:59','2025-12-30 00:05:01'),
(255619,92827,1,88.000,616.000,704.000,'今日收益','2025-12-30 23:59:59','2025-12-31 00:05:01'),
(255654,92827,1,88.000,704.000,792.000,'今日收益','2025-12-31 23:59:59','2026-01-01 00:05:02'),
(255691,92827,1,88.000,792.000,880.000,'今日收益','2026-01-01 23:59:59','2026-01-02 00:05:01'),
(255725,92827,1,88.000,880.000,968.000,'今日收益','2026-01-02 23:59:59','2026-01-03 00:05:02'),
(255759,92827,1,88.000,968.000,1056.000,'今日收益','2026-01-05 23:59:59','2026-01-06 00:05:01'),
(255800,92827,1,88.000,1056.000,1144.000,'今日收益','2026-01-06 23:59:59','2026-01-07 00:05:02'),
(255838,92827,1,88.000,1144.000,1232.000,'今日收益','2026-01-07 23:59:59','2026-01-08 00:05:01'),
(255873,92827,1,88.000,1232.000,1320.000,'今日收益','2026-01-08 23:59:59','2026-01-09 00:05:01'),
(255911,92827,1,88.000,1320.000,1408.000,'今日收益','2026-01-09 23:59:59','2026-01-10 00:05:02'),
(255950,92827,1,88.000,1408.000,1496.000,'今日收益','2026-01-12 23:59:59','2026-01-13 00:05:02'),
(255986,92827,1,88.000,1496.000,1584.000,'今日收益','2026-01-13 23:59:59','2026-01-14 00:05:01'),
(256024,92827,1,88.000,1584.000,1672.000,'今日收益','2026-01-14 23:59:59','2026-01-15 00:05:01'),
(256062,92827,1,88.000,1672.000,1760.000,'今日收益','2026-01-15 23:59:59','2026-01-16 00:05:02'),
(256101,92827,1,88.000,1760.000,1848.000,'今日收益','2026-01-16 23:59:59','2026-01-17 00:05:01'),
(256136,92827,1,88.000,1848.000,1936.000,'今日收益','2026-01-19 23:59:59','2026-01-20 00:05:01'),
(256283,92827,1,88.000,1936.000,2024.000,'今日收益','2026-01-23 23:59:59','2026-01-24 00:05:01'),
(256326,92827,1,88.000,2024.000,2112.000,'今日收益','2026-01-26 23:59:59','2026-01-27 00:05:01'),
(256363,92827,1,88.000,2112.000,2200.000,'今日收益','2026-01-27 23:59:59','2026-01-28 00:05:01'),
(256400,92827,1,88.000,2200.000,2288.000,'今日收益','2026-01-28 23:59:59','2026-01-29 00:05:02'),
(256436,92827,1,88.000,2288.000,2376.000,'今日收益','2026-01-29 23:59:59','2026-01-30 00:05:01'),
(256912,92738,1,88.000,0.000,88.000,'今日收益','2026-03-17 23:59:59','2026-03-18 00:05:00'),
(256917,92738,1,88.000,88.000,176.000,'今日收益','2026-03-18 23:59:59','2026-03-19 00:05:00'),
(256925,92738,1,288.000,176.000,464.000,'今日收益','2026-03-19 23:59:59','2026-03-20 00:05:00'),
(256935,92738,1,188.000,464.000,652.000,'今日收益','2026-03-20 23:59:59','2026-03-21 00:05:00'),
(256946,92738,1,188.000,652.000,840.000,'今日收益','2026-03-23 23:59:59','2026-03-24 00:05:00'),
(256956,92738,1,88.000,840.000,928.000,'今日收益','2026-03-24 23:59:59','2026-03-25 00:05:01'),
(256971,92738,1,88.000,928.000,1016.000,'今日收益','2026-03-25 23:59:59','2026-03-26 00:05:00'),
(257089,92738,1,88.000,1016.000,1104.000,'今日收益','2026-04-07 23:59:59','2026-04-08 00:05:00'),
(257103,92738,1,88.000,1104.000,1192.000,'今日收益','2026-04-08 23:59:59','2026-04-09 00:05:00'),
(257120,92738,1,88.000,1192.000,1280.000,'今日收益','2026-04-09 23:59:59','2026-04-10 00:05:00'),
(257133,92738,1,88.000,1280.000,1368.000,'今日收益','2026-04-10 23:59:59','2026-04-11 00:05:00'),
(257144,92738,1,88.000,1368.000,1456.000,'今日收益','2026-04-13 23:59:59','2026-04-14 00:05:00'),
(257154,92738,1,88.000,1456.000,1544.000,'今日收益','2026-04-14 23:59:59','2026-04-15 00:05:00');
-- ---------- `eb_user_integral_record` (174 rows) ----------
INSERT INTO `eb_user_integral_record` VALUES
(2574,92738,'33','selfbonus',1,'个人奖金奖励',138.150,138,'个人奖金变动奖励奖金金额276.300积分138',3,0,0,'2026-02-19 02:23:00','2026-02-19 02:23:00',33),
(2576,92827,'36','selfbonus',1,'个人奖金奖励',160.438,160,'个人奖金变动奖励奖金金额320.877积分160',3,0,0,'2026-02-19 02:28:00','2026-02-19 02:28:00',36),
(2608,92738,'order53330177150998248328316','order',2,'用户订单付款成功',138.000,0,'订单支付抵扣138积分购买商品',3,0,0,'2026-02-19 14:07:00','2026-02-19 14:07:00',NULL),
(2634,92738,'88','selfbonus',1,'个人奖金奖励',118.778,118,'个人奖金变动奖励奖金金额237.557积分118',3,0,0,'2026-02-20 02:22:00','2026-02-20 02:22:00',88),
(2643,92827,'97','selfbonus',1,'个人奖金奖励',150.624,311,'个人奖金变动奖励奖金金额301.248积分150',3,0,0,'2026-02-20 03:07:00','2026-02-20 03:07:00',97),
(2686,92738,'133','selfbonus',1,'个人奖金奖励',122.342,241,'个人奖金变动奖励奖金金额244.684积分122',3,0,0,'2026-02-23 02:01:00','2026-02-23 02:01:00',133),
(2689,92827,'142','selfbonus',1,'个人奖金奖励',170.209,481,'个人奖金变动奖励奖金金额340.418积分170',3,0,0,'2026-02-23 02:02:00','2026-02-23 02:02:00',142),
(2761,92738,'207','selfbonus',1,'个人奖金奖励',125.771,367,'个人奖金变动奖励奖金金额251.543积分125',3,0,0,'2026-02-24 02:05:00','2026-02-24 02:05:00',207),
(2799,92827,'246','selfbonus',1,'个人奖金奖励',158.382,639,'个人奖金变动奖励奖金金额316.764积分158',3,0,0,'2026-02-24 07:00:00','2026-02-24 07:00:00',246),
(2839,92738,'283','selfbonus',1,'个人奖金奖励',126.733,493,'个人奖金变动奖励奖金金额253.466积分126',3,0,0,'2026-02-25 02:15:00','2026-02-25 02:15:00',283),
(2847,92827,'291','selfbonus',1,'个人奖金奖励',141.977,781,'个人奖金变动奖励奖金金额283.955积分141',3,0,0,'2026-02-25 03:21:00','2026-02-25 03:21:00',291),
(2878,92738,'316','selfbonus',1,'个人奖金奖励',177.421,671,'个人奖金变动奖励奖金金额354.843积分177',3,0,0,'2026-02-26 02:02:00','2026-02-26 02:02:00',316),
(2921,92827,'361','selfbonus',1,'个人奖金奖励',133.949,915,'个人奖金变动奖励奖金金额267.899积分133',3,0,0,'2026-02-26 04:52:00','2026-02-26 04:52:00',361),
(2956,92738,'399','selfbonus',1,'个人奖金奖励',119.444,790,'个人奖金变动奖励奖金金额238.889积分119',3,0,0,'2026-02-27 02:05:00','2026-02-27 02:05:00',399),
(2961,92827,'403','selfbonus',1,'个人奖金奖励',148.407,1063,'个人奖金变动奖励奖金金额296.815积分148',3,0,0,'2026-02-27 02:06:00','2026-02-27 02:06:00',403),
(2990,92738,'order81152177216655907318829','order',2,'用户订单付款成功',16.000,774,'订单支付抵扣16积分购买商品',3,0,0,'2026-02-27 04:30:00','2026-02-27 04:30:00',NULL),
(2991,92738,'order64483177216661181274432','order',2,'用户订单付款成功',198.000,570,'订单支付抵扣198积分购买商品',3,0,0,'2026-02-27 04:31:00','2026-02-27 04:31:00',NULL),
(2992,92738,'order90315177216663203753390','order',2,'用户订单付款成功',6.000,570,'订单支付抵扣6积分购买商品',3,0,0,'2026-02-27 04:31:00','2026-02-27 04:31:00',NULL),
(2993,92738,'order43546177216691840360834','order',2,'用户订单付款成功',40.000,530,'订单支付抵扣40积分购买商品',3,0,0,'2026-02-27 04:36:00','2026-02-27 04:36:00',NULL),
(3029,92827,'446','selfbonus',1,'个人奖金奖励',143.836,1207,'个人奖金变动奖励奖金金额287.673积分143',3,0,0,'2026-03-02 02:02:01','2026-03-02 02:02:01',446),
(3084,92738,'493','selfbonus',1,'个人奖金奖励',126.524,657,'个人奖金变动奖励奖金金额253.048积分126',3,0,0,'2026-03-02 06:58:02','2026-03-02 06:58:02',493),
(3117,92827,'525','selfbonus',1,'个人奖金奖励',147.497,1355,'个人奖金变动奖励奖金金额294.994积分147',3,0,0,'2026-03-03 02:11:00','2026-03-03 02:11:00',525),
(3145,92738,'554','selfbonus',1,'个人奖金奖励',130.984,788,'个人奖金变动奖励奖金金额261.969积分130',3,0,0,'2026-03-03 05:54:02','2026-03-03 05:54:02',554),
(3196,92738,'602','selfbonus',1,'个人奖金奖励',150.177,938,'个人奖金变动奖励奖金金额300.355积分150',3,0,0,'2026-03-04 02:06:02','2026-03-04 02:06:02',602),
(3202,92827,'611','selfbonus',1,'个人奖金奖励',161.501,1516,'个人奖金变动奖励奖金金额323.003积分161',3,0,0,'2026-03-04 04:46:02','2026-03-04 04:46:02',611),
(3203,92827,'611','selfbonus',1,'个人奖金奖励',161.501,1678,'个人奖金变动奖励奖金金额323.003积分161',3,0,0,'2026-03-04 04:46:04','2026-03-04 04:46:04',611),
(3236,92738,'648','selfbonus',1,'个人奖金奖励',156.479,1094,'个人奖金变动奖励奖金金额312.959积分156',3,0,0,'2026-03-05 02:03:01','2026-03-05 02:03:01',648),
(3237,92827,'647','selfbonus',1,'个人奖金奖励',156.793,1835,'个人奖金变动奖励奖金金额313.586积分156',3,0,0,'2026-03-05 02:03:01','2026-03-05 02:03:01',647),
(3299,92827,'703','selfbonus',1,'个人奖金奖励',142.779,1977,'个人奖金变动奖励奖金金额285.559积分142',3,0,0,'2026-03-06 02:02:03','2026-03-06 02:02:03',703),
(3301,92738,'701','selfbonus',1,'个人奖金奖励',144.025,1238,'个人奖金变动奖励奖金金额288.050积分144',3,0,0,'2026-03-06 02:02:03','2026-03-06 02:02:03',701),
(3351,92827,'order56575177278039716981154','order',2,'用户订单付款成功',158.000,1819,'订单支付抵扣158积分购买商品',3,0,0,'2026-03-06 07:00:01','2026-03-06 07:00:01',NULL),
(3352,92827,'order48973177278042001138676','order',2,'用户订单付款成功',280.000,1184,'订单支付抵扣280积分购买商品',3,0,0,'2026-03-06 07:01:01','2026-03-06 07:01:01',NULL),
(3353,92827,'order70907177278045167978700','order',2,'用户订单付款成功',39.000,1184,'订单支付抵扣39积分购买商品',3,0,0,'2026-03-06 07:01:01','2026-03-06 07:01:01',NULL),
(3354,92827,'order36916177278045918468986','order',2,'用户订单付款成功',316.000,1184,'订单支付抵扣316积分购买商品',3,0,0,'2026-03-06 07:01:01','2026-03-06 07:01:01',NULL),
(3355,92827,'order15066177278047415274322','order',2,'用户订单付款成功',40.000,1066,'订单支付抵扣40积分购买商品',3,0,0,'2026-03-06 07:02:01','2026-03-06 07:02:01',NULL),
(3357,92827,'order22712177278048380363920','order',2,'用户订单付款成功',78.000,1066,'订单支付抵扣78积分购买商品',3,0,0,'2026-03-06 07:02:01','2026-03-06 07:02:01',NULL),
(3358,92827,'order75437177278054789222811','order',2,'用户订单付款成功',17.000,551,'订单支付抵扣17积分购买商品',3,0,0,'2026-03-06 07:03:00','2026-03-06 07:03:00',NULL),
(3359,92827,'order17881177278055766426513','order',2,'用户订单付款成功',498.000,551,'订单支付抵扣498积分购买商品',3,0,0,'2026-03-06 07:03:01','2026-03-06 07:03:01',NULL),
(3369,92827,'765','selfbonus',1,'个人奖金奖励',150.276,702,'个人奖金变动奖励奖金金额300.552积分150',3,0,0,'2026-03-09 02:02:01','2026-03-09 02:02:01',765),
(3379,92738,'767','selfbonus',1,'个人奖金奖励',159.628,1398,'个人奖金变动奖励奖金金额319.257积分159',3,0,0,'2026-03-09 02:03:01','2026-03-09 02:03:01',767),
(3471,92738,'854','selfbonus',1,'个人奖金奖励',158.183,1556,'个人奖金变动奖励奖金金额316.367积分158',3,0,0,'2026-03-10 02:19:01','2026-03-10 02:19:01',854),
(3506,92738,'order21693177312544524179804','order',2,'用户订单付款成功',138.000,1418,'订单支付抵扣138积分购买商品',3,0,0,'2026-03-10 06:51:00','2026-03-10 06:51:00',NULL),
(3513,92827,'891','selfbonus',1,'个人奖金奖励',154.000,856,'个人奖金变动奖励奖金金额308.000积分154',3,0,0,'2026-03-10 07:42:01','2026-03-10 07:42:01',891),
(3531,92738,'914','selfbonus',1,'个人奖金奖励',176.900,1595,'个人奖金变动奖励奖金金额353.801积分176',3,0,0,'2026-03-11 02:04:01','2026-03-11 02:04:01',914),
(3555,92827,'927','selfbonus',1,'个人奖金奖励',156.144,1012,'个人奖金变动奖励奖金金额312.289积分156',3,0,0,'2026-03-11 02:08:01','2026-03-11 02:08:01',927),
(3633,92738,'1005','selfbonus',1,'个人奖金奖励',179.663,1775,'个人奖金变动奖励奖金金额359.326积分179',3,0,0,'2026-03-12 04:16:00','2026-03-12 04:16:00',1005),
(3655,92738,'order65105177330346342376245','order',2,'用户订单付款成功',1689.000,86,'订单支付抵扣1689积分购买商品',3,0,0,'2026-03-12 08:18:00','2026-03-12 08:18:00',NULL),
(3661,92827,'1034','selfbonus',1,'个人奖金奖励',185.417,1197,'个人奖金变动奖励奖金金额370.834积分185',3,0,0,'2026-03-13 02:02:01','2026-03-13 02:02:01',1034),
(3687,92738,'1060','selfbonus',1,'个人奖金奖励',128.728,214,'个人奖金变动奖励奖金金额257.456积分128',3,0,0,'2026-03-13 02:07:01','2026-03-13 02:07:01',1060),
(3746,92738,'1100','selfbonus',1,'个人奖金奖励',184.561,399,'个人奖金变动奖励奖金金额369.122积分184',3,0,0,'2026-03-16 02:05:01','2026-03-16 02:05:01',1100),
(3758,92827,'1117','selfbonus',1,'个人奖金奖励',189.116,1386,'个人奖金变动奖励奖金金额378.232积分189',3,0,0,'2026-03-16 02:27:01','2026-03-16 02:27:01',1117),
(3788,92738,'1145','selfbonus',1,'个人奖金奖励',166.929,566,'个人奖金变动奖励奖金金额333.858积分166',3,0,0,'2026-03-17 02:02:01','2026-03-17 02:02:01',1145),
(3795,92738,'1146','selfbonus',1,'个人奖金奖励',168.568,734,'个人奖金变动奖励奖金金额337.137积分168',3,0,0,'2026-03-17 02:03:01','2026-03-17 02:03:01',1146),
(3827,92827,'1177','selfbonus',1,'个人奖金奖励',192.130,1578,'个人奖金变动奖励奖金金额384.261积分192',3,0,0,'2026-03-17 06:55:01','2026-03-17 06:55:01',1177),
(3860,92827,'1204','selfbonus',1,'个人奖金奖励',200.633,1779,'个人奖金变动奖励奖金金额401.267积分200',3,0,0,'2026-03-18 02:06:01','2026-03-18 02:06:01',1204),
(3867,92738,'1216','selfbonus',1,'个人奖金奖励',183.883,918,'个人奖金变动奖励奖金金额367.767积分183',3,0,0,'2026-03-18 02:14:00','2026-03-18 02:14:00',1216),
(3869,92738,'1217','selfbonus',1,'个人奖金奖励',181.014,1099,'个人奖金变动奖励奖金金额362.028积分181',3,0,0,'2026-03-18 02:16:01','2026-03-18 02:16:01',1217),
(3912,92738,'1257','selfbonus',1,'个人奖金奖励',150.177,1250,'个人奖金变动奖励奖金金额300.354积分150',3,0,0,'2026-03-19 02:04:01','2026-03-19 02:04:01',1257),
(3916,92827,'1261','selfbonus',1,'个人奖金奖励',186.444,1966,'个人奖金变动奖励奖金金额372.889积分186',3,0,0,'2026-03-19 02:05:01','2026-03-19 02:05:01',1261),
(3927,92738,'1274','selfbonus',1,'个人奖金奖励',121.051,1371,'个人奖金变动奖励奖金金额242.103积分121',3,0,0,'2026-03-19 02:08:01','2026-03-19 02:08:01',1274),
(3936,92738,'1282','selfbonus',1,'个人奖金奖励',128.213,1499,'个人奖金变动奖励奖金金额256.426积分128',3,0,0,'2026-03-19 02:43:01','2026-03-19 02:43:01',1282),
(3938,92738,'1284','selfbonus',1,'个人奖金奖励',103.326,1602,'个人奖金变动奖励奖金金额206.652积分103',3,0,0,'2026-03-19 02:45:01','2026-03-19 02:45:01',1284),
(3974,92738,'1308','selfbonus',1,'个人奖金奖励',152.958,1755,'个人奖金变动奖励奖金金额305.916积分152',3,0,0,'2026-03-20 02:02:01','2026-03-20 02:02:01',1308),
(3985,92738,'1317','selfbonus',1,'个人奖金奖励',108.302,1863,'个人奖金变动奖励奖金金额216.604积分108',3,0,0,'2026-03-20 02:04:01','2026-03-20 02:04:01',1317),
(3988,92738,'1326','selfbonus',1,'个人奖金奖励',142.354,2006,'个人奖金变动奖励奖金金额284.709积分142',3,0,0,'2026-03-20 02:05:01','2026-03-20 02:05:01',1326),
(4015,92827,'1349','selfbonus',1,'个人奖金奖励',155.142,2121,'个人奖金变动奖励奖金金额310.284积分155',3,0,0,'2026-03-20 02:55:01','2026-03-20 02:55:01',1349),
(4037,92738,'order74425177408743283472486','order',2,'用户订单付款成功',328.000,1678,'订单支付抵扣328积分购买商品',3,0,0,'2026-03-21 10:04:00','2026-03-21 10:04:00',NULL),
(4038,92738,'order43850177408751498176562','order',2,'用户订单付款成功',138.000,1540,'订单支付抵扣138积分购买商品',3,0,0,'2026-03-21 10:06:00','2026-03-21 10:06:00',NULL),
(4039,92738,'order42948177408757558794042','order',2,'用户订单付款成功',138.000,1402,'订单支付抵扣138积分购买商品',3,0,0,'2026-03-21 10:07:00','2026-03-21 10:07:00',NULL),
(4040,92738,'order27637177408766345096957','order',2,'用户订单付款成功',1104.000,298,'订单支付抵扣1104积分购买商品',3,0,0,'2026-03-21 10:08:00','2026-03-21 10:08:00',NULL),
(4041,92738,'order71066177408773069784463','order',2,'用户订单付款成功',198.000,100,'订单支付抵扣198积分购买商品',3,0,0,'2026-03-21 10:09:00','2026-03-21 10:09:00',NULL),
(4049,92738,'1370','selfbonus',1,'个人奖金奖励',102.289,202,'个人奖金变动奖励奖金金额204.579积分102',3,0,0,'2026-03-23 02:03:01','2026-03-23 02:03:01',1370),
(4056,92827,'1364','selfbonus',1,'个人奖金奖励',148.943,2270,'个人奖金变动奖励奖金金额297.887积分148',3,0,0,'2026-03-23 02:03:01','2026-03-23 02:03:01',1364),
(4069,92738,'1392','selfbonus',1,'个人奖金奖励',114.170,316,'个人奖金变动奖励奖金金额228.340积分114',3,0,0,'2026-03-23 02:06:01','2026-03-23 02:06:01',1392),
(4079,92738,'1396','selfbonus',1,'个人奖金奖励',187.880,504,'个人奖金变动奖励奖金金额375.760积分187',3,0,0,'2026-03-23 02:08:01','2026-03-23 02:08:01',1396),
(4115,92738,'1437','selfbonus',1,'个人奖金奖励',121.781,626,'个人奖金变动奖励奖金金额243.563积分121',3,0,0,'2026-03-24 02:03:01','2026-03-24 02:03:01',1437),
(4116,92827,'1436','selfbonus',1,'个人奖金奖励',144.605,2414,'个人奖金变动奖励奖金金额289.210积分144',3,0,0,'2026-03-24 02:03:01','2026-03-24 02:03:01',1436),
(4124,92738,'1441','selfbonus',1,'个人奖金奖励',132.276,758,'个人奖金变动奖励奖金金额264.552积分132',3,0,0,'2026-03-24 02:04:01','2026-03-24 02:04:01',1441),
(4128,93140,'1446','selfbonus',1,'个人奖金奖励',101.866,101,'个人奖金变动奖励奖金金额203.733积分101',3,0,0,'2026-03-24 02:05:00','2026-03-24 02:05:00',1446),
(4192,92738,'1506','selfbonus',1,'个人奖金奖励',121.338,880,'个人奖金变动奖励奖金金额242.677积分121',3,0,0,'2026-03-25 02:05:01','2026-03-25 02:05:01',1506),
(4194,93140,'1514','selfbonus',1,'个人奖金奖励',107.128,208,'个人奖金变动奖励奖金金额214.257积分107',3,0,0,'2026-03-25 02:06:01','2026-03-25 02:06:01',1514),
(4215,92827,'1525','selfbonus',1,'个人奖金奖励',127.416,2542,'个人奖金变动奖励奖金金额254.832积分127',3,0,0,'2026-03-25 02:23:01','2026-03-25 02:23:01',1525),
(4232,92738,'1536','selfbonus',1,'个人奖金奖励',185.661,1065,'个人奖金变动奖励奖金金额371.323积分185',3,0,0,'2026-03-25 03:34:01','2026-03-25 03:34:01',1536),
(4276,93150,'1581','selfbonus',1,'个人奖金奖励',103.659,103,'个人奖金变动奖励奖金金额207.318积分103',3,0,0,'2026-03-26 02:05:01','2026-03-26 02:05:01',1581),
(4280,92738,'1584','selfbonus',1,'个人奖金奖励',142.600,1208,'个人奖金变动奖励奖金金额285.201积分142',3,0,0,'2026-03-26 02:06:01','2026-03-26 02:06:01',1584),
(4283,92827,'1586','selfbonus',1,'个人奖金奖励',118.581,2660,'个人奖金变动奖励奖金金额237.163积分118',3,0,0,'2026-03-26 02:07:01','2026-03-26 02:07:01',1586),
(4287,93140,'1595','selfbonus',1,'个人奖金奖励',113.879,322,'个人奖金变动奖励奖金金额227.758积分113',3,0,0,'2026-03-26 02:09:01','2026-03-26 02:09:01',1595),
(4315,92738,'1617','selfbonus',1,'个人奖金奖励',146.878,1355,'个人奖金变动奖励奖金金额293.757积分146',3,0,0,'2026-03-27 02:00:01','2026-03-27 02:00:01',1617),
(4323,93150,'1631','selfbonus',1,'个人奖金奖励',105.730,209,'个人奖金变动奖励奖金金额211.460积分105',3,0,0,'2026-03-27 02:02:01','2026-03-27 02:02:01',1631),
(4334,92827,'1634','selfbonus',1,'个人奖金奖励',128.499,2789,'个人奖金变动奖励奖金金额256.999积分128',3,0,0,'2026-03-27 02:03:01','2026-03-27 02:03:01',1634),
(4371,93140,'1673','selfbonus',1,'个人奖金奖励',115.127,438,'个人奖金变动奖励奖金金额230.255积分115',3,0,0,'2026-03-27 05:29:01','2026-03-27 05:29:01',1673),
(4379,92827,'order84514177461129994374488','order',2,'用户订单付款成功',138.000,2651,'订单支付抵扣138积分购买商品',3,0,0,'2026-03-27 11:35:00','2026-03-27 11:35:00',NULL),
(4380,92827,'order41479177461133957328078','order',2,'用户订单付款成功',138.000,2513,'订单支付抵扣138积分购买商品',3,0,0,'2026-03-27 11:36:00','2026-03-27 11:36:00',NULL),
(4381,92827,'order98718177461140058776240','order',2,'用户订单付款成功',2484.000,29,'订单支付抵扣2484积分购买商品',3,0,0,'2026-03-27 11:37:00','2026-03-27 11:37:00',NULL),
(4442,92738,'1736','selfbonus',1,'个人奖金奖励',139.231,1494,'个人奖金变动奖励奖金金额278.462积分139',3,0,0,'2026-03-30 04:09:01','2026-03-30 04:09:01',1736),
(4449,93150,'1742','selfbonus',1,'个人奖金奖励',102.289,311,'个人奖金变动奖励奖金金额204.579积分102',3,0,0,'2026-03-30 06:16:01','2026-03-30 06:16:01',1742),
(4456,92827,'1750','selfbonus',1,'个人奖金奖励',172.155,201,'个人奖金变动奖励奖金金额344.311积分172',3,0,0,'2026-03-30 07:13:01','2026-03-30 07:13:01',1750),
(4458,93140,'1752','selfbonus',1,'个人奖金奖励',118.581,556,'个人奖金变动奖励奖金金额237.163积分118',3,0,0,'2026-03-30 07:44:01','2026-03-30 07:44:01',1752),
(4480,93140,'1776','selfbonus',1,'个人奖金奖励',124.439,681,'个人奖金变动奖励奖金金额248.878积分124',3,0,0,'2026-03-31 02:06:01','2026-03-31 02:06:01',1776),
(4484,93150,'1780','selfbonus',1,'个人奖金奖励',113.271,424,'个人奖金变动奖励奖金金额226.542积分113',3,0,0,'2026-03-31 02:07:01','2026-03-31 02:07:01',1780),
(4487,92738,'1783','selfbonus',1,'个人奖金奖励',114.509,1608,'个人奖金变动奖励奖金金额229.019积分114',3,0,0,'2026-03-31 02:08:01','2026-03-31 02:08:01',1783),
(4520,92827,'1813','selfbonus',1,'个人奖金奖励',191.357,392,'个人奖金变动奖励奖金金额382.715积分191',3,0,0,'2026-03-31 07:37:01','2026-03-31 07:37:01',1813),
(4536,93150,'1829','selfbonus',1,'个人奖金奖励',117.866,542,'个人奖金变动奖励奖金金额235.733积分117',3,0,0,'2026-04-01 02:02:01','2026-04-01 02:02:01',1829),
(4552,92827,'1840','selfbonus',1,'个人奖金奖励',188.447,581,'个人奖金变动奖励奖金金额376.894积分188',3,0,0,'2026-04-01 02:04:01','2026-04-01 02:04:01',1840),
(4564,92738,'1856','selfbonus',1,'个人奖金奖励',141.596,1750,'个人奖金变动奖励奖金金额283.193积分141',3,0,0,'2026-04-01 02:07:01','2026-04-01 02:07:01',1856),
(4594,93140,'1886','selfbonus',1,'个人奖金奖励',116.669,797,'个人奖金变动奖励奖金金额233.338积分116',3,0,0,'2026-04-01 11:37:01','2026-04-01 11:37:01',1886),
(4616,93150,'1912','selfbonus',1,'个人奖金奖励',103.045,645,'个人奖金变动奖励奖金金额206.091积分103',3,0,0,'2026-04-02 02:04:01','2026-04-02 02:04:01',1912),
(4636,93140,'1927','selfbonus',1,'个人奖金奖励',129.577,927,'个人奖金变动奖励奖金金额259.154积分129',3,0,0,'2026-04-02 02:07:01','2026-04-02 02:07:01',1927),
(4659,92738,'1947','selfbonus',1,'个人奖金奖励',149.775,1900,'个人奖金变动奖励奖金金额299.551积分149',3,0,0,'2026-04-02 08:04:01','2026-04-02 08:04:01',1947),
(4660,92827,'1946','selfbonus',1,'个人奖金奖励',188.676,769,'个人奖金变动奖励奖金金额377.353积分188',3,0,0,'2026-04-02 08:04:01','2026-04-02 08:04:01',1946),
(4694,93140,'1988','selfbonus',1,'个人奖金奖励',125.127,1052,'个人奖金变动奖励奖金金额250.255积分125',3,0,0,'2026-04-03 02:12:01','2026-04-03 02:12:01',1988),
(4714,92738,'1968','selfbonus',1,'个人奖金奖励',150.220,2050,'个人奖金变动奖励奖金金额300.440积分150',3,0,0,'2026-04-03 02:12:01','2026-04-03 02:12:01',1968),
(4722,93150,'1960','selfbonus',1,'个人奖金奖励',105.222,751,'个人奖金变动奖励奖金金额210.445积分105',3,0,0,'2026-04-03 02:12:01','2026-04-03 02:12:01',1960),
(4729,92738,'order35464177518377840514243','order',2,'用户订单付款成功',2016.000,34,'订单支付抵扣2016积分购买商品',3,0,0,'2026-04-03 02:40:00','2026-04-03 02:40:00',NULL),
(4739,92827,'2019','selfbonus',1,'个人奖金奖励',167.563,937,'个人奖金变动奖励奖金金额335.127积分167',3,0,0,'2026-04-03 07:10:01','2026-04-03 07:10:01',2019),
(4744,92827,'2019','selfbonus',1,'个人奖金奖励',167.563,1105,'个人奖金变动奖励奖金金额335.127积分167',3,0,0,'2026-04-03 07:10:03','2026-04-03 07:10:03',2019),
(4798,92827,'2051','selfbonus',1,'个人奖金奖励',161.406,1266,'个人奖金变动奖励奖金金额322.813积分161',3,0,0,'2026-04-06 02:05:01','2026-04-06 02:05:01',2051),
(4800,93140,'2049','selfbonus',1,'个人奖金奖励',107.687,1160,'个人奖金变动奖励奖金金额215.374积分107',3,0,0,'2026-04-06 02:05:01','2026-04-06 02:05:01',2049),
(4824,92738,'2077','selfbonus',1,'个人奖金奖励',158.897,193,'个人奖金变动奖励奖金金额317.794积分158',3,0,0,'2026-04-06 02:50:01','2026-04-06 02:50:01',2077),
(4835,93150,'2089','selfbonus',1,'个人奖金奖励',104.838,855,'个人奖金变动奖励奖金金额209.676积分104',3,0,0,'2026-04-06 03:50:01','2026-04-06 03:50:01',2089),
(4894,92738,'2135','selfbonus',1,'个人奖金奖励',110.597,303,'个人奖金变动奖励奖金金额221.195积分110',3,0,0,'2026-04-07 02:04:01','2026-04-07 02:04:01',2135),
(4899,92738,'2140','selfbonus',1,'个人奖金奖励',139.070,443,'个人奖金变动奖励奖金金额278.140积分139',3,0,0,'2026-04-07 02:08:01','2026-04-07 02:08:01',2140),
(4929,92827,'2172','selfbonus',1,'个人奖金奖励',155.381,1421,'个人奖金变动奖励奖金金额310.762积分155',3,0,0,'2026-04-07 02:22:01','2026-04-07 02:22:01',2172),
(4942,93140,'2185','selfbonus',1,'个人奖金奖励',105.512,1265,'个人奖金变动奖励奖金金额211.024积分105',3,0,0,'2026-04-07 06:03:00','2026-04-07 06:03:00',2185),
(4952,93150,'2195','selfbonus',1,'个人奖金奖励',102.781,958,'个人奖金变动奖励奖金金额205.562积分102',3,0,0,'2026-04-07 06:15:01','2026-04-07 06:15:01',2195),
(4986,93140,'order26667177560992185495558','order',2,'用户订单付款成功',1188.000,77,'订单支付抵扣1188积分购买商品',3,0,0,'2026-04-08 00:59:00','2026-04-08 00:59:00',NULL),
(5028,93140,'2243','selfbonus',1,'个人奖金奖励',164.149,241,'个人奖金变动奖励奖金金额328.298积分164',3,0,0,'2026-04-08 02:12:01','2026-04-08 02:12:01',2243),
(5029,92827,'2246','selfbonus',1,'个人奖金奖励',114.979,1536,'个人奖金变动奖励奖金金额229.959积分114',3,0,0,'2026-04-08 02:16:01','2026-04-08 02:16:01',2246),
(5052,92738,'2262','selfbonus',1,'个人奖金奖励',155.465,598,'个人奖金变动奖励奖金金额310.931积分155',3,0,0,'2026-04-08 03:55:00','2026-04-08 03:55:00',2262),
(5058,92738,'2268','selfbonus',1,'个人奖金奖励',112.907,711,'个人奖金变动奖励奖金金额225.814积分112',3,0,0,'2026-04-08 05:27:01','2026-04-08 05:27:01',2268),
(5074,93150,'2285','selfbonus',1,'个人奖金奖励',106.178,1064,'个人奖金变动奖励奖金金额212.357积分106',3,0,0,'2026-04-08 07:20:00','2026-04-08 07:20:00',2285),
(5112,93150,'2314','selfbonus',1,'个人奖金奖励',103.086,1167,'个人奖金变动奖励奖金金额206.172积分103',3,0,0,'2026-04-09 02:05:01','2026-04-09 02:05:01',2314),
(5115,92827,'2318','selfbonus',1,'个人奖金奖励',167.662,1704,'个人奖金变动奖励奖金金额335.324积分167',3,0,0,'2026-04-09 02:06:01','2026-04-09 02:06:01',2318),
(5148,92738,'2353','selfbonus',1,'个人奖金奖励',115.127,826,'个人奖金变动奖励奖金金额230.255积分115',3,0,0,'2026-04-09 02:52:01','2026-04-09 02:52:01',2353),
(5152,92738,'2358','selfbonus',1,'个人奖金奖励',139.308,965,'个人奖金变动奖励奖金金额278.617积分139',3,0,0,'2026-04-09 05:03:01','2026-04-09 05:03:01',2358),
(5163,93140,'2368','selfbonus',1,'个人奖金奖励',117.672,359,'个人奖金变动奖励奖金金额235.345积分117',3,0,0,'2026-04-09 06:55:00','2026-04-09 06:55:00',2368),
(5187,93150,'2389','selfbonus',1,'个人奖金奖励',112.311,1280,'个人奖金变动奖励奖金金额224.623积分112',3,0,0,'2026-04-10 02:03:01','2026-04-10 02:03:01',2389),
(5224,93140,'2426','selfbonus',1,'个人奖金奖励',123.041,482,'个人奖金变动奖励奖金金额246.083积分123',3,0,0,'2026-04-10 03:11:01','2026-04-10 03:11:01',2426),
(5225,92827,'2427','selfbonus',1,'个人奖金奖励',169.789,1874,'个人奖金变动奖励奖金金额339.578积分169',3,0,0,'2026-04-10 03:12:01','2026-04-10 03:12:01',2427),
(5230,92738,'2432','selfbonus',1,'个人奖金奖励',114.885,1080,'个人奖金变动奖励奖金金额229.771积分114',3,0,0,'2026-04-10 04:36:01','2026-04-10 04:36:01',2432),
(5233,92738,'2435','selfbonus',1,'个人奖金奖励',143.488,1224,'个人奖金变动奖励奖金金额286.976积分143',3,0,0,'2026-04-10 05:30:01','2026-04-10 05:30:01',2435),
(5283,93140,'2470','selfbonus',1,'个人奖金奖励',125.058,607,'个人奖金变动奖励奖金金额250.117积分125',3,0,0,'2026-04-13 02:02:01','2026-04-13 02:02:01',2470),
(5288,93150,'2466','selfbonus',1,'个人奖金奖励',118.755,1399,'个人奖金变动奖励奖金金额237.510积分118',3,0,0,'2026-04-13 02:02:01','2026-04-13 02:02:01',2466),
(5310,92827,'2488','selfbonus',1,'个人奖金奖励',180.111,2054,'个人奖金变动奖励奖金金额360.222积分180',3,0,0,'2026-04-13 02:06:01','2026-04-13 02:06:01',2488),
(5320,92738,'2499','selfbonus',1,'个人奖金奖励',136.325,1360,'个人奖金变动奖励奖金金额272.650积分136',3,0,0,'2026-04-13 02:13:01','2026-04-13 02:13:01',2499),
(5342,92738,'2521','selfbonus',1,'个人奖金奖励',122.139,1482,'个人奖金变动奖励奖金金额244.278积分122',3,0,0,'2026-04-13 08:39:01','2026-04-13 08:39:01',2521),
(5349,93150,'2528','selfbonus',1,'个人奖金奖励',119.505,1518,'个人奖金变动奖励奖金金额239.010积分119',3,0,0,'2026-04-14 02:00:01','2026-04-14 02:00:01',2528),
(5377,93140,'2554','selfbonus',1,'个人奖金奖励',122.139,729,'个人奖金变动奖励奖金金额244.278积分122',3,0,0,'2026-04-14 02:05:01','2026-04-14 02:05:01',2554),
(5393,92827,'2572','selfbonus',1,'个人奖金奖励',183.209,2237,'个人奖金变动奖励奖金金额366.418积分183',3,0,0,'2026-04-14 03:32:01','2026-04-14 03:32:01',2572),
(5410,92738,'2587','selfbonus',1,'个人奖金奖励',125.551,1608,'个人奖金变动奖励奖金金额251.103积分125',3,0,0,'2026-04-14 07:15:01','2026-04-14 07:15:01',2587),
(5412,92738,'2589','selfbonus',1,'个人奖金奖励',125.182,1733,'个人奖金变动奖励奖金金额250.364积分125',3,0,0,'2026-04-14 07:30:01','2026-04-14 07:30:01',2589),
(5432,93150,'2622','selfbonus',1,'个人奖金奖励',104.706,1623,'个人奖金变动奖励奖金金额209.413积分104',3,0,0,'2026-04-15 02:02:01','2026-04-15 02:02:01',2622),
(5443,92738,'2612','selfbonus',1,'个人奖金奖励',130.890,1864,'个人奖金变动奖励奖金金额261.781积分130',3,0,0,'2026-04-15 02:02:01','2026-04-15 02:02:01',2612),
(5453,92827,'2630','selfbonus',1,'个人奖金奖励',178.709,2416,'个人奖金变动奖励奖金金额357.418积分178',3,0,0,'2026-04-15 02:03:01','2026-04-15 02:03:01',2630),
(5469,92738,'order68661177621871701084953','order',2,'用户订单付款成功',792.000,1072,'订单支付抵扣792积分购买商品',3,0,0,'2026-04-15 02:06:00','2026-04-15 02:06:00',NULL),
(5472,93140,'2646','selfbonus',1,'个人奖金奖励',133.292,862,'个人奖金变动奖励奖金金额266.585积分133',3,0,0,'2026-04-15 02:06:01','2026-04-15 02:06:01',2646),
(5475,92738,'order93193177621884587987279','order',2,'用户订单付款成功',690.000,382,'订单支付抵扣690积分购买商品',3,0,0,'2026-04-15 02:08:00','2026-04-15 02:08:00',NULL),
(5477,93140,'order49083177621885635728466','order',2,'用户订单付款成功',724.000,138,'订单支付抵扣724积分购买商品',3,0,0,'2026-04-15 02:08:00','2026-04-15 02:08:00',NULL),
(5479,92827,'order65878177621896033723311','order',2,'用户订单付款成功',2352.000,64,'订单支付抵扣2352积分购买商品',3,0,0,'2026-04-15 02:10:00','2026-04-15 02:10:00',NULL),
(5532,93140,'2687','selfbonus',1,'个人奖金奖励',126.782,265,'个人奖金变动奖励奖金金额253.565积分126',3,0,0,'2026-04-16 02:01:01','2026-04-16 02:01:01',2687),
(5541,92738,'2695','selfbonus',1,'个人奖金奖励',139.174,521,'个人奖金变动奖励奖金金额278.349积分139',3,0,0,'2026-04-16 02:02:01','2026-04-16 02:02:01',2695),
(5545,92827,'2707','selfbonus',1,'个人奖金奖励',171.038,235,'个人奖金变动奖励奖金金额342.077积分171',3,0,0,'2026-04-16 02:03:01','2026-04-16 02:03:01',2707),
(5621,93150,'2750','selfbonus',1,'个人奖金奖励',106.132,1729,'个人奖金变动奖励奖金金额212.264积分106',3,0,0,'2026-04-16 06:28:00','2026-04-16 06:28:00',2750),
(5673,92738,'2770','selfbonus',1,'个人奖金奖励',130.586,652,'个人奖金变动奖励奖金金额261.172积分130',3,0,0,'2026-04-20 02:01:01','2026-04-20 02:01:01',2770),
(5679,93140,'2776','selfbonus',1,'个人奖金奖励',103.662,369,'个人奖金变动奖励奖金金额207.324积分103',3,0,0,'2026-04-20 02:04:01','2026-04-20 02:04:01',2776),
(5695,93150,'2793','selfbonus',1,'个人奖金奖励',140.101,1869,'个人奖金变动奖励奖金金额280.203积分140',3,0,0,'2026-04-20 06:10:01','2026-04-20 06:10:01',2793),
(5696,92827,'2794','selfbonus',1,'个人奖金奖励',166.341,401,'个人奖金变动奖励奖金金额332.683积分166',3,0,0,'2026-04-20 06:47:01','2026-04-20 06:47:01',2794),
(5729,92738,'2824','selfbonus',1,'个人奖金奖励',137.179,789,'个人奖金变动奖励奖金金额274.358积分137',3,0,0,'2026-04-21 02:03:01','2026-04-21 02:03:01',2824),
(5731,93140,'2829','selfbonus',1,'个人奖金奖励',112.595,481,'个人奖金变动奖励奖金金额225.191积分112',3,0,0,'2026-04-21 02:04:01','2026-04-21 02:04:01',2829),
(5736,92827,'2834','selfbonus',1,'个人奖金奖励',171.332,572,'个人奖金变动奖励奖金金额342.664积分171',3,0,0,'2026-04-21 02:11:01','2026-04-21 02:11:01',2834),
(5761,93150,'2859','selfbonus',1,'个人奖金奖励',107.531,1977,'个人奖金变动奖励奖金金额215.062积分107',3,0,0,'2026-04-21 06:15:01','2026-04-21 06:15:01',2859),
(5770,93140,'order77013177676957691467666','order',2,'用户订单付款成功',468.000,13,'订单支付抵扣468积分购买商品',3,0,0,'2026-04-21 11:07:00','2026-04-21 11:07:00',NULL),
(5780,92738,'2881','selfbonus',1,'个人奖金奖励',138.538,927,'个人奖金变动奖励奖金金额277.077积分138',3,0,0,'2026-04-22 02:02:01','2026-04-22 02:02:01',2881),
(5800,93150,'2898','selfbonus',1,'个人奖金奖励',107.088,2084,'个人奖金变动奖励奖金金额214.177积分107',3,0,0,'2026-04-22 02:58:01','2026-04-22 02:58:01',2898);

View File

@@ -0,0 +1,46 @@
-- 太原树英商贸 / yangtangyoupin 数据清理
-- 依据: docs/com-sxsy80-data-imgration.md
-- 执行前请备份数据库。建议在事务中先 SELECT 核对影响行数后再 COMMIT。
SET NAMES utf8mb4;
START TRANSACTION;
SET FOREIGN_KEY_CHECKS = 0;
-- 订单类:整表清空
TRUNCATE TABLE `wa_order`;
TRUNCATE TABLE `wa_withdraw`;
TRUNCATE TABLE `eb_store_order`;
-- 寄售商品:与迁移文档一致——从源 dump 解析 id 列表(见 wa_merchandise_keep_ids_from_dump_sxsy80.txt删库中不在集合内的行
DELETE FROM `wa_merchandise`
WHERE `id` NOT IN (
163212,163213,163214,163223,163224,163225,163226,163231,163234,163235,163237,163239,163244,163245,163246,163254,163255,163259
);
-- 日志类:仅保留名单内 user_id
DELETE FROM `wa_selfbonus_log`
WHERE `user_id` NOT IN (92566,92801,92839,93004,92637,92965,93093,93096,93116,92787,93121,93129,92884,93007,93020,93094,93099,93110,92638);
DELETE FROM `wa_sharebonus_log`
WHERE `user_id` NOT IN (92566,92801,92839,93004,92637,92965,93093,93096,93116,92787,93121,93129,92884,93007,93020,93094,93099,93110,92638);
DELETE FROM `wa_coupon_log`
WHERE `user_id` NOT IN (92566,92801,92839,93004,92637,92965,93093,93096,93116,92787,93121,93129,92884,93007,93020,93094,93099,93110,92638);
-- 积分记录表实际字段为 uid非 user_id
DELETE FROM `eb_user_integral_record`
WHERE `uid` NOT IN (92566,92801,92839,93004,92637,92965,93093,93096,93116,92787,93121,93129,92884,93007,93020,93094,93099,93110,92638);
-- 商城用户:保留 uid 在名单内
DELETE FROM `eb_user`
WHERE `uid` NOT IN (92566,92801,92839,93004,92637,92965,93093,93096,93116,92787,93121,93129,92884,93007,93020,93094,93099,93110,92638);
-- WA 用户:保留 id 在名单内
DELETE FROM `wa_users`
WHERE `id` NOT IN (92566,92801,92839,93004,92637,92965,93093,93096,93116,92787,93121,93129,92884,93007,93020,93094,93099,93110,92638);
SET FOREIGN_KEY_CHECKS = 1;
COMMIT;

View File

@@ -0,0 +1,12 @@
-- 仅处理 wa_merchandise与 com-sxsy80-data-imgration.md 一致
-- 从源 dump 解析应保留的 id 列表(见 wa_merchandise_keep_ids_from_dump_sxsy80.txt删除库中 id 不在此集合的所有行。
SET NAMES utf8mb4;
START TRANSACTION;
DELETE FROM `wa_merchandise`
WHERE `id` NOT IN (
163212,163213,163214,163223,163224,163225,163226,163231,163234,163235,163237,163239,163244,163245,163246,163254,163255,163259
);
COMMIT;

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""从 dump 解析 wa_merchandise 应保留的 id与 com-sxsy80-data-imgration.md 一致)。"""
from __future__ import annotations
import re
import sys
from pathlib import Path
KEEP_USERS = {
92566, 92801, 92839, 93004, 92637, 92965, 93093, 93096, 93116, 92787, 93121, 93129,
92884, 93007, 93020, 93094, 93099, 93110, 92638,
}
CUTOFF = "2026-04-22 00:00:00"
# id, old_id, user_id, title, image, price, is_show, status, created_at, updated_at
ROW_RE = re.compile(
r"\((\d+),(\d+),(\d+),'(?:[^'\\]|\\.)*','(?:[^'\\]|\\.)*',[\d.]+,\d+,\d+,'(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})','(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})'\)"
)
def main() -> int:
dump = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).resolve().parents[2].parent / "db" / "ccd-yangtangyoupin_2026-04-26_10-25-01_mysql_data.sql"
if not dump.is_file():
print("dump not found:", dump, file=sys.stderr)
return 1
line = None
with dump.open("r", encoding="utf-8", errors="replace") as f:
for ln in f:
if "INSERT INTO `wa_merchandise`" in ln and "VALUES" in ln:
line = ln
break
if not line:
print("no wa_merchandise INSERT", file=sys.stderr)
return 1
keep: set[int] = set()
for m in ROW_RE.finditer(line):
mid, _old, user_id, created_at, _upd = m.groups()
if created_at >= CUTOFF and int(user_id) in KEEP_USERS:
keep.add(int(mid))
out = Path(__file__).resolve().parent / "wa_merchandise_keep_ids_from_dump_sxsy80.txt"
body = (
"# 从 dump 解析条件created_at >= 2026-04-22 且 user_id 在名单\n"
"# 重新生成python3 docs/sql/extract_wa_merchandise_keep_ids_from_dump.py /path/to/dump.sql\n\n"
+ ",".join(str(i) for i in sorted(keep))
+ "\n"
)
out.write_text(body, encoding="utf-8")
print("wrote", out, "count=", len(keep))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,218 @@
#!/usr/bin/env python3
"""
依据 docs/com-czrt6-data-imgration-2.md从 dump 生成批量 INSERT SQL。
数据源默认: docs/czcf82-yangtangyoupin_2026-04-26_10-25-02_mysql_data.sql
输出: docs/sql/com-czrt6-data-imgration-2_inserts.sql
"""
from __future__ import annotations
import sys
from pathlib import Path
# 与 com-czrt6-data-imgration-2.md 一致
KEEP_IDS = {92827, 92738, 93140, 93150}
MERCH_CUTOFF = "2026-04-16 00:00:00"
TABLES_ORDER = [
"wa_users",
"eb_user",
"wa_merchandise",
"wa_selfbonus_log",
"wa_sharebonus_log",
"wa_coupon_log",
"eb_user_integral_record",
]
def split_top_level_tuples(values_blob: str) -> list[str]:
out: list[str] = []
i = 0
n = len(values_blob)
while i < n:
if values_blob[i] != "(":
i += 1
continue
depth = 0
in_quote = False
start = i
j = i
while j < n:
c = values_blob[j]
if in_quote:
if c == "'":
if j + 1 < n and values_blob[j + 1] == "'":
j += 2
continue
in_quote = False
j += 1
continue
j += 1
continue
if c == "'":
in_quote = True
j += 1
continue
if c == "(":
depth += 1
elif c == ")":
depth -= 1
if depth == 0:
out.append(values_blob[start : j + 1])
j += 1
break
j += 1
i = j
return out
def split_mysql_fields(inner: str) -> list[str]:
out: list[str] = []
cur: list[str] = []
i = 0
n = len(inner)
while i < n:
c = inner[i]
if c == "'":
cur.append(c)
i += 1
while i < n:
c = inner[i]
cur.append(c)
if c == "'":
if i + 1 < n and inner[i + 1] == "'":
cur.append(inner[i + 1])
i += 2
continue
i += 1
break
i += 1
continue
if c == ",":
out.append("".join(cur).strip())
cur = []
i += 1
continue
cur.append(c)
i += 1
if cur:
out.append("".join(cur).strip())
return out
def unquote_sql_string(raw: str) -> str:
raw = raw.strip()
if raw.startswith("'") and raw.endswith("'"):
return raw[1:-1].replace("''", "'")
return raw
def find_insert_line(dump: Path, table: str) -> str | None:
needle = f"INSERT INTO `{table}` VALUES"
with dump.open("r", encoding="utf-8", errors="replace") as f:
for line in f:
if needle in line:
return line
return None
def values_blob_from_line(line: str, table: str) -> str:
marker = f"INSERT INTO `{table}` VALUES"
idx = line.index(marker) + len(marker)
blob = line[idx:].strip()
if blob.endswith(";"):
blob = blob[:-1].strip()
return blob
def filter_tuples(table: str, tuples: list[str]) -> list[str]:
kept: list[str] = []
for tup in tuples:
inner = tup.strip()[1:-1]
fields = split_mysql_fields(inner)
ok = False
try:
if table == "wa_users":
ok = len(fields) >= 1 and int(fields[0].strip()) in KEEP_IDS
elif table == "eb_user":
ok = len(fields) >= 1 and int(fields[0].strip()) in KEEP_IDS
elif table == "wa_merchandise":
if len(fields) >= 10:
uid = int(fields[2].strip())
status = int(fields[7].strip())
created = unquote_sql_string(fields[8])
ok = (
created >= MERCH_CUTOFF
and uid in KEEP_IDS
and status == 1
)
elif table in ("wa_selfbonus_log", "wa_sharebonus_log", "wa_coupon_log"):
ok = len(fields) >= 2 and int(fields[1].strip()) in KEEP_IDS
elif table == "eb_user_integral_record":
ok = len(fields) >= 2 and int(fields[1].strip()) in KEEP_IDS
except (ValueError, IndexError):
ok = False
if ok:
kept.append(tup.strip())
return kept
def main() -> int:
root = Path(__file__).resolve().parents[2]
dump = (
Path(sys.argv[1]).resolve()
if len(sys.argv) > 1
else root / "docs" / "czcf82-yangtangyoupin_2026-04-26_10-25-02_mysql_data.sql"
)
if not dump.is_file():
print("dump not found:", dump, file=sys.stderr)
return 1
out_path = root / "docs" / "sql" / "com-czrt6-data-imgration-2_inserts.sql"
chunks: list[str] = []
chunks.append(
f"""-- 池州瑞棠商贸 / com-czrt6-data-imgration-2.md
-- 源 dump: {dump.name}
-- 用户范围: {sorted(KEEP_IDS)}
-- wa_merchandise: created_at >= {MERCH_CUTOFF[:10]} 且 user_id 在范围且 status=1未售
-- 执行前请备份;注意外键与主键冲突,按需调整顺序或使用 SET FOREIGN_KEY_CHECKS=0
SET NAMES utf8mb4;
"""
)
stats: list[tuple[str, int]] = []
for table in TABLES_ORDER:
line = find_insert_line(dump, table)
if not line:
chunks.append(f"-- 未找到 INSERT: `{table}`\n\n")
stats.append((table, 0))
continue
blob = values_blob_from_line(line, table)
tuples = split_top_level_tuples(blob)
kept = filter_tuples(table, tuples)
stats.append((table, len(kept)))
chunks.append(f"-- ---------- `{table}` ({len(kept)} rows) ----------\n")
if not kept:
if table == "wa_merchandise":
chunks.append(
"-- 无匹配行dump 中满足「日期 + user_id 在范围」的寄售商品均为 status=0已售\n"
"-- 若业务上「未售」应对应其它字段,请改脚本 filter 后重新 python3 本脚本。\n\n"
)
else:
chunks.append("-- (无匹配行,跳过 INSERT)\n\n")
continue
chunks.append(f"INSERT INTO `{table}` VALUES\n")
chunks.append(",\n".join(kept))
chunks.append(";\n\n")
out_path.write_text("".join(chunks), encoding="utf-8")
print("wrote", out_path)
for t, n in stats:
print(f" {t}: {n}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,168 @@
#!/usr/bin/env python3
"""
从 dump 的 INSERT INTO wa_merchandise 中筛出:
created_at >= 2026-04-22 00:00:00 且 user_id 在数据范围
生成批量 INSERT SQL与 docs/com-sxsy80-data-imgration.md 一致)。
用法:
python3 docs/sql/generate_wa_merchandise_insert_from_dump.py [dump.sql路径]
默认 dump: ../../db/ccd-yangtangyoupin_2026-04-26_10-25-01_mysql_data.sql相对仓库根
"""
from __future__ import annotations
import sys
from pathlib import Path
KEEP_USERS = {
92566, 92801, 92839, 93004, 92637, 92965, 93093, 93096, 93116, 92787, 93121, 93129,
92884, 93007, 93020, 93094, 93099, 93110, 92638,
}
CUTOFF = "2026-04-22 00:00:00"
def split_top_level_tuples(values_blob: str) -> list[str]:
out: list[str] = []
i = 0
n = len(values_blob)
while i < n:
if values_blob[i] != "(":
i += 1
continue
depth = 0
in_quote = False
start = i
j = i
while j < n:
c = values_blob[j]
if in_quote:
if c == "'":
if j + 1 < n and values_blob[j + 1] == "'":
j += 2
continue
in_quote = False
j += 1
continue
j += 1
continue
if c == "'":
in_quote = True
j += 1
continue
if c == "(":
depth += 1
elif c == ")":
depth -= 1
if depth == 0:
out.append(values_blob[start : j + 1])
j += 1
break
j += 1
i = j
return out
def split_mysql_fields(inner: str) -> list[str]:
"""inner: 不含最外层括号的字段串"""
out: list[str] = []
cur: list[str] = []
i = 0
n = len(inner)
while i < n:
c = inner[i]
if c == "'":
cur.append(c)
i += 1
while i < n:
c = inner[i]
cur.append(c)
if c == "'":
if i + 1 < n and inner[i + 1] == "'":
cur.append(inner[i + 1])
i += 2
continue
i += 1
break
i += 1
continue
if c == ",":
out.append("".join(cur).strip())
cur = []
i += 1
continue
cur.append(c)
i += 1
if cur:
out.append("".join(cur).strip())
return out
def parse_created_at(fields: list[str]) -> str:
# id, old_id, user_id, title, image, price, is_show, status, created_at, updated_at
raw = fields[8].strip()
if raw.startswith("'") and raw.endswith("'"):
return raw[1:-1].replace("''", "'")
return raw
def parse_user_id(fields: list[str]) -> int:
return int(fields[2].strip())
def main() -> int:
root = Path(__file__).resolve().parents[2]
dump = (
Path(sys.argv[1]).resolve()
if len(sys.argv) > 1
else root.parent / "db" / "ccd-yangtangyoupin_2026-04-26_10-25-01_mysql_data.sql"
)
if not dump.is_file():
print("dump not found:", dump, file=sys.stderr)
return 1
insert_line = None
with dump.open("r", encoding="utf-8", errors="replace") as f:
for line in f:
if "INSERT INTO `wa_merchandise`" in line and "VALUES" in line:
insert_line = line
break
if not insert_line:
print("no INSERT wa_merchandise", file=sys.stderr)
return 1
marker = "VALUES"
idx = insert_line.index(marker) + len(marker)
blob = insert_line[idx:].strip()
if blob.endswith(";"):
blob = blob[:-1].strip()
kept: list[str] = []
for tup in split_top_level_tuples(blob):
inner = tup.strip()[1:-1]
fields = split_mysql_fields(inner)
if len(fields) < 10:
print("skip malformed tuple:", tup[:80], file=sys.stderr)
continue
created = parse_created_at(fields)
uid = parse_user_id(fields)
if created >= CUTOFF and uid in KEEP_USERS:
kept.append(tup.strip())
if not kept:
print("no rows matched", file=sys.stderr)
return 1
out_sql = root / "docs" / "sql" / "wa_merchandise_insert_from_dump_sxsy80.sql"
header = f"""-- 由 generate_wa_merchandise_insert_from_dump.py 生成
-- 源: {dump.name}
-- 条件: created_at >= {CUTOFF} 且 user_id卖家在数据范围{len(kept)} 行)
-- 执行前请确认目标库;若存在主键冲突可先处理或改用 INSERT IGNORE / REPLACE
"""
body = "INSERT INTO `wa_merchandise` VALUES\n" + ",\n".join(kept) + ";\n"
out_sql.write_text(header + body, encoding="utf-8")
print("wrote", out_sql, "rows=", len(kept))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,238 @@
#!/usr/bin/env python3
"""
在目标库执行 com-czrt6-data-imgration-2 迁移(见 docs/com-czrt6-data-imgration-2.md
1) SET FOREIGN_KEY_CHECKS=0
2) 从 inserts SQL 解析各 INSERT 的主键,按依赖逆序 DELETE避免与 dump 主键冲突,如他人曾占用同 id
3) 再按文件顺序执行 SET NAMES + INSERT
环境变量:
CZRT6_DB_HOST / CZRT6_DB_USER / CZRT6_DB_PASSWORD / CZRT6_DB_NAME
"""
from __future__ import annotations
import os
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
SQL_FILE = ROOT / "docs" / "sql" / "com-czrt6-data-imgration-2_inserts.sql"
def split_top_level_tuples(values_blob: str) -> list[str]:
out: list[str] = []
i = 0
n = len(values_blob)
while i < n:
if values_blob[i] != "(":
i += 1
continue
depth = 0
in_quote = False
start = i
j = i
while j < n:
c = values_blob[j]
if in_quote:
if c == "'":
if j + 1 < n and values_blob[j + 1] == "'":
j += 2
continue
in_quote = False
j += 1
continue
j += 1
continue
if c == "'":
in_quote = True
j += 1
continue
if c == "(":
depth += 1
elif c == ")":
depth -= 1
if depth == 0:
out.append(values_blob[start : j + 1])
j += 1
break
j += 1
i = j
return out
def split_mysql_fields(inner: str) -> list[str]:
out: list[str] = []
cur: list[str] = []
i = 0
n = len(inner)
while i < n:
c = inner[i]
if c == "'":
cur.append(c)
i += 1
while i < n:
c = inner[i]
cur.append(c)
if c == "'":
if i + 1 < n and inner[i + 1] == "'":
cur.append(inner[i + 1])
i += 2
continue
i += 1
break
i += 1
continue
if c == ",":
out.append("".join(cur).strip())
cur = []
i += 1
continue
cur.append(c)
i += 1
if cur:
out.append("".join(cur).strip())
return out
def load_executable_statements(path: Path) -> list[str]:
raw = path.read_text(encoding="utf-8")
lines = []
for line in raw.splitlines():
if line.strip().startswith("--"):
continue
lines.append(line)
text = "\n".join(lines)
stmts: list[str] = []
buf: list[str] = []
for line in text.splitlines():
if not line.strip():
continue
buf.append(line)
if line.rstrip().endswith(";"):
stmts.append("\n".join(buf).strip())
buf = []
if buf:
stmts.append("\n".join(buf).strip())
return [s for s in stmts if s and not s.startswith("--")]
def parse_insert_table_and_pks(statement: str) -> tuple[str | None, list[int]]:
m = re.match(r"INSERT INTO `(\w+)` VALUES\s*(.*)\s*;\s*$", statement, re.S | re.I)
if not m:
return None, []
table = m.group(1)
blob = m.group(2).strip()
if blob.endswith(";"):
blob = blob[:-1].strip()
pks: list[int] = []
for tup in split_top_level_tuples(blob):
inner = tup.strip()[1:-1]
fields = split_mysql_fields(inner)
if not fields:
continue
try:
pks.append(int(fields[0].strip()))
except ValueError:
continue
return table, pks
def chunk_in(ids: list[int], size: int = 500) -> list[list[int]]:
return [ids[i : i + size] for i in range(0, len(ids), size)]
def main() -> int:
try:
import pymysql
except ImportError:
print("需要: pip install pymysql", file=sys.stderr)
return 1
host = os.environ.get("CZRT6_DB_HOST", "101.37.101.6")
user = os.environ.get("CZRT6_DB_USER", "yangtangyoupin")
password = os.environ.get("CZRT6_DB_PASSWORD")
database = os.environ.get("CZRT6_DB_NAME", "yangtangyoupin")
if not password:
print("请设置 CZRT6_DB_PASSWORD", file=sys.stderr)
return 1
if not SQL_FILE.is_file():
print("missing", SQL_FILE, file=sys.stderr)
return 1
stmts = load_executable_statements(SQL_FILE)
if not stmts:
print("no statements in", SQL_FILE, file=sys.stderr)
return 1
by_table: dict[str, list[int]] = {}
for sql in stmts:
if not sql.upper().startswith("INSERT"):
continue
tbl, pks = parse_insert_table_and_pks(sql)
if tbl and pks:
by_table[tbl] = pks
# 逆序:先删子表/日志,再 eb_user、wa_users
delete_order = [
"eb_user_integral_record",
"wa_coupon_log",
"wa_sharebonus_log",
"wa_selfbonus_log",
"wa_merchandise",
"eb_user",
"wa_users",
]
pk_col = {"eb_user": "`uid`", "wa_users": "`id`"}
conn = pymysql.connect(
host=host,
user=user,
password=password,
database=database,
charset="utf8mb4",
autocommit=False,
)
try:
with conn.cursor() as cur:
cur.execute("SET NAMES utf8mb4")
cur.execute("SET FOREIGN_KEY_CHECKS=0")
for t in delete_order:
if t not in by_table:
continue
ids = sorted(set(by_table[t]))
col = pk_col.get(t, "`id`")
for chunk in chunk_in(ids):
cl = ",".join(str(x) for x in chunk)
sql = f"DELETE FROM `{t}` WHERE {col} IN ({cl})"
cur.execute(sql)
if cur.rowcount:
print(f"DELETE {t}", cur.rowcount)
for i, sql in enumerate(stmts, 1):
head = sql.split()[0].upper()
cur.execute(sql)
if head == "INSERT":
tbl = re.match(r"INSERT INTO `(\w+)`", sql, re.I)
tname = tbl.group(1) if tbl else "?"
print(f"[{i}] INSERT `{tname}` rowcount={cur.rowcount}")
else:
print(f"[{i}] {head} ok")
conn.commit()
print("COMMIT ok")
except Exception as e:
conn.rollback()
print("ROLLBACK:", e, file=sys.stderr)
raise
finally:
try:
with conn.cursor() as cur:
cur.execute("SET FOREIGN_KEY_CHECKS=1")
conn.commit()
except Exception:
pass
conn.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""执行 com-sxsy80-data-cleanup.sql。
需安装: pip install pymysql
连接信息见 docs/com-sxsy80-data-imgration.md通过环境变量传入避免把密码写进仓库
export YTYP_DB_HOST=106.14.132.80
export YTYP_DB_USER=yangtangyoupin
export YTYP_DB_PASSWORD='...'
export YTYP_DB_NAME=yangtangyoupin
python3 docs/sql/run_com_sxsy80_cleanup.py
"""
from __future__ import annotations
import os
import pathlib
import sys
import pymysql
ROOT = pathlib.Path(__file__).resolve().parents[2]
SQL_FILE = ROOT / "docs" / "sql" / "com-sxsy80-data-cleanup.sql"
def load_statements(path: pathlib.Path) -> list[str]:
text = path.read_text(encoding="utf-8")
stmts: list[str] = []
buf: list[str] = []
for line in text.splitlines():
s = line.strip()
if s.startswith("--") or not line.strip():
continue
buf.append(line)
if ";" in line:
chunk = "\n".join(buf).strip()
if chunk:
stmts.append(chunk)
buf = []
if buf:
chunk = "\n".join(buf).strip()
if chunk:
stmts.append(chunk)
return stmts
def main() -> int:
host = os.environ.get("YTYP_DB_HOST", "106.14.132.80")
user = os.environ.get("YTYP_DB_USER", "yangtangyoupin")
password = os.environ.get("YTYP_DB_PASSWORD")
database = os.environ.get("YTYP_DB_NAME", "yangtangyoupin")
if not password:
print("请设置环境变量 YTYP_DB_PASSWORD见脚本头部说明", file=sys.stderr)
return 1
if not SQL_FILE.is_file():
print("missing", SQL_FILE, file=sys.stderr)
return 1
statements = load_statements(SQL_FILE)
conn = pymysql.connect(
host=host,
user=user,
password=password,
database=database,
charset="utf8mb4",
autocommit=False,
)
try:
with conn.cursor() as cur:
for i, sql in enumerate(statements, 1):
cur.execute(sql)
head = sql.split()[0].upper()
if head == "DELETE" and cur.rowcount >= 0:
print(f"[{i}] DELETE … rowcount={cur.rowcount}")
else:
print(f"[{i}] ok")
conn.commit()
print("COMMIT ok")
except Exception as e:
conn.rollback()
print("ROLLBACK:", e, file=sys.stderr)
raise
finally:
conn.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,24 @@
-- 由 generate_wa_merchandise_insert_from_dump.py 生成
-- 源: ccd-yangtangyoupin_2026-04-26_10-25-01_mysql_data.sql
-- 条件: created_at >= 2026-04-22 00:00:00 且 user_id卖家在数据范围共 18 行)
-- 执行前请确认目标库;若存在主键冲突可先处理或改用 INSERT IGNORE / REPLACE
INSERT INTO `wa_merchandise` VALUES
(163212,169703,93004,'鲜锋活力宝','/upload/image/20251118/bc323c55ca713eee8badf5bf358893b7_691c26bd07fc3.jpg',22532.37,1,0,'2026-04-22 14:50:16','2026-04-23 10:00:01'),
(163213,169715,92801,'鲜锋活力宝','/upload/image/20251118/bc323c55ca713eee8badf5bf358893b7_691c26bd07fc3.jpg',35655.95,1,0,'2026-04-22 14:50:52','2026-04-23 10:01:13'),
(163214,169743,92566,'鲜锋活力宝','/upload/image/20251118/bc323c55ca713eee8badf5bf358893b7_691c26bd07fc3.jpg',29788.66,1,0,'2026-04-22 14:51:06','2026-04-23 10:00:00'),
(163223,169735,93007,'鲜锋活力宝','/upload/image/20251118/bc323c55ca713eee8badf5bf358893b7_691c26bd07fc3.jpg',33613.59,1,0,'2026-04-22 15:00:30','2026-04-23 10:01:40'),
(163224,169726,93116,'鲜锋活力宝','/upload/image/20251118/bc323c55ca713eee8badf5bf358893b7_691c26bd07fc3.jpg',34621.99,1,0,'2026-04-22 15:04:45','2026-04-23 10:00:13'),
(163225,169692,92638,'鲜锋活力宝','/upload/image/20251118/bc323c55ca713eee8badf5bf358893b7_691c26bd07fc3.jpg',24784.54,1,0,'2026-04-22 15:05:51','2026-04-23 10:04:15'),
(163226,169708,92787,'鲜锋活力宝','/upload/image/20251118/bc323c55ca713eee8badf5bf358893b7_691c26bd07fc3.jpg',23081.44,1,0,'2026-04-22 15:05:54','2026-04-23 10:00:13'),
(163231,169728,93096,'鲜锋活力宝','/upload/image/20251118/bc323c55ca713eee8badf5bf358893b7_691c26bd07fc3.jpg',26916.09,1,0,'2026-04-22 15:10:21','2026-04-23 10:04:22'),
(163234,169707,92637,'鲜锋活力宝','/upload/image/20251118/bc323c55ca713eee8badf5bf358893b7_691c26bd07fc3.jpg',24404.25,1,0,'2026-04-22 15:22:39','2026-04-23 10:01:00'),
(163235,169693,93121,'鲜锋活力宝','/upload/image/20251118/bc323c55ca713eee8badf5bf358893b7_691c26bd07fc3.jpg',33031.21,1,0,'2026-04-22 15:29:57','2026-04-23 10:00:00'),
(163237,169724,92965,'鲜锋活力宝','/upload/image/20251118/bc323c55ca713eee8badf5bf358893b7_691c26bd07fc3.jpg',23693.45,1,0,'2026-04-22 15:34:45','2026-04-23 10:02:03'),
(163239,169698,92884,'鲜锋活力宝','/upload/image/20251118/bc323c55ca713eee8badf5bf358893b7_691c26bd07fc3.jpg',23904.59,1,0,'2026-04-22 15:34:57','2026-04-23 10:03:01'),
(163244,169700,93093,'鲜锋活力宝','/upload/image/20251118/bc323c55ca713eee8badf5bf358893b7_691c26bd07fc3.jpg',23693.45,1,0,'2026-04-22 16:46:21','2026-04-23 10:01:55'),
(163245,169699,93129,'鲜锋活力宝','/upload/image/20251118/bc323c55ca713eee8badf5bf358893b7_691c26bd07fc3.jpg',23904.59,1,0,'2026-04-22 16:53:43','2026-04-23 10:00:22'),
(163246,169720,92839,'鲜锋活力宝','/upload/image/20251118/bc323c55ca713eee8badf5bf358893b7_691c26bd07fc3.jpg',23081.44,1,0,'2026-04-22 16:57:44','2026-04-23 10:01:35'),
(163254,169741,93020,'鲜锋活力宝','/upload/image/20251118/bc323c55ca713eee8badf5bf358893b7_691c26bd07fc3.jpg',23693.45,1,0,'2026-04-22 17:25:34','2026-04-23 10:00:02'),
(163255,169756,93094,'鲜锋活力宝','/upload/image/20251118/bc323c55ca713eee8badf5bf358893b7_691c26bd07fc3.jpg',40227.74,1,0,'2026-04-22 17:28:19','2026-04-23 10:01:00'),
(163259,169704,93099,'鲜锋活力宝','/upload/image/20251118/bc323c55ca713eee8badf5bf358893b7_691c26bd07fc3.jpg',22939.85,1,0,'2026-04-22 17:43:38','2026-04-23 10:00:02');

View File

@@ -0,0 +1,4 @@
# 从 dump 解析条件created_at >= 2026-04-22 且 user_id 在名单
# 重新生成python3 docs/sql/extract_wa_merchandise_keep_ids_from_dump.py /path/to/dump.sql
163212,163213,163214,163223,163224,163225,163226,163231,163234,163235,163237,163239,163244,163245,163246,163254,163255,163259

View File

@@ -6,7 +6,7 @@
// let domain = 'https://jfanyue.szxingming.com'
// let domain = 'https://jf.wenjinhui.com'
// let domain = 'https://jjy-jf.fwxgpt.com'
let domain = 'https://czcf-jf.uj345.com'
let domain = 'https://sxsy-jf.cichude.com'
// let domain = 'https://jf.hapengran.com'
// let domain = 'https://jjy-jf.uj345.com'
// let domain = 'https://ccd-jf.cichude.com'
@@ -17,7 +17,7 @@ module.exports = {
// HTTP_REQUEST_URL:'',
HTTP_REQUEST_URL: domain,
// H5商城地址
HTTP_H5_URL: 'https://czcf-jf.uj345.com',
HTTP_H5_URL: 'https://sxsy-jf.cichude.com',
// #endif
// #ifdef H5
HTTP_REQUEST_URL:domain,

View File

@@ -34,7 +34,7 @@
export default {
data() {
return {
pdfUrl: '/static/sign_contract_czcf82.pdf',
pdfUrl: '/static/sign_contract_sxsy80.pdf',
userId: '',
isMobile: false,
usePdfJs: false,

View File

@@ -338,7 +338,7 @@ export default {
// 跳转到抢购页面
goToRushBuy() {
// #ifdef H5
window.location.href = 'https://czcf.uj345.com/?#/pages/personal/index'
window.location.href = 'https://sxsy.cichude.com/?#/pages/personal/index'
// window.location.href = 'https://ccd.cichude.com/?#/pages/personal/index'
// window.location.href = 'https://shop.wenjinhui.com/?#/pages/personal/index'
//window.location.href = 'https://anyue.szxingming.com/?#/pages/personal/index'
@@ -347,7 +347,7 @@ export default {
// #endif
// #ifndef H5
uni.navigateTo({
url: '/pages/web-view/index?url=' + encodeURIComponent('https://czcf.uj345.com/?#/pages/personal/index')
url: '/pages/web-view/index?url=' + encodeURIComponent('https://sxsy.cichude.com/?#/pages/personal/index')
})
// #endif
},

View File

@@ -16,7 +16,7 @@ export default {
},
onLoad(options) {
const url = options && options.url ? decodeURIComponent(options.url) : '/static/sign_contract_czcf82.pdf'
const url = options && options.url ? decodeURIComponent(options.url) : '/static/sign_contract_sxsy80.pdf'
this.pdfUrl = url
},

View File

@@ -360,7 +360,7 @@ export default {
});
// 返回
setTimeout(() => {
window.location.href = 'https://czcf.uj345.com/?#/pages/rushing/index' + (this.userId ? ('?user_id=' + this.userId) : '')
window.location.href = 'https://sxsy.cichude.com/?#/pages/rushing/index' + (this.userId ? ('?user_id=' + this.userId) : '')
// window.location.href = 'https://shop.wenjinhui.com/?#/pages/rushing/index' + (this.userId ? ('?user_id=' + this.userId) : '')
// window.location.href = 'https://anyue.szxingming.com/?#/pages/rushing/index' + (this.userId ? ('?user_id=' + this.userId) : '')
// window.location.href = 'https://xiashengjun.com/?#/pages/rushing/index' + (this.userId ? ('?user_id=' + this.userId) : '')

Binary file not shown.