Skip to content

Commit

Permalink
支持自动驻守任务,支持配置文件,支持docker
Browse files Browse the repository at this point in the history
  • Loading branch information
zc2638 committed Apr 14, 2022
1 parent 4641fba commit a2700dd
Show file tree
Hide file tree
Showing 9 changed files with 644 additions and 166 deletions.
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,27 @@
## 安装
### Releases
[Github Release](https://github.com/zc2638/ddshop/releases) 下载
### Docker
```shell
docker pull zc2638/ddshop:latest
```
### 源码
```shell
go install github.com/zc2638/ddshop/cmd/ddshop@latest
```

## 使用
### 命令行工具
1. 使用抓包工具获取 叮咚买菜上的用户 `cookie` (DDXQSESSID)
2. 使用获取到的 `cookie` 替换下面命令中的 `<custom-cookie>`
```shell
ddshop --cookie <custom-cookie>
```

自定义请求间隔(ms), 默认为 500.
接口连续请求的时间间隔,太频繁容易被封
使用配置文件,需要先创建配置文件(可参考 [配置详情](./config/config.yaml)
`<custom-config-path>` 替换为实际的配置文件路径,例如:`config/config.yaml`
```shell
ddshop --cookie <custom-cookie> --interval 500
ddshop -c <custom-config-path>
```

支持预设置支付方式
Expand All @@ -40,6 +45,16 @@ Bark推送提醒 [点击查看详情](https://github.com/Finb/Bark)
```shell
ddshop --cookie <custom-cookie> --bark-key <custom-bark-key>
```
### Docker
环境变量
```shell
docker run --name ddshop -it -e DDSHOP_COOKIE=<custom-cookie> -e DDSHOP_PAYTYPE=wechat -e DDSHOP_BARKKEY= zc2638/ddshop
```
配置文件方式,将 `<custom-config-dir>` 替换成宿主机存放配置文件的目录
详细配置项请点击 [配置详情](./config/config.yaml)
```shell
docker run --name ddshop -it -v <custom-config-dir>:/work/config zc2638/ddshop
```

## 抓包
[Charles抓包教程](https://www.jianshu.com/p/ff85b3dac157)
Expand Down
32 changes: 32 additions & 0 deletions build/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
FROM golang:1.17 as builder

RUN apt-get update && apt-get install -y libasound2-dev

ENV GOPROXY=https://goproxy.cn,https://goproxy.io,direct
ENV GO111MODULE=on

WORKDIR /go/cache
ADD go.mod .
ADD go.sum .
RUN go mod download

WORKDIR /work
ADD . .
RUN GOOS=linux CGO_ENABLED=1 GOARCH=amd64 go build -ldflags="-w -s" -o /usr/local/bin/ddshop github.com/zc2638/ddshop/cmd/ddshop

FROM alpine:3.6
MAINTAINER zc
LABEL maintainer="zc" \
email="[email protected]"

ENV TZ="Asia/Shanghai"

RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories && \
apk update && \
apk --no-cache add tzdata ca-certificates libc6-compat libgcc libstdc++ alsa-lib-dev

COPY --from=builder /usr/local/bin/ddshop /usr/local/bin/ddshop
COPY --from=builder /work/config/config.yaml /work/config/config.yaml

WORKDIR /work
CMD ["ddshop", "-c", "config/config.yaml"]
169 changes: 27 additions & 142 deletions cmd/ddshop/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,183 +15,68 @@
package app

import (
"context"
"errors"
"fmt"
"math/rand"
"os"
"time"

"github.com/sirupsen/logrus"
"github.com/spf13/viper"

"github.com/pkgms/go/server"

"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"

"github.com/zc2638/ddshop/asserts"
"github.com/zc2638/ddshop/core"
"github.com/zc2638/ddshop/pkg/notice"
)

type Option struct {
Config string
Cookie string
BarkKey string
PayType string
Interval int64
}

var (
successCh = make(chan struct{}, 1)
errCh = make(chan error, 1)
)

func NewRootCommand() *cobra.Command {
opt := &Option{}
cmd := &cobra.Command{
Use: "ddshop",
Short: "Ding Dong grocery shopping automatic order program",
Short: "叮咚买菜自动抢购下单程序",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if opt.Cookie == "" {
return errors.New("请输入用户Cookie.\n你可以执行此命令 `ddshop --cookie xxx` 或者 `DDSHOP_COOKIE=xxx ddshop`")
cfg := &core.Config{
Cookie: opt.Cookie,
Interval: opt.Interval,
BarkKey: opt.BarkKey,
PayType: opt.PayType,
}

session := core.NewSession(opt.Cookie, opt.Interval)
if err := session.GetUser(); err != nil {
return fmt.Errorf("获取用户信息失败: %v", err)
if opt.Config != "" {
viper.SetConfigType("yaml")
if err := server.ParseConfigWithEnv(opt.Config, cfg, "DDSHOP"); err != nil {
return err
}
} else {
logrus.Warning("未设置配置文件,使用参数解析")
}
if err := session.Choose(opt.PayType); err != nil {
return err
if cfg.Cookie == "" {
return errors.New("请输入用户Cookie.\n你可以执行此命令 `ddshop --cookie xxx` 或者 `DDSHOP_COOKIE=xxx ddshop`")
}
fmt.Println()

ctx, cancelFunc := context.WithCancel(context.Background())
go func() {
for {
select {
case <-ctx.Done():
logrus.Warningf("context done")
return
default:
}
if err := Start(session); err != nil {
switch err {
case core.ErrorNoValidProduct:
logrus.Error("购物车中无有效商品,请先前往app添加或勾选!")
errCh <- err
return
case core.ErrorNoReserveTime:
sleepInterval := 3 + rand.Intn(6)
logrus.Warningf("暂无可预约的时间,%d 秒后重试!", sleepInterval)
time.Sleep(time.Duration(sleepInterval) * time.Second)
default:
logrus.Error(err)
}
fmt.Println()
}
}
}()

select {
case resultErr := <-errCh:
cancelFunc()
go func() {
if opt.BarkKey == "" {
return
}
ins := notice.NewBark(opt.BarkKey)
if err := ins.Send("抢菜异常", resultErr.Error()); err != nil {
logrus.Warningf("Bark消息通知失败: %v", err)
}
}()
return resultErr
case <-successCh:
cancelFunc()
core.LoopRun(10, func() {
logrus.Info("抢菜成功,请尽快支付!")
})

go func() {
if opt.BarkKey == "" {
return
}
ins := notice.NewBark(opt.BarkKey)
if err := ins.Send("抢菜成功", "叮咚买菜 抢菜成功,请尽快支付!"); err != nil {
logrus.Warningf("Bark消息通知失败: %v", err)
}
}()

if err := asserts.Play(); err != nil {
logrus.Warningf("播放成功提示音乐失败: %v", err)
}
// 异步放歌,歌曲有3分钟
time.Sleep(3 * time.Minute)
return nil
session, err := core.NewSession(cfg)
if err != nil {
return err
}
return session.Start()
},
}

configEnv := os.Getenv("DDSHOP_CONFIG")
cookieEnv := os.Getenv("DDSHOP_COOKIE")
barkKeyEnv := os.Getenv("DDSHOP_BARKKEY")
payTypeEnv := os.Getenv("DDSHOP_PAYTYPE")
cmd.Flags().StringVarP(&opt.Config, "config", "c", configEnv, "设置配置文件路径")
cmd.Flags().StringVar(&opt.Cookie, "cookie", cookieEnv, "设置用户个人cookie")
cmd.Flags().StringVar(&opt.BarkKey, "bark-key", barkKeyEnv, "设置bark的通知key")
cmd.Flags().StringVar(&opt.PayType, "pay-type", payTypeEnv, "设置支付方式,支付宝、微信、alipay、wechat")
cmd.Flags().Int64Var(&opt.Interval, "interval", 500, "设置请求间隔时间(ms),默认为100")
cmd.Flags().Int64Var(&opt.Interval, "interval", 100, "设置请求间隔时间(ms),默认为100")
return cmd
}

func Start(session *core.Session) error {
logrus.Info("=====> 获取购物车中有效商品")

if err := session.CartAllCheck(); err != nil {
return fmt.Errorf("全选购物车商品失败: %v", err)
}
cartData, err := session.GetCart()
if err != nil {
return err
}

products := cartData["products"].([]map[string]interface{})
for k, v := range products {
logrus.Infof("[%v] %s 数量:%v 总价:%s", k, v["product_name"], v["count"], v["total_price"])
}

for {
logrus.Info("=====> 获取可预约时间")
multiReserveTime, err := session.GetMultiReserveTime(products)
if err != nil {
return fmt.Errorf("获取可预约时间失败: %v", err)
}
if len(multiReserveTime) == 0 {
return core.ErrorNoReserveTime
}
logrus.Infof("发现可用的配送时段!")

logrus.Info("=====> 生成订单信息")
checkOrderData, err := session.CheckOrder(cartData, multiReserveTime)
if err != nil {
return fmt.Errorf("检查订单失败: %v", err)
}
logrus.Infof("订单总金额:%v\n", checkOrderData["price"])

var wg errgroup.Group
for _, reserveTime := range multiReserveTime {
sess := session.Clone()
sess.SetReserve(reserveTime)
wg.Go(func() error {
startTime := time.Unix(int64(sess.Reserve.StartTimestamp), 0).Format("2006/01/02 15:04:05")
endTime := time.Unix(int64(sess.Reserve.EndTimestamp), 0).Format("2006/01/02 15:04:05")
timeRange := startTime + "——" + endTime
logrus.Infof("=====> 提交订单中, 预约时间段(%s)", timeRange)
if err := sess.CreateOrder(context.Background(), cartData, checkOrderData); err != nil {
logrus.Warningf("提交订单(%s)失败: %v", timeRange, err)
return err
}

successCh <- struct{}{}
return nil
})
}
_ = wg.Wait()
return nil
}
}
9 changes: 9 additions & 0 deletions config/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
cookie: ""
interval: 100 # 连续发起请求间隔时间(ms)
payType: "wechat" # 支付方式:支付宝、alipay、微信、wechat
barkKey: "" # Bark 通知推送的 Key
#periods: # 抢购时间段
# - start: "05:59"
# end: "06:10"
# - start: "08:29"
# end: "08:35"
33 changes: 33 additions & 0 deletions core/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright © 2022 zc2638 <[email protected]>.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package core

type Config struct {
Cookie string `json:"cookie"`
Interval int64 `json:"interval"`
PayType string `json:"payType"`
BarkKey string `json:"barkKey"`
Periods []TimePeriod `json:"periods"`
}

type TimePeriod struct {
Start string `json:"start"`
End string `json:"end"`

startHour int
startMinute int
endHour int
endMinute int
}
2 changes: 2 additions & 0 deletions core/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ func (e Error) Error() string {
}

const (
ErrorOutPeriod = Error("当前时间段未抢到")

ErrorNoValidProduct = Error("无有效商品")
ErrorNoReserveTime = Error("无可预约时间段")
)
Loading

0 comments on commit a2700dd

Please sign in to comment.