This commit is contained in:
2025-03-30 11:40:50 +08:00
parent d597483902
commit c6076dee43
48 changed files with 4383 additions and 1 deletions

25
.dockerignore Normal file
View File

@@ -0,0 +1,25 @@
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/.idea
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

39
.gitignore vendored Normal file
View File

@@ -0,0 +1,39 @@
#Ignore thumbnails created by Windows
Thumbs.db
#Ignore files built by Visual Studio
*.obj
*.exe
*.pdb
*.user
*.aps
*.pch
*.vspscc
*_i.c
*_p.c
*.ncb
*.suo
*.tlb
*.tlh
*.bak
*.cache
*.ilk
*.log
[Bb]in
[Dd]ebug*/
*.lib
*.sbr
obj/
[Rr]elease*/
_ReSharper*/
[Tt]est[Rr]esult*
.vs/
#Nuget packages folder
packages/
.vscode/tasks.json
.vscode/launch.json
.vscode/settings.json
*.orig
.idea/
wwwroot/

38
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,38 @@
# This file is a template, and might need editing before it works on your project.
# This is a sample GitLab CI/CD configuration file that should run without any modifications.
# It demonstrates a basic 3 stage CI/CD pipeline. Instead of real tests or scripts,
# it uses echo commands to simulate the pipeline execution.
#
# A pipeline is composed of independent jobs that run scripts, grouped into stages.
# Stages run in sequential order, but jobs within stages run in parallel.
#
# For more information, see: https://docs.gitlab.com/ee/ci/yaml/index.html#stages
#
# You can copy and paste this template into a new `.gitlab-ci.yml` file.
# You should not add this template to an existing `.gitlab-ci.yml` file by using the `include:` keyword.
#
# To contribute improvements to CI/CD templates, please follow the Development guide at:
# https://docs.gitlab.com/ee/development/cicd/templates.html
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Getting-Started.gitlab-ci.yml
stages: # List of stages for jobs, and their order of execution
- build
- deploy
build-job: # This job runs in the build stage, which runs first.
stage: build
script:
- echo "starting build..."
- docker compose down || true
- docker rmi jd-coupons || true
- docker compose up --build -d
- echo "build completed."
deploy-job: # This job runs in the deploy stage.
stage: deploy # It only runs when *both* jobs in the test stage complete successfully.
environment: production
script:
- echo "deploying..."
- docker compose up -d
- echo "deploy completed."

1
1
View File

@@ -1 +0,0 @@
1

28
Dockerfile Normal file
View File

@@ -0,0 +1,28 @@
FROM node:22.12.0 AS frontend
RUN npm install -g vite
COPY . .
WORKDIR /src/Front
RUN npm i
RUN npm run build
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
COPY . .
RUN dotnet restore "/src/Web/Web.csproj"
WORKDIR "/src/Web"
RUN dotnet build "Web.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "Web.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Web.dll"]

93
README.md Normal file
View File

@@ -0,0 +1,93 @@
# JdCoupons
## Getting started
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Add your files
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
```
cd existing_repo
git remote add origin http://c1283ff797ac/suncheng/jdcoupons.git
git branch -M main
git push -uf origin main
```
## Integrate with your tools
- [ ] [Set up project integrations](http://c1283ff797ac/suncheng/jdcoupons/-/settings/integrations)
## Collaborate with your team
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Test and Deploy
Use the built-in continuous integration in GitLab.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.

54
docker-compose.yaml Normal file
View File

@@ -0,0 +1,54 @@
services:
jd-coupons-proxy:
image: beevelop/nginx-basic-auth:latest
container_name: jd-coupons-proxy
restart: always
networks:
- all_in
ports:
- "27482:80"
environment:
- TZ=Asia/Shanghai
- HTPASSWD=suncheng:$$apr1$$2QX32QHP$$HIGAbCuTt8jxdc4uDzNLI1
- FORWARD_PORT=8080
- FORWARD_HOST=jd-coupons
jd-h5st-proxy:
image: beevelop/nginx-basic-auth:latest
container_name: jd-h5st-proxy
restart: always
networks:
- all_in
ports:
- "27483:80"
environment:
- TZ=Asia/Shanghai
- HTPASSWD=suncheng:$$apr1$$2QX32QHP$$HIGAbCuTt8jxdc4uDzNLI1
- FORWARD_PORT=3001
- FORWARD_HOST=jd-h5st
jd-coupons:
image: jd-coupons
build:
context: .
dockerfile: ./Dockerfile
container_name: jd-coupons
restart: always
networks:
- all_in
environment:
- TZ=Asia/Shanghai
jd-h5st:
image: youfak/jd_h5st_server:latest
container_name: jd-h5st
restart: always
networks:
- all_in
environment:
- TZ=Asia/Shanghai
networks:
all_in:
external: true

31
jdcoupons.sln Normal file
View File

@@ -0,0 +1,31 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{22C5F48D-3205-4A5D-A814-144C181C520C}"
ProjectSection(SolutionItems) = preProject
docker-compose.yaml = docker-compose.yaml
.gitlab-ci.yml = .gitlab-ci.yml
Dockerfile = Dockerfile
.dockerignore = .dockerignore
.gitignore = .gitignore
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Web", "src\Web\Web.csproj", "{66A8DE76-1439-4BB5-861D-E37BDD75E350}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{66A8DE76-1439-4BB5-861D-E37BDD75E350}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{66A8DE76-1439-4BB5-861D-E37BDD75E350}.Debug|Any CPU.Build.0 = Debug|Any CPU
{66A8DE76-1439-4BB5-861D-E37BDD75E350}.Release|Any CPU.ActiveCfg = Release|Any CPU
{66A8DE76-1439-4BB5-861D-E37BDD75E350}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

6
src/Front/.editorconfig Normal file
View File

@@ -0,0 +1,6 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

30
src/Front/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,7 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}

8
src/Front/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode"
]
}

35
src/Front/README.md Normal file
View File

@@ -0,0 +1,35 @@
# test
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

View File

@@ -0,0 +1,19 @@
import js from '@eslint/js'
import pluginVue from 'eslint-plugin-vue'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
export default [
{
name: 'app/files-to-lint',
files: ['**/*.{js,mjs,jsx,vue}'],
},
{
name: 'app/files-to-ignore',
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
},
js.configs.recommended,
...pluginVue.configs['flat/essential'],
skipFormatting,
]

13
src/Front/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

8
src/Front/jsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

2581
src/Front/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
src/Front/package.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "test",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --fix",
"format": "prettier --write src/"
},
"dependencies": {
"element-plus": "^2.9.0",
"pinia": "^2.2.6",
"vue": "^3.5.13",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@eslint/js": "^9.14.0",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/eslint-config-prettier": "^10.1.0",
"eslint": "^9.14.0",
"eslint-plugin-vue": "^9.30.0",
"prettier": "^3.3.3",
"vite": "^6.0.1",
"vite-plugin-vue-devtools": "^7.6.5"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

39
src/Front/src/App.vue Normal file
View File

@@ -0,0 +1,39 @@
<script setup>
import { RouterLink, RouterView } from 'vue-router'
</script>
<template>
<el-container style="height: 100vh;width: 100vw">
<el-header>
<el-tabs v-model="activeName" @tab-click="handleClick">
<el-tab-pane label="Cookie" name="cookie"></el-tab-pane>
<el-tab-pane label="店铺" name="store"></el-tab-pane>
</el-tabs>
</el-header>
<el-main>
<RouterView />
</el-main>
</el-container>
</template>
<script>
export default {
data() {
return {
activeName: 'cookie'
};
},
methods: {
handleClick(tab, event) {
console.log(tab,event);
RouterLink.push({ name: tab.name });
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,85 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 0;
}
}

17
src/Front/src/main.js Normal file
View File

@@ -0,0 +1,17 @@
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(ElementPlus)
app.use(createPinia())
app.use(router)
app.mount('#app')

View File

@@ -0,0 +1,23 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/AboutView.vue'),
},
],
})
export default router

View File

@@ -0,0 +1,12 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

View File

@@ -0,0 +1,9 @@
<template>
<div>
2
</div>
</template>
<style>
</style>

View File

@@ -0,0 +1,3 @@
<template>
<div>1</div>
</template>

21
src/Front/vite.config.js Normal file
View File

@@ -0,0 +1,21 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
build: {
outDir: '../Web/wwwroot',
},
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})

View File

@@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Web.Services;
namespace Web.Controllers;
[ApiController]
[Route("api/[controller]/[action]")]
public class ManualController(
IManualService manualService
) : ControllerBase
{
[HttpGet]
public async Task<string> GetServiceTimeAsync()
{
try
{
var time = await manualService.GetServiceTimeAsync();
return time.ToString("yyyy-MM-dd HH:mm:ss.fff");
}
catch (Exception e)
{
throw new Exception("GetServiceTimeAsync failed", e);
}
}
[HttpPost]
public async Task<string> GetCouponAsync([FromQuery] string cookieName, [FromQuery] string storeName, [FromQuery] int couponIndex)
{
try
{
var result = await manualService.GetCoupon(cookieName, storeName, couponIndex);
return JsonConvert.SerializeObject(result, Formatting.Indented);
}
catch (Exception e)
{
throw new Exception("GetCoupon failed", e);
}
}
}

View File

@@ -0,0 +1,10 @@
namespace Web.Interfaces.Jd.Dto;
public record CouponParamDto
{
public string StoreUrl { get; set; } = null!;
public string CouponUrl { get; set; } = null!;
public string Cookie { get; set; } = null!;
}

View File

@@ -0,0 +1,29 @@
namespace Web.Interfaces.Jd.Dto;
public record CouponResultDto
{
public int? Code { get; set; }
public CouponResultDataDto? Data { get; set; }
public string? Message { get; set; }
public int? SubCode { get; set; }
}
public record CouponResultDataDto
{
public long? BatchId { get; set; }
public decimal? Discount { get; set; }
public decimal? Quota { get; set; }
public long? BeginTime { get; set; }
public long? EndTime { get; set; }
public long ResultCode { get; set; }
public string ResultMsg { get; set; } = null!;
}

View File

@@ -0,0 +1,283 @@
using System.IO.Compression;
using System.Net.Http.Headers;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Web.Interfaces.Jd.Dto;
using Web.Utils;
namespace Web.Interfaces.Jd;
public interface IJdInterface
{
Task<DateTime> GetServiceTimeAsync();
Task<CouponResultDto?> GetCouponAsync(CouponParamDto param);
}
public class JdInterface(
IHttpClientFactory httpClientFactory,
ILogger<JdInterface> logger
) : IJdInterface
{
public async Task<DateTime> GetServiceTimeAsync()
{
var client = httpClientFactory.CreateClient(Constants.JdMobileApiClient);
var postContent = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "appid", "yinliu" },
{ "functionId", "yinliu_service_display" },
{ "loginType", "2" },
{ "_", GetTimespan().ToString() },
{ "cthr", "1" },
{ "body", "{\"busUrl\":\"https://m.jd.com/\",\"functionName\":\"DISPLAY\"}" }
});
postContent.Headers.Add("origin", "https://m.jd.com");
postContent.Headers.Add("sec-fetch-dest", "empty");
postContent.Headers.Add("sec-fetch-mode", "cors");
postContent.Headers.Add("sec-fetch-site", "same-site");
postContent.Headers.Add("x-referer-page", "https://m.jd.com/");
var response = await client.PostAsync("/", postContent);
var responseHeaders = response.Headers;
if (!responseHeaders.TryGetValues("x-api-request-id", out var dateValues))
{
logger.LogError("x-api-request-id not found, 使用本地时间");
return DateTime.Now;
}
var param = dateValues.FirstOrDefault()?.Split("-", StringSplitOptions.RemoveEmptyEntries);
if (param is not { Length: 3 })
{
logger.LogError("x-api-request-id 格式错误, 使用本地时间");
return DateTime.Now;
}
if (!long.TryParse(param[2], out var time))
{
logger.LogError("x-api-request-id 时间戳解析失败, 使用本地时间");
return DateTime.Now;
}
logger.LogInformation($"x-api-request-id: {time}, 与本地时间差: {time - GetTimespan()}");
return DateTimeOffset.FromUnixTimeMilliseconds(time).LocalDateTime;
}
public async Task<CouponResultDto?> GetCouponAsync(CouponParamDto param)
{
var vender = new Uri(param.StoreUrl).Query.Split(["&", "?"], StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(x => x.Contains("venderId="));
if (vender == null)
{
throw new Exception("venderid not found by store url \r\n" + param.StoreUrl);
}
var venderid = vender.Split("=")[1];
if (int.TryParse(venderid, out var venderidInt) == false)
{
throw new Exception("venderid value not found by store url \r\n" + param.StoreUrl);
}
var couponUrl = new Uri(param.CouponUrl);
var to = couponUrl.Query.Split(["&", "?"], StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(x => x.Contains("to="));
if (to == null)
{
throw new Exception("to not found by coupon url \r\n" + param.CouponUrl);
}
var toValue = to.Split("=")[1];
if (string.IsNullOrEmpty(toValue))
{
throw new Exception("to value not found by coupon url \r\n" + param.CouponUrl);
}
var roleId = couponUrl.Query.Split(["&", "?"], StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(x => x.Contains("roleId="));
if (roleId == null)
{
throw new Exception("roleId not found by coupon url \r\n" + param.CouponUrl);
}
var roleIdValue = roleId.Split("=")[1];
if (string.IsNullOrEmpty(roleIdValue))
{
throw new Exception("roleId value not found by coupon url \r\n" + param.CouponUrl);
}
var key = couponUrl.Query.Split(["&", "?"], StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(x => x.Contains("key="));
if (key == null)
{
throw new Exception("key not found by coupon url \r\n" + param.CouponUrl);
}
var keyValue = key.Split("=")[1];
if (string.IsNullOrEmpty(keyValue))
{
throw new Exception("key value not found by coupon url \r\n" + param.CouponUrl);
}
var body = new
{
key = keyValue,
roleId = roleIdValue,
linkKey = "",
to = toValue,
venderid = venderidInt
};
var appid = "h5_awake_wxapp";
var functionId = "mcoupon_getcoupon";
var cookieDic = GetCookieDic(param.Cookie);
var client = httpClientFactory.CreateClient(Constants.JdMobileApiClient);
var ua = client.DefaultRequestHeaders.UserAgent.ToString();
var (h5St, token, appCode) = await GetH5St(body, ua, cookieDic);
// post url 参数传递
var postContent = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "appid", appid },
{ "functionId", functionId },
{ "body", JsonConvert.SerializeObject(body) },
{ "h5st", h5St },
{ "x-api-eid-token", token },
{ "loginType", "2" },
{ "client", "wh5" },
{ "t", GetTimespan().ToString() },
{ "_stk", "appid,body,client,functionId,t" },
{ "_ste", "1" },
{ "g_login_type", "1" },
{ "appCode", appCode },
{ "g_ty", "ajax" },
{ "_", GetTimespan().ToString() },
{ "sceneval", "2" }
});
// 设置
postContent.Headers.Add("authority", "api.m.jd.com");
postContent.Headers.Add("origin", "https://coupon.m.jd.com");
postContent.Headers.Add("sec-fetch-dest", "empty");
postContent.Headers.Add("sec-fetch-mode", "cors");
postContent.Headers.Add("sec-fetch-site", "same-site");
postContent.Headers.Add("x-referer-page", "https://coupon.m.jd.com/coupons/show.action");
postContent.Headers.Add("x-rp-client", "h5_1.0.0");
postContent.Headers.Add("cookie", cookieDic.Select(x => $"{x.Key}={x.Value}").ToArray());
postContent.Headers.Add("priority", "u=1, i");
var apiUrl = "https://api.m.jd.com/client.action";
var apiResponse = await client.PostAsync(apiUrl, postContent);
var contentEncoding = apiResponse.Content.Headers.ContentEncoding;
var responseBytes = await apiResponse.Content.ReadAsByteArrayAsync();
string apiResponseSting;
if (contentEncoding.Contains("gzip"))
{
using var compressedStream = new MemoryStream(responseBytes);
await using var decompressionStream = new GZipStream(compressedStream, CompressionMode.Decompress);
using var reader = new StreamReader(decompressionStream, Encoding.UTF8);
apiResponseSting = await reader.ReadToEndAsync();
}
else
{
apiResponseSting = Encoding.UTF8.GetString(responseBytes);
}
return JsonConvert.DeserializeObject<CouponResultDto>(apiResponseSting);
}
private async Task<(
string h5st,
string token,
string appCode
)> GetH5St(
object body,
string ua,
IDictionary<string, string> cookieDic)
{
using var client = httpClientFactory.CreateClient(Constants.H5StClient);
var appid = "h5_awake_wxapp";
var functionId = "mcoupon_getcoupon";
if (cookieDic.TryGetValue("cd_eid", out var token) == false
|| string.IsNullOrWhiteSpace(token))
{
throw new Exception("cookie cd_eid not found by cookie \r\n" + string.Join("\r\n", cookieDic.Select(x => $"{x.Key}={x.Value}")));
}
if (cookieDic.TryGetValue("pt_pin", out var pin) == false
|| string.IsNullOrWhiteSpace(pin))
{
throw new Exception("cookie pt_pin not found by cookie \r\n" + string.Join("\r\n", cookieDic.Select(x => $"{x.Key}={x.Value}")));
}
if (cookieDic.TryGetValue("appCode", out var appCode) == false
|| string.IsNullOrWhiteSpace(appCode))
{
throw new Exception("cookie appCode not found by cookie \r\n" + string.Join("\r\n", cookieDic.Select(x => $"{x.Key}={x.Value}")));
}
var request = new
{
client = "wh5",
appId = appid,
version = "4.9.1",
pin,
ua,
body = new
{
functionId,
appid = appCode,
body = JsonConvert.SerializeObject(body)
}
};
var requestContent = JsonContent.Create(request);
var h5StResponse = await client.PostAsync("/h5st", requestContent);
var h5StJson = await h5StResponse.Content.ReadAsStringAsync();
var h5StObj = JsonConvert.DeserializeObject<JObject>(h5StJson);
var h5St = h5StObj?["body"]?["h5st"]?["h5st"]?.ToString();
if (string.IsNullOrEmpty(h5St))
{
throw new Exception("h5st not found by h5st response \r\n" + h5StJson);
}
return (
h5St,
token,
appCode
);
}
private static IDictionary<string, string> GetCookieDic(string cookie)
{
try
{
var arr = cookie.Split(";").Select(x => x.Trim()).Where(x => !string.IsNullOrEmpty(x));
return arr.Select(x => x.Split("=")).ToDictionary(x => x[0], x => x[1]);
}
catch (Exception e)
{
throw new Exception("cookie error", e);
}
}
private static long GetTimespan()
{
return DateTimeOffset.Now.ToUnixTimeMilliseconds();
}
}

92
src/Web/Program.cs Normal file
View File

@@ -0,0 +1,92 @@
using System.Net.Http.Headers;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Web.Interfaces.Jd;
using Web.Utils;
using Web.Services;
using Web.Services.Dto;
using Web.Stores;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<ConfigStore>(services =>
{
// 从 wwwroot/config.json中加载配置到 stores
var config = File.ReadAllText("Stores/config.json");
var configStore = JsonConvert.DeserializeObject<JObject>(config);
if (configStore == null)
{
throw new Exception("config.json is null");
}
var cookies = configStore["cookies"]?.ToObject<CookieDto[]>();
var stores = configStore["stores"]?.ToObject<StoreDto[]>();
if (cookies == null || stores == null)
{
throw new Exception("cookies or stores is null");
}
ConfigStore store = new();
store.Init(cookies, stores);
return store;
});
builder.Services.AddSingleton<IManualService, ManualService>();
builder.Services.AddSingleton<IJdInterface, JdInterface>();
builder.Services.AddHostedService<JobTimeService>();
builder.Services.AddHostedService<JobService>();
builder.Services.AddHttpClient(Constants.JdMobileApiClient, client =>
{
client.BaseAddress = new Uri("https://api.m.jd.com");
client.DefaultRequestHeaders.Add("accept", "application/json");
client.DefaultRequestHeaders.Add("accept-encoding", "gzip, deflate, br, zstd");
client.DefaultRequestHeaders.Add("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6");
client.DefaultRequestHeaders.Add("referer", "https://coupon.m.jd.com");
client.DefaultRequestHeaders.Add("user-agent", RandomUtil.GetUserAgent()); // TODO 研究下 ua 的刷新时机
});
builder.Services.AddHttpClient(Constants.H5StClient, (provider, client) =>
{
var configuration = provider.GetRequiredService<IConfiguration>();
var host = configuration["Interfaces:H5StService:Url"];
var username = configuration["Interfaces:H5StService:UserName"];
var password = configuration["Interfaces:H5StService:Password"];
if (string.IsNullOrEmpty(host))
{
throw new Exception("Interfaces:H5StService is null");
}
if (!string.IsNullOrEmpty(username)
&& !string.IsNullOrEmpty(password))
{
var auth = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", auth);
}
client.BaseAddress = new Uri(host);
});
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.UseDefaultFiles();
app.UseStaticFiles();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
app.MapControllers();
app.Run();

View File

@@ -0,0 +1,14 @@
{
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5017",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,21 @@
namespace Web.Services.Dto;
public class CookieDto
{
public DateTime Date { get; set; }
public string Desc { get; set; } = null!;
public string Cookie { get; set; } = null!;
}
public class StoreDto
{
public DateTime Date { get; set; }
public string Desc { get; set; } = null!;
public string Url { get; set; } = null!;
public (string url, string cron)[] Coupons { get; set; } = [];
}

View File

@@ -0,0 +1,127 @@
using Cronos;
using Newtonsoft.Json;
using Web.Interfaces.Jd;
using Web.Interfaces.Jd.Dto;
using Web.Stores;
namespace Web.Services;
public class JobService(
ConfigStore configDto,
IJdInterface jdInterface,
ILogger<JobService> logger
) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await DoWork();
// 每100ms 再执行下一轮
await Task.Delay(100, stoppingToken);
}
}
private static Dictionary<(string storeDesc, string coupon), DateTime> _lastRunTimes = new();
private async Task DoWork()
{
foreach (var store in configDto.Stores)
{
foreach (var coupon in store.Coupons)
{
if (!_lastRunTimes.ContainsKey((store.Desc, coupon.url)))
{
_lastRunTimes.Add((store.Desc, coupon.url), default);
}
var cron = GetCronExpression(coupon.cron);
if (cron == null)
{
continue;
}
// 下次执行时间赋值 为jd时间-1s 后计算 cron 时间
var nextRunTime = cron.GetNextOccurrence(JdTime.ServiceTime)?.AddSeconds(-1);
if (nextRunTime == null)
{
continue;
}
logger.LogInformation($"下次执行时间: {nextRunTime.Value} {store.Desc} {coupon.url}");
_lastRunTimes[(store.Desc, coupon.url)] = nextRunTime.Value;
}
}
var tasks = new List<(object request, Task<CouponResultDto?> response)>();
foreach (var item in _lastRunTimes)
{
if (item.Value > JdTime.ServiceTime || item.Value == default)
{
continue;
}
foreach (var cookie in configDto.Cookies)
{
var storeUrl = configDto.Stores.FirstOrDefault(x => x.Desc == item.Key.storeDesc)?.Url;
if (storeUrl == null)
{
logger.LogError("storeUrl not found");
continue;
}
var task = jdInterface.GetCouponAsync(new CouponParamDto
{
Cookie = cookie.Cookie,
CouponUrl = item.Key.coupon,
StoreUrl = storeUrl
});
tasks.Add((new
{
cookie = cookie.Desc,
store = item.Key.storeDesc,
item.Key.coupon,
DateTime.Now
}, task));
}
}
if (tasks.Count == 0)
{
return;
}
await Task.WhenAll(tasks.Select(x => x.response));
foreach (var task in tasks)
{
if (await task.response == null)
{
continue;
}
logger.LogInformation($"领取优惠券结果: {JsonConvert.SerializeObject(task.request)} {await task.response}");
}
}
private CronExpression? GetCronExpression(string cron)
{
try
{
return CronExpression.Parse(cron);
}
catch (Exception e)
{
logger.LogError(e, "GetCronExpression failed");
return null;
}
}
}

View File

@@ -0,0 +1,22 @@
using Web.Interfaces.Jd;
using Web.Stores;
namespace Web.Services;
public class JobTimeService(
IJdInterface jdInterface
) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var jdTime = await jdInterface.GetServiceTimeAsync();
JdTime.ServiceTime = jdTime;
// 每分钟执行一次
await Task.Delay(60000, stoppingToken);
}
}
}

View File

@@ -0,0 +1,54 @@
using Web.Interfaces.Jd;
using Web.Interfaces.Jd.Dto;
using Web.Stores;
namespace Web.Services;
public interface IManualService
{
Task<CouponResultDto?> GetCoupon(string cookieName, string storeName, int couponIndex);
Task<DateTime> GetServiceTimeAsync();
}
public class ManualService(
ConfigStore configStore,
IJdInterface jdInterface
) : IManualService
{
public async Task<CouponResultDto?> GetCoupon(string cookieName, string storeName, int couponIndex)
{
var cookie = configStore.Cookies.FirstOrDefault(x => x.Desc == cookieName)?.Cookie;
if (string.IsNullOrEmpty(cookie))
{
throw new Exception("cookie not found");
}
var store = configStore.Stores.FirstOrDefault(x => x.Desc == storeName);
if (store == null)
{
throw new Exception("store not found");
}
if (couponIndex < 0 || couponIndex >= store.Coupons.Length)
{
throw new Exception("coupon index out of range");
}
var coupon = store.Coupons[couponIndex];
var result = await jdInterface.GetCouponAsync(new CouponParamDto
{
Cookie = cookie,
CouponUrl = coupon.url,
StoreUrl = store.Url
});
return result;
}
public async Task<DateTime> GetServiceTimeAsync()
{
return await jdInterface.GetServiceTimeAsync();
}
}

View File

@@ -0,0 +1,21 @@
using Web.Services.Dto;
namespace Web.Stores;
public class ConfigStore
{
public void Init(CookieDto[] cookies, StoreDto[] stores)
{
Cookies = cookies;
Stores = stores;
}
public CookieDto[] Cookies { get; private set; } = null!;
public StoreDto[] Stores { get; private set; } = null!;
}
public static class JdTime
{
public static DateTime ServiceTime { get; set; }
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,8 @@
namespace Web.Utils;
public static class Constants
{
public const string JdMobileApiClient = "JdMobileApi";
public const string H5StClient = "Default";
}

View File

@@ -0,0 +1,25 @@
namespace Web.Utils;
public static class RandomUtil
{
public static string GetUserAgent()
{
var random = new Random();
return Ua[random.Next(0, Ua.Length)];
}
private static readonly string[] Ua =
[
"Mozilla/5.0 (Linux; Android 7.1.1; OPPO R9sk) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.111 Mobile Safari/537.36",
"Mozilla/5.0 (Android 7.1.1; Mobile; rv:68.0) Gecko/68.0 Firefox/68.0",
"Mozilla/5.0 (Linux; Android 7.1.1; OPPO R9sk Build/NMF26F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Mobile Safari/537.36 OPR/53.0.2569.141117",
"Mozilla/5.0 (Linux; Android 7.1.1; OPPO R9sk) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.90 Mobile Safari/537.36 EdgA/42.0.2.3819",
"Mozilla/5.0 (Linux; U; Android 7.1.1; zh-cn; OPPO R9sk Build/NMF26F) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/66.0.3359.126 MQQBrowser/9.6 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; U; Android 7.1.1; zh-cn; OPPO R9sk Build/NMF26F) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/70.0.3538.80 Mobile Safari/537.36 OppoBrowser/10.5.1.2",
"Mozilla/5.0 (Linux; Android 7.1.1; OPPO R9sk Build/NMF26F; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.97 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; U; Android 7.1.1; zh-CN; OPPO R9sk Build/NMF26F) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 UCBrowser/12.6.0.1040 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1.1; OPPO R9sk Build/NMF26F; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/70.0.3538.80 Mobile Safari/537.36 LieBaoFast/5.12.3",
"Mozilla/5.0 (Linux; Android 7.1.1; OPPO R9sk Build/NMF26F; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/48.0.2564.116 Mobile Safari/537.36 T7/9.1 baidubrowser/7.19.13.0 (Baidu; P1 7.1.1)",
"Mozilla/5.0 (Linux; Android 7.1.1; OPPO R9sk Build/NMF26F; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/70.0.3538.80 Mobile Safari/537.36 Mb2345Browser/11.0.1"
];
}

23
src/Web/Web.csproj Normal file
View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Cronos" Version="0.9.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
<ItemGroup>
<Content Update="wwwroot\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,15 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Interfaces": {
"H5StService": {
"Url": "http://suncheng.online:27483",
"UserName": "suncheng",
"Password": "SCsunch940622"
}
}
}

14
src/Web/appsettings.json Normal file
View File

@@ -0,0 +1,14 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Interfaces": {
"H5StService": {
"Url": "http://jd-h5st:3001"
}
}
}

38
src/Web/bodys.txt Normal file
View File

@@ -0,0 +1,38 @@
// 抢购成功
{
"code" : 0,
"data" : {
"batchId" : 1115819535,
"beginTime" : 1733328000000,
"couponId" : "386482391638",
"discount" : 80.0,
"endTime" : 1733673599000,
"haveShare" : 0,
"quota" : 1800.0,
"resultCode" : 999,
"resultMsg" : "领取成功!感谢您的参与,祝您购物愉快~"
},
"message" : "",
"subCode" : 0
}
// 已经参加
{
"code" : 0,
"data" : {
"batchId" : 0,
"haveShare" : 0,
"resultCode" : 15,
"resultMsg" : "您今天已经参加过此活动,别太贪心哟,明天再来~"
},
"message" : "",
"subCode" : 0
}
// 没抢到
{
"code":0,
"message": "此时排队领券的人太多,看下其他券吧~ ",
"subCode":1001
}

231
src/Web/cookie.txt Normal file
View File

@@ -0,0 +1,231 @@
__jdu=1709213308743173682908;
shshshfpa=6fd919ce-ef6b-4cf5-442b-f6f346022be5-1709213310;
shshshfpx=6fd919ce-ef6b-4cf5-442b-f6f346022be5-1709213310;
pinId=zJ-FB-D6TEE8yxkEQLuparV9-x-f3wj7;
areaId=12;
PCSYCityID=CN_320000_320500_0;
TrackID=10r-n2F5xViEUSOnR04BexXSyeOGK3naTAflfsipIG6uHxlfZCcVTTYWZeDMrOkEmkeamb-gjnO-jX8k9UIcbBNTmke2lJUE4_SWgMf59ZEUbWzXmzc-iVozzrPNvWIsr;
thor=9D682B1019126F2D724E8FD091A5EF32162B4EB323EAB93CACF8A59C3AEAD9FA73F3720539109EBAEC5A7264261AD898EECAFC5D483D744E2AC905E013DCA51F74651F71D794C5B5AA1BB7650B0A78B969D9EE04986E8DC7E08F0E23470FE12ACBDE35171931A2EF9C55244F6C5598709030B078BADB750C6E24A73F192B7CF73C2B326C7BDC0CF8DBADD65CB5D7CE03BA23AFB9F1694265584CAFF9CFF6E061;
light_key=AASBKE7rOxgWQziEhC_QY6yaboOsu7b0PHOrRox4zrMIAySnyjpARax46M_iFDcLjsUgKiu2;
pin=jd_6d0087dae2c25;
unick=jd_141521353;
_tp=nMhwGCgE1aEoheqAQUkNhfMX89BVigm1i5H5WRHna8M%3D;
_pst=jd_6d0087dae2c25;
user-key=0ad4a1d9-e64a-453b-a529-02c0fbabdd12;
autoOpenApp_downCloseDate_auto=1732959177591_1800000;
unpl=JF8EALNnNSttUUhWDUtSHRFDS1hTW10LSR9XPWVWXVpdHgYBEwJOFBh7XlVdWBRLFx9tZhRUW1NJVg4eBSsSEXtdU11UC3sRAGZmB1VUUE5kBRwBEhEgSF1kX20ITRYLbGcHVlxRTVEHGwQZFRlIVVVXbQl7FwtoVwVVXVhOUgceAh8aEk5cZG5aDUwRCmZiAWRcaEpkRHUFGBQQShBUWFwASBcBbWYMUlhaS1IHHAsYGhFCbVVuXg;
warehistory=\"10114734159340,10114734159340,10114734159340,10114734159340,10114734159340,10114734159340,10114734159340,10114734159340,10114734159340,10114734159340,\";
autoOpenApp_downCloseDate_autoOpenApp_autoPromptly=1732959228129_1;
__jdv=94967808%7Clianmeng__9__cps.youmai.com%7Ct_16282_728030894%7Cjingfen%7C8239aa72b14641239ec3b864dc591d79%7C1732959228147;
mt_xid=V2_52007VwMUU1VbUlgdSBBaAGUDFFBaUVFSHkApVVdlAUEHXAtOUxocGUAAYlNGTg0NAFkDHUkIUjMAGwVZWlEPL0oYXwB7AxJOX1lDWhZCGFoOYwMiUG1YYlgZQRlUBmMHF1BaaFJZHEs%3D;
jcap_dvzw_fp=EEm3fT6O6awEo2MZdqZ6zWKR4WtOPci4YxtLs4LIQ_9qhI-o8kJwl7HTZ1Jj7LK5a6TTZjiRzpvVwhMrv2O3UA==;
whwswswws=;
autoOpenApp_downCloseDate_jd_independent_coupon_openapp=1733211527214_1;
mba_muid=1709213308743173682908;
retina=1;
cid=9;
webp=1;
visitkey=7582830784651254661;
sc_width=414;
equipmentId=4F7K566PLIEHJ6JCQTSBN3R7HRLZ674CRV4RJ3X2S4CTG42J6QNRCT4X2WLNSPVZ6C3FOTLUOVUOAGERKLBMYB6W3U;
fingerprint=12d463d3ff8c36dc514d71d277f625c7;
deviceVersion=604.1;
deviceOS=ios;
deviceOSVersion=16.6;
deviceName=Safari;
TrackerID=WAc5MJ1h-A_0O0LYZ1ERUblkDxA2aCcD-WXn3XrXB24lU7vXBIaQ4xJivUziSovNP_UNpNOc91m9YT5xBTQLzS-DKhKcDb0qdCdHrLqBdpR999rZSrWl1bl6mIT8ZnQieTPqG3LtBebYbrRqgY6Rww;
pt_key=AAJnTwjvADCjgPbemDeePe_5nPlawvkb2owY5ewTkaDWVPObyRFHbVyiuq8j5jiiNWPgGWPyZoE;
pt_pin=jd_6d0087dae2c25;
pt_token=00xa2lv9;
pwdt_id=jd_6d0087dae2c25;
sfstoken=tk01m930a1bdda8sMSszeDF4MXgx+IjD/IqCwYZm+0++aU5ob6jY0AmLq6I/YxoFyZwaQ+FvLlKGoNpMIQp+osSgfhAN;
cn=10;
ipLoc-djd=12-988-40034-58081.4001065249;
ipLocation=%u6c5f%u82cf;
3AB9D23F7A4B3C9B=4F7K566PLIEHJ6JCQTSBN3R7HRLZ674CRV4RJ3X2S4CTG42J6QNRCT4X2WLNSPVZ6C3FOTLUOVUOAGERKLBMYB6W3U;
cartNum=10;
kplTitleShow=1;
e_wq_addr=DNU1CJuyENC4DIU3GzPpDzTpCtqnEV8mTJdNTXU1CzO3TXU0HUPNXyV1DtcnHMV1EJYzCyV1DJCzGV8vdJHPCNuvdJczGUYvdJUyCzKvdJU2HOSvdJczGUYvdJHPDOSvdJu1HtHpTJdNTXU1CzO3TXU0HUPNTXU2DzPOTXU5DtCzTXU1CzDLTXU0HJK5TXU3C0PQTXU1CtCmTXU1DuHMTXU3C0PQTXU0HJHMTXU5DUY0TJdNCJO2BtG0DMUyGzC5BtuyCJu=;
wq_addr=4551928385%7C1_72_2819_0%7C%u5317%u4EAC_%u671D%u9633%u533A_%u4E09%u73AF%u5230%u56DB%u73AF%u4E4B%u95F4_%7C%u5317%u4EAC%u671D%u9633%u533A%u4E09%u73AF%u5230%u56DB%u73AF%u4E4B%u95F4%7C116.444%2C39.9219;
jdAddrId=1_72_2819_0;
jdAddrName=%u5317%u4EAC_%u671D%u9633%u533A_%u4E09%u73AF%u5230%u56DB%u73AF%u4E4B%u95F4_;
commonAddress=4551928385;
regionAddress=1%2C72%2C2819%2C0;
mitemAddrId=1_72_2819_0;
mitemAddrName=%u5317%u4EAC%u671D%u9633%u533A%u4E09%u73AF%u5230%u56DB%u73AF%u4E4B%u95F4;
flash=3_x7oFJSmkwddMrpaKudB7np2k_f8HI1c7i8TmD-vmy2aMnqEGyZhQB_j5nK3okn3GGRxYBUHc3TKEWMmjHp5E1hNUL-cIiyCrurW_TB0-vL3kE_sVwd-nYnu7ujl_OaEngkkrzFkT210G0NFSdYqYqgzutNmV8zSRTUnb25PR_IXDnvWhzvvu3e**;
RT=\"z=1&dm=jd.com&si=n459aq77yu&ss=m48i0fff&sl=1&tt=0&nu=d839338b6dedbc6f545971c201379804&cl=5bqn&obo=1&ld=2d0qp&r=995bb8ebcd478c9cd03c2057729821fc&ul=2d0qq&hd=2d0r3\";
wxa_level=1;
jxsid=17335356307253755643;
__jda=23334881.1709213308743173682908.1709213309.1733236091.1733535630.39;
__jdc=23334881;
cd_eid=jdd034F7K566PLIEHJ6JCQTSBN3R7HRLZ674CRV4RJ3X2S4CTG42J6QNRCT4X2WLNSPVZ6C3FOTLUOVUOAGERKLBMYB6W3UAAAAMTRTV6Z4AAAAAADD77R2WGNBL4UQX;
3AB9D23F7A4B3CSS=jdd034F7K566PLIEHJ6JCQTSBN3R7HRLZ674CRV4RJ3X2S4CTG42J6QNRCT4X2WLNSPVZ6C3FOTLUOVUOAGERKLBMYB6W3UAAAAMTT3DITDYAAAAADFOAFVRLZE33BUX;
_gia_d=1;
autoOpenApp_downCloseDate_jd_homePage=1733535631078_1;
appCode=ms0ca95114;
mba_sid=17335356307383218012726934471.3;
__wga=1733535656553.1733535656553.1733236091722.1733232870573.1.3;
PPRD_P=UUID.1709213308743173682908;
share_cpin=;
share_open_id=;
share_gpin=;
shareChannel=;
source_module=;
erp=;
jxsid_s_t=1733535656577;
jxsid_s_u=https%3A//my.m.jd.com/;
shshshfpb=BApXST3nOnfZAL7feHSyiQfbin4T9xJo_BktgND9-9xJ1ItZfQtDSwkqz2y_7NtRwIeBEUCGCsg;
__jd_ref_cls=W_jdgwxcx_MyJD_orderSelect;
wqmnx1=MDEyNjM2MXQvLmNkZHRzc2F4NzYzM28wMWU9dDUxY255ZG9lMTI0NHo1UCBQTzYgU3BiMDVNa2tyMW8xIGkxMTAyWWEtNDFSUyMhKQ%3D%3D;
__jdb=23334881.4.1709213308743173682908|39.1733535630
// ----------------------------------------------------------------------------
_gia_d=1;
__jdb=122270672.3.17331957831321736669308|1.1733195783;
mba_sid=17331957831337422035107284255.4;
wqmnx1=MDEyNjM2MnRtbzAxNE1hKGUgZTdsYVhsaS4oLCApaS5vMSBpMUYybi0zUVVPKiZI;
cd_eid=jdd03QLAOQ67L3E7KBN7R6GYSQRWNO6U5CLCF6EAGSM6KLPLMURRBARVI5CEJAWSTGATVYMLHMTCORINFRBHLGCKZCCP34AAAAAMTRKCN5NQAAAAACPKMZRNGOF5S2QX;
jxsid=17331957830637784125;
wxa_level=1;
sfstoken=tk01m7d111b5fa8sM3gxeDJ4M1A5aw6+5dYWZO6pM+wgqeX3KSXlR2nGFl4TKtaS8Ay3oaMNTtWHHvYgwbA62XuCcGEe;
__jdv=122270672%7Cdirect%7C-%7Cnone%7C-%7C1733195783133;
cid=9;
retina=1;
__jda=122270672.17331957831321736669308.1733195783.1733195783.1733195783.1;
mba_muid=17331957831321736669308;
3AB9D23F7A4B3C9B=QLAOQ67L3E7KBN7R6GYSQRWNO6U5CLCF6EAGSM6KLPLMURRBARVI5CEJAWSTGATVYMLHMTCORINFRBHLGCKZCCP34A;
3AB9D23F7A4B3CSS=jdd03QLAOQ67L3E7KBN7R6GYSQRWNO6U5CLCF6EAGSM6KLPLMURRBARVI5CEJAWSTGATVYMLHMTCORINFRBHLGCKZCCP34AAAAAMTRKCN5NQAAAAACPKMZRNGOF5S2QX;
pt_key=AAJnTngiADC9gRJIni4QlYu5AhTp_yblJ0zzsWhvL9Fs-C18ewqvsv90AWt3bG1aEBdq2f5iJ7M;
pt_pin=jd_ipVkJufWWBjn;
pt_token=ls6t9uox;
pwdt_id=jd_ipVkJufWWBjn;
whwswswws=;
visitkey=8045458191883437915;
webp=1;
shshshfpx=0113eee0-e0a4-d9a1-911c-99306b9eadbe-1733195788;
shshshfpa=0113eee0-e0a4-d9a1-911c-99306b9eadbe-1733195788;
shshshfpb=BApXSfp-NifZA5UDwEuM3FSPV8EfQWBxkBnFYUKdo9xJ1Mq5DU4G2;
jcap_dvzw_fp=lyjmfVqUtx8NoyHMAbwdq_SStYg0E8KDF8QgHOn9DdUMxDpNeukWgWVNUEXi59IH2tOZtyxThZstl9AiAMx0Sw==;
TrackerID=3fmCY7KTwSPR8pBoEZsL3jAUQj6uN85TuSopYjbkV6UYVHl8HjznPi4tNDLYA0tzjfgx8iW8m4leiRjSiIEmI6q8yJAAK6fV_KKA4KVC3UDs3nCecsqxwsiL2B8D4QXS4Vn3KODr9gZogbbf0gzL_g;
appCode=ms0ca95114;
__jdc=122270672;
autoOpenApp_downCloseDate_jd_homePage=1733195811084_1;
__jd_ref_cls=MDownLoadFloat_OpenAppSchema;