确定友商登陆逻辑

This commit is contained in:
zhl 2021-06-21 20:53:44 +08:00
parent 17bc00a42f
commit a2de599f81
10 changed files with 333 additions and 4 deletions

79
doc/partner.md Normal file
View File

@ -0,0 +1,79 @@
# 金蚕游戏管理后台相关说明
## 修改记录
## 说明
1. 所有请求参数中带*号的不能为空
2. 如无特殊说明, 所有接口返回json, 顶级结构如下, 接口Response的数据结构说明只包含data部分
``` JSON
{
"code": 0, //0:成功 2: 缺少必要参数(accountid, sessionid) 4: 帐号被封, 5: 帐号未找到 100: 所有未定义的错误
"msg": "", //错误描述, 一般在code=0时, 该字段为空
"data": {}, // 数据
}
```
2. 页面列表
> 所有的页面均可单独调用, url: https://puzzle-admin.kingsome.cn/页面url?token=token&mini=1
> token为 1号接口获取
| 页面名称 | 页面url |
| ---------- | ----------------- |
| 店铺管理员 | /#/shop/shopadmin |
| 游戏设置 | /#/shop/setting |
| 活动列表 | /#/shop/activity_list |
| 挑战活动列表 | /#/shop/exam_list |
| 抽奖转盘 | /#/shop/lottery_setting |
| 分享设置 | /#/shop/share_setting |
| 优惠券设置 | /#/marketing/coupon |
| 题库设置 | /#/question/setting |
| 自定义题库 | /#/question/shoppuzzles |
| 邮件 | 暂不开放 |
| 公告 | 暂不开放 |
## 接口列表
### 1. 获取token
1. Method: POST
2. URI: /api/partner/login
> POST参数
| 字段 | 说明 |
| -------- | -------------------------------------- |
| name | *店铺完整名称 |
| sname | 店铺显示的短名称(如果该字段为空的话, 取name字段) |
| sid | *唯一的店铺id, 接口根据此字段判断该次上报是否是同一家店铺 |
| logo | 店铺的logo, 一个可以外网访问的url地址 |
| timestamp | *10或13位均可 |
| sign | *签名 |
> 签名字段说明:
>
> 取name,sid, timestamp和我们提供的SecretKey字段拼接成 name=店铺名称&sid=店铺id:timestamp:SecretKey, 取该字符串的sha1
```js
let signStr = `name=${name}&sid=${sid}:${timestamp}:${secretKey}`
let sha1sum = crypto.createHash('sha1')
sha1sum.update(signStr)
let sign = sha1sum.digest('hex')
```
3. Response: JSON
```js
{
token: '1231231aasa'
}
```

View File

@ -2,11 +2,12 @@ import BaseController from '../../common/base.controller'
import { permission, role, router } from '../../decorators/router'
import { ZError } from '../../common/ZError'
import { Game } from '../../models/content/Game'
import { generateQrFile } from '../../services/File'
import { downloadRemoteFile, generateQrFile } from '../../services/File'
import { getInviteeNum } from '../../services/JCFW'
import { UserItem } from '../../models/user/UserItem'
import { checkText } from '../../services/Baidu'
import { msgSecCheck, refreshToken } from '../../services/Wechat'
import { uploadToCDN } from '../../services/TencentCDN'
class GameController extends BaseController {
@role('anon')
@ -27,8 +28,12 @@ class GameController extends BaseController {
// // return { token }
// const { data } = await msgSecCheck(txt)
// return { baidu: res.data, wx: data }
return {}
let { url } = req.params
let path = (await downloadRemoteFile(url)) as string
let { data } = await uploadToCDN(path, 'test')
return data
}
@permission(['game:read', 'shop:game_setting'])
@router('post /api/games')
async list(req, res) {

View File

@ -0,0 +1,70 @@
import BaseController from '../../common/base.controller'
import { router } from '../../decorators/router'
import { customAlphabet } from 'nanoid'
import { ZError } from '../../common/ZError'
import { checkSign, createSign } from '../../utils/security.util'
import { Shop } from '../../models/shop/Shop'
import { Admin } from '../../models/admin/Admin'
import { Game } from '../../models/content/Game'
import { downloadRemoteFile } from '../../services/File'
import { uploadToCDN } from '../../services/TencentCDN'
const SECRET_KEY = '37284c327e10d8b73cf4325f33a3de4b34032e3e'
const passwd = customAlphabet('2345678abcdefghjkmnpqrstwxy', 10)
class PartnerController extends BaseController {
/**
*
*
* logo?
* @param req
* @param res
* @return {Promise<{}>}
*/
@router('post /api/partner/login')
async login(req: any, res: any) {
let { name, sname, sid, logo, timestamp, sign } = req.params
if (!name || !sid || !timestamp || !sign) {
throw new ZError(10, '缺少必要参数')
}
const signKeys = ['name', 'sid']
if (!checkSign({ secretKey: SECRET_KEY, data: req.params, timestamp, sign, signKeys })) {
throw new ZError(21, 'sign error')
}
sname = sname || name
const shopData = { name, showName: sname, source: 1, publish: true }
const shop = await Shop.insertOrUpdate({ partnerId: sid }, shopData)
// 保存一下, 以生成sid和numid
if (shop.newRecord) {
let game = await Game.findAvaOne()
if (game) {
shop.gameInfo = { gameid: game.id, versionid: game.versions[0]._id + '' }
}
}
if (shop.logoPartner !== logo) {
shop.logoPartner = logo
setImmediate(async () => {
let path = (await downloadRemoteFile(logo)) as string
let { data } = await uploadToCDN(path, 'shop')
console.log(`upload logo finished`, data)
if (!data.errcode) {
shop.logo = data.url_cdn
await shop.save()
account.avatar = data.url_cdn
await account.save()
}
})
}
await shop.save()
const username = sid
let accountData = { level: 9, roles: ['shopadmin'], department: shop.id, showname: sname }
const account = await Admin.insertOrUpdate({ username }, accountData)
if (account.newRecord) {
account.updatePassword(shop.sid)
await account.save()
}
const token = await res.jwtSign({ id: account.id })
return { token }
}
}

View File

@ -1,4 +1,4 @@
import { getModelForClass, index, modelOptions, prop, ReturnModelType } from '@typegoose/typegoose'
import { getModelForClass, index, modelOptions, pre, prop, ReturnModelType } from '@typegoose/typegoose'
import { dbconn } from 'decorators/dbconn'
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses'
@ -35,6 +35,9 @@ export interface AdminClass extends Base, TimeStamps {}
@dbconn()
@index({ username: 1 }, { unique: true })
@modelOptions({ schemaOptions: { collection: 'admin_user', timestamps: true } })
@pre<AdminClass>('save', function () {
this.newRecord = false
})
// @ts-ignore
class AdminClass extends BaseModule {
@prop({ maxlength: 20, minlength: 5, required: true })
@ -86,6 +89,12 @@ class AdminClass extends BaseModule {
*/
@prop()
public level: number
/**
* , insertOrUpdate返回的记录是否为新纪录
* @type {boolean}
*/
@prop({ default: true })
public newRecord: boolean
public static async findByName(this: ReturnModelType<typeof AdminClass>, username) {
return this.findOne({ username, deleted: false }).exec()

View File

@ -20,8 +20,10 @@ class GameInfo {
@dbconn()
@index({ location: '2dsphere' })
@index({ sid: 1 }, { unique: true })
@index({ partnerId: 1 }, { unique: false })
@modelOptions({ schemaOptions: { collection: 'shop', timestamps: true } })
@pre<ShopClass>('save', async function () {
this.newRecord = false
if (!this.sid) {
let sid = ''
while (!sid) {
@ -46,6 +48,13 @@ class ShopClass extends BaseModule {
@prop()
public numid: number
/**
* id
* @type {string}
*/
@prop()
public partnerId: string
@prop({ required: true })
public name!: string
/**
@ -128,6 +137,18 @@ class ShopClass extends BaseModule {
*/
@prop()
public logo: string
/**
* logo原始信息
* @type {string}
*/
@prop()
public logoPartner: string
/**
*
* @type {number} 0: 后台添加, 1: 合作伙伴接口添加
*/
@prop({ default: 0 })
public source: number
/**
*
* @type {string}
@ -153,6 +174,13 @@ class ShopClass extends BaseModule {
@prop({ type: () => [String] })
public qtypes: string[]
/**
* , insertOrUpdate返回的记录是否为新纪录
* @type {boolean}
*/
@prop({ default: true })
public newRecord: boolean
public static parseQueryParam(params) {
let options: any = {
opt: { deleted: false, show: true, publish: true },

View File

@ -3,7 +3,13 @@ import * as jetpack from 'fs-jetpack'
import { Game } from '../models/content/Game'
import { ZError } from '../common/ZError'
import { generateQr } from './Wechat'
import nodeHtmlToImage, { NodeHtmlToImageOptions } from 'node-html-to-image'
import fs from 'fs'
import os from 'os'
import nodeHtmlToImage from 'node-html-to-image'
import axios from 'axios'
import * as http from 'http'
import { customAlphabet } from 'nanoid'
const nanoid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 26)
export function generateUploadPath(subPath: string) {
const base = config.file.upload_location
@ -110,3 +116,28 @@ export function getCouponUrl(shop: string, couponId: string) {
let subPath = `/coupon/${shop}`
return `${config.file.show_url}${subPath}/${couponId}.png`
}
export async function downloadRemoteFile(url: string) {
const tempPath = os.tmpdir()
const fileName = nanoid()
const dest: string = `${tempPath}/${fileName}.png`
if (jetpack.exists(dest)) {
jetpack.remove(dest)
}
return new Promise((resolve, reject) => {
axios({
url,
responseType: 'stream',
}).then(resp => {
const writer = fs.createWriteStream(dest)
resp.data.pipe(writer)
writer.on('finish', () => {
console.log('finish')
resolve && resolve(dest)
})
writer.on('error', err => {
reject && reject(err)
})
})
})
}

View File

@ -0,0 +1,21 @@
import axios from 'axios'
import fs from 'fs'
import FormData from 'form-data'
export function uploadToCDN(localPath: string, subPath: string) {
let data = new FormData()
data.append('sub_path', subPath)
data.append('type', 'image')
data.append('image-file', fs.createReadStream(localPath))
let config: any = {
method: 'post',
url: 'https://opm.kingsome.cn/api/upload',
headers: {
...data.getHeaders(),
},
data: data,
}
return axios(config)
}

20
src/utils/pinyin.ts Normal file

File diff suppressed because one or more lines are too long

View File

@ -36,3 +36,33 @@ export function md5(str) {
str = md5sum.digest('hex')
return str
}
export function createSign(secretKey, paramStr, timestamp) {
paramStr = `${paramStr}:${timestamp}:${secretKey}`
return sha1(paramStr)
}
export function checkSign({
secretKey,
data,
timestamp,
sign,
signKeys,
}: {
secretKey: string
data: {}
timestamp: string
sign: string
signKeys: string[]
}) {
signKeys.sort()
let signStr = ''
for (let key of signKeys) {
if (signStr.length > 0) {
signStr += '&'
}
signStr += `${key}=${data[key]}`
}
let sign1 = createSign(secretKey, signStr, timestamp)
return sign1 === sign
}

View File

@ -68,3 +68,39 @@ export function isObjectId(id: string): boolean {
//mongoose.Types.ObjectId.isValid(id)
return /^[a-fA-F0-9]{24}$/.test(id)
}
/**
* 10 -> 62
* @param {string | number} number
* @return {string}
*/
export function string10to62(number: string | number) {
const chars = '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ'.split('')
const radix = chars.length
let qutient = +number
const arr = []
do {
const mod = qutient % radix
qutient = (qutient - mod) / radix
arr.unshift(chars[mod])
} while (qutient)
return arr.join('')
}
/**
* 62 -> 10
* @param {string} numberCode
* @return {number}
*/
export function string62to10(numberCode: string) {
const chars = '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ'
const radix = chars.length
numberCode = numberCode + ''
const len = numberCode.length
let i = 0
let originNumber = 0
while (i < len) {
originNumber += Math.pow(radix, i++) * (chars.indexOf(numberCode.charAt(len - i)) || 0)
}
return originNumber
}