8 Commits

Author SHA1 Message Date
danaisuiyuan
bdabe3ba95 chore: update deployment docs and assets 2026-06-15 09:33:00 +08:00
danaisuiyuan
4d12a49f7c feat(sqszx202): configure integral mall deployment 2026-06-14 18:57:01 +08:00
danaisuiyuan
c3f2494243 docs: record sqszx202 data cleanup 2026-06-14 09:21:04 +08:00
danaisuiyuan
01e373faf6 docs: add bosenyuan miao80 cleanup record 2026-06-04 22:52:38 +08:00
danaisuiyuan
6b940e424c chore: update byhlc112 deployment domains 2026-06-04 09:01:52 +08:00
danaisuiyuan
5762f4e762 docs: add czcf82 cleanup records 2026-06-01 21:17:19 +08:00
danaisuiyuan
718d8c5a3c fix byhlc112 contract upload flow 2026-05-30 20:08:25 +08:00
danaisuiyuan
9eac385378 feat(byhlc112): add project deployment configuration 2026-05-29 09:19:30 +08:00
96 changed files with 25158 additions and 14478 deletions

5
.gitignore vendored
View File

@@ -37,3 +37,8 @@ deploy/docker/scripts/server.env
deploy/docker/step1-integral/.env
deploy/docker/step1-integral/houtai.env
deploy/docker/step2-single-shop/.env
# Local migration backups and generated Python cache
docs/sql/backups/
__pycache__/
**/__pycache__/

View File

@@ -13,8 +13,8 @@ ENV = 'development'
# shjjy153 项目
# VUE_APP_BASE_API = 'http://jjy-jfadmin.fwxgpt.com'
# czleilei240 项目
VUE_APP_BASE_API = 'https://leilei-jf.czchunfang.com'
# sqszx202 项目
VUE_APP_BASE_API = 'https://jf.j3s4s5.com'
# hapr191 项目(淮安鹏然商贸)
# VUE_APP_BASE_API = 'http://jfadmin.hapengran.com'

View File

@@ -13,8 +13,8 @@ ENV = 'production'
# shjjy153 项目
# VUE_APP_BASE_API = 'http://jjy-jfadmin.fwxgpt.com'
# czleilei240 项目
VUE_APP_BASE_API = 'https://leilei-jf.czchunfang.com'
# sqszx202 项目
VUE_APP_BASE_API = 'https://jf.j3s4s5.com'
# hapr191 项目(淮安鹏然商贸)
# VUE_APP_BASE_API = 'http://jfadmin.hapengran.com'

View File

@@ -50,7 +50,7 @@ public class UploadController {
@ApiImplicitParam(name = "model", value = "模块 用户user,商品product,微信wechat,news文章"),
@ApiImplicitParam(name = "pid", value = "分类ID 0编辑器,1商品图片,2拼团图片,3砍价图片,4秒杀图片,5文章图片,6组合数据图,7前台用户,8微信系列 ", allowableValues = "range[0,1,2,3,4,5,6,7,8]")
})
public CommonResult<FileResultVo> image(MultipartFile multipart,
public CommonResult<FileResultVo> image(@RequestParam("multipart") MultipartFile multipart,
@RequestParam(value = "model") String model,
@RequestParam(value = "pid") Integer pid) throws IOException {
return CommonResult.success(uploadService.imageUpload(multipart, model, pid));
@@ -66,7 +66,7 @@ public class UploadController {
@ApiImplicitParam(name = "model", value = "模块 用户user,商品product,微信wechat,news文章"),
@ApiImplicitParam(name = "pid", value = "分类ID 0编辑器,1商品图片,2拼团图片,3砍价图片,4秒杀图片,5文章图片,6组合数据图,7前台用户,8微信系列 ", allowableValues = "range[0,1,2,3,4,5,6,7,8]")
})
public CommonResult<FileResultVo> file(MultipartFile multipart,
public CommonResult<FileResultVo> file(@RequestParam("multipart") MultipartFile multipart,
@RequestParam(value = "model") String model,
@RequestParam(value = "pid") Integer pid) throws IOException {
return CommonResult.success(uploadService.fileUpload(multipart, model, pid));
@@ -75,4 +75,3 @@ public class UploadController {
}

View File

@@ -0,0 +1,60 @@
# CRMEB 相关配置
crmeb:
captchaOn: false # 是否开启行为验证码
asyncConfig: true #是否同步config表数据到redis
server:
port: 30032
# 订单同步配置每个单商户实例需要配置不同的source-id和target-mer-id
sync:
source-id: shop_16
target-mer-id: 16
spring:
datasource:
name: byhlc112
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://rm-bp1a178eq62lxba9xbo.mysql.rds.aliyuncs.com:3306/${spring.datasource.name}?useUnicode=true&serverTimezone=GMT%2B8&characterEncoding=utf8
username: yangtangyoupin
password: 5Fn8eWrbYFtAhCZw
redis:
host: 39.97.236.112 #地址
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: ./crmeb_log
# mybatis 配置
mybatis-plus:
# 配置sql打印日志
configuration:
log-impl:
#swagger 配置
swagger:
basic:
enable: true #是否开启界面
check: false #是否打开验证
username: crmeb #访问swagger的账号
password: crmeb.com #访问swagger的密码

View File

@@ -0,0 +1,60 @@
# CRMEB 相关配置
crmeb:
captchaOn: false # 是否开启行为验证码
asyncConfig: true #是否同步config表数据到redis
server:
port: 30032
# 订单同步配置每个单商户实例需要配置不同的source-id和target-mer-id
sync:
source-id: shop_17
target-mer-id: 17
spring:
datasource:
name: sqszx202
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://rm-bp1a178eq62lxba9xbo.mysql.rds.aliyuncs.com:3306/${spring.datasource.name}?useUnicode=true&serverTimezone=GMT%2B8&characterEncoding=utf8
username: yangtangyoupin
password: 5Fn8eWrbYFtAhCZw
redis:
host: 59.110.91.202 #地址
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: ./crmeb_log
# 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: czleilei240
active: sqszx202
servlet:
multipart:
max-file-size: 50MB #设置单个文件大小
@@ -172,4 +172,4 @@ aj:
# local定时清除过期缓存(单位秒),设置为0代表不执行
timing-clear: 3600
history-data-clear-enable: false
history-data-clear-enable: false

View File

@@ -48,11 +48,11 @@ public class UploadFrontController {
@ApiImplicitParam(name = "model", value = "模块 用户user,商品product,微信wechat,news文章"),
@ApiImplicitParam(name = "pid", value = "分类ID 0编辑器,1商品图片,2拼团图片,3砍价图片,4秒杀图片,5文章图片,6组合数据图,7前台用户,8微信系列 ", allowableValues = "range[0,1,2,3,4,5,6,7,8]")
})
public CommonResult<FileResultVo> image(MultipartFile multipart, @RequestParam(value = "model") String model,
public CommonResult<FileResultVo> image(@RequestParam("multipart") MultipartFile multipart,
@RequestParam(value = "model") String model,
@RequestParam(value = "pid") Integer pid) throws IOException {
return CommonResult.success(uploadService.imageUpload(multipart, model, pid));
}
}

View File

@@ -49,7 +49,7 @@ public class UserUploadController {
@ApiImplicitParam(name = "model", value = "模块 用户user,商品product,微信wechat,news文章"),
@ApiImplicitParam(name = "pid", value = "分类ID 0编辑器,1商品图片,2拼团图片,3砍价图片,4秒杀图片,5文章图片,6组合数据图,7前台用户,8微信系列 ", allowableValues = "range[0,1,2,3,4,5,6,7,8]")
})
public CommonResult<FileResultVo> image(MultipartFile multipart,
public CommonResult<FileResultVo> image(@RequestParam("multipart") MultipartFile multipart,
@RequestParam(value = "model") String model,
@RequestParam(value = "pid") Integer pid) throws IOException {
@@ -66,9 +66,9 @@ public class UserUploadController {
@ApiImplicitParam(name = "model", value = "模块 用户user,商品product,微信wechat,news文章"),
@ApiImplicitParam(name = "pid", value = "分类ID 0编辑器,1商品图片,2拼团图片,3砍价图片,4秒杀图片,5文章图片,6组合数据图,7前台用户,8微信系列 ", allowableValues = "range[0,1,2,3,4,5,6,7,8]")
})
public CommonResult<FileResultVo> imageOuter(MultipartFile multipart,
@RequestParam(value = "model") String model,
@RequestParam(value = "pid") Integer pid) throws IOException {
public CommonResult<FileResultVo> imageOuter(@RequestParam("multipart") MultipartFile multipart,
@RequestParam(value = "model") String model,
@RequestParam(value = "pid") Integer pid) throws IOException {
return CommonResult.success(uploadService.imageUpload(multipart, model, pid));
}
@@ -83,7 +83,7 @@ public class UserUploadController {
@ApiImplicitParam(name = "model", value = "模块 用户user,商品product,微信wechat,news文章"),
@ApiImplicitParam(name = "pid", value = "分类ID 0编辑器,1商品图片,2拼团图片,3砍价图片,4秒杀图片,5文章图片,6组合数据图,7前台用户,8微信系列 ", allowableValues = "range[0,1,2,3,4,5,6,7,8]")
})
public CommonResult<FileResultVo> file(MultipartFile multipart,
public CommonResult<FileResultVo> file(@RequestParam("multipart") MultipartFile multipart,
@RequestParam(value = "model") String model,
@RequestParam(value = "pid") Integer pid) throws IOException {
return CommonResult.success(uploadService.fileUpload(multipart, model, pid));
@@ -92,4 +92,3 @@ public class UserUploadController {
}

View File

@@ -6,6 +6,7 @@ import com.zbkj.common.response.WaLoginResponse;
import com.zbkj.common.response.WaUserInfoResponse;
import com.zbkj.common.result.CommonResult;
import com.zbkj.common.token.FrontTokenComponent;
import com.zbkj.common.config.CrmebConfig;
import com.zbkj.common.vo.FileResultVo;
import com.zbkj.front.service.WaUserService;
import com.zbkj.service.dao.consignment.WaUsersDao;
@@ -14,7 +15,6 @@ import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
@@ -22,6 +22,9 @@ import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
@@ -44,12 +47,13 @@ import java.util.Date;
* |
* +----------------------------------------------------------------------
*/
@Slf4j
@RestController
@RequestMapping("api/front/wa/user")
@Api(tags = "寄卖服务 -- 用户认证")
public class WaUserController {
private static final Logger log = LoggerFactory.getLogger(WaUserController.class);
@Autowired
private WaUserService waUserService;
@@ -62,6 +66,17 @@ public class WaUserController {
@Autowired
private WaUsersDao waUsersDao;
@Autowired
private CrmebConfig crmebConfig;
private String buildPublicFileUrl(String relativeUrl) {
String domain = StringUtils.defaultString(crmebConfig.getDomain(), "https://j3s4s5.com").trim();
if (!StringUtils.startsWithAny(domain, "http://", "https://")) {
domain = "https://" + domain;
}
return StringUtils.removeEnd(domain, "/") + "/" + StringUtils.removeStart(relativeUrl, "/");
}
/**
* 处理PDF文件添加用户签名和签署日期
* @param signatureImage 用户签名图片
@@ -73,7 +88,7 @@ public class WaUserController {
FileInputStream fileInputStream = null;
try {
// 读取模板PDF文件
Resource resource = new ClassPathResource("pdf/sign_contract_czleilei240.pdf");
Resource resource = new ClassPathResource("pdf/sign_contract_sqszx202.pdf");
InputStream pdfInputStream = resource.getInputStream();
document = PDDocument.load(pdfInputStream);
pdfInputStream.close();
@@ -185,21 +200,27 @@ public class WaUserController {
@ApiImplicitParam(name = "model", value = "模块 用户user,商品product,微信wechat,新闻文章"),
@ApiImplicitParam(name = "pid", value = "分类ID 0编辑器,1商品图片,2拼团图片,3砍价图片,4秒杀图片,5文章图片,6组合数据图,7前台用户,8微信系列 ", allowableValues = "range[0,1,2,3,4,5,6,7,8]")
})
public CommonResult<FileResultVo> image(MultipartFile multipart, @RequestParam(value = "model") String model,
@RequestParam(value = "pid") Integer pid) throws IOException {
public CommonResult<FileResultVo> image(@RequestParam("multipart") MultipartFile multipart,
@RequestParam(value = "model") String model,
@RequestParam(value = "pid", required = false) Integer pid,
@RequestParam(value = "userId", required = false) Integer userId) throws IOException {
// 如果是用户模型且上传的是图片则先处理PDF文件
if ("user".equals(model) && multipart != null) {
// 处理PDF文件添加用户签名和签署日期
FileResultVo pdfResultVo = processPdfWithSignature(multipart);
if (pdfResultVo != null) {
// 更新用户contract字段
if (pid != null) {
Integer targetUserId = userId != null ? userId : pid;
if (targetUserId == null) {
targetUserId = frontTokenComponent.getUserId();
}
if (targetUserId != null) {
WaUsers user = new WaUsers();
user.setId(pid);
user.setId(targetUserId);
// 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://leilei.czchunfang.com/"+pdfResultVo.getUrl());
user.setContract(buildPublicFileUrl(pdfResultVo.getUrl()));
waUsersDao.updateById(user);
}
return CommonResult.success(pdfResultVo);
@@ -265,4 +286,4 @@ public class WaUserController {
public CommonResult<Boolean> changePassword(@RequestBody @Validated PasswordRequest request) {
return CommonResult.success(waUserService.changePassword(request));
}
}
}

View File

@@ -0,0 +1,55 @@
crmeb:
imagePath: /www/wwwroot/h5y2c.com/ # 服务器图片路径配置 斜杠结尾
domain: h5y2c.com # 当前项目域名,合同/PDF 等公开地址拼接使用
asyncConfig: true #是否同步config表数据到redis
server:
port: 30031
spring:
datasource:
name: byhlc112
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://rm-bp1a178eq62lxba9xbo.mysql.rds.aliyuncs.com:3306/${spring.datasource.name}?useUnicode=true&serverTimezone=GMT%2B8&characterEncoding=utf8
username: yangtangyoupin
password: 5Fn8eWrbYFtAhCZw
redis:
host: 39.97.236.112 #地址
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

@@ -0,0 +1,55 @@
crmeb:
imagePath: /www/wwwroot/j3s4s5.com/ # 服务器图片路径配置 斜杠结尾
domain: https://j3s4s5.com/ # 当前项目域名,合同/PDF 等公开地址拼接使用
asyncConfig: true #是否同步config表数据到redis
server:
port: 30031
spring:
datasource:
name: sqszx202
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://rm-bp1a178eq62lxba9xbo.mysql.rds.aliyuncs.com:3306/${spring.datasource.name}?useUnicode=true&serverTimezone=GMT%2B8&characterEncoding=utf8
username: yangtangyoupin
password: 5Fn8eWrbYFtAhCZw
redis:
host: 59.110.91.202 #地址
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

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

View File

@@ -2,6 +2,13 @@
> 详细方案见仓库根目录的 `DOCKER_DEPLOY.md`。本文件只列必要操作。
## 已提供的项目化部署目录
| 项目 | 步骤一 | 步骤二 |
|-----|------|------|
| `byhlc112` | `deploy/docker/step1-integral-byhlc112` | `deploy/docker/step2-single-shop-byhlc112` |
| `bygsf212`(鼎信汇商贸) | 待步骤一项目目录 | `deploy/docker/step2-single-shop-bygsf212` |
## 1. 准备环境变量
```bash
@@ -68,7 +75,8 @@ done
## 7. "fast" 模式(跳过前端构建,使用源码已有 dist
如果源码目录里 `backend-adminend/dist``single_uniapp22miao/unpackage/dist/build/` 已经是最新构建产物,可加速:
- **手动使用HBuilder编译发布**
- 如果源码目录里 `backend-adminend/dist``single_uniapp22miao/unpackage/dist/build/` 已经是最新构建产物,可加速:
```bash
docker compose build --build-arg=BUILDKIT_INLINE_CACHE=1 \
@@ -110,4 +118,3 @@ ssh-copy-id root@116.62.83.240
> 用密码模式需要先 `brew install hudochenkov/sshpass/sshpass`macOS
> 用 SSH key 模式则任何依赖都不需要。

View File

@@ -0,0 +1,54 @@
upstream resell_api {
server 127.0.0.1:18085;
keepalive 10240;
}
server
{
listen 80;
listen 443 ssl http2;
server_name admin.lehoo6.com;
index index.html index.htm default.htm default.html;
root /www/wwwroot/admin.lehoo6.com;
include /www/server/panel/vhost/nginx/extension/admin.lehoo6.com/*.conf;
#CERT-APPLY-CHECK--START
include /www/server/panel/vhost/nginx/well-known/admin.lehoo6.com.conf;
#CERT-APPLY-CHECK--END
#SSL-START
set $isRedcert 1;
if ($server_port != 443) {
set $isRedcert 2;
}
if ( $uri ~ /\.well-known/ ) {
set $isRedcert 1;
}
if ($isRedcert != 1) {
rewrite ^(/.*)$ https://$host$1 permanent;
}
ssl_certificate /www/wwwroot/integral-shop/deploy/docker/ssl-cert/lehoo6.com_cert/nginx/lehoo6.com.pem;
ssl_certificate_key /www/wwwroot/integral-shop/deploy/docker/ssl-cert/lehoo6.com_cert/nginx/lehoo6.com.key;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_tickets on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
add_header Strict-Transport-Security "max-age=31536000";
error_page 497 https://$host$request_uri;
#SSL-END
location / {
proxy_pass http://resell_api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
client_max_body_size 50m;
}
access_log /www/wwwlogs/admin.lehoo6.com.log;
error_log /www/wwwlogs/admin.lehoo6.com.error.log;
}

View File

@@ -0,0 +1,92 @@
upstream jifenmall_h5 {
server 127.0.0.1:18082;
keepalive 10240;
}
server
{
listen 80;
listen 443 ssl http2;
server_name jf.lehoo6.com;
index index.html index.htm default.htm default.html;
root /www/wwwroot/jf.lehoo6.com;
include /www/server/panel/vhost/nginx/extension/jf.lehoo6.com/*.conf;
#CERT-APPLY-CHECK--START
include /www/server/panel/vhost/nginx/well-known/jf.lehoo6.com.conf;
#CERT-APPLY-CHECK--END
#SSL-START
set $isRedcert 1;
if ($server_port != 443) {
set $isRedcert 2;
}
if ( $uri ~ /\.well-known/ ) {
set $isRedcert 1;
}
if ($isRedcert != 1) {
rewrite ^(/.*)$ https://$host$1 permanent;
}
ssl_certificate /www/wwwroot/integral-shop/deploy/docker/ssl-cert/lehoo6.com_cert/nginx/lehoo6.com.pem;
ssl_certificate_key /www/wwwroot/integral-shop/deploy/docker/ssl-cert/lehoo6.com_cert/nginx/lehoo6.com.key;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_tickets on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
add_header Strict-Transport-Security "max-age=31536000";
error_page 497 https://$host$request_uri;
#SSL-END
#REWRITE-START
include /www/server/panel/vhost/rewrite/html_jf.lehoo6.com.conf;
#REWRITE-END
location /api/front {
proxy_pass http://127.0.0.1:30033/api/front;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
client_max_body_size 50m;
}
location /api/admin {
proxy_pass http://127.0.0.1:30032/api/admin;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
client_max_body_size 50m;
}
location /api/external {
proxy_pass http://127.0.0.1:30032/api/external;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
client_max_body_size 50m;
}
location ^~ / {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_set_header Connection "";
if (!-f $request_filename) {
proxy_pass http://jifenmall_h5;
}
}
access_log /www/wwwlogs/jf.lehoo6.com.log;
error_log /www/wwwlogs/jf.lehoo6.com.error.log;
}

View File

@@ -0,0 +1,59 @@
upstream jifenmall_admin {
server 127.0.0.1:18081;
keepalive 10240;
}
server
{
listen 80;
listen 443 ssl http2;
server_name jfadmin.lehoo6.com;
index index.html index.htm default.htm default.html;
root /www/wwwroot/jfadmin.lehoo6.com;
include /www/server/panel/vhost/nginx/extension/jfadmin.lehoo6.com/*.conf;
#CERT-APPLY-CHECK--START
include /www/server/panel/vhost/nginx/well-known/jfadmin.lehoo6.com.conf;
#CERT-APPLY-CHECK--END
#SSL-START
set $isRedcert 1;
if ($server_port != 443) {
set $isRedcert 2;
}
if ( $uri ~ /\.well-known/ ) {
set $isRedcert 1;
}
if ($isRedcert != 1) {
rewrite ^(/.*)$ https://$host$1 permanent;
}
ssl_certificate /www/wwwroot/integral-shop/deploy/docker/ssl-cert/lehoo6.com_cert/nginx/lehoo6.com.pem;
ssl_certificate_key /www/wwwroot/integral-shop/deploy/docker/ssl-cert/lehoo6.com_cert/nginx/lehoo6.com.key;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_tickets on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
add_header Strict-Transport-Security "max-age=31536000";
error_page 497 https://$host$request_uri;
#SSL-END
#REWRITE-START
include /www/server/panel/vhost/rewrite/html_jfadmin.lehoo6.com.conf;
#REWRITE-END
location ^~ / {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_set_header Connection "";
if (!-f $request_filename) {
proxy_pass http://jifenmall_admin;
}
}
access_log /www/wwwlogs/jfadmin.lehoo6.com.log;
error_log /www/wwwlogs/jfadmin.lehoo6.com.error.log;
}

View File

@@ -0,0 +1,55 @@
upstream resell_h5 {
server 127.0.0.1:18080;
keepalive 10240;
}
server
{
listen 80;
listen 443 ssl http2;
server_name lehoo6.com;
index index.html index.htm default.htm default.html;
root /www/wwwroot/lehoo6.com;
include /www/server/panel/vhost/nginx/extension/lehoo6.com/*.conf;
#CERT-APPLY-CHECK--START
include /www/server/panel/vhost/nginx/well-known/lehoo6.com.conf;
#CERT-APPLY-CHECK--END
#SSL-START
set $isRedcert 1;
if ($server_port != 443) {
set $isRedcert 2;
}
if ( $uri ~ /\.well-known/ ) {
set $isRedcert 1;
}
if ($isRedcert != 1) {
rewrite ^(/.*)$ https://$host$1 permanent;
}
ssl_certificate /www/wwwroot/integral-shop/deploy/docker/ssl-cert/lehoo6.com_cert/nginx/lehoo6.com.pem;
ssl_certificate_key /www/wwwroot/integral-shop/deploy/docker/ssl-cert/lehoo6.com_cert/nginx/lehoo6.com.key;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_tickets on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
add_header Strict-Transport-Security "max-age=31536000";
error_page 497 https://$host$request_uri;
#SSL-END
location ^~ / {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_set_header Connection "";
if (!-f $request_filename) {
proxy_pass http://resell_h5;
}
}
access_log /www/wwwlogs/lehoo6.com.log;
error_log /www/wwwlogs/lehoo6.com.error.log;
}

View File

@@ -1,7 +1,7 @@
# =============================================================
# 积分商城 管理端 APImiao-admin-2.2.jar
# JAR 由宿主机 bind-mount 进来(/app/app.jar无需 Maven 编译
# 宿主机路径:${SINGLE_ADMIN_JAR} /app/app.jar
# 宿主机路径:${SINGLE_ADMIN_JAR} -> /app/app.jar
# FTP 更新 JAR 后docker compose --env-file .env restart single-admin-api
# =============================================================
@@ -11,7 +11,6 @@ ENV TZ=Asia/Shanghai \
LANG=C.UTF-8 LC_ALL=C.UTF-8 \
DEBIAN_FRONTEND=noninteractive
# 切换阿里云 Ubuntu 镜像源(服务器访问 archive.ubuntu.com 超时)
RUN sed -i \
-e 's|http://archive.ubuntu.com|https://mirrors.aliyun.com|g' \
-e 's|http://security.ubuntu.com|https://mirrors.aliyun.com|g' \
@@ -25,10 +24,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /app /config /usr/local/crmeb/crmebimage /app/log
# 堆大小(可通过 compose environment 覆盖)
ENV JAVA_HEAP_OPTS="-Xms128m -Xmx256m"
# Spring Boot 2.2.6 + Java 17 必须的模块开放参数
ENV JAVA_MODULE_OPTS="\
--add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/java.lang.reflect=ALL-UNNAMED \
@@ -39,10 +36,8 @@ ENV JAVA_MODULE_OPTS="\
--add-opens java.base/java.net=ALL-UNNAMED"
WORKDIR /app
# /app/app.jar 由 docker-compose volumes bind-mount 进来
EXPOSE 30032
# 等价于nohup java -Xms128m -Xmx256m -jar miao-admin-2.2.jar > admin.log &
ENTRYPOINT ["sh","-c","exec java \
$JAVA_HEAP_OPTS \
$JAVA_MODULE_OPTS \

View File

@@ -2,23 +2,19 @@
# 积分商城 管理后台前端Vue 2 SPA
# 纯 Nginx 运行时镜像,不含 Node 构建阶段
# 静态文件由宿主机 bind-mount 进来(${SINGLE_ADMIN_WEB_DIR}:/usr/share/nginx/html
# 宿主机目录示例:/www/wwwroot/leilei-jfadmin.czchunfang.com/
# 更新方式rsync 新 dist 到宿主机目录 → 无需重建镜像
# 更新方式rsync 新 dist 到宿主机目录 -> 无需重建镜像
# =============================================================
FROM nginx:1.25-alpine
ENV TZ=Asia/Shanghai
# 切换阿里云 Alpine 镜像源
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk add --no-cache tzdata \
&& cp /usr/share/zoneinfo/$TZ /etc/localtime \
&& echo $TZ > /etc/timezone \
&& rm -f /etc/apk/cache/*.apk
# Nginx 反代配置
# /api/ 和 /adminapi/ 代理到 single-admin-api 容器
RUN cat > /etc/nginx/conf.d/default.conf <<'NGX'
server {
listen 80;

View File

@@ -8,7 +8,8 @@ server:
port: ${SERVER_PORT:-30032}
crmeb:
imagePath: /usr/local/crmeb/crmebimage/
imagePath: /usr/local/crmeb/
domain: ${CRMEB_DOMAIN:https://b3y45.com/}
captchaOn: false
asyncConfig: true
demoSite: false

View File

@@ -1,7 +1,7 @@
# =============================================================
# 积分商城 用户端 APImiao-front-2.2.jar
# JAR 由宿主机 bind-mount 进来(/app/app.jar无需 Maven 编译
# 宿主机路径:${SINGLE_FRONT_JAR} /app/app.jar
# 宿主机路径:${SINGLE_FRONT_JAR} -> /app/app.jar
# FTP 更新 JAR 后docker compose --env-file .env restart single-front-api
# =============================================================
@@ -11,7 +11,6 @@ ENV TZ=Asia/Shanghai \
LANG=C.UTF-8 LC_ALL=C.UTF-8 \
DEBIAN_FRONTEND=noninteractive
# 切换阿里云 Ubuntu 镜像源(服务器访问 archive.ubuntu.com 超时)
RUN sed -i \
-e 's|http://archive.ubuntu.com|https://mirrors.aliyun.com|g' \
-e 's|http://security.ubuntu.com|https://mirrors.aliyun.com|g' \
@@ -25,10 +24,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /app /config /usr/local/crmeb/crmebimage /app/log
# 堆大小(可通过 compose environment 覆盖)
ENV JAVA_HEAP_OPTS="-Xms128m -Xmx256m"
# Spring Boot 2.2.6 + Java 17 必须的模块开放参数
ENV JAVA_MODULE_OPTS="\
--add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/java.lang.reflect=ALL-UNNAMED \
@@ -39,10 +36,8 @@ ENV JAVA_MODULE_OPTS="\
--add-opens java.base/java.net=ALL-UNNAMED"
WORKDIR /app
# /app/app.jar 由 docker-compose volumes bind-mount 进来
EXPOSE 30033
# 等价于nohup java -Xms128m -Xmx256m -jar miao-front-2.2.jar > front.log &
ENTRYPOINT ["sh","-c","exec java \
$JAVA_HEAP_OPTS \
$JAVA_MODULE_OPTS \

View File

@@ -1,24 +1,20 @@
# =============================================================
# 积分商城 用户端 H5uni-app SPA
# 积分商城 用户端 H5uni-app/HBuilder SPA
# 纯 Nginx 运行时镜像,不含 Node 构建阶段
# 静态文件由宿主机 bind-mount 进来(${SINGLE_H5_DIR}:/usr/share/nginx/html
# 宿主机目录示例:/www/wwwroot/leilei-jf.czchunfang.com/
# 更新方式rsync 新 dist 到宿主机目录 → 无需重建镜像
# 更新方式HBuilder 编译后 rsync 新 H5 产物到宿主机目录 -> 无需重建镜像
# =============================================================
FROM nginx:1.25-alpine
ENV TZ=Asia/Shanghai
# 切换阿里云 Alpine 镜像源
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk add --no-cache tzdata \
&& cp /usr/share/zoneinfo/$TZ /etc/localtime \
&& echo $TZ > /etc/timezone \
&& rm -f /etc/apk/cache/*.apk
# Nginx 反代配置
# API 请求代理到 single-front-api 容器Docker 内网,不经宝塔 Nginx
RUN cat > /etc/nginx/conf.d/default.conf <<'NGX'
server {
listen 80;
@@ -34,7 +30,6 @@ server {
try_files $uri =404;
}
# 前台 API单点登录/商品/订单等)
location /api/ {
proxy_pass http://single-front-api:30033/api/;
proxy_http_version 1.1;

View File

@@ -1,37 +0,0 @@
# 仅供参考: 内容已内联到 admin-web.Dockerfile
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
client_max_body_size 50m;
add_header X-Frame-Options SAMEORIGIN always;
location ~* \.(?:js|css|png|jpg|jpeg|gif|svg|woff2?|ttf|map)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
try_files $uri =404;
}
location /api/ {
proxy_pass http://single-admin-api:30032/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
client_max_body_size 50m;
}
location /adminapi/ {
proxy_pass http://single-admin-api:30032/adminapi/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location / {
try_files $uri $uri/ /index.html;
}
}

View File

@@ -1,30 +0,0 @@
# 仅供参考: 内容已内联到 h5.Dockerfile
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
client_max_body_size 50m;
add_header X-Frame-Options SAMEORIGIN always;
location ~* \.(?:js|css|png|jpg|jpeg|gif|svg|woff2?|ttf|map)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
try_files $uri =404;
}
location /api/ {
proxy_pass http://single-front-api:30031/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
client_max_body_size 50m;
}
location / {
try_files $uri $uri/ /index.html;
}
}

View File

@@ -1,31 +0,0 @@
# =============================================================
# 步骤一:寄卖商城环境变量 — 池州雷蕾商贸 czleilei240
# cp .env.example .env 并填入真实密码
# .env 不入库
# =============================================================
TZ=Asia/Shanghai
# ---------- Redis容器内 ----------
REDIS_PASSWORD=change-me-redis
# ---------- H5 对外域名(浏览器可达) ----------
INTEGRAL_TITLE=池州雷蕾商贸
INTEGRAL_API_PUBLIC_URL=https://leileiadmin.czchunfang.com
INTEGRAL_IMG_PUBLIC_URL=https://leileiadmin.czchunfang.com
INTEGRAL_H5_PUBLIC_URL=https://leilei.czchunfang.com/
INTEGRAL_SN_ID=17533260260517
INTEGRAL_APP_STR=ZFyTNQTWEkCBczKzyUDJWE9Ecx260517
INTEGRAL_CONTRACT_PAGE=10012
# ---------- 宿主机暴露端口 ----------
INTEGRAL_H5_PORT=18080
# webman API 直连端口(宝塔 Nginx leileiadmin.czchunfang.com → 此端口)
RESELL_API_PORT=18085
# ---------- 宿主机目录映射bind mount与原部署路径一致----------
# 寄卖商城 H5 静态文件目录(手动改 JS/configs.js 直接生效,无需重建镜像)
RESELL_H5_DIR=/www/wwwroot/leilei.czchunfang.com
# webman 后台完整应用目录FTP 上传新 webman.bin/public/ 后 restart 容器即可更新)
# 上传图片、public/upload 等均包含在此目录内,无需单独挂载
RESELL_HOUTAI_DIR=/www/wwwroot/leileiadmin.czchunfang.com

View File

@@ -1,2 +0,0 @@
.env
houtai.env

View File

@@ -1,126 +0,0 @@
# 步骤一:寄卖商城 Docker 部署(池州雷蕾商贸 czleilei240
项目:`integral-resell`(寄卖商城)
服务:`redis` · `integral-houtai`Webman PHP 8.0)· `integral-h5`Nginx 静态站)
步骤二(积分商城)与本步骤完全独立,可以单独部署、单独重启。
---
## 快速部署
```bash
cd deploy/docker/step1-integral
# 1. 准备环境变量
cp .env.example .env
cp houtai.env.example houtai.env
vim .env # 填入 REDIS_PASSWORD
vim houtai.env # 填入 DB_PASSWORDRDS 密码、REDIS_PASSWORD同 .env
# 2. 首次部署:在服务器上确保宿主机目录存在
# (若原部署目录已存在则跳过)
mkdir -p /www/wwwroot/leilei.czchunfang.com
mkdir -p /www/wwwroot/leileiadmin.czchunfang.com/public/upload
# 3. 将 H5 静态文件同步到宿主机目录(首次 / 每次前端更新后)
rsync -av integral-resell/h5/ /www/wwwroot/leilei.czchunfang.com/
# 4. 构建并启动
docker compose --env-file .env up -d --build
# 5. 查看状态
docker compose --env-file .env ps
docker compose --env-file .env logs -f integral-houtai
```
---
## 目录映射(宿主机 ↔ 容器)
| 宿主机路径 | 容器路径 | 用途 |
|---|---|---|
| `/www/wwwroot/leilei.czchunfang.com` | `/usr/share/nginx/html` | H5 静态文件,手动改 JS 即时生效 |
| `/www/wwwroot/leileiadmin.czchunfang.com/public/upload` | `/app/public/upload` | webman 后台上传文件 |
| `./houtai.env` | `/app/.env` | 运行时配置,只读挂入,不打进镜像 |
| `integral-runtime`named vol| `/app/runtime` | webman PID、session 等运行时数据 |
> **H5 文件更新流程**:直接修改 `/www/wwwroot/leilei.czchunfang.com/` 下的文件(如 JS bundle、configs.js
> Nginx 下次请求时自动读取新文件,**无需重启容器**。
> 仅当 Nginx 配置或镜像本身需要变更时,才需要 `docker compose build integral-h5`。
| 域名 | 用途 | Docker 容器端口 | 宿主机端口 | 宝塔 upstream |
|---|---|---|---|---|
| `leilei.czchunfang.com` | 寄卖商城 H5 | integral-h5:80 | **18080** | `resell_h5` |
| `leileiadmin.czchunfang.com` | 寄卖商城 API / 后台 | integral-houtai:**8785** | **18085** | `resell_api` |
> webman.bin 写死监听 **8785** 端口。
> - H5 容器内部 Nginx 已将 `/api/` 和 `/upload/` 代理到 `integral-houtai:8785`Docker 内网,无需暴露)
> - 宝塔 Nginx 的 `leileiadmin.czchunfang.com` 直连宿主机 **18085**(映射到 webman 8785
---
## 宝塔 Nginx 配置
将以下两个文件内容分别粘贴到宝塔面板对应站点的「配置文件」中:
| 配置文件 | 说明 |
|---|---|
| `deploy/docker/nginx/leilei.czchunfang.com.conf` | H5 站点upstream → 127.0.0.1:18080 |
| `deploy/docker/nginx/leileiadmin.czchunfang.com.conf` | API 站点upstream → 127.0.0.1:18085 |
证书路径(文件已在项目中):
```
deploy/docker/ssl-cert/
leilei.czchunfang.com_cert/nginx/leilei.czchunfang.com.{pem,key}
leileiadmin.czchunfang.com_cert/nginx/leileiadmin.czchunfang.com.{pem,key}
```
---
## 验证
| 地址 | 预期 |
|------|------|
| `https://leilei.czchunfang.com/` | 寄卖商城 H5 首页(生产) |
| `https://leileiadmin.czchunfang.com/api/...` | 寄卖商城 API生产 |
| `http://116.62.83.240:18080/` | H5 直连测试(绕过域名/SSL |
---
## 常用命令
```bash
# 重启 webman
docker compose --env-file .env restart integral-houtai
# 看 webman 日志
docker compose --env-file .env logs -f integral-houtai
# 进入 webman 容器
docker compose --env-file .env exec integral-houtai bash
# 仅重建 H5改了 .env 中的域名参数后)
docker compose --env-file .env build integral-h5
docker compose --env-file .env up -d integral-h5
# 停止(保留卷)
docker compose --env-file .env down
# 停止并删除所有卷(慎用:清空上传图片和 runtime
docker compose --env-file .env down -v
```
---
## 关键一致性检查
| 位置 | 值 |
|---|---|
| `.env` INTEGRAL_API_PUBLIC_URL | `https://leileiadmin.czchunfang.com` |
| `.env` INTEGRAL_H5_PUBLIC_URL | `https://leilei.czchunfang.com/` |
| `.env` INTEGRAL_APP_STR | `ZFyTNQTWEkCBczKzyUDJWE9Ecx260517` |
| `houtai.env` APP_SECRET | **同上** |
| `.env` INTEGRAL_SN_ID | `17533260260517` |
| `h5/static/configs.js` sn_id | **同上** |

View File

@@ -1,95 +0,0 @@
# =============================================================
# 步骤一寄卖商城integral-resell独立部署
# 客户:池州雷蕾商贸 czleilei240
# 包含服务redis · integral-houtai(webman) · integral-h5(Nginx)
# =============================================================
name: resell-czleilei240
x-common: &common
restart: unless-stopped
environment:
TZ: ${TZ:-Asia/Shanghai}
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
networks:
integral-net:
driver: bridge
volumes:
integral-redis-data:
integral-runtime:
services:
# ---------- Redis ----------
redis:
<<: *common
build:
context: .
dockerfile: redis.Dockerfile
image: resell-czleilei240/redis:local
container_name: integral-redis
command: ["--requirepass", "${REDIS_PASSWORD}", "--appendonly", "yes"]
volumes:
- integral-redis-data:/data
networks: [integral-net]
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 3s
retries: 5
# ---------- Webman 后端 ----------
integral-houtai:
<<: *common
build:
context: ../../../integral-resell/houtai
dockerfile: ../../deploy/docker/integral-resell/houtai.Dockerfile
image: resell-czleilei240/houtai:latest
container_name: integral-houtai
networks: [integral-net]
ports:
# 宝塔 Nginx 直连 webman APIwebman.bin 写死监听 8785
- "${RESELL_API_PORT:-18085}:8785"
volumes:
# 整个应用目录挂到宿主机 /www/wwwroot/leileiadmin.czchunfang.com/
# FTP 上传新 webman.bin / public/ 后 docker compose restart integral-houtai 即可生效
- ${RESELL_HOUTAI_DIR}:/app
# .env 单独挂入(覆盖宿主机目录里的 .env避免明文密码出现在 wwwroot
- ./houtai.env:/app/.env:ro
# runtime 使用命名卷(日志/session/pid 不受 FTP 覆盖影响)
- integral-runtime:/app/runtime
depends_on:
redis:
condition: service_healthy
# ---------- H5 静态站 ----------
integral-h5:
<<: *common
build:
context: ../../../integral-resell/h5
dockerfile: ../../deploy/docker/integral-resell/h5.Dockerfile
image: resell-czleilei240/h5:latest
container_name: integral-h5
networks: [integral-net]
environment:
TZ: ${TZ:-Asia/Shanghai}
INTEGRAL_TITLE: ${INTEGRAL_TITLE}
INTEGRAL_API_PUBLIC_URL: ${INTEGRAL_API_PUBLIC_URL}
INTEGRAL_IMG_PUBLIC_URL: ${INTEGRAL_IMG_PUBLIC_URL}
INTEGRAL_H5_PUBLIC_URL: ${INTEGRAL_H5_PUBLIC_URL}
INTEGRAL_SN_ID: ${INTEGRAL_SN_ID}
INTEGRAL_APP_STR: ${INTEGRAL_APP_STR}
INTEGRAL_CONTRACT_PAGE: ${INTEGRAL_CONTRACT_PAGE}
volumes:
# H5 静态文件目录挂到宿主机,手动更新 JS/configs.js 直接生效,无需重建镜像
# 子目录 crmebimage/ 同时由步骤二 Java 后端写入PDF/图片Nginx 直接对外提供访问
- ${RESELL_H5_DIR}:/usr/share/nginx/html
ports:
- "${INTEGRAL_H5_PORT:-18080}:80"
depends_on:
- integral-houtai

View File

@@ -1,37 +0,0 @@
# =============================================================
# Webman 后端运行时配置 — 池州雷蕾商贸 czleilei240寄卖商城
# cp houtai.env.example houtai.env 并填入真实密码
# houtai.env 不入库,由 docker-compose volumes: 挂入 /app/.env
# =============================================================
# MySQL阿里云 RDS
DB_HOST = 'rm-bp1a178eq62lxba9xbo.mysql.rds.aliyuncs.com'
DB_PORT = 3306
DB_DATABASE = 'yangtangyoupin'
DB_USERNAME = 'yangtangyoupin'
DB_PASSWORD = 'change-me'
# Redis指向同 compose 内的 redis 容器)
REDIS_HOST = 'redis'
REDIS_PORT = 6379
REDIS_PASSWORD = 'change-me-redis'
# 短信(池州雷蕾商贸专属通道)
SMS_CHANNEL = 'alibaba'
SMS_SIGNNAME = '池州雷蕾商贸'
SMS_TEMPLATE = 'SMS_334320185'
SMS_KEYID = 'LTAI5t7CfS15hZGdNLLEMUwG'
SMS_KEYSECRET = 'ikfTvHbMMg5sStGgdvLNL8iuVYdner'
SMS_SDKAPPID = ''
# OSS不启用则走本地 public/upload
FILE_STORAGE = 'public'
OSS_ACCESS_ID = ''
OSS_ACCESS_SECRET = ''
OSS_BUCKET = ''
OSS_ENDPOINT = ''
OSS_URL = ''
# 业务标识(须与 H5 configs.js 的 sn_id / appStr 以及积分商城 admin 后台一致)
APP_SIGN = '1'
APP_SECRET = 'ZFyTNQTWEkCBczKzyUDJWE9Ecx260517'

View File

@@ -1,13 +0,0 @@
# 使用 Alpine 通过 apk 安装 Redis绕过镜像源问题
FROM alpine:3.19
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk add --no-cache redis tzdata \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone
VOLUME /data
WORKDIR /data
EXPOSE 6379
ENTRYPOINT ["redis-server"]

View File

@@ -1,5 +1,5 @@
# =============================================================
# 步骤二:积分商城环境变量 — 池州雷蕾商贸 czleilei240
# 步骤二:积分商城环境变量 — 鼎信汇商贸 bygsf212
# 使用方法cp .env.example .env 然后填入真实密码
# .env 不入库(已加入 .gitignore
# =============================================================
@@ -11,16 +11,15 @@ REDIS_PASSWORD=change-me-redis
# ---------- 阿里云 RDS ----------
RDS_HOST=rm-bp1a178eq62lxba9xbo.mysql.rds.aliyuncs.com
RDS_DB=yangtangyoupin
RDS_DB=bygsf212
RDS_USER=yangtangyoupin
RDS_PASSWORD=change-me
# ---------- 订单同步(多商户 source / target ----------
SYNC_SOURCE_ID=shop_15
SYNC_TARGET_MER_ID=15
SYNC_SOURCE_ID=shop_18
SYNC_TARGET_MER_ID=18
# ---------- Java JAR 宿主机路径FTP 更新后 restart 容器即可) ----------
# 对应宿主机原启动命令目录:/www/wwwroot/javaapi/
SINGLE_FRONT_JAR=/www/wwwroot/javaapi/miao-front-2.2.jar
SINGLE_ADMIN_JAR=/www/wwwroot/javaapi/miao-admin-2.2.jar
@@ -29,15 +28,14 @@ SINGLE_FRONT_LOG_DIR=/www/wwwroot/javaapi/logs/front
SINGLE_ADMIN_LOG_DIR=/www/wwwroot/javaapi/logs/admin
# ---------- 图片/PDF 目录(与步骤一 H5 Nginx 共享宿主机路径) ----------
# Java 后端写入 /usr/local/crmeb/crmebimage/ → 宿主机 /www/wwwroot/leilei.czchunfang.com/crmebimage/
# 步骤一的 H5 Nginxleilei.czchunfang.com提供对外访问
CRMEB_IMAGE_DIR=/www/wwwroot/leilei.czchunfang.com/crmebimage
CRMEB_IMAGE_DIR=/www/wwwroot/b3y45.com
CRMEB_DOMAIN=https://b3y45.com/
# ---------- 前端静态目录bind-mountrsync 更新后立即生效) ----------
# 积分商城 H5uni-app SPA对应域名 leilei-jf.czchunfang.com
SINGLE_H5_DIR=/www/wwwroot/leilei-jf.czchunfang.com
# 积分商城管理后台Vue SPA对应域名 leilei-jfadmin.czchunfang.com
SINGLE_ADMIN_WEB_DIR=/www/wwwroot/leilei-jfadmin.czchunfang.com
# 积分商城 H5uni-app/HBuilder 编译产物),对应域名 jf.b3y45.com
SINGLE_H5_DIR=/www/wwwroot/jf.b3y45.com
# 积分商城管理后台Vue SPA对应域名 jfadmin.b3y45.com
SINGLE_ADMIN_WEB_DIR=/www/wwwroot/jfadmin.b3y45.com
# ---------- 宿主机暴露端口(供宝塔 Nginx 反代) ----------
SINGLE_ADMIN_PORT=18081

View File

@@ -0,0 +1,120 @@
# 步骤二:积分商城 Docker 部署(鼎信汇商贸 bygsf212
项目:`single-shop-22`(积分商城)
服务:`redis` · `single-front-api`Spring Boot· `single-admin-api`Spring Boot
`single-admin-web`Vue 管理后台)· `single-h5`uni-app/HBuilder H5
步骤一(寄卖商城)与本步骤完全独立,可以单独部署、单独重启。
默认域名:
- 积分商城 H5`jf.b3y45.com`
- 积分商城管理后台:`jfadmin.b3y45.com`
---
## 部署前提:宿主机文件准备
### 1. Java JARSpring Boot API
```bash
mkdir -p /www/wwwroot/javaapi/logs/front
mkdir -p /www/wwwroot/javaapi/logs/admin
scp single-shop-22/backend/crmeb-front/target/miao-front-2.2.jar root@118.31.36.212:/www/wwwroot/javaapi/
scp single-shop-22/backend/crmeb-admin/target/miao-admin-2.2.jar root@118.31.36.212:/www/wwwroot/javaapi/
```
### 2. 前端静态文件
`single_uniapp22miao` 使用 HBuilder/HBuilderX 编译 H5把编译产物上传到积分商城 H5 目录。
```bash
mkdir -p /www/wwwroot/jf.b3y45.com
rsync -a --delete single-shop-22/single_uniapp22miao/unpackage/dist/build/h5/ \
root@118.31.36.212:/www/wwwroot/jf.b3y45.com/
chmod -R 755 /www/wwwroot/jf.b3y45.com/
mkdir -p /www/wwwroot/jfadmin.b3y45.com
rsync -a --delete single-shop-22/backend-adminend/dist/ \
root@118.31.36.212:/www/wwwroot/jfadmin.b3y45.com/
chmod -R 755 /www/wwwroot/jfadmin.b3y45.com/
```
> 如果 HBuilderX 实际输出目录是 `unpackage/dist/build/web/`,把上面的 `build/h5/` 替换成 `build/web/`。
### 3. 图片/PDF 目录
```bash
mkdir -p /www/wwwroot/b3y45.com
```
---
## 快速部署
```bash
cd deploy/docker/step2-single-shop-bygsf212
cp .env.example .env
vim .env
docker compose --env-file .env build
docker compose --env-file .env up -d
docker compose --env-file .env ps
docker compose --env-file .env logs -f single-front-api
docker compose --env-file .env logs -f single-admin-api
```
---
## 域名与端口
| 域名 | 用途 | 宿主机端口 |
|---|---|---|
| `jf.b3y45.com` | 积分商城 H5uni-app/HBuilder | **18082** |
| `jfadmin.b3y45.com` | 积分商城管理后台Vue | **18081** |
> Spring Boot API 端口30032 / 30033仅容器内监听不对外暴露。
> 宝塔 Nginx 通过域名反代到 `127.0.0.1:18081 / 18082`,再由容器内 Nginx 转发到 API。
> 图片/PDF 实际落盘路径为宿主机 `/www/wwwroot/b3y45.com/crmebimage/public/...`。
---
## 验证
| 地址 | 预期 |
|------|------|
| `https://jf.b3y45.com/` | 积分商城 H5 |
| `https://jfadmin.b3y45.com/` | 积分商城管理后台 |
| `http://118.31.36.212:18082/` | H5 直连测试 |
| `http://118.31.36.212:18081/` | 管理后台直连测试 |
---
## bind-mount 目录总览
| 宿主机路径 | 挂入容器路径 | 说明 |
|---|---|---|
| `/www/wwwroot/javaapi/miao-front-2.2.jar` | `/app/app.jar` | 用户端 API JAR |
| `/www/wwwroot/javaapi/miao-admin-2.2.jar` | `/app/app.jar` | 管理端 API JAR |
| `/www/wwwroot/javaapi/logs/front/` | `/app/log` | 用户端 API 日志 |
| `/www/wwwroot/javaapi/logs/admin/` | `/app/log` | 管理端 API 日志 |
| `/www/wwwroot/b3y45.com/` | `/usr/local/crmeb/` | 图片/PDF 写入目录 |
| `/www/wwwroot/jf.b3y45.com/` | `/usr/share/nginx/html` | H5 静态文件 |
| `/www/wwwroot/jfadmin.b3y45.com/` | `/usr/share/nginx/html` | 管理后台静态文件 |
| `../single-shop/application-docker.yml` | `/config/application-docker.yml` | Spring Boot 配置 |
---
## bygsf212 关键配置对照
| 配置项 | 值 |
|---|---|
| RDS Host | `rm-bp1a178eq62lxba9xbo.mysql.rds.aliyuncs.com` |
| DB / User | `bygsf212` / `yangtangyoupin` |
| imagePath 宿主机目录 | `/www/wwwroot/b3y45.com/` |
| CRMEB_DOMAIN | `https://b3y45.com/` |
| SYNC_SOURCE_ID | `shop_18` |
| SYNC_TARGET_MER_ID | `18` |
| Spring profile | `docker`(通过 `application-docker.yml` 注入) |

View File

@@ -0,0 +1,164 @@
# =============================================================
# 步骤二积分商城single-shop-22独立部署
# 客户:鼎信汇商贸 bygsf212
# 包含服务redis · single-admin-api · single-front-api
# single-admin-web(Vue) · single-h5(uni-app/HBuilder)
# =============================================================
name: jifenmall-bygsf212
x-common: &common
restart: unless-stopped
environment:
TZ: ${TZ:-Asia/Shanghai}
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
x-spring-common: &spring-common
<<: *common
environment:
TZ: ${TZ:-Asia/Shanghai}
MYSQL_HOST: ${RDS_HOST}
MYSQL_DATABASE: ${RDS_DB}
MYSQL_USERNAME: ${RDS_USER}
MYSQL_PASSWORD: ${RDS_PASSWORD}
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PASSWORD: ${REDIS_PASSWORD}
CRMEB_DOMAIN: ${CRMEB_DOMAIN}
SYNC_SOURCE_ID: ${SYNC_SOURCE_ID:-shop_18}
SYNC_TARGET_MER_ID: ${SYNC_TARGET_MER_ID:-18}
networks:
single-net:
driver: bridge
volumes:
single-redis-data:
services:
redis:
<<: *common
build:
context: .
dockerfile: redis.Dockerfile
image: jifenmall-bygsf212/redis:local
container_name: single-redis-bygsf212
command: ["--requirepass", "${REDIS_PASSWORD}", "--appendonly", "yes"]
volumes:
- single-redis-data:/data
networks: [single-net]
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 3s
retries: 5
single-front-api:
<<: *spring-common
build:
context: .
dockerfile: ../single-shop/front-api.Dockerfile
image: jifenmall-bygsf212/front-api:local
container_name: single-front-api-bygsf212
networks: [single-net]
ports:
- "127.0.0.1:30033:30033"
volumes:
- ${SINGLE_FRONT_JAR}:/app/app.jar:ro
- ${CRMEB_IMAGE_DIR}:/usr/local/crmeb
- ${SINGLE_FRONT_LOG_DIR}:/app/log
- ../single-shop/application-docker.yml:/config/application-docker.yml:ro
environment:
TZ: ${TZ:-Asia/Shanghai}
MYSQL_HOST: ${RDS_HOST}
MYSQL_DATABASE: ${RDS_DB}
MYSQL_USERNAME: ${RDS_USER}
MYSQL_PASSWORD: ${RDS_PASSWORD}
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PASSWORD: ${REDIS_PASSWORD}
CRMEB_DOMAIN: ${CRMEB_DOMAIN}
SYNC_SOURCE_ID: ${SYNC_SOURCE_ID:-shop_18}
SYNC_TARGET_MER_ID: ${SYNC_TARGET_MER_ID:-18}
SERVER_PORT: 30033
depends_on:
redis:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:30033/actuator/health || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 90s
single-admin-api:
<<: *spring-common
build:
context: .
dockerfile: ../single-shop/admin-api.Dockerfile
image: jifenmall-bygsf212/admin-api:local
container_name: single-admin-api-bygsf212
networks: [single-net]
ports:
- "127.0.0.1:30032:30032"
volumes:
- ${SINGLE_ADMIN_JAR}:/app/app.jar:ro
- ${CRMEB_IMAGE_DIR}:/usr/local/crmeb
- ${SINGLE_ADMIN_LOG_DIR}:/app/log
- ../single-shop/application-docker.yml:/config/application-docker.yml:ro
environment:
TZ: ${TZ:-Asia/Shanghai}
MYSQL_HOST: ${RDS_HOST}
MYSQL_DATABASE: ${RDS_DB}
MYSQL_USERNAME: ${RDS_USER}
MYSQL_PASSWORD: ${RDS_PASSWORD}
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PASSWORD: ${REDIS_PASSWORD}
CRMEB_DOMAIN: ${CRMEB_DOMAIN}
SYNC_SOURCE_ID: ${SYNC_SOURCE_ID:-shop_18}
SYNC_TARGET_MER_ID: ${SYNC_TARGET_MER_ID:-18}
SERVER_PORT: 30032
depends_on:
redis:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:30032/actuator/health || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 90s
single-admin-web:
<<: *common
build:
context: .
dockerfile: ../single-shop/admin-web.Dockerfile
image: jifenmall-bygsf212/admin-web:local
container_name: single-admin-web-bygsf212
networks: [single-net]
ports:
- "${SINGLE_ADMIN_PORT:-18081}:80"
volumes:
- ${SINGLE_ADMIN_WEB_DIR}:/usr/share/nginx/html
depends_on:
- single-admin-api
single-h5:
<<: *common
build:
context: .
dockerfile: ../single-shop/h5.Dockerfile
image: jifenmall-bygsf212/h5:local
container_name: single-h5-bygsf212
networks: [single-net]
ports:
- "${SINGLE_H5_PORT:-18082}:80"
volumes:
- ${SINGLE_H5_DIR}:/usr/share/nginx/html
depends_on:
- single-front-api

View File

@@ -0,0 +1,41 @@
# =============================================================
# 步骤二:积分商城环境变量 — 宝应宏煜春商贸 byhlc112
# 使用方法cp .env.example .env 然后填入真实密码
# .env 不入库(已加入 .gitignore
# =============================================================
TZ=Asia/Shanghai
# ---------- Redis容器内与步骤一独立 ----------
REDIS_PASSWORD=change-me-redis
# ---------- 阿里云 RDS ----------
RDS_HOST=rm-bp1a178eq62lxba9xbo.mysql.rds.aliyuncs.com
RDS_DB=byhlc112
RDS_USER=yangtangyoupin
RDS_PASSWORD=change-me
# ---------- 订单同步(多商户 source / target ----------
SYNC_SOURCE_ID=shop_16
SYNC_TARGET_MER_ID=16
# ---------- Java JAR 宿主机路径FTP 更新后 restart 容器即可) ----------
SINGLE_FRONT_JAR=/www/wwwroot/javaapi/miao-front-2.2.jar
SINGLE_ADMIN_JAR=/www/wwwroot/javaapi/miao-admin-2.2.jar
# ---------- Java 日志目录bind-mount 到宿主机,直接 tail -f 查看) ----------
SINGLE_FRONT_LOG_DIR=/www/wwwroot/javaapi/logs/front
SINGLE_ADMIN_LOG_DIR=/www/wwwroot/javaapi/logs/admin
# ---------- 图片/PDF 目录(与步骤一 H5 Nginx 共享宿主机路径) ----------
CRMEB_IMAGE_DIR=/www/wwwroot/h5y2c.com
# ---------- 前端静态目录bind-mountrsync 更新后立即生效) ----------
# 积分商城 H5uni-app SPA对应域名 jf.h5y2c.com
SINGLE_H5_DIR=/www/wwwroot/jf.h5y2c.com
# 积分商城管理后台Vue SPA对应域名 jfadmin.h5y2c.com
SINGLE_ADMIN_WEB_DIR=/www/wwwroot/jfadmin.h5y2c.com
# ---------- 宿主机暴露端口(供宝塔 Nginx 反代) ----------
SINGLE_ADMIN_PORT=18081
SINGLE_H5_PORT=18082

View File

@@ -0,0 +1 @@
.env

View File

@@ -0,0 +1,123 @@
# 步骤二:积分商城 Docker 部署(宝应宏煜春商贸 byhlc112
项目:`single-shop-22`(积分商城)
服务:`redis` · `single-front-api`Spring Boot· `single-admin-api`Spring Boot
`single-admin-web`Vue 管理后台)· `single-h5`uni-app H5
步骤一(寄卖商城)与本步骤完全独立,可以单独部署、单独重启。
> 这套方案参考 `deploy/docker/step2-single-shop`,按 `czleilei240` 已验证结构复制。
> 当前默认域名假设为:
> - 积分商城 H5`jf.h5y2c.com`
> - 积分商城管理后台:`jfadmin.h5y2c.com`
---
## 部署前提:宿主机文件准备
### 1. Java JARSpring Boot API
```bash
mkdir -p /www/wwwroot/javaapi/logs/front
mkdir -p /www/wwwroot/javaapi/logs/admin
scp single-shop-22/backend/crmeb-front/target/miao-front-2.2.jar root@39.97.236.112:/www/wwwroot/javaapi/
scp single-shop-22/backend/crmeb-admin/target/miao-admin-2.2.jar root@39.97.236.112:/www/wwwroot/javaapi/
```
### 2. 前端静态文件
```bash
mkdir -p /www/wwwroot/jf.h5y2c.com
rsync -a --delete single-shop-22/single_uniapp22miao/unpackage/dist/build/web/ \
root@39.97.236.112:/www/wwwroot/jf.h5y2c.com/
chmod -R 755 /www/wwwroot/jf.h5y2c.com/
mkdir -p /www/wwwroot/jfadmin.h5y2c.com
rsync -a --delete single-shop-22/backend-adminend/dist/ \
root@39.97.236.112:/www/wwwroot/jfadmin.h5y2c.com/
chmod -R 755 /www/wwwroot/jfadmin.h5y2c.com/
```
### 3. 图片/PDF 目录
```bash
mkdir -p /www/wwwroot/h5y2c.com
```
---
## 快速部署
```bash
cd deploy/docker/step2-single-shop-byhlc112
cp .env.example .env
vim .env
docker compose --env-file .env build
docker compose --env-file .env up -d
docker compose --env-file .env ps
docker compose --env-file .env logs -f single-front-api
docker compose --env-file .env logs -f single-admin-api
```
---
## 域名与端口
| 域名 | 用途 | 宿主机端口 |
|---|---|---|
| `jf.h5y2c.com` | 积分商城 H5uni-app | **18082** |
| `jfadmin.h5y2c.com` | 积分商城管理后台Vue | **18081** |
> Spring Boot API 端口30032 / 30033仅容器内监听不对外暴露。
> 宝塔 Nginx 通过域名反代到 `127.0.0.1:18081 / 18082`,再由容器内 Nginx 转发到 API。
> 图片/PDF 实际落盘路径为宿主机 `/www/wwwroot/h5y2c.com/crmebimage/public/...`。
---
## 验证
| 地址 | 预期 |
|------|------|
| `https://jf.h5y2c.com/` | 积分商城 H5 |
| `https://jfadmin.h5y2c.com/` | 积分商城管理后台 |
| `http://39.97.236.112:18082/` | H5 直连测试 |
| `http://39.97.236.112:18081/` | 管理后台直连测试 |
---
## bind-mount 目录总览
| 宿主机路径 | 挂入容器路径 | 说明 |
|---|---|---|
| `/www/wwwroot/javaapi/miao-front-2.2.jar` | `/app/app.jar` | 用户端 API JAR |
| `/www/wwwroot/javaapi/miao-admin-2.2.jar` | `/app/app.jar` | 管理端 API JAR |
| `/www/wwwroot/javaapi/logs/front/` | `/app/log` | 用户端 API 日志 |
| `/www/wwwroot/javaapi/logs/admin/` | `/app/log` | 管理端 API 日志 |
| `/www/wwwroot/h5y2c.com/` | `/usr/local/crmeb/` | 图片/PDF 写入目录 |
| `/www/wwwroot/jf.h5y2c.com/` | `/usr/share/nginx/html` | H5 静态文件 |
| `/www/wwwroot/jfadmin.h5y2c.com/` | `/usr/share/nginx/html` | 管理后台静态文件 |
| `../single-shop/application-docker.yml` | `/config/application-docker.yml` | Spring Boot 配置 |
---
## byhlc112 关键配置对照
| 配置项 | 值 |
|---|---|
| RDS Host | `rm-bp1a178eq62lxba9xbo.mysql.rds.aliyuncs.com` |
| DB / User | `byhlc112` / `yangtangyoupin` |
| imagePath 宿主机目录 | `/www/wwwroot/h5y2c.com/` |
| SYNC_SOURCE_ID | `shop_16` |
| SYNC_TARGET_MER_ID | `16` |
| Spring profile | `docker`(通过 `application-docker.yml` 注入) |
---
## 待确认项
- 如果积分管理后台域名不是 `jfadmin.h5y2c.com`,需要同步替换 `.env.example`、README 和宝塔 Nginx 配置
- Redis 仍按 Docker 内置实例方案生成;若你想接外部 Redis可以再帮你补一版外部 Redis 配置

View File

@@ -1,17 +1,11 @@
# =============================================================
# 步骤二积分商城single-shop-22独立部署
# 客户:池州雷蕾商贸 czleilei240
# 客户:宝应宏煜春商贸 byhlc112
# 包含服务redis · single-admin-api · single-front-api
# single-admin-web(Vue) · single-h5(uni-app)
#
# 优化要点(参考 step1 寄卖商城经验):
# 1. Redis本地 Alpine+apk 构建,不从 DockerHub 拉取镜像
# 2. Java API不含 Maven 编译JAR bind-mount 自宿主机(快速部署/更新)
# 3. 前端Nginx only 镜像,静态文件 bind-mount更新无需重建镜像
# 4. 日志bind-mount 到宿主机,无需进容器查看
# =============================================================
name: jifenmall-czleilei240
name: jifenmall-byhlc112
x-common: &common
restart: unless-stopped
@@ -34,8 +28,8 @@ x-spring-common: &spring-common
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PASSWORD: ${REDIS_PASSWORD}
SYNC_SOURCE_ID: ${SYNC_SOURCE_ID:-shop_15}
SYNC_TARGET_MER_ID: ${SYNC_TARGET_MER_ID:-15}
SYNC_SOURCE_ID: ${SYNC_SOURCE_ID:-shop_16}
SYNC_TARGET_MER_ID: ${SYNC_TARGET_MER_ID:-16}
networks:
single-net:
@@ -45,14 +39,13 @@ volumes:
single-redis-data:
services:
# ---------- RedisAlpine 本地构建,无需拉取 Docker Hub 镜像) ----------
redis:
<<: *common
build:
context: .
dockerfile: redis.Dockerfile
image: jifenmall-czleilei240/redis:local
container_name: single-redis
image: jifenmall-byhlc112/redis:local
container_name: single-redis-byhlc112
command: ["--requirepass", "${REDIS_PASSWORD}", "--appendonly", "yes"]
volumes:
- single-redis-data:/data
@@ -63,27 +56,20 @@ services:
timeout: 3s
retries: 5
# ---------- Front API用户端 Spring Boot ----------
# JAR 文件 bind-mount 自 ${SINGLE_FRONT_JAR}(宿主机 /www/wwwroot/javaapi/miao-front-2.2.jar
# 更新 JARFTP 替换宿主机文件 → docker compose restart single-front-api
single-front-api:
<<: *spring-common
build:
context: .
dockerfile: ../single-shop/front-api.Dockerfile
image: jifenmall-czleilei240/front-api:local
container_name: single-front-api
image: jifenmall-byhlc112/front-api:local
container_name: single-front-api-byhlc112
networks: [single-net]
ports:
- "127.0.0.1:30033:30033"
volumes:
# JAR bind-mountFTP 更新 JAR 后 restart 容器即可
- ${SINGLE_FRONT_JAR}:/app/app.jar:ro
# 图片/PDF 目录:与 step1 H5 Nginx 共享宿主机路径
- ${CRMEB_IMAGE_DIR}:/usr/local/crmeb/crmebimage
# 日志bind-mount 到宿主机,便于直接查看
- ${CRMEB_IMAGE_DIR}:/usr/local/crmeb
- ${SINGLE_FRONT_LOG_DIR}:/app/log
# Spring 配置:只读挂入
- ../single-shop/application-docker.yml:/config/application-docker.yml:ro
environment:
TZ: ${TZ:-Asia/Shanghai}
@@ -94,8 +80,8 @@ services:
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PASSWORD: ${REDIS_PASSWORD}
SYNC_SOURCE_ID: ${SYNC_SOURCE_ID:-shop_15}
SYNC_TARGET_MER_ID: ${SYNC_TARGET_MER_ID:-15}
SYNC_SOURCE_ID: ${SYNC_SOURCE_ID:-shop_16}
SYNC_TARGET_MER_ID: ${SYNC_TARGET_MER_ID:-16}
SERVER_PORT: 30033
depends_on:
redis:
@@ -107,27 +93,20 @@ services:
retries: 5
start_period: 90s
# ---------- Admin API管理端 Spring Boot ----------
# JAR 文件 bind-mount 自 ${SINGLE_ADMIN_JAR}(宿主机 /www/wwwroot/javaapi/miao-admin-2.2.jar
# 更新 JARFTP 替换宿主机文件 → docker compose restart single-admin-api
single-admin-api:
<<: *spring-common
build:
context: .
dockerfile: ../single-shop/admin-api.Dockerfile
image: jifenmall-czleilei240/admin-api:local
container_name: single-admin-api
image: jifenmall-byhlc112/admin-api:local
container_name: single-admin-api-byhlc112
networks: [single-net]
ports:
- "127.0.0.1:30032:30032"
volumes:
# JAR bind-mount
- ${SINGLE_ADMIN_JAR}:/app/app.jar:ro
# 图片/PDF 目录
- ${CRMEB_IMAGE_DIR}:/usr/local/crmeb/crmebimage
# 日志 bind-mount
- ${CRMEB_IMAGE_DIR}:/usr/local/crmeb
- ${SINGLE_ADMIN_LOG_DIR}:/app/log
# Spring 配置
- ../single-shop/application-docker.yml:/config/application-docker.yml:ro
environment:
TZ: ${TZ:-Asia/Shanghai}
@@ -138,8 +117,8 @@ services:
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PASSWORD: ${REDIS_PASSWORD}
SYNC_SOURCE_ID: ${SYNC_SOURCE_ID:-shop_15}
SYNC_TARGET_MER_ID: ${SYNC_TARGET_MER_ID:-15}
SYNC_SOURCE_ID: ${SYNC_SOURCE_ID:-shop_16}
SYNC_TARGET_MER_ID: ${SYNC_TARGET_MER_ID:-16}
SERVER_PORT: 30032
depends_on:
redis:
@@ -151,40 +130,32 @@ services:
retries: 5
start_period: 90s
# ---------- Admin WebVue 管理后台Nginx only ----------
# 静态文件 bind-mount 自 ${SINGLE_ADMIN_WEB_DIR}(宿主机 /www/wwwroot/leilei-jfadmin.czchunfang.com/
# 更新前端rsync 新 dist/ 到宿主机目录 → 浏览器硬刷新即可(无需重启容器)
single-admin-web:
<<: *common
build:
context: .
dockerfile: ../single-shop/admin-web.Dockerfile
image: jifenmall-czleilei240/admin-web:local
container_name: single-admin-web
image: jifenmall-byhlc112/admin-web:local
container_name: single-admin-web-byhlc112
networks: [single-net]
ports:
- "${SINGLE_ADMIN_PORT:-18081}:80"
volumes:
# 静态文件 bind-mountrsync 更新宿主机目录后立即生效
- ${SINGLE_ADMIN_WEB_DIR}:/usr/share/nginx/html
depends_on:
- single-admin-api
# ---------- H5 前端uni-app SPANginx only ----------
# 静态文件 bind-mount 自 ${SINGLE_H5_DIR}(宿主机 /www/wwwroot/leilei-jf.czchunfang.com/
# 更新前端rsync 新 unpackage/dist/build/h5/ 到宿主机目录 → 无需重启容器
single-h5:
<<: *common
build:
context: .
dockerfile: ../single-shop/h5.Dockerfile
image: jifenmall-czleilei240/h5:local
container_name: single-h5
image: jifenmall-byhlc112/h5:local
container_name: single-h5-byhlc112
networks: [single-net]
ports:
- "${SINGLE_H5_PORT:-18082}:80"
volumes:
# 静态文件 bind-mount
- ${SINGLE_H5_DIR}:/usr/share/nginx/html
depends_on:
- single-front-api

View File

@@ -0,0 +1,18 @@
# =============================================================
# RedisAlpine + apk 安装,绕过 Docker Hub 镜像拉取问题)
# 与 step1 方案一致:不依赖 docker.io只需 registry-1.docker.io 拉 alpine:3.19
# =============================================================
FROM alpine:3.19
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk add --no-cache redis tzdata \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& rm -f /etc/apk/cache/*.apk
VOLUME /data
WORKDIR /data
EXPOSE 6379
ENTRYPOINT ["redis-server"]

View File

@@ -1,176 +0,0 @@
# 步骤二:积分商城 Docker 部署(池州雷蕾商贸 czleilei240
项目:`single-shop-22`(积分商城)
服务:`redis` · `single-front-api`Spring Boot· `single-admin-api`Spring Boot
`single-admin-web`Vue 管理后台)· `single-h5`uni-app H5
步骤一(寄卖商城)与本步骤完全独立,可以单独部署、单独重启。
---
## 部署前提:宿主机文件准备
> JAR 和静态文件通过 **bind-mount** 挂入容器,部署前需先把文件放到宿主机对应目录。
### 1. Java JARSpring Boot API
```bash
# 宿主机目录
mkdir -p /www/wwwroot/javaapi/logs/front
mkdir -p /www/wwwroot/javaapi/logs/admin
# 将本地编译好的 JAR 传到服务器macOS 本地执行)
scp single-shop-22/backend/crmeb-front/target/miao-front-2.2.jar root@116.62.83.240:/www/wwwroot/javaapi/
scp single-shop-22/backend/crmeb-admin/target/miao-admin-2.2.jar root@116.62.83.240:/www/wwwroot/javaapi/
```
> 更新 JARFTP 替换宿主机文件 → `docker compose --env-file .env restart single-front-api`
### 2. 前端静态文件
```bash
# H5uni-app
mkdir -p /www/wwwroot/leilei-jf.czchunfang.com
rsync -a --delete single-shop-22/single_uniapp22miao/unpackage/dist/build/h5/ \
root@116.62.83.240:/www/wwwroot/leilei-jf.czchunfang.com/
chmod -R 755 /www/wwwroot/leilei-jf.czchunfang.com/
# 管理后台Vue
mkdir -p /www/wwwroot/leilei-jfadmin.czchunfang.com
rsync -a --delete single-shop-22/backend-adminend/dist/ \
root@116.62.83.240:/www/wwwroot/leilei-jfadmin.czchunfang.com/
chmod -R 755 /www/wwwroot/leilei-jfadmin.czchunfang.com/
```
> 更新前端rsync 同步到宿主机 → 浏览器强刷即可,无需重建镜像或重启容器
### 3. 图片/PDF 目录
```bash
# 与步骤一 H5 Nginx 共享,步骤一已创建则无需重建
mkdir -p /www/wwwroot/leilei.czchunfang.com/crmebimage
```
---
## 快速部署
```bash
cd deploy/docker/step2-single-shop
# 1. 准备环境变量
cp .env.example .env
vim .env # 填入 RDS_PASSWORD、REDIS_PASSWORD
# 2. 构建镜像(仅 JRE + Nginx无 Maven/Node约 2-5 分钟)
docker compose --env-file .env build
# 3. 启动所有服务
docker compose --env-file .env up -d
# 4. 查看状态
docker compose --env-file .env ps
docker compose --env-file .env logs -f single-front-api
docker compose --env-file .env logs -f single-admin-api
```
---
## 域名与端口
| 域名 | 用途 | 宿主机端口 |
|---|---|---|
| `leilei-jf.czchunfang.com` | 积分商城 H5uni-app | **18082** |
| `leilei-jfadmin.czchunfang.com` | 积分商城管理后台Vue | **18081** |
> Spring Boot API 端口30032 / 30033仅容器内监听不对外暴露。
> 宝塔 Nginx 通过域名反代到 `127.0.0.1:18081 / 18082`,再由容器内 Nginx 转发到 API。
---
## 宝塔 Nginx 配置
将以下两个文件内容分别粘贴到宝塔面板对应站点的「配置文件」中:
| 配置文件 | 说明 |
|---|---|
| `deploy/docker/nginx/leilei-jf.czchunfang.com.conf` | H5 站点upstream → 127.0.0.1:18082 |
| `deploy/docker/nginx/leilei-jfadmin.czchunfang.com.conf` | 管理后台upstream → 127.0.0.1:18081 |
证书路径(文件已在项目中):
```
deploy/docker/ssl-cert/
leilei-jf.czchunfang.com_cert/nginx/leilei-jf.czchunfang.com.{pem,key}
leilei-jfadmin.czchunfang.com_cert/nginx/leilei-jfadmin.czchunfang.com.{pem,key}
```
---
## 验证
| 地址 | 预期 |
|------|------|
| `https://leilei-jf.czchunfang.com/` | 积分商城 H5生产 |
| `https://leilei-jfadmin.czchunfang.com/` | 积分商城管理后台(生产) |
| `http://116.62.83.240:18082/` | H5 直连测试(绕过域名/SSL |
| `http://116.62.83.240:18081/` | 管理后台直连测试 |
---
## 常用运维命令
```bash
# 重启 Java API更新 JAR 后)
docker compose --env-file .env restart single-front-api
docker compose --env-file .env restart single-admin-api
# 实时日志(宿主机路径 /www/wwwroot/javaapi/logs/ 也可直接查看)
docker compose --env-file .env logs -f single-admin-api
docker compose --env-file .env logs -f single-front-api
# 进入容器
docker compose --env-file .env exec single-admin-api bash
# 停止(保留卷)
docker compose --env-file .env down
# 停止并删除 Redis 数据卷(慎用)
docker compose --env-file .env down -v
```
---
## bind-mount 目录总览
| 宿主机路径 | 挂入容器路径 | 说明 |
|---|---|---|
| `/www/wwwroot/javaapi/miao-front-2.2.jar` | `/app/app.jar` (front-api) | 用户端 API JAR只读 |
| `/www/wwwroot/javaapi/miao-admin-2.2.jar` | `/app/app.jar` (admin-api) | 管理端 API JAR只读 |
| `/www/wwwroot/javaapi/logs/front/` | `/app/log` (front-api) | 用户端 API 日志 |
| `/www/wwwroot/javaapi/logs/admin/` | `/app/log` (admin-api) | 管理端 API 日志 |
| `/www/wwwroot/leilei.czchunfang.com/crmebimage/` | `/usr/local/crmeb/crmebimage/` (两个 API) | 图片/PDF 写入目录 |
| `/www/wwwroot/leilei-jf.czchunfang.com/` | `/usr/share/nginx/html` (h5) | H5 静态文件 |
| `/www/wwwroot/leilei-jfadmin.czchunfang.com/` | `/usr/share/nginx/html` (admin-web) | 管理后台静态文件 |
| `../single-shop/application-docker.yml` | `/config/application-docker.yml` (两个 API) | Spring Boot 配置(只读) |
---
## czleilei240 关键配置对照
| 配置项 | 值 |
|---|---|
| RDS Host | `rm-bp1a178eq62lxba9xbo.mysql.rds.aliyuncs.com` |
| DB / User | `yangtangyoupin` |
| crmeb.imagePath容器内 | `/usr/local/crmeb/crmebimage/` |
| SYNC_SOURCE_ID | `shop_15` |
| SYNC_TARGET_MER_ID | `15` |
| Spring profile | `docker`application-docker.yml 通过 env 注入) |
---
## 备注
- JVM 参数已包含 Java 17 + Spring Boot 2.2.6 所需的 `--add-opens` 标志(见 Dockerfile
- 图片/PDF 目录 `/www/wwwroot/leilei.czchunfang.com/crmebimage/` 同时挂入 `front-api``admin-api` 两个容器,确保文件共享。
- Redis 实例(`single-redis`)与步骤一(`integral-redis`)完全独立,数据互不干扰。

View File

@@ -0,0 +1,549 @@
# 宝应博森元 miao80 用户数据清理方案
## 基本信息
- 公司:宝应博森元
- Spring profile`miao80`
- MySQL`123.56.214.80:3306`
- 数据库:`yangtangyoupin`
- 任务日期2026-06-04
- 方案目的:删除指定会员及其寄卖、订单、奖金、积分、地址、提现等关联数据。
## 数据来源
1. Excel 清除名单:`/Users/mac/Works26/miao-july/bosenyuan清除名单260604.xlsx`
2. 手工补充名单:此前核查出的 30 个有隐藏未售寄卖商品的会员。
## 清理范围
### Excel 清除名单
| 用户ID | 昵称 | 联系方式 | 上级ID |
| --- | --- | --- | --- |
| 93320 | 崔龙云 | 18921916448 | 93315 |
| 93317 | 杨长霞 | 13852763959 | 93212 |
| 93312 | 成成 | 13905238633 | 93296 |
| 93311 | 袁月 | 18932311595 | 93303 |
| 93310 | 范明中 | 18915115858 | 93303 |
| 93308 | 138****0239 | 13813100239 | 93248 |
| 93304 | 卢红 | 13905235316 | 93303 |
| 93302 | 周顺芹 | 13511707356 | 93250 |
| 93298 | 顾善道 | 15005253578 | 93120 |
| 93293 | 朱梅芳 | 13801441506 | 93136 |
| 93292 | 蔡宛序 | 17625471367 | 93213 |
| 93291 | 130****3522 | 13092023522 | 93254 |
| 93288 | 于桂平 | 13305239833 | 93249 |
| 93286 | 陆哓明 | 13952753228 | 93250 |
| 93285 | 仲妮妮 | 17351389708 | 93212 |
| 93282 | 张伏顺 | 13016592366 | 93254 |
| 93281 | 189****9338 | 18951269338 | 93213 |
| 93280 | 韩国林 | 15861304478 | 93248 |
| 93278 | 陈文青 | 15062891641 | 92775 |
| 93277 | 许良会 | 18260691419 | 93239 |
| 93275 | 黄丽辉 | 15262265025 | 93147 |
| 93269 | 吴发 | 18360337533 | 92687 |
| 93268 | 马猛宏 | 13921912348 | 93250 |
| 93267 | 卢玉凤 | 17558780051 | 93212 |
| 93266 | 周同 | 13115254379 | 93265 |
| 93265 | 王彬 | 18915131699 | 93254 |
| 93263 | 陈斌 | 18012324636 | 93019 |
| 93262 | 徐猫猫 | 13390629258 | 93161 |
| 93261 | 潘年庆 | 13773341597 | 93251 |
| 93260 | 纪开东 | 13179766726 | 93251 |
| 93256 | 卢玉芬 | 18012323088 | 93161 |
| 93253 | 刘加良 | 15298672227 | 93239 |
| 93252 | 夏宝华 | 15262250561 | 93214 |
| 93241 | 孙万万 | 13951441675 | 93214 |
| 93238 | 戴向英 | 15189893800 | 93136 |
| 93233 | 严登文 | 15995116338 | 93214 |
| 93228 | 苗永粉 | 13773321176 | 92688 |
| 93217 | 邹华 | 18796681777 | 93206 |
| 93200 | 戴增中 | 13813104768 | 93193 |
| 93194 | 姚焕桂 | 13773330344 | 93075 |
| 93190 | 姜红爱 | 17715862910 | 93075 |
| 93160 | 季安红 | 13913432863 | 93107 |
| 93153 | 王祥 | 15380316138 | 92973 |
| 93143 | 郑娟 | 17751330452 | 93075 |
| 93142 | 李德荣 | 13813104919 | 93109 |
### 手工补充名单
| 用户ID | 昵称 | 联系方式 | 待清理未售商品数 |
| --- | --- | --- | --- |
| 92801 | 成宏梅 | 18751483086 | 3 |
| 93011 | 李迎春 | 13505270568 | 3 |
| 93032 | 沈宝军 | 13348149448 | 2 |
| 93073 | 陈海霞 | 15152714200 | 1 |
| 93078 | 顾晓燕 | 13151600166 | 1 |
| 93120 | 于秀梅 | 13348140510 | 1 |
| 93136 | 戴玉山 | 13813100018 | 1 |
| 93147 | 陈小燕 | 18066016798 | 1 |
| 93155 | 王锐 | 18952581561 | 1 |
| 93185 | 王学梅 | 15150886020 | 1 |
| 93193 | 相荣 | 18796692299 | 1 |
| 93216 | 张萍 | 18252750442 | 1 |
| 93218 | 李润芝 | 15050708588 | 1 |
| 93248 | 董鲜 | 19533096227 | 1 |
| 93284 | 仇云 | 17317753117 | 1 |
| 93287 | 王珏 | 19741771099 | 1 |
| 93290 | 段玉香 | 17751370387 | 1 |
| 93295 | 季爱玲 | 18932366911 | 1 |
| 93297 | 蔡先生 | 13952533248 | 1 |
| 93301 | 毛天梅 | 18012327099 | 1 |
| 93305 | 王素琴 | 13092025465 | 1 |
| 93306 | 殷先生 | 15262252218 | 1 |
| 93307 | 董先生 | 15252737658 | 1 |
| 93315 | 朱继英 | 15800764854 | 1 |
| 93318 | 王琴 | 15150881748 | 1 |
| 93321 | 爱之香 | 15951439818 | 1 |
| 93322 | 邓学美 | 13505254585 | 1 |
| 93323 | 梁鹤贵 | 15050701288 | 1 |
| 93325 | 李杰 | 15050708139 | 1 |
| 93327 | 刘正娟 | 15062896288 | 1 |
### 合并后用户ID
- Excel45 人
- 手工补充30 人
- 重叠0 人
- 合并后75 人
```sql
SET @cleanup_user_ids = '92801,93011,93032,93073,93078,93120,93136,93142,93143,93147,93153,93155,93160,93185,93190,93193,93194,93200,93216,93217,93218,93228,93233,93238,93241,93248,93252,93253,93256,93260,93261,93262,93263,93265,93266,93267,93268,93269,93275,93277,93278,93280,93281,93282,93284,93285,93286,93287,93288,93290,93291,93292,93293,93295,93297,93298,93301,93302,93304,93305,93306,93307,93308,93310,93311,93312,93315,93317,93318,93320,93321,93322,93323,93325,93327';
```
实际执行建议使用临时表承载 ID
```sql
DROP TEMPORARY TABLE IF EXISTS tmp_bosenyuan_cleanup_users;
CREATE TEMPORARY TABLE tmp_bosenyuan_cleanup_users (
id INT PRIMARY KEY
);
INSERT INTO tmp_bosenyuan_cleanup_users (id) VALUES
(92801),(93011),(93032),(93073),(93078),(93120),(93136),(93142),(93143),(93147),
(93153),(93155),(93160),(93185),(93190),(93193),(93194),(93200),(93216),(93217),
(93218),(93228),(93233),(93238),(93241),(93248),(93252),(93253),(93256),(93260),
(93261),(93262),(93263),(93265),(93266),(93267),(93268),(93269),(93275),(93277),
(93278),(93280),(93281),(93282),(93284),(93285),(93286),(93287),(93288),(93290),
(93291),(93292),(93293),(93295),(93297),(93298),(93301),(93302),(93304),(93305),
(93306),(93307),(93308),(93310),(93311),(93312),(93315),(93317),(93318),(93320),
(93321),(93322),(93323),(93325),(93327);
```
## 当前库只读统计
统计时间2026-06-04`miao80 / yangtangyoupin`
| 表 / 范围 | 命中行数 |
| --- | ---: |
| `wa_users` | 75 |
| `eb_user` | 74 |
| `wa_merchandise` | 1698 |
| `wa_order` seller 或 buyer 命中 | 2609 |
| `wa_selfbonus_log` | 1627 |
| `wa_sharebonus_log` | 1545 |
| `wa_coupon_log` | 198 |
| `wa_withdraw` | 44 |
| `wa_money_log` | 0 |
| `wa_address` | 80 |
| `wa_alipay` | 65 |
| `wa_bank` | 1 |
| `eb_user_integral_record` | 1666 |
| `eb_user_address` | 73 |
| `eb_user_bill` | 47 |
| `eb_user_brokerage_record` | 0 |
| `eb_user_experience_record` | 47 |
| `eb_user_extract` | 0 |
| `eb_user_level` | 0 |
| `eb_user_recharge` | 0 |
| `eb_user_sign` | 0 |
| `eb_user_token` | 0 |
| `eb_user_visit_record` | 12 |
| `eb_store_cart` | 0 |
| `eb_store_coupon_user` | 0 |
| `eb_store_order` | 47 |
| `eb_sms_record` | 0 |
## 特殊检查
### `wa_users` 与 `eb_user` 不一致
`wa_users` 命中 75 人,`eb_user` 命中 74 人。缺失的 `eb_user`
| 用户ID | 昵称 | 联系方式 |
| --- | --- | --- |
| 93305 | 王素琴 | 13092025465 |
### 删除名单外仍指向待删用户的推荐关系
删除前需要决定是否置空为 `0`,或改挂到指定上级。
`wa_users.pid` 外部引用 1 条:
| 用户ID | 昵称 | 联系方式 | 当前上级ID |
| --- | --- | --- | --- |
| 93036 | 赵玉文 | 18091856709 | 92801 |
`eb_user.spread_uid` 外部引用 14 条:
| uid | account | nickname | phone | spread_uid |
| --- | --- | --- | --- | --- |
| 92881 | 18115115512 | 成明强 | 18115115512 | 92801 |
| 92903 | 18036263863 | 徐丹 | 18036263863 | 92801 |
| 92940 | 13773342930 | 朱友华 | 13773342930 | 92801 |
| 92976 | 13773334985 | 何军健 | 13773334985 | 92801 |
| 92983 | 15052568923 | 柴秉丹 | 15052568923 | 92801 |
| 93009 | 13092025465 | 王素琴 | 13092025465 | 92801 |
| 93030 | 15298470085 | 张琴 | 15298470085 | 93011 |
| 93036 | 18091856709 | 赵玉文 | 18091856709 | 92801 |
| 93094 | 15952533800 | 朱鹤峰 | 15952533800 | 92801 |
| 93105 | 18626221249 | 姚春峰 | 18626221249 | 93078 |
| 93133 | 19352900319 | 193****0319 | 19352900319 | 93011 |
| 93138 | 18952533228 | 189****3228 | 18952533228 | 93011 |
| 93146 | 15252502175 | 艾保兄 | 15252502175 | 93011 |
| 93148 | 13601446282 | 许梅 | 13601446282 | 93011 |
建议处理方式:
```sql
UPDATE wa_users
SET pid = 0
WHERE pid IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
AND id NOT IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
UPDATE eb_user
SET spread_uid = 0, spread_time = NULL
WHERE spread_uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
AND uid NOT IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
```
如业务要求保留推荐关系,需要先确认新的承接上级 ID再把上面 SQL 的 `0` 替换为指定 ID。
## 执行前校验 SQL
```sql
SELECT 'wa_users', COUNT(*) FROM wa_users WHERE id IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'eb_user', COUNT(*) FROM eb_user WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'wa_merchandise', COUNT(*) FROM wa_merchandise WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'wa_order_seller_or_buyer', COUNT(*) FROM wa_order WHERE seller_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users) OR buyer_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'wa_selfbonus_log', COUNT(*) FROM wa_selfbonus_log WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'wa_sharebonus_log', COUNT(*) FROM wa_sharebonus_log WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'wa_coupon_log', COUNT(*) FROM wa_coupon_log WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'wa_withdraw', COUNT(*) FROM wa_withdraw WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'wa_address', COUNT(*) FROM wa_address WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'wa_alipay', COUNT(*) FROM wa_alipay WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'wa_bank', COUNT(*) FROM wa_bank WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'eb_user_integral_record', COUNT(*) FROM eb_user_integral_record WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'eb_user_address', COUNT(*) FROM eb_user_address WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'eb_user_bill', COUNT(*) FROM eb_user_bill WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'eb_user_experience_record', COUNT(*) FROM eb_user_experience_record WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'eb_user_visit_record', COUNT(*) FROM eb_user_visit_record WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'eb_store_order', COUNT(*) FROM eb_store_order WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
```
## 备份方案
执行删除前,先在同库创建带日期后缀的备份表。备份表名建议固定使用本次任务时间戳:`20260604`
```sql
CREATE TABLE IF NOT EXISTS bak_20260604_bsy_wa_users AS
SELECT * FROM wa_users WHERE id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
CREATE TABLE IF NOT EXISTS bak_20260604_bsy_eb_user AS
SELECT * FROM eb_user WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
CREATE TABLE IF NOT EXISTS bak_20260604_bsy_wa_merchandise AS
SELECT * FROM wa_merchandise WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
CREATE TABLE IF NOT EXISTS bak_20260604_bsy_wa_order AS
SELECT * FROM wa_order
WHERE seller_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
OR buyer_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
CREATE TABLE IF NOT EXISTS bak_20260604_bsy_wa_selfbonus_log AS
SELECT * FROM wa_selfbonus_log WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
CREATE TABLE IF NOT EXISTS bak_20260604_bsy_wa_sharebonus_log AS
SELECT * FROM wa_sharebonus_log WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
CREATE TABLE IF NOT EXISTS bak_20260604_bsy_wa_coupon_log AS
SELECT * FROM wa_coupon_log WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
CREATE TABLE IF NOT EXISTS bak_20260604_bsy_wa_withdraw AS
SELECT * FROM wa_withdraw WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
CREATE TABLE IF NOT EXISTS bak_20260604_bsy_wa_address AS
SELECT * FROM wa_address WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
CREATE TABLE IF NOT EXISTS bak_20260604_bsy_wa_alipay AS
SELECT * FROM wa_alipay WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
CREATE TABLE IF NOT EXISTS bak_20260604_bsy_wa_bank AS
SELECT * FROM wa_bank WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
CREATE TABLE IF NOT EXISTS bak_20260604_bsy_eb_user_integral_record AS
SELECT * FROM eb_user_integral_record WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
CREATE TABLE IF NOT EXISTS bak_20260604_bsy_eb_user_address AS
SELECT * FROM eb_user_address WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
CREATE TABLE IF NOT EXISTS bak_20260604_bsy_eb_user_bill AS
SELECT * FROM eb_user_bill WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
CREATE TABLE IF NOT EXISTS bak_20260604_bsy_eb_user_experience_record AS
SELECT * FROM eb_user_experience_record WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
CREATE TABLE IF NOT EXISTS bak_20260604_bsy_eb_user_visit_record AS
SELECT * FROM eb_user_visit_record WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
CREATE TABLE IF NOT EXISTS bak_20260604_bsy_eb_store_order AS
SELECT * FROM eb_store_order WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
```
## 删除执行方案
建议用事务执行;执行前确认业务低峰,并暂停相关同步任务或后台定时任务。
```sql
START TRANSACTION;
-- 1. 先解除删除名单外用户对待删用户的推荐引用
UPDATE wa_users
SET pid = 0
WHERE pid IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
AND id NOT IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
UPDATE eb_user
SET spread_uid = 0, spread_time = NULL
WHERE spread_uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
AND uid NOT IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
-- 2. 删除寄卖业务关联数据
DELETE FROM wa_order
WHERE seller_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
OR buyer_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM wa_merchandise
WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM wa_selfbonus_log
WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM wa_sharebonus_log
WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM wa_coupon_log
WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM wa_withdraw
WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM wa_money_log
WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM wa_address
WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM wa_alipay
WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM wa_bank
WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
-- 3. 删除积分商城用户关联数据
DELETE FROM eb_user_integral_record
WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM eb_user_address
WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM eb_user_bill
WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM eb_user_brokerage_record
WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM eb_user_experience_record
WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM eb_user_extract
WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM eb_user_level
WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM eb_user_recharge
WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM eb_user_sign
WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM eb_user_token
WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM eb_user_visit_record
WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM eb_store_cart
WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM eb_store_coupon_user
WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM eb_store_order
WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM eb_sms_record
WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
-- 4. 最后删除用户主表
DELETE FROM eb_user
WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
DELETE FROM wa_users
WHERE id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
-- 5. 事务内复核,确认结果符合预期后再提交
SELECT 'wa_users_remaining', COUNT(*) FROM wa_users WHERE id IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'eb_user_remaining', COUNT(*) FROM eb_user WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'wa_merchandise_remaining', COUNT(*) FROM wa_merchandise WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'wa_order_remaining', COUNT(*) FROM wa_order WHERE seller_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users) OR buyer_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'wa_selfbonus_log_remaining', COUNT(*) FROM wa_selfbonus_log WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'wa_sharebonus_log_remaining', COUNT(*) FROM wa_sharebonus_log WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'wa_coupon_log_remaining', COUNT(*) FROM wa_coupon_log WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'eb_user_integral_record_remaining', COUNT(*) FROM eb_user_integral_record WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'external_wa_pid_refs_remaining', COUNT(*) FROM wa_users WHERE pid IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
UNION ALL SELECT 'external_eb_spread_refs_remaining', COUNT(*) FROM eb_user WHERE spread_uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
-- 确认无误后:
COMMIT;
-- 如结果异常:
-- ROLLBACK;
```
## 执行后复核
```sql
SELECT COUNT(*) AS wa_users_remaining
FROM wa_users
WHERE id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
SELECT COUNT(*) AS eb_user_remaining
FROM eb_user
WHERE uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
SELECT COUNT(*) AS merchandise_remaining
FROM wa_merchandise
WHERE user_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
SELECT COUNT(*) AS order_remaining
FROM wa_order
WHERE seller_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users)
OR buyer_id IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
SELECT COUNT(*) AS wa_pid_refs_remaining
FROM wa_users
WHERE pid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
SELECT COUNT(*) AS eb_spread_refs_remaining
FROM eb_user
WHERE spread_uid IN (SELECT id FROM tmp_bosenyuan_cleanup_users);
```
## 回滚思路
如果已经 `COMMIT`,只能通过备份表恢复。恢复时应优先恢复用户主表,再恢复关联表:
1. `wa_users`
2. `eb_user`
3. `wa_*` 业务表
4. `eb_user_*` / `eb_store_*` 关联表
示例:
```sql
INSERT INTO wa_users
SELECT * FROM bak_20260604_bsy_wa_users;
INSERT INTO eb_user
SELECT * FROM bak_20260604_bsy_eb_user;
```
恢复前需要先确认目标主键是否已经被重新占用。
## 注意事项
- 本文档仅为清理方案,尚未执行删除。
- `wa_merchandise` 中手工补充名单此前核查到的 35 条未售商品均为隐藏状态:`status = 1``is_show = 0`
- `wa_order` 删除条件必须同时覆盖 `seller_id``buyer_id`
- 删除主表前必须先处理名单外用户对待删用户的 `pid` / `spread_uid` 引用。
- 执行完成后,如系统使用 Redis 缓存用户、配置或统计数据,需要按线上运维流程清理相关缓存或重启服务。
## 执行结果
- 执行时间2026-06-04
- 执行库:`miao80 / yangtangyoupin`
- 清理 ID 表:`bak_20260604_bsy_cleanup_users`
- 清理用户数75
- 状态:已执行并 `COMMIT`
### 备份表实际行数
| 备份表 | 行数 |
| --- | ---: |
| `bak_20260604_bsy_cleanup_users` | 75 |
| `bak_20260604_bsy_wa_users` | 75 |
| `bak_20260604_bsy_eb_user` | 74 |
| `bak_20260604_bsy_wa_merchandise` | 1698 |
| `bak_20260604_bsy_wa_order` | 2609 |
| `bak_20260604_bsy_wa_selfbonus_log` | 1627 |
| `bak_20260604_bsy_wa_sharebonus_log` | 1545 |
| `bak_20260604_bsy_wa_coupon_log` | 198 |
| `bak_20260604_bsy_wa_withdraw` | 44 |
| `bak_20260604_bsy_wa_address` | 80 |
| `bak_20260604_bsy_wa_alipay` | 65 |
| `bak_20260604_bsy_wa_bank` | 1 |
| `bak_20260604_bsy_wa_money_log` | 0 |
| `bak_20260604_bsy_eb_user_integral_record` | 1666 |
| `bak_20260604_bsy_eb_user_address` | 73 |
| `bak_20260604_bsy_eb_user_bill` | 47 |
| `bak_20260604_bsy_eb_user_brokerage_record` | 0 |
| `bak_20260604_bsy_eb_user_experience_record` | 47 |
| `bak_20260604_bsy_eb_user_extract` | 0 |
| `bak_20260604_bsy_eb_user_level` | 0 |
| `bak_20260604_bsy_eb_user_recharge` | 0 |
| `bak_20260604_bsy_eb_user_sign` | 0 |
| `bak_20260604_bsy_eb_user_token` | 0 |
| `bak_20260604_bsy_eb_user_visit_record` | 12 |
| `bak_20260604_bsy_eb_store_cart` | 0 |
| `bak_20260604_bsy_eb_store_coupon_user` | 0 |
| `bak_20260604_bsy_eb_store_order` | 47 |
| `bak_20260604_bsy_eb_sms_record` | 0 |
### 独立复核结果
| 复核项 | 剩余行数 |
| --- | ---: |
| `wa_users` | 0 |
| `eb_user` | 0 |
| `wa_merchandise` | 0 |
| `wa_order` seller 或 buyer 命中 | 0 |
| `wa_selfbonus_log` | 0 |
| `wa_sharebonus_log` | 0 |
| `wa_coupon_log` | 0 |
| `wa_withdraw` | 0 |
| `wa_address` | 0 |
| `wa_alipay` | 0 |
| `wa_bank` | 0 |
| `eb_user_integral_record` | 0 |
| `eb_user_address` | 0 |
| `eb_user_bill` | 0 |
| `eb_user_experience_record` | 0 |
| `eb_user_visit_record` | 0 |
| `eb_store_order` | 0 |
| 删除名单外 `wa_users.pid` 指向待删用户 | 0 |
| 删除名单外 `eb_user.spread_uid` 指向待删用户 | 0 |
说明:`information_schema.TABLES.TABLE_ROWS` 对 InnoDB 表可能是估算值;本节备份行数以执行时 `COUNT(*)` 输出为准。

View File

@@ -0,0 +1,153 @@
## 公司名称: 宝应桂圣富商贸/鼎信汇商贸
- host ip: 118.31.36.212
## mysql数据库配置信息
datasource:
rds: rm-bp1a178eq62lxba9xbo.mysql.rds.aliyuncs.com
name: bygsf212
username: yangtangyoupin
password: 5Fn8eWrbYFtAhCZw
## 数据清理任务
- **用户数据范围**`wa_users.id` / `eb_user.uid` 保留名单:
`92688, 92880, 92904, 92964, 93098, 93141, 93164, 93235, 93251, 93259, 93267, 93270, 93272, 93273, 93276, 93284, 93292, 93300`
来源核对:
- 博森元团队成员信息表.xlsx / bsy-yangtangyoupin dump`92688, 92904, 92964, 93164, 93251, 93259, 93272, 93273, 93276`
- 金雅文团队成员信息表.xlsx / jyw-yangtangyoupin dump`92880, 93098, 93141, 93235, 93259, 93267, 93270, 93284, 93292, 93300`
- 备注:`93259` 在两份源 dump 中均存在,但对应不同人员;上方保留名单按 `wa_users.id` / `eb_user.uid` 去重后记录。
- 保留wa_users表中id在用户id数据范围的 ,删除其余用户数据
- 保留eb_user表中uid在用户id数据范围的 ,删除其余用户数据
- wa_order
清空wa_order表中数据
- wa_merchandise
从源数据dump文件中提取“created_at >= 2026-06-12”并且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` 过滤。
## 执行结果
- 已于 **2026-06-14** 按当前保留名单执行清理并 `COMMIT`
- 执行脚本:`docs/sql/run_com_bygsf212_cleanup.py`
- 执行前备份:`docs/sql/backups/bygsf212_cleanup_before_20260614_194640.sql.gz`(已通过 `gzip -t` 校验)
- dump 中满足 `wa_merchandise.created_at >= 2026-06-12``user_id` 在保留名单内的记录:
- `bsy-yangtangyoupin_2026-06-14_14-25-01_mysql_data.sql`15 行
- `jyw-yangtangyoupin_2026-06-14_14-55-01_mysql_data.sql`18 行
- 当前目标库实际命中的 `wa_merchandise` 保留记录18 行
- 保留后行数:
- `wa_users`14
- `eb_user`14
- `wa_order`0
- `wa_merchandise`18
- `wa_selfbonus_log`846
- `wa_sharebonus_log`764
- `wa_coupon_log`173
- `wa_withdraw`0
- `eb_store_order`0
- `eb_user_integral_record`870
- 复核:`wa_users``eb_user``wa_selfbonus_log``wa_sharebonus_log``wa_coupon_log``eb_user_integral_record` 均无保留名单外记录。
- 备注:保留名单共 18 个 ID当前目标库仅存在其中 14 个;执行按 `wa_users.id` / `eb_user.uid` 过滤,未从源 dump 导入缺失用户或改写当前库用户身份。
## 博森元团队补充迁移结果
- 已于 **2026-06-14**`bsy-yangtangyoupin_2026-06-14_14-25-01_mysql_data.sql` 补迁博森元团队数据并 `COMMIT`
- 执行脚本:`docs/sql/run_com_bygsf212_bsy_supplement.py`
- 执行前备份:`docs/sql/backups/bygsf212_bsy_supplement_before_20260614_213738.sql.gz`(已通过 `gzip -t` 校验)
- 迁移策略:不覆盖当前目标库已存在的金雅文/当前用户;博森元中 ID 已被占用但手机号不同的用户分配新 `uid` / `wa_users.id`,并同步改写迁移数据中的 `user_id` / `uid` / `pid` / `spread_uid`
- 博森元用户 ID 映射:
- `92688`:李霞 / `18118281551`(沿用原 ID
- `92904`:邓桂花 / `15951431026`(沿用原 ID
- `92964`:王平君 / `18796696663`(沿用原 ID
- `93164`:周爱平 / `15190438222`(沿用原 ID
- `93251 -> 93315`:乔秀勇 / `18136259551`
- `93259 -> 93316`:郑仁风 / `18352718222`
- `93273 -> 93317`:夏辉 / `18936239839`
- `93272 -> 93318`:刘艾平 / `18724108815`
- `93276 -> 93319`:韩玉霞 / `19281861596`
- 本次补迁插入行数:
- `wa_users`9
- `eb_user`9
- `wa_merchandise`14
- `wa_selfbonus_log`673
- `wa_sharebonus_log`861
- `wa_coupon_log`146
- `eb_user_integral_record`679
- 补迁后行数:
- `wa_users`23
- `eb_user`23
- `wa_merchandise`32
- `wa_selfbonus_log`1519
- `wa_sharebonus_log`1625
- `wa_coupon_log`319
- `eb_user_integral_record`1549
- 复核9 个博森元用户均已在 `wa_users` / `eb_user` 中;原冲突 ID 用户(如 `93251` 龚华侨、`93259` 薛春华等)仍保留;`eb_user_integral_record` 无孤儿 `uid`
- 备注:补迁脚本已处理复跑幂等;补迁完成后再次 dry-run 显示所有博森元手机号已存在,插入行数为 0。
## 移除冲突用户结果
- 已于 **2026-06-15** 清除当前库中 `龚华侨``杜紅梅/杜红梅``戴庆宏``陈晓平` 4 个用户相关数据并 `COMMIT`
- 清除用户:
- `93251`:龚华侨 / `15952530725`
- `93272`:杜紅梅 / `13952547832`
- `93273`:戴庆宏 / `15000637090`
- `93276`:陈晓平 / `15995103126`
- 执行脚本:`docs/sql/run_com_bygsf212_remove_conflict_users.py`
- 执行前备份:`docs/sql/backups/bygsf212_remove_93251_93272_93273_93276_before_20260615_085155.sql.gz`(已通过 `gzip -t` 校验)
- 本次删除行数:
- `wa_users`4
- `eb_user`4
- `wa_selfbonus_log`31
- `wa_sharebonus_log`14
- `wa_address`4
- `wa_alipay`4
- `eb_user_address`4
- `eb_user_bill`2
- `eb_user_experience_record`2
- `eb_user_integral_record`33
- `eb_user_visit_record`2
- 未命中需要清除的数据:`wa_order``wa_merchandise``wa_coupon_log``wa_withdraw``eb_store_order` 等为 0 行;无外部 `wa_users.pid` / `eb_user.spread_uid` 引用需要改挂。
- 清除后复核:
- 上述 4 个 `uid` 与手机号在 `wa_users` / `eb_user` 中均不存在。
- 相关日志、地址、积分、访问记录表中均无这 4 个 `uid` 残留。
- 博森元补迁后分配的新用户 `93315` 乔秀勇、`93316` 郑仁风、`93317` 夏辉、`93318` 刘艾平、`93319` 韩玉霞仍存在。
- 清除后核心表行数:
- `wa_users`19
- `eb_user`19
- `wa_merchandise`32
- `wa_selfbonus_log`1488
- `wa_sharebonus_log`1611
- `wa_coupon_log`319
- `eb_user_integral_record`1516
## 相关文件
- 源数据 dump
- `/Users/mac/Works26/miao-july/宝应鼎信汇/bsy-yangtangyoupin_2026-06-14_14-25-01_mysql_data.sql`
- `/Users/mac/Works26/miao-july/宝应鼎信汇/jyw-yangtangyoupin_2026-06-14_14-55-01_mysql_data.sql`
- 团队成员信息:
- `/Users/mac/Works26/miao-july/宝应鼎信汇/博森元团队成员信息表.xlsx`
- `/Users/mac/Works26/miao-july/宝应鼎信汇/金雅文团队成员信息表.xlsx`

65
docs/com-bygsf212.md Normal file
View File

@@ -0,0 +1,65 @@
## 公司名称: 宝应桂圣富商贸/鼎信汇商贸
- host ip: 118.31.36.212
### **修改任务**
- 新建分支bygsf212 ,合并分支"byhlc112"的最新代码到该分支,并根据上述信息修改相关需要变更项,使符合该新公司项目环境
- 在新建分支下修改
---
### 相关配置
- mysql数据库使用阿里云rdsrm-bp1a178eq62lxba9xbo.mysql.rds.aliyuncs.com
- mysql rds中数据库名bygsf212
- 积分商城地址https://jf.b3y45.com
- **云服务器积分商城目录**/www/wwwroot/jf.b3y45.com
- **云服务器jar存放目录**/www/wwwroot/javaapi
---
### backend/crmeb-front模块变更
- 1. profile: bygsf212
- 2. profile file: application-bygsf212.yml, mysql连接信息修改redis主机ip修改。
- 3. **PDF合同模板文件路径**pdf/sign_contract_bygsf212.pdf
- 4. 用户PDF合同url地址前缀/落库域名https://b3y45.com/
- 5. imagePath: /www/wwwroot/b3y45.com/
### uniapp前端配置变更
- 1. 积分商城domainhttps://jf.b3y45.com
- 2. 抢购页面跳转地址https://b3y45.com
- 3. **PDF合同预览文件路径** /static/sign_contract_bygsf212.pdf
- 4. **手动使用HBuilder编译发布**
---
### backend/crmeb-admin模块变更
- 1. profile: bygsf212
- 2. profile file: application-bygsf212.yml, mysql和redis主机ip修改sync: source-id: shop_18 target-mer-id: 18
### 积分商城后台backend-adminend配置变更
- 1. backend-adminend/.env.development文件中VUE_APP_BASE_API改为https://jf.b3y45.com
- 2. backend-adminend/.env.production文件中VUE_APP_BASE_API改为https://jf.b3y45.com
---
## 相关文件
、、、启动积分商城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
、、、

View File

@@ -0,0 +1,61 @@
# 公司名称:宝应宏煜春商贸
host ip: 39.97.236.112
## mysql数据库配置信息
datasource:
rds: rm-bp1a178eq62lxba9xbo.mysql.rds.aliyuncs.com
name: byhlc112
username: yangtangyoupin
password: 5Fn8eWrbYFtAhCZw
## 数据清理任务
- **数据范围**`wa_users.id` / `eb_user.uid` 保留名单:
`92801, 93011, 93032, 93073, 93076, 93078, 93094, 93120, 93136, 93147, 93155, 93185, 93193, 93216, 93218, 93248, 93284, 93287, 93290, 93295, 93297, 93301, 93305, 93306, 93307, 93315, 93318, 93321, 93322, 93323, 93325, 93327, 93328, 93329`
- 保留wa_users表中id在用户id数据范围的 ,删除其余用户数据
- 保留eb_user表中uid在用户id数据范围的 ,删除其余用户数据
- wa_order
清空wa_order表中数据
- wa_merchandise
从源数据dump文件中提取“created_at >= 2026-05-28”并且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` 过滤。
## 执行结果
- 已于 **2026-05-30** 按当前保留名单执行清理并 `COMMIT`
- 结果:`wa_order``wa_withdraw``eb_store_order` 已清空。
- 保留后行数:
- `wa_users`32
- `eb_user`32
- `wa_selfbonus_log`1285
- `wa_sharebonus_log`1536
- `wa_coupon_log`180
- `eb_user_integral_record`1321
- `wa_merchandise`35
## 相关文件
- 新公司初始会员信息: '/Users/mac/Works26/miao-july/byhlc/新团队成员名单.xlsx'

59
docs/com-byhlc112.md Normal file
View File

@@ -0,0 +1,59 @@
## 公司名称: 宝应宏煜春商贸, host ip: 39.97.236.112
### **修改任务**
- 新建分支byhlc112合并byhlc112分支的最新代码到该分支并根据上述信息修改相关需要变更项使符合该新公司项目环境
- 在新建分支下修改
---
### mysql数据库配置
- mysql数据库使用阿里云rdsrm-bp1a178eq62lxba9xbo.mysql.rds.aliyuncs.com
- rds中项目数据库名byhlc112
---
### backend/crmeb-front模块变更
- 1. profile: byhlc112
- 2. profile file: application-byhlc112.yml, mysql连接信息修改redis主机ip修改。
- 3. **PDF合同模板文件路径**pdf/sign_contract_byhlc112.pdf
- 4. 用户PDF合同url地址前缀/落库域名https://h5y2c.com/
- 5. imagePath: /www/wwwroot/h5y2c.com/
### uniapp前端配置变更
- 1. 积分商城domainhttps://jf.h5y2c.com
- 2. 抢购页面跳转地址https://h5y2c.com
- 3. **PDF合同预览文件路径** /static/sign_contract_byhlc112.pdf
---
### backend/crmeb-admin模块变更
- 1. profile: byhlc112
- 2. profile file: application-byhlc112.yml, mysql和redis主机ip修改sync: source-id: shop_16 target-mer-id: 16
### 积分商城后台backend-adminend配置变更
- 1. backend-adminend/.env.development文件中VUE_APP_BASE_API改为https://jf.h5y2c.com
- 2. backend-adminend/.env.production文件中VUE_APP_BASE_API改为https://jf.h5y2c.com
---
## 相关文件
、、、启动积分商城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
、、、

View File

@@ -0,0 +1,121 @@
# 公司名称:池州春芳商贸
## mysql数据库配置信息
host ip: 121.43.134.82
datasource:
name: yangtangyoupin
username: yangtangyoupin
password: 5Fn8eWrbYFtAhCZw
## 数据清理任务
- **数据范围**:用户 id 集(`eb_user.uid``wa_users.id` 一致92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251
- 说明:之前截图中的 C/D/E 层级用户已与本次 H/K 层级名单按 `uid` 合并,当前范围为唯一 `uid` 集。
| 姓名 | 层级 | 用户ID | 上级ID | 账号 |
| --- | --- | ---: | ---: | --- |
| 徐圣方 | H | 93106 | 93092 | 15249924088 |
| 周祥 | H | 93210 | 93106 | 18095665910 |
| 江玲 | H | 93217 | 93092 | 18156673857 |
| 江燕 | H | 93216 | 93092 | 18056699143 |
| 周松琴 | H | 93248 | 93210 | 13395663023 |
| 黄秀珍 | H | 93185 | 93169 | 13801810233 |
| 袁清弟 | H | 93182 | 93169 | 18016367923 |
| 张本汉 | H | 93205 | 93124 | 13965915911 |
| 吴兆燕 | H | 93204 | 93124 | 13615660918 |
| 吴海根 | H | 93200 | 93171 | 15655976789 |
| 陈巧玲 | H | 93207 | 93124 | 18855619191 |
| 吴美云 | H | 93213 | 93203 | 18056682196 |
| 朱华兵 | H | 93242 | 93203 | 17356611916 |
| 王传 | H | 93251 | 93124 | 18956675871 |
| 汪占云 | H | 93227 | 93167 | 15249909910 |
| 潘运生 | H | 93222 | 93167 | 18656668096 |
| 陆惊蕾 | H | 93119 | 93105 | 15905662876 |
| 徐池英 | H | 93134 | 93119 | 18715478619 |
| 董金权 | H | 93156 | 93119 | 13905663559 |
| 吴可佳 | H | 93173 | 93134 | 15715669883 |
| 薛丽萍 | H | 93187 | 93119 | 18905666240 |
| 陶会丽 | H | 93188 | 93119 | 18056643607 |
| 朱向东 | H | 93196 | 93119 | 18956692810 |
| 李华 | H | 93198 | 93119 | 13865669670 |
| 胡贵宾 | H | 93190 | 93133 | 13866451818 |
| 纪贞凤 | H | 93214 | 93156 | 13856695745 |
| 曹四清 | H | 93220 | 93134 | 15955660777 |
| 舒妍 | H | 93212 | 93187 | 17730497790 |
| 陶丽芳 | H | 93221 | 93188 | 15385460258 |
| 余绍光 | H | 93226 | 93173 | 13956891985 |
| 方云云 | H | 93233 | 93198 | 13965929979 |
| 刘根枝 | H | 93235 | 93190 | 17756625293 |
| 汪能学 | H | 93245 | 93119 | 13965941820 |
| 何小伍 | H | 93240 | 93119 | 13093492768 |
| 王芳 | H | 93132 | 93119 | 15956625685 |
| 章旭东 | H | 93166 | 93132 | 13365666600 |
| 张乔林 | H | 93183 | 93132 | 18156617167 |
| 许恩情 | H | 93172 | 93132 | 15856641370 |
| 芮爱梅 | H | 93197 | 93132 | 13905669269 |
| 宁东梅 | H | 93195 | 93132 | 18056647260 |
| 王珍 | H | 93193 | 93132 | 13355668168 |
| 胡春曲 | H | 93209 | 93172 | 15856637686 |
| 郑雁 | H | 93211 | 93132 | 18956692260 |
| 汤亚丽 | H | 93218 | 93193 | 18056635288 |
| 金花鸡 | H | 93215 | 93193 | 18005663869 |
| 宁美琴 | H | 93224 | 93195 | 13645665392 |
| 张晓玲 | H | 93239 | 93183 | 18256624325 |
| 杨艳霞 | H | 93230 | 93215 | 18856611108 |
| 邓本琼 | H | 93133 | 93119 | 13868989499 |
| 张丽 | H | 93163 | 93133 | 15805662996 |
| 洪安荣 | H | 93191 | 93133 | 18005666645 |
| 章丽君 | H | 93225 | 93163 | 13645665392 |
| 侯美枝 | H | 93199 | 93133 | 19855762520 |
| 胡红琴 | K | 93171 | 93105 | 15212465866 |
| 钟真发 | K | 93208 | 93171 | 15305599997 |
| 洪军 | K | 93231 | 93208 | 18955917212 |
| 王兵启 | K | 92827 | 93195 | 18956691177 |
| 胡晓彩 | K | 92738 | 92544 | 17756627812 |
| 柯美燕 | K | 93140 | 92827 | 13635661721 |
| 王珍华 | K | 93150 | 92738 | 13856372733 |
- 删除用户数据范围内的如下表的相关数据,同时做好数据备份
- wa_users表中id在用户id数据范围的
- eb_user表中uid在用户id数据范围的
- wa_order
- wa_merchandise
- wa_selfbonus_log
- wa_sharebonus_log
- wa_coupon_log
- wa_withdraw
- eb_store_order
- eb_user_integral_record
## 执行脚本
- `docs/sql/com-czcf82-data-delete-0601.sql`
- `docs/sql/com-czcf82-data-delete-0601-2.sql`
## 执行结果
- 已于 2026-06-01 执行完成(首次截图范围 31 人;当前数据范围已按后续截图合并,旧 C/D/E 与新 H/K 以唯一 `uid` 口径汇总)。
- 清理后回查结果:各目标表中对应用户数据均为 0 条。
- 备份表已创建,行数分别为:
- `wa_users_bak_20260601_170641`31
- `eb_user_bak_20260601_170641`31
- `wa_order_bak_20260601_170641`975
- `wa_merchandise_bak_20260601_170641`675
- `wa_selfbonus_log_bak_20260601_170641`818
- `wa_sharebonus_log_bak_20260601_170641`793
- `wa_coupon_log_bak_20260601_170641`194
- `wa_withdraw_bak_20260601_170641`9
- `eb_store_order_bak_20260601_170641`88
- `eb_user_integral_record_bak_20260601_170641`932
- 已于 2026-06-01 完成第二轮清理,新增范围对应数据回查也均为 0 条。
- 第二轮备份表已创建,后缀为 `20260601_210625`
## 相关文件
-

View File

@@ -0,0 +1,80 @@
# 公司名称:宿迁盛泽鑫商贸
- host ip: 59.110.91.202
## mysql数据库配置信息
- datasource:
rds: rm-bp1a178eq62lxba9xbo.mysql.rds.aliyuncs.com
name: sqszx202
username: yangtangyoupin
password: 5Fn8eWrbYFtAhCZw
## 数据清理任务
- **用户数据范围**`wa_users.id` / `eb_user.uid` 保留名单:
`93164, 93133, 93132, 93113, 93216, 93230, 93238, 93258, 93277, 93283, 93284, 93291, 93293, 93308, 93299, 93290, 93280, 93279, 93255, 93252, 93251, 93250, 93249, 93248, 93223, 93221, 93111, 93099, 93263, 93265, 93272, 93270, 93269, 93268, 93267, 93266, 93276, 93278, 93286, 93296, 93309, 93281, 93275, 93271, 93139, 93264, 93247, 93218, 93217, 93194, 93142`
- 保留wa_users表中id在用户id数据范围的 ,删除其余用户数据
- 保留eb_user表中uid在用户id数据范围的 ,删除其余用户数据
- wa_order
清空wa_order表中数据
- wa_merchandise
保留数据库中“created_at >= 2026-06-12”并且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` 过滤。
## 执行结果
- 已于 **2026-06-14** 按当前保留名单执行清理并 `COMMIT`
- 执行脚本:`docs/sql/run_com_sqszx202_cleanup.py`
- 执行前备份:`docs/sql/backups/sqszx202_cleanup_before_20260614_090658.sql.gz`(已通过 `gzip -t` 校验)
- `wa_merchandise` 从源 dump 解析结果dump 中共 3156 行,满足 `created_at >= 2026-06-12``user_id` 在名单内的保留商品为 54 行。
- 删除行数:
- `wa_order`3168
- `wa_withdraw`171
- `eb_store_order`348
- `wa_merchandise`3102
- `wa_selfbonus_log`2255
- `wa_sharebonus_log`2514
- `wa_coupon_log`208
- `eb_user_integral_record`2343
- `eb_user`79
- `wa_users`79
- 保留后行数:
- `wa_order`0
- `wa_withdraw`0
- `eb_store_order`0
- `wa_merchandise`54
- `wa_selfbonus_log`1644
- `wa_sharebonus_log`1791
- `wa_coupon_log`8
- `eb_user_integral_record`1993
- `eb_user`51
- `wa_users`51
- 复核:清理后再次 dry-run以上表剩余待删除行数均为 0。
## 相关文件
- 新公司初始会员信息: '/Users/mac/Works26/miao-july/宿迁盛泽鑫/盛泽鑫团队成员信息表.xlsx'
- 源数据dump文件 '/Users/mac/Works26/miao-july/宿迁盛泽鑫/anpengran-yangtangyoupin_2026-06-14_02-15-02_mysql_data.sql'

64
docs/com-sqszx202.md Normal file
View File

@@ -0,0 +1,64 @@
## 公司名称: 宿迁盛泽鑫商贸
- host ip: 59.110.91.202
### **修改任务**
- 新建分支sqszx202 合并分支byhlc112 的最新代码到该分支,并根据上述信息修改相关需要变更项,使符合该新公司项目环境
- 在新建分支下修改
---
### 相关配置
- mysql数据库使用阿里云rdsrm-bp1a178eq62lxba9xbo.mysql.rds.aliyuncs.com
- rds中项目数据库名sqszx202
- 积分商城地址https://jf.j3s4s5.com
- **云服务器积分商城目录**/www/wwwroot/jf.j3s4s5.com
- **云服务器jar存放目录**/www/wwwroot/javaapi
---
### backend/crmeb-front模块变更
- 1. profile: sqszx202
- 2. profile file: application-sqszx202.yml, mysql连接信息修改redis主机ip修改。
- 3. **PDF合同模板文件路径**pdf/sign_contract_sqszx202.pdf
- 4. 用户PDF合同url地址前缀/落库域名https://j3s4s5.com/
- 5. imagePath: /www/wwwroot/j3s4s5.com/
### uniapp前端配置变更
- 1. 积分商城domainhttps://jf.j3s4s5.com
- 2. 抢购页面跳转地址https://j3s4s5.com
- 3. **PDF合同预览文件路径** /static/sign_contract_sqszx202.pdf
---
### backend/crmeb-admin模块变更
- 1. profile: sqszx202
- 2. profile file: application-sqszx202.yml, mysql和redis主机ip修改sync: source-id: shop_17 target-mer-id: 17
### 积分商城后台backend-adminend配置变更
- 1. backend-adminend/.env.development文件中VUE_APP_BASE_API改为https://jf.j3s4s5.com
- 2. backend-adminend/.env.production文件中VUE_APP_BASE_API改为https://jf.j3s4s5.com
---
## 相关文件
、、、启动积分商城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
、、、

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -1,47 +0,0 @@
# 积分商城front MySQL 远程连接汇总
整理范围:`backend/crmeb-front/src/main/resources/application*.yml`
整理时间2026-05-11
## 口径说明
- 仅汇总 MySQL URL 中 host 为远程地址的配置。
- 所属公司优先按 `docs/company-info-*.md``docs/*data-imgration*.md` 中的公司名称和 host ip 匹配没有明确公司名时按部署文档、域名、profile 或数据库名推断并注明。
- 密码按当前配置文件完整记录。
## 按 MySQL 主机聚合
| MySQL host:port | 关联 profile | 说明 |
| --- | --- | --- |
| `8.140.218.149:3306` | `byjyw149` | 宝应金雅文商贸 |
| `8.136.120.231:3306` | `czc231` | 宝应晨召春商贸 |
| `121.43.134.82:3306` | `czcf82` | 池州春芳商贸 |
| `101.37.101.6:3306` | `czrt6` | 池州瑞棠商贸 |
| `114.55.232.191:3306` | `hapr191` | 淮安鹏然商贸 |
| `106.14.132.80:3306` | `sxsy80` | 太原树英商贸 |
| `39.106.63.33:3306` | `miao33` | 夏盛军商贸 |
| `123.56.214.80:3306` | `miao80` | 宝应博森元 |
| `101.37.253.50:3306` | `miao50` | 上海文锦惠商贸 |
| `101.132.245.153:3306` | `shjjy153` | 上海聚伽源商贸 |
| `182.92.78.159:3306` | `shccd159` | 上海慈初德商贸 |
## 远程 MySQL 配置清单
| Profile | 配置文件 | 所属公司 / 项目 | MySQL host:port | 数据库名 | 用户名 | 密码 | 依据 / 备注 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `byjyw149` | `backend/crmeb-front/src/main/resources/application-byjyw149.yml` | 宝应金雅文商贸 | `8.140.218.149:3306` | `yangtangyoupin` | `yangtangyoupin` | `5Fn8eWrbYFtAhCZw` | `docs/company-info-byjyw149.md``docs/byjyw149-data-imgration.md` |
| `czc231` | `backend/crmeb-front/src/main/resources/application-czc231.yml` | 宝应晨召春商贸 | `8.136.120.231:3306` | `yangtangyoupin` | `yangtangyoupin` | `5Fn8eWrbYFtAhCZw` | `docs/company-czc231-data-imgration.md``docs/company-czc231-integral-imgration.md` |
| `czcf82` | `backend/crmeb-front/src/main/resources/application-czcf82.yml` | 池州春芳商贸 | `121.43.134.82:3306` | `yangtangyoupin` | `yangtangyoupin` | `5Fn8eWrbYFtAhCZw` | `docs/company-info-czcf82.md``docs/company-czcf82-data-imgration.md` |
| `czrt6` | `backend/crmeb-front/src/main/resources/application-czrt6.yml` | 池州瑞棠商贸 | `101.37.101.6:3306` | `yangtangyoupin` | `yangtangyoupin` | `5Fn8eWrbYFtAhCZw` | `docs/company-info-czrt6.md``docs/company-czrt6-data-imgration.md` |
| `hapr191` | `backend/crmeb-front/src/main/resources/application-hapr191.yml` | 淮安鹏然商贸 | `114.55.232.191:3306` | `yangtangyoupin` | `yangtangyoupin` | `5Fn8eWrbYFtAhCZw` | `docs/company-info.md``docs/company-data-imgration.md` |
| `sxsy80` | `backend/crmeb-front/src/main/resources/application-sxsy80.yml` | 太原树英商贸 | `106.14.132.80:3306` | `yangtangyoupin` | `yangtangyoupin` | `5Fn8eWrbYFtAhCZw` | `docs/com-sxsy80.md``docs/com-sxsy80-data-imgration.md` |
| `miao33` | `backend/crmeb-front/src/main/resources/application-miao33.yml` | 夏盛军商贸 | `39.106.63.33:3306` | `yangtangyoupin` | `yangtangyoupin` | `5Fn8eWrbYFtAhCZw` | `docs/com-xsj33-data-imgration.md`;部署文档中也标注 `jfadmin.xiashengjun.com` |
| `miao80` | `backend/crmeb-front/src/main/resources/application-miao80.yml` | 宝应博森元 | `123.56.214.80:3306` | `yangtangyoupin` | `yangtangyoupin` | `5Fn8eWrbYFtAhCZw` | `backend-adminend/DEPLOY.md` 中 by80 示例域名为 `jfadmin.bosenyuan.com``.cursor/plans/bybsy80范围数据删除_1cf340f6.plan.md` 也指向该 host |
| `miao50` | `backend/crmeb-front/src/main/resources/application-miao50.yml` | 上海文锦惠商贸 | `101.37.253.50:3306` | `yangtangyoupin` | `yangtangyoupin` | `5Fn8eWrbYFtAhCZw` | `backend/DEPLOY.md`、OpenClaw 配置文档中标注为生产环境 |
| `shjjy153` | `backend/crmeb-front/src/main/resources/application-shjjy153.yml` | 上海聚伽源商贸 | `101.132.245.153:3306` | `yangtangyoupin` | `yangtangyoupin` | `5Fn8eWrbYFtAhCZw` | `docs/compare-shjjy153-shccd159.md` 标注域名 `jjy-jf.fwxgpt.com``jjy-jfadmin.fwxgpt.com` |
| `shccd159` | `backend/crmeb-front/src/main/resources/application-shccd159.yml` | 上海慈初德商贸 | `182.92.78.159:3306` | `yangtangyoupin` | `yangtangyoupin` | `5Fn8eWrbYFtAhCZw` | `docs/compare-shjjy153-shccd159.md` 标注域名 `ccd-jf.fwxgpt.com``ccd-jfadmin.fwxgpt.com` |

View File

@@ -1,168 +0,0 @@
# 积分模块新增页面 — 功能测试报告 v2
**测试时间:** 2026-03-31
**测试范围:** Coding Plan 交付清单功能验证(静态分析 + 结构检查)
**测试结果:** ✅ 全部通过11/11 项)
---
## T01 — 交付文件存在性检查
| 文件 | 结果 |
|---|:---:|
| `src/layout/EmptyLayout.vue` | ✅ PASS |
| `src/utils/requestNoAuth.js` | ✅ PASS |
| `src/router/modules/integralExternal.js` | ✅ PASS |
| `src/router/index.js`(已注册) | ✅ PASS |
| `src/api/integralExternal.js` | ✅ PASS |
| `src/permission.js`(已修改) | ✅ PASS |
| `src/filters/user.js`(已修改) | ✅ PASS |
| `src/views/integral-external/order/index.vue` | ✅ PASS |
| `src/views/integral-external/user/index.vue` | ✅ PASS |
| `src/views/integral-external/user-integral-detail/index.vue` | ✅ PASS |
| `ExternalIntegralController.java` | ✅ PASS |
**11/11 文件存在**
---
## T02 — permission.js 白名单前缀检查
```js
const whiteList = ['/login', '/auth-redirect'];
const whiteListPrefixes = ['/integral-external'];
// ...
if (whiteList.indexOf(to.path) !== -1
|| whiteListPrefixes.some(prefix => to.path.startsWith(prefix))) {
next();
}
```
-`whiteListPrefixes` 已定义并包含 `/integral-external`
- ✅ 使用 `startsWith` 前缀匹配(支持所有子路径)
---
## T03 — router/index.js 注册检查
-`import integralExternalRouter from './modules/integralExternal'` 已添加
-`integralExternalRouter` 已加入 `constantRoutes`
---
## T04 — 新页面无权限指令检查
| 页面 | v-hasPermi | checkPermi |
|---|:---:|:---:|
| order/index.vue | ✅ 无 | ✅ 无 |
| user/index.vue | ✅ 无 | ✅ 无 |
| user-integral-detail/index.vue | ✅ 无 | ✅ 无 |
**三个页面均不含任何权限指令,符合免认证要求。**
---
## T05 — phoneDesensitize 过滤器链路
1.`filters/user.js` 导出 `phoneDesensitize` 函数
2.`filters/index.js` 通过 `export * from './user'` 自动 re-export
3.`main.js` 通过 `Object.keys(filters).forEach` 全局注册所有过滤器
4.`user/index.vue` 正确使用 `{{ scope.row.phone | phoneDesensitize }}`
---
## T06 — API 函数与后端路径一致性
| API 函数 | 前端 URL | HTTP 方法 |
|---|---|:---:|
| `getExternalOrderList` | `external/integral/order/list` | GET |
| `getExternalUserList` | `external/integral/user/list` | GET |
| `getExternalIntegralLog` | `external/integral/log/list` | POST |
所有 URL 与 `ExternalIntegralController` 中的映射路径完全一致。
---
## T07 — 文件语法结构检查
| 文件 | template | script | name 属性 | 括号平衡 |
|---|:---:|:---:|:---:|:---:|
| EmptyLayout.vue | ✅ | ✅ | ✅ | ✅ |
| order/index.vue | ✅ | ✅ | ✅ | ✅ |
| user/index.vue | ✅ | ✅ | ✅ | ✅ |
| user-integral-detail/index.vue | ✅ | ✅ | ✅ | ✅ |
---
## T08 — 路由路径一致性
| 路由定义(子路径) | 完整路径 | 跳转来源 |
|---|---|---|
| `order` | `/integral-external/order` | 默认 redirect |
| `user` | `/integral-external/user` | — |
| `user/integral-detail` | `/integral-external/user/integral-detail` | user/index.vue `$router.push` |
-`user/index.vue` 导航路径 `/integral-external/user/integral-detail` 与路由定义一致
---
## T09 — EmptyLayout 引用链
-`integralExternal.js` 动态引入 `EmptyLayout`
-`EmptyLayout.vue` 包含 `<router-view />`(子页面正确渲染)
---
## T10 — requestNoAuth 免认证验证
-`api/integralExternal.js` 使用 `requestNoAuth` 实例(非 `request`
-`requestNoAuth.js` 请求拦截器中**无**任何 `Authorization` Header 注入逻辑
-`requestNoAuth.js` 响应拦截器中**无** 401 重定向到登录页逻辑
---
## T11 — 后端 Java 检查
| 检查项 | 结果 |
|---|:---:|
| `@RestController` 注解 | ✅ PASS |
| `@RequestMapping("api/external/integral")` | ✅ PASS |
| `/order/list``@GetMapping` | ✅ PASS与前端 GET 一致) |
| `/user/list``@GetMapping` | ✅ PASS与前端 GET 一致) |
| `/log/list``@PostMapping` | ✅ PASS与前端 POST 一致) |
| **无 `@PreAuthorize`** | ✅ PASS |
| `WebSecurityConfig` permitAll 白名单 | ✅ PASS |
---
## 汇总
| 测试项 | 通过 | 失败 |
|---|:---:|:---:|
| T01 文件存在性11项 | 11 | 0 |
| T02 路由白名单前缀 | 1 | 0 |
| T03 路由注册 | 1 | 0 |
| T04 无权限指令3页 | 3 | 0 |
| T05 过滤器链路4环节 | 4 | 0 |
| T06 API 路径一致性3接口 | 3 | 0 |
| T07 文件语法结构4文件 | 4 | 0 |
| T08 路由路径一致性 | 1 | 0 |
| T09 EmptyLayout 引用链 | 2 | 0 |
| T10 免认证验证3项 | 3 | 0 |
| T11 后端 Java7项 | 7 | 0 |
| **合计** | **40** | **0** |
> ✅ **40/40 全部通过** — 交付物满足 Coding Plan 所有功能需求,可进入联调阶段。
---
## 待联调验证(需运行环境)
以下项目需在实际启动前后端后验证:
- [ ] 浏览器访问 `/integral-external/order` 不跳转登录页
- [ ] 订单列表数据正确渲染(含商品图片)
- [ ] 用户列表手机号脱敏显示138\*\*\*\*5678
- [ ] 点击"查看积分明细"正确传参 uid 并跳转
- [ ] 积分明细页概览卡片显示正确的积分 & 个人奖金
- [ ] 返回按钮回到用户积分列表

View File

@@ -1,169 +0,0 @@
# 积分模块新增页面 — 测试报告
> 执行时间2026-03-30
> 测试类型:静态代码分析(新增页面尚未开发,针对现有代码库做预检)
> 测试依据integral-pages-coding-plan.md § 8 测试方案
---
## 总体结论
| 维度 | 状态 | 说明 |
|------|------|------|
| 新增页面文件 | ❌ 未创建 | 三个新页面均未开发,开发尚未启动 |
| 免登录基础设施 | ❌ 未实现 | `permission.js` / `EmptyLayout` / `requestNoAuth` 均未修改 |
| 参考页面可裁剪性 | ✅ 可行 | 原页面结构清晰,具备裁剪条件 |
| 后端接口认证机制 | ⚠️ 有阻塞 | 积分接口有 `@PreAuthorize` 强认证,需后端配合新增免认证路径 |
---
## A 组:免登录访问测试
> 前提:`EmptyLayout.vue` / `requestNoAuth.js` / 路由 / `permission.js` 白名单均**尚未修改**
| 编号 | 测试场景 | 结果 | 详情 |
|------|---------|------|------|
| A-01 | 无 token 访问积分订单页 | ❌ **FAIL** | `permission.js` 白名单仅含 `['/login', '/auth-redirect']`,精确 `indexOf` 匹配,`/integral-external/order` 会被重定向至 `/login` |
| A-02 | 无 token 访问用户积分页 | ❌ **FAIL** | 同 A-01无对应白名单条目 |
| A-03 | 无 token 访问积分明细页 | ❌ **FAIL** | 同 A-01 |
| A-04 | 免登录页面不影响原有认证 | ✅ **PASS** | 原有 `/order/index` 等路径未做变更,仍需登录 |
| A-05 | 已登录用户访问免登录页面 | ⏭️ **SKIP** | 新页面路由未注册,无法访问 |
**A 组结论**:需在 `permission.js` 第 21 行修改白名单,并将第 59 行 `indexOf` 改为 `startsWith` 前缀匹配。
**修改方案**
```js
// permission.js 第 21 行
const whiteList = ['/login', '/auth-redirect', '/integral-external'];
// 第 59 行
if (whiteList.some(path => to.path.startsWith(path))) {
```
---
## B 组:积分订单页面测试
> 参考文件:`src/views/order/index.vue`1182 行)
| 编号 | 测试场景 | 结果 | 详情 |
|------|---------|------|------|
| B-01 | 默认加载 | ⏭️ **SKIP** | 页面未创建 |
| B-02 | 按订单状态筛选 | ⏭️ **SKIP** | 页面未创建 |
| B-03 | 按时间范围筛选 | ⏭️ **SKIP** | 页面未创建 |
| B-04 | 按订单号搜索 | ⏭️ **SKIP** | 页面未创建 |
| B-05 | 重置筛选条件 | ⏭️ **SKIP** | 页面未创建 |
| B-06 | 分页切换 | ⏭️ **SKIP** | 页面未创建 |
| B-07 | 空数据状态 | ⏭️ **SKIP** | 页面未创建 |
| B-08 | 无操作列 | ⚠️ **PRE-CHECK** | 原页面含 **11 处** `v-hasPermi``发货/退款/出库` 操作按钮、导出功能,裁剪时需逐一清理 |
**B 组预检发现**
- `v-hasPermi` 出现 11 次,需全部移除
- 导出按钮在第 79 行:`<el-button @click="exports" v-hasPermi="['admin:export:excel:order']">导出</el-button>`
- `exports()` 方法在第 896 行,需连同方法一起删除
- 原页面**无 Vuex store 直接依赖**,裁剪负担较轻
---
## C 组:用户积分页面测试
> 参考文件:`src/views/user/list/index.vue`1079 行)
| 编号 | 测试场景 | 结果 | 详情 |
|------|---------|------|------|
| C-01 | 默认加载 | ⏭️ **SKIP** | 页面未创建 |
| C-02 | wa_users 字段展示 | ⏭️ **SKIP** | 页面未创建 |
| C-03 | 积分字段来源验证 | ⚠️ **PRE-CHECK** | `integral` 字段已在原 `user/list` 表格中(第 227 行),`eb_user.integral` 字段存在(`User.java` 第 98 行),来源正确 |
| C-04 | wa_users 无关联数据 | ⚠️ **PRE-CHECK** | admin 端无现成的 wa_users API需前端补充处理空值逻辑 |
| C-05 | 用户搜索 | ⏭️ **SKIP** | 页面未创建 |
| C-06 | 跳转积分明细 | ⏭️ **SKIP** | 页面未创建 |
| C-07 | 分页功能 | ⏭️ **SKIP** | 页面未创建 |
| C-08 | 无权限指令残留 | ⚠️ **PRE-CHECK** | 原页面含 **15 处** `v-hasPermi`,裁剪时均需移除 |
**C 组预检发现**
- `integral` 字段已在原用户列表接口中返回,**无需后端改动**
- admin 端**无独立的 wa_users 查询 API**,需新增或复用 `consignment.js` 中的 `selfBonusLogListApi` 辅助拼合
- 需删除的高级筛选项:等级、分组、标签、国家/省份、消费情况、访问情况、性别、身份(共 8 个筛选项)
---
## D 组:用户积分明细子页面测试
> 参考文件:`src/views/user/integral/index.vue`241 行)
| 编号 | 测试场景 | 结果 | 详情 |
|------|---------|------|------|
| D-01 | 带 uid 参数加载 | ⚠️ **PRE-CHECK** | 原页面 `searchForm.uid` 已存在,只需在 `mounted()``$route.query.uid` 注入即可 |
| D-02 | 概览卡片数据验证 | ⚠️ **PRE-CHECK** | 积分来自 `eb_user.integral` ✅;个人奖金来自 `wa_users.selfBonus`admin 端无现成 API |
| D-03 | 无 uid 参数访问 | ⚠️ **PRE-CHECK** | 原页面无 uid 校验逻辑,需在 `mounted()` 添加 fallback 处理 |
| D-04 | 无效 uid 访问 | ⚠️ **PRE-CHECK** | 后端返回空列表即可,前端需处理空状态显示 |
| D-05 | 时间范围筛选 | ✅ **PRE-PASS** | 原页面已有完整 `DateRangePicker` 实现,直接复用 |
| D-06 | 积分变动显示 | ✅ **PRE-PASS** | 原页面已实现 `type===1` 绿色 `+`、否则红色 `-` 逻辑(第 65-66 行) |
| D-07 | 状态与关联类型 | ✅ **PRE-PASS** | `linkTypeFilter` / `statusFilter` / `statusTypeFilter` 三个方法完整(第 196-223 行) |
| D-08 | 返回按钮 | ⚠️ **PRE-CHECK** | 原页面无返回按钮,需手动添加 |
| D-09 | 分页功能 | ✅ **PRE-PASS** | `[15, 30, 45, 60]` 分页完整实现,直接复用 |
**D 组结论**:参考页面仅 241 行复用度最高5/9 项可直接复用),是三个页面中风险最低的。
---
## E 组:接口与后端认证测试
| 编号 | 测试场景 | 结果 | 详情 |
|------|---------|------|------|
| E-01 | 免认证接口可达性 | ❌ **FAIL** | `UserIntegralController.getList()``@PreAuthorize("hasAuthority('admin:user:integral:list')")`,无 token 必返回 401 |
| E-02 | 原认证接口不受影响 | ✅ **PASS** | 原接口认证逻辑未变动 |
| E-03 | 接口仅读不写 | ✅ **PASS** | 积分 list 接口为 POST 查询,无写操作 |
| E-04 | 大数据量分页 | ⏭️ **SKIP** | 待联调时测试 |
| E-05 | 边界参数 | ⏭️ **SKIP** | 待联调时测试 |
| E-06 | 数据脱敏验证 | ❌ **FAIL** | 当前 admin 接口无脱敏处理,用户手机号明文返回 |
**E 组关键发现**
- 后端 `WebSecurityConfig``permitAll` 白名单**不包含** `/api/admin/user/integral/**`
- 需后端在 `WebSecurityConfig` 第 121 行附近新增:
```java
.antMatchers("/api/admin/user/integral/list").permitAll()
```
或新建 `ExternalIntegralController` 映射至免认证路径
---
## F 组:兼容性与 UI 测试
| 编号 | 测试场景 | 结果 |
|------|---------|------|
| F-01 ~ F-07 | 全部兼容性测试 | ⏭️ **SKIP** — 页面未创建,待开发完成后执行 |
---
## 问题汇总(需在开发中修复)
| 优先级 | 问题 | 影响范围 | 解决方案 |
|--------|------|---------|---------|
| 🔴 P0 | `permission.js` 白名单未更新 | A 组全部 FAIL | 修改白名单为前缀匹配 |
| 🔴 P0 | 后端积分接口有 `@PreAuthorize` 强认证 | E-01 FAIL | 后端新增免认证路径或 controller |
| 🟠 P1 | admin 端无独立 wa_users 查询 API | C-04、D-02 阻塞 | 复用寄卖模块的 `selfBonusLogListApi` 或后端新增聚合接口 |
| 🟠 P1 | 用户手机号无脱敏处理 | E-06 FAIL | 后端接口或前端 filter 处理 `138****8888` |
| 🟡 P2 | 原订单页 11 处权限指令需清理 | B-08 | 开发时逐一删除 |
| 🟡 P2 | 原用户列表页 15 处权限指令需清理 | C-08 | 开发时逐一删除 |
| 🟡 P2 | 积分明细页缺少 uid 空值校验和返回按钮 | D-03、D-08 | 开发时添加 |
---
## 测试覆盖统计
| 组别 | 总用例 | PASS | FAIL | PRE-CHECK | SKIP |
|------|--------|------|------|-----------|------|
| A 组(免登录) | 5 | 1 | 3 | 0 | 1 |
| B 组(订单页) | 8 | 0 | 0 | 1 | 7 |
| C 组(用户积分页) | 8 | 0 | 0 | 3 | 5 |
| D 组(积分明细页) | 9 | 4 | 0 | 5 | 0 |
| E 组(接口) | 6 | 2 | 2 | 0 | 2 |
| F 组(兼容性) | 7 | 0 | 0 | 0 | 7 |
| **合计** | **43** | **7** | **5** | **9** | **22** |
> PASS = 代码层面已满足条件FAIL = 存在明确问题需修复PRE-CHECK = 有条件可实现开发时需注意SKIP = 页面未创建,待开发完成后执行
---
*报告生成时间2026-03-30*

View File

@@ -1,37 +0,0 @@
# 管理后台中积分模块新增如下页面
## 积分订单页面
- **已修复**新建页面,参考原页面:/order/index
## 用户积分页面
- **已修复**新建页面,参考原页面:/user/index增加wa_users的相关字段
### 用户积分明细子页面
- **已修复**一个新建积分明细页面参考原页面“user/index 用户管理-》账户详情-》积分明细”延用原后端api/marketing/integral/integrallog
## 修改
### 积分订单页面修改,路径:/integral-external/order
- **已修复**1. 去除订单类型的选择,默认入参:“普通订单”类型
- **已修复**2. 订单列表中增加“使用积分”列
- **已修复**3. 订单列表中增加用户信息相关列
### 用户积分页面修改,路径:/integral-external/user
- **已修复**列表中个人奖金没有显示出来数据从wa_users表中获取数据
### 用户积分明细页面修改,路径:/integral-external/user/integral-detail
- **已修复**增加用户id用户名称用户手机号输入框作为查询接口参数
- **已修复**没有用户id的时候显示所有的积分明细数据按照id或者时间倒序
- **已修复**关联类型显示中文(含 order/sign/system/selfbonus 及未知值「其他(原值)」)
## 备注
- **已修复**所有新建页面跳过用户登陆状态验证
- **已修复**按照后端api最小修改原则尽量延用原后端api

File diff suppressed because it is too large Load Diff

View File

@@ -1,946 +0,0 @@
---
name: Agent Configuration v3s
overview: 基于本机实际 OpenClaw 环境检查结果修正的配置方案。在现有 1 个 main Agent + 1 个飞书应用的基础上,增量添加 1 个积分商城 PM + 3 个通用开发 Agent不影响已有配置。
todos:
- id: create-feishu-apps
content: 在飞书开放平台创建 4 个机器人应用(或复用现有应用做路由)
status: pending
- id: update-openclaw-json
content: 在现有 openclaw.json 中追加 4 个 Agent、bindings 和飞书账号
status: pending
- id: create-workspaces
content: 创建 4 个 Agent workspace 目录和全套 .md 文件
status: pending
- id: install-skills
content: 安装本地 Skills 和 ClawHub Skills
status: pending
- id: register-and-verify
content: 运行 openclaw doctor 验证配置
status: pending
isProject: false
---
# OpenClaw 多 Agent 配置方案 v3s -- 1 PM + 3 通用开发
> **v3s 核心变更(相对 v3**
>
> 1. 后端/前端/QA 三个 Agent 从"积分商城专用"改为**通用软件开发工程师**,可服务于任何项目
> 2. 仅 PM 保留为积分商城专属项目经理
> 3. Agent ID 重命名:`integral-backend/frontend/qa` → `dev-backend/frontend/qa`
> 4. 项目路径确认为 `/Users/mac/scott-macair-26/integral-shop`
> 5. 通用开发 Agent 的 SOUL.md 移除特定技术栈锁定,改为"按项目要求适配"
---
## 一、实际环境概况
### 1.1 本机 OpenClaw 配置(不可变动)
- **运行环境:** macOSOpenClaw 2026.3.13
- **配置文件:** `/Users/mac/.openclaw/openclaw.json`
- **已有 Agent** 仅 1 个main
- **已有飞书:** 1 个应用(`cli_a930893990799cba`websocket 连接1 条 bindingmain → default
- **模型 provider** moonshotKimi K2.5+ kimi-codingk2p5共用同一 API key
- **默认模型:** `kimi-coding/k2p5`
- **Gateway** 端口 18789local 模式token 鉴权
- **本地 Skills** 0 个(仅飞书插件自带 feishu-doc/drive/perm/wiki
- **Workspace** 1 个共享 workspace默认模板状态
### 1.2 积分商城项目信息
- **项目路径:** `/Users/mac/scott-macair-26/integral-shop`
- **Gitea** `http://49.235.131.69:3000/scottpan/integral-shop.git`
- **子项目:**
- `backend/` → Java Spring Boot 后端Java 1.8 / Spring Boot 2.2.6 / MyBatis Plus 3.3.1 / MySQL 5.7
- `backend-adminend/` → 管理后台 Vue 前端Vue 2.6 / Element UI 2.13
- `single_uniapp22miao/` → 用户端 uni-app H5Vue 3 / uni-app
---
## 二、Agent 角色设计1 专用 PM + 3 通用开发)
**设计理念:** PM 是项目专属的绑定积分商城的需求、PRD、部署流程但开发能力是通用的。3 个开发 Agent 可以同时服务于积分商城和未来的其他项目PM 通过任务分派告诉它们具体的项目上下文。
```mermaid
flowchart TB
User[用户/飞书] -->|积分商城需求| PM["integral-pm (积分商城 PM)"]
User -->|其他项目/通用编码任务| BE["dev-backend (通用后端)"]
User -->|其他项目/通用编码任务| FE["dev-frontend (通用前端)"]
User -->|其他项目/通用编码任务| QA["dev-qa (通用测试)"]
PM -->|后端任务 + 项目上下文| BE
PM -->|前端任务 + 项目上下文| FE
PM -->|测试计划 + 项目上下文| QA
BE -->|API 就绪| FE
BE -->|提测| QA
FE -->|提测| QA
QA -->|Bug 反馈| BE
QA -->|Bug 反馈| FE
QA -->|测试报告| PM
QA -.->|部署申请| PM
PM -.->|部署审批| QA
```
| Agent ID | 角色 | 职责范围 |
| ----------------- | ------------ | --------------------------------------- |
| **integral-pm** | 积分商城项目经理 + 设计 | 积分商城需求拆解、PRD、UI 规范、任务分派、进度跟踪、部署审批 |
| **dev-backend** | 通用后端开发工程师 | 任意项目的后端开发Java/Python/Go/Node 等,按项目要求适配) |
| **dev-frontend** | 通用前端开发工程师 | 任意项目的前端开发Vue/React/uni-app 等,按项目要求适配) |
| **dev-qa** | 通用测试工程师 | 任意项目的功能测试、接口测试、UI 测试、部署执行 |
---
## 三、Agent 间通信协议
### 3.1 通信方式
采用**独立飞书应用方案**(每个 Agent 一个飞书机器人),通过 accountId 路由。
用户可以直接私聊任何开发 Agent 下达通用编码任务;积分商城相关任务则通过 PM 分派。
### 3.2 消息协议格式
PM 分派任务时必须携带项目上下文:
```
【任务分派】<标题>
发送方: integral-pm
接收方: @<dev-agent>
关联任务: <task-id>
项目: 积分商城
项目路径: /Users/mac/scott-macair-26/integral-shop
---
<任务描述>
<技术栈约束>(如有)
<验收标准>
```
开发 Agent 之间、开发与 PM 之间的其他消息类型任务分派、API-就绪、提测通知、Bug-反馈、测试报告、部署申请、部署审批、进度更新。
### 3.3 任务状态机
```
Created → InProgress → CodeReview → Testing → Passed → DeployApproval → Deploying → Done
↓ ↓
BugFound ← ─ ─ ─ ─ ─ ─ ─ ┘
InProgress修复后重新流转
```
---
## 四、openclaw.json 增量修改
**原则:只追加,不修改已有配置。**
### 4.1 在 `agents` 中新增 `list` 字段
当前 `agents` 节点只有 `defaults`,需新增 `list`
```json
"agents": {
"defaults": {
... // 保持不变
},
"list": [
{
"id": "integral-pm",
"name": "integral-pm",
"workspace": "/Users/mac/.openclaw/workspace-integral-pm",
"agentDir": "/Users/mac/.openclaw/agents/integral-pm/agent",
"model": "kimi-coding/k2p5"
},
{
"id": "dev-backend",
"name": "dev-backend",
"workspace": "/Users/mac/.openclaw/workspace-dev-backend",
"agentDir": "/Users/mac/.openclaw/agents/dev-backend/agent",
"model": "kimi-coding/k2p5"
},
{
"id": "dev-frontend",
"name": "dev-frontend",
"workspace": "/Users/mac/.openclaw/workspace-dev-frontend",
"agentDir": "/Users/mac/.openclaw/agents/dev-frontend/agent",
"model": "kimi-coding/k2p5"
},
{
"id": "dev-qa",
"name": "dev-qa",
"workspace": "/Users/mac/.openclaw/workspace-dev-qa",
"agentDir": "/Users/mac/.openclaw/agents/dev-qa/agent",
"model": "kimi-coding/k2p5"
}
]
}
```
### 4.2 在 `bindings` 数组中追加 4 条飞书路由
```json
"bindings": [
{
"agentId": "main",
"match": { "channel": "feishu", "accountId": "default" }
},
{
"agentId": "integral-pm",
"match": { "channel": "feishu", "accountId": "jfshop@macair26" }
},
{
"agentId": "dev-backend",
"match": { "channel": "feishu", "accountId": "dev-backend@macair" }
},
{
"agentId": "dev-frontend",
"match": { "channel": "feishu", "accountId": "dev-frontend@macair" }
},
{
"agentId": "dev-qa",
"match": { "channel": "feishu", "accountId": "dev-qa@macair" }
}
]
```
### 4.3 在 `channels.feishu` 中追加 `accounts`
```json
"channels": {
"feishu": {
"enabled": true,
"appId": "cli_a930893990799cba",
"appSecret": "FfpFz93MKBx0ytC1ceTPF0BnjM7vFVhQ",
"connectionMode": "websocket",
"domain": "feishu",
"groupPolicy": "open",
"dmPolicy": "open",
"allowFrom": ["*"],
"accounts": {
"jfshop@macair26": {
"appId": "cli_a930893990799cba",
"appSecret": "FfpFz93MKBx0ytC1ceTPF0BnjM7vFVhQ",
"agent": "integral-pm",
"dmPolicy": "open",
"allowFrom": ["*"]
},
"dev-backend@macair": {
"appId": "cli_a9316e2a92385bc7",
"appSecret": "t7YyQU1qgqJFiW95HfA1SgnUBdlpx0F1",
"agent": "dev-backend",
"dmPolicy": "open",
"allowFrom": ["*"]
},
"dev-frontend@macair": {
"appId": "cli_a9316ef6f5785bb6",
"appSecret": "dhJ3uAKWtZDzXce25YJ2HXHhw32eBGFR",
"agent": "dev-frontend",
"dmPolicy": "open",
"allowFrom": ["*"]
},
"dev-qa@macair": {
"appId": "cli_a9316f026ebadbc8",
"appSecret": "PHN6UZgU21NGMCW5C6boQckDMFo228un",
"agent": "dev-qa",
"dmPolicy": "open",
"allowFrom": ["*"]
}
}
}
}
```
### 4.4 不变动的部分
`meta``wizard``auth``models``tools``commands``session``gateway``plugins`、main Agent 的 binding 全部保持不变。
---
## 五、双模型架构
| 层 | 用途 | Agent | 模型 |
| -------- | --------- | ---------------------- | ------------------------------- |
| OpenClaw | 飞书对话、任务协调 | 全部 | kimi-coding/k2p5已有 |
| Cursor | 代码编写 | integral-pm | `agent --model claude-4.6-opus` |
| Cursor | 代码编写 | dev-backend/frontend/qa | `agent --model auto` |
---
## 六、Skills 配置
### 6.1 阶段一最小启动集Day 1
仅使用 OpenClaw 内置 Tools
| 内置 Tool | integral-pm | dev-backend | dev-frontend | dev-qa |
| ------------ | :---------: | :---------: | :----------: | :----: |
| git | ● | ● | ● | ● |
| file-manager | ● | ● | ● | ● |
| web-search | ● | ● | ● | ● |
| browser | ● | - | ● | ● |
| code-runner | - | ● | ● | ● |
| http-request | - | ● | - | ● |
| **合计** | **4** | **5** | **5** | **6** |
### 6.2 阶段二:核心 SkillsDay 2-3
```bash
# 搜索 ClawHub 可用 Skill
openclaw skills search gitea
openclaw skills search cursor
openclaw skills search code-review
```
按搜索结果安装 cursor-cli、gitea-tools 等。
### 6.3 阶段三按需引入Week 2+
代码审查、自动化测试、摘要等。
---
## 七、各 Agent Workspace 配置
---
### 1. PM Agent (integral-pm) — 积分商城专属
**workspace 路径:** `/Users/mac/.openclaw/workspace-integral-pm/`
**IDENTITY.md:**
```markdown
# IDENTITY.md
- **Name:** 积分商城PM
- **Creature:** AI 项目经理
- **Vibe:** 结构化、专业、高效
- **Emoji:** 📋
```
**SOUL.md:**
```markdown
# SOUL.md - 积分商城 PM
## 角色定义
积分商城项目的专属项目经理兼 UI 设计指导。
负责积分商城的需求拆解、任务分派、进度跟踪、部署审批。
## 管辖项目
- 项目名称: 单商户积分商城
- 项目路径: /Users/mac/scott-macair-26/integral-shop
- Gitea: http://49.235.131.69:3000/scottpan/integral-shop.git
## 下属 Agent
- dev-backend: 通用后端开发(分派任务时须附带项目上下文和技术栈约束)
- dev-frontend: 通用前端开发(同上)
- dev-qa: 通用测试工程师(同上)
## 沟通风格
- 结构化、简洁、中文为主
- 任务分派必须使用标准消息协议,且包含项目路径和技术栈约束
- 不说废话,直接给结论和下一步行动
## 决策原则
- MVP 优先、增量迭代
- 技术方案交由开发 Agent 决定PM 不干预实现细节
- 部署审批必须确认:测试通过率 ≥ 95%、无 P0 Bug
## 设计输出
以文字描述 + 参考截图形式交付 UI 规范。
管理后台遵循 Element UI 2.13 风格,用户端遵循现有积分商城 H5 风格。
## 积分商城技术栈约束(分派任务时传递给开发 Agent
### 后端
- Java 1.8(禁止 Java 9+、Spring Boot 2.2.6(禁止 3.x、MyBatis Plus 3.3.1、MySQL 5.7(禁止 8.0 特性、Maven 3.6.1、Redis 5.x
### 管理后台前端 (backend-adminend/)
- Vue 2.6(禁止 Vue 3、Element UI 2.13(禁止 Element Plus、Vuex 3.x禁止 Pinia
### 用户端 H5 (single_uniapp22miao/)
- uni-app + Vue 3、微信小程序兼容
## 禁止行为
- 禁止自行修改本文件SOUL.md或 AGENTS.md
```
**AGENTS.md:**
```markdown
# AGENTS.md - PM 工作规范
## Session Startup
1. Read SOUL.md
2. Read USER.md
3. Read memory/YYYY-MM-DD.md今天 + 昨天)
4. Read plans/ 下最新的 PRD
## 工作流
1. 收到需求 → 写 PRD 到 plans/<feature>.md
2. 拆解为子任务 → 写入 tasks/<YYYY-MM-DD>-<feature>-<subtask>.md
3. 通过飞书分别通知 dev-backend / dev-frontend / dev-qa
**重要:** 分派任务时必须附带以下项目上下文:
- 项目路径: /Users/mac/scott-macair-26/integral-shop
- 涉及的子项目: backend/ 或 backend-adminend/ 或 single_uniapp22miao/
- 技术栈约束(从 SOUL.md 的"积分商城技术栈约束"部分复制)
- Git 分支规范和 Gitea 地址
## 任务分派模板
```
【任务分派】<标题>
发送方: integral-pm
接收方: @<dev-agent>
关联任务: <task-id>
项目: 积分商城
项目路径: /Users/mac/scott-macair-26/integral-shop
子项目: <backend | backend-adminend | single_uniapp22miao>
Gitea: http://49.235.131.69:3000/scottpan/integral-shop.git
分支规范: feature/<role>-<name>
---
## 需求描述
<需求正文>
## 技术栈约束
<从 SOUL.md 复制对应子项目的技术栈约束>
## 验收标准
<AC 列表>
```
## 部署审批流程
1. 收到 dev-qa 的【部署申请】
2. 检查:测试报告通过率 ≥ 95%、无 P0 Bug
3. 测试环境by80/ 预发布环境miao33: 直接批准
4. 生产环境miao50: 需 @用户 人工确认后批准
5. 回复【部署审批】消息
## Cursor 使用
- agent --model claude-4.6-opus
- 用途: 需求分析、代码审阅、架构设计
## Memory
- 每日进度汇总到 memory/YYYY-MM-DD.md
```
**TOOLS.md:**
```markdown
# TOOLS.md - PM 环境信息
## 积分商城项目
- 源码路径: /Users/mac/scott-macair-26/integral-shop
- Gitea: http://49.235.131.69:3000/scottpan/integral-shop.git
- 编码工具: Cursor IDE (macOS)
## 子项目结构
- backend/ → Java Spring Boot 后端
- backend-adminend/ → 管理后台 Vue 前端
- single_uniapp22miao/ → 用户端 uni-app H5
## SSH 部署环境
- 部署脚本: backend/shell/deploy-admin-*.sh, deploy-front-*.sh
- 部署配置: backend/deploy.conf
- 环境分级:
- by80: 测试环境PM 审批)
- miao33: 预发布环境PM 审批)
- miao50: 生产环境PM 审批 + 用户确认)
- Admin JAR 远程端口: 30032
- Front JAR 远程端口: 30031
## Cursor CLI
- 模型: agent --model claude-4.6-opus
- 项目目录: /Users/mac/scott-macair-26/integral-shop
## 已启用 Tools
- 内置: git, file-manager, web-search, browser
```
---
### 2. 通用后端开发 (dev-backend)
**workspace 路径:** `/Users/mac/.openclaw/workspace-dev-backend/`
**IDENTITY.md:**
```markdown
# IDENTITY.md
- **Name:** 后端开发
- **Creature:** AI 后端工程师
- **Vibe:** 技术精确、严谨、适应力强
- **Emoji:** ⚙️
```
**SOUL.md:**
```markdown
# SOUL.md - 通用后端开发工程师
## 角色定义
通用后端开发工程师。可服务于任何项目的后端开发工作,不绑定特定项目或技术栈。
## 核心能力
- Java / Spring Boot / MyBatis 生态
- Python / FastAPI / Django
- Node.js / Express / Nest.js
- Go 后端开发
- 数据库设计与优化MySQL / PostgreSQL / MongoDB / Redis
- RESTful API 和 GraphQL 设计
- 微服务架构
## 工作原则
- 接收任务时,严格遵守任务中指定的**技术栈版本约束**
- 如果任务未指定版本,使用项目现有版本,不擅自升级
- 接口变更须提供文档并说明影响范围
- 代码编写在 Cursor IDE 中完成
## 沟通风格
技术精确。变更通知包含:变更接口列表、请求/响应格式变化、影响的前端页面。
## 禁止行为
- 禁止在未获得 PM 或用户明确批准的情况下引入新依赖
- 禁止擅自修改项目配置文件中的端口、数据库连接等关键配置
- 禁止自行修改本文件SOUL.md或 AGENTS.md
```
**AGENTS.md:**
```markdown
# AGENTS.md - 通用后端开发工作规范
## Session Startup
1. Read SOUL.md
2. Read USER.md
3. Read memory/YYYY-MM-DD.md今天 + 昨天)
## 接收任务方式
1. **从 PM 接收**PM 分派的任务包含项目路径、技术栈约束、验收标准,严格按要求执行
2. **从用户直接接收**:用户可以直接私聊下达编码任务,按用户指示执行
## 通用开发流程
1. 阅读任务描述,确认项目路径和技术栈约束
2. 在对应项目目录中创建 feature/<role>-<name> 分支(或按任务指定的分支规范)
3. 在 Cursor 中编码: agent --model auto
4. 完成后通知前端(如有 API 变更)和 QA提测
5. 使用任务指定的消息协议格式发送通知
## 故障恢复
- Cursor CLI 失败: git stash → 记录 memory/errors.md → 通知 PM 或用户
- 构建失败: 分析日志 → 尝试修复 → 3 次失败后上报
## Memory
- 记录各项目的关键信息到 memory/ 下,方便后续会话恢复上下文
```
**TOOLS.md:**
```markdown
# TOOLS.md - 后端开发环境
## 本机环境 (macOS)
- IDE: Cursor
- 可用语言运行时: Java, Python, Node.js, Go按项目需要
## 已知项目
### 积分商城(由 integral-pm 管理)
- 项目路径: /Users/mac/scott-macair-26/integral-shop/backend
- 技术栈: Java 1.8 / Spring Boot 2.2.6 / MyBatis Plus 3.3.1 / MySQL 5.7 / Maven 3.6.1
- 本地运行:
- Admin API: mvn spring-boot:run -pl crmeb-admin (端口 8080)
- Front API: mvn spring-boot:run -pl crmeb-front (端口 8081)
- 打包:
- Admin: mvn clean package -pl crmeb-admin -am -DskipTests
- Front: mvn clean package -pl crmeb-front -am -DskipTests
- 模块: crmeb-admin / crmeb-front / crmeb-service / crmeb-common
- Gitea: http://49.235.131.69:3000/scottpan/integral-shop.git
- 分支规范: feature/backend-<name>, bugfix/backend-<name>
(接手新项目时,在此追加项目信息)
## Cursor CLI
- 模型: agent --model auto
## 已启用 Tools
- 内置: git, file-manager, web-search, code-runner, http-request
```
---
### 3. 通用前端开发 (dev-frontend)
**workspace 路径:** `/Users/mac/.openclaw/workspace-dev-frontend/`
**IDENTITY.md:**
```markdown
# IDENTITY.md
- **Name:** 前端开发
- **Creature:** AI 前端工程师
- **Vibe:** 创意、注重细节、灵活适配
- **Emoji:** 🖥️
```
**SOUL.md:**
```markdown
# SOUL.md - 通用前端开发工程师
## 角色定义
通用前端开发工程师。可服务于任何项目的前端开发工作,不绑定特定项目或技术栈。
## 核心能力
- Vue 2.x / Vue 3.x 全家桶
- React / Next.js
- uni-app / 微信小程序
- Element UI / Ant Design / Tailwind CSS
- TypeScript
- Webpack / Vite 构建工具
- 响应式设计与跨端适配
## 工作原则
- 接收任务时,严格遵守任务中指定的**技术栈版本约束**
- **特别注意**:同一项目可能有多个前端子项目使用不同技术栈(如 Vue 2 管理后台 + Vue 3 用户端),切换时必须确认当前技术栈
- 如果任务未指定版本,使用项目现有版本,不擅自升级
- 代码编写在 Cursor IDE 中完成
## 沟通风格
展示关键代码片段和页面效果说明。
## 禁止行为
- 禁止在未获得 PM 或用户明确批准的情况下引入新 npm 依赖
- 禁止在不同技术栈的子项目间共享组件(可能不兼容)
- 禁止自行修改本文件SOUL.md或 AGENTS.md
```
**AGENTS.md:**
```markdown
# AGENTS.md - 通用前端开发工作规范
## Session Startup
1. Read SOUL.md
2. Read USER.md
3. Read memory/YYYY-MM-DD.md今天 + 昨天)
## 接收任务方式
1. **从 PM 接收**PM 分派的任务包含项目路径、子项目、技术栈约束
2. **从用户直接接收**:用户可直接私聊下达编码任务
## 通用开发流程
1. 阅读任务描述,确认项目路径、子项目和技术栈约束
2. **关键步骤**:确认当前子项目的技术栈版本(避免 Vue 2 项目中写 Vue 3 代码)
3. 创建 feature/frontend-<name> 分支
4. 在 Cursor 中编码: agent --model auto
5. 与后端协作获取 API 文档
6. 完成后通知 QA 提测
## 故障恢复
- 构建失败: 检查 Node 版本和 NODE_OPTIONS 环境变量
- Cursor CLI 失败: git stash → 通知 PM 或用户
```
**TOOLS.md:**
```markdown
# TOOLS.md - 前端开发环境
## 本机环境 (macOS)
- Node.js: 17+
- IDE: Cursor
## 已知项目
### 积分商城(由 integral-pm 管理)
#### 管理后台 (backend-adminend/)
- 路径: /Users/mac/scott-macair-26/integral-shop/backend-adminend
- 技术栈: Vue 2.6 / Element UI 2.13 / Vuex 3.x / Vue Router 3.x
- 开发: npm run dev端口 9527
- 构建: npm run build:prod → dist/
- 注意: Node 17+ 需 export NODE_OPTIONS="--openssl-legacy-provider"
#### 用户端 H5 (single_uniapp22miao/)
- 路径: /Users/mac/scott-macair-26/integral-shop/single_uniapp22miao
- 技术栈: uni-app + Vue 3、微信小程序兼容
- 配置: config/app.jsAPI 基地址)
- 开发: npm run dev:h5
- 构建: npm run build:h5 → unpackage/dist/build/h5/
- Gitea: http://49.235.131.69:3000/scottpan/integral-shop.git
- 分支规范: feature/frontend-<name>, bugfix/frontend-<name>
(接手新项目时,在此追加项目信息)
## Cursor CLI
- 模型: agent --model auto
## 已启用 Tools
- 内置: git, file-manager, web-search, code-runner, browser
```
---
### 4. 通用测试工程师 (dev-qa)
**workspace 路径:** `/Users/mac/.openclaw/workspace-dev-qa/`
**IDENTITY.md:**
```markdown
# IDENTITY.md
- **Name:** 测试工程师
- **Creature:** AI QA 工程师
- **Vibe:** 严谨、细致、不放过任何 Bug
- **Emoji:** 🧪
```
**SOUL.md:**
```markdown
# SOUL.md - 通用测试工程师
## 角色定义
通用 QA 测试工程师 + 部署执行。可服务于任何项目的测试和部署工作。
## 核心能力
- 功能测试、接口测试、UI 测试、回归测试
- SSH 部署执行与验证
- 测试用例编写
- Bug 分析与根因定位(只读分析,不修改源码)
## 工作原则
- 部署操作必须走 PM 审批流程(有 PM 管理的项目)
- 用户直接下达的部署任务可直接执行
- 生产环境部署始终需要用户人工确认
## Bug 描述规范
1. 复现步骤(精确到操作路径)
2. 期望结果
3. 实际结果
4. 截图/日志
5. 影响范围评估P0-P2
## 禁止行为
- 禁止修改源代码(只报 Bug不自行修复
- 禁止自行修改本文件SOUL.md或 AGENTS.md
```
**AGENTS.md:**
```markdown
# AGENTS.md - 通用 QA 工作规范
## Session Startup
1. Read SOUL.md
2. Read USER.md
3. Read memory/YYYY-MM-DD.md今天 + 昨天)
## 接收任务方式
1. **从 PM 接收**PM 分派的任务包含项目上下文、测试范围
2. **从用户/开发 Agent 接收**:提测通知或直接测试任务
## 通用测试流程
1. 阅读任务描述和 API 文档
2. 编写测试用例: tasks/test-<project>-<YYYY-MM-DD>-<feature>.md
3. 执行测试:
- 后端 API: http-request 工具调用接口
- 前端 UI: browser 工具访问页面截图
4. Bug 报告: tasks/bug-<project>-<YYYY-MM-DD>-<id>.md
5. 测试通过 → 向 PM 发送测试报告
## 部署流程
### 有 PM 管理的项目(如积分商城)
1. 发送【部署申请】给 PM → 等待审批 → 执行部署 → 验证
2. 生产环境需 PM 审批 + 用户确认
### 用户直接交办的部署
1. 按用户指示执行,生产环境仍需用户确认
## 部署后验证
- 健康检查、核心接口可用性、页面可访问性
## Cursor 使用
- agent --model auto
- 用途: 编写测试脚本、分析 Bug 根因(只读)
```
**TOOLS.md:**
```markdown
# TOOLS.md - QA 测试环境
## 本机环境 (macOS)
- IDE: Cursor
## 已知项目
### 积分商城(由 integral-pm 管理)
- 项目路径: /Users/mac/scott-macair-26/integral-shop
- 本地服务:
- 管理后台前端: http://localhost:9527
- Admin API: http://localhost:8080
- Front API: http://localhost:8081
- SSH 部署:
- 脚本: backend/shell/deploy-admin-*.sh, deploy-front-*.sh
- 配置: backend/deploy.conf
- 环境分级:
- by80: 测试环境PM 审批)
- miao33: 预发布环境PM 审批)
- miao50: 生产环境PM 审批 + 用户确认)
- Admin JAR 端口: 30032
- Front JAR 端口: 30031
- Gitea: http://49.235.131.69:3000/scottpan/integral-shop.git
(接手新项目时,在此追加项目信息)
## Cursor CLI
- 模型: agent --model auto
## 已启用 Tools
- 内置: git, file-manager, web-search, code-runner, browser, http-request
```
---
## 八、Git 工作流(积分商城)
```
main # 生产分支
develop # 开发主分支
feature/backend-<name> # 后端功能分支
feature/frontend-<name> # 前端功能分支
bugfix/backend-<name> # 后端修复分支
bugfix/frontend-<name> # 前端修复分支
release/<version> # 发布分支
```
> 其他项目的 Git 工作流按各项目要求,由 PM 或用户在任务中指定。
---
## 九、初始化步骤
### 步骤 1在飞书开放平台创建 4 个机器人应用
| 应用名称 | accountId | appId | 状态 |
| --------- | ------------------ | ------------------------ | ---- |
| 积分商城-PM | jfshop@macair26 | `cli_a930893990799cba` | ✅ 复用现有 |
| 后端开发 | dev-backend@macair | `cli_a9316e2a92385bc7` | ✅ 已创建 |
| 前端开发 | dev-frontend@macair| `cli_a9316ef6f5785bb6` | ✅ 已创建 |
| 测试工程师 | dev-qa@macair | `cli_a9316f026ebadbc8` | ✅ 已创建 |
每个应用需启用:机器人能力、接收消息事件。连接模式使用 **websocket**
> 3 个 dev Agent 的飞书应用已创建完毕,仅 integral-pm 待创建。
### 步骤 2备份当前配置
```bash
cp ~/.openclaw/openclaw.json ~/.openclaw/openclaw.json.before-agents
```
### 步骤 3创建目录
```bash
# Workspace 目录
mkdir -p ~/.openclaw/workspace-integral-pm/{memory,plans,tasks}
mkdir -p ~/.openclaw/workspace-dev-{backend,frontend,qa}/{memory,tasks}
# Agent 目录
mkdir -p ~/.openclaw/agents/integral-pm/agent
mkdir -p ~/.openclaw/agents/dev-{backend,frontend,qa}/agent
```
### 步骤 4增量修改 openclaw.json
按第四节追加 `agents.list``bindings``channels.feishu.accounts`
**不删除或修改任何已有配置。**
### 步骤 5写入 Workspace 文件
为每个 workspace 写入第七节中的 IDENTITY.md、SOUL.md、AGENTS.md、USER.md、TOOLS.md。
```bash
for ws in integral-pm dev-backend dev-frontend dev-qa; do
echo "# HEARTBEAT.md" > ~/.openclaw/workspace-$ws/HEARTBEAT.md
done
```
### 步骤 6启用内置 Tools
```bash
# 所有 Agent 通用
for agent in integral-pm dev-backend dev-frontend dev-qa; do
openclaw skills enable git --agent $agent
openclaw skills enable file-manager --agent $agent
openclaw skills enable web-search --agent $agent
done
# 按角色差异化
openclaw skills enable browser --agent integral-pm
openclaw skills enable code-runner --agent dev-backend
openclaw skills enable http-request --agent dev-backend
openclaw skills enable code-runner --agent dev-frontend
openclaw skills enable browser --agent dev-frontend
openclaw skills enable code-runner --agent dev-qa
openclaw skills enable browser --agent dev-qa
openclaw skills enable http-request --agent dev-qa
```
### 步骤 7验证
```bash
openclaw doctor
openclaw agents list
openclaw agents list --bindings
# 在飞书中向 main 机器人发消息确认不受影响
# 分别向 4 个新机器人发消息确认路由正确
```
### 回滚方案
```bash
cp ~/.openclaw/openclaw.json.before-agents ~/.openclaw/openclaw.json
openclaw restart
```
---
## 十、安全性约束
### 10.1 SSH 密钥
- Workspace 文件中不记录 SSH 密钥路径
- 部署脚本通过 deploy.conf 中的环境变量引用
### 10.2 环境分级(积分商城)
| 环境 | QA 直接操作 | PM 审批 | 用户确认 |
| ------ | ------- | ----- | ---- |
| by80 | ● | ● | - |
| miao33 | ● | ● | - |
| miao50 | - | ● | ● |
### 10.3 敏感信息
- API key 仅存在 openclaw.json 和 agent/auth-profiles.json 中
- 飞书 appSecret 仅存在 openclaw.json 中
- Workspace .md 文件不记录任何密钥或密码
---
## 附录v3 → v3s 变更总结
| 维度 | v3 | v3s |
| -------------- | ---------------------------------- | -------------------------------------------- |
| Agent 命名 | integral-backend/frontend/qa | dev-backend/frontend/qa通用命名 |
| 开发 Agent 定位 | 积分商城专用 | **通用软件开发**,可服务任何项目 |
| SOUL.md 技术栈 | 写死特定版本约束 | 列出核心能力,按任务指定的约束执行 |
| TOOLS.md 项目信息 | 只有积分商城 | "已知项目"区块,可追加新项目 |
| PM 任务分派 | 直接下达 | 必须附带**项目路径 + 技术栈约束 + 分支规范** |
| 用户直接使用开发 Agent | 不支持 | **支持**,用户可直接私聊开发 Agent 下达任何编码任务 |
| workspace 目录命名 | workspace-integral-{role} | PM: workspace-integral-pm其余: workspace-dev-{role} |
| 项目路径 | `<PROJECT_ROOT>` 占位符 | `/Users/mac/scott-macair-26/integral-shop` |

View File

@@ -1,127 +0,0 @@
# Phase 1 检查点报告 — 17:30 自动检查
> 生成时间2026-03-30 17:30
> 检查范围:`backend-adminend/src`
---
## 检查结果汇总
| # | 检查项 | 状态 | 说明 |
|---|--------|------|------|
| 1 | `EmptyLayout.vue` 空白布局 | ❌ **未找到** | `src/layout/` 目录下只有 `index.vue`,未创建 EmptyLayout |
| 2 | `requestNoAuth.js` 免认证请求实例 | ❌ **未找到** | `src/utils/` 目录下只有 `request.js`,未创建 requestNoAuth |
| 3 | 路由模块 `integralExternal.js` | ❌ **未找到** | `src/router/modules/` 下无此文件constantRoutes 未注册 |
| 4 | `permission.js` 白名单前缀匹配 | ❌ **未修改** | 当前仍为精确匹配:`whiteList.indexOf(to.path) !== -1`,未改为前缀匹配 |
| 5 | API 文件 `integralExternal.js` | ❌ **未找到** | `src/api/` 目录下无此文件 |
| 6 | 冒烟验证(无 token 访问不跳转登录) | ⚠️ **无法验证** | 基础设施文件均未创建,无法执行冒烟测试 |
---
## 当前实际状态
**Phase 1 全部 5 项任务均未完成。**
当前 `permission.js` 白名单内容:
```js
const whiteList = ['/login', '/auth-redirect'];
// 匹配方式whiteList.indexOf(to.path) !== -1精确匹配
```
访问 `/integral-external/order` 无 token 时,**会被重定向到登录页**。
---
## 建议行动
### 立即按顺序创建以下文件:
**步骤 1创建 `src/layout/EmptyLayout.vue`**
```vue
<template>
<div class="empty-layout">
<router-view />
</div>
</template>
<script>
export default {
name: 'EmptyLayout'
}
</script>
```
**步骤 2创建 `src/utils/requestNoAuth.js`**
```js
import axios from 'axios'
const requestNoAuth = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 15000
})
requestNoAuth.interceptors.response.use(
response => response.data,
error => Promise.reject(error)
)
export default requestNoAuth
```
**步骤 3创建 `src/router/modules/integralExternal.js`**
```js
import EmptyLayout from '@/layout/EmptyLayout'
const integralExternalRouter = {
path: '/integral-external',
component: EmptyLayout,
children: [
{ path: 'order', name: 'IntegralOrder', component: () => import('@/views/integral/external/order/index') },
{ path: 'user', name: 'IntegralUser', component: () => import('@/views/integral/external/user/index') },
{ path: 'detail', name: 'IntegralDetail', component: () => import('@/views/integral/external/detail/index') }
]
}
export default integralExternalRouter
```
**步骤 4修改 `src/permission.js` 白名单为前缀匹配**
```js
// 改为:
const whiteList = ['/login', '/auth-redirect', '/integral-external'];
// 修改匹配逻辑(约第 55 行):
if (whiteList.some(path => to.path.startsWith(path))) {
next();
} else {
next(`/login?redirect=${to.path}`);
NProgress.done();
}
```
**步骤 5创建 `src/api/integralExternal.js`**(基础框架)
```js
import requestNoAuth from '@/utils/requestNoAuth'
export function getIntegralOrderList(params) {
return requestNoAuth({ url: '/api/integral/order/list', method: 'get', params })
}
export function getIntegralUserList(params) {
return requestNoAuth({ url: '/api/integral/user/list', method: 'get', params })
}
export function getIntegralDetail(params) {
return requestNoAuth({ url: '/api/integral/detail/list', method: 'get', params })
}
```
---
## ⚠️ 重要提示
**免登录链路是后续 Phase 2~4 一切工作的前提**,如果 permission.js 白名单不通,所有积分外部页面都无法访问。
请优先确保 `permission.js` 的前缀匹配逻辑正确生效后,再进入 Phase 2 开发。
当前时间已到 17:30**建议立即开始 Phase 1 任务**,完成后方可进入 Phase 2积分订单页面开发。

View File

@@ -1,89 +0,0 @@
# Phase 4 检查点报告 — 18:50 自动检查
> 生成时间2026-03-30 18:50
> 检查范围:`backend-adminend/src`
---
## 检查结果汇总
| # | 检查项 | 状态 | 说明 |
|---|--------|------|------|
| 1 | 积分明细页面(从 `user/integral/index.vue` 复制并修改) | ❌ **未完成** | `views/integral/external/detail/` 目录不存在,未创建任何外部页面 |
| 2 | URL query 参数 `uid` 自动注入搜索参数 | ❌ **未完成** | 外部积分明细页面未创建,无法验证 uid 参数读取 |
| 3 | 顶部概览卡片(`eb_user.integral` + `wa_users.selfBonus` | ❌ **未完成** | 无新增页面,概览卡片不存在 |
| 4 | 返回按钮跳回用户积分列表 | ❌ **未完成** | 页面未创建 |
| 5 | 分页和时间筛选 | ❌ **未完成** | 页面未创建 |
---
## ⚠️ 根因分析
**Phase 4 的全部 5 项检查均未通过,根本原因是 Phase 1 基础设施仍未搭建。**
截至本次检查,以下前置依赖均不存在:
| 前置项 | 状态 |
|--------|------|
| `src/layout/EmptyLayout.vue` | ❌ 未创建 |
| `src/utils/requestNoAuth.js` | ❌ 未创建 |
| `src/router/modules/integralExternal.js` | ❌ 未创建 |
| `src/api/integralExternal.js` | ❌ 未创建 |
| `permission.js` 白名单前缀匹配改造 | ❌ 未修改 |
| `router/index.js` 注册 constantRoutes | ❌ 未修改 |
Phase 1 → Phase 2 → Phase 3 → Phase 4 均为顺序依赖,无法跳过。
---
## 源文件就绪情况
积分明细源页面 `src/views/user/integral/index.vue` 存在242 行),结构清晰:
- ✅ 已有 `searchForm.uid` 字段 — 可直接从 `$route.query.uid` 注入
- ✅ 已有时间选择器 `daterange` — 分页和时间筛选逻辑可复用
- ✅ 已有 `integralListApi` 数据请求 — 需替换为 `requestNoAuth` 版本
- ⬜ 需新增:顶部概览卡片(调用用户详情接口获取 `integral``selfBonus`
- ⬜ 需新增:返回按钮(`this.$router.push('/integral-external/user')`
改造量确实很小(~50 行修改),**确认源页面仅 242 行,风险最低**。
---
## 能否进入 Phase 5
**❌ 不能进入 Phase 5联调验证 + 提交)。**
Phase 5 的前提是 Phase 1~4 全部完成。当前连 Phase 1 都未完成。
---
## 建议行动
### 方案 A快速补救推荐
如果用户仍有时间,建议按以下**压缩顺序**一次性完成 Phase 1 + Phase 4
1. **创建 `EmptyLayout.vue`**1 分钟)
2. **创建 `requestNoAuth.js`**2 分钟)
3. **修改 `permission.js` 白名单**2 分钟)
4. **创建路由模块 + 注册 constantRoutes**3 分钟)
5. **复制 `user/integral/index.vue` → 外部积分明细页面**5 分钟)
- 注入 `$route.query.uid`
- 替换 API 为免认证版本
- 添加概览卡片和返回按钮
6. **冒烟测试**5 分钟)
预计总耗时:~18 分钟
### 方案 B仅完成基础设施
如果时间紧张,优先完成 Phase 1 基础设施确保免登录链路畅通Phase 4 积分明细页面留到下次。
---
## 参考文档
- 开发计划:`docs/integral-pages-schedule.md`
- 技术方案:`docs/integral-pages-coding-plan.md`
- Phase 1 检查报告:`docs/phase1-checkpoint-report.md`17:30 生成,全部未通过)

View File

@@ -0,0 +1,55 @@
# 新公司宝应桂圣富商贸项目的h5端目录h5 修改任务
Ecs ip 118.31.36.212
## **修改任务**
- **已完成**新建分支bygsf212合并byhlc112分支的最新代码到该分支并根据上述信息修改相关需要变更项使符合该新公司项目环境
## 配置项修改
1.新项目公司名称eg宝应桂圣富商贸
2.使用"宝应桂圣富商贸"的**HTML十进制实体**编码修改wa_options表中name=system_config中的value值里的title信息
3.相关配置项:
A. **寄卖商城API地址**https://admin.b3y45.com/api
B. **寄卖商城后台地址**https://admin.b3y45.com
C. **寄卖商城H5地址**https://b3y45.com/
D. **云服务器寄卖商城H5目录**/www/wwwroot/b3y45.com
E. **云服务器寄卖商城后台目录**/www/wwwroot/admin.b3y45.com
F. **积分商城地址**https://jf.b3y45.com
G. **云服务器积分商城目录**/www/wwwroot/jf.b3y45.com
4. 短信服务
SMS_SIGNNAME = '宝应宏煜春商贸'
SMS_TEMPLATE = 'SMS_334545236'
SMS_KEYID = 'LTAI5t7mHU5L4ChxXQk4vw4T'
SMS_KEYSECRET = 'X9yonEufGZJXEMtFXQvY5oJQmk0yno'
5. **webman.bin相关**
sn_id: 17533260260610
APP_SECRET: ZFyTNQTWEkCBczKzyUDJWE9Ecx260610
### **寄卖商城H5**中需要修改的文件
A.修改h5/static/configs.js中如下内容
TITLE: '宝应桂圣富商贸',
BASE_URL: 'https://admin.b3y45.com/api',
IMG_URL: 'https://admin.b3y45.com',
H5_URL: 'https://b3y45.com',
**sn_id**、**appStr**必须修改为webman.bin相关中的值
B. 修改h5/static/js/pages-personal-index.6f5415f9.js
第270行https://jf.b3y45.com/pages/integral/points?username=
C. 修改h5/static/js/pages-sub-pages-webview-index.1042489b.js
第15行https://jf.b3y45.com/?sn_id
第43行https://jf.b3y45.com/?user_id=
### **寄卖商城后端**项目中需要修改的文件
- 修改houtai/.env环境变量配置文件中的短信信息、APP_SECRET。
- mysql数据库使用阿里云rdsrm-bp1a178eq62lxba9xbo.mysql.rds.aliyuncs.com
- rds中项目数据库名bygsf212

55
docs/resell-change-bygsf212.md Executable file
View File

@@ -0,0 +1,55 @@
# 新公司宝应桂圣富商贸项目的h5端目录h5 修改任务
Ecs ip 118.31.36.212
## **修改任务**
- 新建分支bygsf212合并byhlc112分支的最新代码到该分支并根据上述信息修改相关需要变更项使符合该新公司项目环境
## 配置项修改
1.新项目公司名称eg宝应桂圣富商贸
2.使用"宝应桂圣富商贸"的**HTML十进制实体**编码修改wa_options表中name=system_config中的value值里的title信息
3.相关配置项:
A. **寄卖商城API地址**https://admin.b3y45.com/api
B. **寄卖商城后台地址**https://admin.b3y45.com
C. **寄卖商城H5地址**https://b3y45.com/
D. **云服务器寄卖商城H5目录**/www/wwwroot/b3y45.com
E. **云服务器寄卖商城后台目录**/www/wwwroot/admin.b3y45.com
F. **积分商城地址**https://jf.b3y45.com
G. **云服务器积分商城目录**/www/wwwroot/jf.b3y45.com
4. 短信服务
SMS_SIGNNAME = '宝应宏煜春商贸'
SMS_TEMPLATE = 'SMS_334545236'
SMS_KEYID = 'LTAI5t7mHU5L4ChxXQk4vw4T'
SMS_KEYSECRET = 'X9yonEufGZJXEMtFXQvY5oJQmk0yno'
5. **webman.bin相关**
sn_id: 17533260260610
APP_SECRET: ZFyTNQTWEkCBczKzyUDJWE9Ecx260610
### **寄卖商城H5**中需要修改的文件
A.修改h5/static/configs.js中如下内容
TITLE: '宝应桂圣富商贸',
BASE_URL: 'https://admin.b3y45.com/api',
IMG_URL: 'https://admin.b3y45.com',
H5_URL: 'https://b3y45.com',
**sn_id**、**appStr**必须修改为webman.bin相关中的值
B. 修改h5/static/js/pages-personal-index.6f5415f9.js
第270行https://jf.b3y45.com/pages/integral/points?username=
C. 修改h5/static/js/pages-sub-pages-webview-index.1042489b.js
第15行https://jf.b3y45.com/?sn_id
第43行https://jf.b3y45.com/?user_id=
### **寄卖商城后端**项目中需要修改的文件
- 修改houtai/.env环境变量配置文件中的短信信息、APP_SECRET。
- mysql数据库使用阿里云rdsrm-bp1a178eq62lxba9xbo.mysql.rds.aliyuncs.com
- rds中项目数据库名bygsf212

View File

@@ -0,0 +1,50 @@
-- 池州春芳商贸二次数据清理
-- 执行时间2026-06-01 21:06:25
-- 数据范围92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251
START TRANSACTION;
CREATE TABLE IF NOT EXISTS wa_users_bak_20260601_210625 AS
SELECT * FROM wa_users WHERE id IN (92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251);
CREATE TABLE IF NOT EXISTS eb_user_bak_20260601_210625 AS
SELECT * FROM eb_user WHERE uid IN (92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251);
CREATE TABLE IF NOT EXISTS wa_order_bak_20260601_210625 AS
SELECT * FROM wa_order
WHERE seller_id IN (92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251)
OR buyer_id IN (92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251);
CREATE TABLE IF NOT EXISTS wa_merchandise_bak_20260601_210625 AS
SELECT * FROM wa_merchandise WHERE user_id IN (92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251);
CREATE TABLE IF NOT EXISTS wa_selfbonus_log_bak_20260601_210625 AS
SELECT * FROM wa_selfbonus_log WHERE user_id IN (92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251);
CREATE TABLE IF NOT EXISTS wa_sharebonus_log_bak_20260601_210625 AS
SELECT * FROM wa_sharebonus_log WHERE user_id IN (92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251);
CREATE TABLE IF NOT EXISTS wa_coupon_log_bak_20260601_210625 AS
SELECT * FROM wa_coupon_log WHERE user_id IN (92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251);
CREATE TABLE IF NOT EXISTS wa_withdraw_bak_20260601_210625 AS
SELECT * FROM wa_withdraw WHERE user_id IN (92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251);
CREATE TABLE IF NOT EXISTS eb_store_order_bak_20260601_210625 AS
SELECT * FROM eb_store_order WHERE uid IN (92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251);
CREATE TABLE IF NOT EXISTS eb_user_integral_record_bak_20260601_210625 AS
SELECT * FROM eb_user_integral_record WHERE uid IN (92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251);
DELETE FROM wa_users WHERE id IN (92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251);
DELETE FROM eb_user WHERE uid IN (92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251);
DELETE FROM wa_order WHERE seller_id IN (92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251) OR buyer_id IN (92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251);
DELETE FROM wa_merchandise WHERE user_id IN (92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251);
DELETE FROM wa_selfbonus_log WHERE user_id IN (92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251);
DELETE FROM wa_sharebonus_log WHERE user_id IN (92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251);
DELETE FROM wa_coupon_log WHERE user_id IN (92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251);
DELETE FROM wa_withdraw WHERE user_id IN (92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251);
DELETE FROM eb_store_order WHERE uid IN (92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251);
DELETE FROM eb_user_integral_record WHERE uid IN (92738,92827,93106,93119,93132,93133,93134,93140,93150,93156,93163,93166,93171,93172,93173,93182,93183,93185,93187,93188,93190,93191,93193,93195,93196,93197,93198,93199,93200,93204,93205,93207,93208,93209,93210,93211,93212,93213,93214,93215,93216,93217,93218,93220,93221,93222,93224,93225,93226,93227,93230,93231,93233,93235,93239,93240,93242,93245,93248,93251);
COMMIT;

View File

@@ -0,0 +1,52 @@
-- 池州春芳商贸数据清理
-- 执行时间2026-06-01 17:06:41
-- 数据范围93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245
SET @scope_sql := '93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245';
START TRANSACTION;
CREATE TABLE IF NOT EXISTS wa_users_bak_20260601_170641 AS
SELECT * FROM wa_users WHERE id IN (93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245);
CREATE TABLE IF NOT EXISTS eb_user_bak_20260601_170641 AS
SELECT * FROM eb_user WHERE uid IN (93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245);
CREATE TABLE IF NOT EXISTS wa_order_bak_20260601_170641 AS
SELECT * FROM wa_order
WHERE seller_id IN (93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245)
OR buyer_id IN (93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245);
CREATE TABLE IF NOT EXISTS wa_merchandise_bak_20260601_170641 AS
SELECT * FROM wa_merchandise WHERE user_id IN (93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245);
CREATE TABLE IF NOT EXISTS wa_selfbonus_log_bak_20260601_170641 AS
SELECT * FROM wa_selfbonus_log WHERE user_id IN (93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245);
CREATE TABLE IF NOT EXISTS wa_sharebonus_log_bak_20260601_170641 AS
SELECT * FROM wa_sharebonus_log WHERE user_id IN (93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245);
CREATE TABLE IF NOT EXISTS wa_coupon_log_bak_20260601_170641 AS
SELECT * FROM wa_coupon_log WHERE user_id IN (93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245);
CREATE TABLE IF NOT EXISTS wa_withdraw_bak_20260601_170641 AS
SELECT * FROM wa_withdraw WHERE user_id IN (93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245);
CREATE TABLE IF NOT EXISTS eb_store_order_bak_20260601_170641 AS
SELECT * FROM eb_store_order WHERE uid IN (93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245);
CREATE TABLE IF NOT EXISTS eb_user_integral_record_bak_20260601_170641 AS
SELECT * FROM eb_user_integral_record WHERE uid IN (93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245);
DELETE FROM wa_users WHERE id IN (93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245);
DELETE FROM eb_user WHERE uid IN (93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245);
DELETE FROM wa_order WHERE seller_id IN (93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245) OR buyer_id IN (93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245);
DELETE FROM wa_merchandise WHERE user_id IN (93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245);
DELETE FROM wa_selfbonus_log WHERE user_id IN (93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245);
DELETE FROM wa_sharebonus_log WHERE user_id IN (93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245);
DELETE FROM wa_coupon_log WHERE user_id IN (93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245);
DELETE FROM wa_withdraw WHERE user_id IN (93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245);
DELETE FROM eb_store_order WHERE uid IN (93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245);
DELETE FROM eb_user_integral_record WHERE uid IN (93119,93132,93133,93134,93156,93163,93172,93173,93183,93187,93188,93190,93191,93193,93195,93197,93198,93199,93209,93212,93214,93215,93220,93221,93224,93226,93230,93235,93239,93240,93245);
COMMIT;

View File

@@ -0,0 +1,641 @@
#!/usr/bin/env python3
"""Supplement-migrate Bosengyuan team data into bygsf212.
Default mode is a read-only dry run. Use --execute to back up target tables,
insert transformed source rows, and commit the transaction.
"""
from __future__ import annotations
import argparse
import gzip
import json
import re
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
from pathlib import Path
from typing import Any
import pymysql
from openpyxl import load_workbook
from pymysql.cursors import SSCursor
ROOT = Path(__file__).resolve().parents[2]
DOC = ROOT / "docs" / "com-bygsf212-data-imgration.md"
DEFAULT_EXCEL = Path("/Users/mac/Works26/miao-july/宝应鼎信汇/博森元团队成员信息表.xlsx")
DEFAULT_DUMP = Path("/Users/mac/Works26/miao-july/宝应鼎信汇/bsy-yangtangyoupin_2026-06-14_14-25-01_mysql_data.sql")
EXPECTED_DATABASE = "bygsf212"
MERCHANDISE_CUTOFF = "2026-06-12 00:00:00"
TABLES_ORDER = [
"wa_users",
"eb_user",
"wa_merchandise",
"wa_selfbonus_log",
"wa_sharebonus_log",
"wa_coupon_log",
"eb_user_integral_record",
]
BACKUP_TABLES = [
"wa_users",
"eb_user",
"wa_merchandise",
"wa_selfbonus_log",
"wa_sharebonus_log",
"wa_coupon_log",
"eb_user_integral_record",
]
PK_COLUMNS = {
"wa_users": "id",
"eb_user": "uid",
"wa_merchandise": "id",
"wa_selfbonus_log": "id",
"wa_sharebonus_log": "id",
"wa_coupon_log": "id",
"eb_user_integral_record": "id",
}
@dataclass(frozen=True)
class ExcelUser:
old_id: int
nickname: str
phone: str
parent_old_id: int
@dataclass
class UserDecision:
old_id: int
target_id: int
nickname: str
phone: str
action: str
reason: str
def parse_doc_config() -> dict[str, str]:
text = DOC.read_text(encoding="utf-8")
def grab(name: str) -> str:
m = re.search(rf"^\s*{name}:\s*(.+?)\s*$", text, flags=re.M)
if not m:
raise ValueError(f"missing datasource {name} in {DOC}")
return m.group(1).strip()
return {
"host": grab("rds"),
"database": grab("name"),
"user": grab("username"),
"password": grab("password"),
}
def load_excel_users(path: Path) -> list[ExcelUser]:
wb = load_workbook(path, data_only=True, read_only=True)
ws = wb.active
rows = list(ws.iter_rows(values_only=True))
headers = [str(x).strip() for x in rows[0]]
users: list[ExcelUser] = []
for row in rows[1:]:
if not any(row):
continue
data = dict(zip(headers, row))
users.append(
ExcelUser(
old_id=int(data["用户ID"]),
nickname=str(data["昵称"]).strip(),
phone=str(data["联系方式"]).strip(),
parent_old_id=int(data["上级ID"]),
)
)
if len({u.old_id for u in users}) != len(users):
raise ValueError("duplicate user ids in Excel")
if len({u.phone for u in users}) != len(users):
raise ValueError("duplicate phones in Excel")
return users
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 == "\\":
j += 2
continue
if c == "'":
if j + 1 < n and values_blob[j + 1] == "'":
j += 2
continue
in_quote = False
j += 1
continue
if c == "'":
in_quote = True
elif 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:
cur.append(inner[i + 1])
i += 2
continue
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
out.append("".join(cur).strip())
return out
def unescape_mysql_string(body: str) -> str:
body = body.replace("''", "'")
out: list[str] = []
i = 0
escapes = {
"0": "\0",
"b": "\b",
"n": "\n",
"r": "\r",
"t": "\t",
"Z": "\x1a",
"\\": "\\",
"'": "'",
'"': '"',
}
while i < len(body):
ch = body[i]
if ch == "\\" and i + 1 < len(body):
nxt = body[i + 1]
out.append(escapes.get(nxt, nxt))
i += 2
continue
out.append(ch)
i += 1
return "".join(out)
def parse_sql_value(raw: str) -> Any:
raw = raw.strip()
if raw.upper() == "NULL":
return None
if raw.startswith("'") and raw.endswith("'"):
return unescape_mysql_string(raw[1:-1])
if re.fullmatch(r"-?\d+", raw):
return int(raw)
if re.fullmatch(r"-?\d+\.\d+", raw):
return Decimal(raw)
return raw
def parse_dump(dump: Path, source_ids: set[int]) -> tuple[dict[str, list[str]], dict[str, list[list[Any]]]]:
if not dump.is_file():
raise FileNotFoundError(dump)
schemas: dict[str, list[str]] = {}
rows: dict[str, list[list[Any]]] = {table: [] for table in TABLES_ORDER}
current: str | None = None
columns: list[str] = []
with dump.open("r", encoding="utf-8", errors="replace") as f:
for line in f:
if line.startswith("CREATE TABLE `"):
name = line.split("`", 2)[1]
current = name if name in TABLES_ORDER else None
columns = []
continue
if current:
m = re.match(r"\s*`([^`]+)`", line)
if m:
columns.append(m.group(1))
if line.startswith(")") or line.startswith("ENGINE="):
schemas[current] = columns
current = None
continue
if not line.startswith("INSERT INTO `"):
continue
table = line.split("`", 2)[1]
if table not in TABLES_ORDER:
continue
if table not in schemas:
raise ValueError(f"INSERT before schema for {table}")
blob = line[line.index("VALUES") + len("VALUES") :].strip()
if blob.endswith(";"):
blob = blob[:-1].strip()
for tup in split_top_level_tuples(blob):
values = [parse_sql_value(x) for x in split_mysql_fields(tup.strip()[1:-1])]
if keep_source_row(table, schemas[table], values, source_ids):
rows[table].append(values)
missing = [table for table in TABLES_ORDER if table not in schemas]
if missing:
raise ValueError(f"missing schemas in dump: {missing}")
return schemas, rows
def keep_source_row(table: str, columns: list[str], row: list[Any], source_ids: set[int]) -> bool:
idx = {name: i for i, name in enumerate(columns)}
if table == "wa_users":
return int(row[idx["id"]]) in source_ids
if table == "eb_user":
return int(row[idx["uid"]]) in source_ids
if table == "wa_merchandise":
return int(row[idx["user_id"]]) in source_ids and str(row[idx["created_at"]]) >= MERCHANDISE_CUTOFF
if table in {"wa_selfbonus_log", "wa_sharebonus_log", "wa_coupon_log"}:
return int(row[idx["user_id"]]) in source_ids
if table == "eb_user_integral_record":
return int(row[idx["uid"]]) in source_ids
return False
def connect(config: dict[str, str], cursorclass=None):
kwargs = {
"host": config["host"],
"user": config["user"],
"password": config["password"],
"database": config["database"],
"charset": "utf8mb4",
"autocommit": False,
"connect_timeout": 10,
"read_timeout": 120,
"write_timeout": 120,
}
if cursorclass is not None:
kwargs["cursorclass"] = cursorclass
return pymysql.connect(**kwargs)
def get_target_schemas(cur) -> dict[str, list[str]]:
cur.execute("SELECT DATABASE()")
database = cur.fetchone()[0]
if database != EXPECTED_DATABASE:
raise RuntimeError(f"refusing to run against database {database!r}")
schemas: dict[str, list[str]] = {}
for table in BACKUP_TABLES:
cur.execute(f"SHOW COLUMNS FROM `{table}`")
schemas[table] = [row[0] for row in cur.fetchall()]
return schemas
def table_auto_increment(cur, table: str) -> int:
cur.execute("SHOW TABLE STATUS LIKE %s", (table,))
row = cur.fetchone()
return int(row[10] or 1)
def existing_pk_set(cur, table: str, pk_col: str) -> set[int]:
cur.execute(f"SELECT `{pk_col}` FROM `{table}`")
return {int(row[0]) for row in cur.fetchall()}
def determine_user_mapping(cur, users: list[ExcelUser]) -> list[UserDecision]:
old_ids = [u.old_id for u in users]
phones = [u.phone for u in users]
old_clause = ",".join(["%s"] * len(old_ids))
phone_clause = ",".join(["%s"] * len(phones))
cur.execute(f"SELECT id,nickname,mobile FROM `wa_users` WHERE id IN ({old_clause})", old_ids)
by_id = {int(row[0]): {"nickname": row[1], "phone": str(row[2])} for row in cur.fetchall()}
cur.execute(f"SELECT id,nickname,mobile FROM `wa_users` WHERE mobile IN ({phone_clause})", phones)
by_phone = {str(row[2]): {"id": int(row[0]), "nickname": row[1]} for row in cur.fetchall()}
all_user_ids = existing_pk_set(cur, "wa_users", "id") | existing_pk_set(cur, "eb_user", "uid")
next_id = max(table_auto_increment(cur, "wa_users"), max(all_user_ids or {0}) + 1)
decisions: list[UserDecision] = []
def next_free_id() -> int:
nonlocal next_id
while next_id in all_user_ids:
next_id += 1
value = next_id
all_user_ids.add(value)
next_id += 1
return value
for user in users:
existing_same_phone = by_phone.get(user.phone)
existing_same_id = by_id.get(user.old_id)
if existing_same_phone:
decisions.append(
UserDecision(
old_id=user.old_id,
target_id=existing_same_phone["id"],
nickname=user.nickname,
phone=user.phone,
action="skip_existing_phone",
reason=f"phone already exists as uid={existing_same_phone['id']}",
)
)
continue
if not existing_same_id:
all_user_ids.add(user.old_id)
decisions.append(
UserDecision(
old_id=user.old_id,
target_id=user.old_id,
nickname=user.nickname,
phone=user.phone,
action="insert_original_id",
reason="old id not present in target",
)
)
continue
target_id = next_free_id()
decisions.append(
UserDecision(
old_id=user.old_id,
target_id=target_id,
nickname=user.nickname,
phone=user.phone,
action="insert_reassigned_id",
reason=(
f"old id occupied by {existing_same_id['nickname']}/"
f"{existing_same_id['phone']}"
),
)
)
if len({d.target_id for d in decisions}) != len(decisions):
raise ValueError("target user id collision in decisions")
return decisions
def allocate_pk_maps(
cur, source_rows: dict[str, list[list[Any]]], schemas: dict[str, list[str]]
) -> dict[str, dict[int, int]]:
maps: dict[str, dict[int, int]] = {}
for table, rows in source_rows.items():
if table in {"wa_users", "eb_user"}:
continue
pk_col = PK_COLUMNS[table]
pk_idx = schemas[table].index(pk_col)
source_pks = [int(row[pk_idx]) for row in rows]
existing = existing_pk_set(cur, table, pk_col)
used = set(existing) | set(source_pks)
auto = table_auto_increment(cur, table)
next_id = max([auto, *(used or {0})]) + 1
table_map: dict[int, int] = {}
for old_pk in source_pks:
if old_pk in table_map:
continue
if old_pk not in existing:
table_map[old_pk] = old_pk
continue
while next_id in used:
next_id += 1
table_map[old_pk] = next_id
used.add(next_id)
next_id += 1
maps[table] = table_map
return maps
def transform_rows(
source_rows: dict[str, list[list[Any]]],
schemas: dict[str, list[str]],
decisions: list[UserDecision],
pk_maps: dict[str, dict[int, int]],
) -> dict[str, list[list[Any]]]:
user_map = {d.old_id: d.target_id for d in decisions}
insert_user_ids = {d.old_id for d in decisions if d.action.startswith("insert_")}
transformed: dict[str, list[list[Any]]] = {table: [] for table in TABLES_ORDER}
for table in TABLES_ORDER:
cols = schemas[table]
idx = {name: i for i, name in enumerate(cols)}
for source in source_rows[table]:
row = list(source)
if table == "wa_users":
old_id = int(row[idx["id"]])
if old_id not in insert_user_ids:
continue
row[idx["id"]] = user_map[old_id]
if int(row[idx["pid"]] or 0) in user_map:
row[idx["pid"]] = user_map[int(row[idx["pid"]])]
elif table == "eb_user":
old_id = int(row[idx["uid"]])
if old_id not in insert_user_ids:
continue
row[idx["uid"]] = user_map[old_id]
if int(row[idx["spread_uid"]] or 0) in user_map:
row[idx["spread_uid"]] = user_map[int(row[idx["spread_uid"]])]
elif table == "wa_merchandise":
old_pk = int(row[idx["id"]])
old_user_id = int(row[idx["user_id"]])
if old_user_id not in insert_user_ids:
continue
row[idx["id"]] = pk_maps[table][old_pk]
row[idx["user_id"]] = user_map[old_user_id]
elif table in {"wa_selfbonus_log", "wa_sharebonus_log", "wa_coupon_log"}:
old_pk = int(row[idx["id"]])
old_user_id = int(row[idx["user_id"]])
if old_user_id not in insert_user_ids:
continue
row[idx["id"]] = pk_maps[table][old_pk]
row[idx["user_id"]] = user_map[old_user_id]
elif table == "eb_user_integral_record":
old_pk = int(row[idx["id"]])
old_user_id = int(row[idx["uid"]])
if old_user_id not in insert_user_ids:
continue
row[idx["id"]] = pk_maps[table][old_pk]
row[idx["uid"]] = user_map[old_user_id]
update_integral_links(row, idx, pk_maps.get("wa_selfbonus_log", {}))
transformed[table].append(row)
return transformed
def update_integral_links(row: list[Any], idx: dict[str, int], selfbonus_map: dict[int, int]) -> None:
log_id = row[idx["wa_selfbonus_logid"]]
if log_id is not None and int(log_id) in selfbonus_map:
row[idx["wa_selfbonus_logid"]] = selfbonus_map[int(log_id)]
if str(row[idx["link_type"]]) == "selfbonus":
try:
link_id = int(str(row[idx["link_id"]]))
except ValueError:
return
if link_id in selfbonus_map:
row[idx["link_id"]] = str(selfbonus_map[link_id])
def backup_tables(config: dict[str, str], backup_path: Path) -> None:
backup_path.parent.mkdir(parents=True, exist_ok=True)
backup_conn = connect(config, cursorclass=SSCursor)
try:
with gzip.open(backup_path, "wt", encoding="utf-8") as out:
out.write("-- bygsf212 bsy supplement backup\n")
out.write(f"-- created_at: {datetime.now().isoformat(timespec='seconds')}\n")
out.write("SET NAMES utf8mb4;\n")
for table in BACKUP_TABLES:
with backup_conn.cursor() as cur:
cur.execute(f"SHOW CREATE TABLE `{table}`")
create_sql = cur.fetchone()[1]
out.write(f"\n-- Table `{table}`\n")
out.write(f"DROP TABLE IF EXISTS `{table}`;\n")
out.write(create_sql + ";\n")
with backup_conn.cursor() as cur:
cur.execute(f"SELECT * FROM `{table}`")
batch: list[str] = []
row_count = 0
for row in cur:
batch.append("(" + ",".join(backup_conn.literal(v) for v in row) + ")")
row_count += 1
if len(batch) >= 200:
out.write(f"INSERT INTO `{table}` VALUES\n")
out.write(",\n".join(batch) + ";\n")
batch = []
if batch:
out.write(f"INSERT INTO `{table}` VALUES\n")
out.write(",\n".join(batch) + ";\n")
print(f"backup {table}: rows={row_count}")
finally:
backup_conn.close()
def insert_rows(cur, target_schemas: dict[str, list[str]], rows_by_table: dict[str, list[list[Any]]]) -> dict[str, int]:
inserted: dict[str, int] = {}
for table in TABLES_ORDER:
rows = rows_by_table[table]
inserted[table] = 0
if not rows:
continue
cols = target_schemas[table]
col_sql = ",".join(f"`{col}`" for col in cols)
ph = ",".join(["%s"] * len(cols))
sql = f"INSERT INTO `{table}` ({col_sql}) VALUES ({ph})"
for row in rows:
cur.execute(sql, tuple(row))
inserted[table] += cur.rowcount
return inserted
def summarize_pk_remaps(pk_maps: dict[str, dict[int, int]]) -> dict[str, int]:
return {
table: sum(1 for old, new in mapping.items() if old != new)
for table, mapping in pk_maps.items()
}
def print_summary(
users: list[ExcelUser],
source_rows: dict[str, list[list[Any]]],
transformed_rows: dict[str, list[list[Any]]],
decisions: list[UserDecision],
pk_maps: dict[str, dict[int, int]],
) -> None:
print(f"excel_users={len(users)}")
print("user_mapping")
for d in decisions:
suffix = "" if d.old_id == d.target_id else f" -> {d.target_id}"
print(f" {d.old_id}{suffix}: {d.nickname}/{d.phone} [{d.action}] {d.reason}")
print("source_rows")
for table in TABLES_ORDER:
print(f" {table}: {len(source_rows[table])}")
print("insert_rows")
for table in TABLES_ORDER:
print(f" {table}: {len(transformed_rows[table])}")
print("pk_remaps")
print(json.dumps(summarize_pk_remaps(pk_maps), ensure_ascii=False, indent=2))
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--execute", action="store_true", help="insert rows and commit")
parser.add_argument("--excel", type=Path, default=DEFAULT_EXCEL)
parser.add_argument("--dump", type=Path, default=DEFAULT_DUMP)
parser.add_argument("--backup-dir", type=Path, default=ROOT / "docs" / "sql" / "backups")
args = parser.parse_args()
config = parse_doc_config()
users = load_excel_users(args.excel)
source_ids = {u.old_id for u in users}
dump_schemas, source_rows = parse_dump(args.dump, source_ids)
conn = connect(config)
try:
with conn.cursor() as cur:
target_schemas = get_target_schemas(cur)
for table in TABLES_ORDER:
if dump_schemas[table] != target_schemas[table]:
raise RuntimeError(f"schema mismatch for {table}")
decisions = determine_user_mapping(cur, users)
pk_maps = allocate_pk_maps(cur, source_rows, dump_schemas)
transformed_rows = transform_rows(source_rows, dump_schemas, decisions, pk_maps)
print_summary(users, source_rows, transformed_rows, decisions, pk_maps)
if not args.execute:
print("dry_run_only=true")
conn.rollback()
return 0
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_path = args.backup_dir / f"bygsf212_bsy_supplement_before_{stamp}.sql.gz"
print(f"backup_path={backup_path}")
backup_tables(config, backup_path)
with conn.cursor() as cur:
cur.execute("SET FOREIGN_KEY_CHECKS = 0")
inserted = insert_rows(cur, target_schemas, transformed_rows)
cur.execute("SET FOREIGN_KEY_CHECKS = 1")
print("inserted_rows")
print(json.dumps(inserted, ensure_ascii=False, indent=2))
conn.commit()
print("COMMIT ok")
return 0
except Exception as exc:
try:
with conn.cursor() as cur:
cur.execute("SET FOREIGN_KEY_CHECKS = 1")
except Exception:
pass
conn.rollback()
print(f"ROLLBACK: {exc}")
raise
finally:
conn.close()
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,401 @@
#!/usr/bin/env python3
"""Run data cleanup for docs/com-bygsf212-data-imgration.md.
Default mode is a read-only dry run. Use --execute to create a local SQL backup,
delete rows according to the migration document, and commit the transaction.
"""
from __future__ import annotations
import argparse
import gzip
import json
import re
import sys
from datetime import datetime
from pathlib import Path
from typing import Any
import pymysql
from pymysql.cursors import SSCursor
ROOT = Path(__file__).resolve().parents[2]
DOC = ROOT / "docs" / "com-bygsf212-data-imgration.md"
DEFAULT_DUMPS = [
Path("/Users/mac/Works26/miao-july/宝应鼎信汇/bsy-yangtangyoupin_2026-06-14_14-25-01_mysql_data.sql"),
Path("/Users/mac/Works26/miao-july/宝应鼎信汇/jyw-yangtangyoupin_2026-06-14_14-55-01_mysql_data.sql"),
]
CUTOFF = "2026-06-12 00:00:00"
EXPECTED_DATABASE = "bygsf212"
TABLES = [
"wa_order",
"wa_withdraw",
"eb_store_order",
"wa_merchandise",
"wa_selfbonus_log",
"wa_sharebonus_log",
"wa_coupon_log",
"eb_user_integral_record",
"eb_user",
"wa_users",
]
FILTERS = {
"wa_users": "id",
"eb_user": "uid",
"wa_selfbonus_log": "user_id",
"wa_sharebonus_log": "user_id",
"wa_coupon_log": "user_id",
"eb_user_integral_record": "uid",
}
CLEAR_TABLES = ["wa_order", "wa_withdraw", "eb_store_order"]
def parse_doc() -> tuple[dict[str, str], list[int]]:
text = DOC.read_text(encoding="utf-8")
def grab(name: str) -> str:
m = re.search(rf"^\s*{name}:\s*(.+?)\s*$", text, flags=re.M)
if not m:
raise ValueError(f"missing datasource {name} in {DOC}")
return m.group(1).strip()
ids_match = re.search(r"保留名单:\s*\n\s*`([^`]+)`", text)
if not ids_match:
raise ValueError(f"missing user id keep list in {DOC}")
ids = [int(x.strip()) for x in ids_match.group(1).split(",") if x.strip()]
if len(ids) != len(set(ids)):
raise ValueError("duplicate ids in keep list")
config = {
"host": grab("rds"),
"database": grab("name"),
"user": grab("username"),
"password": grab("password"),
}
return config, ids
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 == "\\":
j += 2
continue
if c == "'":
if j + 1 < n and values_blob[j + 1] == "'":
j += 2
continue
in_quote = False
j += 1
continue
if c == "'":
in_quote = True
elif 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:
cur.append(inner[i + 1])
i += 2
continue
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
out.append("".join(cur).strip())
return out
def unquote_sql_string(raw: str) -> str:
raw = raw.strip()
if raw.startswith("'") and raw.endswith("'"):
body = raw[1:-1]
body = body.replace("''", "'")
body = body.replace("\\'", "'").replace("\\\\", "\\")
return body
return raw
def extract_wa_merchandise_keep_ids(dumps: list[Path], keep_users: set[int]) -> list[int]:
keep_ids: set[int] = set()
for dump in dumps:
if not dump.is_file():
raise FileNotFoundError(f"dump not found: {dump}")
total_rows = 0
insert_lines = 0
file_keep_ids: set[int] = set()
with dump.open("r", encoding="utf-8", errors="replace") as f:
for line in f:
if "INSERT INTO `wa_merchandise`" not in line or "VALUES" not in line:
continue
insert_lines += 1
blob = line[line.index("VALUES") + len("VALUES") :].strip()
if blob.endswith(";"):
blob = blob[:-1].strip()
for tup in split_top_level_tuples(blob):
total_rows += 1
fields = split_mysql_fields(tup.strip()[1:-1])
if len(fields) < 9:
raise ValueError(f"malformed wa_merchandise tuple: {tup[:120]}")
row_id = int(fields[0])
user_id = int(fields[2])
created_at = unquote_sql_string(fields[8])
if created_at >= CUTOFF and user_id in keep_users:
file_keep_ids.add(row_id)
if insert_lines == 0:
raise ValueError(f"no INSERT INTO `wa_merchandise` found in dump: {dump}")
keep_ids.update(file_keep_ids)
print(f"dump={dump.name} wa_merchandise_rows={total_rows} keep_by_rule={len(file_keep_ids)}")
return sorted(keep_ids)
def connect(config: dict[str, str], cursorclass=None):
kwargs = {
"host": config["host"],
"user": config["user"],
"password": config["password"],
"database": config["database"],
"charset": "utf8mb4",
"autocommit": False,
"connect_timeout": 10,
"read_timeout": 120,
"write_timeout": 120,
}
if cursorclass is not None:
kwargs["cursorclass"] = cursorclass
return pymysql.connect(**kwargs)
def placeholders(items: list[int] | set[int]) -> str:
if not items:
return "NULL"
return ",".join(["%s"] * len(items))
def count_where(cur, table: str, where: str = "1=1", params: tuple[Any, ...] = ()) -> int:
cur.execute(f"SELECT COUNT(*) FROM `{table}` WHERE {where}", params)
return int(cur.fetchone()[0])
def inspect_schema(cur) -> None:
cur.execute("SELECT DATABASE()")
database = cur.fetchone()[0]
if database != EXPECTED_DATABASE:
raise RuntimeError(f"refusing to run against database {database!r}")
for table in TABLES:
cur.execute("SHOW TABLES LIKE %s", (table,))
if not cur.fetchone():
raise RuntimeError(f"missing table `{table}`")
required = {
"wa_users": {"id"},
"eb_user": {"uid"},
"wa_order": set(),
"wa_withdraw": set(),
"eb_store_order": set(),
"wa_merchandise": {"id", "user_id", "created_at"},
"wa_selfbonus_log": {"user_id"},
"wa_sharebonus_log": {"user_id"},
"wa_coupon_log": {"user_id"},
"eb_user_integral_record": {"uid"},
}
for table, cols in required.items():
if not cols:
continue
cur.execute(f"SHOW COLUMNS FROM `{table}`")
actual = {row[0] for row in cur.fetchall()}
missing = cols - actual
if missing:
raise RuntimeError(f"table `{table}` missing columns: {sorted(missing)}")
def collect_counts(cur, keep_users: list[int], keep_merchandise_ids: list[int]) -> dict[str, dict[str, int]]:
counts: dict[str, dict[str, int]] = {}
user_clause = placeholders(keep_users)
merch_clause = placeholders(keep_merchandise_ids)
for table in CLEAR_TABLES:
total = count_where(cur, table)
counts[table] = {"before": total, "keep": 0, "delete": total}
for table, col in FILTERS.items():
total = count_where(cur, table)
keep = count_where(cur, table, f"`{col}` IN ({user_clause})", tuple(keep_users))
counts[table] = {"before": total, "keep": keep, "delete": total - keep}
total = count_where(cur, "wa_merchandise")
keep = (
count_where(cur, "wa_merchandise", f"`id` IN ({merch_clause})", tuple(keep_merchandise_ids))
if keep_merchandise_ids
else 0
)
counts["wa_merchandise"] = {"before": total, "keep": keep, "delete": total - keep}
return counts
def print_counts(title: str, counts: dict[str, dict[str, int]]) -> None:
print(title)
for table in TABLES:
c = counts[table]
print(f" {table}: before={c['before']} keep={c['keep']} delete={c['delete']}")
def backup_tables(config: dict[str, str], backup_path: Path) -> None:
backup_path.parent.mkdir(parents=True, exist_ok=True)
backup_conn = connect(config, cursorclass=SSCursor)
try:
with gzip.open(backup_path, "wt", encoding="utf-8") as out:
out.write("-- bygsf212 cleanup backup\n")
out.write(f"-- created_at: {datetime.now().isoformat(timespec='seconds')}\n")
out.write("SET NAMES utf8mb4;\n")
for table in TABLES:
with backup_conn.cursor() as cur:
cur.execute(f"SHOW CREATE TABLE `{table}`")
row = cur.fetchone()
create_sql = row[1]
out.write(f"\n-- Table `{table}`\n")
out.write(f"DROP TABLE IF EXISTS `{table}`;\n")
out.write(create_sql + ";\n")
with backup_conn.cursor() as cur:
cur.execute(f"SELECT * FROM `{table}`")
batch: list[str] = []
row_count = 0
for row in cur:
batch.append("(" + ",".join(backup_conn.literal(v) for v in row) + ")")
row_count += 1
if len(batch) >= 200:
out.write(f"INSERT INTO `{table}` VALUES\n")
out.write(",\n".join(batch) + ";\n")
batch = []
if batch:
out.write(f"INSERT INTO `{table}` VALUES\n")
out.write(",\n".join(batch) + ";\n")
print(f"backup {table}: rows={row_count}")
finally:
backup_conn.close()
def execute_cleanup(cur, keep_users: list[int], keep_merchandise_ids: list[int]) -> dict[str, int]:
user_clause = placeholders(keep_users)
deleted: dict[str, int] = {}
cur.execute("SET FOREIGN_KEY_CHECKS = 0")
for table in CLEAR_TABLES:
cur.execute(f"DELETE FROM `{table}`")
deleted[table] = cur.rowcount
if keep_merchandise_ids:
merch_clause = placeholders(keep_merchandise_ids)
cur.execute(f"DELETE FROM `wa_merchandise` WHERE `id` NOT IN ({merch_clause})", tuple(keep_merchandise_ids))
else:
cur.execute("DELETE FROM `wa_merchandise`")
deleted["wa_merchandise"] = cur.rowcount
for table in ["wa_selfbonus_log", "wa_sharebonus_log", "wa_coupon_log"]:
cur.execute(f"DELETE FROM `{table}` WHERE `user_id` NOT IN ({user_clause})", tuple(keep_users))
deleted[table] = cur.rowcount
cur.execute(f"DELETE FROM `eb_user_integral_record` WHERE `uid` NOT IN ({user_clause})", tuple(keep_users))
deleted["eb_user_integral_record"] = cur.rowcount
cur.execute(f"DELETE FROM `eb_user` WHERE `uid` NOT IN ({user_clause})", tuple(keep_users))
deleted["eb_user"] = cur.rowcount
cur.execute(f"DELETE FROM `wa_users` WHERE `id` NOT IN ({user_clause})", tuple(keep_users))
deleted["wa_users"] = cur.rowcount
cur.execute("SET FOREIGN_KEY_CHECKS = 1")
return deleted
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--execute", action="store_true", help="perform DELETEs and COMMIT")
parser.add_argument("--dump", action="append", type=Path, dest="dumps")
parser.add_argument("--backup-dir", type=Path, default=ROOT / "docs" / "sql" / "backups")
args = parser.parse_args()
dumps = args.dumps if args.dumps else DEFAULT_DUMPS
config, keep_users = parse_doc()
keep_merchandise_ids = extract_wa_merchandise_keep_ids(dumps, set(keep_users))
print(f"keep_user_ids={len(keep_users)} keep_wa_merchandise_ids={len(keep_merchandise_ids)}")
conn = connect(config)
try:
with conn.cursor() as cur:
inspect_schema(cur)
before = collect_counts(cur, keep_users, keep_merchandise_ids)
print_counts("before_counts", before)
if not args.execute:
print("dry_run_only=true")
conn.rollback()
return 0
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_path = args.backup_dir / f"bygsf212_cleanup_before_{stamp}.sql.gz"
print(f"backup_path={backup_path}")
backup_tables(config, backup_path)
with conn.cursor() as cur:
deleted = execute_cleanup(cur, keep_users, keep_merchandise_ids)
after = collect_counts(cur, keep_users, keep_merchandise_ids)
print("deleted_rows")
print(json.dumps(deleted, ensure_ascii=False, indent=2))
print_counts("after_counts_before_commit", after)
conn.commit()
print("COMMIT ok")
with conn.cursor() as cur:
final_counts = collect_counts(cur, keep_users, keep_merchandise_ids)
print_counts("final_counts", final_counts)
return 0
except Exception as e:
try:
with conn.cursor() as cur:
cur.execute("SET FOREIGN_KEY_CHECKS = 1")
except Exception:
pass
conn.rollback()
print(f"ROLLBACK: {e}", file=sys.stderr)
raise
finally:
conn.close()
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,372 @@
#!/usr/bin/env python3
"""Remove four conflict users from bygsf212.
Targets:
93251 龚华侨
93272 杜紅梅/杜红梅
93273 戴庆宏
93276 陈晓平
Default mode is a read-only dry run. Use --execute to back up affected rows,
delete related user data, and commit the transaction.
"""
from __future__ import annotations
import argparse
import gzip
import json
import re
import sys
from datetime import datetime
from pathlib import Path
from typing import Any
import pymysql
from pymysql.cursors import SSCursor
ROOT = Path(__file__).resolve().parents[2]
DOC = ROOT / "docs" / "com-bygsf212-data-imgration.md"
EXPECTED_DATABASE = "bygsf212"
TARGETS = {
93251: ("龚华侨", "15952530725"),
93272: ("杜紅梅", "13952547832"),
93273: ("戴庆宏", "15000637090"),
93276: ("陈晓平", "15995103126"),
}
# Tables that can contain rows owned by these users. Keep 0-row tables here so
# the script remains safe if the cleanup is run after new related data appears.
DELETE_CONDITIONS = {
"wa_order": ["`seller_id` IN ({ids}) OR `buyer_id` IN ({ids})"],
"wa_merchandise": ["`user_id` IN ({ids})"],
"wa_selfbonus_log": ["`user_id` IN ({ids})"],
"wa_sharebonus_log": ["`user_id` IN ({ids})"],
"wa_coupon_log": ["`user_id` IN ({ids})"],
"wa_withdraw": ["`user_id` IN ({ids})"],
"wa_address": ["`user_id` IN ({ids})"],
"wa_alipay": ["`user_id` IN ({ids})"],
"wa_bank": ["`user_id` IN ({ids})"],
"wa_money_log": ["`user_id` IN ({ids})"],
"eb_ali_pay_info": ["`seller_id` IN ({ids})"],
"eb_article": ["`uid` IN ({ids})"],
"eb_sms_record": ["`uid` IN ({ids})"],
"eb_store_bargain_user": ["`uid` IN ({ids})"],
"eb_store_bargain_user_help": ["`uid` IN ({ids})"],
"eb_store_cart": ["`uid` IN ({ids})"],
"eb_store_coupon_user": ["`uid` IN ({ids})"],
"eb_store_order": ["`uid` IN ({ids})"],
"eb_store_pink": ["`uid` IN ({ids})"],
"eb_store_product_log": ["`uid` IN ({ids}) OR `pay_uid` IN ({ids})"],
"eb_store_product_relation": ["`uid` IN ({ids})"],
"eb_store_product_reply": ["`uid` IN ({ids})"],
"eb_system_store_staff": ["`uid` IN ({ids})"],
"eb_user_address": ["`uid` IN ({ids})"],
"eb_user_bill": ["`uid` IN ({ids})"],
"eb_user_brokerage_record": ["`uid` IN ({ids})"],
"eb_user_experience_record": ["`uid` IN ({ids})"],
"eb_user_extract": ["`uid` IN ({ids})"],
"eb_user_integral_record": ["`uid` IN ({ids})"],
"eb_user_level": ["`uid` IN ({ids})"],
"eb_user_recharge": ["`uid` IN ({ids})"],
"eb_user_sign": ["`uid` IN ({ids})"],
"eb_user_token": ["`uid` IN ({ids})"],
"eb_user_visit_record": ["`uid` IN ({ids})"],
"t_platform_account": ["`user_id` IN ({ids})"],
"eb_user": ["`uid` IN ({ids})"],
"wa_users": ["`id` IN ({ids})"],
}
UPDATE_CONDITIONS = {
"wa_users": "`pid` IN ({ids}) AND `id` NOT IN ({ids})",
"eb_user": "`spread_uid` IN ({ids}) AND `uid` NOT IN ({ids})",
}
DELETE_ORDER = [
"wa_order",
"wa_merchandise",
"wa_selfbonus_log",
"wa_sharebonus_log",
"wa_coupon_log",
"wa_withdraw",
"wa_address",
"wa_alipay",
"wa_bank",
"wa_money_log",
"eb_ali_pay_info",
"eb_article",
"eb_sms_record",
"eb_store_bargain_user_help",
"eb_store_bargain_user",
"eb_store_cart",
"eb_store_coupon_user",
"eb_store_order",
"eb_store_pink",
"eb_store_product_log",
"eb_store_product_relation",
"eb_store_product_reply",
"eb_system_store_staff",
"eb_user_address",
"eb_user_bill",
"eb_user_brokerage_record",
"eb_user_experience_record",
"eb_user_extract",
"eb_user_integral_record",
"eb_user_level",
"eb_user_recharge",
"eb_user_sign",
"eb_user_token",
"eb_user_visit_record",
"t_platform_account",
"eb_user",
"wa_users",
]
def parse_doc_config() -> dict[str, str]:
text = DOC.read_text(encoding="utf-8")
def grab(name: str) -> str:
m = re.search(rf"^\s*{name}:\s*(.+?)\s*$", text, flags=re.M)
if not m:
raise ValueError(f"missing datasource {name} in {DOC}")
return m.group(1).strip()
return {
"host": grab("rds"),
"database": grab("name"),
"user": grab("username"),
"password": grab("password"),
}
def connect(config: dict[str, str], cursorclass=None):
kwargs = {
"host": config["host"],
"user": config["user"],
"password": config["password"],
"database": config["database"],
"charset": "utf8mb4",
"autocommit": False,
"connect_timeout": 10,
"read_timeout": 120,
"write_timeout": 120,
}
if cursorclass is not None:
kwargs["cursorclass"] = cursorclass
return pymysql.connect(**kwargs)
def ids() -> list[int]:
return list(TARGETS)
def ids_sql() -> str:
return ",".join(["%s"] * len(TARGETS))
def params_for(condition: str) -> tuple[int, ...]:
repeats = condition.count("{ids}")
return tuple(ids() * repeats)
def sql_condition(condition: str) -> str:
return condition.format(ids=ids_sql())
def table_exists(cur, table: str) -> bool:
cur.execute("SHOW TABLES LIKE %s", (table,))
return bool(cur.fetchone())
def collect_counts(cur) -> tuple[dict[str, int], dict[str, int]]:
delete_counts: dict[str, int] = {}
update_counts: dict[str, int] = {}
for table in DELETE_ORDER:
if not table_exists(cur, table):
continue
condition = " OR ".join(f"({sql_condition(c)})" for c in DELETE_CONDITIONS[table])
params: list[int] = []
for c in DELETE_CONDITIONS[table]:
params.extend(params_for(c))
cur.execute(f"SELECT COUNT(*) FROM `{table}` WHERE {condition}", tuple(params))
delete_counts[table] = int(cur.fetchone()[0])
for table, condition_template in UPDATE_CONDITIONS.items():
if not table_exists(cur, table):
continue
condition = sql_condition(condition_template)
cur.execute(f"SELECT COUNT(*) FROM `{table}` WHERE {condition}", params_for(condition_template))
update_counts[table] = int(cur.fetchone()[0])
return delete_counts, update_counts
def validate_targets(cur, require_present: bool) -> None:
cur.execute("SELECT DATABASE()")
database = cur.fetchone()[0]
if database != EXPECTED_DATABASE:
raise RuntimeError(f"refusing to run against database {database!r}")
clause = ids_sql()
cur.execute(f"SELECT id,nickname,mobile FROM `wa_users` WHERE `id` IN ({clause})", ids())
rows = {int(row[0]): (row[1], str(row[2])) for row in cur.fetchall()}
missing = sorted(set(TARGETS) - set(rows))
if missing:
message = f"target wa_users rows missing: {missing}"
if require_present:
raise RuntimeError(message)
print(f"{message}; treating as already removed in dry-run")
mismatched: list[dict[str, Any]] = []
for uid, (expected_name, expected_phone) in TARGETS.items():
if uid not in rows:
continue
actual_name, actual_phone = rows[uid]
if actual_phone != expected_phone:
mismatched.append(
{
"uid": uid,
"expected": [expected_name, expected_phone],
"actual": [actual_name, actual_phone],
}
)
if mismatched:
raise RuntimeError(f"target user phone mismatch: {json.dumps(mismatched, ensure_ascii=False)}")
def print_counts(title: str, counts: dict[str, int]) -> None:
print(title)
for table, count in counts.items():
if count:
print(f" {table}: {count}")
if not any(counts.values()):
print(" (all zero)")
def backup_rows(config: dict[str, str], backup_path: Path) -> None:
backup_path.parent.mkdir(parents=True, exist_ok=True)
backup_conn = connect(config, cursorclass=SSCursor)
try:
with gzip.open(backup_path, "wt", encoding="utf-8") as out:
out.write("-- bygsf212 four-user removal backup\n")
out.write(f"-- created_at: {datetime.now().isoformat(timespec='seconds')}\n")
out.write(f"-- target_ids: {','.join(str(x) for x in ids())}\n")
out.write("SET NAMES utf8mb4;\n")
tables = list(dict.fromkeys([*DELETE_ORDER, *UPDATE_CONDITIONS.keys()]))
for table in tables:
if not table_exists(backup_conn.cursor(), table):
continue
conditions: list[str] = []
params: list[int] = []
for c in DELETE_CONDITIONS.get(table, []):
conditions.append(f"({sql_condition(c)})")
params.extend(params_for(c))
if table in UPDATE_CONDITIONS:
c = UPDATE_CONDITIONS[table]
conditions.append(f"({sql_condition(c)})")
params.extend(params_for(c))
if not conditions:
continue
where = " OR ".join(conditions)
with backup_conn.cursor() as cur:
cur.execute(f"SHOW CREATE TABLE `{table}`")
create_sql = cur.fetchone()[1]
out.write(f"\n-- Table `{table}` affected rows\n")
out.write(f"-- Restore manually with INSERT statements below if needed.\n")
out.write(create_sql + ";\n")
with backup_conn.cursor() as cur:
cur.execute(f"SELECT * FROM `{table}` WHERE {where}", tuple(params))
batch: list[str] = []
row_count = 0
for row in cur:
batch.append("(" + ",".join(backup_conn.literal(v) for v in row) + ")")
row_count += 1
if len(batch) >= 200:
out.write(f"INSERT INTO `{table}` VALUES\n")
out.write(",\n".join(batch) + ";\n")
batch = []
if batch:
out.write(f"INSERT INTO `{table}` VALUES\n")
out.write(",\n".join(batch) + ";\n")
print(f"backup {table}: rows={row_count}")
finally:
backup_conn.close()
def execute_cleanup(cur) -> tuple[dict[str, int], dict[str, int]]:
updated: dict[str, int] = {}
deleted: dict[str, int] = {}
cur.execute("SET FOREIGN_KEY_CHECKS = 0")
condition = sql_condition(UPDATE_CONDITIONS["wa_users"])
cur.execute(f"UPDATE `wa_users` SET `pid` = 0 WHERE {condition}", params_for(UPDATE_CONDITIONS["wa_users"]))
updated["wa_users.pid"] = cur.rowcount
condition = sql_condition(UPDATE_CONDITIONS["eb_user"])
cur.execute(
f"UPDATE `eb_user` SET `spread_uid` = 0, `spread_time` = NULL WHERE {condition}",
params_for(UPDATE_CONDITIONS["eb_user"]),
)
updated["eb_user.spread_uid"] = cur.rowcount
for table in DELETE_ORDER:
if not table_exists(cur, table):
continue
condition = " OR ".join(f"({sql_condition(c)})" for c in DELETE_CONDITIONS[table])
params: list[int] = []
for c in DELETE_CONDITIONS[table]:
params.extend(params_for(c))
cur.execute(f"DELETE FROM `{table}` WHERE {condition}", tuple(params))
deleted[table] = cur.rowcount
cur.execute("SET FOREIGN_KEY_CHECKS = 1")
return updated, deleted
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--execute", action="store_true", help="perform cleanup and commit")
parser.add_argument("--backup-dir", type=Path, default=ROOT / "docs" / "sql" / "backups")
args = parser.parse_args()
config = parse_doc_config()
conn = connect(config)
try:
with conn.cursor() as cur:
validate_targets(cur, require_present=args.execute)
delete_counts, update_counts = collect_counts(cur)
print(f"target_ids={ids()}")
print_counts("delete_counts", delete_counts)
print_counts("external_reference_update_counts", update_counts)
if not args.execute:
print("dry_run_only=true")
conn.rollback()
return 0
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_path = args.backup_dir / f"bygsf212_remove_93251_93272_93273_93276_before_{stamp}.sql.gz"
print(f"backup_path={backup_path}")
backup_rows(config, backup_path)
with conn.cursor() as cur:
updated, deleted = execute_cleanup(cur)
print("updated_rows")
print(json.dumps(updated, ensure_ascii=False, indent=2))
print("deleted_rows")
print(json.dumps(deleted, ensure_ascii=False, indent=2))
after_deletes, after_updates = collect_counts(cur)
print_counts("after_delete_counts_before_commit", after_deletes)
print_counts("after_update_counts_before_commit", after_updates)
conn.commit()
print("COMMIT ok")
return 0
except Exception as exc:
try:
with conn.cursor() as cur:
cur.execute("SET FOREIGN_KEY_CHECKS = 1")
except Exception:
pass
conn.rollback()
print(f"ROLLBACK: {exc}", file=sys.stderr)
raise
finally:
conn.close()
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,388 @@
#!/usr/bin/env python3
"""Run data cleanup for docs/com-sqszx202-data-imgration.md.
Default mode is a read-only dry run. Use --execute to create a local SQL backup,
delete rows according to the migration document, and commit the transaction.
"""
from __future__ import annotations
import argparse
import gzip
import json
import re
import sys
from datetime import datetime
from pathlib import Path
from typing import Any
import pymysql
from pymysql.cursors import SSCursor
ROOT = Path(__file__).resolve().parents[2]
DOC = ROOT / "docs" / "com-sqszx202-data-imgration.md"
DEFAULT_DUMP = Path("/Users/mac/Works26/miao-july/宿迁盛泽鑫/anpengran-yangtangyoupin_2026-06-14_02-15-02_mysql_data.sql")
CUTOFF = "2026-06-12 00:00:00"
TABLES = [
"wa_order",
"wa_withdraw",
"eb_store_order",
"wa_merchandise",
"wa_selfbonus_log",
"wa_sharebonus_log",
"wa_coupon_log",
"eb_user_integral_record",
"eb_user",
"wa_users",
]
FILTERS = {
"wa_users": "id",
"eb_user": "uid",
"wa_selfbonus_log": "user_id",
"wa_sharebonus_log": "user_id",
"wa_coupon_log": "user_id",
"eb_user_integral_record": "uid",
}
CLEAR_TABLES = ["wa_order", "wa_withdraw", "eb_store_order"]
def parse_doc() -> tuple[dict[str, str], list[int]]:
text = DOC.read_text(encoding="utf-8")
def grab(name: str) -> str:
m = re.search(rf"^\s*{name}:\s*(.+?)\s*$", text, flags=re.M)
if not m:
raise ValueError(f"missing datasource {name} in {DOC}")
return m.group(1).strip()
ids_match = re.search(r"保留名单:\s*\n\s*`([^`]+)`", text)
if not ids_match:
raise ValueError(f"missing user id keep list in {DOC}")
ids = [int(x.strip()) for x in ids_match.group(1).split(",") if x.strip()]
if len(ids) != len(set(ids)):
raise ValueError("duplicate ids in keep list")
config = {
"host": grab("rds"),
"database": grab("name"),
"user": grab("username"),
"password": grab("password"),
}
return config, ids
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 == "\\":
j += 2
continue
if c == "'":
if j + 1 < n and values_blob[j + 1] == "'":
j += 2
continue
in_quote = False
j += 1
continue
if c == "'":
in_quote = True
elif 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:
cur.append(inner[i + 1])
i += 2
continue
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
out.append("".join(cur).strip())
return out
def unquote_sql_string(raw: str) -> str:
raw = raw.strip()
if raw.startswith("'") and raw.endswith("'"):
body = raw[1:-1]
body = body.replace("''", "'")
body = body.replace("\\'", "'").replace("\\\\", "\\")
return body
return raw
def extract_wa_merchandise_keep_ids(dump: Path, keep_users: set[int]) -> list[int]:
if not dump.is_file():
raise FileNotFoundError(f"dump not found: {dump}")
keep_ids: set[int] = set()
total_rows = 0
insert_lines = 0
with dump.open("r", encoding="utf-8", errors="replace") as f:
for line in f:
if "INSERT INTO `wa_merchandise`" not in line or "VALUES" not in line:
continue
insert_lines += 1
blob = line[line.index("VALUES") + len("VALUES") :].strip()
if blob.endswith(";"):
blob = blob[:-1].strip()
for tup in split_top_level_tuples(blob):
total_rows += 1
fields = split_mysql_fields(tup.strip()[1:-1])
if len(fields) < 10:
raise ValueError(f"malformed wa_merchandise tuple: {tup[:120]}")
row_id = int(fields[0])
user_id = int(fields[2])
created_at = unquote_sql_string(fields[8])
if created_at >= CUTOFF and user_id in keep_users:
keep_ids.add(row_id)
if insert_lines == 0:
raise ValueError("no INSERT INTO `wa_merchandise` found in dump")
print(f"dump_wa_merchandise_rows={total_rows} keep_by_rule={len(keep_ids)}")
return sorted(keep_ids)
def connect(config: dict[str, str], cursorclass=None):
kwargs = {
"host": config["host"],
"user": config["user"],
"password": config["password"],
"database": config["database"],
"charset": "utf8mb4",
"autocommit": False,
"read_timeout": 120,
"write_timeout": 120,
}
if cursorclass is not None:
kwargs["cursorclass"] = cursorclass
return pymysql.connect(**kwargs)
def placeholders(items: list[int] | set[int]) -> str:
if not items:
return "NULL"
return ",".join(["%s"] * len(items))
def count_where(cur, table: str, where: str = "1=1", params: tuple[Any, ...] = ()) -> int:
cur.execute(f"SELECT COUNT(*) FROM `{table}` WHERE {where}", params)
return int(cur.fetchone()[0])
def inspect_schema(cur) -> None:
cur.execute("SELECT DATABASE()")
database = cur.fetchone()[0]
if database != "sqszx202":
raise RuntimeError(f"refusing to run against database {database!r}")
for table in TABLES:
cur.execute("SHOW TABLES LIKE %s", (table,))
if not cur.fetchone():
raise RuntimeError(f"missing table `{table}`")
required = {
"wa_users": {"id"},
"eb_user": {"uid"},
"wa_order": set(),
"wa_withdraw": set(),
"eb_store_order": set(),
"wa_merchandise": {"id", "user_id", "created_at"},
"wa_selfbonus_log": {"user_id"},
"wa_sharebonus_log": {"user_id"},
"wa_coupon_log": {"user_id"},
"eb_user_integral_record": {"uid"},
}
for table, cols in required.items():
if not cols:
continue
cur.execute(f"SHOW COLUMNS FROM `{table}`")
actual = {row[0] for row in cur.fetchall()}
missing = cols - actual
if missing:
raise RuntimeError(f"table `{table}` missing columns: {sorted(missing)}")
def collect_counts(cur, keep_users: list[int], keep_merchandise_ids: list[int]) -> dict[str, dict[str, int]]:
counts: dict[str, dict[str, int]] = {}
user_clause = placeholders(keep_users)
merch_clause = placeholders(keep_merchandise_ids)
for table in CLEAR_TABLES:
total = count_where(cur, table)
counts[table] = {"before": total, "keep": 0, "delete": total}
for table, col in FILTERS.items():
total = count_where(cur, table)
keep = count_where(cur, table, f"`{col}` IN ({user_clause})", tuple(keep_users))
counts[table] = {"before": total, "keep": keep, "delete": total - keep}
total = count_where(cur, "wa_merchandise")
keep = (
count_where(cur, "wa_merchandise", f"`id` IN ({merch_clause})", tuple(keep_merchandise_ids))
if keep_merchandise_ids
else 0
)
counts["wa_merchandise"] = {"before": total, "keep": keep, "delete": total - keep}
return counts
def print_counts(title: str, counts: dict[str, dict[str, int]]) -> None:
print(title)
for table in TABLES:
c = counts[table]
print(f" {table}: before={c['before']} keep={c['keep']} delete={c['delete']}")
def backup_tables(config: dict[str, str], backup_path: Path) -> None:
backup_path.parent.mkdir(parents=True, exist_ok=True)
backup_conn = connect(config, cursorclass=SSCursor)
try:
with gzip.open(backup_path, "wt", encoding="utf-8") as out:
out.write("-- sqszx202 cleanup backup\n")
out.write(f"-- created_at: {datetime.now().isoformat(timespec='seconds')}\n")
out.write("SET NAMES utf8mb4;\n")
for table in TABLES:
with backup_conn.cursor() as cur:
cur.execute(f"SHOW CREATE TABLE `{table}`")
row = cur.fetchone()
create_sql = row[1]
out.write(f"\n-- Table `{table}`\n")
out.write(f"DROP TABLE IF EXISTS `{table}`;\n")
out.write(create_sql + ";\n")
with backup_conn.cursor() as cur:
cur.execute(f"SELECT * FROM `{table}`")
batch: list[str] = []
row_count = 0
for row in cur:
batch.append("(" + ",".join(backup_conn.literal(v) for v in row) + ")")
row_count += 1
if len(batch) >= 200:
out.write(f"INSERT INTO `{table}` VALUES\n")
out.write(",\n".join(batch) + ";\n")
batch = []
if batch:
out.write(f"INSERT INTO `{table}` VALUES\n")
out.write(",\n".join(batch) + ";\n")
print(f"backup {table}: rows={row_count}")
finally:
backup_conn.close()
def execute_cleanup(cur, keep_users: list[int], keep_merchandise_ids: list[int]) -> dict[str, int]:
user_clause = placeholders(keep_users)
merch_clause = placeholders(keep_merchandise_ids)
deleted: dict[str, int] = {}
cur.execute("SET FOREIGN_KEY_CHECKS = 0")
for table in CLEAR_TABLES:
cur.execute(f"DELETE FROM `{table}`")
deleted[table] = cur.rowcount
cur.execute(f"DELETE FROM `wa_merchandise` WHERE `id` NOT IN ({merch_clause})", tuple(keep_merchandise_ids))
deleted["wa_merchandise"] = cur.rowcount
for table in ["wa_selfbonus_log", "wa_sharebonus_log", "wa_coupon_log"]:
cur.execute(f"DELETE FROM `{table}` WHERE `user_id` NOT IN ({user_clause})", tuple(keep_users))
deleted[table] = cur.rowcount
cur.execute(f"DELETE FROM `eb_user_integral_record` WHERE `uid` NOT IN ({user_clause})", tuple(keep_users))
deleted["eb_user_integral_record"] = cur.rowcount
cur.execute(f"DELETE FROM `eb_user` WHERE `uid` NOT IN ({user_clause})", tuple(keep_users))
deleted["eb_user"] = cur.rowcount
cur.execute(f"DELETE FROM `wa_users` WHERE `id` NOT IN ({user_clause})", tuple(keep_users))
deleted["wa_users"] = cur.rowcount
cur.execute("SET FOREIGN_KEY_CHECKS = 1")
return deleted
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--execute", action="store_true", help="perform DELETEs and COMMIT")
parser.add_argument("--dump", type=Path, default=DEFAULT_DUMP)
parser.add_argument("--backup-dir", type=Path, default=ROOT / "docs" / "sql" / "backups")
args = parser.parse_args()
config, keep_users = parse_doc()
keep_merchandise_ids = extract_wa_merchandise_keep_ids(args.dump, set(keep_users))
print(f"keep_user_ids={len(keep_users)} keep_wa_merchandise_ids={len(keep_merchandise_ids)}")
conn = connect(config)
try:
with conn.cursor() as cur:
inspect_schema(cur)
before = collect_counts(cur, keep_users, keep_merchandise_ids)
print_counts("before_counts", before)
if not args.execute:
print("dry_run_only=true")
conn.rollback()
return 0
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_path = args.backup_dir / f"sqszx202_cleanup_before_{stamp}.sql.gz"
print(f"backup_path={backup_path}")
backup_tables(config, backup_path)
with conn.cursor() as cur:
deleted = execute_cleanup(cur, keep_users, keep_merchandise_ids)
after = collect_counts(cur, keep_users, keep_merchandise_ids)
print("deleted_rows")
print(json.dumps(deleted, ensure_ascii=False, indent=2))
print_counts("after_counts_before_commit", after)
conn.commit()
print("COMMIT ok")
with conn.cursor() as cur:
final_counts = collect_counts(cur, keep_users, keep_merchandise_ids)
print_counts("final_counts", final_counts)
return 0
except Exception as e:
try:
with conn.cursor() as cur:
cur.execute("SET FOREIGN_KEY_CHECKS = 1")
except Exception:
pass
conn.rollback()
print(f"ROLLBACK: {e}", file=sys.stderr)
raise
finally:
conn.close()
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,68 @@
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
from openpyxl.utils import get_column_letter
OUTPUT = "outputs/screenshot_process_table/小程序上线流程表.xlsx"
headers = ["阶段", "任务", "产出物", "负责人", "建议时长"]
rows = [
["需求与原型", "需求澄清、功能清单、原型评审", "PRD、原型图", "产品", "2~3天"],
["UI设计", "界面设计、切图标注", "设计稿、切图", "UI", "3~4天"],
["开发", "前端+后端联调、自测", "代码包", "前端/后端", "6~8天"],
["测试与修复", "功能测试、兼容性、回归", "测试报告", "测试", "3~4天"],
["小程序提审", "提交代码、填写审核信息", "审核中", "产品/运营", "1~7天官方不等"],
["发布上线", "审核通过后全量/灰度发布", "线上版本", "产品/运营", "1天"],
]
wb = Workbook()
ws = wb.active
ws.title = "流程表"
ws.append(headers)
for row in rows:
ws.append(row)
header_fill = PatternFill("solid", fgColor="F3F6FA")
grid = Side(style="thin", color="D9DEE7")
border = Border(bottom=grid)
for cell in ws[1]:
cell.font = Font(name="Arial", bold=True, size=12, color="111827")
cell.fill = header_fill
cell.alignment = Alignment(horizontal="left", vertical="center")
cell.border = border
for row in ws.iter_rows(min_row=2, max_row=ws.max_row):
for cell in row:
cell.font = Font(name="Arial", size=11, color="111827")
cell.alignment = Alignment(horizontal="left", vertical="center", wrap_text=True)
cell.border = border
widths = {
"A": 16,
"B": 34,
"C": 22,
"D": 16,
"E": 22,
}
for col, width in widths.items():
ws.column_dimensions[col].width = width
for row_idx in range(1, ws.max_row + 1):
ws.row_dimensions[row_idx].height = 28
ws.freeze_panes = "A2"
ws.auto_filter.ref = f"A1:{get_column_letter(ws.max_column)}{ws.max_row}"
wb.save(OUTPUT)
check = load_workbook(OUTPUT)
sheet = check["流程表"]
assert sheet.max_row == 7
assert sheet.max_column == 5
assert sheet["A2"].value == "需求与原型"
assert sheet["E6"].value == "1~7天官方不等"
print(OUTPUT)

View File

@@ -274,10 +274,98 @@
}
// #endif
view {
view,
v-uni-view {
box-sizing: border-box;
}
/* H5 build emits v-uni-* tags; give them the same base layout as uni-* tags. */
v-uni-view,
v-uni-scroll-view,
v-uni-swiper,
v-uni-picker,
v-uni-form,
v-uni-label {
display: block;
box-sizing: border-box;
}
v-uni-view[hidden],
v-uni-image[hidden],
v-uni-scroll-view[hidden],
v-uni-swiper[hidden],
v-uni-swiper-item[hidden],
v-uni-text[hidden],
v-uni-button[hidden] {
display: none;
}
v-uni-text {
display: inline;
box-sizing: border-box;
white-space: pre-wrap;
}
v-uni-image {
display: inline-block;
position: relative;
overflow: hidden;
box-sizing: border-box;
line-height: 0;
}
v-uni-image > div,
v-uni-image > img {
width: 100%;
height: 100%;
display: block;
}
v-uni-image > div {
background-repeat: no-repeat;
}
v-uni-scroll-view {
position: relative;
-webkit-overflow-scrolling: touch;
}
v-uni-scroll-view[scroll-x],
v-uni-scroll-view[scroll-y] {
overflow: auto;
}
v-uni-swiper {
position: relative;
height: 150px;
overflow: hidden;
}
v-uni-swiper-item {
display: block;
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
}
v-uni-button {
position: relative;
display: block;
box-sizing: border-box;
margin-left: auto;
margin-right: auto;
padding-left: 14px;
padding-right: 14px;
text-align: center;
text-decoration: none;
line-height: 2.55555556;
border-radius: 5px;
-webkit-tap-highlight-color: transparent;
overflow: hidden;
cursor: pointer;
}
.bg-color-red {
background-color: #E93323;
}
@@ -300,4 +388,4 @@
height: 0;
color: transparent;
}
</style>
</style>

View File

@@ -454,7 +454,7 @@ export function uploadFile(filePath, uploadUrl = 'upload/image') {
});
}
export function uploadUserImage(filePath, pid, model = 'user') {
export function uploadUserImage(filePath, userId, model = 'user') {
return new Promise((resolve, reject) => {
const token = uni.getStorageSync('token');
uni.uploadFile({
@@ -466,7 +466,7 @@ export function uploadUserImage(filePath, pid, model = 'user') {
},
formData: {
model,
pid
userId
},
success: (res) => {
try {
@@ -560,4 +560,3 @@ export default {
uploadFile,
uploadUserImage
};

View File

@@ -0,0 +1,7 @@
module.exports = {
presets: ['@vue/cli-plugin-babel/preset'],
plugins: [
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-nullish-coalescing-operator'
]
}

View File

@@ -6,8 +6,8 @@
// let domain = 'https://jfanyue.szxingming.com'
// let domain = 'https://jf.wenjinhui.com'
// let domain = 'https://jjy-jf.fwxgpt.com'
// czleilei240 项目
let domain = 'https://leilei-jf.czchunfang.com'
// sqszx202 项目
let domain = 'https://jf.j3s4s5.com'
// let domain = 'https://jf.jinyawen.com'
// let domain = 'https://jf.hapengran.com'
// let domain = 'https://jjy-jf.uj345.com'
@@ -19,7 +19,7 @@ module.exports = {
// HTTP_REQUEST_URL:'',
HTTP_REQUEST_URL: domain,
// H5商城地址
HTTP_H5_URL: 'https://leilei-jf.czchunfang.com',
HTTP_H5_URL: 'https://jf.j3s4s5.com',
// #endif
// #ifdef H5
HTTP_REQUEST_URL:domain,

File diff suppressed because it is too large Load Diff

View File

@@ -6,16 +6,30 @@
"build": "vue-cli-service build"
},
"dependencies": {
"@dcloudio/uni-app": "^2.0.2-4080420251103001",
"@dcloudio/uni-h5": "^2.0.2-4080420251103001",
"@dcloudio/uni-stat": "^2.0.2-4080420251103001",
"mp-html": "^2.5.0",
"vue": "^3.5.24"
"sass": "^1.69.5",
"sass-loader": "^10.4.1",
"vue": "^2.6.14",
"vue-template-compiler": "^2.6.14",
"vuex": "^3.6.2"
},
"devDependencies": {
"cross-env": "^7.0.3",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
"@dcloudio/uni-cli-i18n": "^2.0.2-4080420251103001",
"@dcloudio/uni-cli-shared": "^2.0.2-4080420251103001",
"@dcloudio/uni-i18n": "^2.0.2-4080420251103001",
"@dcloudio/uni-migration": "^2.0.2-4080420251103001",
"@dcloudio/uni-template-compiler": "^2.0.2-4080420251103001",
"@dcloudio/vue-cli-plugin-uni": "^2.0.0",
"@dcloudio/vue-cli-plugin-uni": "^2.0.2-4080420251103001",
"@dcloudio/webpack-uni-pages-loader": "^2.0.2-4080420251103001",
"@vue/cli-service": "^5.0.9"
"@vue/cli-plugin-babel": "^4.5.19",
"@vue/cli-service": "^4.5.19",
"cross-env": "^7.0.3",
"vue-loader": "^15.11.1",
"webpack": "^4.46.0"
}
}

View File

@@ -34,7 +34,7 @@
export default {
data() {
return {
pdfUrl: '/static/sign_contract_czleilei240.pdf',
pdfUrl: '/static/sign_contract_sqszx202.pdf',
userId: '',
isMobile: false,
usePdfJs: false,
@@ -436,4 +436,3 @@ export default {
box-shadow: 0 8rpx 20rpx rgba(255, 45, 45, 0.25);
}
</style>

View File

@@ -634,12 +634,13 @@ export default {
min-height: 100vh;
background-color: #F8F8F8;
padding-bottom: 20rpx;
overflow-x: hidden;
}
// 顶部橙色头部
.header-section {
background: linear-gradient(90deg, #FF6900 0%, #F54900 100%);
padding: 60rpx 30rpx 60rpx 1px;
padding: 60rpx 30rpx;
position: relative;
overflow: hidden;
margin-bottom: 1rpx;
@@ -739,14 +740,34 @@ export default {
font-weight: 600;
color: #FFFFFF;
}
.banner-section {
padding: 20rpx;
background-color: #FFFFFF;
}
.banner-swiper {
width: 100%;
height: 260rpx;
border-radius: 16rpx;
overflow: hidden;
}
.banner-image {
display: block;
width: 100%;
height: 100%;
}
// 分类导航
.category-scroll {
white-space: nowrap; font-size: 32rpx;
white-space: nowrap;
font-size: 32rpx;
background-color: #FFFFFF;
height: 108rpx; padding: 10rpx 20rpx;
margin-bottom: 20rpx;
border-bottom: 1px solid #EEEEEE;
box-sizing: border-box;
}
.category-item {
@@ -807,6 +828,7 @@ export default {
}
.goods-image {
display: block;
width: 100%;
height: 100%;
}
@@ -910,6 +932,7 @@ export default {
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100rpx;
background-color: #FFFFFF;
display: flex;
@@ -945,6 +968,7 @@ export default {
}
.tab-icon {
display: block;
width: 44rpx;
height: 44rpx;
// background-color: #CCCCCC; // Placeholder
@@ -1039,6 +1063,7 @@ export default {
}
.empty-image {
display: block;
width: 300rpx;
height: 300rpx;
}

View File

@@ -343,12 +343,12 @@ export default {
// window.location.href = 'https://shop.wenjinhui.com/?#/pages/personal/index'
//window.location.href = 'https://anyue.szxingming.com/?#/pages/personal/index'
// window.location.href = 'https://xiashengjun.com/?#/pages/personal/index'
window.location.href = 'https://leilei.czchunfang.com/?#/pages/personal/index'
window.location.href = 'https://j3s4s5.com/?#/pages/personal/index'
// window.location.href = 'http://shop.bosenyuan.com/?#/pages/personal/index'
// #endif
// #ifndef H5
uni.navigateTo({
url: '/pages/web-view/index?url=' + encodeURIComponent('https://leilei.czchunfang.com/?#/pages/personal/index')
url: '/pages/web-view/index?url=' + encodeURIComponent('https://j3s4s5.com/?#/pages/personal/index')
})
// #endif
},
@@ -921,4 +921,3 @@ export default {
line-height: 48rpx;
}
</style>

View File

@@ -16,7 +16,7 @@ export default {
},
onLoad(options) {
const url = options && options.url ? decodeURIComponent(options.url) : '/static/sign_contract_czleilei240.pdf'
const url = options && options.url ? decodeURIComponent(options.url) : '/static/sign_contract_sqszx202.pdf'
this.pdfUrl = url
},
@@ -70,4 +70,3 @@ export default {
box-shadow: 0 8rpx 20rpx rgba(255, 45, 45, 0.25);
}
</style>

View File

@@ -364,7 +364,7 @@ export default {
// 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) : '')
window.location.href = 'https://leilei.czchunfang.com/?#/pages/rushing/index' + (this.userId ? ('?user_id=' + this.userId) : '')
window.location.href = 'https://j3s4s5.com/?#/pages/rushing/index' + (this.userId ? ('?user_id=' + this.userId) : '')
// window.location.href = 'https://shop.uj345.com/?#/pages/rushing/index' + (this.userId ? ('?user_id=' + this.userId) : '')
}, 1000)
// 返回签名信息给上一页面
@@ -498,4 +498,3 @@ export default {
.btn.primary { background: #FF2D2D; }
.btn-text { color: #fff; font-size: 28rpx; }
</style>

View File

@@ -0,0 +1,36 @@
const postcss = require('postcss')
const uniPostcss = require('@dcloudio/vue-cli-plugin-uni/packages/postcss')
function toVw(value) {
return value.replace(/%\?([+-]?\d+(?:\.\d+)?)\?%/g, (match, number) => {
const rpx = Number(number)
if (!Number.isFinite(rpx)) {
return match
}
const vw = Number((rpx / 7.5).toFixed(6)).toString()
return vw === '0' ? '0' : `${vw}vw`
})
}
const rpxPlaceholderToVw = postcss.plugin('rpx-placeholder-to-vw', () => {
return root => {
root.walkDecls(decl => {
if (decl.value && decl.value.includes('%?')) {
decl.value = toVw(decl.value)
}
})
root.walkAtRules(atRule => {
if (atRule.params && atRule.params.includes('%?')) {
atRule.params = toVw(atRule.params)
}
})
}
})
module.exports = {
plugins: [
uniPostcss(),
rpxPlaceholderToVw()
]
}

View File

@@ -1,10 +1,10 @@
@font-face {
font-family: 'dinProSemiBold';
src: url('static/fonts/D-DIN-PRO-600-SemiBold.otf');
src: url('./D-DIN-PRO-600-SemiBold.otf');
}
@font-face {
font-family: 'dinProRegular';
src: url('static/fonts/D-DIN-PRO-400-Regular.otf');
src: url('./D-DIN-PRO-400-Regular.otf');
}
.semiBold{
@@ -16,4 +16,4 @@
.pingFang{
font-family: 'PingFang SC, PingFang SC';
font-weight: 500;
}
}

View File

@@ -43,6 +43,56 @@
(function() {
var iframe = document.getElementById('iframe');
function safeDecode(value) {
try {
return decodeURIComponent((value || '').replace(/\+/g, ' '));
} catch (e) {
return value || '';
}
}
function buildQuery(params) {
var pairs = [];
for (var key in params) {
if (!Object.prototype.hasOwnProperty.call(params, key)) {
continue;
}
if (params[key] === undefined || params[key] === null || params[key] === '') {
continue;
}
pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key]));
}
return pairs.join('&');
}
function parseQuery(query, ignoredKey) {
var result = {
params: {},
ignoredValue: ''
};
if (!query) {
return result;
}
var pairs = query.split('&');
for (var i = 0; i < pairs.length; i++) {
if (!pairs[i]) {
continue;
}
var pair = pairs[i].split('=');
var key = safeDecode(pair[0]);
var value = pair.length > 1 ? safeDecode(pair.slice(1).join('=')) : '';
if (!key) {
continue;
}
if (key === ignoredKey) {
result.ignoredValue = result.ignoredValue || value;
continue;
}
result.params[key] = value;
}
return result;
}
// 获取URL参数
function getUrlParams() {
var params = {};
@@ -51,35 +101,79 @@
var pairs = search.split('&');
for (var i = 0; i < pairs.length; i++) {
var pair = pairs[i].split('=');
params[decodeURIComponent(pair[0])] = pair[1] ? decodeURIComponent(pair[1]) : '';
var key = safeDecode(pair[0]);
if (!key) {
continue;
}
params[key] = pair.length > 1 ? safeDecode(pair.slice(1).join('=')) : '';
}
}
return params;
}
function normalizeHash(rawHash, username) {
var hash = rawHash || '';
if (hash.charAt(0) === '#') {
hash = hash.substring(1);
}
if (!hash && !username) {
return '';
}
var questionIndex = hash.indexOf('?');
var path = questionIndex >= 0 ? hash.substring(0, questionIndex) : hash;
var query = questionIndex >= 0 ? hash.substring(questionIndex + 1) : '';
var parsed = parseQuery(query, 'username');
var finalUsername = username || parsed.ignoredValue;
if (!path) {
path = '/pages/integral/index';
}
if (finalUsername) {
parsed.params.username = finalUsername;
}
var queryString = buildQuery(parsed.params);
return '#' + path + (queryString ? '?' + queryString : '');
}
function removeHashParam(rawHash, key) {
var hash = rawHash || '';
if (hash.charAt(0) === '#') {
hash = hash.substring(1);
}
if (!hash) {
return '';
}
var questionIndex = hash.indexOf('?');
var path = questionIndex >= 0 ? hash.substring(0, questionIndex) : hash;
var query = questionIndex >= 0 ? hash.substring(questionIndex + 1) : '';
var parsed = parseQuery(query, key);
var queryString = buildQuery(parsed.params);
return '#' + path + (queryString ? '?' + queryString : '');
}
function getParentHashFromIframe(iframeHash) {
var params = getUrlParams();
if (params.username) {
return removeHashParam(iframeHash, 'username');
}
return normalizeHash(iframeHash, '');
}
function updateParentUrl(iframeHash) {
var parentHash = getParentHashFromIframe(iframeHash);
if (parentHash && window.location.hash !== parentHash) {
history.replaceState(null, '', window.location.search + parentHash);
}
}
// 初始化根据父页面URL设置iframe的src
function initIframeSrc() {
var params = getUrlParams();
var hash = window.location.hash;
var iframeSrc = '/';
// 如果父页面有hash传递给iframe
if (hash) {
iframeSrc = '/' + hash;
}
// 如果有username参数附加到iframe URL
if (params.username) {
var separator = iframeSrc.includes('?') ? '&' : (iframeSrc.includes('#') ? (iframeSrc.includes('?') ? '&' : '?') : '?');
// 对于hash模式参数需要放在hash后面
if (hash) {
iframeSrc = iframeSrc + (iframeSrc.includes('?') ? '&' : '?') + 'username=' + encodeURIComponent(params.username);
} else {
iframeSrc = '/#/pages/integral/index?username=' + encodeURIComponent(params.username);
}
}
iframe.src = iframeSrc;
var iframeHash = normalizeHash(window.location.hash, params.username);
iframe.src = iframeHash ? '/' + iframeHash : '/';
}
// 监听iframe内部路由变化同步到父页面URL
@@ -90,19 +184,12 @@
// 监听iframe的hashchange事件
iframeWindow.addEventListener('hashchange', function() {
var iframeHash = iframeWindow.location.hash;
if (iframeHash && window.location.hash !== iframeHash) {
// 保留原有的search参数
var currentSearch = window.location.search;
history.replaceState(null, '', currentSearch + iframeHash);
}
updateParentUrl(iframeHash);
});
// 初始同步
var iframeHash = iframeWindow.location.hash;
if (iframeHash && window.location.hash !== iframeHash) {
var currentSearch = window.location.search;
history.replaceState(null, '', currentSearch + iframeHash);
}
updateParentUrl(iframeHash);
} catch (e) {
console.log('无法访问iframe内容可能跨域:', e);
}
@@ -111,11 +198,12 @@
// 监听父页面hash变化同步到iframe
window.addEventListener('hashchange', function() {
try {
var parentHash = window.location.hash;
var params = getUrlParams();
var parentHash = normalizeHash(window.location.hash, params.username);
var iframeHash = iframe.contentWindow.location.hash;
if (parentHash !== iframeHash) {
iframe.contentWindow.location.hash = parentHash;
iframe.contentWindow.location.hash = parentHash || '#/pages/integral/index';
}
} catch (e) {
console.log('无法同步hash到iframe:', e);
@@ -130,10 +218,7 @@
setInterval(function() {
try {
var iframeHash = iframe.contentWindow.location.hash;
if (iframeHash && window.location.hash !== iframeHash) {
var currentSearch = window.location.search;
history.replaceState(null, '', currentSearch + iframeHash);
}
updateParentUrl(iframeHash);
} catch (e) {}
}, 500);
};
@@ -143,12 +228,8 @@
if (window.innerWidth <= 420) {
// 小屏幕直接跳转到应用
var params = getUrlParams();
var targetUrl = '/';
if (params.username) {
targetUrl = '/#/pages/integral/index?username=' + encodeURIComponent(params.username);
} else if (window.location.hash) {
targetUrl = '/' + window.location.hash;
}
var targetHash = normalizeHash(window.location.hash, params.username);
var targetUrl = targetHash ? '/' + targetHash : '/';
window.location.href = targetUrl;
}
};

Binary file not shown.

Binary file not shown.